/* ============================================================================
   RED page shell — the chrome every slash.red page shares.
   EXTRACTED from index.html 2026-07-26 when /words became the second consumer.
   Until then the shell lived inline in index.html and there was nothing to
   drift against; a second page is what makes a shared copy worth its cost.

   RULE: this file is the ONLY copy of the shell. Do not paste chunks of it back
   into a page. If a page needs a variation, add a class here, not a fork there.
   The drift this prevents is documented and real: seven older drafts in this
   repo still ship --red:#e5322d (fails WCAG AA at small text; live is #d92b26)
   because they were made by copying.

   Page-specific CSS stays inline in each page's own <style>.
   ============================================================================ */
  :root{
    color-scheme:light dark;
    --red:#d92b26;   /* was #e5322d (4.35:1 on white, fails AA at small text); this passes at 4.86:1 */
    --red-solid:#d92b26;   /* CTA button fill (white text on top of it) — fixed in BOTH themes, never
                              swaps with --red below. That pairing is self-contained (white vs this
                              exact red) and never touches the page background, so it already clears
                              AA in dark mode too; --red itself has to brighten there instead, for the
                              many places it sits as plain text directly on the (now near-black) page. */
    --ink:#0a0a0a;
    --ink-solid:#0a0a0a;   /* Studio's CTA fill (2026-07-23, Antimo: "keep studio cta black") — fixed
                              in BOTH themes, same reasoning as --red-solid: white text on top of it
                              never touches the page bg, so it doesn't need to track --ink's dark-mode
                              swap to near-white. Gets a visible border in dark mode (see .c-cta.studio)
                              since a literal-black fill would otherwise nearly vanish against #121212. */
    --bg:#ffffff;
    --muted:#6b6b6b;
    --faint:#767676;   /* was #8a8a8a — darkened to pass WCAG AA (4.54:1) at the small sizes it's used */
    --hair:#e7e7e7;
    --hair-strong:#d7d7d7;
    --surface:#f4f4f5;
    --tint:rgba(0,0,0,0.05);
    --rail-w:33.33%;
    --pad:clamp(1.15rem,3vw,2.4rem);
    --pad-h:calc(var(--pad) / 2);   /* horizontal-only half pad (2026-07-22 layout pass, item 1) — vertical rhythm (--pad) is untouched */
    --main-pad-h:clamp(0.5rem,0.8vw,0.9rem);   /* main's own horizontal padding (2026-07-25) — shared with .rail-r's right-side padding so the embed→icon gap and icon→page-edge gap derive from one value. Tightened twice (Antimo, referencing Linear's tight edge-to-content margin as a direction, not a pixel target); second pass measured: was 17.3px, leaving the embed 42px off the icon. */
    /* collapsed icon-column width. Measured, not eyeballed (Antimo: "the space between right side of
       embed and sidebar icon is still too much... after you got better measurements"): the icon is
       only 0.92em (~14px), so at the old 3.5rem/56px the column held 42px of dead air to the icon's
       left while its right inset was 17px — the gap Antimo could see. Now icon + one --main-pad-h of
       breathing room, so the embed stops ~1 pad short of the icon instead of ~3. Also the collapsed
       LEFT rail's width, which is why .rail's own padding is overridden to --main-pad-h in the open
       state — its default --pad-h (19px) is wider than this whole column. */
    --edge-w:2.1rem;
    --panel-w:17rem;   /* fallback only — JS overwrites it with the measured width of the longest item (see sizePanel) so the panel is never wider than its content */
    /* the gutter between the two pricing cards, promoted from a literal inside .cards to a named
       token (2026-07-25) because Antimo tied a second measurement to it: "when the sidebar is
       expanded on desk the space between the left edge of the items and the visual edge of the embed
       should be the same as the horiz space between the two pricing cards". Naming it makes that a
       structural relationship rather than two numbers that happen to match — retune the gutter and
       the panel's inset follows. Maps 1:1 onto the engine store's --ck-layout-card-gap. */
    --card-gap:clamp(1rem,2vw,1.5rem);
    --mob-pad-h:clamp(0.5rem,2vw,0.75rem);   /* mobile horizontal page inset — was this literal clamp inline in two mobile rules (.rail, main); named so the mobile rail icon can share the exact same right inset instead of a third copy. Same computed value, no visual change. */
    --sp-pad-v:0.4rem;   /* .sp-item vertical padding — 0.4rem is not a fresh guess, it's exactly .feat's (the pricing-card line items) own padding, which Antimo named as the spacing reference. Item height now matches .feat's 37.1px measured. A var, not a literal, because .side-panel's alignment offset is derived from it. Same value on mobile: the spacing was called out as too loose on BOTH breakpoints, so there is no mobile override any more — the touch target is enlarged on .panel-toggle instead, which no longer affects item spacing. */
  }
  /* dark mode (2026-07-22) — follows the system setting, same as the Neynar embed already does
     (Antimo: "if i change mode, the neynar embed switches but my site doesn't"). --bg is a dark
     grey, NOT pure black (Antimo: "full black too much") — measured the embed's own dark bg live
     (strange-footprint-136085.framer.app renders #222 in dark mode) and picked something a step
     darker than that, so our page still reads as a deliberate grey the embed's card floats on top
     of, not a void. --red is recomputed, not just brightened arbitrarily: white text on --red-solid
     over the page and --red as text directly on that same page are two DIFFERENT contrast pairs
     (one via white, one via the page's own dark grey), so one red value can't satisfy both —
     --red-solid keeps the original (already-AA) value for the button-fill case, --red gets its own
     AA-checked value (~5.3:1 on #121212) for the text case. */
  @media (prefers-color-scheme:dark){
    :root{
      --red:#e75a55;
      --ink:#f2f2f2;
      --bg:#121212;
      --muted:#a3a3a3;
      --faint:#8a8a8a;
      --hair:#2c2c2c;
      --hair-strong:#404040;
      --surface:#1e1e1e;
      --tint:rgba(255,255,255,0.06);
    }
    .wslide{--tone:#1e1e1e !important;}   /* the inline style="--tone:#ececed" on the element outranks any non-!important stylesheet rule, so this needs !important to win */
  }
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
  /* one font, one weight, ONE SIZE, everywhere (Antimo, 2026-07-22 — extended from weight-only to
     size-only, no exceptions: the pull-quote, prices and tiny caps labels all now match body text
     exactly). Only color may differ. Headings default to BOTH bold and a larger size in every
     browser even with zero CSS — two hidden defaults, not one; font-size:inherit closes the second. */
  h1,h2,h3,h4,h5,h6{font-weight:400;font-size:inherit;}
  /* form controls (button/input/etc) default to the BROWSER's own UI font in every engine, not the
     page's — a second hidden default alongside headings' bold. Caught live: the email button (one
     of Antimo's two named reference elements) was silently rendering in Arial, not Inter. */
  button,input,select,textarea{font:inherit;color:inherit;}
  html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;}   /* reduced-motion override below forces auto */
  body{
    font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
    background:var(--bg);color:var(--ink);
    font-size:clamp(0.855rem,0.945vw,0.945rem);line-height:1.6;font-weight:400;letter-spacing:-0.003em;   /* -10% 2026-08-13 (Antimo): was clamp(0.95rem,1.05vw,1.05rem). Every stop scaled by 0.9 so the responsive curve keeps its shape rather than flattening. NOTE: red-site.design.md still records the OLD clamp as --ck-typography-size-body — that silo was minted FROM this file, so it is now stale and needs re-minting. */
    -webkit-font-smoothing:antialiased;
  }
  a{color:inherit;}
  .lnk{text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;}
  /* inline pfp before the @antimofm handle (2026-07-23) — sized/aligned to sit in the text flow like
     a single enlarged character, not a separate block. */
  .pfp-inline{width:1.1em;height:1.1em;border-radius:50%;vertical-align:-0.2em;margin-right:0.25em;object-fit:cover;}
  .price{font-variant-numeric:tabular-nums;}
  .is-hidden{display:none !important;}
  /* visually hidden but readable by assistive tech (clip pattern — keeps AT access, no layout) */
  .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}
  /* staged entrance (2026-07-25, Antimo: "mail > welcome para > sidebar icon > proverb > embed").
     JS adds .in to each in turn — see the seqIn chain in the script. visibility, not opacity alone,
     for the same reason .work-hero uses it: .panel-toggle is a real button and #wslides a tabbable
     region, so opacity:0 alone would leave a keyboard user able to focus fully invisible controls
     during the entrance. Reserving layout (visibility, not display) means nothing shifts as each
     lands — the sequence is purely a fade. */
  .seq{opacity:0;visibility:hidden;transition:opacity .5s cubic-bezier(.22,.61,.36,1);}
  .seq.in{opacity:1;visibility:visible;}

  /* ============ ONE SPLIT FOR THE WHOLE PAGE — same 1/3 : 2/3 proportion top to bottom.
     The rail column is in-flow; its children use sticky/fixed so the left side FEELS fixed:
     · email — sticky at top, never moves
     · intro — starts at the bottom of the first viewport (hero position), scrolls at page
       speed, then pins at vertical mid-page and stays for the rest of the page (2026-07-23:
       was "pins just below the email" — Antimo moved the pin point to mid-page)
     · explore note — fixed bottom-left, slides in from below once the intro pins (2026-07-23:
       replaces the old booking-stack CTAs with a plain paragraph — FAQ/Farcaster/X) ============ */
  /* transition on grid-template-columns: browsers interpolate grid tracks using their USED (resolved
     px) values each frame, same as a plain width transition — mismatched authored units (% here,
     rem in the open-state rule below) still animate smoothly in every current engine. */
  .split{display:grid;grid-template-columns:var(--rail-w) 1fr var(--edge-w);
        transition:grid-template-columns .32s cubic-bezier(.22,.61,.36,1);}
  /* ---- THE OPEN STATE — ONE class, ONE source of truth (2026-07-25).
     Every trigger routes through .panel-open, set by JS: pointer hover on the icon (desktop), tap
     (touch), and keyboard. An earlier pass drove hover straight from CSS with
     `.split:has(.panel-toggle:hover)` and used `&` nesting to avoid repeating the declarations for
     each trigger. Both were dropped deliberately, because that combination has a nasty PARTIAL
     failure mode rather than a clean one: `:has()` and CSS nesting shipped in different releases
     (Chrome 105 vs 112, Safari 15.4 vs 17.2, and Firefox in the opposite order — 121 vs 117), so
     there are real engines that parse the `:has()` selector, apply the grid-track change, and then
     silently drop every nested `&` rule — collapsing the left rail to 56px with the full intro
     paragraph still inside it. Routing through a class means no :has(), no nesting, no cliff, and it
     also keeps aria-expanded honest (CSS :hover is invisible to JS, so the attribute used to lie).
     Scoped to DESKTOP: mobile's .panel-open is a transform-based drawer instead (see the mobile
     block), where the left rail must keep rendering normally as it slides off, not collapse to a dot
     mid-slide. `not all and (max-width:900px)` is the EXACT complement of the mobile query — a plain
     min-width:901px would leave fractional widths (browser zoom, 900.5px) matching neither. ---- */
  @media not all and (max-width:900px){
    /* ---- THE LEFT RAIL GIVES UP EXACTLY WHAT THE PANEL TAKES — nothing more (2026-07-25, Antimo:
       "should the sidebar expansion always fully hide the left col? ... if not necessary it
       shouldn't"). It shouldn't, and answering it this way is also what makes the motion fluid,
       which was the other half of the same message.
       The previous version collapsed the rail to --edge-w, freeing ~446px while the panel only needed
       ~107px, so <main> GREW by ~339px mid-animation. That resize is what Antimo was seeing as lag:
       an iframe re-runs its own document layout whenever its viewport changes, and a live external
       site (Framer) does that on a debounce — hence the momentary empty strip on the embed's right,
       and the embedded page visibly reflowing its own breakpoints.
       Subtracting the panel's growth from the rail instead makes main's used width algebraically
       CONSTANT: closed it is T−R−E; open it is T−(R−P+E)−P, which reduces to the same T−R−E. Because
       both tracks animate on one shared easing, the two deltas cancel at every frame, not just at the
       endpoints — so main never resizes, it only translates. No iframe reflow at all, and the whole
       thing stays a single instant gesture rather than a staged move-then-grow.
       It also removes the collapse entirely: at 33.33% − 107px the rail still comfortably clears
       .intro's max-width:40ch and .explore's max-width:34ch, so NOTHING in the left column even
       rewraps — which is why the email-dot / explore-icon / intro-hide swaps this block used to carry
       are all gone. They only ever existed to survive a 33.6px rail. As a bonus this also kills the
       ~31px page-content jump the .work-hero aspect cap produced on every accidental hover, since
       that cap is derived from main's width, which no longer moves. ---- */
    .split.panel-open{grid-template-columns:calc(var(--rail-w) - var(--panel-w) + var(--edge-w)) 1fr var(--panel-w);}
  }
  /* ---- NARROW DESKTOP: opening COLLAPSES the left rail instead of shrinking it -------------
     Above this width the rail simply gives up the panel's width and nothing visibly moves in it,
     because .intro is capped at max-width:40ch and never reaches its container. Below it, that
     same 107px comes straight out of the paragraph's measure and it rewraps taller — the layout
     visibly degrades rather than sliding. So under 1330px the rail collapses to --edge-w and
     shows the brand dot, which is the behaviour this page had in an earlier iteration.

     1330px is measured, not chosen by eye: it is the width at which
        (viewport/3 − 2*--pad-h) − (--panel-w − --edge-w)
     drops below 28ch, the narrowest measure the intro still reads at. Verified across 940→1920px;
     at 1280px the post-open measure is 26.5ch (too tight), at 1366px it is 29.2ch (fine).
     Retune by moving this one number — the rules below are regime-agnostic.

     The lower bound is written as a `not all and (max-width:900px)` complement rather than a
     min-width, because ANY min-width value leaves a sliver uncovered — 901px misses 900.5, and
     900.02px misses 900.01. Browser zoom produces exactly those fractional widths. */
  @media not all and (max-width:900px){
   @media (max-width:1330px){
    .split.panel-open{grid-template-columns:var(--edge-w) 1fr var(--panel-w);}
    /* Hold the rail's content hidden across the CLOSING transition too. Without this the
       display:none lifts on the same frame the track starts widening, so for 320ms the intro is
       laid out in a ~34px column — a 0-width, ~800px-tall ribbon — and the email paints on top of
       the embed. Fading them back in over the track's own duration means they only become visible
       once there is a column to hold them. */
    .rail .email,.rail .breadcrumb,.rail-main{transition:opacity .18s ease .16s;}
    /* the collapsed rail is narrower than its own default --pad-h padding, which would push the
       dot out of the column and over the embed */
    .split.panel-open .rail{padding-left:var(--main-pad-h);padding-right:var(--main-pad-h);}
    /* everything in the rail goes; the dot stands in for the lot. display:none, not opacity —
       text has a min-content width that would hold the track open past --edge-w. */
    .split.panel-open .email,
    .split.panel-open .breadcrumb,
    .split.panel-open .rail-main{visibility:hidden;opacity:0;transition:none;}
    /* content-visibility keeps them out of layout (so their min-content width cannot hold the
       track open past --edge-w) while still allowing the opacity transition on the way back —
       display:none would kill the transition and reintroduce the flash. */
    .split.panel-open .rail-main{content-visibility:hidden;}
    .split.panel-open .rail-dot{display:block;}
    /* the explore note goes away ENTIRELY while the rail is collapsed (2026-07-27, Antimo: the
       bottom-left glyph "is not wanted"). It used to swap to a small Lucide text-align-start icon
       standing in for the sentence, mirroring what the email did with the brand dot. Two reasons
       that was the wrong call and the icon is now deleted outright (CSS + markup on all three
       pages): it was a second, unrelated three-line glyph sitting diagonally opposite the panel
       toggle's own three-line glyph, which reads as a second menu button rather than as a
       shrunken sentence; and unlike the brand dot it stood in for nothing recoverable — there is
       no hover or click that expands it back into the copy, so it was decoration that looked like
       a control. The rail is 33.6px wide here; the honest answer at that width is "nothing".
       Hiding the visible <p> is all that's needed — .explore's own box then has no rendered
       content — and it deliberately spares the .sr-only paragraph, so the Farcaster/X/Telegram
       links stay in the DOM and in the accessibility tree exactly as they are in every other
       regime. A visibility:hidden or display:none on .explore itself would have taken them out
       of the a11y tree with it, which is a real loss for zero visual gain. */
    .split.panel-open .explore p:not(.sr-only){display:none;}
   }
  }
  /* the brand dot: the rail's entire content while collapsed. Sticky at the same offset the email
     and breadcrumb use, so it appears exactly where they were. */
  .rail-dot{-webkit-user-select:none;user-select:none;display:none;position:sticky;top:var(--pad-h);z-index:12;
           width:0.7em;height:0.7em;border-radius:50%;background:var(--red-solid);}
  /* (.explore-icon — the collapsed stand-in glyph — lived here until 2026-07-27. Removed with its
     markup; see the note in the collapse-regime block above for why.) */

  /* ---- Products / Engine scramble-on-hover -------------------------------------------------
     tabular-nums + a fixed inline-size stop the label reflowing while characters cycle: the
     scrambled glyphs are not the same widths as the real ones, and without this the whole list
     would jitter horizontally on hover. */
  .sp-item.is-soon{font-variant-numeric:tabular-nums;}
  .sp-item.is-soon .scr{display:inline-block;white-space:pre;}

  /* panel contents reveal at EVERY breakpoint — mobile's drawer shows the same items */
  .split.panel-open .side-panel{opacity:1;pointer-events:auto;}
  .rail{position:relative;padding:var(--pad-h);min-width:0;}   /* uniform on all 4 sides (was --pad top/bottom vs --pad-h left/right — Antimo: mismatched, fixed 2026-07-22). min-width:0 (2026-07-25) so this grid item can shrink to --edge-w too, matching main/.rail-r — same defensive reasoning, even though today's in-flow content during collapse is already narrow enough on its own */
  /* right rail — 3rd column (2026-07-25). Zero left padding (not var(--pad-h)) — main's OWN right
     padding already provides the embed→icon gap, so a second padding stacked on top of it would make
     that gap bigger than the icon→page-edge gap on the right (var(--main-pad-h) below), the exact
     mismatch Antimo flagged. Right padding reuses that same var so the two gaps are structurally
     forced equal. */
  /* user-select:none on the whole right rail (2026-07-27, Antimo: "remove text selection from
     anywhere is not needed"). Everything in here is chrome — the menu items, the clock, the
     proverb, the icon — and a stray drag selecting "Cambridge 14:07" is noise, never intent.
     Deliberately NOT applied to page content: the intro paragraph, the pieces, the FAQ answers
     and the email address all stay selectable, because copying those IS intent. */
  .rail-r{position:relative;min-width:0;padding:0 var(--main-pad-h) 0 0;
         -webkit-user-select:none;user-select:none;}
  /* sticky, viewport-tall inner (2026-07-25) — THE FIX for "if i scroll the page sidebar items
     currently dont appear (theyre anchored somehow to the scroll progress)": the panel used to be
     plain in-flow content inside a grid item as tall as <main> (thousands of px), so it sat at the
     very top of that box and scrolled away for good. Only the icon was sticky, which is why the icon
     stayed and the items didn't. One sticky viewport-height flex column now carries all three pieces
     together, so the whole rail is pinned as a unit at any scroll position — and it's what puts the
     icon at the top and the proverb at the bottom without either being scroll-dependent.
     align-items:flex-end keeps everything hugging the true right edge at a CONSTANT inset regardless
     of collapsed/expanded width, so the icon never visually jumps when the panel fires. */
  .rail-r-inner{position:sticky;top:0;height:100svh;padding:var(--pad-h) 0;
               display:flex;flex-direction:column;align-items:flex-end;}
  /* the icon's own row. .side-panel is flex:1 here and the icon flex:none, so the panel's right edge
     sits FLUSH against the icon's left edge — that's both an unbroken mouse path from icon into menu
     and, with align-items:flex-start + matched box heights below, what puts the first item on exactly
     the icon's line (Antimo: "first item must be horiz on the same line of the icon itself"). */
  /* justify-content:flex-end matters only in the COLLAPSED state: there the column's content box
     (--edge-w minus its right pad) is narrower than the toggle's own hit-area box, and .side-panel
     (flex:1) has already shrunk to zero, so the toggle overflows. Default flex-start sent that
     overflow rightward and pushed the icon 1.5px past the page's right inset, breaking the alignment
     with the email's copy icon; flex-end sends it leftward into the column's dead space instead,
     where it is invisible. No effect when expanded, since there is free space for flex:1 to absorb. */
  .rail-r-head{display:flex;align-items:flex-start;justify-content:flex-end;width:100%;min-width:0;}
  /* icon TOP edge aligned to the left copy icon's top edge (2026-07-25, Antimo). Both sit one
     --pad-h down from their column's top, so the only remaining offset is each icon being centered
     within its own text line: (1em line-box − 0.92em icon) / 2 = 0.04em. Derived, not measured-and-
     hardcoded, so it survives a font-size change. Was align-items:center in a 40px box, which pushed
     the icon 13px down — measured 8.6px below the copy icon.
     The padding on the other three sides only grows the CLICK box (leftward and downward, away from
     the icon's top-right anchor), so the icon itself does not move: without it the button would be a
     14px target, since it is now content-sized rather than a full item-height box. */
  .panel-toggle{flex:none;display:flex;align-items:flex-start;justify-content:flex-end;
               padding:0.04em 0 0.6rem 0.6rem;
               background:none;border:none;cursor:pointer;color:var(--ink);}
  /* list-sort-descending, mirrored (2026-07-25, Antimo's spec) — scaleX(-1) flips the whole glyph
     rather than hand-mirroring the path data, so it stays a byte-for-byte copy of Lucide's real
     geometry (verified live against unpkg/jsdelivr/GitHub, not reconstructed from memory). Sized to
     0.92em, exactly matching .email .copyicon/.checkicon, per Antimo: "must be as big as the copy
     icon next to the mail addy". */
  .panel-toggle svg{width:0.92em;height:0.92em;transform:scaleX(-1);}
  /* open -> the same button becomes an X (2026-07-25, Antimo). Both glyphs are always in the DOM and
     swapped by display, same pattern as the FAQ's +/- markers, so there's no icon-swap JS to keep in
     sync with the panel state. scaleX(-1) is unset for the X: it's symmetrical, so mirroring it is a
     no-op visually but would still flip the stroke-join rendering order for no reason. */
  .panel-toggle .ic-close{display:none;transform:none;}
  .split.panel-open .panel-toggle .ic-open{display:none;}
  .split.panel-open .panel-toggle .ic-close{display:block;}
  /* ---- HOVER SWEEP ON THE MENU GLYPH (2026-07-27) -------------------------------------------
     Hovering the icon no longer opens the panel (that moved to click — see shell.js), which left
     the control with no hover feedback whatsoever: the pointer landed on something that looked
     inert until you clicked it. This puts the affordance back WITHOUT a state change — the three
     bars wipe out and back in, left to right across the glyph, and nothing else happens.

     Geometry first, because every number below is read off it rather than eyeballed. The glyph is
     Lucide's list-sort-descending and `.panel-toggle svg` mirrors the whole element with
     scaleX(-1), so path coordinates are reflected about x=12 before they reach the screen and
     "left" in the path data is "right" on screen. Mirrored, the three paths are:
        :nth-child(1)  M15 12H3   length 12   screen x  9 -> 21   MIDDLE bar, drawn L->R on screen
        :nth-child(2)  M3 5h18    length 18   screen x 21 ->  3   TOP    bar, drawn R->L on screen
        :nth-child(3)  M9 19H3    length  6   screen x 15 -> 21   BOTTOM bar, drawn L->R on screen
     All three are flush at the right (x=21) and ragged on the left. Two consequences, and both are
     the reason this looked wrong on the first pass: the stagger has to follow the LEFT edges
     (3, 9, 15 = top, middle, bottom) or the "wipe" arrives in the wrong order, and :nth-child(2) is
     drawn in the OPPOSITE direction from the other two, so its dash offsets need the opposite sign
     — hence --dir. Verified from frame captures, not from the path data alone; a mirrored glyph is
     exactly the kind of thing that reads backwards if you only reason about it on paper.

     stroke-dashoffset rather than a scaleX transform per <path>: scaling a stroked line squashes
     its round caps into ellipses, and transform-box:fill-box on the zero-height bounding box of a
     horizontal line is not something to rely on across engines. Dashes leave the caps alone.

     ONE monotonic animation per bar, no jump-cuts. The dash pattern is `len (2*len)` — a single
     dash and a DOUBLE-length gap, period 3*len — so walking dashoffset steadily from 0 to -3*len
     passes through all three phases in order and lands back on a bar that is identical to the
     start:
        0     -> -len     bar retracts, its left end chasing right          (wipe out)
        -len  -> -2len    entirely inside the gap, nothing drawn            (hold)
        -2len -> -3len    the next period's dash arrives from the left      (wipe in)
     The obvious keyframe — full, empty, other-empty, full — needs a discontinuity between the two
     empty offsets, and interpolating across it passes through 0 (a fully drawn bar) for one frame,
     which flickers. The oversized gap removes the discontinuity rather than trying to hide it.

     THREE keyframe blocks with literal numbers, not one parameterised block. The first version of
     this did the obvious DRY thing — one @keyframes using `calc(var(--bar) * var(--dir) * -1)` and
     a --bar/--dir pair per path — and it was silently broken: Chromium does not INTERPOLATE
     keyframe values that are calc() expressions containing var(), it falls back to discrete
     substitution, so each bar snapped between the four keyframe values instead of sliding through
     them. Frame captures showed whole bars blinking on and off — a strobe, not a wipe — while the
     computed offsets read as the exact keyframe numbers at every sample. Literal values interpolate
     properly. Keep them literal; the lengths are fixed by the path data above and can only change
     if the glyph itself does.

     .42s each + .06s stagger = .54s end to end. linear, not an ease: a wipe that accelerates and
     decelerates three times inside one gesture reads as a wobble, not a sweep. `both` fill so a bar
     waiting out its delay is drawn normally instead of blank.

     Scoped to .ic-open, so the X never animates — it is display:none while closed anyway, but the
     scoping is the point: the close affordance should be still and unambiguous. Gated on
     (hover:hover) because :hover latches after a tap on touch and would fire this on a gesture that
     is already opening the drawer. Nothing here is a layout property, so the icon cannot shift or
     resize and the button box is untouched.

     Reduced motion is handled by GATING the block, not by overriding it from the
     prefers-reduced-motion section at the foot of this file — which is where it started, and where
     it silently did nothing. `animation:none` there has specificity (0,3,1) while the per-path
     `:nth-child` rules below carry (0,4,1), so animation-name and stroke-dasharray both survived
     the override and the sweep still ran for reduced-motion users. Declaring it only under
     `no-preference` means there is nothing to override in the first place. It also fails in the
     right direction on a UA that supports neither value: no match, no animation. ---- */
  @media (hover:hover) and (prefers-reduced-motion:no-preference){
    .panel-toggle:hover .ic-open path{animation-duration:.42s;animation-timing-function:linear;animation-fill-mode:both;}
    .panel-toggle:hover .ic-open path:nth-child(1){stroke-dasharray:12 24;animation-name:panel-icon-sweep-mid;animation-delay:.06s;}   /* middle bar — left edge x=9  */
    .panel-toggle:hover .ic-open path:nth-child(2){stroke-dasharray:18 36;animation-name:panel-icon-sweep-top;animation-delay:0s;}     /* top bar    — left edge x=3, so it leads */
    .panel-toggle:hover .ic-open path:nth-child(3){stroke-dasharray:6 12;animation-name:panel-icon-sweep-bot;animation-delay:.12s;}    /* bottom bar — left edge x=15, so it trails */
  }
  /* 38% out / 12% hold / 50% back in — the erase is deliberately quicker than the redraw, so the
     glyph feels wiped away and then written, rather than blinking symmetrically. The sign is the
     only difference between the three: the top bar's path runs right-to-left ON SCREEN once the
     parent's scaleX(-1) is applied, so its offsets have to travel the other way to wipe the same
     way. Get that backwards and the middle bar sweeps one direction while the top sweeps the
     other — which is precisely what the first build did. */
  @keyframes panel-icon-sweep-top{   /* M3 5h18  — length 18, drawn R->L on screen */
    0%{stroke-dashoffset:0;} 38%{stroke-dashoffset:18;} 50%{stroke-dashoffset:36;} 100%{stroke-dashoffset:54;}
  }
  @keyframes panel-icon-sweep-mid{   /* M15 12H3 — length 12, drawn L->R on screen */
    0%{stroke-dashoffset:0;} 38%{stroke-dashoffset:-12;} 50%{stroke-dashoffset:-24;} 100%{stroke-dashoffset:-36;}
  }
  @keyframes panel-icon-sweep-bot{   /* M9 19H3  — length 6, drawn L->R on screen */
    0%{stroke-dashoffset:0;} 38%{stroke-dashoffset:-6;} 50%{stroke-dashoffset:-12;} 100%{stroke-dashoffset:-18;}
  }
  /* 急がば回れ (isogaba maware — "if you're in a hurry, take the long way round"; more haste, less
     speed) set as real vertical Japanese (tategaki), not sideways latin text. writing-mode:vertical-rl
     is enough on its own — text-orientation defaults to `mixed`, which already keeps CJK characters
     upright (only non-CJK script would get rotated 90°), so every character here renders correctly
     with zero extra properties. margin-top:auto pins it to the bottom of the sticky column (Antimo:
     "move jp proverb to bottom of sidebar").
     Noto Sans JP, not the system CJK fallback (Antimo: "change to a font thats more similar in
     geometry and weight as our inter implementation") — Hiragino/Yu Gothic read lighter and rounder
     than Inter next to it; Noto Sans JP shares the same humanist-grotesque skeleton and has a real
     400 to match body weight exactly. font-size:0.92em is not a look-tuned number: CJK glyphs are
     full-width (advance = 1em), so at 0.92em the single-glyph column measures exactly 0.92em wide —
     the same 0.92em the icon above it is sized to (Antimo: "visually as wide as the icon"). */
  /* foot row — pinned to the bottom of the sticky column. margin-left:auto on the label (rather than
     justify-content:space-between on the row) is what keeps the proverb hard right in BOTH cases:
     with the dot present it's pushed right by the free space, and with the dot display:none'd on
     desktop it's still pushed right by all of it. space-between would have dropped the lone label to
     the left edge the moment the dot was hidden. */
  .rail-r-foot{margin-top:auto;width:100%;display:flex;align-items:flex-end;}
  .rail-r-dot{display:none;width:0.7em;height:0.7em;border-radius:50%;background:var(--red-solid);}
  .rail-r-label{margin-left:auto;writing-mode:vertical-rl;
               font-family:'Noto Sans JP','Inter',-apple-system,BlinkMacSystemFont,sans-serif;
               font-size:0.92em;font-weight:400;color:var(--faint);line-height:1;}
  /* hover-expand mockup (2026-07-25) — DUMMY ITEMS ONLY, no real destination behind any of them yet;
     purely to assess the interaction/visual treatment before any real panel content exists. Reworked
     same day from a position:fixed overlay (Antimo: "should not be an overlay... should push") to a
     normal in-flow block that fills its own (now width-animated) grid column instead — main shrinks
     out of its way structurally, nothing floats on top of the embed anymore. That overlay version's
     box-shadow was also the actual cause of the faint permanent "gradient" Antimo spotted smeared
     across the page's right edge even while fully closed: a fixed+translated element's box-shadow
     still paints past its own translated edge, blur and all. Gone now that there's no fixed
     positioning and no shadow at all — nothing to bleed. min-width:0 + overflow:hidden are the fix
     for a DIFFERENT bug adversarial review would otherwise catch: white-space:nowrap items have a
     wide intrinsic min-content width, which — without these two — would hold .rail-r's grid track
     open past --edge-w even while collapsed and opacity:0, since opacity doesn't remove layout
     footprint the way display:none does. */
  /* margin-top re-aligns the FIRST item's text centre onto the icon's centre now that the icon is
     top-aligned rather than centred in an item-height box. Derived from the same two numbers as
     .panel-toggle's padding, so the two can't drift: icon centre sits 0.04em + 0.46em = 0.5em below
     the head's top, while item 1's centre sits (--sp-pad-v + 0.8em) below the panel's top; the
     difference is the offset. No `gap` any more — .feat, the spacing reference Antimo pointed at,
     uses padding alone, so a gap on top of it would reintroduce exactly the looseness he flagged. */
  /* padding-left makes the embed->items gap equal the pricing-card gutter (Antimo, 2026-07-25). The
     total gap is main's own right padding PLUS this, because the embed's visual edge sits one
     --main-pad-h inside main's border edge, which is exactly where .rail-r's track begins — so the
     inset only has to make up the difference. Subtracting rather than hardcoding 12.5px keeps it
     correct at every viewport, since both operands are independent clamps (verified positive across
     the whole desktop range: ~10px at 901px, 12.5px at 1440px, 9.6px at 2000px). Mobile overrides
     this with --mob-pad-h further down; there the drawer is full-width and the relationship is moot. */
  .side-panel{flex:1;min-width:0;overflow:hidden;
             padding-left:calc(var(--card-gap) - var(--main-pad-h));
             margin-top:calc(0.5em - 0.8em - var(--sp-pad-v));
             display:flex;flex-direction:column;align-items:flex-start;
             white-space:nowrap;opacity:0;pointer-events:none;
             transition:opacity .2s ease .08s;}
  /* padding on the vertical axis ONLY, matching .feat exactly (Antimo: "look at the line items in the
     pricing cards for vert spacing") — no horizontal padding, so the text sits flush to the panel's
     edge and the measured panel width is the text width, nothing more.
     No hover background/pill any more (Antimo: "they should not have hover states. make them gray on
     hover") — so ink at rest, muted on hover, which is the opposite of the old muted-at-rest. */
  .sp-item{padding:var(--sp-pad-v) 0;color:var(--ink);transition:color .12s ease;
          text-decoration:none;display:block;}
  /* :not(.sp-place) — the place/time item stopped being a link (2026-07-27) and must stop
     behaving like one; a hover lightening still read as "clickable". */
  .sp-item:not(.sp-place):hover{color:var(--muted);}
  /* not-yet-live entries. Muted and non-interactive, but deliberately still LISTED — the roadmap
     is part of the message. aria-disabled (not the disabled attr, which <span> ignores anyway)
     states it for AT, since "looks grey" carries nothing to a screen reader. */
  .sp-item.is-soon{color:var(--hair-strong);cursor:default;}
  .sp-item.is-soon:hover{color:var(--hair-strong);}
  /* place + local time. Sits at the foot of the list; the time is the only live thing on the page,
     which is the whole point of it — a studio of one, in a real timezone. */
  .sp-place{display:flex;align-items:center;gap:0.4em;white-space:nowrap;}
  .sp-pin{width:0.92em;height:0.92em;flex:none;}
  .sp-time{color:var(--muted);font-variant-numeric:tabular-nums;}
  .sp-time:not(:empty)::before{content:"";}
  /* vertical-align:top (2026-07-25) fixes a pre-existing 4px jump: as an inline-flex box, .email sat
     on its line box's BASELINE at y=23.2, but its own sticky top is --pad-h (19.2), so it visibly
     hopped up 4px the moment the page scrolled and it pinned. Aligning it to the line-box top makes
     natural and pinned position identical — and, since the right rail's icon is now aligned to this
     element, gives that alignment one stable target instead of a scroll-dependent one. */
  /* ---- BREADCRUMB — the global pattern for every internal page (2026-07-26) ----
     Occupies the exact slot the homepage gives the email button: same sticky offset, same
     line-height:1, same vertical-align:top (which is what stops it hopping 4px on first scroll —
     see the .email note below, same fix, same reason). So the top-left anchor of the site never
     moves between pages; only its content changes. Home = email, everything else = breadcrumb.

     ADOPTED FROM THE ENGINE REGISTRY (slash.red/engine/, .de-rs-crumb) 2026-07-27 — that page had
     the canonical version and this one had diverged: a literal "/" in a <span> instead of the
     U+203A + NBSP ::before, and a non-link active crumb. The registry's is the reference because
     it is the surface actually built from the engine's own component set. Local token names map
     1:1 onto the store's (--muted = --ck-text-secondary, --ink = --ck-text-primary,
     --faint = --ck-text-tertiary), so the rendering is identical without importing the --ck-* names.
     Minted back into the store as the `breadcrumb` component in components/silos/red-site/.
     Keep it one level deep (RED > Section). If a third level is ever needed, add it here — do
     NOT invent a per-page variant. */
  .breadcrumb{position:sticky;top:var(--pad-h);z-index:12;vertical-align:top;
             display:inline-flex;align-items:center;line-height:1;
             -webkit-user-select:none;user-select:none;}   /* navigation chrome, not copy */
  .breadcrumb .crumb-item{color:var(--muted);text-decoration:none;transition:color .12s ease;}
  /* the home crumb is the brand mark, not the word (2026-07-27, Antimo) — same solid red circle
     the collapsed rail and the mobile drawer use, so "RED" is one consistent glyph across every
     place the site refers to itself. It keeps a real accessible name via .sr-only, so the trail
     still reads "RED › Words" to a screen reader. Sized to the separator's own optical weight
     rather than the text's, so it sits on the same baseline run. */
  .breadcrumb .crumb-home{display:inline-flex;align-items:center;position:relative;}
  .breadcrumb .crumb-dot{width:0.62em;height:0.62em;border-radius:50%;background:var(--red-solid);
             display:block;transition:opacity .12s ease;}
  .breadcrumb .crumb-home:hover .crumb-dot{opacity:0.7;}
  /* separator is a ::before on ADJACENT items, not an element in the markup — matches the engine
     registry byte for byte: U+203A + NBSP, tertiary ink, asymmetric margins (more lead-in than
     lead-out). Doing it in CSS means the markup is just a flat list of links and a page can never
     get the separator count wrong. */
  .breadcrumb .crumb-item + .crumb-item::before{content:"\203A\00a0";
             color:var(--faint);margin:0 0.15em 0 0.35em;}
  .breadcrumb .crumb-item.active{color:var(--ink);}
  .breadcrumb .crumb-item:not(.active):hover{color:var(--ink);}

  .email{position:sticky;top:var(--pad-h);z-index:12;line-height:1;color:var(--ink);vertical-align:top;
         background:none;border:none;padding:0;cursor:pointer;display:inline-flex;align-items:center;gap:0.45em;}
  .email .copyicon,.email .checkicon{width:0.92em;height:0.92em;}
  .email .copyicon{opacity:0.5;transition:opacity .15s;}
  .email:hover .copyicon{opacity:0.85;}
  .email .checkicon{display:none;}
  /* click swaps the icon only — no label text change (2026-07-23, Antimo: "no copy change just
     change the copy icon to a checkmark icon"). color:red cascades to the label text via normal
     inheritance (no extra rule needed) and to the checkmark via its stroke="currentColor". */
  .email.copied{color:var(--red);}
  .email.copied .copyicon{display:none;}
  .email.copied .checkicon{display:inline;}
  /* NOTE: the whole collapsed-left-rail treatment that used to live here (email label/icons swapping
     to a red dot, the copy-feedback colour flip on that dot, .explore's paragraph swapping to a
     text-align-start icon) is gone as of the "rail gives up only what the panel takes" change above.
     None of it is reachable any more: the rail no longer narrows past its own content, so the email,
     the explore note and the intro paragraph all just stay as they are and shift. The red dot lives
     on only in the MOBILE drawer's bottom-left corner — see .rail-r-dot. */
  /* "Work with us" — a real continuation of the SAME paragraph/typed text, not a separate element
     (2026-07-22 fix — every earlier attempt to make this a sibling block caused overlap bugs,
     because .live's bottom:0 positioning depends on nothing else sharing its containing block).
     Sentence case: only the leading W is capitalized, matching the page's usual CTA voice. */
  /* no underline, red only (Antimo, 2026-07-22). The :hover colour change was removed on main
     (9026684) — "Work with us" now stays red in every state; ported here because the rule moved
     into the shell when the CSS was extracted. */
  .wlink{color:var(--red);text-decoration:none;}
  /* the ↘ arrow: shared class so it always matches .wlink's color regardless of render mode — this
     was the fix for "why does the arrow render black on mobile" (2026-07-22): it was a bare,
     unstyled span there. The arrow is nested INSIDE the <a class="wlink"> in every mode (both the
     reduced-motion branch and the line-reveal branch below) — same click/hover target as the rest
     of the link either way (a 2026-07-23 fix: an earlier version of the line-reveal branch made it
     a sibling span instead, so clicking/hovering the glyph silently did nothing). */
  .wlink-arrow{color:var(--red);}
  /* line-reveal word tokens — same mechanism on BOTH breakpoints (2026-07-22 rework; originally
     mobile-only, desktop typed char-by-char) */
  .wln{opacity:0;transition:opacity .35s ease;}
  .wln.on{opacity:1;}

  /* intro — sticky paragraph; JS sets margin-top so it loads bottom-anchored like the old hero,
     then sets top so it PINS at vertical mid-page once scrolled (2026-07-23: was a fixed
     top:calc(--pad-h + 2.6rem) pinning it just below the email — Antimo: "scrolls up to mid
     page"). 50vh here is just the pre-JS fallback; placeIntro() overwrites it with a value
     computed from the paragraph's own height so it's actually centered, not just top-anchored
     at the midpoint. */
  .intro{position:sticky;top:50vh;max-width:40ch;
         line-height:1.6;letter-spacing:-0.003em;}
  /* own positioned wrapper for ghost+live (2026-07-22 fix) — .live's bottom:0 must resolve against
     the ghost's own box, not .intro's total height. Without this, adding .intro-cta as a sibling
     grew .intro taller, so .live's bottom:0 shifted down onto the CTA's space (Antimo: overlap bug). */
  .intro-text{position:relative;}
  .intro-text .ghost{visibility:hidden;white-space:pre-wrap;}
  .intro-text .live{position:absolute;left:0;right:0;bottom:0;white-space:pre-wrap;}

  /* explore note — fixed bottom-left, slides in from below once .intro finishes scrolling up and
     pins (2026-07-23, replaces the old CTA booking stack that lived here — same slide-in mechanic,
     reused as-is, just a plain paragraph instead of buttons now). visibility toggles with the
     transform so it's out of the tab order and AT while off-screen — same reasoning .book.on used
     to rely on for its own CTAs. */
  .explore{position:fixed;left:var(--pad-h);bottom:var(--pad-h);z-index:20;
          width:calc(33.33vw - 2*var(--pad-h));max-width:34ch;
          line-height:1.6;
          visibility:hidden;
          transform:translateY(calc(100% + var(--pad-h) + 1rem));
          transition:transform .55s cubic-bezier(.22,.61,.36,1),visibility 0s linear .55s;}
  .explore.on{visibility:visible;transform:none;
             transition:transform .55s cubic-bezier(.22,.61,.36,1),visibility 0s;}
  @media (max-height:480px){ #explore{display:none;} }   /* short windows: rail can't fit intro + note */
  .explore-repeat{display:none;}   /* mobile-only — see the media query below and the HTML comment where it's placed, between .work-hero and .cards-hero */
  /* .explore needs no open-state handling at all now: its own max-width:34ch (~258px) is far inside
     the rail's narrowest open width (~334px of content box at 1440), so it neither overflows nor
     rewraps when the panel takes its 107px. Same reason .intro (max-width:40ch) is left alone. */

  /* ============ RIGHT COLUMN — the only thing that scrolls ============ */
  main{margin-left:0;min-width:0;padding:0 var(--main-pad-h);}   /* horizontal padding halved (item 1); min-width:0 so the grid item can shrink */
  /* margin-top:25vh (not padding) — a clean, guaranteed gap regardless of what precedes it, same
     value on both breakpoints since vh is already relative to each device's own viewport height
     (2026-07-23, Antimo: "25%vh" between work/tiers/quote/faq, same rhythm on mobile too). Only
     used by the quote + faq sections now (close-sec, the third .section, was removed) — safe to
     put directly on the shared class rather than a third distinguishing class. */
  .section{padding:clamp(2.6rem,6vw,4.5rem) 0;margin-top:25vh;}


  /* ---- mobile: rail becomes a top block, single column (shell half) ---- */
  @media (max-width:900px){
    .split{grid-template-columns:1fr;}
    /* top padding is a SINGLE --pad (was clamp(3.2rem,10vw,4rem), ~2x --pad — item 6's "doubled" pad);
       horizontal halved (item 1); bottom unchanged (vertical); rule below the intro removed (item 3);
       min-height:55svh so the 90svh carousel below shows exactly 45svh (50%) above the first fold (item 8) —
       UNCHANGED (2026-07-23): the embed's distance from the top of the page must stay put, so this
       stays 55svh; only what happens INSIDE that 55svh changes below. */
    .rail{position:static;padding:var(--pad) var(--mob-pad-h) clamp(1.6rem,5vw,2rem);min-height:55svh;
          display:flex;flex-direction:column;}
    /* 55svh exists to put the embed's top edge 50% above the first fold — it is home's number, and
       it is only meaningful because home fills that space with the welcome paragraph. On a page with
       no .rail-main the rail is just a breadcrumb, so 55svh became 464px of nothing and pushed the
       first real content to y=483 (home's paragraph sits at y=175).
       A single svh constant CANNOT match home exactly, and it is worth writing down why: home's
       offset is emergent, not set. The paragraph is centred in the leftover space, so its top moves
       with its own height, which jumps discretely as the text rewraps. Measured, the height needed
       to match it is 18.00svh at 360x800, 18.48 at 390x844, 20.25 at 402x874 and 20.71 at 430x932 —
       it climbs with WIDTH, not height. Hence the width term: 18svh at 360px wide, plus 0.33px per
       px of extra width, clamped so a wide short tablet cannot run away with it. Max error across
       those four sizes is ~6px, against ~12px for the best possible flat constant.
       Written as :not(:has()) rather than the inverse on purpose: if an engine drops the selector,
       every page keeps 55svh — today's behaviour, rather than a broken hero. */
    .rail:not(:has(.rail-main)){
      min-height:clamp(18svh, calc(18svh + (100vw - 360px) * 0.33), 26svh);}
    .email,.breadcrumb{position:static;}
    /* centered in the leftover space between the email and the embed's top edge (2026-07-23,
       Antimo: "the welcome paragraph moves in the centre vertically between the email and the top
       edge of the embed... otherwise there's not enough space") — was a fixed margin-top:1.4rem
       sitting right under the email, leaving a big dead gap below before the carousel. margin-top/
       bottom:auto on a flex child centers it in the remaining space once a sibling (email) is
       already flush at the top — no JS needed, this is a static layout property, not scroll-driven,
       so it holds regardless of scroll position ("stays there when you scroll up"). */
    .intro{position:relative;top:auto;margin-top:auto !important;margin-bottom:auto;max-width:60ch;}   /* relative (not static!) so .live keeps its containing block; !important beats the JS inline push */
    .explore{display:none;}   /* desktop-only (2026-07-23) — mobile has no scroll-pinned rail to trigger off of */
    main{margin-left:0;padding:0 var(--mob-pad-h);}   /* horizontal halved (item 1) */
    /* ---- mobile right rail (2026-07-25, Antimo: "add the sidebar icon to mobile, same place, but
       sidebar will open full page expanding from right to left (and pushing content out to the left
       rather than overlaying it)"). Was display:none. position:fixed rather than a grid column,
       because the mobile grid is a single stacked column with no gutter to live in — this keeps the
       icon at the top-right corner where it is on desktop. The closed strip is pointer-events:none so
       it can't swallow taps meant for the content behind it; only the button opts back in. ---- */
    .rail-r{display:block;position:fixed;top:0;right:0;bottom:0;z-index:40;
           width:var(--edge-w);padding:0 var(--mob-pad-h) 0 0;pointer-events:none;
           transition:width .32s cubic-bezier(.22,.61,.36,1);}
    .rail-r-inner{position:static;height:100%;padding:var(--pad) 0;}   /* --pad (not --pad-h) matches .rail's own mobile top padding above, so the icon lands on exactly the email's row */
    /* 44px tap target (the platform minimum) instead of the icon's bare 14px, WITHOUT moving the
       icon: justify-content:flex-end keeps the glyph flush to the button's right edge and
       align-items:flex-start (inherited from the base rule) keeps it flush to the top, so the box
       only ever grows left and down, into empty gutter. Growing it rightward with a negative margin
       would have pushed past --mob-pad-h and out of the viewport, since .rail-r is position:fixed and
       nothing clips it. min-height rather than height so it can't squash the icon's 0.04em top inset,
       which is what aligns it with the email's copy icon. */
    .panel-toggle{pointer-events:auto;width:44px;min-height:44px;}
    .rail-r-label{display:none;}   /* mobile shows the proverb only inside the OPEN drawer (Antimo) — in the closed strip there's no room for it beside the content */
    /* padding-left: same inset as .side-panel's items, so the dot lines up under them.
       padding-bottom: lift the foot clear of the phone's rounded display corners (Antimo: "the red
       dot and proverb sit right on the curvature and look weird"). These two are the worst case for
       it — one in each bottom corner, where the curve eats the most vertical space. Stacked on
       .rail-r-inner's own --pad (18.4px) this puts them ~44px off the bottom, about a current
       handset's display corner radius. env() adds the home-indicator inset on top wherever the
       browser reports one; it resolves to 0 here (that needs viewport-fit=cover, which we do NOT
       want — it would push content INTO the unsafe area, the opposite of this fix) and is kept only
       so the clearance still holds if that ever changes. */
    /* The dot is a 9.4px target — a quarter of the 44px platform minimum, and the only way back to
       home from an internal page. A transparent ::after gives it a real 44x44 hit box with ZERO
       layout effect, so the glyph itself does not move or grow. Preferred over making the whole
       breadcrumb a home link: "Words" is the current page, and labelling it as a route to somewhere
       else is wrong for a pointer user and actively misleading to a screen reader. The box does
       overlap the start of "Words", which is harmless — that crumb only ever links to itself.
       Anchored to the crumb's LEFT edge and pulled out by the page inset, not centred on the dot:
       the dot sits 8px from the screen edge, so a centred 44px box would put half its area
       off-screen where nothing can tap it — measured, 2 of 5 probe points missed. This way the full
       44px is reachable. */
    .breadcrumb .crumb-home::after{content:"";position:absolute;top:50%;left:0;
             transform:translateY(-50%);width:44px;height:44px;margin-left:calc(var(--mob-pad-h) * -1);}
    .rail-r-foot{padding-left:var(--mob-pad-h);padding-bottom:calc(1.6rem + env(safe-area-inset-bottom,0px));}
    .side-panel{padding-left:var(--mob-pad-h);}   /* items shouldn't sit flush against the drawer's left edge once it's full-width */
    .split.panel-open{grid-template-columns:1fr;}   /* neutralises the desktop 3-track collapse — mobile pushes with a transform instead */
    .split.panel-open .rail-r{width:100vw;background:var(--bg);pointer-events:auto;}
    .split.panel-open .rail-r-label{display:block;}   /* .rail-r-foot's margin-top:auto + the label's own margin-left:auto put it bottom-RIGHT of the open drawer, as asked */
    .split.panel-open .rail-r-dot{display:block;}   /* red dot, bottom-LEFT of the open drawer (Antimo, 2026-07-25) — mobile only; on desktop the email itself stays visible, so the dot has no job there */
    .split.panel-open .rail,
    .split.panel-open main{transform:translateX(-100vw);}
    /* scroll lock — the drawer is opaque and full-page, so without this a swipe scrolls the document
       underneath with zero visual feedback and you surface somewhere else entirely on close. Set on
       <html> by JS (.panel-locked) rather than on .split, so it's the actual scrolling element. */
    .panel-locked{overflow:hidden;}
    /* the push itself. Declared on the elements (not only in the open state) so it animates BOTH ways
       — a transition that only exists in the open state snaps back instantly on close. */
    .rail,main{transition:transform .32s cubic-bezier(.22,.61,.36,1);}
    /* overflow-x:clip, not hidden: clip doesn't create a scroll container, so it can't become an
       unintended scroll parent for sticky/fixed descendants — it just stops the translated-off-screen
       content from making the page horizontally scrollable. */
    .split{overflow-x:clip;}
  }
  @media (prefers-reduced-motion:reduce){
    *{scroll-behavior:auto;} .explore{transition:none;}
    .seq{opacity:1;visibility:visible;transition:none;}   /* same belt-and-suspenders as .work-hero below — the JS chain also fires with a zero stagger in this branch */
    .wln{transition:none;opacity:1;}   /* belt-and-suspenders: the JS reduced-motion branch already renders full text instantly */
    .work-hero{transition:none;opacity:1;transform:none;visibility:visible;}   /* same belt-and-suspenders — JS also adds .in immediately in this branch */
    /* NOTE: the menu glyph's hover sweep is NOT disabled from here. It is skipped entirely — no
       animation, no dash pattern — by only ever being declared under
       `(prefers-reduced-motion:no-preference)`; see the long note on .panel-toggle .ic-open above.
       An override in this block was the first attempt and it lost the cascade to the per-path
       :nth-child selectors, which is worth remembering before adding one here again. */
  }
