/* landing.css — the marketing page's own scaffolding.
   NOT part of the language: the page borrows the product's tokens
   (shared/tokens.css) and the product's components (chrome.css) verbatim, and
   adds only what a marketing page needs and the chrome does not — a page
   grid, a display type scale above the chrome's, entrance/scroll choreography,
   and the tab rig that drives the mock. Every colour here is a role
   (canon#1); no shadows, no borders (canon#3); radii from the fixed scale
   (canon#4); motion is opacity + small transforms only (canon#9). */

*{box-sizing:border-box;margin:0}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{background:var(--paper);color:var(--ink);
  font-family:var(--font-body);font-size:16px;line-height:1.55;
  letter-spacing:-0.01em;overflow-x:clip}
button{appearance:none;-webkit-appearance:none;font:inherit;color:inherit}
/* the chrome's components declare their own display, which outranks the UA's
   [hidden] — the trap chrome.css warns about. On a page that scripts state by
   hiding things, hidden has to mean hidden. */
[hidden]{display:none!important}
a{color:inherit;text-decoration:none}
img,svg{display:block}

/* ---------- the page grid ----------
   One measure for prose, one wider for the stages the mocks sit in. */
.wrap{--inset:20px;width:min(100% - 2 * var(--inset),1140px);margin-inline:auto}
.wrap.wide{--inset:16px;width:min(100% - 2 * var(--inset),1400px)}
.wrap.narrow{width:min(100% - 2 * var(--inset),820px)}
section{position:relative}

/* ---------- display type ----------
   DM Sans 600 above the chrome's scale (canon#5); the chrome's own sizes are
   untouched inside every .app mock, which is the point — the product looks
   like itself on the page that sells it. */
.dsp{font-family:var(--font-title);font-weight:600;letter-spacing:-0.035em;
  line-height:1.03;text-wrap:balance}
h1.dsp{font-size:clamp(38px,6.6vw,84px)}
h2.dsp{font-size:clamp(30px,4.2vw,54px);line-height:1.06;letter-spacing:-0.03em}
h3.dsp{font-family:var(--font-title);font-weight:600;font-size:19px;
  letter-spacing:-0.02em;line-height:1.3}
.lede{font-size:clamp(16px,1.45vw,19px);line-height:1.55;color:var(--ink-2);
  text-wrap:pretty}
.kick{font-size:12px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:var(--ink-2)}
.mono{font-family:var(--font-emph)}

/* ---------- nav ----------
   A floating stadium that takes a tint only once the page has moved under it. */
.nav{position:fixed;top:0;left:0;right:0;z-index:60;padding:7px 0;
  transition:padding var(--t-med) var(--ease)}
.nav-in{display:flex;align-items:center;gap:18px;height:52px;padding:0 8px 0 18px;
  border-radius:var(--r-full);background:transparent;
  transition:background var(--t-med) var(--ease),padding var(--t-med) var(--ease)}
/* Once the page has moved under it the bar FROSTS what passes beneath rather
   than hiding it: the fill has to stay translucent for the filter to have
   anything to do — an opaque --panel would blur a surface nobody can see.
   The blur is clipped to the stadium by its own radius, so the frosted patch
   is the bar's shape, not a band across the page. -webkit- first: Safari
   still ships the filter only under the prefix. */
.nav.stuck .nav-in{background:color-mix(in srgb,var(--panel) 72%,transparent);
  -webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}
/* no backdrop-filter: the translucent fill would let text read straight
   through the bar, so that one case steps back to the opaque panel */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .nav.stuck .nav-in{background:var(--panel)}}
.nav .brand{display:inline-flex;align-items:center;gap:9px;flex:none}
.nav .brand .g-logo{width:19px;height:19px}
.nav .brand .wordmark{font-size:17px}
.nav-links{display:flex;align-items:center;gap:4px;margin-left:auto}
.nav-links a{font-size:13.5px;color:var(--ink-2);padding:7px 12px;
  border-radius:var(--r-full);transition:color var(--t-fast) var(--ease),
    background var(--t-fast) var(--ease)}
.nav-links a:hover{color:var(--ink);background:var(--accent-soft)}
.nav .nav-cta{display:flex;align-items:center;gap:4px;flex:none;margin-left:auto}
.nav .btn{border-radius:var(--r-full)}
.nav .btn.solid{padding:9px 18px;white-space:nowrap}
@media (max-width:820px){.nav-links{display:none}}
/* narrow: the bar keeps the brand and the one action. Sign in is a link the
   thread page offers anyway, and a row that overflows is a page that pans
   (canon#10). */
@media (max-width:560px){
  .nav{padding:5px 0}
  .nav-in{height:48px;padding:0 6px 0 14px;gap:10px}
  .nav .brand .wordmark{font-size:15px}
  .nav .brand .g-logo{width:17px;height:17px}
  .nav .nav-cta .btn:not(.solid){display:none}
  .nav .btn.solid{padding:8px 15px;font-size:13.5px}}
/* the bar is fixed, so a bare anchor jump would tuck the target's first
   line beneath it — every jump target clears the bar's height plus a
   breath. #artifacts is the closing call, whose own 150px lead the jump
   should spend, not stack: the negative margin lands its HEADING just
   under the bar (150 - 54 = 96). */
#how,#skills,#compute{scroll-margin-top:90px}
#artifacts{scroll-margin-top:-54px}

/* ---------- entrance & reveal ----------
   One choreography for the whole page: a small rise out of a soft blur.
   Reduced motion drops to the settled state with no transition at all. */
[data-rise]{opacity:0;transform:translateY(16px);filter:blur(7px);
  transition:opacity 620ms var(--ease),transform 620ms var(--ease),
    filter 620ms var(--ease);transition-delay:var(--d,0ms)}
[data-rise].in{opacity:1;transform:none;filter:none}
@media (prefers-reduced-motion:reduce){
  [data-rise]{opacity:1;transform:none;filter:none;transition:none}}

/* ---------- hero ---------- */
.hero{padding:140px 0 0;text-align:center}
/* the hero line sits a rank lighter than the page's other display heads —
   the rotating word carries the emphasis, the sentence around it recedes */
.hero > .wrap h1{margin:0 auto;max-width:15ch;font-weight:500}
.hero .lede{margin:20px auto 0;max-width:60ch}
.eyebrow{display:inline-flex;align-items:center;gap:9px;margin-bottom:26px;
  padding:6px 14px 6px 6px;border-radius:var(--r-full);background:var(--panel);
  font-size:13px;color:var(--ink-2);transition:background var(--t-fast) var(--ease)}
.eyebrow:hover{background:var(--panel-2)}
.eyebrow .tag{padding:3px 9px;border-radius:var(--r-full);
  background:var(--ink);color:var(--paper);font-size:10.5px;font-weight:600;
  letter-spacing:.09em;text-transform:uppercase}
.eyebrow .ic{width:14px;height:14px;color:var(--ink-3)}
.hero-cta{display:flex;align-items:center;justify-content:center;gap:8px;
  margin-top:30px;flex-wrap:wrap}
.hero-cta .btn{font-size:14.5px;padding:13px 24px;border-radius:var(--r-full)}
.hero-cta .btn.solid:hover{opacity:.9}
@media (max-width:640px){.hero{padding-top:112px}}

/* ---------- the hero rotor ----------
   The headline's object cycles: each word arrives in its own face, its own
   step of the palette, its own entrance. This is the ONE deliberately
   polyglot surface on the page — the extra faces are loaded for this span
   alone (a marketing flourish, not a change to the type canon) and none of
   them may read as the heading's own DM Sans — the rotating word is a
   different voice by construction. Colour is the palette's blues only, and
   only the steps that clear display-size contrast on --paper
   (--c1, --c3, --c4, --c5; --c2/--c6 are too pale in light).
   Every entrance is opacity + a small transform (canon#9); reduced motion
   swaps words with no motion at all. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.rotor{display:block;position:relative;height:1.14em;margin-top:.05em}
.rotor-stage{position:absolute;inset:0}
.rotor-stage.clip{overflow:hidden}
.rotor-word{position:absolute;left:0;right:0;top:0;white-space:nowrap;
  line-height:1.1;will-change:transform,opacity,filter}
/* the faces — each sized so every voice sits on the same visual rank */
.rotor-word.f-slab{font-family:'Zilla Slab',Georgia,serif;font-weight:600;
  font-size:.97em;letter-spacing:-0.02em}
/* a connected cursive: never given the per-letter entrance, which would
   split its joins across spans and leave the word permanently disconnected */
.rotor-word.f-script{font-family:'Dancing Script',cursive;font-weight:600;
  font-size:1.14em;letter-spacing:0}
.rotor-word.f-inst{font-family:'Instrument Serif',Georgia,serif;
  font-style:italic;font-weight:400;font-size:1.08em;letter-spacing:0}
.rotor-word.f-hand{font-family:'Caveat',cursive;font-weight:600;
  font-size:1.2em}
/* the entrances */
.rotor-word.in-rise{animation:rot-rise 620ms var(--ease) both}
@keyframes rot-rise{from{opacity:0;transform:translateY(.45em);filter:blur(9px)}}
.rotor-word.in-zoom{animation:rot-zoom 560ms var(--ease) both}
@keyframes rot-zoom{from{opacity:0;transform:scale(.72);filter:blur(10px)}}
.rotor-word.in-focus{animation:rot-focus 660ms var(--ease) both}
@keyframes rot-focus{from{opacity:0;transform:scale(1.18);filter:blur(18px)}}
.rotor-word.in-flip{transform-origin:50% 100%;
  animation:rot-flip 620ms var(--ease) both}
@keyframes rot-flip{from{opacity:0;transform:perspective(700px) rotateX(72deg)}}
.rotor-word.in-swing{transform-origin:50% 92%;
  animation:rot-swing 640ms var(--ease) both}
@keyframes rot-swing{from{opacity:0;transform:rotate(-7deg) translateY(.25em)}}
.rotor-word.in-clip{animation:rot-clip 560ms var(--ease) both}
@keyframes rot-clip{from{transform:translateY(112%)}}
.rotor-word.in-type{animation:rot-fade 220ms var(--ease) both}
@keyframes rot-fade{from{opacity:0}}
.rotor-word.typing::after{content:"";display:inline-block;width:.45ch;
  height:.78em;margin-left:.1ch;vertical-align:-.05em;background:currentColor;
  animation:rot-caret 750ms steps(1,end) infinite}
@keyframes rot-caret{50%{opacity:0}}
/* letters: each glyph takes the page's own rise, a beat apart */
.rotor-word.in-letters .lt{display:inline-block;opacity:0;
  transform:translateY(.5em);filter:blur(6px);
  transition:opacity 480ms var(--ease),transform 480ms var(--ease),
    filter 480ms var(--ease)}
.rotor-word.in-letters.go .lt{opacity:1;transform:none;filter:none}
/* one quiet leave for every voice */
.rotor-word.out{opacity:0;transform:translateY(-.3em);filter:blur(7px);
  transition:opacity 260ms var(--ease),transform 260ms var(--ease),
    filter 260ms var(--ease);animation:none}
@media (prefers-reduced-motion:reduce){
  .rotor-word{animation:none!important;transition:none!important}
  .rotor-word.in-letters .lt{opacity:1;transform:none;filter:none;
    transition:none}
  .rotor-word.typing::after{animation:none}}

/* ---------- the hero stage ----------
   The Anthropic move: the frame arrives inset and small, and grows to its
   full measure as the reader scrolls it into place. `--p` (0→1) is written
   by the page each frame; every value below is derived from it, so reduced
   motion only has to pin --p at 1. */
.stage-scroll{margin-top:34px;padding-bottom:24px}
.hero-stage{--p:0;
  /* a laptop's proportions (16:10), and it ENDS at the page's full measure: the
     frame arrives inset and grows to fill the width, which is the whole move.
     Its height follows from the ratio and is allowed to exceed the viewport —
     a hero the reader scrolls THROUGH, not one squeezed to fit above the fold. */
  --w:min(100% - 160px,1320px);
  width:calc(var(--w) * (0.58 + 0.42 * var(--p)));
  margin-inline:auto;
  transform:translateY(calc((1 - var(--p)) * 26px));
  transition:none}
.hero-stage .frame-in{border-radius:var(--r-lg);background:var(--panel);
  padding:10px;overflow:hidden}
.appframe{position:relative;border-radius:var(--r-md);overflow:hidden;
  background:var(--paper);aspect-ratio:16/10}
/* the hero centres its column; the product inside it does not inherit that */
.appframe,.app{text-align:left}
.appframe .app{height:100%}
/* narrow: a 16:10 window 340px wide would be 212px tall — the shape stops
   being a laptop and starts being a letterbox, so the frame takes a height */
@media (max-width:900px){.appframe{aspect-ratio:auto}.appframe .app{height:560px}}
@media (max-width:640px){.appframe .app{height:520px}}

/* ---------- the mock's own state ----------
   The panes, pages and folds are the product's; these are the few rules a mock
   needs that a running app gets from its own renderer. */
.app.mock .thread-slot .composer{padding-inline:0}
.app.mock .page{padding:0}
/* below 768 the chrome hides the thread region unless both panels are marked
   shut; a mock has no region toggles to press before it is read */
@media (max-width:768px){
  .app.mock .main,.app.stage .main{visibility:visible}
  .app.mock .thread-host:not(.off) .thread-slot.live{visibility:visible}}
.fold-body[hidden],.grp-body[hidden],.fu-list[hidden]{display:none}
.grp[aria-expanded="false"] .chev{transform:none}
/* the model chip carries its harness mark, as the composer's does */
.model-chip .h-mark{width:13px;height:13px;color:inherit;margin-right:1px}
.tc-fu-count{cursor:pointer}
.thread-card,.gal-row,.acard{cursor:pointer}
/* a hovered artifact row says it opens something */
.acard:hover{background:var(--panel-2)}

/* pages render their own header, so the mock's `.page` keeps the chrome's
   own padding; the details region is collapsed on every non-thread view */
.app.mock .page{padding:0 12px}
/* the fields are the product's own textareas now; only the mock's cards shelf
   and the fold's flat steps need saying */
.app.mock .shelf[hidden],.app.mock .fold-step[hidden]{display:none}
.app.mock .acards{margin-top:10px}
/* the home composer's logo is Logo({size:18}), not the 14px sidenav mark */
.app.mock .home-composer .pill-lead .g-logo{width:18px;height:18px}
.app.mock .page[data-page="home"]{padding:0}
.fu-pane[hidden],[data-dbody][hidden],[data-dhead][hidden]{display:none}
.app.mock .details-in{position:relative}
.app.mock .fu-pane{height:100%}
/* the skills shelf renders as cards; rows view swaps the grid */
.page.cards-view .gal-rows{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:10px}
.page.cards-view .gal-row{grid-template-columns:1fr;padding:16px 18px;border-radius:var(--r-lg)}
.page.cards-view .gal-row .from,.page.cards-view .gal-row .agent.mdl{display:none}
.skill-field{display:flex;flex-direction:column;gap:6px;margin-bottom:18px}
.field-label{font-size:12px;color:var(--ink-2)}
.skill-trigger{font-size:13.5px;line-height:1.6;color:var(--ink);margin:0}
.skill-doctrine{font-size:13px;line-height:1.6;color:var(--ink-2)}
.skill-doctrine h4{font-family:var(--font-title);font-weight:600;font-size:13.5px;
  color:var(--ink);margin:14px 0 4px}
.skill-doctrine p{margin:0 0 8px}
.skill-doctrine code{font-family:var(--font-mono);font-size:12px;background:var(--code);
  border-radius:var(--r-xs);padding:1px 5px}
/* a bar figure's key, since bars carry no direct label on the mark */
.fig-key{display:flex;gap:14px;margin-top:6px;font-size:11px;color:var(--ink-2)}
.fig-key span::before{content:"";display:inline-block;width:8px;height:8px;border-radius:2px;
  margin-right:5px;vertical-align:-1px}
.fig-key .k1::before{background:var(--c1)}
.fig-key .k2::before{background:var(--c4)}
.fig-svg .s1f{fill:var(--c1)}
.fig-svg .s2f{fill:var(--c4)}
.fig-svg .s3f{fill:var(--c3)}
.fig-svg .tk.cat{font-size:7.5px}

/* A report takes the whole frame — the panel is `.bare` and the guest document
   is the surface, with the doc header floating over it. The mock only supplies
   what a real viewport would: the frame's height. */
.appframe .skill-modal{padding:0}
.appframe .modal-frame.doc{width:100%;height:100%;max-height:100%}
.appframe .modal-frame.doc .doc-anno{height:100%}
.appframe iframe.rep-frame{width:100%;border:0;background:var(--paper);display:block;
  height:100%;--text:14px}
/* the frame grows to the document's own height and the BARE panel scrolls it,
   which is what `host-owns-scroll` in the guest is counting on */
.appframe .modal-frame.doc .skill-modal-panel.bare{overflow-y:auto;scrollbar-width:none}
.appframe .modal-frame.doc .skill-modal-panel.bare::-webkit-scrollbar{display:none}
.appframe .modal-frame:not(.doc){margin:26px}

/* a figure opens in the ORDINARY modal (ArtifactModal.tsx): header with the
   kind tags, then the guest frame at its self-reported height */
.appframe .modal-frame:not(.doc) .skill-modal-panel{display:flex;flex-direction:column;
  max-height:100%;overflow-y:auto;scrollbar-width:none;min-width:min(880px,90%)}
.appframe .modal-frame:not(.doc) .skill-modal-panel::-webkit-scrollbar{display:none}
.appframe .modal-frame:not(.doc) iframe.rep-frame{width:100%;border:0;
  background:var(--paper);display:block;border-radius:var(--r-md);min-height:280px}

/* ---------- the artifact modal, inside the frame ----------
   The product portals it to the body over the whole viewport; here the frame
   IS the viewport, so it is absolute within it and the scrim covers the app. */
.appframe .skill-modal{position:absolute;padding:26px}
.appframe .skill-modal[hidden]{display:none}
.appframe .modal-frame{max-height:100%}
.rep-body h3.doc-sub{font-family:var(--font-title);font-weight:600;font-size:15px;
  letter-spacing:-0.02em;margin:18px 0 6px}
.rep-body h3.doc-sub:first-child{margin-top:4px}
.rep-body p{font-size:13.5px;line-height:1.62;color:var(--ink);margin:0}

/* the checkpoint's own styles are the runtime's, linked verbatim from
   artifacts/checkpoint.css — nothing is restated here. */

/* the mock shells: chrome's own .app grid, re-proportioned for a stage that
   is not a whole window */
.app.mock{--w-side:272px;--w-details:290px}
/* The mock sets its own region widths, which sit at the same specificity as
   chrome's `.no-side` / `.no-details` and land later in the cascade — so the
   toggles set the class and nothing moved. Restate the collapse ON the mock. */
.app.mock.no-side{--w-side:0px!important}
.app.mock.no-details{--w-details:0px!important}
.app.mock.no-side>.side,.app.mock.no-details>.details{opacity:0}
.app.mock .side-in{--wc-side:272px;padding:0 16px}
.app.mock .details-in{--wc-details:290px;padding:0 12px 14px}
.app.stage .details-in{--wc-details:280px}
.app.mock .main{padding:0 16px}
.app.mock .thread-scroll>:not(.thread-h){max-width:680px}
/* dropping a region means dropping its TRACKS too — a display:none region
   leaves the next child in the collapsed column otherwise (the same trap the
   stage hits below) */
@media (max-width:1100px){
  .app.mock{grid-template-columns:var(--w-side,272px) 8px minmax(0,1fr)}
  .app.mock>.details,.app.mock>.rz-details{display:none}}
@media (max-width:760px){
  .app.mock{grid-template-columns:minmax(0,1fr)}
  .app.mock>.side,.app.mock>.rz-side{display:none}}
/* Below 768 the chrome switches to its one-region-at-a-time model and hides
   `.main` unless BOTH panels are marked closed (chrome.css § max-width:768px).
   A mock has no region toggles to press, so it says outright that the thread
   is the region on screen. */
@media (max-width:768px){
  .app.mock .main,.app.stage .main{visibility:visible}}

/* ---------- the figure inside the mock ----------
   A figure is guest content: it brings its own paper inside the embed, and it
   is the one surface in the whole page allowed chromatic colour — series marks,
   three DISTANT steps of the one blue (canon#2), each carrying a direct label
   so the hue never has to be read alone. */
.fig-guest{background:var(--paper);border-radius:var(--r-md);padding:13px 15px 11px}
.fig-cap{font-size:11.5px;color:var(--ink-2);margin-bottom:4px}
.fig-svg{width:100%;height:auto;display:block;overflow:visible}
.fig-svg .ax{stroke:var(--ink-3);stroke-width:1}
.fig-svg .tk{fill:var(--ink-2);font-family:var(--font-body);font-size:9.5px;
  text-anchor:middle;font-variant-numeric:tabular-nums}
.fig-svg .axl{fill:var(--ink-3);font-family:var(--font-body);font-size:10px;
  text-anchor:middle}
.fig-svg path{fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}
.fig-svg .s1{stroke:var(--c1)}
.fig-svg .s2{stroke:var(--c4)}
.fig-svg .s3{stroke:var(--c3)}
.fig-svg .lbl{font-family:var(--font-body);font-size:10.5px;font-weight:600;
  letter-spacing:-0.01em}
.fig-svg .t1{fill:var(--c1)}
.fig-svg .t2{fill:var(--c4)}
.fig-svg .t3{fill:var(--c3)}
.fig-note{font-size:11.5px;line-height:1.5;color:var(--ink-2);margin-top:7px}
.fig-note .agent{font-size:11.5px;color:var(--ink)}

/* ---------- the three-point strip ----------
   One surface per claim, not a box in a box: the stage IS the tile — a
   generous panel window the product plays in — and the words sit under it
   on bare paper, the way a figure carries its caption. */
.points{padding:112px 0 0}
.points-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:44px 14px}
.point h3{margin:0 2px 8px}
.point p{font-size:14.5px;line-height:1.55;color:var(--ink-2);margin:0 2px}
/* the stages need real width to play in: below ~1080 three columns would
   squeeze the traces into clipping, so the strip stacks a good deal earlier
   than the old text-only cards did */
@media (max-width:1080px){.points-grid{grid-template-columns:1fr}}

/* ---------- the vignettes ----------
   The cards no longer assert — each is a stage of the product's own
   components enacting the card's claim on loop (landing.js § the
   vignettes). The stage is the strip's one surface (--panel on the page's
   paper, tint separation per canon#3); anything that carries its own
   surface inside it steps once more, to --panel-2, exactly as the chrome
   nests its tints. Ambient on purpose: non-interactive, and the showcase
   below is where the reader drives. */
.vig .vig-stage{--text:13px;height:232px;margin-bottom:22px;padding:22px 24px;
  border-radius:var(--r-lg);background:var(--panel);overflow:hidden;
  font-size:var(--text);line-height:1.5;pointer-events:none;user-select:none}
/* the loop's rewind: the stage's CONTENT dips out for a beat while the
   markup resets — the surface itself stays put. Declared only under .fade
   so the rows' own step transitions are untouched mid-play. */
.vig-stage.fade>*{opacity:0;transition:opacity 280ms var(--ease)}
/* every stage starts its content on the same line — the air the window
   grants is spent BETWEEN the rows below, never above the first one */
.vig-stage .trace{gap:12px;font-size:var(--text);line-height:1.5}
/* the surfaces inside the surface: one tint step up, never a border */
.vig-stage .pill-input{background:var(--panel-2);align-items:center}
.vig-stage .embed{background:var(--panel-2)}
/* the landed flash: the chrome's row-landed, restated for a stage with no
   trace around the embed — a halo one step past the embed's own edge,
   reading against the stage's tint. */
.vig-land{border-radius:var(--r-lg);padding:6px;margin:-6px -6px 0}
.vig-land.row-landed{animation:vig-landed 1.6s var(--ease)}
@keyframes vig-landed{from{background:var(--panel-2)}to{background:transparent}}
@media (prefers-reduced-motion:reduce){.vig-land.row-landed{animation:none}}
/* trace: the subagent narration wraps to two lines inside a narrow card */
.vig-stage .trace-row{align-items:flex-start}
/* machine: the statement's ledger rows, and the dot that says "you are here"
   quietly insists on it (chrome's own pulse; static under reduced motion) */
.vig-stage .land-row{padding:10px 2px}
/* the first row starts on the stage's shared start line — its rhythm
   padding belongs between rows, not above the content */
.vig-stage .land-row:first-child{padding-top:0}
.vig-stage .land-row .h-mark{width:15px;height:15px;flex:none;color:var(--ink)}
.vig-stage .here-dot{animation:pulse 2.2s var(--ease) infinite}
.vig-tally{display:flex;align-items:baseline;gap:8px;margin-top:14px;padding:0 2px}
/* thread: a fragment of the record, and the three depths that hang off one
   sentence — indentation says nesting, never a line (canon#3) */
.vig-rec{font-size:var(--text);line-height:1.6;color:var(--ink)}
.vig-depths{margin:16px 0 0 20px;display:flex;flex-direction:column;gap:12px}
.vig-row{display:flex;align-items:center;gap:9px;min-width:0}
.vig-row .ic{width:13px;height:13px;color:var(--ink-2);flex:none}
.vig-row>span{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vig-row .vig-ink{color:var(--ink)}
.vig-row .vig-fu{font-weight:500;color:var(--ink)}
.vig-row .g-run{flex:none;margin-left:2px}
/* skill: the folder read as a directory, then the composer that carries it */
.vig-skill{display:flex;flex-direction:column;gap:10px;margin-bottom:18px;padding:0 2px}
.vig-file{display:flex;align-items:baseline;gap:10px}
.vig-file .agent{white-space:nowrap}
.vig-file .meta{margin-left:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vig-tree{display:flex;flex-direction:column;gap:8px;margin-left:16px}
.vig-frow{display:flex;align-items:baseline;gap:10px;min-width:0}
.vig-frow .fn{font-family:var(--font-mono);font-size:12px;color:var(--ink);white-space:nowrap}
.vig-frow .meta{margin-left:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vig-stage .pill-input{pointer-events:none;padding:9px 12px}
.vig-stage .pill-input .input{font-size:13px;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
/* keeps: the embed row lands, then joins the shelf's spine of dates */
.vig-stage .embed-head{padding:10px 14px;gap:10px}
.vig-stage .embed-head .t{font-size:13px}
.vig-shelf{margin-top:16px;display:flex;flex-direction:column;gap:4px;padding:0 2px}
/* the joining row: closed to nothing until it joins, then it slides open
   and the older rows make room — the 0fr→1fr move the rail's sentences use */
.vig-join{display:grid;grid-template-rows:0fr;opacity:0;
  transition:grid-template-rows var(--t-med) var(--ease),
    opacity var(--t-med) var(--ease)}
.vig-join-in{overflow:hidden;min-height:0}
.vig-join.in{grid-template-rows:1fr;opacity:1}
@media (prefers-reduced-motion:reduce){
  .vig-join{grid-template-rows:1fr;opacity:1;transition:none}}
.vig-shelf .label{font-size:12px;margin-bottom:3px}
.vig-srow{display:flex;align-items:baseline;gap:12px;padding:5px 0;min-width:0}
.vig-srow .nm{flex:1;min-width:0;font-family:var(--font-title);font-weight:600;
  font-size:13px;letter-spacing:-0.01em;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vig-srow .when{font-size:12px;color:var(--ink-2);font-variant-numeric:tabular-nums;
  white-space:nowrap}
@media (prefers-reduced-motion:reduce){.vig-stage .here-dot{animation:none}}

/* ---------- section heads ---------- */
.head{max-width:660px}
.head .kick{display:block;margin-bottom:18px}
.head .lede{margin-top:20px;max-width:52ch}
.sec{padding:132px 0 0}

/* ---------- the showcase ----------
   The tab rail drives one shell: the sidebar is shared and constant, the
   thread and the details region swap. Nothing about the mock is a picture —
   it is the product's own components under the product's own stylesheet. */
.show{margin-top:52px;display:grid;grid-template-columns:296px 1fr;gap:20px;
  align-items:start}
/* the showcase spends the window it is given: its wrap tracks the height the
   appframe already allows the frame (its max-height below), so the 16:10 shell
   is exactly as large as the viewport can show whole — a big window fills
   instead of stranding its room around a fixed measure, and a short one no
   longer leaves dead panel where the transferred max-width stops the frame
   before the stagebox ends. 336px = rail + gap + stagebox padding. */
#how .wrap.wide{width:min(100% - 2 * var(--inset),
  336px + max(420px,100vh - 150px) * 1.6)}
/* The rail reads as the product's own trace: a state glyph per row — nothing
   yet, the pulsing dots while its scene plays, the done disk once it has
   (the page borrowing _design#5's closed set). No loading bars: progress is
   said the way the product says it, and selection is tint (_design#9). */
.rail{display:flex;flex-direction:column;gap:2px;position:sticky;top:96px}
.tab{position:relative;display:flex;align-items:flex-start;gap:11px;width:100%;
  text-align:left;border:0;background:none;padding:12px 14px;
  border-radius:var(--r-lg);cursor:pointer;
  transition:background var(--t-fast) var(--ease)}
.tab:hover{background:var(--accent-soft)}
.tab.on{background:var(--panel)}
.tab .g{flex:none;width:13px;height:13px;margin-top:4px;display:inline-flex;
  align-items:center;justify-content:center}
.tab .tx{min-width:0;flex:1}
.tab .t{display:block;font-family:var(--font-title);font-weight:600;
  font-size:14.5px;letter-spacing:-0.02em;color:var(--ink-2);
  transition:color var(--t-fast) var(--ease)}
.tab.on .t,.tab:hover .t{color:var(--ink)}
/* the sentence belongs to the open tab: closed rows are a list of names */
.tab .s{display:grid;grid-template-rows:0fr;opacity:0;font-size:13px;
  line-height:1.5;color:var(--ink-2);
  transition:grid-template-rows var(--t-med) var(--ease),
    opacity var(--t-med) var(--ease),margin var(--t-med) var(--ease)}
.tab .s>span{overflow:hidden;min-height:0}
.tab.on .s{grid-template-rows:1fr;opacity:1;margin-top:3px}

/* ---------- the scroll tour ----------
   The showcase rides the reader's own scroll, exactly as the statement does:
   the track is eight segments long, the shell pins for its length, and the
   segment the scroll is in is the gesture on stage. Only when the page ARMS
   it (motion allowed, shell fits the viewport) — otherwise the track has no
   height, the pin does not stick, and the timed tour plays as before. */
.show-track.armed{height:var(--tour-h)}
.show-track.armed .show-pin{position:sticky;top:var(--tour-top,96px)}

.stagebox{position:relative;background:var(--panel);border-radius:var(--r-lg);
  padding:10px}
/* the tour needs the WHOLE shell on screen: on wide-but-short viewports the
   16:10 frame outgrows the window, so past that point the frame gives up
   the ratio before the pin gives up the tour (arm() in landing.js measures
   the result). The floor keeps the scenes legible; below it the tour
   simply does not arm. */
.stagebox .appframe{max-height:max(420px,calc(100vh - 150px))}
.stagebox .appframe .app{--h:566px}
/* the shell keeps ONE sidebar across every scene — only the thread and the
   details region swap, so the mock morphs rather than cutting */
.app.stage{--w-side:236px;--w-details:280px;
  grid-template-columns:var(--w-side) 8px 1fr}
.app.stage .side-in{--wc-side:236px}
/* the shared sidebar carries the real anatomy — nav, groups, account — at
   the stage's density */
.app.stage .side-in{padding:0 14px}
.app.stage .side-threads{padding-bottom:64px}
.scenes{position:relative;min-width:0;overflow:hidden}
.scene{position:absolute;inset:0;display:grid;min-width:0;
  grid-template-columns:1fr 8px var(--w-details,280px);
  grid-template-rows:minmax(0,1fr);
  opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity var(--t-med) var(--ease)}
.scene.on{opacity:1;visibility:visible;pointer-events:auto}
/* a one-column scene: the page views (details closed, as Shell.tsx derives
   it) and the thread scenes whose whole subject is the artifact — the panel
   would only repeat what the stage is already showing */
.scene.full{grid-template-columns:minmax(0,1fr)}
/* with the width to spend, a full scene's thread takes the product's own
   measure and centers, exactly as the composer already does (--thread-col) */
.scene.full .thread-scroll>:not(.compute-in){max-width:var(--thread-col);
  margin-inline:auto}
@media (prefers-reduced-motion:reduce){.scene{transition:none}}
/* the stage is narrower than a window, and the thread column is what every
   scene is actually about — so the SIDEBAR is what goes first here (the
   reverse of the hero shell, which has a whole page's width to spend). The
   details region only leaves once one column is all there is room for. */
@media (max-width:1280px){
  /* removing the two leading tracks' CONTENT is not enough — the scenes would
     land in the 0px first column. The grid loses the tracks with them. */
  .app.stage{grid-template-columns:minmax(0,1fr)}
  .app.stage>.side,.app.stage>.rz-side{display:none}}
@media (max-width:900px){
  .scene{grid-template-columns:1fr}
  .scene>.details,.scene>.rz-details{display:none}}

@media (max-width:1000px){
  .show{grid-template-columns:1fr;gap:16px}
  .rail{position:static;flex-direction:row;gap:6px;
    /* a deliberate slidable row, with the edge fade the canon requires
       (canon#10) — never a page that pans */
    overflow-x:auto;scrollbar-width:none;scroll-snap-type:x mandatory;
    /* the bleed is exactly the wrap's own inset — a hard-coded one made the
       page 4px wider than the viewport, which is a page that pans (canon#10) */
    margin-inline:calc(-1 * var(--inset));padding-inline:var(--inset);
    /* the fade is asymmetric at rest: there is nothing before the first tab,
       so dimming it would read as a broken control. The trailing edge is where
       the row actually continues. */
    mask-image:linear-gradient(to right,#000 0,#000 calc(100% - 34px),
      transparent 100%)}
  .rail::-webkit-scrollbar{display:none}
  .tab{width:auto;flex:none;scroll-snap-align:start;padding:9px 14px;
    border-radius:var(--r-full);align-items:center;gap:8px}
  .tab .g{margin-top:0}
  .tab .s{display:none}
}

/* ---------- scene-local pieces ----------
   Only geometry the chrome has no opinion about (a mock has no viewport, so
   the floating pieces are placed here rather than measured). */
.scene .anno-float,.scene .menu.bar,.scene .note-card{position:absolute}
.scene .float-l{left:26px}
.scene .selbar{top:112px;opacity:0;transform:translateY(-4px);
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease)}
.scene .selbar.up{opacity:1;transform:none}
.scene .noteinput{top:150px;width:min(420px,74%);opacity:0;transform:translateY(-4px);
  transition:opacity var(--t-fast) var(--ease),transform var(--t-fast) var(--ease)}
.scene .noteinput.up{opacity:1;transform:none}
.scene .notecard{top:150px;opacity:0;transition:opacity var(--t-fast) var(--ease)}
.scene .notecard.up{opacity:1}
/* the sweeping selection — a live selection cannot be faked with ::selection,
   so the scene paints the same band and hands it to .hl on commit */
.selrun{--sel-band:color-mix(in srgb,var(--ink) 16%,transparent);
  border-radius:var(--r-full);padding:0.085em 0.4em;margin-inline:-0.4em;
  background-image:linear-gradient(var(--sel-band),var(--sel-band));
  background-repeat:no-repeat;background-size:0% 100%;
  -webkit-box-decoration-break:clone;box-decoration-break:clone;
  transition:background-size 460ms var(--ease)}
.selrun.sweep{background-size:100% 100%}
/* on commit the band yields to the reader's ink: the chrome's own .hl */
.selrun.hl{margin-inline:0;background-image:none;
  background-size:auto;transition:none}
@media (prefers-reduced-motion:reduce){.selrun{transition:none}}
/* the drafting caret: the mark of a focused input someone is writing in —
   it arrives with the input, rides the end of the draft as it grows, and
   blinks in the pauses. Steps, not a fade: a caret is either there or not. */
.input.typing::after{content:"";display:inline-block;width:1.5px;height:1.05em;
  margin-left:1px;vertical-align:-0.14em;background:currentColor;
  animation:caret-blink 1.06s steps(1,end) infinite}
@keyframes caret-blink{50%{opacity:0}}
@media (prefers-reduced-motion:reduce){.input.typing::after{animation:none}}

/* steps: a scene's rows arrive rather than exist */
.step{opacity:0;transform:translateY(6px);
  transition:opacity var(--t-med) var(--ease),transform var(--t-med) var(--ease)}
.step.in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
  .step{opacity:1;transform:none;transition:none}
  .scene .selbar,.scene .noteinput,.scene .notecard{opacity:1;transform:none}}
/* the stage's guest frames: a figure grows to the height it announces */
.scene .fig-body .rep-frame{width:100%;border:0;background:var(--paper);
  display:block}
/* an expanded checkpoint: guest content brings its own paper inside the
   embed — the same inset the figure body takes (chrome § .embed-fig), so
   the quiz never sits flush against the panel's rounded edge */
.scene .embed-body{padding:0 10px 10px}
.scene .embed-inner{background:var(--paper);border-radius:var(--r-md);
  padding:16px 18px}
.scene .embed-head .chev{transition:transform var(--t-fast) var(--ease)}
.scene .embed-head[aria-expanded="true"] .chev{transform:rotate(90deg)}
.scene .quiz{max-width:520px}
/* phase swaps inside a scene (the branch) are hard cuts the fade covers */
[data-ph1][hidden],[data-ph2][hidden]{display:none!important}

/* ---------- the statement ----------
   The scroll-through box: it holds still while the reader passes it and its
   lines take their ink one at a time. Progress is `--q` (0→1), written by
   the page; a line lights when the progress crosses its share. */
.statement{padding:160px 0 0}
.statement .track{height:280vh;position:relative}
.statement .pin{position:sticky;top:0;height:100vh;display:flex;
  align-items:center}
/* NO panel: the statement is set straight on the page's paper and holds the
   ordinary measure the rest of the page uses. The only thing that carries a
   surface here is the choice card, which has to read as a distinct object. */
.statement .box{--q:0;width:100%;
  /* the words themselves arrive: slightly small, settling as they centre */
  transform:scale(calc(0.94 + 0.06 * min(1,var(--q) * 4)))}
.statement .box .kick{margin-bottom:26px}
.statement .lines{display:flex;flex-direction:column;gap:6px}
.statement .ln{font-family:var(--font-title);font-weight:600;
  font-size:clamp(27px,4.3vw,58px);letter-spacing:-0.032em;line-height:1.1;
  /* the waiting rank was --ink-3, which sits under even the large-text
     contrast floor — --ink-2 keeps the lit/unlit step while staying legible
     to a reader the choreography hasn't reached yet (WCAG large-text 3:1) */
  color:var(--ink-2);transition:color 420ms var(--ease);text-wrap:balance}
.statement .ln.lit{color:var(--ink)}
/* each line's load-bearing phrase speaks in one of the page's own faces
   (the hero rotor's voices), and takes the brand blue only when its line
   lights — before that it waits in the same faint rank as its sentence */
.statement .ln .w{font-style:normal;transition:color 420ms var(--ease)}
.statement .ln .w-hand{font-family:'Caveat',cursive;font-weight:600;
  font-size:1.14em;line-height:1}
.statement .ln .w-script{font-family:'Dancing Script',cursive;font-weight:600;
  font-size:1.1em;line-height:1}
.statement .ln .w-slab{font-family:'Zilla Slab',Georgia,serif;font-weight:600}
.statement .ln .w-inst{font-family:'Instrument Serif',Georgia,serif;
  font-style:italic;font-weight:400;font-size:1.06em}
/* ONE step of the blue per phrase, not one blue for all four — page-local
   tokens (a substrate may append its own, never redefine canon's) so the
   ramp is re-stepped per theme in a single place rather than four selectors.
   Light deepens as the argument lands: brand → mid → deep navy, with the
   closing pair splitting deep/slate so two accents in one line stay apart.
   Dark cannot reuse those values — --c1 and --c3 are heavy as TYPE on a
   near-black ground — so it runs the same ramp over dark's own steps.
   Measured on each theme's paper at this size (58px, well past the 3:1 WCAG
   large-text floor's 24px): light c1 4.81, c4 2.98, c3 11.46, c5 7.09 ·
   dark c4 8.48, c2 12.79, c5 6.08, c6 3.17. Light's --c4 sits a hair UNDER
   the floor and is the one step here that would not survive being reused at
   body size — it is legible only because these are display words. Every
   other paler step of both themes is excluded outright. */
:root{--stmt-1:var(--c1);--stmt-2:var(--c4);--stmt-3:var(--c3);--stmt-4:var(--c5)}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --stmt-1:var(--c4);--stmt-2:var(--c2);--stmt-3:var(--c5);--stmt-4:var(--c6)}}
:root[data-theme="dark"]{
  --stmt-1:var(--c4);--stmt-2:var(--c2);--stmt-3:var(--c5);--stmt-4:var(--c6)}
.statement .ln.lit .w-hand{color:var(--stmt-1)}
.statement .ln.lit .w-script{color:var(--stmt-2)}
.statement .ln.lit .w-slab{color:var(--stmt-3)}
.statement .ln.lit .w-inst{color:var(--stmt-4)}
/* the hand-drawn arrow: a marker stroke from the paragraph to the choice,
   DRAWN by the reader's own scroll. `pathLength=1` normalises both paths so
   one dash formula fits either, and the section's own --q is the pen: the
   body writes itself over 0.62→0.80 of the pass, the head lands right after.
   Nothing here animates on a timer — the stroke is exactly as far along as
   the reader is. */
/* the stroke takes the ramp's OPENING step, so the arrow that points from
   the paragraph to the choice speaks in the voice the lines opened in */
.stmt-arrow{width:min(230px,18vw);height:auto;overflow:visible;color:var(--stmt-1);
  align-self:center;margin-bottom:6px}
.stmt-arrow path{fill:none;stroke:currentColor;stroke-width:7;
  stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:1;stroke-dashoffset:calc(1 - var(--draw,0))}
.stmt-arrow .a-body{--draw:max(0,min(1,(var(--q,0) - 0.62) * 5.6))}
.stmt-arrow .a-head{--draw:max(0,min(1,(var(--q,0) - 0.78) * 12.5))}
/* the arrow is the first thing dropped — and the row must re-declare its
   tracks with it gone, or the card lands in the arrow's column */
@media (max-width:1100px){
  .stmt-arrow{display:none}
  .statement .stmt-below{grid-template-columns:minmax(0,1fr) 340px}}
/* the closing pair: the paragraph at a reading size that can hold the box's
   last word, and beside it the one thing the paragraph says does not exist
   yet — the hosted tier, drawn as a ghost of the compute pill above it.
   Cloud glyph in the narration rank, a gauge that only sweeps (a run with
   nowhere to land yet), and the eyebrow's own SOON chip. */
/* paragraph · arrow · choice — the arrow is a column of its own so it never
   overlaps either side, and it is the first thing dropped when the row is
   too narrow to hold all three */
/* The card's track is DEFINITE, never `auto`: a percentage width inside an
   auto track sizes the track to the card's max-content (the note on one
   unwrapped line, ~854px), which starved the paragraph's 1fr to zero. */
.statement .stmt-below{display:grid;
  grid-template-columns:minmax(0,1fr) auto 400px;
  gap:20px 28px;align-items:center;margin-top:44px}
/* margin/padding zeroed explicitly: the page FOOTER is also `.foot`, and its
   block spacing (margin-top:120px) otherwise lands on this paragraph too */
.statement .foot{margin:0;padding:0;max-width:52ch;
  font-size:clamp(15.5px,1.35vw,18px);line-height:1.6;color:var(--ink-2);
  opacity:0;transform:translateY(8px);
  transition:opacity 500ms var(--ease),transform 500ms var(--ease)}
.statement .foot.lit{opacity:1;transform:none}
/* the landing ledger: the Compute page's machine rows (chrome.css § compute
   page), restated as a card — one surface, two destinations. The row that
   exists carries ink and the --c1 dot of the machine the reader is ON; the
   row that is coming sits a rank down, its gauge only sweeping (a run with
   nowhere to land yet), under the eyebrow's own SOON chip. */
/* Now that the section sits on bare paper, the card is the ONE surface here
   and a paper-on-paper fill would be no surface at all — so it takes --code,
   the lightest step and the one token defined as a FILL rather than a ground:
   it is built to read ON paper, panel and panel-2 alike, which is exactly a
   card sitting on bare paper. A token, not a mix: it is already re-stepped
   per theme (canon#8), so nothing here needs a dark override, and colour
   stays where the canon puts it — the accent words and the arrow — rather
   than on a whole surface. */
.land{background:var(--code);
  border-radius:var(--r-lg);padding:20px 24px 18px;width:100%;
  opacity:0;transform:translateY(8px);
  transition:opacity 500ms var(--ease) 120ms,transform 500ms var(--ease) 120ms}
/* general sibling, not adjacent: the arrow stands between them in the row */
.statement .foot.lit ~ .land{opacity:1;transform:none}
.land-kick{display:block;font-size:10.5px;font-weight:600;letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink-3);margin-bottom:8px}
.land-row{display:flex;align-items:center;gap:10px;padding:10px 2px}
.land-row .ic{width:16px;height:16px;color:var(--ink);flex:none}
.land-name{font-family:var(--font-mono);font-size:13px;color:var(--ink);
  white-space:nowrap}
.land-meta{margin-left:auto;font-size:12px;color:var(--ink-2);white-space:nowrap}
.land-row.off .ic,.land-row.off .land-name{color:var(--ink-3)}
.land-row .soon-gauge{margin-left:auto}
/* --panel-2 again, the app's own gauge track (chrome.css § the in-use gauge):
   the card is --code now, so the track reads as a step below it once more */
.soon-gauge{width:72px;height:4px;border-radius:var(--r-full);
  background:var(--panel-2);overflow:hidden;flex:none}
.soon-gauge i{display:block;height:100%;width:34%;border-radius:var(--r-full);
  background:var(--ink-3);animation:soonsweep 1.9s var(--ease) infinite}
@keyframes soonsweep{from{transform:translateX(-110%)}to{transform:translateX(310%)}}
.soon-tag{padding:3px 9px;border-radius:var(--r-full);background:var(--ink);
  color:var(--paper);font-size:10px;font-weight:600;letter-spacing:.09em;
  text-transform:uppercase;flex:none}
/* the note takes the card's own measure — a max-width here left a ragged
   column inside a box that had room for the whole sentence */
.land-note{display:block;margin-top:10px;
  font-size:12px;line-height:1.55;color:var(--ink-2)}
@media (max-width:860px){
  .statement .stmt-below{grid-template-columns:1fr;gap:24px}
  .statement .land{justify-self:stretch}}
@media (max-width:760px){.statement .track{height:220vh}}
@media (prefers-reduced-motion:reduce){
  .statement .track{height:auto}
  .statement .pin{position:static;height:auto;padding:0}
  .statement .box{transform:none}
  .statement .ln{color:var(--ink)}
  .statement .ln .w{color:var(--c1)}
  /* --q is never written under reduced motion, so the stroke is drawn whole */
  .stmt-arrow path{stroke-dashoffset:0}
  .statement .foot{opacity:1;transform:none}
  .statement .land{opacity:1;transform:none}
  .soon-gauge i{animation:none}}

/* ---------- the closing call ----------
   The page ends on what a run leaves behind: two figures and a checkpoint
   from one thread, in one horizontal line — but nothing lies flat. Every
   card keeps its own slope until a hover levels it; a click opens the real
   artifact in the same modal the app uses. */
.close{padding:150px 0 0;text-align:center}
/* uncapped: at the narrow wrap's 820px the line breaks in two; any ch cap
   under ~30 pushes it to three */
.close h2{margin-inline:auto}
.close .lede{margin:22px auto 0;max-width:52ch}
/* the fan breaks out of the narrow wrap: the cards want the page's full
   measure, not the lede's. Margin centring, not a transform — data-rise
   already animates this element's transform and would override it. */
.close-fan{--fanw:min(100vw - 2 * var(--inset),1200px);
  width:var(--fanw);margin:150px 0 0 calc((100% - var(--fanw)) / 2);
  display:flex;justify-content:center;align-items:flex-start;gap:26px;text-align:left}
.fan-card{background:var(--panel);border-radius:var(--r-lg);flex:1 1 0;min-width:0;
  max-width:392px;cursor:pointer;
  transform:rotate(var(--tilt,0deg)) translateY(var(--lift,0px));
  transition:transform var(--t-med) var(--ease)}
.fan-card:hover,.fan-card:focus-visible{transform:rotate(0deg) translateY(var(--lift,0px))}
.fan-card .embed-head{padding:14px 16px;align-items:flex-start}
/* the card's whole point is its title: let it wrap instead of truncating */
.fan-card .embed-head .t{white-space:normal;overflow:visible;text-overflow:clip}
.fan-card .embed-head .go{margin-left:auto;color:var(--ink-2);flex:none;margin-top:1px}
/* the window is inert on purpose: the preview is for looking, the card for
   opening — a live guest here would swallow the click */
.fan-view{margin:0 10px 10px;height:266px;border-radius:var(--r-md);background:var(--paper);
  overflow:hidden;pointer-events:none}
/* the guest renders at double width and is scaled to half: a real window onto
   the artifact, not a thumbnail image of one */
.fan-view .rep-frame{display:block;border:0;background:var(--paper);width:200%;height:420px;
  transform:scale(.5);transform-origin:0 0}
/* the checkpoint preview is the runtime's own quiz markup (checkpoint.css),
   set slightly smaller so the first question fits the window */
.fan-view .fan-quiz{width:140%;transform:scale(.715);transform-origin:0 0;
  padding:16px 18px 0}
@media (max-width:860px){
  .close-fan{flex-direction:column;align-items:center;gap:26px}
  .fan-card{flex:none;width:min(100%,340px);max-width:none}}
@media (prefers-reduced-motion:reduce){.fan-card{transition:none}}

/* ---------- footer ----------
   No box: one quiet line on the paper, and the page ends on the wordmark
   itself — set near the page's full measure and cropped by the bottom edge,
   so the last thing the reader sees is the name being cut off mid-letter. */
.foot{margin-top:120px;padding:34px 0 0}
/* --ink-2, not -3: this is a sentence to read, not a waiting state, and
   body-size text needs the full 4.5:1 (ink-2 clears it on paper) */
.foot .fine{font-size:15px;color:var(--ink-2);text-align:center}
/* the cropped mark: nowrap display type sized to the viewport, its line box
   cut short of the em so the bottom edge slices through the letterforms.
   Full-bleed (outside .wrap); body's overflow-x:clip catches the spill. */
.foot-mark{margin-top:64px;font-family:var(--font-title);font-weight:600;
  letter-spacing:-0.045em;color:var(--ink);text-align:center;white-space:nowrap;
  font-size:max(64px,16.2vw);line-height:1;height:.62em;overflow:hidden;
  user-select:none}

/* the specimen's own way back — this page is a specimen page like the rest */
.spec-back{position:fixed;left:16px;bottom:14px;z-index:60;font-size:12px;
  color:var(--ink-3);transition:color var(--t-fast) var(--ease)}
.spec-back:hover{color:var(--ink-2)}
@media (max-width:900px){.spec-back{display:none}}
