
:root{
  /* --accent is the loomloom brand green (#00fdbc) used AS TEXT directly on
     --bg -- tuned per theme for AA contrast, the same way --red used to be
     (light gets a darkened #007A5C shade: pure #00fdbc as text on the near-
     white light background is ~1.2:1, nowhere near readable; dark keeps the
     literal brand color, since it's ~11:1 against the near-black dark bg).
     --accent-fill/--on-accent-fill are a SEPARATE pair for solid swatches
     (badge, compare-slider handle): always the literal, undimmed brand
     green with dark ink on top, constant across both themes -- confirmed
     white text on the pure green is ~1.3:1 (fails badly), so those spots
     never use plain white regardless of which theme is active. */
  --bg:#F4F4F0; --surface:#FFFFFF; --ink:#0B0B0B; --muted:#4A4A46;
  --faint:#68675E; --accent:#007A5C; --accent-fill:#00FDBC; --on-accent-fill:#062017;
  --rule:#D8D6CE; --rule-strong:#0B0B0B;
  --shadow: 0 1px 3px rgba(11,11,11,0.08);
}
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --bg:#15150F; --surface:#1D1D16; --ink:#F2F1EA; --muted:#B3B0A5;
    --faint:#726F63; --accent:#00FDBC; --accent-fill:#00FDBC; --on-accent-fill:#062017;
    --rule:#33322A; --rule-strong:#F2F1EA;
    --shadow: 0 1px 3px rgba(0,0,0,0.4);
  }
}
:root[data-theme="dark"]{
  --bg:#15150F; --surface:#1D1D16; --ink:#F2F1EA; --muted:#B3B0A5;
  --faint:#726F63; --accent:#00FDBC; --accent-fill:#00FDBC; --on-accent-fill:#062017;
  --rule:#33322A; --rule-strong:#F2F1EA;
  --shadow: 0 1px 3px rgba(0,0,0,0.4);
}
*{box-sizing:border-box;}
html{-webkit-text-size-adjust:100%;}
body{margin:0;background:var(--bg);color:var(--ink);font-family:"Source Serif 4",Georgia,serif;font-size:18px;line-height:1.6;overflow-x:hidden;}
.wrap{max-width:1280px;margin:0 auto;padding:0 24px;}
/* 1280px (revised down from an earlier 1920px try): 1920 across the whole
   page read as too extreme once actually seen rendered -- narrow ~800px
   prose blocks sitting inside a 1920px page left huge, unbalanced empty
   margins next to them. 1280 keeps the same "whole page scales together,
   not just the compare widget" idea (a human specifically wanted the
   Before/After section to keep its aspect-ratio:16/9 box at a size usable
   for real screen recordings, and the rest of the page to match rather
   than sit narrow beside one wide section), while landing at a page/prose
   ratio (1280 vs the ~800px reading-width blocks below) that actually
   reads as a deliberate, balanced layout instead of a wide page with an
   accidentally-narrow column floating in it. Flowing prose
   (.chapter-narrative, .loomloom-note, .request-block, and everything
   under the shared 800px cap below) still gets its own narrower max-width
   for real readability; non-text elements (headings, the compare widget,
   stat/fact grids, swatches) use the full column. */
a{color:var(--accent);}
.topbar{display:flex;align-items:center;justify-content:space-between;padding:20px 0;border-bottom:2px solid var(--rule-strong);gap:16px;}
.topbar .left{display:flex;align-items:center;gap:12px;min-width:0;}
.topbar img{height:18px;flex-shrink:0;}
:root:not([data-theme="light"]) .topbar img{filter:invert(1);}
:root[data-theme="dark"] .topbar img{filter:invert(1);}
.topbar .tag{font-family:"IBM Plex Mono",monospace;font-size:11px;letter-spacing:0.1em;text-transform:uppercase;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.copy-page-btn{font-family:"IBM Plex Mono",monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;background:none;border:1px solid var(--rule-strong);color:var(--ink);padding:7px 14px;cursor:pointer;flex-shrink:0;}
.copy-page-btn:hover{background:var(--ink);color:var(--bg);}
.hero{padding:72px 0 56px;border-bottom:2px solid var(--rule-strong);text-align:center;}
.hero-eyebrow{font-family:"IBM Plex Mono",monospace;font-size:12px;letter-spacing:0.18em;text-transform:uppercase;color:var(--accent);margin-bottom:22px;}
.badge{display:inline-block;font-family:"IBM Plex Mono",monospace;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;background:var(--accent-fill);color:var(--on-accent-fill);padding:5px 12px;margin-bottom:18px;}
.hero .subject{font-family:Arial,"Helvetica Neue",sans-serif;font-weight:900;font-size:clamp(2rem,6vw,3.2rem);letter-spacing:-0.02em;line-height:1.02;text-transform:uppercase;margin:0 0 32px;text-wrap:balance;}
.hero-shift{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:28px;}
.hero-shift .label{font-family:"IBM Plex Mono",monospace;font-size:14px;letter-spacing:0.02em;color:var(--muted);padding:8px 18px;border:1px solid var(--rule);}
.hero-shift .label.after{color:var(--ink);border-color:var(--rule-strong);font-weight:700;}
.hero-shift .arrow{font-size:20px;color:var(--accent);line-height:1;}
.hero .tagline{font-family:"IBM Plex Mono",monospace;font-size:12px;letter-spacing:0.1em;text-transform:uppercase;color:var(--faint);margin:0 0 24px;}
.swatches{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;}
.swatch{width:28px;height:28px;border:1px solid var(--rule-strong);}
section{padding:64px 0;border-bottom:2px solid var(--rule);}
section:last-of-type{border-bottom:none;}
.section-label{font-family:"IBM Plex Mono",monospace;font-size:11px;letter-spacing:0.12em;text-transform:uppercase;color:var(--accent);display:block;margin-bottom:10px;font-weight:700;}
h2{font-family:Arial,"Helvetica Neue",sans-serif;font-weight:900;font-size:1.9rem;letter-spacing:-0.01em;text-transform:uppercase;margin:0 0 28px;}
.compare-frame{position:relative;width:100%;aspect-ratio:16/9;border:2px solid var(--rule-strong);overflow:hidden;user-select:none;}
.compare{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;touch-action:pan-y;}
.compare-inner{position:relative;width:100%;}
.compare img{width:100%;height:auto;display:block;}
/* Reveal is done with clip-path, not width. width is a layout-triggering
   CSS property -- animating it forces the browser to recompute layout for
   this box on every single drag frame, not just repaint it, and this box
   contains a whole live iframe (one of them with an actively-decoding
   <video> inside). Confirmed the real cause of visible drag stutter on a
   real deployed page: clip-path is compositor-only (GPU, no layout/
   reflow), the standard technique for exactly this before/after slider
   pattern. The layer itself is now always the full widget width/height --
   both images and both iframes can just be plain width:100% (matching
   each other automatically), no more JS-computed pixel width needed for
   either mode; see layout() below, much shorter now. */
.compare .after-layer{position:absolute;inset:0;width:100%;height:100%;overflow:hidden;clip-path:inset(0 0 0 50%);will-change:clip-path;}
.compare .after-layer img{width:100%;height:auto;display:block;}
.compare .divider{position:absolute;top:0;bottom:0;left:50%;width:2px;background:var(--accent-fill);pointer-events:none;}
/* Live-embed mode (real <iframe> in place of a static screenshot): neither
   side's real content height is readable here -- the "before" iframe is a
   different origin (the live external site), which browsers block reading
   scrollHeight from regardless of X-Frame-Options, so the Math.max(before,
   after) natural-image-height trick above can't apply to this mode at all,
   not even for an after side that happens to be same-origin. Both sides
   instead fill the existing fixed 16:9 .compare-frame box completely and
   scroll internally on their own -- this is a real, deliberate trade-off,
   not an oversight: the previous synced-scroll-together behavior needed
   one shared scrolling container with both images stacked in normal flow,
   which an embedded live page (with its own real DOM, scripts, and scroll
   position) can't be made to participate in from the parent page's JS. */
.compare-frame.is-live .compare{overflow:hidden;touch-action:auto;}
.compare-frame.is-live .compare-inner{height:100%;}
/* `.compare-inner > iframe` (direct-child combinator) only ever matched the
   BEFORE iframe -- the AFTER iframe lives one level deeper, inside
   .after-layer, so it never matched this rule at all and was rendering at
   the browser's bare default iframe size (~300x150px) inside a much taller
   container. A real, confirmed bug: the visible boundary at the bottom of
   that undersized box looked exactly like a stray horizontal scrollbar,
   and -- being a fixed default height, unrelated to the drag handle or the
   video's own playback -- it stayed put at every reveal position, which is
   what made it so easy to mistake for something else while debugging.
   Listing both selectors together closes the gap.
   pointer-events:none is permanent here now, not toggled -- this widget's
   two live iframes are frozen to the top of each real page on purpose (no
   scrolling, no clicking into either real site): every attempt at making
   them genuinely scrollable ran into the same real, confirmed wall --
   wheel-scroll over the cross-origin "before" iframe gets silently
   swallowed by the parent page's own scroll instead of scrolling the
   iframe, reproducible any time a cross-origin iframe sits inside a
   position:absolute ancestor on a scrollable page (isolated and confirmed
   with controlled tests, independent of clip-path/siblings/nesting depth
   -- a genuine Chromium cross-origin-iframe limitation, not a CSS mistake
   here). A custom-built scrollbar for the same-origin "after" side alone
   fixed only half the problem and added its own real interaction bugs.
   Freezing both to a static top-of-page comparison sidesteps all of it --
   the full, real, scrollable comparison lives in the second (screenshot-
   based, no video, no cross-origin routing problem) widget below, behind
   the "see the full page" toggle; see render_compare_widget_html and the
   .compare-full rule further down. */
.compare-frame.is-live .compare-inner > iframe,
.compare-frame.is-live .after-layer iframe{position:absolute;inset:0;top:0;left:0;width:100%;height:100%;border:0;background:#fff;pointer-events:none;overflow:hidden;}
/* width:100% is required here, not optional -- confirmed the hard way on
   the real deployed site: an <iframe> is a *replaced element* (same CSS
   category as <img>/<video>/<object>), and for an absolutely positioned
   replaced element, inset:0 alone does NOT stretch it to fill its
   container the way it would a plain <div> -- width:auto instead falls
   back to the browser's intrinsic default iframe size (300px). Without
   this rule the BEFORE iframe (which has no JS-set inline width to
   override that default, unlike the after iframe below) was stuck at
   exactly 300px, positioned at the widget's left edge -- entirely
   underneath whatever the after-layer's own reveal width was covering,
   so the actual visible "before" region showed nothing at all. The after
   iframe's JS-set inline style.width (below) still wins over this
   width:100% for that element specifically, since an inline style always
   beats a stylesheet rule regardless of specificity -- this line is safe
   for both, not just the one it happens to fix. */
/* No .after-layer width/height override needed for live mode anymore --
   .after-layer is unconditionally position:absolute;inset:0;width:100%;
   height:100% now (the base rule above), same in both screenshot and
   live-embed mode, with clip-path doing the reveal instead of a narrower
   box. That also means the after iframe's width:100% (the shared rule
   above) is now its real effective width, not just a fallback needing a
   JS override to matter -- previously JS had to force it to the full
   widget width specifically because .after-layer itself was narrower than
   that (whatever % was revealed); now .after-layer is always full width,
   so the iframe's own plain width:100% already matches the before
   iframe's, with nothing left for JS to override. */
.compare-frame .handle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:44px;height:44px;background:var(--accent-fill);display:flex;align-items:center;justify-content:center;cursor:ew-resize;box-shadow:var(--shadow);z-index:2;touch-action:none;}
.compare-frame .handle::before{content:"\2194";color:var(--on-accent-fill);font-size:18px;font-weight:900;}
.compare-frame .handle:focus-visible{outline:3px solid var(--ink);outline-offset:3px;}
.compare:focus-visible{outline:3px solid var(--ink);outline-offset:-3px;}
.compare-caption{display:flex;justify-content:space-between;font-family:"IBM Plex Mono",monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:var(--faint);margin-top:10px;}
.compare-full-toggle{text-align:center;margin:20px 0 0;}
/* The full-page comparison: real, static screenshots (no video, no
   cross-origin iframe), sized to the same 800px column as the rest of the
   article's prose instead of the 1280px full-bleed width the frozen live
   widget above uses -- it's a secondary, opt-in view, not the page's main
   visual anchor. Hidden by default ([hidden], a real attribute not just a
   class, so it's inert to layout/AT until toggled) and revealed by the
   button's click handler in script.js. */
.compare-full{max-width:800px;margin:20px auto 0;}
.compare-full[hidden]{display:none;}
.quick-hits{display:grid;gap:14px;max-width:800px;margin-left:auto;margin-right:auto;}
.quick-hit{display:flex;align-items:baseline;gap:14px;padding:14px 0;border-top:1px solid var(--rule);}
.quick-hit:first-child{border-top:none;padding-top:0;}
.quick-hit .num{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--accent);font-weight:700;flex-shrink:0;}
.quick-hit p{margin:0;font-size:17px;}
.chapter{padding:36px 0;border-top:1px solid var(--rule);max-width:800px;margin-left:auto;margin-right:auto;}
.chapter:first-of-type{border-top:none;padding-top:0;}
.chapter-head{display:flex;align-items:baseline;gap:14px;margin-bottom:16px;}
.chapter-num{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--accent);font-weight:700;}
.chapter h3{font-family:Arial,"Helvetica Neue",sans-serif;font-weight:800;font-size:1.25rem;margin:0;letter-spacing:-0.005em;}
.chapter-facts{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:18px;}
.fact{background:var(--surface);border:1px solid var(--rule);padding:14px 16px;}
.fact-label{display:block;font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--faint);margin-bottom:8px;}
.fact p{margin:0;font-size:13px;color:var(--muted);line-height:1.55;font-family:"IBM Plex Mono",monospace;word-break:break-word;}
.chapter-narrative{margin:0;font-size:17px;color:var(--ink);}
.chapter-share{display:flex;align-items:center;gap:10px;margin-top:16px;}
.share-label{font-family:"IBM Plex Mono",monospace;font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:var(--faint);}
.share-btn{font-family:"IBM Plex Mono",monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;background:none;border:1px solid var(--rule-strong);color:var(--ink);padding:6px 12px;cursor:pointer;}
.share-btn:hover{background:var(--ink);color:var(--bg);}
@media (max-width:640px){ .chapter-facts{grid-template-columns:1fr;} }
.request-block{background:var(--surface);border:1px solid var(--rule);padding:16px 20px;margin:0 auto 20px;max-width:800px;}
.request-block p{margin:6px 0 0;font-size:15px;font-style:italic;color:var(--ink);line-height:1.5;}
.pipeline{display:grid;gap:1px;background:var(--rule);border:1px solid var(--rule);margin:0 auto 28px;max-width:800px;}
.pipeline-stage{background:var(--surface);padding:18px 22px;display:grid;grid-template-columns:140px 1fr;gap:16px;align-items:baseline;}
.pipeline-stage .name{font-family:Arial,"Helvetica Neue",sans-serif;font-weight:800;font-size:14px;text-transform:uppercase;letter-spacing:0.03em;}
.pipeline-stage .desc{font-size:14px;color:var(--muted);margin:0;}
@media (max-width:640px){ .pipeline-stage{grid-template-columns:1fr;gap:4px;} }
.loomloom-note{background:var(--surface);border:1px solid var(--rule);padding:18px 20px;font-size:14px;color:var(--muted);max-width:720px;margin-left:auto;margin-right:auto;}
.validate-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;background:var(--rule);border:1px solid var(--rule);}
.validate-cell{background:var(--surface);padding:20px 22px;}
.validate-cell .check{color:#2f6e4e;font-weight:700;margin-right:8px;}
.validate-cell .metric{font-family:Arial,"Helvetica Neue",sans-serif;font-weight:900;font-size:1.4rem;}
.validate-cell .label{display:block;font-size:13px;color:var(--muted);margin-top:4px;}
@media (max-width:640px){ .validate-grid{grid-template-columns:1fr;} }
.repro-block{background:var(--surface);border:2px solid var(--rule-strong);padding:22px 24px;max-width:800px;margin-left:auto;margin-right:auto;}
.tool-list{list-style:none;margin:0;padding:0;max-width:800px;}
.tool-list li{padding:12px 0;border-bottom:1px solid var(--rule);}
.tool-list li:last-child{border-bottom:none;}
.tool-list .name{font-family:"IBM Plex Mono",monospace;font-size:13px;font-weight:700;}
.tool-list .role{display:block;font-size:14px;color:var(--muted);margin-top:4px;line-height:1.5;}
.compare-embed{display:flex;align-items:center;gap:10px;margin-top:14px;}
.live-note{font-weight:400;text-transform:none;letter-spacing:0;}
.note{font-size:13px;color:var(--faint);margin-top:18px;}
.final-cta{text-align:center;padding:56px 0;}
.final-cta h2{margin-bottom:24px;}
.final-cta .btn{display:inline-block;font-family:"IBM Plex Mono",monospace;font-size:13px;text-transform:uppercase;letter-spacing:0.06em;background:var(--ink);color:var(--bg);padding:14px 28px;text-decoration:none;font-weight:700;}
footer{padding:32px 0 48px;font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--faint);text-align:center;}
@media (prefers-reduced-motion: reduce){ *{transition:none !important;} }
