/* ─────────────────────────────────────────────────────────────
   skin-site-worlds.css — the fenn.chat homepage, place cut

   Load order (docs/site-directions.md): tokens.css → foundation.css
   → this file. tokens.css is read-only (D10).

   The site is fenn's world rather than a page. Each section is a
   diorama: a rounded panel of one of the funnel's ten skies, with
   that sky's own props, its own mascot fenn, one group's moment
   playing in it, and the outcome pinned like a postcard.

   Why panels and not one continuous sky (which is site/great-day's
   answer): these are *places*, and a place wants an edge. The page
   grey becomes the gallery wall, each world reads as something you
   are looking into, and — usefully — the ink problem solves itself,
   because a panel's contents sit on their own surfaces (bubbles,
   postcards) rather than naked on a gradient.

   What this file does NOT do (Rail 6): the funnel's fixed
   #scenes/#fg twins, D59's gutter system and the deck's morph engine
   stay with the deck. Here every world owns its own sky in normal
   flow, and one variable per visible world drives its parallax.

   The floor below the fold marks — nav, thread, footer, fair
   questions — is the same floor site/great-day uses. Duplicated per
   branch on purpose: one skin per direction, and directions never
   merge into each other (FOUNDATIONS.md). What was byte-identical
   across the copies has been promoted (2026-08-25, S17): the page
   reset, the shadow ramp, the display/lede scale, the button shell
   and the prop frame are foundation.css's now, and this file keeps
   only what is this direction's own. docs/site/components.md is the
   map.
   ───────────────────────────────────────────────────────────── */

/* The page reset, .skip and :focus-visible are foundation.css's
   (S17), behind `<body class="site">` — both pages this skin serves
   carry that class. The :root block that used to sit here was dead:
   a duplicate of the live one below, plus --pp/--drift-range/--drift,
   which no writer on this branch has ever set (that trio is
   site/great-day's page-progress driver; this direction drives one
   --wp per visible world instead). */

/* ─────────────────────────────────────────────────────────────
   1 · A world

   Sequential sections, each its own sky. Not the funnel's two fixed
   full-viewport layers driven by the deck's progress — those exist to
   serve a horizontal card deck (Rail 6).

   JS writes one variable per *visible* world, --wp, that world's own
   progress from 0 as it enters to 1 as it leaves. Everything inside
   moves off that.
   ───────────────────────────────────────────────────────────── */
:root {
  /* The reading column. Wide enough for a thread, never wide enough
     to read like a document. */
  --col: min(calc(100% - 2 * var(--page-pad-x)), 552px);
  /* --nav-h, the shadow ramp and the display/lede scale are
     foundation.css's now (S17). --nav-h in particular was measured
     here and hard-coded in three places before it had a name; the
     script's own `navEl.offsetHeight` still reads the same 64px at
     runtime. */
  --wtravel: 120px;             /* total prop travel across one world */

  /* The three plan worlds' sticky hold (§11): --hold-h is the held
     panel's own constant height (the funnel's "slide is a constant
     height, the card morphs inside it" rule, transplanted) and
     --hold-run is the extra runway scrolled through while it plateaus
     at --t 1. Neither depends on --t.

     Round 3, R2 — the panel is a framed diorama with EQUAL padding on
     all four sides, and it fits inside the fold. The old clamp
     (850/84vh+300/1100) was a free-standing height with no relation to
     the viewport it had to sit in: at 390×844 it resolved to 1009px in
     an 844px window, so the panel's bottom edge was 165px past the
     fold and the four gaps read 120/120/64/−14.

     One length drives all of it now. --panel-gap is the intended
     margin, --hold-h is what actually fits, and --panel-top / --panel-gap are the gaps
     that RESULTS — identical to --panel-gap whenever the cap isn't
     binding, larger (and still equal on four sides, because the width
     uses it too) when it is. No vw term anywhere, so a classic
     scrollbar cannot skew the horizontal gap against the vertical one.
     The cap only binds above ~1210px of viewport height. */
  /* The closing zone's two dissolves (R12). Both are fixed lengths
     rather than percentages of a box whose height depends on how the
     footer's links wrap, and both run on an eased alpha ramp rather
     than a two-stop linear one: a 400px linear fade has a visible
     start and stop where its derivative jumps, and on a 1440 screen
     it bands. Nine stops approximating a smoothstep put the
     derivative at ~0 on both ends and break the ramp into short
     enough segments that 8-bit rounding has nowhere to stack. */
  --ocean-fade: clamp(300px, 48svh, 540px);
  /* Longer than the 220-380px it was, not shorter (R15). Compressing
     the black is .get's own bottom padding's job — the darkening runs
     from .get's bottom edge upward, so its length decides how far up
     the descent STARTS, not how much flat black sits above the
     footer. And it had to reach further up, because shortening the
     zone moved .get-claim from 40% of the deep-ocean ramp to 28% of
     it, onto the brightest cyan the gradient makes: white on it
     measured 2.93:1, under the 3:1 that large text needs.

     R16.4 asked for less black once more, and this time both levers
     moved together — a tenth off the descent here, a third off the
     padding below. The pair moves the claim the other way: a shorter
     pad drops it FURTHER DOWN the darkening, which is the half of
     the ramp that gets darker, so the measured contrast rose at all
     four viewports instead of falling (390×844 6.92 → 7.91:1,
     375×667 5.79 → 6.62, 800×700 5.37 → 6.11 — the worst of the four
     — 1440×900 7.08 → 8.21). What came down is the empty run between
     the badge and the footer: 92.8 → 59.1px at 390 and 99 → 63px at
     1440, of which 39 and 41.5px are flat black by pixel decode.

     S18's second pass adds the Ask fenn field between the claim and
     the badge, and the +84px it costs (26 of margin, 58 of field) all
     lands BELOW the claim — so it slid the claim back up the ramp
     exactly the way R15's shorter zone did, and by almost the same
     amount. Measured before the retune: 7.91 → 3.70:1 at 390×844,
     6.62 → 3.36 at 375×667, 6.11 → 3.17 at 800×700, 8.21 → 3.75 at
     1440×900 — still over the 3:1 bar at all four, and with nothing
     left under it.
     The fix is the same 84px, added to the descent rather than taken
     off it: the white band, the lit water and the darkening then keep
     the lengths R12 and R16.4 tuned them to, and the field is what
     the section grew by. Written as the old expression plus a
     constant, because the field's height is a constant and not a
     fraction of the viewport. After: 5.98 / 5.22 / 4.92 / 6.13:1.

     One side effect, disclosed rather than hidden. The empty run
     between the badge and the footer is UNCHANGED — 59.1px at 390,
     63 at 1440, the numbers R16.4 landed on — but a longer descent
     reaches full black further from the bottom, so more of that same
     run is flat rather than still ramping: 39 → 53.5px at 390 and
     41.5 → 56.5 at 1440. The band a reader sees is the same height;
     what moved is whether its top rows are rgb(0,0,0) or rgb(4,4,4).
     The other lever for the same contrast is 84px off .get's own top
     padding, which would keep the descent where it was and spend the
     white dissolve's runway instead — R12's tuning, so it is Paul's
     call and not one to make while shipping a field. */
  /* A tenth shorter (Paul, 08-27, Bim on the alias: "reduce the amount
     of the dark blue gradient at the bottom just slightly"): 334/484
     → 300/436, the svh term with them. */
  --ocean-dark: clamp(300px, calc(30.6svh + 76px), 436px);
  /* The floor of the deep (S28, Paul 08-26): the closing darkens into
     the ocean's own navy, not into black. Not a token — tokens.css has
     no deep-ocean colour; this is the scene gradient's last stop
     (0,10,153) taken down to where white footer type still clears
     17:1. Hand-tuned, and hand-tuned against a palette that is the
     client's: re-check it on a rebrand — nothing upstream will. */
  --ocean-floor: #030b4c;
  /* How far the closing zone starts UNDER the FAQ (R15). Paul asked
     for a smaller FAQ and explicitly released the rule that the light
     page has to contain it: the ocean may begin rising beneath the
     section's lower part. The fade itself is untouched — R12 tuned
     its length and its nine stops — so this moves where it STARTS
     rather than how long it takes, and the last rows get a faint blue
     wash behind them instead of the section ending on flat grey. It
     lands in the FAQ's own overlay landing room, which is empty
     except when an answer is open, and an answer is opaque. */
  --ocean-under: clamp(120px, 20vh, 220px);

  --panel-gap: 22px;             /* == --page-pad-x, the page's own gutter */
  --hold-h-cap: 1100px;
  /* The stuck card clears the nav (S28, Paul 08-26: the panel sat 8px
     UNDER the 64px header at 1440, 42px under it on a phone). The top
     gap is the nav's height plus a breath of air; the other three gaps
     stay --panel-gap. --hold-h pays for the difference. */
  --nav-air: 8px;
  --panel-top: calc(var(--nav-h) + var(--nav-air));
  /* svh is the toolbar-EXPANDED height; iOS collapses the toolbar on the
     first scroll down and every hold sits below the masthead, so the
     panel was ~40-120px short of the fold Safari actually shows (S29,
     Paul's iPhone 08-27). dvh re-resolves only when a toolbar
     transition ENDS, and the panel is stuck by its top: it grows or
     shrinks from its own floor, and --hold-run pays the same delta at
     .world--hold's own bottom — nothing above the stuck point moves.
     Rejected: lvh, which would never relayout but would put the
     panel's floor under the toolbar whenever it is expanded, i.e. on
     every upward scroll. */
  --hold-h: min(var(--hold-h-cap), calc(100dvh - var(--panel-top) - var(--panel-gap)));
  --hold-run: 90svh;

  /* The band of sky the card leaves at the top of the panel, and
     --grow derived from it rather than set as a magic number: the card
     is exactly as tall as the panel allows at every viewport, which is
     what retires §13's short-viewport --grow override (the fan no
     longer overhangs the card, so there is nothing left for a short
     window to break).

     There is a head and no foot. The head is sky the world needs — it
     is where the mascot stands and where the folder's own fan
     breathes. The foot was 12-24px of it under the card, and the
     card's bottom edge is the drawn phone's crop line, so that band
     read as a strip of sky between a cut-off phone and the panel's
     floor — a rendering fault rather than a screen rising out of the
     world (Paul, live review, R10). The card's bottom edge IS the
     panel's floor now. Checked at the tightest viewport it produces:
     at 375 the screen is 229.6px in a 331px panel, so its bottom
     corners sit 10.7px inside the panel's own 40px corner arc and
     never overhang it. */
  --card-head: clamp(40px, 9vh, 96px);
  --card-h0: 132px;              /* the filed folder's own height */
  /* One cap on top of that, and it is the phone's: the columns stop
     widening at --wcard's own cap, so past ~720px of card the drawn
     screen can no longer be tall enough to reach the crop line and
     floats in the middle of it instead. A 1920×1080 window gets more
     sky rather than a taller card. */
  --card-h1: min(720px, calc(var(--hold-h) - var(--card-head)));
  --grow: calc(var(--card-h1) - var(--card-h0));

  /* How far the folder fans open (the multiplier on its 120px native
     plate), and how much of the stacked card the phone column takes
     below the text group. Both are per-surface: a 2.2× fan is 264px
     of folder, which a 390px phone's card cannot spend and a 1440px
     one can.

     S24 removes the cards' own .wcta door and gives the room it freed
     in .wtextcol back to this instead — 38%→42% here, the default for
     any viewport this tier's media queries below don't already touch
     (≥780px tall). Measured: --vis rises 0.577→0.644 at 390×844 and
     0.508→0.568 at 430×932, with 85px and 69px of .wtextcol slack (the
     centred folder/title/desc group's own spare room above the crop
     line) left over at each — plenty clear of the ≥8px floor. */
  --fan-max: 1.6;

  /* The card's two rows under 900px (S38, the contract's point 1). The
     text column is sized by its CONTENT and the phone column takes what
     is left, never under --phone-min — where --phone-band used to be a
     fixed share of the card with the text group CENTRED inside it, which
     is what let a two-line title spill out of the band both ways (#42:
     the description over the phone, the folder above the card's roof).
     --text-top is the text column's own top pad, and the folder is its
     first item, so it is also where the folder's roof sits: 4.2cqh of
     .wcard reads 28.3px at 390×844, which is S36's measured 36.4 less
     Paul's 8 (#41). It rides the card's own height, so a shorter card
     gets a shorter pad (21.5px at 375×667) instead of the same one. */
  --phone-min: 40%;
  --text-top: clamp(20px, 4.2cqh, 40px);

  /* The drawn screen (§11's .mini, R3). --phone-w is in cqw of .wcard
     so it tracks the card rather than the window; it is also the
     radius and island's own base, which is why it has to be a length
     and not a percentage — a percentage border-radius resolves per
     axis and would draw an ellipse on a 9:16 box. --phone-drop is how
     far below its rest position the screen starts, --phone-float the
     extra drift it keeps on through the plateau. --bezel is measured
     against the mascots' own rendered stroke (see .mini). */
  --phone-w: clamp(180px, 80cqw, 330px);
  --phone-drop: 96px;
  --phone-float: 26px;
  --phone-head: 14px;
  --bezel: clamp(7px, 1.35vw, 9px);
}

.worlds { position: relative; z-index: 1; }

/* No scroll snap on the document. S36 c put a proximity snap here as the
   "magnet"; it lasted a day. Paul, 08-28 (desktop): "it takes too much
   scroll to continue to the other world card" — measured with 120px wheel
   ticks at 1440×900: sixteen ticks in a row stalled at scrollY 1737, the
   end of brunch's hold, Chrome pulling every one of them back to a snap
   position, while the same ticks with the snap off advanced 120px each.
   A wheel ends a scroll at every tick, so a proximity snap turns each tick
   into a tug-of-war; touch only escaped it on momentum. See .world below. */

.world {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* The nav is sticky, so anything scrolled to lands under it —
     ?world= jumps included (site/great-day found this with its
     masthead's ascenders clipped). The 8px is slack on top of the
     band, not part of it: the band itself is --nav-h now. */
  scroll-margin-top: calc(var(--nav-h) + 8px);
  /* The "magnet" Paul asked for on 08-28 ("we lack smoothness and
     auto-magnetism") was tried as a proximity snap on the document with
     each hold as a snap point, and taken out the same day: a scroll that
     ends near a card settled onto it, but on a wheel EVERY tick ends a
     scroll, so leaving a card took a dozen tugs (measured — see the note
     above .world). The roulette deck can snap because its scroller is its
     own and the gesture is one swipe; a document with sticky holds
     cannot. What remains of the ask is the 140ms low-pass on --t in
     index.html: the card glides and settles, the page scrolls natively. */
  /* Each world declares its own progress so the props have a value
     before JS has ever run — a world below the fold that never gets
     observed still renders at its rest pose. */
  --wp: 0.5;
  /* --wp/--wtravel are this direction's names for the foundation
     prop frame's --p-progress/--p-travel (S17). --p-travel is bridged
     on the zone rather than on :root so .closing's own shorter travel
     resolves against its own --wtravel rather than against the page's.
     --p-progress is bridged on .wsky (below), since S30: --wp no longer
     inherits, and the bridge has to sit where the props are. */
  --p-travel: var(--wtravel);
}
/* The four scroll scalars are registered NON-inherited (S30, Paul
   08-27: the panels' zoom read laggy on Safari and Chrome). Measured
   over CDP invalidation tracing at 390×844, one frame of a hold's entry
   re-styled 189 elements: --t/--t2/--t3 were written on .whold and
   --wp on .world, and an inherited custom property changing on an
   ancestor makes the engine re-resolve every descendant — the drawn
   phone screens, hundreds of elements that never read them, included.
   Non-inherited, a write touches the element written and nothing
   below it, so the driver writes each scalar on the handful of elements
   that read it (index.html's writeT and its .wsky/.mini writes) and the
   screens sit out the morph. Two consequences to keep in mind:
   var(--t) on an element now reads that element's OWN value (initial 0),
   never an ancestor's — so a reader that isn't written is a reader at
   rest — and the foundation bridge --p-progress lives on .wsky, not on
   .world, or its own inheritance would carry the change back down the
   whole zone. --tp and --ta are the same story one level down: derived
   on .mini / .wtitle and read there, never below. */
@property --t  { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --t2 { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --t3 { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --wp { syntax: '<number>'; inherits: false; initial-value: 0.5; }
@property --tp { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --ta { syntax: '<number>'; inherits: false; initial-value: 0; }

/* The prop frame's progress, bridged on the sky (S30): --wp is written
   here by the driver, and --p-progress inherits from here to the props
   and no further. */
.wsky { --p-progress: var(--wp); }
/* The dioramas: inset, rounded, the page grey showing between them. */
.world--panel {
  width: var(--col);
  margin: clamp(18px, 4vw, 34px) auto;
  /* A diorama is the biggest raised surface on its screen, so it
     wears the card's clothes: --radius-card — the one non-token
     radius the system carries — and the card shadow. */
  border-radius: var(--radius-card);
  min-height: clamp(520px, 78vh, 760px);
  box-shadow: var(--card-shadow);
}
/* The masthead is edge to edge: an opening should not look like one
   more card in a stack. Exactly one viewport (Paul, 08-24) — the
   clamp this carried let a tall phone see a sliver of the first card
   before any scroll, which is the "one viewport" rule failing by a
   few dozen pixels. 100svh, the repo's own convention (.whold uses
   it below) rather than 100vh, so an iOS toolbar showing or hiding
   cannot change the masthead's height mid-scroll.

   And it starts at y=0, not y=--nav-h (round 3, R1). The nav is
   `position: sticky`, so it occupies flow and main.worlds began
   exactly one nav-height down the page; .wsky is `inset: 0` on this
   box, so it structurally could not paint above it and the top band
   of every load was bare body grey. Two consequences of the same
   bug: 100svh measured from --nav-h also ended one nav-height PAST
   the fold, so "exactly one viewport" was off by 64px as well.
   A negative margin (rather than switching the nav to
   `position: fixed`) is the smaller lever: it moves this one section
   and everything below it up together, leaves the nav in flow for
   every other page that shares this skin, and the script's own
   door-fade/nav-ink machine reads `navEl.offsetHeight` at runtime and
   is unaffected either way. The stage below pays the height back as
   padding so nothing lands under the nav. */
.world--open {
  width: 100%;
  margin: calc(-1 * var(--nav-h)) 0 0;
  border-radius: 0;
  min-height: 100svh;
}
/* The masthead spends a whole viewport leaving, and no other zone gives
   its props that much room, so it carries its own --p-travel rather
   than a diorama's (Paul, 08-25). Its --wp covers 0.5 → 1 over that
   exit — rest pose at load, as drawn — so the number here is twice the
   travel any prop actually shows: 170 buys the near hill 81px and the
   farthest cloud 8.5.

   Keyed to the homepage's own masthead. fair-questions.html shares
   .world--open, but its stage is a scrolling list rather than a
   one-viewport exit, so its props keep the diorama travel. */
.world--open[data-world="clear-sky"] { --p-travel: 170px; }

/* The four story worlds: a runway, not a panel. The section itself
   carries no cosmetics and no --t dependence (the funnel's own
   load-bearing rule: a morph must never resize the thing scroll is
   snapping/measuring against) — it is just scroll room. --whold is
   the sticky panel inside it, and it is what actually wears the
   diorama's radius and shadow now. */
.world--hold {
  min-height: calc(var(--hold-h) + var(--hold-run));
  isolation: auto;
  /* The base .world rule's overflow: hidden would make this runway a
     scroll container of its own — since it never actually scrolls
     internally, .whold's sticky position would resolve against its
     static box and never stick at all. Visible, so the containing
     block .whold sticks against is the real document scroller. */
  overflow: visible;
  /* A real, static gap between runways (Paul, 08-24: two panels could
     render touching) — margin, not padding, so adjacent .world--hold
     siblings collapse to one gap rather than stacking two. A card can
     never render outside its own runway's box (that is what sticky
     positioning is), so this margin is a hard floor on the on-screen
     distance between one card's release and the next one's entry —
     verified by the CDP scroll sweep (tools/cdp-worlds-check.mjs),
     not just asserted here. */
  margin-block: clamp(56px, 9vh, 120px);
}
/* .whold is now pure geometry — sticky offset, size, nothing else. It
   carries no cosmetics and, critically, no transform: the --t driver
   reads this box's own distance from stuck (index.html), so scaling it
   would feed its own output back into its input. .wpanel inside it is
   what wears the diorama and what scales. */
.whold {
  position: sticky;
  top: var(--panel-top);
  height: var(--hold-h);
  width: calc(100% - 2 * var(--panel-gap));
  margin-inline: auto;
  /* Visible, not hidden: a mascot may still break the frame (§10) —
     only the sky clips. */
  overflow: visible;
}
/* The whole world, minimised → rest → minimised again. Background
   included: the sky, its props, its mascot and the card are all inside
   this one wrapper, so the panel enters as a small complete diorama
   rather than as a frame with things popping inside it. 0.88 is the
   floor because a deeper zoom starts to read as a slide transition
   rather than as a world arriving. */
.wpanel {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-card);
  box-shadow: var(--card-shadow);
  display: grid;
  place-items: center;
  transform: scale(calc(0.88 + 0.12 * var(--t, 0)));
  /* Its own compositor layer (S30, Paul 08-27: the scale read laggy on
     Safari and Chrome). Without it the whole diorama — sky, props,
     mascot, card — was re-rasterised at every step of the 0.88→1 zoom;
     on its own layer the zoom is compositor work and only what really
     changes inside is repainted. The sky gets a layer of its own for
     the same reason, so the card growing on --t never repaints the
     gradient behind it. Measured over CDP tracing, a 180-frame entry
     at 390×844, software raster: RasterTask 686ms → 441ms with the
     three promotions (this, .wsky, .wstack); style and layout cost
     were unchanged, which is where a phone's remaining work is — the
     card's height is a size container and every --t step re-resolves
     .wtitle's container query. */
  will-change: transform;
}
.wpanel .wsky { overflow: hidden; border-radius: inherit; will-change: transform; }

/* The sky fills its world and nothing else. Gradients are the
   funnel's own, negative stop percentages intact (D49). Selectors are
   keyed to the plan each world now carries (brunch/laser/karaoke),
   not to the scene name, since a scene can move between plans (D49
   still owns the scene itself, in the funnel). */
/* overflow-anchor: none (08-27) — a sky is scenery and must never be
   the scroll anchor. Its props are lazy images with height: auto, so
   one that decodes after a jump grows from 0 to its box and Chrome's
   scroll anchoring "compensates" by moving the page: measured on the
   closing at 1126×854, a jump to the end landed 160px short of the
   floor until a second scroll, because the coral loaded on arrival.
   Excluding the sky leaves the anchoring to the content in front. */
.wsky { position: absolute; inset: 0; z-index: 0; isolation: isolate; overflow-anchor: none; }
[data-world="clear-sky"] .wsky {
  background: linear-gradient(0deg, rgba(242,242,242,0.4) 27.916%, rgba(150,224,255,0.4) 55.994%, rgba(0,180,255,0.4) 107.684%),
              var(--surface-primary);
}
[data-world="laser"] .wsky {
  background: linear-gradient(-0.971deg, rgba(145,167,247,0.9) -4.807%, rgba(190,150,255,0.75) 45%, rgba(236,224,255,0.7) 97.208%),
              var(--surface-primary);
}
[data-world="karaoke"] .wsky {
  background: linear-gradient(-0.971deg, rgba(54,141,255,0.8) -4.807%, rgba(0,43,255,0.8) 31.102%, rgba(0,0,238,0.8) 97.208%),
              var(--surface-primary);
}
[data-world="brunch"] .wsky {
  background: linear-gradient(0deg, rgba(255,241,223,0.5) 20%, rgba(160,179,241,0.55) 62%, rgba(120,150,240,0.6) 105%),
              var(--surface-primary);
}

/* The prop frame — the 420×912 artboard the props were drawn in, so
   --ex/--ey/--ew stay the funnel's own numbers, and the --er rotation
   R7 needed for the ocean's seaweed and coral — is foundation.css's
   now (S17). What stays here is the column it sits in: this one is
   100% of its own panel where site/great-day's is 100vw of the page,
   which is the whole difference between a diorama and a full-bleed
   sky. */
.p-col { position: absolute; inset: 0; margin-inline: auto; width: min(100%, 480px); }

/* One fenn per world, and four of the five have never been on a
   screen before. Sized by --im (ink as a fraction of its own box) and
   placed by --ic, both measured with tools/measure-mascot.mjs, which
   reproduces the funnel's own six. Only filter-free exports are used:
   eight of the twenty carry un-flattened Figma feGaussianBlur drop
   shadows, and brand.md forbids internal filters on a character
   because they rasterize soft on device. */
.wsky .fenn {
  position: absolute;
  --box: calc(var(--fink) / var(--im));
  width: var(--box);
  left: calc(var(--fx) - 0.5 * var(--box));
  top: calc(var(--fy) - var(--ic) * var(--box));
  transform: translate3d(0, calc((0.5 - var(--p-progress, 0.5)) * var(--p-travel) * 0.85), 0);
}

/* ── The living balloon (Paul, 08-27): "the hot-air balloon should be
   animated — it should float, the eyes following the mouse, moving a
   bit towards the mouse, a subtle and elegant magnetism; on click or
   tap it should blink." The masthead's mascot is the one inlined <svg>
   on the page (hot-balloon.svg verbatim, its own <g id="eyes"> kept as
   .eyes › .lids › .pupil), so its parts can be driven the way pool-cc's
   hero fenn is (origin/pool-cc:index.html, "Eyes follow the pointer").
   Three transforms on three nested groups, so none fights another or
   the parallax translate the svg itself carries as .wsky .fenn:
     .float   — a 7s rise-and-settle with a 1.2° lean, the balloon's own
                idle (SVG user units, so it scales with the mascot);
     .magnet  — index.html writes translate(x, y) frame by frame: the
                slow drift toward wherever the cursor is in the hero
                (a quarter of the hero each way, ~8s to arrive);
     .eyes    — the gaze, 140ms: the balloon sweeps −28…+10u across and
                ±8u up/down (its pupils are drawn right of the face, so
                the range is asymmetric to reach the far side); the card
                mascots ±13/±6 (index.html converts px to units);
     .lids    — pool-cc's blink loop (a double blink every 7.4s) and the
                one-shot blink a click or tap plays over it.
   Reduced motion keeps the gaze (a transition, not a loop) and drops
   the float and the blinks; .is-off (S30) pauses both loops once the
   masthead has left the viewport. */
.fenn--live { overflow: visible; cursor: pointer; }
.fenn--live .float { transform-origin: var(--fo, 51px 45px); animation: fenn-float 7s ease-in-out infinite; }   /* --fo/--lo: each mascot's own centres, keyed by data-mascot below */
.fenn--live .magnet { transition: none; }   /* index.html drives it frame by frame — the slow approach is the script's */
.fenn--live .eyes { transition: transform 140ms var(--ease-out); }
.fenn--live .lids { transform-origin: var(--lo, 65px 47px); animation: fenn-blink 7.4s infinite; }

/* Each mascot's own measured numbers, keyed by data-mascot (S37). They
   rode inline on the four <svg>s until the audit of 08-28: placement is
   the skin's job, and measure-mascot.mjs's output belongs beside the
   rules that read it. Byte-identical to the attributes they replaced —
   the balloon declares no --fo/--lo because its own centres ARE the two
   defaults above. --fink stays an ordinary declaration on purpose: the
   two tiers below override it with !important, which beat the inline
   style before and beat these the same way. */
.fenn--live[data-mascot="hot-balloon"] {
  --im: 1.1383; --ic: 0.5692; --fink: clamp(76px, 21vw, 148px); --fx: 78%; --fy: 24%;
}
.fenn--live[data-mascot="picnic-02"] {
  --im: 0.9983; --ic: 0.5533; --fink: clamp(60px, 16vw, 114px); --fx: 24%; --fy: 12%;
  --fo: 56px 78px; --lo: 56px 71px;
}
.fenn--live[data-mascot="laser-tag"] {
  --im: 0.9967; --ic: 0.3646; --fink: clamp(60px, 16vw, 114px); --fx: 76%; --fy: 12%;
  --fo: 60px 50px; --lo: 73px 45px;
}
.fenn--live[data-mascot="northern-lights"] {
  --im: 0.9975; --ic: 0.4988; --fink: clamp(84px, 22vw, 148px); --fx: 23%; --fy: 13%;
  --fo: 70px 70px; --lo: 71px 70px;
}

@keyframes fenn-float {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-4px) rotate(1.2deg); }
}
@keyframes fenn-blink {
  0%, 36%, 40%, 88%, 92%, 96%, 100% { transform: scaleY(1); }
  38%      { transform: scaleY(0.08); }
  90%, 94% { transform: scaleY(0.08); }
}
@keyframes fenn-blink-once {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(0.08); }
}
.world--open.is-off .fenn--live .float,
.world--open.is-off .fenn--live .lids,
.whold:not(.is-playing) .fenn--live .float,
.whold:not(.is-playing) .fenn--live .lids { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .fenn--live .float, .fenn--live .lids { animation: none; }
  .fenn--live .magnet { transition: none; }
}

/* ─────────────────────────────────────────────────────────────
   2 · What stands in a world

   Almost no naked prose. A world's content is bubbles and postcards
   — surfaces that carry their own ink — which is what lets the dark
   skies and the light ones use one set of rules.
   ───────────────────────────────────────────────────────────── */
.stage {
  position: relative; z-index: 2;
  /* The top pad is a reserved band of sky, and it is what the world's
     fenn stands in. Without it the fragment fills the panel and the
     mascot ends up behind a bubble — and D30's rule is that no fenn
     crosses a card. */
  padding: clamp(104px, 30vw, 210px) clamp(20px, 5vw, 40px) clamp(28px, 6vw, 46px);
  min-height: inherit;
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: 18px;
}
/* The masthead box now starts under the nav (R1), so the stage pays
   the band back as padding: the sky paints from y=0, the type never
   does. */
.world--open .stage {
  justify-content: center; gap: 0;
  padding-top: calc(var(--nav-h) + clamp(26px, 6vw, 44px));
}
/* The masthead fits the first screen on a short tablet (S38 Part D). Paul,
   08-28, #39 at 768×611: "check the size of the text. It looks too big. It
   moves all the components out of the view port. We have to keep everything
   contain within the viewport." Measured there: the doors' feet landed at
   630.9 against a 611px fold — 19.9px under it, with the whole .world--open
   box 676.9px tall.

   Keyed on (min-width: 700px) and (max-height: 700px): a phone is already
   handled by its own type scale, and this is the window that has a desktop's
   type in a phone's height. svh, not vh or dvh — the masthead is the FIRST
   screen, so the toolbar is showing and svh is the height it is actually
   given. The title takes a height cap, the stage gives back the air it
   reserves for a mascot that has room to spare here, and the lede drops to
   body-md. */
@media (min-width: 700px) and (max-height: 700px) {
  .world--open .display { font-size: min(var(--display-size), 12svh); }
  .world--open .stage {
    padding-top: calc(var(--nav-h) + 16px);
    padding-bottom: 20px;
    gap: 0;
  }
  .world--open .lede { font-size: var(--body-md-size); }
}

/* The masthead has to end without ending (R13). Its gradient already
   lands exactly on --surface-primary at the bottom — the 0deg ramp's
   first stop is rgba(242,242,242,0.4) at 27.9%, and below that the
   stop holds, so it composites to #f2f2f2 over the page's own colour.
   What was guillotined was the props: the hills and the low cloud band
   are placed as fractions of a 912 frame and .wsky clips them at the
   section's own edge, so the section ended on three cut silhouettes
   and then flat grey.

   The same eased ramp R12 uses for the ocean, run upward from the
   bottom edge in the page's own colour: the props dissolve into the
   page instead of being cut by it, and because the scrim's own colour
   IS what the gradient lands on, there is no boundary left to see.
   Inside .wsky and after .p-col, so it covers the props and nothing
   else — .stage is z-index 2 and paints over all of it. */
.world--open .wsky::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: clamp(190px, 34svh, 400px);
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(242, 242, 242, 1) 0%,
    rgba(242, 242, 242, 0.985) 11%,
    rgba(242, 242, 242, 0.93) 23%,
    rgba(242, 242, 242, 0.82) 35%,
    rgba(242, 242, 242, 0.64) 48%,
    rgba(242, 242, 242, 0.43) 61%,
    rgba(242, 242, 242, 0.25) 73%,
    rgba(242, 242, 242, 0.10) 86%,
    rgba(242, 242, 242, 0) 100%);
}

/* ─────────────────────────────────────────────────────────────
   3 · Type and doors
   ───────────────────────────────────────────────────────────── */
.display {
  font-size: var(--display-size);
  line-height: var(--display-line);
  letter-spacing: var(--display-track);
  font-weight: 860;
  text-wrap: balance;
}
/* 17ch (S20, unchanged by S23): "Do the " is 8 characters, so the
   prefix still holds its own row wherever the size lets it — the
   binding row is now the tail (see the SOURCE block below), not the
   prefix. text-wrap: wrap, not the base's balance — balance re-flows
   the whole h1 to even out its lines, so a span whose length changes
   on a timer would re-break the row above it and flap the h1's own box
   (the gate asserts that box is identical for all five words). The
   size is this masthead's own --display-size, untouched: same concept
   and motion as great-day, at this scale. */
.world--open .display { max-width: 17ch; text-wrap: wrap; }

/* PORTED from site/great-day@1f0d001:assets/skin-site-great-day.css:325-332 — verbatim.
   The visually-hidden sentence under the typed title, for AT. */
.vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}
/* /PORTED */

/* PORTED from site/great-day@1f0d001:assets/skin-site-great-day.css:922-945 — verbatim.
   The typed word (S20): its own row, never wrapped, holding one line of
   height even while it is empty between words. Colour carries which of
   the five moves is being named; the pen shows only while writing or
   erasing and never blinks (D8). Hues re-measured on this masthead's
   sky in the gate, not on white. */
.typed { display: block; white-space: nowrap; min-height: 1lh; }
/* The five hero moves, in the five hues that clear 3:1 on white at
   this size (measured on #fff, recorded in S19): pink 3.14, violet
   4.61, sky 3.13, green 3.07, red 3.47. --sender-11 is the page's
   accent and is not in the rotation; 03/04/05/07/09 do not clear 3:1
   and are not either. */
.typed[data-w="0"] { color: var(--sender-14); }
.typed[data-w="1"] { color: var(--sender-12); }
.typed[data-w="2"] { color: var(--sender-10); }
.typed[data-w="3"] { color: var(--sender-08); }
.typed[data-w="4"] { color: var(--sender-01); }
/* The pen. Present only while the word is being written or taken
   back, gone the moment it lands — and it never blinks, because a
   blink is a loop that says nothing (D8). */
.typed::after {
  content: "";
  display: inline-block;
  width: 0.07em; height: 0.78em;
  margin-left: 0.06em;
  vertical-align: -0.06em;
  border-radius: 1px;
  background: currentColor;
}
.typed[data-phase="hold"]::after { content: none; }
/* /PORTED */

/* SOURCE — the word row, from Figma 2472:43242 "handle that" (S22, Paul
   08-25): a VERTICAL gradient clipped to the type, an ANGULAR white
   outline 3px OUTSIDE at 192px (0.0156em), and a 0 5 20 glow at 30%.
   The five words keep the OG thumbnails' colour pairs (S21) — "coloured,
   for all of them" — run top to bottom now, as the sheet's own primary
   pair runs.

   The outline is the hard part. CSS can stroke text (-webkit-text-stroke)
   but only in one flat colour, and only CENTRED on the glyph edge — so
   the angular paint and the outside alignment are both built: the row
   wrapper's ::before repeats the word (content: attr(data-text), the
   machine mirrors it) as a white stroke of twice the weight, and the
   gradient word sits ON TOP of it, covering the inner half — what is
   left showing is exactly the outside 3/192. The angle is a conic
   mask on that layer alone: Figma's stops, 20% white around the sweep
   with two full-white spikes 169° apart. The stroke lives under the
   fill because a clipped-gradient fill is its element's own background
   and nothing inside that element can paint beneath it; the row's
   pseudo is a different element, and z-index orders the two.

   That took the second pseudo, so the caret (S21) is a real <i> in the
   row now: after the word, painted in the hue's first stop, blinking
   on a 1s step; reduced motion keeps it and drops the blink. The
   ported .typed block above stays verbatim — display, min-height and
   its own pen are all overridden here on specificity alone. */
.world--open .typed-row {
  position: relative;
  display: block;
  white-space: nowrap;
  min-height: 1lh;
}
.world--open .typed-row::before {
  content: attr(data-text);
  position: absolute; left: 0; top: 0;
  white-space: nowrap;
  color: transparent;
  -webkit-text-fill-color: transparent;
  -webkit-text-stroke: 0.03125em #fff;   /* 2 × 3/192: the fill covers the inner half */
  -webkit-mask-image: conic-gradient(from 281deg at 50% 50%,
    rgba(0, 0, 0, 0.2) 0% 41.1%, #000 47%, rgba(0, 0, 0, 0.2) 53.8% 92%, #000 97.4%, rgba(0, 0, 0, 0.2) 100%);
  mask-image: conic-gradient(from 281deg at 50% 50%,
    rgba(0, 0, 0, 0.2) 0% 41.1%, #000 47%, rgba(0, 0, 0, 0.2) 53.8% 92%, #000 97.4%, rgba(0, 0, 0, 0.2) 100%);
  pointer-events: none;
}
.world--open .typed {
  display: inline-block;
  position: relative; z-index: 1;
  min-height: 0;
  vertical-align: top;
  /* A clipped-gradient fill only paints inside the element's own box,
     and at line-height 1 the box stops at the em: the g of "things"
     lost its descender flat at the baseline row and the glow ended on
     a hard right edge (Paul, 08-26). Padding grows the paint box past
     the descender and the caret; the matching negative margin keeps
     the layout box — and every gate measurement — where it was. */
  padding: 0.1em 0.16em 0.22em 0.06em;
  margin: -0.1em -0.16em -0.22em -0.06em;
  background-image: linear-gradient(180deg, var(--w-a) 0%, var(--w-b) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 5px 20px var(--w-glow));
}
.world--open .typed::after { content: none; }
.world--open .typed[data-phase="hold"]::after { content: none; }
.world--open .typed-row[data-w="0"] { --w-a: #6e0040; --w-b: #4d0080; --w-glow: rgba(110, 0, 64, 0.3); }   /* cotton-candy */
.world--open .typed-row[data-w="1"] { --w-a: #3d0f8c; --w-b: #1a3aa8; --w-glow: rgba(61, 15, 140, 0.3); }  /* sunburst */
.world--open .typed-row[data-w="2"] { --w-a: #002ea8; --w-b: #1c66a8; --w-glow: rgba(0, 46, 168, 0.3); }   /* dawn-meadow */
.world--open .typed-row[data-w="3"] { --w-a: #0a4d2b; --w-b: #0f6b52; --w-glow: rgba(10, 77, 43, 0.3); }   /* misty-forest */
.world--open .typed-row[data-w="4"] { --w-a: #8f1030; --w-b: #a8440f; --w-glow: rgba(143, 16, 48, 0.3); }  /* desert-sunset */
.world--open .typed-caret {
  display: inline-block;
  width: 0.07em; height: 0.78em;
  margin-left: 0.06em;
  vertical-align: top;
  margin-top: 0.14em;
  border-radius: 1px;
  background: var(--w-a);
  animation: typed-blink 1s step-end infinite;
}
/* The tail (S23). "you talk about" is its own row under the slot at every
   width below 1280: the sentence is prefix / slot / tail, and the slot's
   width changes on a timer, so the tail must never share its line. */
.world--open .typed-tail { display: block; }
@media (min-width: 1280px) {
  /* Desktop reads "Do the karaoke night|" / "you talk about" (Paul, 08-26):
     the slot goes inline after the prefix; the tail keeps its own row.
     MEASURED at 1280 and 1440 (same 96px font-size at both, the clamp's
     own ceiling): "Do the karaoke night" is the longest line at 879.11px,
     the other four run 571.23–666.59px, none wraps at either width.
     (571.23px for "things" — S24 checker re-measurement corrected this
     from a mismeasured 594.97px; same method, same tier, DOM Range
     from h1.firstChild through .typed-caret at 1280×800.)

     A bare max-width is not enough. The masthead's text column is a flex
     item with align-items:center (:719), which sizes it to its widest
     child's own preferred width rather than stretching it — so with the
     row inline, h1's OWN preferred width now varies with the word (it is
     that widest child), and 22ch measures 1440.5px in this 860-weight
     display face (its "0" is unusually wide), well past every word's own
     width, so it never actually caps anything: max-width alone still lets
     the whole column jog sideways every ~2s as the word cycles — the
     "h1 box identical for all five words" gate check (typed) caught it.
     900px is a fixed width, not a cap: 879.11 + ~21px, comfortably past
     the longest word with room for the caret, small enough that the
     column still reads left of centre at both 1280 and 1440. */
  .world--open .typed-row { display: inline-block; vertical-align: top; }
  .world--open .display { width: 900px; max-width: 22ch; }
}
@keyframes typed-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* And it stops blinking once the masthead has left the viewport (S30):
   a running CSS animation costs a style pass every frame whether or
   not anyone can see it, and this one was still ticking under every
   plan card. .is-off is the observer's own leave signal. */
.world--open.is-off .typed-caret { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .world--open .typed-caret { animation: none; }
}
/* /SOURCE */
/* SOURCE — the YC credibility mark (S25), a masthead eyebrow above the
   claim. Non-interactive by decision: it is a <p>, not an <a>, and
   carries no .btn — it reads, it does not lead anywhere, so neither the
   buttons gate nor the 44px touch-target rule applies to it. The pill
   geometry (34px tall, 999px radius) is this branch's own placeholder,
   sized to sit comfortably above the h1 until Marc supplies the
   official mark. */
.credit {
  display: inline-flex; align-items: center; gap: 8px;
  height: 34px; padding: 0 12px 0 8px; border-radius: 999px;
  background: var(--surface-raised); box-shadow: var(--pill-shadow);
  font-size: var(--body-sm-size); font-weight: 510; color: var(--text-primary);
  margin-bottom: 16px;
}
.credit svg { width: 18px; height: 18px; flex: 0 0 18px; }
/* /SOURCE */
.lede {
  margin-top: 18px;
  font-size: var(--lede-size); line-height: 1.38; font-weight: var(--body-weight);
  color: var(--ink-support);
  max-width: 30ch;
}
.doors { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }

/* ─────────────────────────────────────────────────────────────
   2 · Nav — both doors, live in the first frame (the LA-38 lesson)

   Fully transparent now (Paul, 08-24): the funnel's own glass — fill,
   frost, the bright inset rim — is gone, because a diorama page reads
   better with nothing framing the top of it. Nothing left to catch a
   highlight, so the rim went with the frost rather than sitting on
   its own as a stray line. .nav--on-dark is the one adaptive hook:
   the script flips it while a dark world's own sky is passing
   directly behind the nav band. */
.nav {
  position: sticky; top: 0; z-index: 40;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px;
  padding: 10px var(--page-pad-x);
  color: var(--text-primary);
  transition: color 200ms linear;
}
/* The lockup is fenn.chat's own (Paul, 08-28, Bim #45: "fenn text and
   icons are too far from each other — use the already existing logo +
   fenn text from the current website, and just change the color of the
   text"). Measured off that site's nav: mark clamp(30px, 3vw, 36px), gap
   9px, word clamp(20px, 2.2vw, 24px) at 800 with −0.04em tracking. The
   mark stays our inline #fennmark (the same character, a vector); only
   the ink is this page's — it flips with .nav--on-dark like the links. */
/* S42 (Bim #48/#49): the mark is #fennmark-nav on a tight viewBox, so
   the box IS the ink. Before, the nav <svg> carried the symbol's own
   `21 9.5 …` viewBox and <use> applied that offset a second time — the
   drawing sat 11px left and 5px above its box at 36px (4px at the old
   21, which nobody saw), so the "9px" gap measured 22.7 to the ink. Paul
   asked for 8px less than what he saw: 15px, and now 15px is what it is. */
.nav .wordmark { gap: 15px; }
.nav .wordmark svg { width: clamp(30px, 3vw, 36px); aspect-ratio: 24.6 / 26; }
.nav .wordmark > span { font-size: clamp(20px, 2.2vw, 24px); font-weight: 800; letter-spacing: -0.04em; }
/* The mark itself needs no swap: the character is a white shape with
   a black line (the symbol's own fallback fill/stroke), which reads
   against any sky without help. Only plain text ink — the wordmark's
   own word and the "fair questions" link — needs to flip. */
.nav--on-dark { color: var(--text-primary-dark-only); }
.nav--on-dark .nav-links a:not(.cta):not(.cta-ghost) { color: var(--text-subtle-dark-fixed); }
.nav-links { display: flex; align-items: center; gap: 8px; }
.nav-links a:not(.cta):not(.cta-ghost) {
  padding: 12px 10px;
  font-size: var(--body-sm-size);
  font-weight: var(--body-weight);
  color: var(--ink-support);
}
.nav-links a:not(.cta):not(.cta-ghost):hover { color: var(--text-primary); }
/* Under 700px the text link moves to the footer and the closing beat;
   the masthead still carries both doors, so nothing is lost. */
.nav-links a:not(.cta):not(.cta-ghost) { display: none; }
/* The handoff (S27). On a nav that declares .nav--handoff the pair is
   driven by --nav-pair, which the scroll machine writes as 1 minus the
   masthead pair's own opacity and pulls back to 0 as the closing pair
   rises — so one pair crossfades into the next and two are never legible
   together. The default is 0 (hidden until the machine says otherwise,
   and hidden for good without JS, where the masthead and the closing
   still carry both doors). visibility, not display: the box keeps its
   place so the nav's layout never shifts, and it drops out of the tab
   order while invisible. fair-questions' nav carries .nav--handoff too
   now (S29): it draws no masthead pair of its own, so its script starts
   the nav pair on and only ever pulls it back for its own closing pair. */
/* They grow in, they don't fade in (Paul, 08-27, 17:39: "avoid
   cross-fading when appearing the buttons while scrolling — we can
   scale up with some ease"): --nav-pair drives scale 0.6 → 1 over the
   handoff's 140px, and the opacity is spent in the first 40% of it,
   so what the eye reads is the pair swelling into place, not a
   dissolve. The scroll IS the ease; no time-based transition, so the
   button contract (one transition list on every .btn) is untouched. */
.nav--handoff .nav-links .cta,
.nav--handoff .nav-links .cta-ghost {
  opacity: min(1, calc(var(--nav-pair, 0) * 2.5));
  scale: calc(0.6 + 0.4 * var(--nav-pair, 0));
  transform-origin: 50% 50%;
}
.nav--handoff:not(.nav--pair-on) .nav-links .cta,
.nav--handoff:not(.nav--pair-on) .nav-links .cta-ghost { visibility: hidden; pointer-events: none; }
/* And the text link holds the right edge while the pair is away (S28):
   translated by the pair's measured width, scaled by (1 − --nav-pair),
   so it slides left on the same crossfade the doors arrive on. */
.nav--handoff .nav-links a:not(.cta):not(.cta-ghost) {
  transform: translateX(calc((1 - var(--nav-pair, 0)) * var(--nav-pair-w, 0px)));
}

/* ─────────────────────────────────────────────────────────────
   4 · One button system (round 3, R5)

   Before this round the page had five button heights (39/44/51/54.5),
   two different accent gradients, an inline no-curve sparkle path on
   the card doors, no hover anywhere, and a .wcta with no transition
   at all. A visitor could not tell which of them meant the same
   thing, because none of them looked like the same object.

   One anatomy, two sizes, and since the 08-25 promote (S17) the
   anatomy is foundation.css's: .btn is the shell, .btn--nav the 44px
   header tier and .btn--primary the 54px call-to-action tier, and
   every door in this skin's markup carries one of the two. The shell
   also owns --ease-btn, the six shadow tokens and the hover/press
   states — the z-lift Paul asked for (R14). ui-contract.md §Buttons
   and §Motion are the contract.

   What is left here is what is this direction's own: which fill,
   which ink, and which rung of the shadow ramp each class rides. */

/* The primary door is foundation's .share-cta anatomy without its
   rim: the gradient, the glow, the sheen (ui-contract.md §Buttons). */
.cta {
  background: var(--gradient-primary);
  box-shadow: var(--btn-shadow);
  color: var(--text-inverted);
  text-shadow: 0 0 4px rgba(255, 255, 255, 0.5);
}
.cta:hover { box-shadow: var(--btn-shadow-hover); }
.cta:active { box-shadow: var(--btn-shadow-press); }

/* The secondary door: the raised surface itself, flat, on the house
   pill shadow. It carried a white→grey gradient and a 1px rim before;
   both went with the system, and the shadow is what separates it from
   the sky now. */
.cta-ghost {
  background: var(--surface-raised);
  box-shadow: var(--btn-ghost-shadow);
  color: var(--text-primary);
}
.cta-ghost:hover { box-shadow: var(--btn-ghost-shadow-hover); }
.cta-ghost:active { box-shadow: var(--btn-ghost-shadow-press); }

/* Keep the longer travel actions clear of the logo when they appear on scroll. */
.nav .wordmark { flex-shrink: 0; }
@media (max-width: 699px) {
  .nav { gap: 8px; }
  .nav .wordmark { gap: 8px; }
  .nav-links .btn--nav { padding-inline: 12px; }
}
@media (max-width: 380px) {
  .nav .wordmark > span { display: none; }
}

/* ─────────────────────────────────────────────────────────────
   5 · Desktop — the width the props were drawn for

   This is the direction that earns a wide window: the worlds become
   panoramas. Per-world placement, in the panel's own box; the
   funnel's --gut/--sw machinery is the deck's and does not transfer.
   ───────────────────────────────────────────────────────────── */
@media (min-width: 700px) {
  .nav-links a:not(.cta):not(.cta-ghost) { display: inline-flex; }
  .nav { padding-inline: clamp(22px, 4vw, 46px); }
  /* The gallery wall grows with the window: a 22px gutter reads as a
     phone's gutter, not as a frame, once there is room for one. The
     panel gets WIDER at the same time — the gutter is all that is
     subtracted from the full width now (R2), where the old
     --card-col capped it at 620/1200. */
  :root { --panel-gap: 34px; }
}
@media (min-width: 900px) {
  :root {
    --col: min(calc(100% - 96px), 1080px); --wtravel: 190px;
    --panel-gap: 56px;
    --nav-air: 24px;
    --hold-run: 100vh;
    --fan-max: 2.2;
    /* 46cqw is the half-card the phone column occupies; the 56px is
       that column's own two insets, so the screen fills ~92% of it at
       every card width this tier produces. */
    --phone-w: min(calc(46cqw - 56px), 520px);
    --phone-drop: 130px;
  }
  .world--open[data-world="clear-sky"] { --p-travel: 240px; }
  .world--panel { min-height: min(660px, 82vh); }
  .p-col { width: 100%; }
  .world--open .stage { align-items: center; }
}

/* ─────────────────────────────────────────────────────────────
   6 · Reduced motion

   foundation.css kills animations and transitions. What it cannot
   reach is the prop drift, because that is a live custom property
   rather than an animation — the same hole D57 found in the deck's
   parallax. Frozen at the rest pose here, and the entries are simply
   already in.
   ───────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .world, .closing, .wsky, .mini { --wp: 0.5 !important; }
  /* Sticky holds remain — scroll distance isn't motion — but the fold
     morph is, so every card renders fully expanded and readable. Set
     on .whold rather than .wcard since R2: .wpanel's own scale reads
     --t too, and it is the card's ancestor, not its descendant. All
     three of the staggered scalars, since R6 — final poses only, and
     a stagger left at 0 is not a final pose, it is an invisible one. */
  .whold, .wpanel, .wcard, .wfolder, .wstack, .wph, .wtitle, .wdesc, .mini {
    --t: 1 !important; --t2: 1 !important; --t3: 1 !important;
  }
}

/* ─────────────────────────────────────────────────────────────
   10 · fair questions

   <details> and nothing else: it opens with no JS, it is keyboard
   operable for free, and find-in-page inside a closed answer opens
   it — which an ARIA accordion has to be taught to do. `name="fq"`
   makes them exclusive the way a radio group is, still with no JS.
   Adding a question is adding an <li>.
   ───────────────────────────────────────────────────────────── */
.sky--only { opacity: 1; }

/* Round 3, R8. The rows were 52.8px closed, had no hover at all, and
   the only thing that moved when one opened was the chevron: the
   answer appeared instantly and the row it came out of never
   acknowledged the click. Bigger rows, a hover, an animated open, and
   an open row that reads as lifted off the stack.

   The animation is progressive enhancement rather than a script for
   everyone. Where the browser can interpolate to `auto`
   (interpolate-size + ::details-content — Chrome 131+ has both;
   Safari 26 has the pseudo and not the keyword, see S29) it does the
   whole thing itself, both directions, including the close that
   name="" exclusivity fires on a sibling. Everywhere else a
   twelve-line tween in the page handles the open (see the script);
   the close stays instant there, and the rows still work with no JS
   at all, which is why they are <details> in the first place. */
.qa { margin-top: 30px; display: flex; flex-direction: column; gap: 10px; }
/* The rows are a fixed stack. An answer opens as an OVERLAY on top of
   the rows under it, never by growing the row it came out of (R11):
   the section's own height is identical open and closed, and the
   closing beat below it never moves. name="" exclusivity is what makes
   that safe — one answer is open at a time, so what it covers is only
   ever hidden while it is being read.

   The rim went with it. It used to be a 1px inner line on the row's
   own border box, and the overlay below the summary is a separate box:
   the two could not be made to line up to the pixel, and the shadow
   already does the separating (§4 made the same call for the
   buttons). */
.qa details {
  position: relative;
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.86);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  box-shadow: var(--pill-shadow);
  /* Visible, not hidden: the answer overlay lives outside this box.
     What clips the animation is the overlay's own overflow. */
  overflow: visible;
  /* ONE curve for the whole open (S38 Part E, Paul 08-28 #36 at 1126×854:
     "When expanding we can notice the lower part of the row is cut. The
     transition from a shape to the other one is not smooth."). The lift
     used to run on --ease-spring while the answer's own height ran on
     --ease-out over the same 240ms, and recorded at 40ms the two do
     different things: the height rises monotonically to 82.59px and stops,
     while the scale goes past its target to 1.0652 at ~167ms and comes back
     to 1.06 by ~249ms. A panel that grows, overshoots and settles while the
     answer inside it only grows is two shapes, which is what he is
     describing. The overshoot was deliberate once (a small spring reads as
     a response to a click) and it is the thing being asked about, so the
     transform joins the rest of the list on --ease-out. border-radius rides
     it too, so the summary's bottom corners square off in step with the
     answer rather than snapping the moment [open] flips. */
  transition: box-shadow var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out),
              border-radius var(--dur-base) var(--ease-out),
              opacity var(--dur-base) var(--ease-out),
              background-color var(--dur-fast) linear;
}
/* What the overlay covers recedes. Without this the row directly under
   an open answer shows its own bottom third sticking out below the
   panel — a horizontally sliced question, which reads as a rendering
   fault rather than as one card in front of another. Everything after
   the open row dims together, so the panel is plainly the thing in
   front and the stack behind it is plainly still there. */
.qa li:has(details[open]) ~ li details { opacity: 0.3; }
/* Hover only where a pointer can hover (S30, Paul 08-27): on a touch
   screen :hover sticks to whatever was last tapped, so a tapped row
   kept the lifted white of a hover after it closed. (hover: hover) is
   false on iOS and true under the gate's phone emulation, so the faq
   hover assertion still runs there. */
@media (hover: hover) {
  .qa details:hover {
    background-color: rgba(255, 255, 255, 0.96);
    box-shadow: 0 8px 18px rgba(173, 179, 189, 0.26),
                0 26px 30px rgba(173, 179, 189, 0.18);
  }
}
/* Open sits on a higher plane than the rows either side of it. 1.06,
   not the 1.02 it was (R16.2): a 2% lift on a 74px row is under a
   pixel and a half of growth, which the eye reads as the row settling
   rather than as it coming forward, and the shadow was doing all the
   work on its own. 6% is 4.4px of vertical growth and 43px of
   horizontal at the widest column — plainly in front, and still
   inside the section's own side padding at every viewport, so nothing
   clips. The shadow went with it rather than staying put; a card that
   grows without casting further reads as a scaling bug. */
.qa details[open] {
  z-index: 1;
  transform: scale(1.06);
  background-color: var(--surface-raised);
  box-shadow: var(--qa-shadow-open);
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}
:root {
  --qa-shadow-open: 0 14px 30px rgba(173, 179, 189, 0.34),
                    0 44px 56px rgba(173, 179, 189, 0.26);
}
.qa summary {
  display: flex; align-items: center; gap: 12px;
  /* No grey tap flash on iOS (S30, Paul 08-27): Safari paints its own
     highlight over anything tappable, and on a row that lifts and
     opens it read as a grey overlay. The row's own open state is the
     acknowledgement. Same pair the Ask field already carries. */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  /* 44px was the floor when the row was the target; ~60px is the
     size the row wants to be next to a 54px button. */
  min-height: 56px;
  padding: 19px 22px;
  cursor: pointer;
  list-style: none;
  font-size: var(--body-md-size);
  line-height: 1.3;
  font-weight: 700;
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary::after {
  content: "";
  flex: 0 0 auto;
  margin-left: auto;
  width: 11px; height: 11px;
  border-right: 2.5px solid var(--ink-support);
  border-bottom: 2.5px solid var(--ink-support);
  transform: rotate(45deg) translateY(-2px);
  transition: transform var(--dur-base) var(--ease-out);
}
.qa details[open] summary::after { transform: rotate(225deg) translateY(-2px); }
.qa .a { padding: 0 22px 20px; }
/* interpolate-size is scoped to the rows rather than set on :root:
   it is inherited, ::details-content picks it up from here, and
   nothing else on the page should quietly gain the ability to
   animate to a keyword. */
/* The overlay itself, in whichever of the two boxes this browser
   gives us. It carries the row's own surface so the summary and the
   answer read as one card, and it is opaque so the rows it covers do
   not print through. top: 100% is the summary's bottom edge — the
   row's height IS the summary's, since this box is out of flow. */
.qa details::details-content,
.qa details > .a {
  position: absolute;
  top: 100%; left: 0; right: 0;
  background: var(--surface-raised);
  border-end-start-radius: var(--radius-md);
  border-end-end-radius: var(--radius-md);
  box-shadow: var(--qa-shadow-open);
  overflow: hidden;
}
/* Two features, two gates (S29, Paul's iPhone 08-27). Safari 26 draws
   ::details-content but has no interpolate-size, so the two @supports
   below are NOT one: where the pseudo exists it IS the overlay and the
   div inside it goes back to being flow content — gated on the
   selector, because that is the fact it depends on. Gating it on
   interpolate-size (as it was) left Safari with BOTH boxes absolute:
   .a at top:100% of an empty overflow-hidden pseudo, clipped to
   nothing — a lifted row, dimmed rows under it, and no answer. */
@supports selector(::details-content) {
  .qa details > .a { position: static; background: none; box-shadow: none; overflow: visible; }
}
/* And only where the browser can interpolate to `auto` does the
   pseudo animate itself; elsewhere the page's own tween drives .a's
   height and the pseudo, overflow hidden, grows with it. */
@supports (interpolate-size: allow-keywords) {
  /* Scoped to the rows rather than set on :root: it is inherited,
     ::details-content picks it up from here, and nothing else on the
     page should quietly gain the ability to animate to a keyword. */
  .qa details { interpolate-size: allow-keywords; }
  .qa details::details-content {
    block-size: 0;
    transition: block-size var(--dur-base) var(--ease-out),
                content-visibility var(--dur-base) allow-discrete;
  }
  .qa details[open]::details-content { block-size: auto; }
}
.qa .a p {
  font-size: var(--body-md-size);
  line-height: 1.44;
  font-weight: var(--body-weight);
  color: var(--ink-support);
}
.qa .a p + p { margin-top: 10px; }
.inline {
  font-weight: 700;
  color: #1f5fbf;              /* 5.62 on white */
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* ─────────────────────────────────────────────────────────────
   7 · The signpost world — fair questions

   The sub-page is one world rather than a document, so it belongs to
   the same place as the rest. misty-forest, its own leaves, and the
   accordion stacked in it.
   ───────────────────────────────────────────────────────────── */
[data-world="misty-forest"] .wsky {
  background: linear-gradient(0deg, rgba(242,242,242,0.4) 27.916%, rgba(129,193,127,0.4) 55.994%, rgba(0,213,255,0.4) 107.684%),
              var(--surface-primary);
}
.stage--qa {
  display: block;
  padding: clamp(30px, 7vw, 56px) clamp(20px, 5vw, 40px) clamp(150px, 20vh, 210px);
  /* Same nav-band repayment as the masthead's own stage (R1): this
     page's .world--open is pulled up under the nav too, and unlike
     the masthead this stage is block-flow, so its first line would
     land in the band without it. */
  padding-top: calc(var(--nav-h) + clamp(30px, 7vw, 56px));
  max-width: 640px;
  margin-inline: auto;
}
.world--open:has(.stage--qa) { min-height: 100vh; }


/* ─────────────────────────────────────────────────────────────
   8 · The solve, made visible (the value pass, 08-24)

   The did chip fenn's result once landed as (channel surface, pill
   shadow, his own sparkle, D17) moved with the thread it was part of
   — the wcard apparition (§11) is what makes the outcome visible in
   the three plan worlds now.
   ───────────────────────────────────────────────────────────── */
.lede em {
  font-style: normal;
  font-weight: var(--h4-heavy-weight);
  background-image: linear-gradient(96deg, var(--accent) 0%, var(--accent-alt) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 2px 8px rgba(80, 177, 249, 0.25));
}

/* ─────────────────────────────────────────────────────────────
   10 · Breaking the frame (Paul: push the layout)

   The mascots stop living inside their panels: a fenn can stand on
   the panel's edge, half out of the world — the OG cards' own move,
   where the mascots overlap the type. The panel keeps clipping its
   sky; only the fenn is freed.
   ───────────────────────────────────────────────────────────── */
.world--panel { overflow: visible; }
.world--panel .wsky { overflow: hidden; border-radius: inherit; }
.world--panel { isolation: auto; }
.wsky { border-radius: inherit; }
/* No .fenn--free any more (08-27): the apple stands in brunch's sky
   under the same rules as laser's and karaoke's mascots. */
/* The same floor for the mascots that live in a held panel's sky.
   Theirs is placed as a fraction of the panel (--fy), which used to
   sit well clear of the nav because .whold's own sticky offset had a
   hard 64px floor; the panel's gutter is 22–56px now, so 13% of it
   lands karaoke's northern-lights fenn in the band. Three terms
   above the band, all measured rather than guessed: ~40px of prop
   drift still running while --t plateaus (--wtravel × 0.85 × the
   half-range --wp covers during a hold), ~6px of sticky slop that
   --t's own 0.999 plateau threshold tolerates, and 10px of margin. */
.world--hold .wsky .fenn {
  top: max(calc(var(--fy) - var(--ic) * var(--box)),
           calc(var(--nav-h) + 56px - var(--panel-top)));
}

/* ─────────────────────────────────────────────────────────────
   11 · The apparition

   Paul's review round (08-24): the single flowing column splits into
   two — a text column (folder, then title, then a two-line
   description of the one feature the card is about) and a phone
   column (one big drawn screen, cropped at the card's own bottom
   edge), alternating sides card by card. Still the Plans funnel's own
   single-scalar morph (skin-roulette.css §"The card, morphing on
   --t"): minimised it is a ~132px folder — the funnel's own
   plate-back tab, three prints, front pocket (skin-roulette.css:
   747-815), verbatim, wrapped in one scale so the whole assembly
   reads at world scale instead of deck scale. Expanded, the folder
   fans open at the head of the text group, and the phone column's
   single screen rises through the card on the other side.

   --t comes from the sticky hold (§1's .whold), not from an
   IntersectionObserver "in" class: it is the scroll position itself,
   continuous, entry and exit the same curve reversed. Both columns
   are absolutely positioned, like every apparition part before them
   — .wcard's own height stays a straight function of --t, never of
   its children's content (the funnel's own rule, transplanted), so a
   column's content can never resize the thing scroll is measuring
   against.

   Round 3, R4: the text column is a vertically centred flex group
   (folder, then title + description, then the door) rather than a set
   of boxes pinned to fixed offsets from the card's top. The old
   `.wtext { top: 78px }` left 294px of dead space under the
   description at 1440, and the 78px itself only existed to clear a
   fan that rose 228.8px above the card. The fan is inside the group
   now, so neither number is needed and the group sits in the middle
   of its own column at every card height. */
.wcard {
  position: relative;
  z-index: 2;
  width: min(440px, calc(100% - 44px));
  height: calc(var(--card-h0) + var(--grow) * var(--t, 0));
  /* Anchored to the panel's floor, not centred in it: the phone's crop
     line is the card's own bottom edge, and it wants to BE the ground
     rather than to hover above it. It also means the filed folder
     rises out of the world's floor as --t climbs, which is the deck's
     own arrival read at world scale. */
  align-self: end;
  contain: layout style;
  /* A size container, not just layout/style: the @container query on
     .wtitle below reads .wcard's own height (a pure function of --t,
     S12) to tell the still-filed range from the huge expanded one
     (B2 fix). */
  container-type: size;
  container-name: wcard-morph;
  /* A grid, not two absolute bands (S38). Row 1 is the text column at
     its content's height, row 2 is the phone column at whatever is left
     — never under --phone-min. The ≥900px block puts it back to `block`
     with both columns absolute, because side by side they each want the
     card's full height. */
  display: grid;
  grid-template-rows: auto minmax(var(--phone-min, 40%), 1fr);
}

/* The text column: mobile stacks it above the phone column (one
   column, the funnel's own vertical idiom); ≥900px docks it to one
   side, .world--mirror flipping which (§5 below). It spans its own
   region top to bottom and centres its contents in it — the column
   itself still carries no background, and .wcard's height is still a
   pure function of --t, so nothing in here can resize what scroll is
   measuring against. Under 900px it is the grid's first row and its
   height is its own content's (S38); --phone-band, the fixed share it
   used to be given with its group centred inside, is gone. */
.wtextcol {
  position: static;
  min-height: 0;
  padding: var(--text-top) 22px 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  gap: clamp(12px, 2vh, 22px);
}

/* The folder: skin-roulette.css's .stack/.plate-back/.plate/.ph*
   verbatim (same clip-path, same pile/fan coordinates), wrapped in a
   scale that ranges 1.1→--fan-max instead of the deck's 0.533→1 —
   120px of native plate height reads at 132px filed and world scale
   expanded, without redrawing a single coordinate. The horizontal
   travel (30.8px) is the deck's own 14 at 2.2×.

   .wfolder is the flex item; .wstack floats inside it. The wrapper's
   height is exactly the scaled plate's (120 × the scale), so the
   centred group's own height tracks the fan as it opens instead of
   the fan escaping its layout box. The vertical travel the deck has
   (−104, −228.8 here) is gone with it: the fan used to rise to clear
   a top-pinned title, and there is no top-pinned title any more. */
/* PORTED from assets/skin-roulette.css:697-808 — verbatim except
   s/.stack/.wstack/, s/.plate/.wplate/, s/.ph/.wph/, .wfolder, .wstack.
   The two named selectors are the whole divergence and both are S6: the
   fan opens to --fan-max here instead of the deck's fixed 0.533→1, and
   it needs a flex-item wrapper whose height tracks the scale, so
   .wstack's left/top/transform are re-derived and .wfolder is new. Every
   coordinate, the clip-path and the three-layer cool-cast shadow are the
   funnel's own, unchanged. Divergence is a decision; record it as S-n before you write it. */
.wfolder {
  position: relative;
  flex: 0 0 auto;
  width: 151.3px;
  height: calc(120px * (1.1 + (var(--fan-max, 1.7) - 1.1) * var(--t, 0)));
}
.wstack {
  position: absolute;
  width: 151.3px; height: 120px;
  transform-origin: top left;
  /* The plate sits 15.6px inside the stack and the stack scales from
     its top-left, so −15.6px × the scale puts the PLATE's left edge on
     the text column's own at every --t (S38, #41: "drifted on the left
     by 6px" — measured 12.1px at --t 1). The old −30.8px × --t was the
     deck's fan-out travel, which is a different job. The ≥900px block
     restores that travel for the desktop, where the card is two columns
     and the folder is not standing on a stacked text column's edge. */
  left: calc(-15.6px * (1.1 + (var(--fan-max, 1.7) - 1.1) * var(--t, 0)));
  top: 0;
  transform: scale(calc(1.1 + (var(--fan-max, 1.7) - 1.1) * var(--t, 0)));
}
.wplate-back,
.wplate {
  position: absolute;
  left: 15.6px;
  width: 130.84px;
  border-radius: 17.1px;
}
.wplate {
  box-shadow: 0 15.5px 31px rgba(0, 0, 0, 0.1),
              inset 0 1px 0 rgba(255, 255, 255, 0.55),
              inset 0 -6px 12px -6px rgba(34, 120, 160, 0.18);
}
.wplate-back {
  top: 0; height: 120px;
  /* The folder shape itself, straight off the funnel's own path — a
     level tab for the first 53px, then an 8px step for the rest. */
  clip-path: path('M0 25.04C0 17.68 0 14 1.11 11.07C2.85 6.48 6.48 2.85 11.07 1.11C14 0 17.68 0 25.04 0C27.01 0 27.99 0 28.96 0.11C30.46 0.28 31.94 0.65 33.35 1.21C34.25 1.57 35.12 2.03 36.85 2.96L40.38 4.85C42.64 6.05 43.77 6.66 44.96 7.08C46.02 7.46 47.11 7.74 48.22 7.9C49.47 8.09 50.75 8.09 53.31 8.09L103.42 8.09C113.02 8.09 117.81 8.09 121.48 9.95C124.71 11.6 127.33 14.22 128.97 17.45C130.84 21.11 130.84 25.91 130.84 35.51L130.84 92.58C130.84 102.17 130.84 106.97 128.97 110.64C127.33 113.87 124.71 116.49 121.48 118.13C117.81 120 113.02 120 103.42 120L27.42 120C17.82 120 13.03 120 9.36 118.13C6.13 116.49 3.51 113.87 1.87 110.64C0 106.97 0 102.17 0 92.58L0 25.04Z');
  filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.10))
          drop-shadow(0 0 0.6px rgba(34, 120, 160, 0.45));
}
.wplate { top: 72.8px; height: 47.3px; }
.wplate-back {
  background:
    linear-gradient(105deg, rgba(255, 255, 255, 0.38) 0 14%, rgba(255, 255, 255, 0) 46%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.5) -17%, rgba(242, 242, 242, 0.5) 100%);
}
.wplate {
  background:
    linear-gradient(105deg, rgba(255, 255, 255, 0.4) 0 16%, rgba(255, 255, 255, 0) 44%),
    linear-gradient(0deg, rgba(255, 255, 255, 0.2) 71%, rgba(242, 242, 242, 0.2) 156%);
}
/* Pile (--px/--py/--pr) to fan (--fx/--fy/--fr), the deck's own
   numbers, interpolated on the same --t the card morphs on. */
.wph {
  position: absolute;
  left: 0; top: 0;
  border: 5.5px solid var(--brand-white);
  border-radius: 9.6px;
  object-fit: cover;
  background: var(--surface-primary);
  box-shadow: 0.7px 1.4px 3.4px rgba(34, 120, 160, 0.10),
              3.4px 4.8px 5.5px rgba(34, 120, 160, 0.09),
              8.2px 10.3px 7.5px rgba(34, 120, 160, 0.05);
  transform:
    translate(calc(var(--px) + (var(--fx) - var(--px)) * var(--t, 0)),
              calc(var(--py) + (var(--fy) - var(--py)) * var(--t, 0)))
    rotate(calc(var(--pr) + (var(--fr) - var(--pr)) * var(--t, 0)));
}
.wph-back  { width: 71.4px; height: 71.4px; --px: 40px; --py: 19px; --pr: -3deg; --fx: 5px;  --fy: 27px; --fr: -19deg; }
.wph-mid   { width: 84px;   height: 84px;   --px: 33px; --py: 12px; --pr: 2deg;  --fx: 37px; --fy: 0px;  --fr: -5deg; }
.wph-front { width: 84px;   height: 84px;   --px: 35px; --py: 14px; --pr: 0deg;  --fx: 70px; --fy: 16px; --fr: 2.5deg; }
/* /PORTED */
/* The fan's stack on its own layer too (S30) — the block above is
   ported and stays verbatim, so the promotion sits here beside it: its
   scale and slide on --t become compositor work, and the two
   drop-shadows on the clip-path plate under it stop re-rasterising at
   every frame of the fan. */
.wstack { will-change: transform; }
/* Glass, not tint (Paul, 08-27, Bim on the alias: "the folder material
   should have a glass effect — currently it's just transparent"): the
   sky and its props blur through the plates, the same recipe as the
   nav's 14/1.4 at a lighter 10px because the plates scale 1.1→2.2 on
   --t and the blur scales with them. Beside the ported block, not in
   it — the block stays verbatim for `ports`. */
.wplate-back, .wplate {
  -webkit-backdrop-filter: blur(10px) saturate(1.3);
  backdrop-filter: blur(10px) saturate(1.3);
}
/* The shuffle (Paul, 08-27, Bim on the alias: "add click effect on the
   folder and same for mobile tap — it should switch the image in
   front: the third goes into position 1, 1 to 2 and 2 to 3, a smooth
   animation involving position, rotation and a bit of scale to
   simulate a z-order effect"). index.html rotates the three .wph-*
   classes on click; the pile/fan coordinates travel with the class,
   so each print glides to its next slot. Stacking follows the class
   too (the ported block relies on DOM order, which no longer says who
   is in front). The transition is scoped to the ~600ms of a shuffle
   (.is-shuffling) so the fan's own --t morph stays scroll-driven; the
   print coming forward lifts on its way (scale, its own shadow
   deepening) so the swap reads as a card pulled from the back. */
.wstack { cursor: pointer; }
.wph-back  { z-index: 1; }
.wph-mid   { z-index: 2; }
.wph-front { z-index: 3; }
.wplate    { z-index: 4; }   /* the pocket stays in front of every print (Paul, 08-27, pin 21) */
.wstack.is-shuffling .wph { transition: transform 620ms var(--ease-spring); }
.wstack.is-shuffling .wph.is-lifting { animation: wph-lift 620ms var(--ease-out); }
@keyframes wph-lift {
  0%   { scale: 1;    filter: drop-shadow(0 0 0 rgba(34, 120, 160, 0)); }
  45%  { scale: 1.12; filter: drop-shadow(0 10px 12px rgba(34, 120, 160, 0.22)); }
  100% { scale: 1;    filter: drop-shadow(0 0 0 rgba(34, 120, 160, 0)); }
}

/* The title and the description, the second item in the centred
   group. No longer pinned or scaled: the old box carried a crossing
   lerp and a 0.45→1 scale purely to dodge a fan that swept over the
   top of it, and both are gone with the fan's own rise. What is left
   is an arrival — a small rise and a fade, on its own stagger (§16) —
   which costs no layout and composes with .wpanel's own zoom instead
   of fighting it as a second, differently-timed scale would. */
.wtext { align-self: stretch; }
.wtitle, .wdesc {
  transform: translateY(calc((1 - var(--ta, 0)) * 14px));
  opacity: var(--ta, 0);
}
/* --t2 is the text's own arrival, eased in JS after its offset rather
   than remapped off a --t that has already spent half its curve (R6).
   The description takes one small further step off the same scalar so
   the line lands under the title rather than with it — one shift on an
   already-eased curve is a delay, which is all this needs. */
.wtitle { --ta: var(--t2, 0); }
.wdesc  { --ta: clamp(0, calc(var(--t2, 0) * 1.35 - 0.35), 1); }
/* Sized off .wcard's own container box, not the viewport: the column
   is half the card at ≥900px and the whole of it below, so a vw-based
   size put a 56px title in a 296px-wide column at the 800×700 corner
   and wrapped "Bottomless" onto three lines. */
.wtitle {
  margin: 0;
  font-size: clamp(28px, 11cqw, 40px);
  line-height: 1.04;
  letter-spacing: -0.03em;
  font-weight: var(--h4-heavy-weight);
  color: var(--text-primary);
  text-wrap: balance;
}
/* Below the title: two lines on the one feature the card is about,
   fenn's own voice (brand.md). Invisible until the card is mostly
   open (the same 1.4t-0.4 gate the app surface and the door use), so
   there is nothing to clip while still filed — only .wtitle needs the
   ellipsis safety net below. max-width holds the wrap to two lines
   regardless of column width: greedy-wrapped by hand at a ~28-char
   line (ch is the "0" advance, close enough to this copy's own
   average glyph width to trust) each of the three descriptions comes
   in at 2 lines — "fenn checks afternoons, books the one that works."
   is the long one, 49 characters over "fenn checks afternoons," (23)
   then "books the one that works." (25); the other two are shorter
   still. Verified against the render, not just the count. */
.wdesc {
  margin: 8px 0 0;
  max-width: 27ch;
  font-size: var(--lede-size);
  line-height: 1.32;
  font-weight: var(--body-weight);
  color: var(--ink-support);
}
/* Filed, the title is the deck's own .ptitle idiom verbatim
   (skin-roulette.css:812 on): one line, ellipsis a safety net, not a
   fit guarantee — "titles are written to fit". Expanded, the huge,
   sometimes-two-line balanced title is a different read entirely and
   has to survive untouched, so the clip is gated to .wcard's own
   still-filed range (a size container, above) rather than running
   always: 200px is comfortably short of either grow target's halfway
   point and clear of the 292px-wide expanded state that legitimately
   wraps. .wdesc needs no clip of its own — it is invisible for the
   whole of that range. */
@container wcard-morph (max-height: 200px) {
  .wtitle {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
.world--dark .wtitle { color: var(--text-primary-dark-only); }
.world--dark .wdesc { color: var(--text-subtle-dark-fixed); }

/* The phone column: mobile stacks it below the text column; ≥900px
   docks it to whichever side .wtextcol is not on. overflow: hidden is
   scoped to this column alone (never to .wcard, whose fan still
   overhangs its own gutters), so the one big screen — a size no
   viewport can fully clear at this size — is "allowed to crop at the
   card's bottom edge (card clips it)" by design, not by mistake:
   bottom:0 makes the column's own lower edge the card's own, so the
   crop point sits exactly there at every value of --t.

   The stacked split is a fraction of the card, not the old fixed
   280px: the card's height is now derived from the panel's (R2), so a
   px offset that fitted 390×844 left the 375×667 card with almost no
   phone and the 1920×1080 one with a third of a card of dead space. */
.wphonecol {
  /* relative, not absolute: it is the grid's second row now, and .mini
     is still absolutely placed inside it. */
  position: relative;
  min-height: 0;
  margin: 0 22px;
  overflow: hidden;
}
/* The one screen (§12's own map/calendar/chat), big enough to
   dominate its column and rising through the card as --t climbs
   (R3). Sized in cqw off .wcard's own container (declared above)
   rather than vw: the card's width is capped well under the viewport
   at every tier, so a viewport-relative size once let the screen
   outgrow a narrow card at the wide end of the mobile range.

   9:16, not the 390/844 (0.462) a real iPhone is. The drawn screen is
   an illustration of a phone, not a spec sheet, and at this size the
   taller ratio read as a strip: 9:16 keeps the whole thing recognisable
   at a glance from the crop line up.

   The bezel is fenn's own pen. --brand-black (#000000, the mark's
   stroke colour) rather than the #111 it carried, and its thickness
   is matched to the RENDERED stroke of the mascot standing beside it —
   every mascot export draws at stroke-width 10 in its own user units,
   so the on-screen stroke is 10 × rendered-width / viewBox-width.
   Measured at the plateau: 5.66px (the apple at 390 and 375), 9.56px
   (the apple at 800 and up), 7.58px and 8.70px (laser's and karaoke's
   own fenns at 1440). clamp(6.5px, 1.35vw, 8.8px) lands within ±20%
   of every one of those, and the ramp between the two ends tracks the
   apple's own 16vw growth over the same range. */
.mini {
  position: absolute;
  top: var(--phone-head); left: 50%;
  width: var(--phone-w);
  aspect-ratio: 9 / 16;
  border: var(--bezel) solid var(--brand-black);
  border-radius: calc(var(--phone-w) * 0.154);
  background: #ffffff;
  overflow: hidden;
  box-shadow: var(--pill-shadow);
  /* A size container so §12's drawing scales with the screen instead
     of huddling in its top-left corner at three times the old width. */
  container-type: size;
  container-name: mini;
  /* Three terms, one transform list. The centring never moves. The
     drop pushes the screen below the card's own crop line while the
     card is still filed, so it pokes up from the bottom edge as the
     card opens rather than fading in where it will end up. The float
     is the plateau's own drift: --t is flat for the whole hold but
     --wp keeps evolving, so it is the only driver that can still move
     anything once the panel is stuck.

     The float is one-directional — (1 − --wp), never (0.5 − --wp).
     Centred on the rest pose it swung 22px either way, and the upward
     half cropped the screen's own top bezel against the column's
     overflow mid-scroll (Paul, live review, R10). Written this way the
     term is never negative, so the screen's top never rises past its
     --phone-head offset: the crop stays at the bottom, where it is
     the point, and the drift still reads because it is the last thing
     on the card still moving once --t has flattened. */
  transform: translate(-50%, calc(
      (1 - var(--tp, 0)) * var(--phone-drop)
      + (1 - var(--wp, 0.5)) * var(--phone-float)));
  opacity: var(--tp, 0);
  --tp: var(--t3, 0);
  /* Always cut by the floor (Paul, 08-27, Bim on the alias at 1126×854:
     "the cellphone should always be cropped at the bottom, despite the
     size of the screen"). The screen's width is the column's (46cqw),
     its height 9:16 of that, but the column's height is the panel's —
     so a tall, narrow window (1126×854 with Bim's panel docked, an
     iPad in portrait, 1000×900) leaves a short phone in a tall column,
     showing whole with its chin on the floor (25px past it at 1126×854,
     145px ABOVE it at 1024×1366). S28's rule is ≥40px past the floor at
     every size; the gate only sampled sizes that happened to comply.

     The fix keeps 9:16 and keeps --phone-head as the ceiling: when the
     drawn height cannot reach the floor, the screen sits lower in its
     column instead, its top at (column − phone + overhang), so it rises
     out of the floor the way it does on a phone. --phone-over is the
     corner radius and a little more (0.16 of the width, never under
     S28's 40px), so the cut lands past the curve and the sides run
     straight into the ground rather than ending in a tangent. Where the
     phone already overruns the floor (390, 375, 1440) max() picks
     --phone-head and nothing moves. */
  --phone-over: max(40px, calc(var(--phone-w) * 0.16));
  top: max(var(--phone-head), calc(100% - var(--phone-w) * 16 / 9 + var(--phone-over)));
}
.mini::before {                         /* the island */
  content: "";
  position: absolute; left: 50%;
  top: calc(var(--phone-w) * 0.039);
  transform: translateX(-50%);
  width: calc(var(--phone-w) * 0.25);
  height: calc(var(--phone-w) * 0.056);
  border-radius: calc(var(--phone-w) * 0.039);
  background: var(--brand-black);
  z-index: 2;
}

/* ─────────────────────────────────────────────────────────────
   §5 · The two columns, ≥900px

   Text and phone dock to opposite sides now, rather than one flowing
   column with the app row shifted to alternate edges. .world--mirror
   (not :nth-of-type(even) — the masthead throws off the parity a
   pure sibling count would need) marks the one card that flips: cards
   1 and 3 keep text left/phone right, card 2 mirrors. The
   mirrored-geometry lesson still holds: flipping the anchor alone
   isn't a mirror, because the pile/fan coordinates keep growing in
   the same absolute direction and the assembly fans off .whold's far
   edge instead of toward the card's centre — --px/--fx/--pr/--fr are
   negated and .wplate-back's own tab shape is scaleX(-1)'d (not a
   photograph, free to mirror outright) to bring it back to the spine
   side, exactly as the single-column cut did it.
   ───────────────────────────────────────────────────────────── */
/* Two columns from 700px (S38 Part C, Paul 08-28 #38 at 768×611: "We need to
   revise the Ipad format. The UI looks bad in the phone. Maybe we might need to
   move the cellphone to the right on tablet. This view could be only on
   mobile."). This block is the shape — text one side, phone the other, the
   mirror flipping which; the (min-width: 700px) and (max-width: 899px) block
   further down re-cuts the numbers for a tablet, which has the desktop's layout
   and nothing like its room. */
@media (min-width: 700px) {
  /* The card grows with the panel it sits in (R2 widened the panel
     from a 1200px-capped --card-col to the full width minus the
     gallery gutter), so the two columns get a real measure instead of
     760px of card floating in 1328px of sky. */
  .wcard { width: min(1040px, calc(100% - 120px)); }
  .wtitle { font-size: clamp(36px, 5.4cqw, 60px); }
  /* Side by side, so both columns span the card's full height and the
     text group centres against the phone rather than against a
     stacked band of it. */
  /* The base is a grid with two stacked rows (S38); side by side, each
     column wants the card's full height, so both go back to absolute in
     a block card and the base's own padding/margin insets give way to
     this tier's 40px ones. */
  .wcard { display: block; }
  .wtextcol { position: absolute; padding: 0; justify-content: center;
              top: 0; bottom: 0; left: 40px; right: auto; width: calc(50% - 60px); }
  .wphonecol { position: absolute; margin: 0;
               top: 0; bottom: 0; left: auto; right: 40px; width: calc(50% - 60px); }

  .world--mirror .wtextcol { left: auto; right: 40px; align-items: flex-end; text-align: right; }
  .world--mirror .wphonecol { right: auto; left: 40px; }
  .world--mirror .wdesc { margin-left: auto; }

  .world--mirror .wstack {
    left: auto;
    right: calc(0px - 30.8px * var(--t, 0));
    transform-origin: top right;
  }
  .world--mirror .wplate-back,
  .world--mirror .wplate {
    left: auto;
    right: 15.6px;
  }
  .world--mirror .wplate-back {
    /* Not a photograph — the folder tab's clip-path is free to mirror
       outright, so the tab lands back on the spine (the anchor) side
       instead of stranding the flap where the un-mirrored shape put
       it once the fan itself reversed. */
    transform: scaleX(-1);
  }
  /* The photographs anchor to the stack's RIGHT edge in the mirror.
     Negating --px/--fx alone moved them left FROM THE LEFT EDGE, which
     is a translation and not a reflection: the fan ended up ~71px off
     its own plate, sitting left of the glass instead of centred on it
     (Paul, live review, R10). With right:0 the same negated numbers
     mirror the pile exactly — measured fan-centroid to plate-centroid
     −3.8px un-mirrored, +3.8px here. */
  .world--mirror .wph { left: auto; right: 0; }
  .world--mirror .wph-back  { --px: -40px; --pr: 3deg;  --fx: -5px;  --fr: 19deg; }
  .world--mirror .wph-mid   { --px: -33px; --pr: -2deg; --fx: -37px; --fr: 5deg; }
  .world--mirror .wph-front { --px: -35px; --pr: 0deg;  --fx: -70px; --fr: -2.5deg; }

  /* laser's and karaoke's mascots have to live in the gutter between
     the panel's edge and the card's own column, and round 3 changed
     the arithmetic of that gutter twice over: the panel grew to the
     full window minus one gallery gap, and the card grew with it.
     What is left either side is (panel − card) / 2 + the column's own
     40px inset — 184px at 1440, but only ~100px at the 900px floor of
     this tier, and a 148px mascot fits neither reliably nor at all.

     So: sized to the gutter rather than to the viewport, and anchored
     to the panel's own edge in px rather than to a percentage of a
     panel whose width now varies by a factor of two across this tier.
     8.5vw keeps the ink inside the narrowest gutter this tier can
     produce (77px at 900 wide) and still reads at 122px on a 1440 and
     the full 148 beyond 1740. !important because these are inline
     custom properties (the reduced-motion block's own escape hatch,
     reused for the same reason).

     Their vertical placement is handled once for every held mascot in
     §10's own max() floor, not per card here: the previous fix parked
     them at 12–13% of a panel whose sticky offset used to have a hard
     64px floor, and the panel's gutter is 22–56px now. */
  [data-world="laser"] .fenn,
  [data-world="karaoke"] .fenn { --fink: clamp(64px, 8.5vw, 148px) !important; }
  /* The apple too (Paul, 08-27, Bim: "same size as the other two, inside
     the world"), on karaoke's side — brunch's phone is on the right.
     Capped at 120, not 148: picnic-02's viewBox is 113 against laser's
     162 and karaoke's 141, so at one ink width its stroke is the
     heaviest of the three; 120 keeps it inside the ±20% of the bezel
     the phone gate holds every mascot to (10.6px vs 8.8 at 1440). */
  [data-world="brunch"] .fenn { --fink: clamp(64px, 8.5vw, 120px) !important; left: 12px; right: auto; }
  [data-world="laser"] .fenn { left: auto; right: 12px; }
  [data-world="karaoke"] .fenn { left: 12px; right: auto; }
}
/* The desktop keeps the deck's own fan-out travel (S38). Its own block,
   not the layout block above, because the tablet tier (S38 Part C) lowers
   that one to 700px and a two-column tablet card still wants the plate on
   the text column's edge — only the ≥900 desktop, whose numbers this
   round is required to leave byte-unchanged, keeps −30.8px × --t. */
@media (min-width: 900px) {
  .wstack { left: calc(0px - 30.8px * var(--t, 0)); }
}

/* On phones the three stand where the free apple used to (Paul, 08-27:
   "bring them back in mobile indeed"): the stacked card has no gutter,
   but its head is the fan on the left and nothing on the right — 13-255
   of 346 is the fan at 390, the mascot takes 256-334. The old free
   apple's own size, 78px at 390. Under 700px only since S38: 700–899 is the
   tablet tier now, and a two-column card has the desktop's own gutter. */
@media (max-width: 699px) {
  [data-world="brunch"] .fenn,
  [data-world="laser"] .fenn,
  [data-world="karaoke"] .fenn { --fink: clamp(64px, 20vw, 112px) !important; left: auto; right: 12px; }
  /* Paul's own 08-28 ask on this card — "reduce by 25% the size of the
     folder, increase by 50% the size of the phone, there's top padding
     that could be used" — is answered by --fan-max and --phone-w (the
     band went with S38's grid card). They are NOT declared here: a phone
     is a width, not a width-and-height, and this block loses to the
     short-viewport tiers further down. They live in the
     (max-width: 528px) block after them. */
}

/* ─────────────────────────────────────────────────────────────
   12 · The three app surfaces — the real screens, staged

   Map, Organizer and chat, drawn as the app draws them: squircle
   avatars with their location badges standing on the map's own pin,
   white cards on --surface-primary, the tab-bar pill, message bubbles
   with a sender colour, a time and a reactions pill — and fenn's own
   messages, which are none of those things: plain text on the page
   grey with the mark at the last paragraph's foot. One self-contained
   block,
   depending on nothing but tokens.css and the #fennmark / #i-* symbols
   in the markup, so a sibling direction can lift it unchanged.

   ── The unit, and why it is not cqw ──

   §11's .mini is 9:16 and it is CROPPED at the card's own bottom edge
   (R10): the phone pokes up out of the card, so only the top of the
   screen is ever seen. How much differs enormously by tier — 57.6% at
   390×844, 91.5% at 1440×900, 46.9% at 800×700, 61.6% at 375×667 — and
   an app screen has to fill what is VISIBLE, not what is drawn. A
   single cqw-proportional drawing therefore cannot work: composed for
   the desktop crop it loses its own bottom half on a phone, composed
   for the phone it floats in the top third of a desktop card.

   So the machine measures the crop (index.html's measureCrop) and
   writes --vis, the visible fraction, on each .mini. --u is a fraction
   of the PHONE, not of the crop:

       --u: calc(100cqh / 579)

   579 is not a new number. It is 530 / 0.915 — the desktop tier's own
   unit restated against the whole 9:16 drawing rather than against the
   part of it that shows. That one substitution is the whole of the
   08-25 correction (item 9). Until then --u carried a `var(--vis) *`
   factor, which tied the drawing's SCALE to how much of the phone the
   card happened to leave visible: a tier that showed less of the phone
   drew the app smaller, so the surface rendered at 1.22x the app's own
   size at 1440 and 0.77x at 390, 0.84x at 375 and 0.64x at 800x700 —
   the tier with the least room got the smallest type. Measured now, it
   is 1.20x at every tier, and the body text is 20.25px at 1440, 11.48
   at 390, 9.82 at 800x700 and 7.47 at 375. A miniature scales with the
   phone, not with the window it is seen through.

   --vis stays: --crop is still built from it, and it is now also what
   the machine reads to write data-room (below).

   Two conventions follow, and one invariant went:
   - VERTICAL positions and every component size are in --u, so a
     64 avatar is 64 units wherever it is drawn;
   - HORIZONTAL positions are in %, because the screen's width in units
     is what varies (319u at 1440, 318u at 390 — nearly equal now, and
     that is the point) and a % keeps the margins proportional;
   - what is GONE is "the visible window is always 530 units tall". It
     is 579 x --vis now: 530u at 1440, 358u at 375, 334u at 390 and
     272u at 800x700. So the three compositions cannot be laid out down
     one fixed ruler any more — each is a flex column that gives its
     own middle away first (§12's own "what gives" note per screen),
     and data-room is the one breakpoint that is measured rather than
     guessed: the machine writes tight on any .mini showing less than
     55% of its phone, which is the only tier where hiding a piece of
     chrome beats clipping the content.

   --crop is the strip below the fold, so the bottom chrome (tab bar,
   composer) anchors just above the crop line rather than at a screen
   bottom nobody can see.

   ── The apparition ──

   Each screen carries data-step, incremented once per entry by the
   sequencer in index.html when the hold reaches its plateau. Every
   state below is a [data-step] rule over a transition, so the
   reduced-motion kill switch resolves the whole sequence to its final
   frame for free (the machine sets the final step immediately there).
   It plays once and never loops, which is what keeps it inside
   brand.md's "nothing loops for decoration": it is the content.
   ───────────────────────────────────────────────────────────── */

/* SOURCE — this block is this branch's own and self-contained: it
   depends on nothing but tokens.css and the symbols index.html inlines,
   which is what makes it liftable. A sibling direction that takes it
   carries a PORTED header citing site/worlds@<sha>:assets/skin-site-worlds.css
   and the line range this marker and its /SOURCE closer delimit, and its
   own gate diffs that range back against this file. Keep the two markers
   around the whole block when you edit inside it. */

/* The fallback is the 390×844 crop, so the surface is already composed
   correctly before the machine's first write and stays correct with no
   script at all. The machine writes the measured number as an inline
   style on .mini, which outranks this. */
.mini { --vis: 0.576; }

.scr {
  position: absolute; inset: 0;
  --u: calc(100cqh / 579);
  --crop: calc((1 - var(--vis)) * 100cqh);
  /* §11's island is sized in fractions of --phone-w, so it is the one
     thing on the screen that does NOT scale with --u — at 390 it eats
     35 units of the visible window and at 1440 only 25. --top is where
     a header may start: the island's own bottom edge in screen
     coordinates (0.039 + 0.056 of --phone-w, less the bezel the screen
     box does not include) plus a little air. The map has no header and
     runs under it, which is what the app does too. */
  --isle: calc(9.5cqw - 0.8 * var(--bezel));
  --top: calc(var(--isle) + 6 * var(--u));
  font-family: var(--font-sans);
  font-size: calc(16 * var(--u));
  line-height: calc(19 * var(--u));
  font-weight: var(--body-weight);
  letter-spacing: var(--tracking);
  color: var(--text-primary);
  background: var(--surface-primary);
  overflow: hidden;
}
.scr b, .scr strong { font-weight: var(--body-weight-bold); }
.scr i, .scr em, .scr s { font-style: normal; text-decoration: none; }

/* ── Avatar (Figma 891:93240) ──
   64 square, --radius-avatar-xl (26 of 64 = 0.406 of the box), a 3px
   white rim, a slight per-avatar tilt so a cluster reads as people and
   not as a swatch strip. --av is the size in units; --sender fills the
   initials variant from tokens.css's own 14. */
.scr .av {
  position: relative;
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: calc(var(--av, 54) * var(--u));
  height: calc(var(--av, 54) * var(--u));
  border-radius: calc(var(--av, 54) * var(--u) * 0.406);
  border: calc(3 * var(--u)) solid var(--brand-white);
  background-color: var(--sender, var(--surface-primary));
  /* 110%, not cover. Every assets/faces/*.png is a 200x200 circle on
     alpha, and this frame is a squircle: at radius 26 of 64 the
     corner's farthest point is sqrt(2)*(32-26)+26 = 34.49 from centre,
     a circle of 107.8% of the box, so `cover` left four grey
     --surface-primary corners around every photograph. 110% clears it
     with margin; the sources are square, so one value cannot distort
     them, and an initials tile carries no image for the rule to touch. */
  background-size: 110%; background-position: center;
  color: var(--brand-white);
  font-size: calc(var(--av, 54) * var(--u) * 0.33);
  line-height: 1;
  font-weight: var(--h4-heavy-weight);
  letter-spacing: 0;
  rotate: var(--tilt, 0deg);
  overflow: hidden;
}
/* The overlapping row. -24 of a 64 avatar is 0.375 of the box; kept as
   a fraction so it holds at every --av this block uses. */
.scr .stack { display: flex; align-items: center; }
.scr .stack .av + .av { margin-left: calc(var(--av, 54) * var(--u) * -0.375); }
/* On the map the photograph is whole (Paul, 08-27, Bim on the alias, two
   pins: "check the crop below the image"): the 110% that clears a
   squircle's corners also cut a chin off at the tile's flat foot, and
   under a pin that cut is what the eye lands on. 100% here, and the
   corners it leaves are the rim's own white. */
.mp-who .av:not(.av--ini) {
  /* 112%, bottom-aligned (Paul, 08-28, Bim #33/#34: "a cut and a blank
     space below the profile picture"). The cut was in the FILES: five of
     the six faces ended at row ~176 of 200 with opaque white under the
     chin — a padded crop masked to a circle — so at 100% the frame showed
     the photograph's own white foot, and at the base rule's 110% the
     calendar showed it too. The PNGs are re-cut now (the largest circle
     inside each disc that ends where the content ends, ×1.13–1.15), which
     puts every chin at the circle's edge; bottom-aligned keeps that chin
     on the frame's foot, 112% takes the crop out of the hair, and the two
     bottom corners lose only a ~1px sliver (a circle that filled them
     with the chin at the foot would need 134%). Callie was already clean. */
  background-size: 112%; background-position: center 100%;
  background-repeat: no-repeat; background-clip: padding-box;
  background-color: var(--brand-white);
}
/* :not(.av--ini): the first cut of this rule (08-27, earlier) painted
   Mia's initials tile white — white on white, "we lost the profile
   picture at the top right". no-repeat + padding-box: at 100% the
   photograph tiled into the 3u rim, a sliver of its own top edge under
   the chin — the crop Paul still saw at 16:06. */

/* Location-status badge (bottom-right of an avatar): a white pill,
   radius 10, the app's own two-part shadow, an activity glyph and an
   optional label at half opacity. */
.scr .bdg {
  position: absolute; right: calc(-5 * var(--u)); bottom: calc(-5 * var(--u));
  display: grid; place-items: center;
  min-width: calc(22 * var(--u)); height: calc(22 * var(--u));
  padding: 0 calc(4 * var(--u));
  border-radius: calc(10 * var(--u));
  background: var(--surface-raised);
  box-shadow: 0 calc(7.5 * var(--u)) calc(15 * var(--u)) rgba(0, 0, 0, 0.05),
              inset 0 calc(1 * var(--u)) 0 rgba(255, 255, 255, 0.7);
  font-size: calc(11 * var(--u));
  line-height: 1;
}

/* A white card on the page grey. No shadow: in the app these are flat
   white on --surface-primary and the separation is the value step. */
.scr .card {
  background: var(--surface-raised);
  border-radius: calc(26 * var(--u));
}

/* ── The tab bar (Figma 899:100360, and screenshots 25-27) ──
   A white pill of three 44-wide icon tiles, the active one on a
   --surface-primary tile, with detached round buttons beside it. It
   anchors to the CROP line, not to the screen's own foot: the foot is
   below the card's bottom edge on every tier and a tab bar nobody can
   see is not chrome. */
.scr .tb {
  position: absolute; left: 0; right: 0;
  bottom: calc(var(--crop) + 10 * var(--u));
  display: flex; justify-content: center; align-items: center;
  gap: calc(10 * var(--u));
  z-index: 3;
}
.scr .tb-pill {
  display: flex; align-items: center; gap: calc(4 * var(--u));
  padding: calc(5 * var(--u));
  border-radius: calc(26 * var(--u));
  background: var(--surface-raised);
  box-shadow: 0 calc(7.5 * var(--u)) calc(15 * var(--u)) rgba(0, 0, 0, 0.05);
}
.scr .tb-i {
  display: grid; place-items: center;
  width: calc(44 * var(--u)); height: calc(40 * var(--u));
  border-radius: calc(15 * var(--u));
  color: var(--brand-black);
}
.scr .tb-i.is-on { background: var(--surface-primary); }
.scr .tb-i svg { width: calc(25 * var(--u)); height: calc(25 * var(--u)); display: block; }
/* The centre tab is the mark itself. 6.5 rather than the sheet's 8:
   the stroke was tuned against a 28px mark and goes heavier than any
   mascot on the page at this size (brand.md §The character). */
.scr .tb-mark {
  width: calc(26 * var(--u)); height: auto; aspect-ratio: 61.5 / 66.5;
  overflow: visible;
  --mark-fill: #ffffff; --mark-line: var(--brand-black); --mark-stroke: 6.5;
}
/* No detached round buttons beside the pill any more (Paul, 08-27,
   Bim: "remove the search button", "remove this button" ×2): the
   pill stands alone, centred by .tb's own justify-content. */

/* ─────────────────────────────────────────────────────────────
   Card 1 · the map — fenn finds the fair spot
   ───────────────────────────────────────────────────────────── */

/* The ground and everyone on it travel together, because a pan is the
   map moving under the pins and not the pins sliding over it. Oversized
   on every side so the drag never exposes an edge. */
.mp-pan {
  /* 56u above and below, not 14 (Paul, 08-27, Bim on the alias: "the
     upper part of the map looks cut — we can notice the grey background
     — make sure we have a big map"): the pre-pan pose sits 30u down, so
     a 14u overhang left 16u of the phone's own grey showing above the
     ground at step 0/1 (his screenshot). 56u covers the pose and the
     drag's own 8% reach on top of it (59px at 1440, where 40u was 50).
     The sides stay at the funnel's 14%: every --x on this surface is a
     percentage of the pan's width, so widening the sides moved every
     pin (78% of a 140% pan walked Mia off the phone's edge, Paul's pin
     of 16:06). 14% covers the 10% drag once the 6.5% pan has landed;
     during the first 900ms the reach is a few percent short, which is
     invisible at the speed a first drag happens. Every --y on this surface adds the overhang
     back (three places below), so the numbers still read off the
     screen's top edge. */
  position: absolute; inset: calc(-56 * var(--u)) -14%;
  /* --dx/--dy are the drag (index.html), on top of the pan's own pose. */
  transform: translate(calc(6.5% + var(--dx, 0px)), calc(30 * var(--u) + var(--dy, 0px)));
  transition: transform 640ms var(--ease-out);
}
.mp-pan.is-dragging { transition: none; }
.scr--map { touch-action: pan-y; cursor: grab; }
.scr--map:has(.is-dragging) { cursor: grabbing; }
.scr--map[data-step="2"] .mp-pan,
.scr--map[data-step="3"] .mp-pan,
.scr--map[data-step="4"] .mp-pan { transform: translate(var(--dx, 0px), var(--dy, 0px)); }
.mp-ground { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* A place already on the map: the app's own white chip. */
/* --y is a PERCENT of the visible band now, not a unit offset (item 9).
   A scatter is a placement, not a component: where a friend stands on
   the map has to hold whether the card shows 272 units of phone or
   530, while the chip they stand under stays 26 units across. The 56u
   is .mp-pan's own overhang above the screen, added back so the number
   reads off the screen's top edge like every other --y here. */
.mp-poi {
  position: absolute; left: var(--x);
  top: calc(56 * var(--u) + var(--y) / 100 * (100cqh - var(--crop)));
  translate: -50% -50%;
  display: grid; place-items: center;
  width: calc(26 * var(--u)); height: calc(26 * var(--u));
  border-radius: 50%;
  background: var(--surface-raised);
  box-shadow: 0 calc(3 * var(--u)) calc(7 * var(--u)) rgba(0, 0, 0, 0.07);
  font-size: calc(13 * var(--u)); line-height: 1;
}

/* A person on the map: the avatar sits ON the pin (Figma 2532:44096,
   "avatar + pin.map"). It used to be a 4x13 black needle, which read as
   a drop-pin stem rather than as the app's own pin — the sheet draws a
   56x33 white teardrop tail hanging at top 51 of the 64 avatar, so the
   squircle covers its wide end and only the taper shows. Everything
   below is that ratio against --av. */
.mp-who {
  position: absolute; left: var(--x);
  top: calc(56 * var(--u) + var(--y) / 100 * (100cqh - var(--crop)));
  --av: 50;
  /* translate and scale as individual properties, never inside one
     transform list: the property order is translate ∘ rotate ∘ scale,
     so the centring shift is applied OUTSIDE the pop and a pin at
     scale .35 still sits on its own coordinate. --y is the tip line. */
  translate: -50% -100%;
  /* The tip, not the old needle's: 51/64 down plus 33/56 of a 56/64
     width puts it at 1.3125 of the box. A pin pops out of the ground
     it is planted in. */
  transform-origin: 50% 131%;
  opacity: 0;
  scale: 0.35;
  transition: opacity var(--dur-base) var(--ease-out),
              scale var(--dur-slow) var(--ease-spring);
  transition-delay: var(--d, 0ms);
}
.scr--map:not([data-step="0"]) .mp-who { opacity: 1; scale: 1; }
/* 56/64 = 0.875 wide, hung at 51/64 = 0.797 down, centred on the
   avatar. The viewBox carries the sheet's own ground shadow under the
   tip; the tail's fill is currentColor, so the pin is the same white
   as every other piece of furniture on this surface. It is first in the
   markup, which is what puts it under the face. */
/* The tack (S28), placed the way plans-like places it under its 96px
   tile: the 141-wide canvas centred, its top at 0.896 of the tile, so
   the leg's head is behind the squircle and the foot lands ~0.3 of the
   tile below it. The export carries its own ground shadow, so no CSS
   shadow on top of it. */
.mp-who .pin-tail {
  position: absolute; left: 50%;
  /* 0.786, not the sheet's 0.896 (Paul, 08-27, Bim on the alias at
     1126 wide: "moved a bit upward under the pin, like maybe 6px").
     Under a round face the leg's head emerges where the squircle
     curves away, so the join read as a gap; 0.11 of the tile is 6px
     at his 54.8px tile, 4 at 390, 7 at 1440 — it scales with the
     face the way everything else on the pin does. The spot's own
     tail keeps 0.896: a square tile has no curve to clear. */
  top: calc(var(--av) * var(--u) * 0.786);
  width: calc(var(--av) * var(--u) * 1.469);
  margin-left: calc(var(--av) * var(--u) * -0.7345);
  height: auto; aspect-ratio: 141 / 82.5;
  display: block;
  overflow: visible;
  /* Wider than the avatar it hangs from, so foundation's svg
     { max-width: 100% } would cap it at the avatar's width and leave the
     negative margin pulling a narrower canvas off-centre — measured
     14.8px left at 1440 before this line. */
  max-width: none;
}
.mp-spot .pin-tail {
  position: absolute; left: 50%; z-index: 0;
  /* The friends' leg, not a bigger one (Paul, 08-27, Bim on the alias:
     "the pin below the image has the wrong height — consistent with
     the other pins"): same 0.786 head as .mp-who's tail and the same
     50u-based width, only the tile above it is 52u. */
  top: calc(52 * var(--u) * 0.786);
  width: calc(50 * var(--u) * 1.469);
  margin-left: calc(50 * var(--u) * -0.7345);
  height: auto; aspect-ratio: 141 / 82.5;
  display: block;
  overflow: visible;
}

/* The chosen spot. .mm-pin keeps its name from round 3 — the gate
   measures it against the crop line and it is still the same thing:
   the one place the card is about. */
.mp-spot {
  position: absolute; left: 0; right: 0;
  /* Inside .mp-pan since 08-27 (it drags with the ground), so the pan's
     56u overhang is added back like the friends' and the chips'. */
  top: calc(56 * var(--u) + var(--y) / 100 * (100cqh - var(--crop)));
  text-align: center;
  z-index: 2;
  opacity: 0;
  translate: 0 calc(-16 * var(--u));
  transition: opacity var(--dur-base) var(--ease-out),
              translate var(--dur-slow) var(--ease-spring);
}
.scr--map[data-step="3"] .mp-spot,
.scr--map[data-step="4"] .mp-spot { opacity: 1; translate: 0 0; }
.mm-pin {
  position: relative; z-index: 1;
  display: grid; place-items: center;
  width: calc(52 * var(--u)); height: calc(52 * var(--u));
  margin: 0 auto;
  border-radius: calc(21 * var(--u));
  border: calc(3 * var(--u)) solid var(--brand-white);
  background: var(--gradient-primary);
  box-shadow: 0 calc(8 * var(--u)) calc(18 * var(--u)) rgba(0, 0, 0, 0.18);
  font-size: calc(24 * var(--u)); line-height: 1;
}
/* One pulse, once, on the beat fenn settles — the map's own "here".
   A single iteration by declaration, so it is a state change and not a
   loop; reduced motion kills it and the ring simply never paints. */
.mp-ring {
  position: absolute; left: 50%; top: calc(26 * var(--u));
  width: calc(52 * var(--u)); height: calc(52 * var(--u));
  margin: calc(-26 * var(--u)) 0 0 calc(-26 * var(--u));
  border-radius: 50%;
  border: calc(3 * var(--u)) solid var(--accent);
  opacity: 0;
}
.scr--map[data-step="4"] .mp-ring { animation: mp-pulse 900ms var(--ease-out) 1 both; }
@keyframes mp-pulse {
  from { opacity: 0.55; transform: scale(0.6); }
  to   { opacity: 0;    transform: scale(2.6); }
}
/* The label card under the pin — the Figma pin's own caption card. */
.mp-label {
  display: block;
  margin: calc(16 * var(--u)) auto 0;
  width: max-content; max-width: 92%;
  padding: calc(7 * var(--u)) calc(12 * var(--u));
  border-radius: calc(12 * var(--u));
  background: var(--surface-raised);
  box-shadow: 0 calc(7.5 * var(--u)) calc(15 * var(--u)) rgba(0, 0, 0, 0.08);
  font-size: calc(14 * var(--u)); line-height: calc(17 * var(--u));
  color: var(--text-secondary);
  white-space: nowrap;
}
.mp-label b { color: var(--text-primary); }
/* After the pin, not with it (Paul, 08-27, Bim on the alias: "the label
   should have some delay — first we see the pin land, then the label;
   the appearance could start from a white circle with the text masked
   inside, expanding to its final state"). The circle is a clip-path on
   the label's own white; it opens once the pin's spring has landed. */
.mp-label {
  clip-path: circle(9% at 50% 50%);
  opacity: 0;
  transition: clip-path 560ms var(--ease-out) 520ms, opacity 160ms var(--ease-out) 520ms;
}
.scr--map[data-step="3"] .mp-label,
.scr--map[data-step="4"] .mp-label { clip-path: circle(75% at 50% 50%); opacity: 1; }

/* ─────────────────────────────────────────────────────────────
   Card 2 · the Organizer calendar — fenn picks the day
   ───────────────────────────────────────────────────────────── */

/* What gives, and in what order (item 9). The band runs 272u…530u
   now, and this composition wants 363: head, segmented control, month
   card, picked-day label and event row. So it is a column whose MIDDLE
   is the elastic part — the month card takes what is left and its grid
   clips from the bottom, which is what a month view you have not
   scrolled looks like. The label and the row never give: they are the
   answer the card is about, and the gate holds them above the crop.
   At the tight tier the head and the segmented control go instead of a
   third week, because a calendar with no weeks in it is not a
   calendar. fenn's pick moved to the second row for the same reason —
   see index.html. */
.scr--cal {
  display: flex; flex-direction: column;
  padding: var(--top) 5.5% calc(var(--crop) + 74 * var(--u));
}
.scr--cal > * { flex: 0 0 auto; }
/* The ladder, and what each rung costs the SCREEN rather than the
   grid — because a month card with no days in it is not a calendar,
   and every rung here was measured against that. mid (390, 375) drops
   the "Organizer" title and the weekday header: the phone already says
   which app this is, and Sun-Mon-Tue is the one part of a month grid
   you can read off the numbers. tight cuts the numbers themselves
   instead — see its own block below. What NEVER goes at any rung is the grid
   itself, fenn's filled pick, the picked-day label or the event row:
   those four are the sentence the card is here to say. */
.mini[data-room="mid"] .cl-head,
.mini[data-room="mid"] .cl-week,
.mini[data-room="tight"] .cl-head,
.mini[data-room="tight"] .cl-week { display: none; }
/* Every rung keeps every row (Paul, 08-27, Bim on the alias: "the last
   row should not be hidden — hug the whole container"); the segmented
   control that used to buy the room is gone at every rung, and the
   28u pitch Paul asked for at 1512 gives back what a shorter room
   can't spend: mid keeps the 22u pitch, tight goes to 16u and the event
   row's old 8u pad — measured against the event row clearing the tab bar
   (it was 12-30px under it at 28u, still 2-14 under at 24/20).
   The month TITLE is not a rung any more (S38, Paul 08-28 #40: "we have
   to display the title"). mid used to drop it while tight, re-cut below,
   shows it — an inversion: the smaller room said the month and the
   larger one did not. It is shown in every room now, at tight's own 12u
   where the room is short. */
.mini[data-room="mid"] .cl-top,
.mini[data-room="tight"] .cl-top { padding-bottom: calc(6 * var(--u)); font-size: calc(12 * var(--u)); }
.mini[data-room="mid"] .cl-grid i { height: calc(22 * var(--u)); }
.mini[data-room="mid"] .cl-row { padding-block: calc(8 * var(--u)); }
/* tight, re-cut for the phone (S38, Paul 08-28 #40 at 375×667: "On mobile
   the calendar is broken. We need to keep a min gap, surely reduce the size
   of the numbers. We have to display the title."). Three answers in his own
   order. The month TITLE comes back — tight used to drop .cl-top, and a
   month grid with no month over it is a puzzle — at 12u rather than the
   base 14u; that line sits up with mid's, which shows it now too.
   The GAP is the thing that never gives now: 3u between rows and
   2u between columns, up from 2u and nothing, because a grid whose numbers
   touch reads as one block of digits rather than as weeks. What pays for
   both is the NUMBERS: the cell drops 16u → 15u. .cl-row's pad comes in to
   6u/8u from 8u/15u for the same reason — the room it gives back is the
   room the gap and the title take. */
.mini[data-room="tight"] .cl-grid i { height: calc(14 * var(--u)); font-size: calc(14 * var(--u)); }
/* The picked day's disc is 28u everywhere else — twice a tight cell, so it
   sat over its neighbours' rows; 22u here (Paul, 08-28: "the mobile is
   noticeably too tight" — the air comes from smaller digits and disc,
   the pitch itself is pinned by the tab bar's 6px floor at 375×667). */
.mini[data-room="tight"] .cl-grid i.pick::before { width: calc(22 * var(--u)); height: calc(22 * var(--u)); margin: calc(-11 * var(--u)) 0 0 calc(-11 * var(--u)); }
.mini[data-room="tight"] .cl-row { padding: calc(6 * var(--u)) calc(8 * var(--u)); }
.mini[data-room="tight"] .cl-card { margin-top: calc(4 * var(--u)); padding-top: calc(6 * var(--u)); padding-bottom: calc(8 * var(--u)); }
.mini[data-room="tight"] .cl-grid { row-gap: calc(5 * var(--u)); column-gap: calc(2 * var(--u)); }
.mini[data-room="mid"] .cl-when,
.mini[data-room="tight"] .cl-when { margin-top: calc(9 * var(--u)); }
/* (The 11u row pad and the 13u PICKED gap Paul asked for at 1512 are
   wide-room numbers: 430×932 is mid and was 1px under the tab bar with
   them.) */
.cl-head {
  text-align: center;
  font-size: calc(20 * var(--u)); line-height: calc(24 * var(--u));
  font-weight: var(--body-weight-bold);
}
/* No segmented control any more (Paul, 08-27): the month card takes
   its room. */
.cl-card {
  /* The elastic middle. min-height: 0 is what lets a flex item shrink
     below its content at all, and the overflow is what turns that into
     a clip instead of an overlap. */
  /* Hugs its grid since 08-27 (Paul, Bim on the alias: "the last row
     should not be hidden — let's hug the whole container"): the card
     was the elastic middle that clipped rows in a short room; now it
     is as tall as October, and the room ladder above trims the chrome
     around it instead. */
  flex: 0 0 auto;
  display: flex; flex-direction: column;
  margin-top: calc(8 * var(--u));
  /* 9/14/14, not 8/10/6 (Paul, 08-27, Bim on the alias at 1512: "more
     padding on the edges and mostly at the bottom — the numbers look
     too close to the bottom"). The last row's numerals sat 6u off the
     card's edge with a 26u radius curving in under them. */
  padding: calc(9 * var(--u)) calc(14 * var(--u)) calc(14 * var(--u));
  background: var(--surface-raised);
  border-radius: calc(26 * var(--u));
  border: 1px solid var(--surface-primary);
}
.cl-top, .cl-week { flex: 0 0 auto; }
.cl-top {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 calc(4 * var(--u)) calc(8 * var(--u));
  border-bottom: 1px solid var(--surface-primary);
  font-size: calc(14 * var(--u));   /* 19 × 0.75 (Paul, 08-27: "reduce by 25% the size of the month") */
  font-weight: var(--body-weight-bold);
}
.cl-top s { display: block; color: var(--text-tertiary); }
.cl-top s svg { display: block; width: calc(17 * var(--u)); height: calc(17 * var(--u)); }
.cl-top s + s svg { rotate: 180deg; }
.cl-week, .cl-grid {
  display: grid; grid-template-columns: repeat(7, 1fr);
  text-align: center;
}
.cl-week {
  padding: calc(5 * var(--u)) 0 calc(2 * var(--u));
  font-size: calc(13 * var(--u));
  color: var(--text-tertiary);
}
.cl-grid { flex: 0 0 auto; row-gap: calc(4 * var(--u)); }
/* The rows breathe (Paul, 08-28, on a 1512×862 desktop — a `mid` room,
   the card is short there: "the calendar is too tight — slightly more
   gaps between the numbers"): 4u → 9u between rows in mid and wide (7u on the first pass — "a bit
   more"); tight 3u → 5u, and its cells give a unit (15u → 14u) for it — 6u
   left 375×667 4px from the tab bar, under the gate's 6px floor. */
.mini[data-room="wide"] .cl-grid,
.mini[data-room="mid"] .cl-grid { row-gap: calc(9 * var(--u)); }
.cl-grid i {
  position: relative;
  display: grid; place-items: center;
  /* 28u, not 22 (Paul, 08-27, Bim: "4px more gap top and bottom between
     the numbers" — 3u each way at his 1.3px unit). */
  height: calc(28 * var(--u));
  font-size: calc(17 * var(--u));
  opacity: 0;
  translate: 0 calc(-4 * var(--u));
  transition: opacity var(--dur-base) var(--ease-out),
              translate var(--dur-base) var(--ease-out);
  transition-delay: calc((var(--r) - 1) * 62ms);
}
.cl-grid i.off { color: var(--text-tertiary); }
.scr--cal:not([data-step="0"]) .cl-grid i { opacity: 1; translate: 0 0; }
/* Event dots — 6px in --sender-12, the app's own event colour. */
.cl-grid i.has::after {
  content: "";
  position: absolute; left: 50%; bottom: calc(-5 * var(--u));
  width: calc(6 * var(--u)); height: calc(6 * var(--u));
  margin-left: calc(-3 * var(--u));
  border-radius: 50%;
  background: var(--sender-12);
  opacity: 0;
  scale: 0.2;
  transition: opacity var(--dur-fast) var(--ease-out), scale var(--dur-base) var(--ease-spring);
  transition-delay: var(--d, 0ms);
}
.scr--cal[data-step="2"] .cl-grid i.has::after,
.scr--cal[data-step="3"] .cl-grid i.has::after,
.scr--cal[data-step="4"] .cl-grid i.has::after { opacity: 1; scale: 1; }
/* Except under the day fenn fills. The pick circle is 28u centred on a
   24u cell, so its bottom edge lands at +14u and the dot's own -5u puts
   its centre INSIDE the fill — a dot swallowed by the gradient it is
   supposed to sit under. It steps down to -11u for exactly the two
   beats the fill is painted; every other cell keeps the app's own
   spacing. */
.scr--cal[data-step="3"] .cl-grid i.pick.has::after,
.scr--cal[data-step="4"] .cl-grid i.pick.has::after { bottom: calc(-11 * var(--u)); }
/* fenn's pick. The app fills TODAY in iOS red; this is not today, it
   is the afternoon fenn found, so it wears the brand gradient. */
.cl-grid i.pick::before {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: calc(28 * var(--u)); height: calc(28 * var(--u));
  margin: calc(-14 * var(--u)) 0 0 calc(-14 * var(--u));
  border-radius: 50%;
  background: var(--gradient-primary);
  opacity: 0;
  scale: 0.3;
  transition: opacity var(--dur-fast) var(--ease-out), scale var(--dur-slow) var(--ease-spring);
}
.scr--cal[data-step="3"] .cl-grid i.pick::before,
.scr--cal[data-step="4"] .cl-grid i.pick::before { opacity: 1; scale: 1; }
/* The numeral rides above the fill it is dropped into: ::before is a
   positioned child, so unwrapped text would paint under it. */
.cl-grid i span { position: relative; z-index: 1; transition: color var(--dur-fast) var(--ease-out); }
.scr--cal[data-step="3"] .cl-grid i.pick span,
.scr--cal[data-step="4"] .cl-grid i.pick span { color: var(--brand-white); }

.cl-when {
  display: flex; align-items: baseline; gap: calc(7 * var(--u));
  margin: calc(13 * var(--u)) 0 calc(4 * var(--u)) calc(6 * var(--u));   /* 7u + 6u ≈ 8px at 1512 (Paul, 08-27: "add some gap on top of this container, 8px") */
  font-size: calc(14 * var(--u)); line-height: calc(17 * var(--u));
  color: var(--text-tertiary);
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-out);
}
.cl-when b {
  font-size: calc(13 * var(--u));
  letter-spacing: 0.04em;
  color: var(--accent);
}
.cl-row {
  display: flex; align-items: center; gap: calc(11 * var(--u));
  /* +3u each way (Paul, 08-27, Bim: "increase paddings here, maybe
     4px" — 4px is 3u at his 1512-wide read, where --u is 1.3px). */
  padding: calc(11 * var(--u)) calc(15 * var(--u));
  opacity: 0;
  translate: 0 calc(18 * var(--u));
  transition: opacity var(--dur-base) var(--ease-out), translate var(--dur-slow) var(--ease-out);
}
.scr--cal[data-step="4"] .cl-when, .scr--cal[data-step="4"] .cl-row { opacity: 1; }
.scr--cal[data-step="4"] .cl-row { translate: 0 0; }
.cl-bar {
  flex: 0 0 auto;
  width: calc(3 * var(--u)); height: calc(30 * var(--u));
  border-radius: calc(2 * var(--u));
  background: var(--sender-12);
}
.cl-time { flex: 0 0 auto; font-size: calc(17 * var(--u)); line-height: calc(18 * var(--u)); font-weight: var(--body-weight-bold); }
.cl-time i { display: block; font-size: calc(13 * var(--u)); line-height: calc(15 * var(--u)); font-weight: var(--body-weight); color: var(--text-secondary); }
.cl-what { flex: 1 1 auto; min-width: 0; }
.cl-what b { display: block; font-size: calc(17 * var(--u)); line-height: calc(18 * var(--u)); }
.cl-what i { display: block; font-size: calc(13 * var(--u)); line-height: calc(15 * var(--u)); color: var(--text-secondary); }
.cl-row .stack { --av: 34; }
/* The friends accept one by one (Paul, 08-27, Bim on the alias: "animate
   the appearance of the profile pictures — it should represent the
   friends accepting the invitation"): each avatar pops in on the row's
   own step, 320ms apart, left to right, on the spring. Reduced motion
   resolves to the final frame with everything else. */
.scr--cal .cl-row .av {
  opacity: 0; scale: 0.4;
  transition: opacity var(--dur-fast) var(--ease-out), scale var(--dur-slow) var(--ease-spring);
}
.scr--cal[data-step="4"] .cl-row .av { opacity: 1; scale: 1; }
.scr--cal[data-step="4"] .cl-row .av:nth-child(1) { transition-delay: 320ms; }
.scr--cal[data-step="4"] .cl-row .av:nth-child(2) { transition-delay: 640ms; }
.scr--cal[data-step="4"] .cl-row .av:nth-child(3) { transition-delay: 960ms; }

/* ─────────────────────────────────────────────────────────────
   Card 3 · Today, a tap, and the chat — fenn sends the message
   ───────────────────────────────────────────────────────────── */

.ph-today, .ph-chat { position: absolute; inset: 0; }
/* The iOS push: the outgoing screen drifts a quarter of the way left
   under the incoming one, which is what makes it read as a stack. */
/* What gives (item 9). Today reads top-down — the header, then the
   next thing happening, then the threads — so unlike the chat it clips
   from the BOTTOM: the message list takes what is left and the rows
   below the fold are simply below the fold, which is what a list on a
   phone does. The one row that must always render is the karaoke
   thread, because it is a door; it is first, so it always does. At the
   tight tier the check-in card goes rather than the list, since a
   Messages section with no message in it is not a screen. */
.ph-today {
  display: flex; flex-direction: column;
  padding: calc(16 * var(--u)) 5.5% calc(var(--crop) + 74 * var(--u));
  transition: transform 360ms var(--ease-out), filter 360ms var(--ease-out);
}
.ph-today > * { flex: 0 0 auto; }
.td-list { flex: 0 1 auto; min-height: 0; overflow: hidden; }
.mini[data-room="tight"] .td-row { display: none; }
.ph-chat {
  /* A column, not three absolute boxes: the header's own height moves
     with the island clearance and the thread has to give that room
     back rather than sit under it. The composer rides the crop line
     through the padding, so it is still chrome you can see. */
  display: flex; flex-direction: column;
  padding-bottom: calc(var(--crop) + 12 * var(--u));
  background: var(--surface-primary);
  transform: translateX(100%);
  transition: transform 360ms var(--ease-out);
  box-shadow: calc(-10 * var(--u)) 0 calc(24 * var(--u)) rgba(0, 0, 0, 0.10);
}
/* Which screen is on top is data-nav, not the beat (P5). The
   sequencer writes it at beat 2, and after the sequence has finished
   the back chevron and the karaoke row write it too — so the push is a
   transition the visitor owns, not a frame of a film they can only
   watch. Same 360ms both ways: a pop back IS the push reversed. */
.scr--chat[data-nav="chat"] .ph-chat { transform: translateX(0); }
.scr--chat[data-nav="chat"] .ph-today {
  transform: translateX(-24%); filter: brightness(0.94);
}

/* One line since 08-25 (P4: the app's Today header is the word and no
   date), so the account avatar centres on it instead of hanging off a
   two-line block's top. */
.td-head { display: flex; align-items: center; justify-content: space-between; gap: calc(10 * var(--u)); }
.td-head b { display: block; font-size: calc(38 * var(--u)); line-height: calc(40 * var(--u)); font-weight: var(--body-weight-bold); }
.td-head .av { --av: 50; }
.td-row {
  display: flex; align-items: center; gap: calc(12 * var(--u));
  margin-top: calc(15 * var(--u));
  padding: calc(11 * var(--u)) calc(14 * var(--u));
}
.td-time { flex: 0 0 auto; font-size: calc(17 * var(--u)); font-weight: var(--body-weight-bold); color: var(--text-secondary); }
.td-time i { display: block; font-size: calc(13 * var(--u)); font-weight: var(--body-weight); }
.td-what { flex: 1 1 auto; min-width: 0; }
/* Truncated, not wrapped: at 1.20x the app's scale (item 9) "Regular
   check-in" takes two lines on the phone tier, and a two-line title in
   a one-line list row reads as a bug. The thread rows below already
   truncate; this is the same rule. */
.td-what b {
  display: block; font-size: calc(17 * var(--u));
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.td-what i { display: block; font-size: calc(13 * var(--u)); color: var(--text-secondary); }
.td-row .stack { --av: 34; }
.td-lab {
  margin: calc(16 * var(--u)) 0 calc(9 * var(--u)) calc(4 * var(--u));
  font-size: calc(21 * var(--u)); line-height: calc(24 * var(--u));
  font-weight: var(--body-weight-bold);
}
.td-list { padding: 0 calc(14 * var(--u)); }
/* The rounded card must keep its corners while its content is cut, so
   the clip is on the card and the rows run under it. */
.td-msg {
  position: relative;
  display: flex; align-items: center; gap: calc(12 * var(--u));
  /* The karaoke row is a <button> (P5), so it has to give back
     everything a button brings with it and inherit the type it is
     drawn in. Everything else here is unchanged. */
  width: 100%;
  appearance: none; -webkit-appearance: none;
  border: 0; background: none;
  font: inherit; color: inherit; text-align: left;
  padding: calc(11 * var(--u)) 0;
  border-radius: calc(16 * var(--u));
  transition: background var(--dur-fast) var(--ease-out);
}
.td-msg.is-tap { cursor: pointer; }
/* The preview line the row carries changes when the message lands, so
   Today tells the truth on the way back (P5). */
/* Scoped through .td-msg: the generic `.td-msg em` rule below sets a
   display and outranks a bare `.td-now`. */
.td-msg .td-now { display: none; }
.scr--chat:is([data-step="4"], [data-step="5"], [data-step="6"], [data-step="7"], [data-step="8"]) .td-msg .td-was { display: none; }
.scr--chat:is([data-step="4"], [data-step="5"], [data-step="6"], [data-step="7"], [data-step="8"]) .td-msg .td-now { display: block; }
.td-msg + .td-msg { box-shadow: 0 -1px 0 var(--surface-primary); }
.td-msg .av { --av: 46; }
.td-msg span { flex: 1 1 auto; min-width: 0; }
.td-msg b { display: block; font-size: calc(17 * var(--u)); }
.td-msg em {
  display: block;
  font-size: calc(14 * var(--u)); line-height: calc(17 * var(--u));
  color: var(--text-tertiary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.td-msg s { flex: 0 0 auto; font-size: calc(13 * var(--u)); color: var(--text-tertiary); }
.scr--chat[data-step="1"] .td-msg.is-tap,
.scr--chat[data-step="2"] .td-msg.is-tap,
.td-msg.is-tap:hover { background: var(--surface-primary); }
/* The tap: a soft translucent disc landing on the row fenn opens. */
.ph-tap {
  /* 225u, not 237: the Today header lost its date line (P4) and the
     row it lands on came up with it. Measured off the row's own centre
     at all three tiers, which agree — everything above it is in units. */
  position: absolute; left: 33%; top: calc(225 * var(--u));
  width: calc(56 * var(--u)); height: calc(56 * var(--u));
  margin: calc(-28 * var(--u)) 0 0 calc(-28 * var(--u));
  border-radius: 50%;
  background: rgba(61, 134, 255, 0.18);
  border: calc(2 * var(--u)) solid rgba(61, 134, 255, 0.35);
  opacity: 0; scale: 1.9;
  z-index: 4;
  transition: opacity var(--dur-fast) var(--ease-out), scale var(--dur-base) var(--ease-out);
}
.scr--chat[data-step="1"] .ph-tap { opacity: 1; scale: 1; }

/* Chat header: back, the group's avatar, the title, call and map. */
.ch-head {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: calc(10 * var(--u));
  padding: var(--top) 5.5% calc(10 * var(--u));
  font-size: calc(19 * var(--u));
  font-weight: var(--body-weight-bold);
}
.ch-head .av { --av: 42; }
.ch-head b { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ch-head s { display: block; color: var(--brand-black); }
.ch-head s svg { display: block; width: calc(22 * var(--u)); height: calc(22 * var(--u)); }
/* A real <button> (P5), stripped back to the glyph it was drawn as. */
.ch-head .ch-back {
  display: block; flex: 0 0 auto;
  appearance: none; -webkit-appearance: none;
  border: 0; background: none; padding: 0;
  color: var(--brand-black); cursor: pointer;
}
.ch-head .ch-back svg { display: block; width: calc(19 * var(--u)); height: calc(19 * var(--u)); }

/* The thread sits on the composer and grows upward, the way a
   conversation does. */
.ch-thread {
  flex: 1 1 auto; min-height: 0;
  padding: 0 5.5%;
  display: flex; flex-direction: column; justify-content: flex-end;
  /* No flex gap. Four of this column's children are arrivals that spend
     most of the sequence at zero height, and a gap is held open by a
     zero-height child just as firmly as by a full one — measured, that
     left ~32 units of dead air between the last bubble and the
     composer before anything had arrived, in a thread whose whole
     point is that it sits ON the composer. The spacing lives on the
     rows instead, and inside the arrivals, where it grows with them. */
  gap: 0;
  /* Clipped, so the turn above the visible three is cut by the header
     the way a scrolled thread is, instead of printing over it. */
  overflow: hidden;
}

/* Message bubble (Figma 990:113768): white on a 1px --surface-primary
   rim, radius 16, the sender's own colour on the username, the time
   bottom-right in the caption style. */
.ch-thread > * + * { margin-top: calc(8 * var(--u)); }
/* …except an arrival, which carries its own on the box being grown, so
   a collapsed one is worth exactly nothing. The inner wrapper's
   overflow: hidden makes a block formatting context, so this margin
   cannot escape it either. */
.ch-thread > * + .bub-grow { margin-top: 0; }
.bub-grow > * > * { margin-top: calc(8 * var(--u)); }
.bub-row { display: flex; align-items: flex-end; gap: calc(8 * var(--u)); }
.bub-row .av { --av: 40; align-self: flex-end; }
.bub {
  max-width: 82%;
  padding: calc(8 * var(--u)) calc(11 * var(--u)) calc(7 * var(--u));
  border-radius: calc(16 * var(--u));
  background: var(--surface-raised);
  border: 1px solid var(--surface-primary);
}
.bub .who { display: block; font-size: calc(13 * var(--u)); line-height: calc(15 * var(--u)); font-weight: var(--body-weight-bold); color: var(--sender, var(--text-secondary)); }
.bub p { margin: 0; font-size: calc(16 * var(--u)); line-height: calc(19 * var(--u)); }
.bub .tm {
  display: block; text-align: right;
  margin-top: calc(2 * var(--u));
  font-size: calc(10 * var(--u)); line-height: calc(13 * var(--u));
  font-weight: var(--caption-weight);
  /* Figma's metadata row tracks 0.2 PX, which is not tokens.css's
     --tracking-caption (0.2em, the uppercase caption style): at this
     size that spaced a timestamp out into four separate digits. */
  letter-spacing: 0.02em;
  color: var(--text-tertiary);
}
/* A reactions pill: the app's own grey capsule, emoji plus who left it. */
.bub .rx {
  display: inline-flex; align-items: center; gap: calc(5 * var(--u));
  margin-top: calc(5 * var(--u));
  padding: calc(3 * var(--u)) calc(6 * var(--u)) calc(3 * var(--u)) calc(7 * var(--u));
  border-radius: calc(12 * var(--u));
  background: var(--surface-primary);
  font-size: calc(15 * var(--u)); line-height: 1;
}
.bub .rx .av { --av: 24; border-width: calc(2 * var(--u)); }
.bub-row--me { justify-content: flex-end; }
.bub--me {
  background: var(--gradient-primary);
  border-color: transparent;
  color: var(--text-primary-dark-only);
}
.bub--me .tm { color: rgba(255, 255, 255, 0.72); }

/* Every late arrival grows its own wrapper from zero height, so the
   thread above is carried up over the same frames instead of jumping
   (brand.md §Motion). Four of them now, one per beat of the exchange:
   the visitor's line at 4, fenn's dots at 5, fenn's answer and its
   card at 6, Mia's thank-you at 7 — and the dots collapse again at 6,
   which is the whole trick of a typing indicator being REPLACED by the
   message rather than pushed off by it.

   :is() rather than one selector per beat: "from 4 onward" is four
   attribute values and writing them out four times per rule was the
   thing that made this block unreadable at five beats, let alone
   seven. */
.bub-grow { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-slow) var(--ease-out); }
.bub-grow > * { overflow: hidden; min-height: 0; }
.bub-grow > * > * { opacity: 0; transition: opacity var(--dur-base) var(--ease-out); }
/* Eight beats since 08-27: 5 the dots (held 1.8s), 6 the answer, 7 the
   calendar card on its own, 8 the thank-you. */
.scr--chat:is([data-step="4"], [data-step="5"], [data-step="6"], [data-step="7"], [data-step="8"]) .bub-grow--me,
.scr--chat[data-step="5"] .bub-grow--dots,
.scr--chat:is([data-step="6"], [data-step="7"], [data-step="8"]) .bub-grow--fenn,
.scr--chat:is([data-step="7"], [data-step="8"]) .bub-grow--card,
.scr--chat[data-step="8"] .bub-grow--ta { grid-template-rows: 1fr; }
.scr--chat:is([data-step="4"], [data-step="5"], [data-step="6"], [data-step="7"], [data-step="8"]) .bub-grow--me > * > *,
.scr--chat[data-step="5"] .bub-grow--dots > * > *,
.scr--chat:is([data-step="6"], [data-step="7"], [data-step="8"]) .bub-grow--fenn > * > *,
.scr--chat:is([data-step="7"], [data-step="8"]) .bub-grow--card > * > *,
.scr--chat[data-step="8"] .bub-grow--ta > * > * { opacity: 1; }

/* ── fenn has no bubble ──
   The one thing the old drawing had backwards, and the reason Paul sent
   two screenshots of the real thread: fenn's messages are not messages
   in the bubble sense. They are plain left-aligned text ON the page
   grey — no fill, no rim, no sender line, no timestamp — running the
   full column, as many paragraphs as the answer needs, with the mark
   tucked at the bottom-left of the LAST paragraph. It is the character
   signing the bottom of what it just said, not an avatar heading it.

   The mark is the same shared symbol at 28u, bottom-aligned by the
   row's own align-items, so it lands on the last line whether the
   answer is one paragraph or four. */
.fnmark {
  flex: 0 0 auto;
  width: calc(28 * var(--u)); height: auto; aspect-ratio: 61.5 / 66.5;
  overflow: visible;
  --mark-fill: #ffffff; --mark-line: var(--brand-black); --mark-stroke: 6.5;
}
.fmsg { display: flex; align-items: flex-end; gap: calc(8 * var(--u)); }
.fmsg-in { flex: 1 1 auto; min-width: 0; }
.fmsg-in p { margin: 0; font-size: calc(16 * var(--u)); line-height: calc(20 * var(--u)); }
.fmsg-in p + p { margin-top: calc(9 * var(--u)); }

/* fenn thinking: the same left gutter, the same absence of a bubble,
   three dots where the words will be. One keyframe, three delays. It
   runs only on the beat it is shown and is gone the moment the answer
   lands, so nothing here loops at rest (brand.md §Motion) — and under
   reduced motion the screen jumps to the final frame, where the dots
   are already collapsed and the animation never starts. */
.fdots { display: flex; align-items: flex-end; gap: calc(8 * var(--u)); }
.fdots span { display: flex; align-items: center; gap: calc(5 * var(--u)); padding-bottom: calc(6 * var(--u)); }
.fdots i {
  width: calc(7 * var(--u)); height: calc(7 * var(--u));
  border-radius: 50%;
  background: var(--text-tertiary);
  animation: f-dot 1000ms var(--ease-out) infinite;
}
/* Three infinite animations were running from page load, on a
   typing indicator nobody had reached yet — three style passes a
   frame under every card (S30). They run only while their sequence
   plays; .is-playing is playSeq's own flag on the hold. */
.whold:not(.is-playing) .fdots i { animation-play-state: paused; }
.fdots i:nth-child(2) { animation-delay: 140ms; }
.fdots i:nth-child(3) { animation-delay: 280ms; }
@keyframes f-dot {
  0%, 62%, 100% { opacity: 0.45; translate: 0 0; }
  31%           { opacity: 1;    translate: 0 calc(-3 * var(--u)); }
}

/* The event card fenn leaves when it has written to the calendar
   (reference: the real thread's own card). Borderless on the page
   grey, the sparkled mark in the left gutter and the three lines
   centred on the SCREEN rather than on what is left beside the icon —
   which is why the icon is positioned out of flow. */
.evt {
  position: relative;
  /* 22u above and 12u below, not 14/0 (Paul, 08-27: "add more space
     around the event message laser tag"). Its own grow since the same
     day, so the margin is the card's and not the answer's. */
  margin-top: calc(22 * var(--u)); margin-bottom: calc(12 * var(--u));
  text-align: center;
}
/* The chat's own rungs of the room ladder (§12's unit note), and what
   each costs the SCREEN. The thread is bottom-anchored, so a short band
   clips it from the TOP — which means every message kept at the bottom
   is paid for by one lost off the top, and the one that was falling off
   was fenn's own answer. mid (390, 375) therefore drops the closing
   thank-you: it is the newest message and the only one that adds
   nothing to "you asked, fenn booked it, the calendar is updated".
   tight (800x700) drops it too and takes back the card's top margin
   with it.

   And at tight that is still not enough, which is the whole lesson of
   this rung: the thread is bottom-anchored, so hiding a message ABOVE
   the visitor's ask moves the ask by exactly nothing — the column's
   foot is pinned and the overflow spills off the top. Measured, hiding
   all three opening friend bubbles at 800×700 left every remaining box
   on the same pixel. The only lever that moves the ask down the band is
   trimming what sits BELOW it, so tight drops the opening paragraph of
   fenn's answer and the margin that only existed to separate two
   paragraphs. That buys the 49u which carries the ask's foot — its
   gradient tail and its timestamp — back onto the top edge of the
   band, which is the reading 390 and 375 have always drawn and the one
   this rung is now held to. Without it the bubble was laid out but
   clipped away entirely, and the screen read "fenn answered and updated
   a calendar" with no question anywhere in it.

   What NEVER goes at any rung is the END of the answer — the paragraph
   it finishes on, the mark that signs it — and the card fenn left,
   which is the sentence this screen is here to say. The gate asserts
   those three against the thread's own box, and the ask's foot with
   them. */
.mini[data-room="mid"] .bub-grow--ta,
.mini[data-room="tight"] .bub-grow--ta { display: none; }
.mini[data-room="mid"] .evt { margin-top: calc(12 * var(--u)); margin-bottom: calc(4 * var(--u)); }   /* the 22/12 is wide's: at 430×932 it pushed the ask to 49% inside the band (floor 60%) */
.mini[data-room="tight"] .evt { margin-top: 0; margin-bottom: 0; }
.mini[data-room="tight"] .fmsg-in p:first-child { display: none; }
/* The separator goes with the paragraph it was separating: display: none
   leaves the second p matching `p + p`, so the 9u would otherwise hang
   at the top of a one-paragraph answer as dead band. */
.mini[data-room="tight"] .fmsg-in p + p { margin-top: 0; }
/* S38, after the card became a grid: at tight the CHROME gives way, never
   the content. The text column now takes its content's height, so the
   phone row lost ~12px at 375×667 and 402×714, and the head (80u) plus
   the composer plus the tab bar were eating 128 of a 215px screen — the
   thread was left with 86.9px to hold an ask 51.8 tall, an answer and the
   card fenn leaves, and the ask fell to 27% inside the band against the
   gate's 60% floor. The head comes down to 56u for the 24u the thread
   needs: the avatar 42u → 28u, the pad under it 10u → 4u, and 4u off the
   air below the island (the island's own clearance, var(--top), is kept —
   a header that runs under it is not a header). The back chevron is
   untouched: it is a real door and ONLY=focus finds it. */
.mini[data-room="tight"] .ch-head {
  padding-top: calc(var(--top) - 4 * var(--u));
  padding-bottom: calc(4 * var(--u));
}
.mini[data-room="tight"] .ch-head .av { --av: 28; }
/* And 6u off the air ABOVE the composer, the second lever the same
   decision names. The head alone carried 402×714 to 82% of the ask
   inside the band but 375×667 only to 61.2% — 0.6px of margin on a
   51.8px bubble against a 60% floor, which is a subpixel away from red.
   This buys 4.4px there (69.7%) and costs the composer nothing but air. */
.mini[data-room="tight"] .ph-chat > .cmp { margin-top: calc(6 * var(--u)); }
/* A box, not a bare mark: the sparkles sit at its top-left and the
   mark at its bottom-right, and BOTH stay inside it. They used to hang
   off the mark on negative offsets, which .bub-grow's own
   overflow: hidden quietly cut in half — the thread's padding box is
   the left edge of the world here, and nothing may reach past it. The
   tilt is on the mark alone for the same reason: rotating the whole
   box would push its corners back out. */
.evt-icon {
  position: absolute; left: 0; top: 50%;
  translate: 0 -50%;
  display: block;
  /* The calendar fenn's own 34×40 box (Paul, 08-27: "the fenn with stars
     should be the calendar fenn with stars — check pool cc"): Marc's
     export, a tilted calendar with the face on it and three gold stars,
     inlined in index.html so the stars breathe the way pool-cc's do. */
  width: calc(44 * var(--u)); height: calc(52 * var(--u));
}
.evt-fenn { display: block; width: 100%; height: 100%; overflow: visible; }
.evt-fenn path[fill="#FFFA63"] {
  transform-box: fill-box; transform-origin: center;
  animation: evt-star 2.4s ease-in-out infinite;
}
.evt-fenn path[fill="#FFFA63"] + path[fill="#FFFA63"] { animation-delay: -0.8s; }
.evt-fenn path[fill="#FFFA63"] + path[fill="#FFFA63"] + path[fill="#FFFA63"] { animation-delay: -1.6s; }
@keyframes evt-star { 0%, 100% { transform: scale(1); } 50% { transform: scale(0.8); } }
.whold:not(.is-playing) .evt-fenn path[fill="#FFFA63"] { animation-play-state: paused; }
.evt b { display: block; font-size: calc(17 * var(--u)); line-height: calc(21 * var(--u)); }
.evt i { display: block; font-size: calc(16 * var(--u)); line-height: calc(20 * var(--u)); color: var(--text-secondary); }
.evt em { display: block; font-size: calc(16 * var(--u)); line-height: calc(20 * var(--u)); color: var(--accent); }

/* ── The composer, both states (Figma 899:100360 idle, 949:107433
   typing) ──
   Idle: the + tile, then the field with "Message" at 20% black and the
   mic at its right. Nothing else — no send door for a field with
   nothing in it.
   Typing: the same two boxes, the placeholder replaced by real
   --text-primary ink and a gradient caret, the mic gone and the send
   INSIDE the field at its right — a gradient pill, not the round
   button this used to float outside the bar. That difference is the
   whole of P3: the sheet's send lives in the field.

   The 1u multiples below are the sheet's px at 1u ≈ 1pt: the field's
   own 0 7.5 7.5 shadow and its inset 0 1 0 white highlight, the pill's
   6/12 padding on --radius-button-sm (12) and its
   0 5 10 rgba(80,177,249,.3) glow. --radius-button-sm is named in
   tokens.css but is a px value, and everything on this surface is in
   units, so the number is written out and the token cited. */
.cmp {
  flex: 0 0 auto;
  margin: calc(12 * var(--u)) 5.5% 0;
  display: flex; align-items: center; gap: calc(8 * var(--u));
}
.cmp s { display: grid; place-items: center; flex: 0 0 auto; }
.cmp-plus {
  width: calc(44 * var(--u)); height: calc(44 * var(--u));
  border-radius: calc(16 * var(--u));
  background: var(--surface-raised);
  box-shadow: 0 calc(7.5 * var(--u)) calc(7.5 * var(--u)) rgba(0, 0, 0, 0.05),
              inset 0 calc(1 * var(--u)) 0 rgba(255, 255, 255, 0.7);
  color: var(--brand-black);
}
.cmp-plus svg { display: block; width: calc(24 * var(--u)); height: calc(24 * var(--u)); }
.cmp-field {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: calc(6 * var(--u));
  min-height: calc(44 * var(--u));
  padding: calc(5 * var(--u)) calc(5 * var(--u)) calc(5 * var(--u)) calc(12 * var(--u));
  border-radius: calc(16 * var(--u));
  background: var(--surface-raised);
  box-shadow: 0 calc(7.5 * var(--u)) calc(7.5 * var(--u)) rgba(0, 0, 0, 0.05),
              inset 0 calc(1 * var(--u)) 0 rgba(255, 255, 255, 0.7);
}
.cmp-txt {
  flex: 1 1 auto; min-width: 0;
  font-size: calc(16 * var(--u)); line-height: calc(19 * var(--u));
  font-weight: var(--body-weight);
  color: var(--text-primary);
}
.cmp-txt:empty::before { content: "Message"; color: rgba(0, 0, 0, 0.2); }
/* The caret only while somebody is actually typing — 2 x 19 on the
   brand gradient, which is the sheet's, not a plain accent bar. */
.scr--chat[data-step="3"] .cmp-txt::after {
  content: "";
  display: inline-block; vertical-align: calc(-3 * var(--u));
  width: calc(2 * var(--u)); height: calc(19 * var(--u));
  margin-left: calc(1 * var(--u));
  border-radius: calc(10 * var(--u));
  background: var(--gradient-primary);
}
.cmp-mic { color: var(--text-tertiary); margin-right: calc(6 * var(--u)); }
.cmp-mic svg { display: block; width: calc(20 * var(--u)); height: calc(20 * var(--u)); }
.cmp-send {
  padding: calc(6 * var(--u)) calc(12 * var(--u));
  border-radius: calc(12 * var(--u));   /* --radius-button-sm */
  border: calc(1 * var(--u)) solid rgba(255, 255, 255, 0.2);
  background: var(--gradient-primary);
  box-shadow: 0 calc(5 * var(--u)) calc(10 * var(--u)) rgba(80, 177, 249, 0.3);
  color: var(--brand-white);
}
/* Scoped through .cmp-field on purpose: `.cmp s` above already sets a
   display and outranks a bare `.cmp-send`, so the hidden state has to
   be at least as specific as the thing it is overriding. */
.cmp-field .cmp-send { display: none; }
.cmp-send svg { display: block; width: calc(20 * var(--u)); height: calc(20 * var(--u)); }
/* One beat wears the typing state and the rest wear idle — including
   the final frame, which is the app's own resting composer (the fix
   pass's item 5, now that the visitor is the one who sent). */
.scr--chat[data-step="3"] .cmp-field .cmp-send { display: grid; }
.scr--chat[data-step="3"] .cmp-field .cmp-mic { display: none; }
/* /SOURCE */

/* ─────────────────────────────────────────────────────────────
   13 · Short holds — retired by R2's geometry

   This section used to gate --grow down to 280px below 780px of
   viewport height. The problem it solved: .whold's height was a
   free-standing clamp with a floor, so on a short window the panel
   barely shrank while the fan's own 228.8px rise above the card did
   not, and the fan climbed into the nav band. Two thresholds — the
   fan needing the card's top ≥234px down, the door needing it ≤138px
   down — that did not overlap at 800×700, so --grow was the only
   lever that moved both at once.

   Neither term survives round 3. --hold-h is now derived from the
   viewport (100svh minus the gallery gutter), so a short window gets
   a proportionally shorter panel; --grow is derived from --hold-h, so
   the card is always exactly as tall as the panel allows; and the fan
   sits inside a vertically centred group (R4) instead of rising above
   the card, so it has no independent travel left to outrun anything.
   The 800×700 focus probe is the proof and it is unchanged.

   What a short window still needs is a smaller fan and a shorter
   phone band, because the centred group's own height (folder + two
   lines of title + two of description + the door) does not shrink
   with the viewport the way the panel does — at 800×700 and 375×667
   the 1.7× fan overflowed its column by ~16px at each end. Both
   levers, not one: dropping only the fan leaves the group's text
   crowding the crop line, and dropping only the band leaves the
   folder eating the column. Declared after the ≥900px tier so it
   also reaches a short *wide* window (1200×760), which gets the 2.2×
   fan from that tier and has no more room for it than a phone does.
   ───────────────────────────────────────────────────────────── */
@media (max-height: 780px) {
  :root { --fan-max: 1.35; }   /* --phone-band went with S38's grid card: the
    split is the text column's own content plus --phone-min now, not a share
    of the card with the group centred in it */
}
/* The screen at a short viewport that is ALSO stacking its columns
   (checker, round 3 polish). Above 900px wide the phone column spans
   the card top to bottom and the screen reads whole; below it the
   column is what the text column leaves of a card that a short window
   has already shrunk, and the two compound: at 800×700 that left 134.7px of a
   586.7px screen showing — 23.7%, a top bezel and a notch. The
   calendar and the chat survive that (their content sits in the top
   quarter of the screen) but the map does not: its pin is at 40% of
   the screen's own height, so the one thing the card is about was
   below the crop line. R3 asks for the feature to be unmistakable,
   which outranks the screen being as large as the card can hold.

   Three levers, because no one of them gets there alone. S24 pushes
   the band further — 33%→41% here (34% in the plain short tier above,
   up from 30%) — now that removing the cards' own .wcta door freed
   .wtextcol room to spend: measured, 55px of slack survives at
   800×700 and 37.7px at 375×667 (this tier's own tightest), both
   comfortably past the ≥8px floor; the centred group still doesn't
   shrink with the viewport the way the card does. The screen itself
   widens too — clamp(176px, 58cqw, 240px), up from clamp(160px,
   52cqw, 210px) — which is the D3 fix (S19 flagged 375×667's 146px
   phone and its 7.47px body as a --phone-w question, not a unit one):
   every screen this tier draws gets a bigger --u from a bigger cqh,
   this drawing's own container. And the head and the plateau drift
   still halve, because on a short column their 27px stays 14% of
   everything visible.

   Scoped to (max-width: 899px) rather than added to the block above:
   a short WIDE window (1200×760) is in the two-column tier, its
   column is already the full card height, and shrinking its screen
   would be a loss for nothing.

   Measured after: 800×700 shows 51.9% of the screen and 375×667
   70.8%, both past the pin — and 375×667's own --vis now crosses the
   wide rung (0.709, was 0.619): the phone reads the room measureCrop
   gives 1440/1920, no longer the mid tier it used to share with
   390×844 (still mid, 0.644). The crop stays at the bottom (the
   screen still overflows its column), so R10's flush-bottom read is
   unchanged. */
@media (max-width: 899px) and (max-height: 780px) {
  /* This tier is the short WINDOW's (800×700, 1200×760's narrow half),
     not a phone's — phones are carried past it by the 528px block
     below, whatever their height. */
  :root {
    --phone-w: clamp(176px, calc(0.58 * min(440px, 100vw - 88px)), 216px);   /* viewport, not cqw — same number, see the 528px block */
    --phone-head: 6px;
    --phone-float: 12px;
    --phone-drop: 74px;
  }
}

/* A phone, whatever its height (S36 b, corrected 08-28). The card is
   min(440px, 100vw − 88px): below 528px it is narrower than its cap, and
   that — not the viewport's height — is what makes a phone. Stated here,
   after the short-window tier, so a phone never inherits that tier's
   numbers: iOS Safari's layout viewport is 714 tall on a 874 screen with
   its toolbars showing and crosses 780 as they collapse, so anything
   keyed on height would resize the card mid-scroll. Paul, on his iPhone:
   "reduce by 25% the size of the folder, increase by 50% the size of the
   phone, there's top padding that could be used" — fan 1.6 → 1.2, band
   45% → 52%, phone 0.86 of the card (270px at 402, 247 at 375). */
@media (max-width: 528px) {
  :root {
    --fan-max: 1.2;
    --phone-w: clamp(180px, calc(0.86 * min(440px, 100vw - 88px)), 330px);
  }
}

/* The tablet, 700–899 (S38 Part C). Paul, 08-28, #38 at 768×611: "We need to
   revise the Ipad format. The UI looks bad in the phone. Maybe we might need
   to move the cellphone to the right on tablet. This view could be only on
   mobile." The shape is the desktop's — two columns, the mirror flipping which
   — and it is declared in the layout block at (min-width: 700px); what this
   block carries is the numbers, because a tablet has the desktop's layout and
   nothing like its room.

   Placed AFTER the short-viewport tiers for the same reason the phone block
   above is: 768×611 matches both (max-height: 780px) and the compound
   short-window block, and both of those were written for a one-column card. A
   tablet is a two-column card that happens to be short. What it does still
   take from them is --phone-head/--phone-float/--phone-drop, the vertical
   offsets a short room genuinely wants. */
@media (min-width: 700px) and (max-width: 899px) {
  :root {
    --fan-max: 1.6;
    /* 42% of the card, in viewport arithmetic and never cqw: the panel is
       100vw − 2·34 and the card is that less 60, so the card is 100vw − 128
       and the screen is 0.42 of it — 269px at 768 and 324 at 899, inside a
       column that is 280 and 346 wide. */
    --phone-w: min(calc(0.42 * (100vw - 128px)), 330px);
    /* And its own pen with it. --bezel is matched to the RENDERED stroke of
       the mascot standing beside the phone (see .mini), and this tier's
       mascot is 9vw where the desktop's is 8.5vw of a window twice as wide:
       at 800 the global clamp's 9px cap against picnic-02's 6.38px stroke is
       1.41× — the phone gate holds that ratio inside ±20%. 0.85vw between 6.5
       and 7 lands at 1.07× across the whole tier. */
    --bezel: clamp(6.5px, 0.85vw, 7px);
  }
  .wcard { width: min(1040px, calc(100% - 60px)); }
  .wtextcol { left: 30px; right: auto; width: calc(50% - 40px); }
  .wphonecol { right: 30px; left: auto; width: calc(50% - 40px); }
  .world--mirror .wtextcol { left: auto; right: 30px; }
  .world--mirror .wphonecol { right: auto; left: 30px; }
  .wtitle { font-size: clamp(30px, 4.6cqw, 44px); }
  /* Its own gutter number: the ≥900 tier's 8.5vw was sized against a gutter
     that is (panel − card)/2 + the column's 40px inset, and a tablet's is
     narrower on both counts. */
  [data-world="brunch"] .fenn,
  [data-world="laser"] .fenn,
  [data-world="karaoke"] .fenn { --fink: clamp(64px, 9vw, 112px) !important; }
}

/* ─────────────────────────────────────────────────────────────
   14 · fair questions, the short version

   Ported from origin/site/great-day (edd1fdd), markup and copy
   byte-identical. Styled with THIS skin's own .qa (§10) rather than
   great-day's — that direction needed a scrim because its own page is
   one continuous sky and the FAQ sits directly on top of it; here the
   three worlds are dioramas with the plain page grey between them, so
   the section already sits on a safe surface and needs no ground of
   its own. Self-contained: nothing here depends on a card or a sky,
   so a sibling direction can lift it unchanged, same doctrine as §12.
   ───────────────────────────────────────────────────────────── */
.faq {
  /* 800px rather than 640 (R16.1). R15 made the section compact and
     Paul's read of the deployed build was that it had gone quiet with
     it; a quarter more column is the half of "bigger" the rows
     themselves cannot supply. --col is still the outer bound, so this
     only widens the desktop read — below 900px the page's own gutter
     was already binding and mobile is untouched. */
  width: min(var(--col), 800px);
  margin: 0 auto;
  /* The bottom pad is the overlay's landing room (R11): the last row's
     answer floats down past the list, and the section has to own that
     band rather than borrowing it from the ocean below. Sized against
     the tallest of the five answers, measured, not guessed, and
     re-measured every time the rows change size — R15 cut it from
     150-210px to 96-128px, and R16.1's quarter-bigger type plus
     R16.2's deeper lift (a 6% scale on a tall panel reaches further
     down than a 2% one did) put it back up. The last answer is the
     tallest of the five and it overhangs the content box by 158px at
     320, 127px at 375-390 and 66px at 1440; the floor is what binds
     below ~910px of viewport height, because the overhang is a
     function of how the answer WRAPS and not of how tall the window
     is — a narrow phone is the worst case and it is also the
     shortest one.

     Every other number here is R15's (Paul, on the deployed build,
     "the FAQ section is too big on screen"). The .qa scaling is
     scoped to .faq rather than set on the shared .qa:
     fair-questions.html is a page whose whole content is this list,
     and it has no reason to grow because the homepage's did. */
  padding: clamp(36px, 6vh, 64px) clamp(20px, 5vw, 40px) clamp(164px, 18vh, 176px);
}
/* The 320 floor (Fable's arbitration, 08-25). The clamp's own floor is
   what binds on a narrow phone, and at 320 the tallest answer wraps one
   line more than it does at 375 — the overhang goes 127px to 158px, which
   leaves the 164px budget 5.88px of landing room. One more wrapped line
   anywhere in that answer and R11 breaks: the section grows and the ocean
   below it moves.

   One measured line of a 20px answer is 31.3px (it is exactly the gap
   between the 5.88px of room at 320 and the 37.18px at 375), so the floor
   takes a line and a little. 200px leaves 41.9px, the same order of slack
   the 360-375 tier already ships on. Bounded at 340 so that 375 and every
   viewport above it is untouched, and it moves the reserved budget only —
   no row, no type, no column width changes with it. */
@media (max-width: 340px) {
  .faq { padding-bottom: 200px; }
}
/* On phones the rows stand on the page gutter, like the panels above
   them and like fair-questions' own rows (S29, Paul 08-27): --col
   already pays --page-pad-x, and the side pad on top of it was the
   desktop column's, read on a 390 as 42px against the panels' 22.
   The open row's 1.06 lift now reaches ~10px into the gutter, which
   is what fair-questions' rows have always done. 899 is the ladder's
   own rung (§13). */
@media (max-width: 899px) {
  .faq { padding-inline: 0; }
}
/* Every length in a row × 1.25 (R16.1). A closed row goes 58.8 → 74px,
   which is the "~60 → ~75" Paul asked for, and the type goes with it —
   scaling the box alone would have made a bigger row around the same
   small question. */
.faq .qa summary {
  min-height: 70px;              /* 56 */
  padding: 24px 28px;            /* 19px 22px */
  gap: 15px;                     /* 12 */
  font-size: 20px;               /* --body-md-size, 16 */
}
.faq .qa summary::after {
  width: 14px; height: 14px;     /* 11 */
  border-right-width: 3px;       /* 2.5 */
  border-bottom-width: 3px;
}
.faq .qa .a { padding: 0 28px 25px; }
.faq .qa .a p { font-size: 20px; }
.faq .qa .a p + p { margin-top: 13px; }
/* No .faq h2 rule and no .faq .qa spacing override any more (R16.3):
   the heading wears .display and the intro .lede, which is exactly
   what fair-questions.html's own stage does, and the base .qa's
   30px/10px is exactly the spacing that page puts under its lede. The
   two lists are one component shown twice and the homepage's was
   drifting into a caption. */
/* 28px and centred, not 6px flush left (Paul, 08-27, Bim on the alias:
   "add some top padding, and place it centered"): the link is the
   list's sign-off, not a sixth row's caption. */
.faq-more { margin-top: 28px; text-align: center; }

/* ─────────────────────────────────────────────────────────────
   15 · Get the app + footer — shared component

   Ported from origin/site/great-day@77cf517 (its own §16/§7): the
   .get close and the full-black, centred .foot treatment. Markup,
   copy and every .get-icon/.get-claim rule below are byte-identical
   to that commit — only the backdrop is adapted. The badge that used
   to close this section is NOT: on 08-25 Paul replaced it with the
   shared `get fenn` door and its rules left with it.
   great-day's page is one continuous sky, so its own .get carries the
   FAQ's vivid --faq-blue down into the footer's black for a clean
   handoff; that variable doesn't exist here on purpose (S1: this
   direction's worlds are dioramas on the plain page grey, not one
   travelling sky), so .get instead starts from transparent — the
   same page grey the FAQ above it already sits on — fading to the
   same solid black .foot immediately below it. .foot itself drops
   great-day's old translucent fill + blur (this skin's own glass,
   now fully opaque and centred to match) and its own top margin, so
   nothing but this section's own gradient ever separates two blacks.
   ───────────────────────────────────────────────────────────── */
/* ── The closing zone (round 3, R7) ──────────────────────────────
   The page ends under water. .closing wraps the App Store beat and
   the footer in one box wearing the funnel's own deep-ocean gradient
   (skin-roulette.css:188-191, the −8.417deg ramp with its alpha
   fade-out, negative stops intact — D49's rule), so the black footer
   reads as the sea floor rather than as an abrupt band under a grey
   page.

   Two gradients ride on top of the funnel's own. The first is the
   handoff INTO the ocean: deep-ocean's ramp is already cyan at 0% of
   its own line, which against the plain page grey above the FAQ is a
   hard seam, so the page's own surface holds for the first quarter of
   the zone and dissolves. The second is .get's own darkening (below),
   unchanged in mechanism from the ported version — it reaches full
   black exactly at .get's bottom edge, which is exactly the footer's
   top, which is what makes the handoff seamless rather than merely
   close. */
.closing {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* The beat stands in a container of its own (Paul, 08-27, Bim on
     the alias: "place the object into a container, move it up, and it
     will look visually centred"). The zone is a column: the fade's
     own scroll room on top (--fade-room), then .get filling exactly
     the last screen minus the footer, with the icon-claim-field-doors
     group centred in it. Before, the group's centre sat 132px under
     the visible centre at 1126×854 (118 at 390, 152 at 1440) — the
     top pad was the dissolve's distance and the bottom pad 60px of
     air, so the group hung off the floor. --fade-room is what the
     dissolve still needs above the group's own head: at 854 the fade
     is 410 and the group's top lands at 231 + 192 = 423. The bottom
     air rides INSIDE the group — the icon's head and the doors' foot —
     so the centred thing is air+group+air and the group's own centre
     lands on the screen's; as padding it would push the box 60px up
     the screen. On a short viewport (320×568) .get simply grows past
     the screen and the air is what keeps the doors off the footer.
     Zone total at 1126×854: 994 (was 900). */
  display: flex; flex-direction: column;
  --fade-room: clamp(200px, 27vh, 300px);
  --air-b: clamp(52px, 7vh, 80px);
  min-height: calc(100dvh + var(--fade-room));
  /* Under the FAQ's lower part, not below it (R15). main.worlds is
     already a stacking context at z-index 1 and this zone is at auto,
     so the FAQ paints over the ocean without either of them needing a
     z-index of its own — the section has no background, so what shows
     through is exactly the top of the fade. */
  margin-top: calc(-1 * var(--ocean-under));
  /* A world that never gets observed still renders at its rest pose. */
  --wp: 0.5;
  /* Calmer than a diorama's, because this is the deep — but the zone
     only ever shows about a third of its own 0..1, so the total has to
     be bigger than what you see (Paul, 08-25): 110 buys the highest
     bubble 39px of drift and the coral on the floor 11.
     The name stays --wtravel because .wsky .fenn and the reduced-motion
     rule both read this direction's own driver names. */
  --wtravel: 110px;
  /* The same bridge .world makes onto the foundation prop frame (S17),
     repeated here because the closing zone is not a .world. */
  --p-progress: var(--wp);
  --p-travel: var(--wtravel);
  background:
    linear-gradient(to bottom,
      rgba(242, 242, 242, 1) 0%,
      rgba(242, 242, 242, 0.985) 12%,
      rgba(242, 242, 242, 0.93) 24%,
      rgba(242, 242, 242, 0.82) 36%,
      rgba(242, 242, 242, 0.65) 48%,
      rgba(242, 242, 242, 0.45) 60%,
      rgba(242, 242, 242, 0.26) 72%,
      rgba(242, 242, 242, 0.11) 84%,
      rgba(242, 242, 242, 0.03) 93%,
      rgba(242, 242, 242, 0) 100%) top / 100% var(--ocean-fade) no-repeat,
    linear-gradient(-8.417deg, rgba(242,242,242,0.7) -23.136%, rgba(0,180,255,0.7) -6.501%, rgba(0,55,255,0.7) 66.078%, rgba(0,44,230,0.525) 78.928%, rgba(0,32,204,0.35) 91.779%, rgba(0,21,178,0.175) 104.629%, rgba(0,10,153,0) 117.479%),
    var(--surface-primary);
}
/* The props live in .get's own box, not the wrapper's: --eyb is a
   bottom anchor, and the wrapper's bottom is under an opaque footer.
   .get's is the sea floor. */
.get .wsky { overflow: hidden; z-index: 0; }
/* The darkening rides above the props rather than behind them, so the
   coral and the seaweed recede into the deep instead of sitting lit on
   top of it. Its own bottom edge is .get's, so it hands off to .foot
   with no gap — the assertion the gate already makes. */
.get::after {
  content: "";
  /* The bottom is −1px, not 0 (checker, round 3 polish). .get's own
     bottom edge lands on a fractional CSS pixel at most viewports —
     737.56 at 390×844 — and a `no-repeat` layer anchored to `bottom`
     is laid out against that fraction, so the last device pixel row
     of the section was only partly covered by the darkening and the
     lit ocean underneath printed through it: one full-width row of
     rgb(72,171,251) between the black above it and the black footer
     below. Overshooting by a pixel puts the ramp's own end under the
     footer, which is opaque, so there is nothing left to round. */
  position: absolute; inset: 0 0 -1px; z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top,
      rgba(3, 11, 76, 1) 0%,
      rgba(3, 11, 76, 0.985) 10%,
      rgba(3, 11, 76, 0.93) 22%,
      rgba(3, 11, 76, 0.82) 34%,
      rgba(3, 11, 76, 0.64) 47%,
      rgba(3, 11, 76, 0.43) 60%,
      rgba(3, 11, 76, 0.25) 72%,
      rgba(3, 11, 76, 0.10) 85%,
      rgba(3, 11, 76, 0) 100%) bottom / 100% var(--ocean-dark) no-repeat;
}
.get > * { position: relative; z-index: 2; }
/* Below 738px the funnel drops its own accent tier — the floaters
   that carry no structure — and this mirrors it exactly: the bubbles
   go, the gradient and the floor stay, and the beat still reads as
   the deep. */
@media (max-height: 737px) {
  .closing .p[data-accent] { display: none; }
}

.get {
  position: relative; z-index: 1;
  text-align: center;
  /* Much taller than the ported version, and the height is the
     transition (R12): the white band above the icon is --ocean-fade
     long, the black band below it --ocean-dark, and the lit water in
     between is what is left. The padding is what buys all three the
     scroll distance a dissolve needs to stop reading as an edge.

     The bottom pad is a fifth of what it was (R15). Paul, on the
     deployed build: the flat black between the App Store badge and
     the footer links is far too tall. It measured 300px at 1440 and
     283 at 390 — 270 of pad plus the footer's own 30 of head — and
     since --ocean-dark reaches near-full black over its last third,
     essentially all of it read as an empty black section rather than
     as a floor. 84-124px put the badge close above the links and
     left the darkening the only thing between them.

     52-80px now (R16.4: Paul, again, on the deployed build — still a
     bit too much). That is the badge's own breathing room and nothing
     past it; the footer's 22px head is the rest of the gap, so 81px
     stands between the badge and the first link at 390 and 85px at
     1440. Shortening it is what re-measures the claim, and that
     measurement lives on --ocean-dark above. */
  padding: var(--fade-room) var(--page-pad-x) 0;
  /* The container (08-27): fills the zone's remaining height — one
     screen minus the footer, exactly the last screen — and centres
     the group in it. No bottom pad: see --air-b on .closing. */
  flex: 1 0 auto;
  display: flex; flex-direction: column; justify-content: center;
}
/* No border-radius and no box-shadow: R9's asset is the App Store icon
   itself, squircle and soft edge already in its alpha. A radius would
   clip that edge back off, and a box-shadow would draw a hard
   rectangle out past its transparent corners — which is exactly what
   the favicon-512 version needed one for. drop-shadow follows the
   alpha instead, and it is tuned for the ocean it now floats in
   rather than for the grey the ported version sat on. */
.get-icon {
  width: clamp(104px, 26vw, 132px); height: auto;
  margin: var(--air-b) auto 0; /* the group's head air (08-27) */
  filter: drop-shadow(0 18px 30px rgba(0, 26, 72, 0.42));
}
/* 19px is the floor, not 18 (checker, round 3 polish). White on the
   lit ocean measures 3.42:1 here, and WCAG's large-text threshold —
   which 3.42 clears and 4.5 is the alternative to — starts at 14pt
   bold, i.e. 18.66px. The ported 18px clamp resolved to exactly 18px
   at 390 (4.6vw = 17.94), 0.66px under the line, so the claim was
   being judged as body text against a ratio it could not reach
   without repainting the ocean. */
.get-claim {
  margin: 22px auto 0;
  max-width: 26ch;
  font-size: clamp(19px, 4.6vw, 22px);
  line-height: 1.35;
  font-weight: var(--body-weight-bold);
  color: var(--brand-white);
}

/* ─────────────────────────────────────────────────────────────
   16 · The Ask fenn field (S18)

   Kimi's "Try Kimi" field under their hero icon, in fenn's own
   language: the funnel's Ask Fenn bar standing under the claim, with
   the sparkle button swapped for a send door.
   ───────────────────────────────────────────────────────────── */

/* SOURCE — this block is this branch's own and self-contained: it
   depends on nothing but tokens.css, foundation's .btn shell and the
   #fennmark / #i-mic / #i-up symbols index.html inlines, which is what
   makes it liftable. A sibling direction that takes it carries a PORTED
   header citing site/worlds@<sha>:assets/skin-site-worlds.css and the
   line range this marker and its /SOURCE closer delimit, and its own
   gate diffs that range back against this file. Keep the two markers
   around the whole block when you edit inside it.

   Its ANATOMY is the funnel's Ask Fenn bar — plans.html's #bar, styled
   at assets/skin-roulette.css:1037-1077: fenn's mark at 24px, an 8px
   gap, 16px/510 text with -0.02em on it, the 44x42 mic box at the
   right, a white raised surface with the node's own inner top
   highlight. It is deliberately NOT marked as a port of that block, and
   the reason is the contract in docs/site/components.md rather than
   convenience: the funnel's is a <button> with a label <span> and this
   is a <form> with a real <input>, so the height, the radius, the
   shadow, the ink, the caret, the transition and the mic's own cursor
   all differ — a PORTED header would be a list of exceptions rather
   than a copy, and at that point it is not a port. The differences,
   named, because a reader coming from the funnel will look for them:

   · 48px → a 58px min-height, and the radius with it (16 → 20, off the
     token ladder on purpose: the funnel's pill is 16 on 48 and this
     keeps that ratio at the height Paul asked for, so the 46px circle
     inside it does not sit in a corner that is too tight for it).
     --radius-xl (26) would have made it a lozenge.
   · a centred column, not a flex child of a fixed bar.
   · the shadow. The funnel's bar floats on near-white, so its
     0 7.5px 7.5px rgba(0,0,0,.05) is enough to lift it. This one lands
     on lit ocean, where that shadow is invisible — it wears the same
     navy the App Store icon's drop-shadow uses (R9), which is the only
     shadow on this page tuned for blue water.
   · the label became an input, so it carries the caret and the ink a
     field needs and gives up the funnel's --text-secondary (3.95:1 on
     white, a button label's ratio, not a typed line's).
   · the sparkle button became the send door, and joined the shared
     .btn shell rather than carrying its own press.
   · the mic is ornamental here and the rule says so — no cursor, no
     pointer events. The homepage has no dictation engine behind it
     (Rail 6) and the field would not read as fenn's without it. */
.ask {
  width: min(100%, 540px);
  margin: 26px auto 0;
  min-height: 58px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 5px 5px 16px;
  border-radius: 20px;
  background: var(--surface-raised);
  box-shadow: 0 14px 30px rgba(0, 26, 72, 0.34),
              inset 0 1px 0 rgba(255, 255, 255, 0.7);
  text-align: left;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-base) var(--ease-btn),
              box-shadow var(--dur-base) var(--ease-btn);
}
.ask:active { transform: scale(0.985); }
.ask-in {
  flex: 1 1 auto;
  min-width: 0;
  align-self: stretch;
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font-family: inherit;
  font-size: var(--body-md-size);
  line-height: 19px;
  font-weight: var(--body-weight);
  letter-spacing: -0.02em;
  color: var(--text-primary);
  caret-color: var(--accent);
  white-space: nowrap;
  overflow: hidden;
}
.ask-mic {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 44px; height: 42px;
  border-radius: var(--radius-button-sm);
  color: var(--ink-support);
  pointer-events: none;
}
.ask-mic svg { width: 20px; height: 20px; display: block; }


.ask-in:focus { outline: none; }
.ask-in::placeholder { color: var(--ink-support); }
/* While the loop owns the value it is a suggestion, not a line
   somebody wrote, and it wears the placeholder's own ink to say so.
   The class comes off the first time the visitor types. */
.ask--auto .ask-in { color: var(--ink-support); }

/* The z-lift the FAQ rows and the buttons carry (§4, R14): the field
   is a raised surface the pointer picks up, and it lifts as one
   object — mark, text, mic and door together. --dur-base/--ease-btn
   are declared on the base state inside the port above, so leaving
   eases back exactly the way arriving eased in.

   The press is restated here rather than left to the ported rule:
   :hover and :active weigh the same, so the later one wins while a
   pointer is holding a hovered field, and that has to be the press. */
.ask:hover {
  transform: translateY(-3px) scale(1.02);
  box-shadow: 0 26px 48px rgba(0, 26, 72, 0.44),
              inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.ask:active { transform: translateY(0) scale(0.99); }

/* The send door. A .btn-family control, so the shell's own hover,
   press, easing and focus ring all arrive with the class; what is
   here is the fill, the glow and the circle. It is the drawn chat
   composer's sibling, not its twin: §12's .cmp-send is the app's own
   send, a gradient PILL living inside the field (Figma 949:107433),
   where this one is a round door standing beside a field the funnel
   never draws. Same gradient, same glow, different shape, on purpose. */
/* The send door, off the sheet (Figma 949:107433, S28): not a circle —
   a 12px-radius pill inside the field, the primary gradient run top to
   bottom, a 1px white-at-20% rim and the sheet's own 0 5 10 glow at 30%.
   40px tall, padded 14 so the arrow has air. And no avatar before the
   field any more (Paul, 08-26): the composer is the field and its door. */
.ask-send {
  flex: 0 0 auto;
  height: 40px; padding: 0 14px;
  border-radius: var(--radius-button-sm);
  background: linear-gradient(180deg, var(--accent), var(--accent-alt));
  border: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 5px 10px rgba(80, 177, 249, 0.3);
  color: var(--brand-white);
}
.ask-send:hover { box-shadow: 0 7px 14px rgba(80, 177, 249, 0.38); }
.ask-send:active { box-shadow: 0 3px 6px rgba(80, 177, 249, 0.3); }
.ask-send svg { width: 20px; height: 20px; }
/* /SOURCE */

/* The App Store door (Paul, 08-25). It was Apple's own badge until
   this round — the last control on the page carrying its own anatomy,
   a glyph and two type sizes inside a white fill, against a button
   system that had already collapsed everything else into one shell
   (S17). It is the shared primary now and needs exactly one rule of
   its own: the air between it and the field above it. Everything else
   — the 54px tier, the gradient, the glow, the lift and the press —
   arrives with .cta .btn .btn--primary. Since S27 the door stands in
   a .doors pair (get fenn → start something, like every pair), centred
   under the field; the 28px of air moves to the pair. */
.get .doors { justify-content: center; margin-top: 28px; margin-bottom: var(--air-b); /* the foot air, once .get's own bottom pad (08-27) */ }

/* fair-questions' close (S29). The same zone on the same ramp, cut to
   a page that carries two doors here and nothing else: shorter fade
   and darkening so the ramp still ends before the doors, and the pad
   that buys the dissolve sized to the doors alone. At 390×844: fade
   270, dark 287, zone 501 — the pair stands 59px off the floor on
   ~0.9 of the darkening, where the homepage's own pair stands. No
   --ocean-under pull-up: the world above has a sky of its own, so the
   zone starts at its edge — the fade's first stop is the page grey the
   sky's last stop already resolves to. */
.closing--qa {
  margin-top: 0;
  min-height: 0; /* no screen to centre in (08-27) */
  /* Its own sky, not the ocean (Paul, 08-27, 17:38, Bim on the alias:
     "do not add the ocean background, but use the other one already
     there; also add again the logo app icon and composer there"): the
     misty-forest gradient the page opens on, run the other way down —
     page grey where the world's own last stop left off, then the
     green, then the cyan — so the page is one sky from top to foot.
     The footer sits on it too, in the page's ink, rather than as a
     navy slab under a cyan sky. */
  background: linear-gradient(180deg, rgba(242,242,242,0.4) 0%, rgba(129,193,127,0.4) 48%, rgba(0,213,255,0.4) 100%),
              var(--surface-primary);
}
.get--qa::after { background: none; }
.get--qa { padding-top: clamp(96px, 14vh, 160px); }
.get--qa .get-claim { color: var(--text-primary); }
.closing--qa .foot { background: transparent; color: var(--text-primary); }
.closing--qa .foot a { color: var(--text-secondary); }
.closing--qa .foot a:hover, .closing--qa .foot .wordmark { color: var(--text-primary); }
.closing--qa .foot .wordmark svg { --mark-fill: #ffffff; --mark-line: var(--brand-black); }

.foot {
  position: relative; z-index: 2;
  /* Tight (R15): the links keep their own 10px of vertical padding, so
     the row is still a 44px target — what came off is the section's
     head and foot, which were reading as more empty black. */
  padding: 22px var(--page-pad-x) calc(26px + env(safe-area-inset-bottom));
  background: var(--ocean-floor); /* the deep's own floor, not black (S28) */
  color: var(--text-inverted);
}
.foot-in { width: var(--col); margin-inline: auto; display: flex; flex-wrap: wrap; justify-content: center; gap: 18px 26px; align-items: center; }
.foot a { font-size: var(--body-sm-size); font-weight: 510; color: rgba(255, 255, 255, 0.72); padding: 10px 0; }
.foot a:hover { color: #ffffff; }
.foot .wordmark { color: #ffffff; }
.foot .wordmark svg { --mark-fill: #1a1a1a; --mark-line: #ffffff; }
