/* predicated.ai, the page at the apex.
 *
 * A different site from the forecast that lives under it, sharing the brand
 * and nothing else. It restates the token values rather than loading
 * `tokens.css`, and that is deliberate: this page is committed to one dark
 * palette with no theme switch, it is deployed on its own to the bucket root,
 * and a change made for a chart on the forecast site must not be able to
 * silently restyle the firm's front door.
 *
 * The three type roles are the forecast's, doing the same jobs. Space Grotesk
 * -- the wordmark face -- takes the display role here and is set at size
 * nowhere else on the brand, which is what keeps this page from reading as the
 * forecast with different copy. Newsreader carries the firm's own two
 * sentences, the hero's and the footer's, and nothing in between.
 */

:root {
  color-scheme: dark;

  --ground: #101619;
  --surface: #182126;
  --sunken: #0b1013;
  --ink: #e8eef1;
  --muted: #a2b3be;
  --rule: #2a3940;

  /* Sampled from the aurora inside the mark, so an accent on the page and the
     fill in the mark are the same three colours rather than a near miss. */
  --warm: #f2913f;
  --hot: #ef5a3c;
  --cool: #39b4c9;
  --teal: #3aada7;
  --violet: #a988e0;

  --prose: Newsreader, Georgia, serif;
  --display: Archivo, "Helvetica Neue", sans-serif;
  --body: Archivo, system-ui, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", monospace;
  --wordmark: "Space Grotesk", Archivo, system-ui, sans-serif;
  /* The name alone: the lockups in the masthead and the footer, and the name
     where a sentence carries it. Set apart from `--wordmark`, which every
     headline on the page also uses, so the logo can change without them. */
  --logotype: Outfit, "Space Grotesk", system-ui, sans-serif;

  --page: 1240px;
}

* { box-sizing: border-box; }

/* What shows outside the document when a touch device rubber-bands past
   either end. That area is painted from `html`, and there is only one of it,
   so both ends of the page have to agree on a colour: the hero sits on
   `--sunken` and the footer's ground is within a point of it. Reported from an
   iPad as a black band under the footer, back when the footer was pale.
   Stated here rather than left to inherit from `body`, so that changing either
   ground has to come past this note. */
html { background: var(--sunken); }

body {
  margin: 0;
  background: var(--sunken);
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; }

/* The clip path the two marks are drawn through. It renders nothing and must
   take no room. */
.brand-defs { position: absolute; inline-size: 0; block-size: 0; overflow: hidden; }

.wrap { max-inline-size: var(--page); margin-inline: auto; padding-inline: clamp(1.2rem, 4vw, 3rem); }

/* The apparatus role -- eyebrows, column heads, metadata -- mono, tracked,
   upper, the way the forecast site sets the same things. */
.label {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
/* An eyebrow that lists several things wraps on a phone, and left to itself it
   breaks inside one of them: "machine / learning", "live & / public". Each
   item is wrapped in a span so the only break points are the separators
   between them. */
.label span { white-space: nowrap; }

/* Off-screen until focused, and drawn the way the forecast site draws it. */
.skip-link {
  position: fixed; inset-block-start: 0.6rem; inset-inline-start: 0.6rem; z-index: 40;
  transform: translateY(-250%);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--rule); border-radius: 999px;
  padding: 0.45rem 1rem; font-family: var(--mono); font-size: 0.74rem; text-decoration: none;
}
.skip-link:focus { transform: none; }

/* The masthead is fixed, so focusing the landmark would otherwise put the top
   of the page underneath it. */
#content { scroll-margin-block-start: 5rem; outline: none; }

/* Every interactive thing keeps a visible ring. The page is dark and the
   default ring is nearly invisible on it. */
a:focus-visible {
  outline: 2px solid var(--cool);
  outline-offset: 3px;
  border-radius: 2px;
}

/* The firm's name set inside a sentence: the logotype, so it reads as the
   name rather than as a word. Bold caps like the lockups, but smaller and a
   little tighter, because caps at full size shout inside a line of serif. */
.wm {
  font-family: var(--logotype); font-weight: 700; font-size: 0.82em;
  letter-spacing: 0.08em; text-transform: uppercase;
}

/* One treatment for the top-level domain, wherever the name is set on the
   dark ground: dimmed rather than coloured, so `.ai` reads as the quieter
   half of one word. It was the headline's cyan in the dek and dimmed in the
   masthead, which made the same name look like two different marks a screen
   apart. The footer is the deliberate exception -- it wears the brand accent
   there, the way the forecast site's footer does. */
/* The suffix is the one part of the name that is not the word, so it is set
   apart -- but it was set apart by being dimmed, which made the firm's own
   name the darkest thing in the masthead. It takes the page's cool token
   instead: already in the palette, so no new colour enters the system, and it
   is the colour at the base of the mark standing next to it. One rule for all
   three places the name is set, so the nav, the dek and the footer agree on
   every page. */
.top .wordmark .tld,
.wm .tld { color: var(--cool); }

/* ---- the field --------------------------------------------------------- */

/* One field of dots runs behind the whole page. It opens as a cloud, falls
   into lanes that merge into one stream in a screen of its own, and scatters
   again for the close. The page never explains it and does not need to: the
   gesture is many agents working at once and one line of work coming out,
   which is what the studio builds and the consultancy brings in.

   Prose never sits on the field, with one exception. The sections that carry
   argument stand on their own opaque plates, so the dots read in the gutters
   and at the seams rather than through a paragraph. The exception is the
   interlude, and it is handled rather than tolerated: `landing.js` dims and
   softens the field for exactly as long as the paragraph is crossing the lanes. */
#field { position: fixed; inset: 0; z-index: 0; display: block; }
.page { position: relative; z-index: 1; }

/* ---- masthead ---------------------------------------------------------- */

/* The lockup is drawn larger than it is on the forecast site, and the
   difference is a difference in job: there it is chrome above an article, here
   the firm is the subject of the page. It scales with the viewport rather than
   stepping at a breakpoint, because a lockup that jumps size mid-resize reads
   as a bug. */
.top {
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: 3;
  display: flex; align-items: center; gap: 1rem;
  padding: clamp(0.8rem, 1.4vw, 1.2rem) clamp(1.2rem, 4vw, 3rem);
  backdrop-filter: blur(14px);
  background: color-mix(in oklab, var(--sunken) 62%, transparent);
  border-block-end: 1px solid color-mix(in oklab, var(--rule) 60%, transparent);
}
.top .lockup { display: flex; align-items: center; gap: clamp(0.55rem, 0.9vw, 0.85rem); text-decoration: none; }
.top .brand-mark {
  display: block; inline-size: auto; block-size: clamp(30px, 2.9vw, 42px);
  aspect-ratio: 503 / 706; color: var(--cool);
}
/* Outfit in bold caps, tracked open. Light caps read as thin against the
   masthead's own weight and the dark ground. Set in CSS rather than typed in
   capitals, so the name is still "predicated.ai" to a screen reader, a search
   engine and anyone who copies it. */
.top .wordmark {
  font-family: var(--logotype); font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; font-size: clamp(0.98rem, 1.3vw, 1.22rem); line-height: 1;
}
.top .lockup:hover .wordmark { text-decoration: underline; text-underline-offset: 5px; }
.top .cta {
  margin-inline-start: auto; font-family: var(--mono); font-size: 0.76rem;
  letter-spacing: 0.06em; text-decoration: none; white-space: nowrap;
  color: var(--sunken); background: var(--ink);
  padding: 0.55rem 1.1rem; border-radius: 999px;
}
.top .cta:hover { background: var(--cool); }

/* ---- hero -------------------------------------------------------------- */

.hero { position: relative; min-block-size: 100svh; display: flex; align-items: center; }
/* The field, dimmed where the copy sits and left alone everywhere else.
   The hero's copy is the only text on the page set over the open field, and
   even at 84% ink with a halo it was reading against sharp dots at about 14px
   on a handset. This dims them rather than blurring them away or walling the
   copy off behind a panel: the stars stay stars, individually visible, just
   muted -- and they come back to full strength toward the edges.
   Two gradients, because they answer different questions. The background is
   how dark, and it is centred. The mask is where, and it is anchored over the
   copy rather than over the viewport, so the right-hand half of a wide screen
   keeps its dots. Without mask support the wash simply covers more, which is
   the same idea less precisely aimed rather than a broken layout.
   A wash rather than a `backdrop-filter`: the frosted versions of this read
   well too, but they recomposite a full-viewport region over a canvas that
   repaints every frame, and this costs nothing at all.
   Negative z-index rather than a layer on top. `.page` already establishes
   the stacking context, so the wash cannot fall behind the canvas -- and the
   copy needs no z-index of its own, which matters because the scroll hint is
   positioned against `.hero` from inside the copy block and giving that block
   a position of its own would move it. */
.hero::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: radial-gradient(100% 100% at 50% 50%,
    rgba(9, 14, 17, 0.82) 0%, rgba(9, 14, 17, 0.62) 55%, rgba(9, 14, 17, 0) 100%);
  -webkit-mask-image: radial-gradient(125% 62% at 30% 52%, #000 0%, #000 42%, transparent 80%);
  mask-image: radial-gradient(125% 62% at 30% 52%, #000 0%, #000 42%, transparent 80%);
}
.hero .in { padding-block: 9rem 4rem; }
.hero h1 {
  font-family: var(--wordmark); font-weight: 600;
  font-size: clamp(2.4rem, 7vw, 5.4rem); line-height: 1.02; letter-spacing: -0.035em;
  margin: 0; max-inline-size: 15ch;
  text-shadow: 0 2px 40px rgba(6, 10, 12, 0.85);
  /* Balanced, so the first sentence breaks "Agents write / the code." rather
     than leaving "code." alone on a line of its own. */
  text-wrap: balance;
}
/* The second half of the tagline is one clause and one colour, and a block,
   so it always opens a line of its own: inline, a 393px phone set "code. We
   build" on one line and the two beats ran together. It is allowed to wrap,
   balanced by the rule above: "We build the software factory." is wider than
   the page's measure at the desktop size, so holding it on one line -- as the
   shorter taglines before it were held -- would push the page sideways. */
.hero h1 span { display: block; color: var(--cool); }
/* The dek is the firm's one sentence about itself, so it is set in the prose
   face rather than the chrome face -- the same serif that closes the page in
   the footer, and set nowhere in between. */
.hero .dek {
  font-family: var(--prose); margin-block-start: 1.6rem;
  max-inline-size: 34ch; font-size: clamp(1.22rem, 2.1vw, 1.72rem); line-height: 1.42;
  color: var(--ink); text-shadow: 0 1px 24px rgba(6, 10, 12, 0.9);
  /* `pretty` rather than `balance`: the dek is two sentences of different
     weight, and the only thing wanted is no last word alone on a line. */
  text-wrap: pretty;
}
/* This line is the smallest type on the page and the only type set over the
   open field, which is the pairing that made it hard to read on a handset:
   muted grey at about 14px with dots passing behind the glyphs, and dimmed a
   second time by the dek's own opacity. So it is near-ink rather than muted,
   dimmed once rather than twice, and carries a tight halo of its own -- the
   wide 24px shadow above softens a whole area, which does nothing about a dot
   sitting on a stem. */
.hero .dek .rest {
  display: block; margin-block-start: 0.7rem;
  font-size: 0.74em; line-height: 1.5; max-inline-size: 44ch;
  color: color-mix(in oklab, var(--ink) 84%, var(--ground));
  text-shadow: 0 1px 3px rgba(6, 10, 12, 0.95), 0 0 14px rgba(6, 10, 12, 0.92);
}
.scroll-hint {
  position: absolute; inset-block-end: 1.6rem; font-family: var(--mono);
  font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
}

/* ---- the fleet --------------------------------------------------------- */

/* The headline drawn: a team's agents at work around the copy. Two walls
   turned in at the screen's edges -- a ledger of what the agents report over
   a session on the left, a session over what they have learned on the right
   -- and up to four smaller sessions in rows above and below them. As the
   hero leaves, the walls swing open like doors and every window goes, each
   away from the copy, and the field below falls into line as it always has.

   `fleet.js` fills it, measures it and places it; this only draws it. The
   walls' width (`--wing-w`), how much of the scene fits (`data-tier`), and
   where each small window goes are all its measurements, because none of
   them can be known in CSS: the walls are turned in perspective, and the
   projection pulls a wall's inner edge further toward the copy than its own
   width says. `--p` is how far the hero has left, 0 to 1.

   Behind the copy and above the hero's wash: both are `z-index: -1` inside
   `.page`'s stacking context and this comes later in the tree, so the copy
   needs no z-index and the scroll hint keeps its containing block. Only
   transforms and opacity move, and nothing here blurs what is behind it: the
   canvas under it repaints every frame. */
.fleet {
  position: absolute; inset: 0; z-index: -1; pointer-events: none; overflow: clip;
  perspective: 1200px; perspective-origin: 50% 45%;
  --p: 0; --wing-w: 240px;
}
.fleet.cramped { display: none; }

/* The walls: about half the hero's height, centred on the copy, which sits
   20px below the middle -- leaving a band above and below for the small
   windows. */
.fleet .wall {
  position: absolute; display: flex; flex-direction: column; gap: 16px;
  inline-size: var(--wing-w);
  inset-block-start: calc(50% + 20px - min(27%, 320px)); block-size: min(54%, 640px);
  opacity: calc(1 - var(--p) * 1.15);
  will-change: transform, opacity;
}
.fleet .wall.l {
  --dir: -1; inset-inline-start: 3vw; transform-origin: 0% 50%;
  transform: rotateY(calc(24deg + var(--p) * 62deg)) translateX(calc(var(--p) * -6vw));
}
.fleet .wall.r {
  --dir: 1; inset-inline-end: 3vw; transform-origin: 100% 50%;
  transform: rotateY(calc(-24deg - var(--p) * 62deg)) translateX(calc(var(--p) * 6vw));
}
/* An animation rather than a transition: the opacity is also the scroll's,
   and a transition on it made the walls lag a scroll by a second. */
.fleet.arrived .wall { animation: fleet-in 1.2s ease backwards; }
@keyframes fleet-in { from { opacity: 0; } }

.fleet .panel {
  flex: 1; min-block-size: 0; display: flex; flex-direction: column;
  border-radius: 12px;
  background: linear-gradient(180deg, color-mix(in oklab, #131c21 82%, transparent), color-mix(in oklab, #0d1417 74%, transparent));
  border: 1px solid color-mix(in oklab, var(--rule) 80%, transparent);
  box-shadow: 0 40px 100px rgba(0, 0, 0, 0.5);
}
.fleet .cap {
  display: flex; align-items: center; padding: 13px 16px 10px;
  border-block-end: 1px solid color-mix(in oklab, var(--rule) 70%, transparent);
}
.fleet .label .short { display: none; }

/* A log anchored at its foot: entries arrive at the bottom, the rest rise,
   and the oldest fade out under the top edge -- full at any height. */
.fleet .tape {
  position: relative; flex: 1; min-block-size: 0; overflow: hidden;
  display: flex; flex-direction: column; justify-content: flex-end;
  -webkit-mask-image: linear-gradient(transparent, #000 22%, #000 94%, transparent);
  mask-image: linear-gradient(transparent, #000 22%, #000 94%, transparent);
}
.fleet .feed {
  list-style: none; margin: 0; padding: 0 16px 6px;
  font-family: var(--mono); font-size: 10.5px; line-height: 1.5; color: var(--muted);
  transition: translate 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.fleet .feed li {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 9px;
  padding: 8px 6px; margin-inline: -6px; border-radius: 6px;
  border-block-end: 1px solid color-mix(in oklab, var(--rule) 45%, transparent);
}
.fleet .feed li i { inline-size: 6px; block-size: 6px; margin-block-start: 5px; border-radius: 50%; background: var(--hue); box-shadow: 0 0 8px var(--hue); }
.fleet .feed li b { color: var(--ink); font-weight: 500; }
.fleet .feed li time,
.fleet .feed li small { grid-column: 2; font-size: 9.5px; letter-spacing: 0.06em; color: color-mix(in oklab, var(--muted) 70%, transparent); }
.fleet .feed .ok { color: var(--teal); }
.fleet .feed .ask { color: var(--warm); }
/* One line an entry, cut short: entries wrapped three lines deep were most
   of what read as clutter. */
.fleet .feed.ledger li div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fleet .feed.learned li span { color: var(--ink); font-family: var(--prose); font-size: 13.5px; line-height: 1.35; }
.fleet .feed li.new { animation: fleet-flash 2.4s ease; }
@keyframes fleet-flash { 0%, 25% { background: color-mix(in oklab, var(--hue) 16%, transparent); } }

/* The roll call: one dot per agent, lit as that agent reports. */
.fleet .roll { margin-inline-start: auto; display: flex; gap: 6px; }
.fleet .roll i { inline-size: 6px; block-size: 6px; border-radius: 50%; background: var(--hue); opacity: 0.4; }
.fleet .roll i.on { animation: fleet-roll 1.3s ease; }
@keyframes fleet-roll { 15% { opacity: 1; scale: 1.9; box-shadow: 0 0 10px var(--hue); } 100% { opacity: 0.4; scale: 1; } }

/* A session window. The look is on `.body`, so its entrance and drift
   compose with the scroll's transform on the window rather than replace it. */
.fleet .win .body {
  border-radius: 10px; overflow: hidden;
  background: color-mix(in oklab, #121a1e 94%, transparent);
  border: 1px solid color-mix(in oklab, var(--hue) 24%, var(--rule));
  box-shadow: 0 22px 60px rgba(0, 0, 0, 0.55), 0 0 40px -12px color-mix(in oklab, var(--hue) 45%, transparent);
  font-family: var(--mono); font-size: 10.5px; line-height: 1.6; color: var(--muted);
  opacity: 0; translate: 0 14px;
  transition: opacity 0.9s ease var(--delay, 0s), translate 1.1s cubic-bezier(0.2, 0.7, 0.2, 1) var(--delay, 0s);
  animation: fleet-bob 7s ease-in-out infinite alternate;
}
.fleet.arrived .win .body { opacity: 1; translate: 0 0; }
@keyframes fleet-bob { from { transform: translateY(-3px); } to { transform: translateY(4px); } }
.fleet .win .head {
  display: flex; align-items: center; gap: 7px; padding: 7px 10px;
  border-block-end: 1px solid color-mix(in oklab, var(--rule) 70%, transparent);
  background: color-mix(in oklab, var(--hue) 7%, transparent);
}
.fleet .win .head i { inline-size: 7px; block-size: 7px; border-radius: 50%; background: var(--hue); box-shadow: 0 0 8px var(--hue); }
.fleet .win:is(.ping, .caught) .head i { animation: fleet-ping 1s ease; }
@keyframes fleet-ping { 30% { scale: 2.4; } }
.fleet .win .head b { color: var(--ink); font-weight: 500; letter-spacing: 0.04em; }
.fleet .win .head .st {
  margin-inline-start: auto; padding: 1px 7px; border: 1px solid currentColor; border-radius: 999px;
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
}
.fleet .st.running { color: var(--cool); }
.fleet .st.waiting { color: var(--warm); }
.fleet .st.review { color: var(--violet); }
.fleet .st.shipped { color: var(--teal); }
.fleet .win .log { padding: 8px 10px 4px; min-block-size: 6.4em; white-space: pre; overflow: hidden; }
.fleet .win .log div { animation: fleet-line 0.35s ease both; }
.fleet .win .log em { font-style: normal; color: var(--hue); }
.fleet .win .log div:last-child::after {
  content: ""; display: inline-block; inline-size: 6px; block-size: 11px; margin-inline-start: 3px;
  vertical-align: -2px; background: var(--hue); animation: fleet-caret 1s steps(1) infinite;
}
@keyframes fleet-line { from { opacity: 0; translate: 0 4px; } }
@keyframes fleet-caret { 50% { opacity: 0; } }
.fleet .win .meter { block-size: 2px; margin: 4px 10px 10px; border-radius: 2px; overflow: hidden; background: color-mix(in oklab, var(--rule) 80%, transparent); }
.fleet .win .meter span { display: block; block-size: 100%; inline-size: 0; background: var(--hue); transition: inline-size 0.8s ease; }

/* A wall's own session breaks off it on the way out and goes further. */
.fleet .wall .win {
  flex: none;
  transform: translateX(calc(var(--dir) * var(--p) * 26vw)) translateY(calc(var(--p) * -8vh)) rotate(calc(var(--dir) * var(--p) * 12deg));
}

/* The small windows. Placed by `fleet.js`; turned toward the middle in the
   walls' own perspective rather than tilted flat, so the scene has one
   camera; each leaves in its own direction, always away from the copy, its
   turn growing as it goes. */
.fleet .mini {
  position: absolute; inset-block-start: 0; inset-inline-start: 0; inline-size: 216px;
  transform:
    translate(calc(var(--p) * var(--vx) * 1vw), calc(var(--p) * var(--vy) * 1vh))
    rotateY(calc(var(--yaw) * (1 + var(--p) * 3)))
    scale(var(--s));
  opacity: calc(var(--o) * (1 - var(--p) * 1.4));
  will-change: transform, opacity;
}
.fleet .mini.off { display: none; }
.fleet .mini .body { animation: fleet-hover var(--dur, 11s) ease-in-out infinite alternate; font-size: 10px; line-height: 1.55; }
@keyframes fleet-hover {
  from { transform: translate(calc(var(--dx) * -1), calc(var(--dy) * -1)); }
  to { transform: translate(var(--dx), var(--dy)); }
}
.fleet .mini .head { padding: 5px 9px; gap: 6px; }
.fleet .mini .head .st { padding: 0 6px; font-size: 8.5px; }
.fleet .mini .log { min-block-size: 4.7em; padding: 6px 10px 2px; }
.fleet .mini.far .log { min-block-size: 3.2em; }
.fleet .mini .meter { margin: 3px 9px 8px; }
.fleet .mini.far .body { box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5); }

/* A hand-off: a dot of the sender's colour on its way to the receiver. */
.fleet .relay { position: absolute; inset: 0; }
.fleet .relay b {
  position: absolute; inset-block-start: 0; inset-inline-start: 0;
  inline-size: var(--size, 7px); block-size: var(--size, 7px); border-radius: 50%;
  background: var(--hue); box-shadow: 0 0 12px 2px color-mix(in oklab, var(--hue) 70%, transparent);
  offset-anchor: center; offset-rotate: 0deg;
}

/* Less room, less scene. `compact`: walls too narrow for their long captions
   and wrapped entries keep a short caption and one line an entry, and their
   sessions three lines. `minimal`: no walls at all. */
.fleet[data-tier="compact"] .label .long { display: none; }
.fleet[data-tier="compact"] .label .short { display: inline; }
.fleet[data-tier="compact"] .feed li { padding-block: 7px; }
.fleet[data-tier="compact"] .feed li time,
.fleet[data-tier="compact"] .feed li small { display: none; }
.fleet[data-tier="compact"] .feed.learned li span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12.5px; }
.fleet[data-tier="compact"] .wall .win .log { min-block-size: 4.8em; }
.fleet[data-tier="compact"] .wall .win .log div:first-child:nth-last-child(4) { display: none; }
.fleet[data-tier="minimal"] .wall { display: none; }
/* With no walls the pair has only the short band above the headline and the
   space beside the paragraph, so its windows show two lines, not three --
   enough to fit the pair on an iPad held sideways. */
.fleet[data-tier="minimal"] .mini .log { min-block-size: 3.2em; }
.fleet[data-tier="minimal"] .mini .log div:first-child:nth-last-child(3) { display: none; }

/* ---- plates: prose stands on its own ground ---------------------------- */

.plate-band { background: var(--ground); border-block: 1px solid var(--rule); }
.plate-band.sunk { background: var(--surface); }

.practice { padding-block: clamp(3.5rem, 8vw, 6rem); }
.practice .head { margin-block-end: 1rem; }
.practice .head h2 {
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(2.1rem, 4.6vw, 3.4rem);
  letter-spacing: -0.03em; line-height: 1.05; margin: 0.7rem 0 0;
  max-inline-size: 20ch; text-wrap: balance;
}

/* A practice as a full-width row: apparatus left, argument right -- the base
   layout, which `.doors` below sets two to a row. The
   quadrant this replaced fitted all four onto one screen, which is exactly
   what made it small -- two columns of 54ch capped the headings at 1.3rem and
   pushed the body copy into muted grey, so the section read as a footnote to
   the masthead above it. Given the whole measure a practice can state itself
   at a size that answers the masthead and still hold a 60ch line. */
.cards { display: grid; grid-template-columns: 1fr; }
.card {
  display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  column-gap: clamp(1.5rem, 4vw, 3.5rem); padding-block: clamp(2rem, 4vw, 3rem);
  border-block-start: 1px solid var(--rule);
}
.card .dotmark { grid-column: 1; grid-row: 1; display: flex; gap: 5px; margin-block-start: 0.6rem; }
.card .dotmark i { inline-size: 11px; block-size: 11px; border-radius: 50%; display: block; background: var(--accent); }
.card .dotmark i:nth-child(2) { opacity: 0.6; }
.card .dotmark i:nth-child(3) { opacity: 0.3; }
/* The rail below the dotmark: the practice's name, and the mark that draws it.
   One cell rather than two, so the mark hangs off the label wherever the label
   ends up -- the label is aligned to the top of the paragraph beside it, not
   to the heading, and pinning the mark to a row of its own would have set it
   against the pills instead. */
.card .rail { grid-column: 1; grid-row: 2; align-self: start; }
.card .label { display: block; color: var(--accent); }
/* Hairline, `currentColor`, and no container: the mark takes the accent the
   label above it is already set in, so the rail reads as one apparatus block
   in one colour. The page spends its boldness on the field behind it, so
   these stay drawings rather than badges -- no tint, no ring, no chip. */
.card .glyph {
  display: block; inline-size: 64px; block-size: 64px;
  margin-block-start: 1.2rem; color: var(--accent);
}
.card.studio { --accent: var(--warm); }
.card.consult { --accent: var(--cool); }
.card h3 {
  grid-column: 2; grid-row: 1;
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(1.6rem, 3vw, 2.5rem);
  letter-spacing: -0.025em; line-height: 1.15; margin: 0 0 0.9rem;
}
.card p {
  grid-column: 2; grid-row: 2; margin: 0; max-inline-size: 60ch;
  font-size: 1.12rem; line-height: 1.62;
  color: color-mix(in oklab, var(--ink) 80%, var(--ground));
}
.card ul {
  grid-column: 2; grid-row: 3;
  list-style: none; margin: 1.4rem 0 0; padding: 0; font-family: var(--mono);
  font-size: 0.76rem; color: var(--muted); display: flex; flex-wrap: wrap; gap: 0.4rem;
}
.card li { border: 1px solid var(--rule); border-radius: 999px; padding: 0.25rem 0.75rem; }

/* Two doors rather than four rows: the studio and the consultancy side by
   side, so each audience finds its own way in before scrolling. Each card is
   one column inside its half -- the rail as a line, mark beside the name, the
   way the stacked phone layout already draws it -- because a 15rem apparatus
   column beside the argument would leave the argument about 20ch wide. */
.cards.doors {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
}
.doors .card { grid-template-columns: minmax(0, 1fr); align-content: start; }
.doors .card > * { grid-column: 1; grid-row: auto; }
.doors .card .dotmark { margin: 0 0 0.9rem; }
.doors .card .rail { display: flex; align-items: center; gap: 0.9rem; margin-block-end: 0.9rem; }
.doors .card .glyph { order: -1; flex: none; inline-size: 48px; block-size: 48px; margin-block-start: 0; }
.doors .card h3 { font-size: clamp(1.5rem, 2.6vw, 2.1rem); text-wrap: balance; }

/* ---- the interludes: the two stretches where the field is in the open ----
   The lanes after the practice section, and the forecast's arc before the
   close. Each forms in the upper half of the screen, and the copy sits at the
   foot of a section just tall enough that it arrives as the shape completes:
   at 108svh the lanes formed a screen above their own caption, and a reader
   scrolled through the shape wondering what it was before reading what it
   said. The copy then rises through the shape, and the field gets out of its
   way while it does -- which is why the type needs so little shadow. */
/* The top padding is the shape's own room. On a desktop the 80svh already
   leaves it and the padding changes nothing; on a phone the copy is taller
   than the screen, and without it the paragraph filled the whole section
   and the arc was only ever seen through the text. */
.interlude {
  min-block-size: 80svh; display: flex; align-items: flex-end;
  padding-block: 38svh clamp(2rem, 6vw, 4rem);
}
/* Set at the masthead's scale rather than a caption's. It names the shape
   on screen, and at the old size it read as a footnote to it. */
.interlude h2 {
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(2.2rem, 5.2vw, 4.4rem);
  letter-spacing: -0.035em; line-height: 1.02; margin: 0.8rem 0 1.1rem; max-inline-size: 18ch;
  text-shadow: 0 2px 40px rgba(6, 10, 12, 0.8);
}
/* The second beat starts a line of its own wherever it does not fit beside
   the first, and wraps balanced inside itself where it is wider than a phone:
   "One crew, not a crowd." is, and unbalanced it left "crowd." alone. */
.interlude h2 { text-wrap: balance; }
.interlude h2 span { display: inline-block; }
.interlude p {
  margin: 0; max-inline-size: 58ch; font-size: clamp(1.05rem, 1.5vw, 1.3rem); line-height: 1.55;
  color: color-mix(in oklab, var(--ink) 84%, var(--ground));
  text-shadow: 0 1px 18px rgba(6, 10, 12, 0.85);
}
.interlude p + p { margin-block-start: 0.9rem; }
.interlude p a { text-decoration-color: color-mix(in oklab, var(--cool) 70%, transparent); text-underline-offset: 3px; }
.interlude p a:hover { color: var(--cool); }
.interlude .row { --accent: var(--cool); }

/* ---- work -------------------------------------------------------------- */

.work { padding-block: clamp(3.5rem, 8vw, 6rem); }
/* Same head as the practice section above it, at the same size. It used to be
   a baseline row -- eyebrow beside the heading -- which read as a caption once
   the practice heading grew, and made the section that closes the argument
   look like the smaller of the two. */
.work .head { margin-block-end: 2.4rem; }
.work .head h2 {
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(2.1rem, 4.6vw, 3.4rem);
  letter-spacing: -0.03em; line-height: 1.05; margin: 0.7rem 0 0;
  max-inline-size: 20ch; text-wrap: balance;
}
/* A block with links in it rather than one big link, for the reason the
   entries below give: an anchor wrapping the heading, the paragraph and four
   figures announces all ninety words as its own name, and a reader using one
   cannot skim past it. The card still lights up as a whole on hover, which is
   the only thing wrapping it ever bought. */
.case {
  display: block; padding: clamp(1.6rem, 3.5vw, 2.6rem);
  border: 1px solid var(--rule); border-radius: 6px; background: var(--ground);
  position: relative; overflow: hidden;
}
/* The aurora as a four-pixel edge: the brand's own colours marking the one
   piece of work that is live rather than archived. */
.case::before {
  content: ""; position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: 4px;
  background: var(--aurora-edge) center / cover;
}
.case:has(a:hover), .case:focus-within { border-color: var(--cool); }
.case h3 {
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(1.5rem, 3vw, 2.2rem);
  letter-spacing: -0.03em; margin: 0.6rem 0 0.7rem;
}
.case h3 a { text-decoration: none; }
.case h3 a:hover { text-decoration: underline; text-underline-offset: 5px; }
.case p { margin: 0; color: var(--muted); max-inline-size: 62ch; font-size: 0.97rem; }
.case .row, .interlude .row { display: flex; align-items: flex-end; gap: 2rem; flex-wrap: wrap; margin-block-start: 1.6rem; }
.case .row div b, .interlude .row div b { display: block; font-family: var(--mono); font-size: 1.4rem; font-variant-numeric: tabular-nums; }
.case .row div span, .interlude .row div span {
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted);
}
.go { font-family: var(--mono); font-size: 0.78rem; color: var(--accent, var(--cool)); text-decoration: none; }
.go:hover { text-decoration: underline; text-underline-offset: 3px; }
.case .go, .interlude .row .go { margin-inline-start: auto; }
.case p + p { margin-block-start: 0.8rem; }
.case .links { margin-inline-start: auto; display: flex; flex-wrap: wrap; gap: 1.2rem; }
.case .links .go { margin-inline-start: 0; }
/* The flagship's own mark, in the half of the card its prose leaves empty.
   A grid rather than a float, so the mark centres against the whole card and
   the figure row below still runs the full width beside it. */
.case.with-mark {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 15rem);
  column-gap: clamp(1.5rem, 4vw, 3.5rem);
}
.case.with-mark > * { grid-column: 1; }
.case.with-mark .mark {
  grid-column: 2; grid-row: 1 / span 5; align-self: center; justify-self: center;
  inline-size: 100%; max-inline-size: 15rem; block-size: auto;
}

/* A card is a block with links in it rather than one big link, because one of
   the three has two destinations -- the library and the paper about it -- and
   an anchor cannot hold another anchor. It also stops a screen reader reading
   a whole paragraph out as the name of a link. */
.entries { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-block-start: 1rem; }
.entry {
  display: flex; flex-direction: column;
  --pad: clamp(1.4rem, 2.6vw, 1.9rem); padding: var(--pad);
  border: 1px solid var(--rule); border-radius: 6px; background: var(--ground);
}
.entry:has(a:hover), .entry:focus-within { border-color: var(--accent); }
.entry.eng { --accent: var(--teal); }
.entry.llm { --accent: var(--cool); }
.entry.mle { --accent: var(--violet); }
.entry h3 {
  font-family: var(--wordmark); font-weight: 600; font-size: 1.2rem; letter-spacing: -0.025em;
  line-height: 1.2; margin: 0.7rem 0 0.6rem; overflow-wrap: anywhere;
}
.entry h3 a { text-decoration: none; }
.entry h3 a:hover { text-decoration: underline; text-underline-offset: 4px; }
.entry p { margin: 0 0 0.7rem; color: var(--muted); font-size: 0.92rem; }
.entry p:last-of-type { margin-block-end: 0; }
.entry code { font-family: var(--mono); font-size: 0.85em; color: var(--ink); overflow-wrap: anywhere; }
/* Each project's own mark above its name, and the four together on the org's
   front card: the colour in this section comes from the work itself. Sized
   by height, because the marks are not one shape -- mockstack is tall and
   narrow, pdfalive nearly square. */
/* `align-self` because a slide is a flex column: left to stretch, the mark
   was drawn 308px wide at 48px tall. */
.entry .logo { display: block; align-self: flex-start; block-size: 3rem; inline-size: auto; margin-block-start: 0.6rem; }
.entry .logos { display: flex; align-items: center; gap: 0.9rem; margin-block-start: 0.7rem; }
.entry .logos img { block-size: 2.4rem; inline-size: auto; }
/* A post's thumbnail: a crop of the blog's own pixel art, as a banner across
   the slide. Cropped clear of the generator's watermark in the corner. */
.entry .thumb {
  display: block; align-self: stretch; inline-size: 100%; block-size: auto;
  aspect-ratio: 640 / 267; object-fit: cover; border-radius: 4px;
  margin-block-start: 0.7rem; border: 1px solid var(--rule);
}
/* The project names are links now, and look like it without shouting. The
   code face has already lifted them out of the muted paragraph around them,
   so the rule underneath can stay faint until a reader wants it and take the
   card's own accent on hover.

   Four inline links rather than one anchor around the card, which is the same
   reasoning the case block above is built on: an anchor wrapping a paragraph
   announces the whole paragraph as its own name, and a reader moving by link
   cannot skim past it. Here it also means the card names four repositories and
   offers four ways in, instead of one that lands on the org. */
.entry p a { color: var(--ink); text-decoration: underline; text-decoration-thickness: 1px;
  text-underline-offset: 3px; text-decoration-color: color-mix(in oklab, var(--accent) 50%, transparent); }
.entry p a:hover, .entry p a:focus-visible { text-decoration-color: var(--accent); }
.entry p a code { color: inherit; }
.entry .links {
  margin-block-start: auto; padding-block-start: 1.1rem;
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.2rem;
}

/* A reel: one rubric, several projects, a dot for each. The strip is a
   scroll-snap container and is the whole carousel on its own -- it swipes,
   scrolls and follows focus with no script at all. `reel.js` adds the half
   CSS cannot do everywhere yet, which is the dots knowing which slide is in
   view.

   The strip bleeds to the card's border and each slide carries the card's
   padding back, so a slide on its way in slides under the border rather than
   being cut off at an edge the reader cannot see. Every slide is as tall as
   the tallest, so the card does not change height as it turns.

   A scroll container clips in both axes, and the focus ring round the links
   at the foot of a slide draws five pixels below them -- outside the strip,
   so it was cut off. The strip takes that much padding and gives it back as
   margin, which leaves the card exactly as tall and the ring whole. */
.reel {
  --dwell: 9s;
  display: grid; grid-template-rows: auto 1fr; grid-template-columns: minmax(0, 1fr); overflow: hidden;
}
.reel .slides {
  grid-area: 2 / 1; display: grid; grid-auto-flow: column; grid-auto-columns: 100%;
  margin-inline: calc(-1 * var(--pad)); padding-block-end: 6px; margin-block-end: -6px;
  overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.reel .slides::-webkit-scrollbar { display: none; }
.reel .slide {
  display: flex; flex-direction: column; padding-inline: var(--pad);
  scroll-snap-align: start; scroll-snap-stop: always;
}
@media (prefers-reduced-motion: no-preference) {
  .reel .slides { scroll-behavior: smooth; transition: opacity 0.22s ease; }
  /* The turn from the last slide back to the first: out, across, in. */
  .reel .slides.rewinding { opacity: 0; }
}

/* The dots and the pause button sit at the foot of the card, on the line
   its links take -- laid over the strip's last row in the same grid cell, so
   they cost the card no height. They were beside the label first, and five
   dots and a button left the label too little room: it wrapped to two lines
   and dropped the card's headline 17px below its neighbours'. Every slide's
   links stop short of the foot, `--n` dots and a button wide, so a long row
   of links wraps rather than running under them. */
.reel-foot {
  grid-area: 2 / 1; align-self: end; justify-self: end; z-index: 1;
  display: flex; align-items: center; margin-block-end: -2px; margin-inline-end: -8px;
}
.reel .links { padding-inline-end: calc((var(--n, 1) + 1) * 24px); }

/* The dots are drawn small and hit large: a 7px mark centred in a 24px
   target, the smallest WCAG allows a pointer to aim at. The current one
   stretches into a filled pill in the card's accent -- the shape changes as
   well as the fill, so it does not rest on colour alone. The foot hangs out
   by the target's slack so the marks, not the targets, line up with the
   card's edge. */
.dots { display: flex; }
.dots a { display: grid; place-items: center; inline-size: 24px; block-size: 24px; border-radius: 999px; }
.dots a::before, .dots a::after { content: ""; grid-area: 1 / 1; block-size: 7px; border-radius: 999px; }
.dots a::before {
  inline-size: 7px; box-sizing: border-box;
  border: 1px solid color-mix(in oklab, var(--accent) 70%, transparent);
}
.dots a:hover::before { background: color-mix(in oklab, var(--accent) 40%, transparent); }
.dots a[aria-current="true"]::before { inline-size: 18px; background: var(--accent); border-color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .dots a::before { transition: inline-size 0.25s ease, background-color 0.25s ease; }
}

/* While a reel turns, its current pill is a timer: drawn faint, and filled
   from its start over one dwell, so a reader can see how long the card will
   stay. The fill's end is what turns the reel -- `reel.js` listens for it --
   so pausing the fill pauses the turn, and the reasons to pause are all
   here: a pointer over the card, focus inside it, and `data-held`, which the
   script sets while the card is off screen or the tab is in the background.
   `--wait` is the stagger, applied to the first dwell only.

   No reduced-motion query is needed: under one, the script never sets
   `data-rotating`, and the pill is the solid one above. */
.reel[data-rotating] .dots a[aria-current="true"]::before {
  background: color-mix(in oklab, var(--accent) 22%, transparent);
}
.reel[data-rotating] .dots a[aria-current="true"]::after {
  justify-self: start; margin-inline-start: 3px; inline-size: 0; background: var(--accent);
  animation: dwell var(--dwell) linear var(--wait, 0s) both;
}
.reel[data-rotating]:is(:hover, :focus-within, [data-held]) .dots a::after { animation-play-state: paused; }
@keyframes dwell { to { inline-size: 18px; } }

/* Pause and play, at the end of the dots and drawn as quietly as they are:
   two bars while it turns, a triangle once it has stopped. */
.hold {
  display: grid; place-items: center; inline-size: 24px; block-size: 24px; padding: 0;
  border: 0; border-radius: 999px; background: none; color: var(--muted); cursor: pointer;
}
.hold:hover { color: var(--accent); }
.hold:focus-visible { outline: 2px solid var(--cool); outline-offset: 1px; }
.hold::before {
  content: ""; inline-size: 6px; block-size: 8px; box-sizing: border-box;
  border-inline: 2px solid currentColor;
}
.hold[data-state="stopped"]::before {
  inline-size: 7px; border: 0; background: currentColor;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* Read by a screen reader and drawn nowhere: the name each dot goes to. */
.sr {
  position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ---- close: back onto the field ---------------------------------------- */

.close { min-block-size: 74svh; display: flex; align-items: center; }
.close h2 {
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(2rem, 5.2vw, 3.6rem);
  line-height: 1.05; letter-spacing: -0.03em; margin: 0.8rem 0 1.6rem; max-inline-size: 17ch;
  text-shadow: 0 2px 40px rgba(6, 10, 12, 0.85); text-wrap: balance;
}
/* The second beat in the masthead's colour, on a line of its own, so the
   close answers the headline it opens with. */
.close h2 span { display: block; color: var(--cool); }
.close .big-cta {
  display: inline-flex; align-items: center; gap: 0.7rem;
  font-family: var(--mono); font-size: 0.86rem; text-decoration: none;
  color: var(--sunken); background: var(--ink); padding: 0.9rem 1.6rem; border-radius: 999px;
}
.close .big-cta:hover { background: var(--cool); }

/* ---- footer ------------------------------------------------------------
   The forecast site's footer, in its dark palette, with different links in it.
   Same slab, same aurora wash, same generated grain, same lockup at the same
   size, and the values below are that footer's own dark-theme block copied
   rather than reinterpreted -- a reader arriving from the forecast should not
   be able to tell the two apart except by what they say.

   It was drawn pale first, on the reasoning that a change of ground is what
   separates a footer from a page. That is true on the forecast site, where
   the page above it is white in the light theme; here the page is near-black
   from the masthead down and the pale slab arrived as a flashbulb at the end
   of a dark scroll. The separation comes from the wash and the hairline
   instead, which is exactly how the forecast site does it in dark.

   Grain over a pale ground has to darken; over a dark one it has to lift --
   hence `overlay` and white rather than `multiply` and grey, and a stronger
   opacity because a dark ground swallows it. The wash alphas step up for the
   same reason. */
.pub-foot {
  --foot-ground: #0b1114;
  --foot-ink: #eef3f6;
  --foot-muted: #a7bac6;
  --foot-rule: rgba(238, 243, 246, 0.15);
  --foot-accent: #ffab63;
  --grain-ink: #ffffff;
  --grain-blend: overlay;
  --grain-opacity: 0.34;
  --wash-warm: rgba(252, 120, 56, 0.3);
  --wash-mid: rgba(195, 136, 139, 0.2);
  --wash-cool: rgba(61, 167, 189, 0.15);
  position: relative; isolation: isolate; overflow: hidden;
  background: var(--foot-ground); color: var(--foot-ink);
  border-block-start: 1px solid var(--foot-rule);
}
.foot-wash, .foot-grain { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.foot-wash {
  background: radial-gradient(120% 150% at 92% 40%,
    var(--wash-warm) 0%, var(--wash-mid) 30%, var(--wash-cool) 55%, transparent 74%);
}
.foot-grain {
  inline-size: 100%; block-size: 100%;
  opacity: var(--grain-opacity); mix-blend-mode: var(--grain-blend);
}
.foot-grain rect { fill: var(--grain-ink); }

.foot-in {
  position: relative; display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: 2.5rem 2rem; padding-block: clamp(2.8rem, 6vw, 3.5rem) 2.5rem;
}
.pub-foot a:focus-visible { outline-color: var(--foot-accent); }
.foot-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 1.1rem; }
.brand-lockup { display: flex; align-items: center; gap: 1rem; text-decoration: none; color: inherit; }
.foot-brand .brand-mark {
  display: block; block-size: clamp(72px, 8vw, 100px); inline-size: auto;
  aspect-ratio: 503 / 706; color: var(--foot-accent);
}
.brand-word {
  font-family: var(--logotype); font-weight: 700; font-size: clamp(1.25rem, 2vw, 1.6rem);
  letter-spacing: 0.1em; text-transform: uppercase; line-height: 1;
}
.brand-word .tld { color: var(--cool); }
.brand-lockup:hover .brand-word { text-decoration: underline; text-underline-offset: 4px; }
.brand-line {
  margin: 0; color: var(--foot-muted); font-family: var(--prose);
  font-size: 1rem; line-height: 1.55; max-inline-size: 34rem;
}
/* The same call the masthead and the close make, drawn the way the forecast
   site draws it: under the firm's own line, in the accent, underlined. Third
   time it is asked on this page, and the first time it is asked of a reader
   who has finished reading. */
.brand-cta {
  font-family: var(--display); font-weight: 600; font-size: 0.95rem;
  color: var(--foot-accent); text-decoration: none;
  border-block-end: 1px solid currentcolor; padding-block-end: 2px;
}
.brand-cta:hover { color: var(--foot-ink); }
.foot-col h2 {
  font-family: var(--display); font-weight: 600; font-size: 0.78rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--foot-muted); margin: 0 0 0.8rem;
}
.foot-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.45rem; }
.foot-col a { color: var(--foot-ink); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
.foot-col a:hover { text-decoration: underline; text-underline-offset: 3px; }
.foot-col .social-icon { inline-size: 15px; block-size: 15px; }
.foot-base {
  position: relative; grid-column: 1 / -1; display: flex; gap: 1.5rem; flex-wrap: wrap;
  padding-block-start: 1.5rem; border-block-start: 1px solid var(--foot-rule);
  font-size: 0.85rem; color: var(--foot-muted);
}
.foot-base p { margin: 0; }

/* ---- the page that is not there ---------------------------------------- */

/* Every 404 on this domain lands here, including the ones under the forecast's
   own path, so it points at both things rather than at one. It carries no
   field: a page that has to say something went wrong should not also be
   putting on the show. */
.gone { min-block-size: 100svh; display: flex; align-items: center; }
.gone .in { padding-block: 9rem 4rem; }
.gone h1 {
  font-family: var(--wordmark); font-weight: 600; font-size: clamp(2.2rem, 6vw, 4rem);
  line-height: 1.04; letter-spacing: -0.035em; margin: 0.8rem 0 1rem; max-inline-size: 16ch;
}
.gone p { margin: 0; color: var(--muted); max-inline-size: 46ch; font-size: 1.02rem; }
.gone ul { list-style: none; margin: 2rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.6rem 1.8rem; }

/* ---- narrow ------------------------------------------------------------ */

@media (max-width: 900px) {
  .entries { grid-template-columns: 1fr; }
  .cards.doors { grid-template-columns: 1fr; }
  .case.with-mark { grid-template-columns: minmax(0, 1fr); }
  .case.with-mark .mark { grid-column: 1; grid-row: auto; justify-self: start; max-inline-size: 7rem; margin-block-end: 0.4rem; }
  .card { display: block; }
  .card .dotmark { margin: 0 0 0.9rem; }
  /* Stacked, the rail is a line rather than a column: the mark goes beside
     the name at the size a phone can afford, and `order` moves it in front
     without moving it in the document -- it is `aria-hidden`, so there is
     nothing there to read out of order. The label still wraps to two lines at
     320px, which is why the mark is centred against it rather than topped. */
  .card .rail { display: flex; align-items: center; gap: 0.9rem; margin-block-end: 0.6rem; }
  .card .glyph { order: -1; flex: none; inline-size: 48px; block-size: 48px; margin-block-start: 0; }
  .foot-in { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .foot-brand { grid-column: 1 / -1; }
}

/* ---- the fleet on a phone ---------------------------------------------- */

/* A phone or a tablet held upright: the copy takes the width, so there are
   no walls and no windows. The six sessions are drawn as small tags instead
   -- a name, a state and the latest line -- three in the room above the
   headline and three under the paragraph, joined by faint lines.
   `fleet.js` sets the `phone` tier, places the tags and draws the lines.
   The walls give up their own boxes (`display: contents`), so the two
   sessions on them are placed like the other four, and their panels are not
   drawn. Leaving, each tag goes the way `--tx`/`--ty` say, in viewport
   units: up and out above the copy, sideways and soon below it, so nothing
   crosses the scroll hint. Until the script has chosen the tier, nothing
   shows here at all. */
@media (max-width: 900px) {
  .fleet:not([data-tier="phone"]) { display: none; }
}
.fleet .ties { display: none; }
.fleet[data-tier="phone"] .ties {
  display: block; position: absolute; inset: 0; inline-size: 100%; block-size: 100%;
  opacity: calc(0.9 - var(--p) * 3);
}
.fleet[data-tier="phone"] .ties path {
  fill: none; stroke: color-mix(in oklab, var(--muted) 30%, transparent);
  stroke-width: 1; stroke-dasharray: 2 4;
}
.fleet[data-tier="phone"] .wall { display: contents; }
.fleet[data-tier="phone"] .wall .panel { display: none; }
.fleet[data-tier="phone"] .win {
  position: absolute; inset-block-start: 0; inset-inline-start: 0; inline-size: var(--tag-w, 160px);
  transform: translate(calc(var(--p) * var(--tx, 0) * 1vw), calc(var(--p) * var(--ty, 0) * 1vh));
  opacity: calc(1 - var(--p) * var(--fade, 1.4));
}
.fleet[data-tier="phone"] .win.off { display: none; }
/* A hand-off runs down a margin about 19px wide beside the copy, so its dot
   is drawn smaller there, with a glow that stays inside the margin. */
.fleet[data-tier="phone"] .relay b {
  inline-size: calc(var(--size, 7px) - 1px); block-size: calc(var(--size, 7px) - 1px);
  box-shadow: 0 0 5px 1px color-mix(in oklab, var(--hue) 70%, transparent);
}
.fleet[data-tier="phone"] .win .body {
  --dx: 2px; --dy: 3px;
  padding: 7px 10px; font-size: 10px; line-height: 1.45;
  border-color: color-mix(in oklab, var(--hue) 28%, var(--rule));
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5), 0 0 30px -10px color-mix(in oklab, var(--hue) 50%, transparent);
  animation: fleet-hover var(--dur, 12s) ease-in-out infinite alternate;
}
.fleet[data-tier="phone"] .win .head { padding: 0; border: 0; background: none; gap: 6px; }
.fleet[data-tier="phone"] .win .head .st { padding: 0; padding-inline-start: 10px; border: 0; font-size: 8.5px; }
.fleet[data-tier="phone"] .win .log { padding: 0; min-block-size: 0; }
.fleet[data-tier="phone"] .win .log div:not(:last-child) { display: none; }
.fleet[data-tier="phone"] .win .log div:last-child { overflow: hidden; text-overflow: ellipsis; }
.fleet[data-tier="phone"] .win .log div:last-child::after { content: none; }
.fleet[data-tier="phone"] .win .meter { display: none; }

/* A reader who has asked for no motion gets no canvas at all rather than a
   still frame of one, because a still frame of dots means nothing. The fleet
   is the opposite case -- a still frame of it still reads -- so it stays, at
   rest: walls at their angle, windows on their first lines, nothing
   arriving, drifting or leaving. `fleet.js` starts no clock for this reader
   and does not follow the scroll. */
@media (prefers-reduced-motion: reduce) {
  #field { display: none; }
  body { background: var(--ground); }
  .fleet .wall.l { transform: rotateY(24deg); }
  .fleet .wall.r { transform: rotateY(-24deg); }
  .fleet .wall,
  .fleet .wall .win { opacity: 1; }
  .fleet .wall .win { transform: none; }
  .fleet .mini { transform: rotateY(var(--yaw)) scale(var(--s)); opacity: var(--o); }
  .fleet.arrived .wall,
  .fleet .win .body,
  .fleet .win .log div,
  .fleet .win .log div:last-child::after { animation: none; }
  .fleet .win .body { opacity: 1; translate: none; transition: none; }
  .fleet[data-tier="phone"] .win { transform: none; opacity: 1; }
  .fleet[data-tier="phone"] .win .body { animation: none; }
  .fleet[data-tier="phone"] .ties { opacity: 0.9; }
}
