
: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;}
/* Enhanced mode (a real <iframe> or a real <video> in place of a static
   screenshot on at least one side -- see render_compare_widget_html's
   before_type/after_type, decided independently per side): none of these
   content types expose a real content height here the way a plain <img>
   does -- an iframe's "before" side is typically a different origin (the
   live external site), which browsers block reading scrollHeight from
   regardless of X-Frame-Options, and a <video> has no comparable "how
   tall would this be as a page" concept at all, so the Math.max(before,
   after) natural-image-height trick above can't apply to this mode,
   even for an after side that happens to be same-origin. Every side
   instead fills the existing fixed 16:9 .compare-frame box completely and
   plays/scrolls internally on its 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, and a <video> was never a scrolling
   participant to begin with. */
.compare-frame.is-enhanced .compare{overflow:hidden;touch-action:auto;}
.compare-frame.is-enhanced .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-enhanced .compare-inner > iframe,
.compare-frame.is-enhanced .compare-inner > video,
.compare-frame.is-enhanced .after-layer iframe{position:absolute;inset:0;top:0;left:0;width:100%;height:100%;border:0;background:#fff;pointer-events:none;overflow:hidden;}
/* object-fit/object-position are no-ops on iframe (not a replaced element
   with an intrinsic ratio the way <video> is) but required for a real
   <video> before-side: without them the video distorts to the box's own
   aspect ratio instead of cropping like every other real screenshot/frame
   in this widget, and without object-position:top specifically the crop
   would center vertically instead -- a real source taller/wider than this
   fixed 16:9 box, cropped to its vertical middle, would contradict this
   widget's own "top of page only" promise (the caption text right below
   it, and the plain-screenshot mode's own top-anchored real content). */
.compare-frame.is-enhanced .compare-inner > video{object-fit:cover;object-position:top;}
/* 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{margin:20px 0 0;text-align:center;}
/* 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;} }
