/* Shift — a shift checklist and handover demo.
   The whole interface is built around one rule: a block closes when the hour does. Time is the
   spine, so the layout is a vertical rail rather than a dashboard or a card grid. */

:root {
  --ink: #101318;
  --panel: #171b22;
  --panel-2: #1d222b;
  --line: #262c36;
  --chalk: #e9edf2;
  --mute: #8a94a3;
  --live: #e8a33d;
  --sealed: #4fb477;
  --missed: #d9544d;
  --rail: 3.25rem;
  /* Where the track sits inside the block grid, so the line and every node share one x. */
  --rail-x: 2.6rem;
  --font-ui: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-data: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
  --font-hand: "Caveat", "Segoe Script", "Bradley Hand", cursive;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  color-scheme: dark;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  margin: 0;
  background: var(--ink);
  color: var(--chalk);
  font-family: var(--font-ui);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* `clip`, never `hidden`. `hidden` computes the other axis to `auto`, which makes this a scroll
     container, which silently stops every `position: sticky` inside the page from pinning. */
  overflow-x: clip;
}
img { max-width: 100%; display: block; }
button { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--live); outline-offset: 2px; border-radius: 2px; }
::selection { background: var(--live); color: var(--ink); }

.wrap { max-width: 74rem; margin: 0 auto; padding: 0 1.25rem; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---------------------------------------------------------------- masthead */
.masthead {
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 40;
  /* Solid, not a blurred panel. A backdrop-filter sitting over a scroll story makes Safari
     recompute the blur every frame, which is one of the four things that made pinned sections
     shake. On a dark ground the solid version looks identical. */
  background: var(--ink);
}
.masthead .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; min-height: 3.5rem;
}
.mast-right { display: flex; align-items: center; gap: 0.75rem; }
.mark {
  font-family: var(--font-data);
  font-size: 0.9rem; letter-spacing: 0.22em; text-transform: uppercase;
  display: flex; align-items: center; gap: 0.6rem;
}
.mark b { font-weight: 600; letter-spacing: 0.22em; }
.mark .dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%;
  background: var(--live); box-shadow: 0 0 0 3px color-mix(in srgb, var(--live) 22%, transparent);
}
.mark span { color: var(--mute); letter-spacing: 0.14em; }

.mast-cta {
  display: none;
  font-family: var(--font-data); font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink); background: var(--live); border-radius: 4px;
  padding: 0.55rem 0.9rem; text-decoration: none; font-weight: 600;
}
@media (min-width: 46rem) { .mast-cta { display: inline-block; } }

.trade-picker { display: flex; align-items: center; gap: 0.55rem; }
.trade-picker > span {
  font-family: var(--font-data); font-size: 0.68rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--mute);
}
@media (max-width: 30rem) { .trade-picker > span { display: none; } }
.trade-picker select {
  font-family: var(--font-data); font-size: 0.78rem; letter-spacing: 0.06em;
  background: var(--panel-2); color: var(--chalk);
  border: 1px solid var(--line); border-radius: 4px;
  padding: 0.4rem 0.55rem; min-height: 2.25rem;
}

/* ---------------------------------------------------------------- shared type */
h1 {
  font-size: clamp(2.15rem, 5.6vw, 3.9rem);
  line-height: 1.02; letter-spacing: -0.038em; font-weight: 700;
  margin: 0 0 1.25rem; text-wrap: balance; max-width: 15ch;
}
h1 em { font-style: normal; color: var(--live); }
.lede { font-size: clamp(1rem, 1.5vw, 1.14rem); color: var(--mute); margin: 0 0 1.75rem; max-width: 46ch; }
.lede strong { color: var(--chalk); font-weight: 600; }

.btn-lg {
  display: inline-block; text-decoration: none;
  font-family: var(--font-data); font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase;
  font-weight: 600; color: var(--ink); background: var(--live);
  border: 1px solid var(--live); border-radius: 5px; padding: 0.85rem 1.4rem;
}
.btn-lg.ghost { background: transparent; border-color: var(--line); color: var(--chalk); font-weight: 500; }
.btn-lg.ghost:hover { border-color: var(--mute); }

/* ════════════════════════════════════════════════════════════════════════════
 * SHIFT SEALS — the signature scroll moment.
 *
 * A whole shift plays out on the time rail as you scroll. The clock travels down
 * the rail, each hour comes due and its jobs get ticked, one job runs out of
 * time and turns red, the handover is signed by both people and the shift seals.
 * The red one then walks down onto the handover sheet instead of disappearing.
 *
 *   .seal         the runway. Tall, and it owns the named view timeline.
 *   .seal-pane    sticky, exactly 100svh. Never taller, or it slides before it pins.
 *   .seal-board   the stage. Carries the size container so travel distances can be
 *                 written as a share of the board and hold at every screen size.
 *
 * NO JAVASCRIPT IN ANY OF THIS. Every hidden start state lives inside keyframes
 * behind `@supports (animation-timeline: view())` and `prefers-reduced-motion`,
 * so Firefox and anyone who asks for less motion gets the finished, sealed shift,
 * fully readable. There is no failure mode where the board is empty.
 *
 * SAFARI. The sticky pane carries `will-change: transform` and
 * `backface-visibility: hidden`, and deliberately has NO `container-type` and NO
 * `overflow: clip`: that combination is what stopped pinned sections shaking in
 * Safari while Chrome looked fine. Nothing containing text scales or rotates.
 * State changes (a lamp going from amber to green) use `steps(1)` so they repaint
 * once rather than on every scroll frame.
 * ════════════════════════════════════════════════════════════════════════════ */

.seal {
  position: relative;
  /* The pane plus a runway of about three and a quarter screens. Long enough for eleven beats
     to land, short enough that nobody feels trapped in it. */
  height: 420svh;
}
.seal-grid { height: 100%; display: flex; flex-direction: column; }
.seal-track { flex: 1; min-height: 0; }

.seal-copy { padding: 2.5rem 1.25rem 0; max-width: 40rem; margin: 0 auto; }
.seal-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; }
.seal-note {
  margin: 1.8rem 0 0; display: flex; align-items: center; gap: 0.7rem;
  font-family: var(--font-data); font-size: 0.6rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: color-mix(in srgb, var(--mute) 72%, transparent);
}
.seal-note::before {
  content: ""; width: clamp(1.4rem, 4vw, 2.8rem); height: 1px; background: var(--live);
  transform-origin: left center; flex: none;
}

.seal-pane {
  position: sticky; top: 0; height: 100svh;
  display: flex; align-items: center; justify-content: center;
  padding: 3.5rem 1.25rem 1.25rem;
  will-change: transform;
  backface-visibility: hidden;
}

@media (min-width: 62rem) {
  .seal-grid {
    display: grid; grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
    align-items: stretch;
    gap: clamp(2rem, 4vw, 4.5rem);
    max-width: 78rem; margin: 0 auto; padding: 0 clamp(1.5rem, 3vw, 3rem);
  }
  .seal-copy {
    position: sticky; top: 0; height: 100svh;
    display: flex; flex-direction: column; justify-content: center;
    padding: 3.5rem 0 1.5rem; max-width: none;
    will-change: transform;
    backface-visibility: hidden;
  }
  .seal-pane { padding: 3.5rem 0 1.5rem; justify-content: flex-start; }
}

.seal-figure { margin: 0; width: 100%; max-width: 35rem; display: flex; flex-direction: column; gap: 1rem; }

/* ── the board ─────────────────────────────────────────────────────────────
 * Size containment lives HERE and not on the sticky pane, so the travel of the
 * missed job can be written in cq units (a share of the board) and lands in the
 * same place whatever the screen is. */
.seal-board {
  position: relative;
  width: 100%;
  /* The board's height is a plain rem value, switched in steps by viewport height below, and
     NOT `min(76svh, ...)`. Everything inside is a percentage of it or a rem, which makes the
     distance the missed job travels an exact sum rather than a guess.
     Container query units were tried for that travel and were abandoned: Chromium resolved
     `cqh` inside the keyframe against a stale board height, so the job landed in a different
     place depending on how you had scrolled to get there. */
  --sb-h: 42rem;
  height: var(--sb-h);
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--panel) 92%, #fff 2%), var(--panel) 38%);
  border: 1px solid var(--line);
  border-radius: 0.9rem;
  overflow: clip;
  box-shadow: 0 1.8rem 3.6rem -1.6rem rgba(0, 0, 0, 0.9);
  --sb-time-w: 2.9rem;
  --sb-gap: 1.25rem;
  --sb-pad: 1.05rem;
  --sb-stamp-gutter: 6rem;
  /* Where the missed job lands on the handover sheet.
     Across: the gap between the rail's job column and the sheet's left margin, which is a fixed
     sum of rems whatever the board is.
     Down: affine in the board height, because the rail and the sheet are both a percentage of it
     and everything inside them is rem. Measured at two board heights and solved. */
  --carry-dx: -3.55rem;
  --carry-dy: 15.11rem;
}

/* The board steps down on short viewports rather than scaling, so that the travel above stays an
   exact number. Each step carries its own measured drop. */
@media (min-width: 62rem) and (max-height: 51rem) {
  .seal-board { --sb-h: 38rem; --carry-dy: 14.33rem; }
}
@media (min-width: 62rem) and (max-height: 47rem) {
  .seal-board { --sb-h: 34rem; --carry-dy: 12.49rem; }
}

.sb-head {
  height: 7%;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--sb-pad);
  border-bottom: 1px solid var(--line);
}
.sb-site { font-size: 0.82rem; font-weight: 600; letter-spacing: -0.01em; }
.sb-date { font-family: var(--font-data); font-size: 0.6rem; color: var(--mute); letter-spacing: 0.1em; }

.sb-rail {
  position: relative;
  height: 55%;
  padding: 0.9rem var(--sb-pad) 1.7rem;
  display: flex; flex-direction: column; justify-content: space-between;
}
.sb-spine {
  position: absolute; z-index: 0;
  left: calc(var(--sb-pad) + var(--sb-time-w) + var(--sb-gap) / 2);
  top: 0.4rem; bottom: 1.2rem; width: 1px;
  background: var(--line);
  transform: translateX(-0.5px) scaleY(1);
  transform-origin: top center;
}

/* The clock. It is the full height of the rail with the bar pinned to its own top, so walking it
   from the first hour to the handover is translateY(0) to translateY(96%) and needs no measured
   distance. */
.sb-now {
  position: absolute; z-index: 2;
  left: var(--sb-pad); right: var(--sb-pad); top: 0.9rem;
  /* Stops short of the rail's own bottom so the readout lands in clear air under the last
     job rather than sitting on top of its completion time. */
  height: calc(100% - 2.4rem);
  pointer-events: none;
  transform: translateY(100%);
}
.sb-now-bar {
  position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: linear-gradient(to right,
    color-mix(in srgb, var(--live) 0%, transparent),
    color-mix(in srgb, var(--live) 70%, transparent) 18%,
    var(--live) 55%,
    color-mix(in srgb, var(--live) 24%, transparent));
}
.sb-roll {
  position: absolute; right: 0; top: 0;
  transform: translateY(-50%);
  display: inline-grid;
  background: var(--ink);
  border: 1px solid color-mix(in srgb, var(--live) 45%, transparent);
  border-radius: 3px; padding: 0.12rem 0.34rem;
}
.sb-roll i {
  grid-area: 1 / 1; font-style: normal;
  font-family: var(--font-data); font-size: 0.62rem; letter-spacing: 0.04em; color: var(--live);
  opacity: 0;
}
/* The last readout is the sealed time, and it is the one that shows with no scroll timeline. */
.sb-roll i:last-child { opacity: 1; }

/* ── blocks and jobs ──────────────────────────────────────────────────────── */
/* Deliberately NO z-index and no stacking context on the block. The missed job has to be able to
   climb above the handover sheet when it walks down onto it; boxing it into the block's own
   stacking context left it stuck underneath and the carry looked like the job had vanished. */
.sb-block {
  position: relative;
  display: grid;
  grid-template-columns: var(--sb-time-w) minmax(0, 1fr);
  column-gap: var(--sb-gap);
  align-content: start;
}
.sb-mark {
  position: absolute; z-index: 2;
  left: calc(var(--sb-time-w) + var(--sb-gap) / 2);
  top: 0.32rem;
  width: 0.55rem; height: 0.55rem; border-radius: 50%;
  transform: translateX(-50%);
  background: var(--sealed);
  box-shadow: 0 0 0 3px var(--panel);
}
.sb-block.miss .sb-mark { background: var(--missed); }
.sb-time {
  grid-column: 1; grid-row: 1;
  font-family: var(--font-data); font-size: 0.68rem; color: var(--mute);
  text-align: right; line-height: 1.25;
}
.sb-tags { grid-column: 2; grid-row: 1; display: inline-grid; justify-items: start; }
.sb-tag {
  grid-area: 1 / 1;
  font-family: var(--font-data); font-size: 0.53rem; font-weight: 500;
  letter-spacing: 0.17em; text-transform: uppercase;
  border: 1px solid currentColor; border-radius: 3px; padding: 0.04rem 0.32rem;
  white-space: nowrap;
}
.sb-tag.done { color: var(--sealed); }
.sb-tag.bad { color: var(--missed); }
.sb-tag.live { color: var(--live); opacity: 0; }
.sb-tag.lock { color: color-mix(in srgb, var(--mute) 70%, transparent); opacity: 0; }

.sb-slot { grid-column: 2; position: relative; height: 2.35rem; }
.sb-ghost {
  position: absolute; inset: 0.3rem 0 0.3rem 0;
  display: flex; align-items: center; padding-left: 1.7rem;
  border: 1px dashed color-mix(in srgb, var(--missed) 34%, transparent);
  border-radius: 4px;
  font-family: var(--font-data); font-size: 0.55rem; letter-spacing: 0.08em;
  color: color-mix(in srgb, var(--missed) 72%, var(--mute));
}
.sb-row {
  position: absolute; inset: 0;
  display: flex; align-items: center; gap: 0.55rem;
}
.sb-row.missed {
  /* Narrower than the other rows so that when it lands in the carried-over field it sits
     inside the box with air on both sides rather than flush against its edges. */
  z-index: 6;
  right: 2rem;
  translate: var(--carry-dx) var(--carry-dy);
  /* A solid slip, not bare text. On its way down to the carried-over field it passes over
     "Cardio machines wiped down" and then the two signatures' labels; with no background of its
     own the words printed through each other and read as a rendering fault. Opaque, it reads as
     what it is: one job being physically carried past everything else to the handover. */
  padding: 0 0.6rem;
  margin-left: -0.6rem;
  /* Exactly the colour the carried-over box shows (its ten per cent red tint over the panel), but
     opaque, so once it lands it is indistinguishable from the box it sits in, and on the way down
     it still hides whatever it passes over. */
  background: color-mix(in srgb, var(--missed) 10%, var(--panel-2));
  border-radius: 6px;
  /* No border and no shadow of its own. With them, the slip landed inside the carried-over box
     as a second outlined box a few pixels off the first (Tom spotted it). The solid background
     is all it needs to cover the rows it passes over on the way down. */
}
.sb-box {
  flex: none; width: 1.05rem; height: 1.05rem; border-radius: 3px;
  border: 1px solid var(--sealed); background: color-mix(in srgb, var(--sealed) 20%, transparent);
  display: grid; place-items: center;
}
.sb-box svg { width: 0.72rem; height: 0.72rem; overflow: visible; }
.sb-tick {
  fill: none; stroke: var(--sealed); stroke-width: 2.2;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 1; stroke-dashoffset: 0;
}
.sb-row.missed .sb-box { border-color: var(--missed); background: color-mix(in srgb, var(--missed) 16%, transparent); }
.sb-cross { width: 0.55rem; height: 0.55rem; position: relative; }
.sb-cross::before, .sb-cross::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1.5px;
  background: var(--missed); border-radius: 2px;
}
.sb-cross::before { transform: rotate(45deg); }
.sb-cross::after { transform: rotate(-45deg); }

/* Hugs its own text, so the red underline that gets drawn when the hour runs out is under
   the words and not a rule across the whole row. */
.sb-job {
  position: relative; min-width: 0; flex: 0 1 auto;
  font-size: 0.76rem; line-height: 1.25; color: var(--mute);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sb-row.missed .sb-job > span:first-child { color: color-mix(in srgb, var(--chalk) 80%, var(--missed)); }
.sb-underline {
  position: absolute; left: 0; right: 0; bottom: 0.1rem; height: 1.5px;
  background: var(--missed); border-radius: 2px;
  transform: scaleX(1); transform-origin: left center;
}
.sb-at {
  flex: none; margin-left: auto; padding-left: 0.5rem;
  font-family: var(--font-data); font-size: 0.58rem; letter-spacing: 0.04em;
  color: color-mix(in srgb, var(--sealed) 78%, var(--mute));
}
.sb-at.bad { color: var(--missed); }

/* ── the handover sheet ───────────────────────────────────────────────────── */
/* It is on the board from the first frame, as a blank form. An earlier version slid it up from
   under the board at the two thirds mark, which left a third of the board empty until then and
   read as a hole rather than as a sheet. */
.sb-sheet {
  position: absolute; z-index: 4; left: 0; right: 0; bottom: 0;
  height: 37%;
  padding: 0.8rem var(--sb-stamp-gutter) 0.85rem var(--sb-pad);
  background: linear-gradient(180deg, var(--panel-2), color-mix(in srgb, var(--panel-2) 88%, #000));
  border-top: 1px solid var(--line);
  /* space-between, so the slack in the sheet is shared out between the form's fields rather
     than piling up as one hole above the day strip. */
  display: flex; flex-direction: column; justify-content: space-between; gap: 0.45rem;
  box-shadow: 0 -1rem 2rem -1rem rgba(0, 0, 0, 0.7);
}
.sb-sheet-head {
  display: flex; align-items: baseline; justify-content: space-between;
  font-family: var(--font-data); font-size: 0.58rem; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--mute);
  padding-bottom: 0.4rem;
}
.sb-sigs { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; }
.sb-sig { position: relative; display: flex; flex-direction: column; }
.sb-sig-ink { display: block; width: 100%; height: 2.9rem; overflow: visible; }
.sb-sig-path {
  fill: none; stroke: var(--chalk); stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 1; stroke-dashoffset: 0;
}
.sb-sig-rule { height: 1px; background: var(--line); }
.sb-sig-cap {
  margin-top: 0.3rem;
  font-family: var(--font-data); font-size: 0.55rem; letter-spacing: 0.07em; color: var(--mute);
}
.sb-sig-cap b { color: var(--chalk); font-weight: 500; }

.sb-carry {
  position: relative;
  height: 2.5rem;
  border: 1px solid color-mix(in srgb, var(--missed) 38%, transparent);
  background: color-mix(in srgb, var(--missed) 10%, transparent);
  border-radius: 5px;
}
.sb-carry-label {
  position: absolute; top: -0.42rem; left: 0.6rem;
  background: var(--panel-2); padding: 0 0.3rem;
  font-family: var(--font-data); font-size: 0.5rem; letter-spacing: 0.17em;
  text-transform: uppercase; color: var(--missed);
}

.sb-day { display: flex; gap: 2px; }
.sb-day i {
  flex: 1; height: 1.1rem; border-radius: 2px;
  background: color-mix(in srgb, var(--sealed) 62%, transparent);
}
.sb-day i.bad { background: color-mix(in srgb, var(--missed) 82%, transparent); }

.sb-owner {
  margin: 0; display: grid;
  font-family: var(--font-data); font-size: 0.55rem; letter-spacing: 0.06em; color: var(--mute);
}
.sb-owner > span { grid-area: 1 / 1; }
.sb-owner b { color: var(--chalk); font-weight: 500; }
.sb-owner-a { opacity: 0; }

/* The stamp gets its own margin down the right of the sheet. Slammed across the signatures it
   looked better and made the second name unreadable, which is the wrong trade. */
.sb-stamp {
  position: absolute; z-index: 5;
  right: 0.75rem; top: 50%;
  width: 5rem; height: 5rem;
  margin-top: -2.5rem;
  display: grid; place-items: center;
  rotate: -11deg;
  opacity: 0.95;
}
.sb-stamp svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.sb-stamp-ring {
  fill: none; stroke: var(--sealed); stroke-width: 3.5;
  stroke-dasharray: 1; stroke-dashoffset: 0;
}
.sb-stamp span {
  font-family: var(--font-data); font-size: 0.6rem; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--sealed);
}

/* ── captions: the same story in words ────────────────────────────────────── */
/* A window one caption tall, with the eight captions in a column inside it. The column slides
   up one caption at each beat, so the old words physically leave rather than fading.

   They used to be stacked in one grid cell and cross-faded, each window overlapping the next by
   one per cent. Chromium and Playwright's WebKit cleared them cleanly, but real Safari leaves
   the outgoing text visible, which is the same fault Tom found on Clarity Dental and Fathom: the
   last frame of the story read as two sentences printed over each other. With a window, a
   mistimed engine can only ever show the wrong caption, never two.

   Every row is the same fixed height so one row is exactly an eighth of the column and the
   keyframes can be plain percentages. The height fits the longest caption at each width. */
.seal-captions { height: 3.4rem; overflow: hidden; }
.seal-captions-col { transform: translateY(-87.5%); will-change: transform; }
.seal-captions p {
  height: 3.4rem; margin: 0;
  display: flex; align-items: flex-start;
  font-size: 0.92rem; line-height: 1.45; color: var(--mute);
  max-width: 46ch;
}
.seal-captions p b { color: var(--chalk); font-weight: 600; }
/* The LAST caption is the one that shows with no scroll timeline (the column rests at -87.5%),
   because the board's own base state is the sealed shift. Leaving the first one visible had a
   fallback reader looking at a finished board under the words "the shift opens". */

/* ── choreography ─────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .seal-track { view-timeline-name: --seal; }

    /* The state animations (a lamp going green, a box filling, a field turning red) run with
       `none` fill, because a fill mode would leave their keyframe applied for the whole rest of
       the scroll. That means outside their range the element falls back to its base style, which
       is the FINISHED state, so they all have to start at `cover 0%` and not `contain 0%`.
       Started at `contain 0%` the board sat there fully sealed while the section scrolled up
       into place and then snapped back to the locked start the moment it pinned.

       Everything that moves gets promoted, and only the things that move. Promoting the whole
       board put the rail, the sheet and the text on one layer and Safari re-rasterised all of it
       every frame. */
    .sb-now, .sb-row.missed, .sb-stamp, .sb-sig-ink { will-change: transform, opacity; }

    .sb-spine {
      animation: sb-grow linear both;
      animation-timeline: --seal;
      animation-range: cover 0% contain 7%;
    }

    /* The clock walks the rail. `contain`, not `cover`: the pane is pinned across the contained
       part of the runway, so it starts the moment the board pins. */
    .sb-now {
      animation: sb-clock linear both;
      animation-timeline: --seal;
      animation-range: contain 3% contain 56%;
    }
    /* Each readout holds for its own slice. `steps(1)` so the numbers snap like an instrument
       instead of dissolving, and `none` fill so a readout does not linger past its slice. */
    .sb-roll i {
      animation: sb-flash steps(1) none;
      animation-timeline: --seal;
      animation-range:
        contain calc((3 + var(--i) * 3.53) * 1%)
        contain calc((3 + (var(--i) + 1) * 3.53) * 1%);
    }
    /* The final readout is the base state, so it has to be actively HIDDEN until the clock
       reaches it. Leaving it to its own slice printed it underneath every other readout all the
       way down the rail and the clock looked double exposed. It then HOLDS to the end, because
       the rail has run out but the shift has not. */
    .sb-roll i:first-child {
      animation-range: cover 0% contain 6.53%;
    }
    .sb-roll i:last-child {
      animation-name: sb-hide;
      animation-range: cover 0% contain 52.4%;
    }

    /* Lamps. Grey until the hour comes due, amber while it is open, then the base colour, which
       is already the sealed one. One repaint each, not one per frame. */
    .sb-mark {
      animation: sb-mark-locked steps(1) none, sb-mark-live steps(1) none;
      animation-timeline: --seal, --seal;
      animation-range:
        cover 0% contain calc(var(--a) * 1%),
        contain calc(var(--a) * 1%) contain calc(var(--c) * 1%);
    }
    .sb-tag.done, .sb-tag.bad {
      animation: sb-hide steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc(var(--c) * 1%);
    }
    .sb-tag.live {
      animation: sb-hold steps(1) none;
      animation-timeline: --seal;
      animation-range: contain calc(var(--a) * 1%) contain calc(var(--c) * 1%);
    }
    .sb-tag.lock {
      animation: sb-hold steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc(var(--a) * 1%);
    }

    /* A job waiting for its hour is on the board but held back. Then its tick draws itself. */
    .sb-row {
      animation: sb-dim steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc(var(--lk) * 1%);
    }
    .sb-tick {
      animation: sb-draw linear both;
      animation-timeline: --seal;
      animation-range: contain calc(var(--t1) * 1%) contain calc(var(--t2) * 1%);
    }
    .sb-box {
      animation: sb-box-empty steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc(var(--t1) * 1%);
    }
    .sb-at {
      animation: sb-fade linear both;
      animation-timeline: --seal;
      animation-range: contain calc(var(--t1) * 1%) contain calc(var(--t2) * 1%);
    }

    /* The one that does not get done. It waits like the others, its hour runs out, it is struck
       through and stamped, and then it walks down onto the handover sheet rather than being
       quietly dropped off the board. */
    .sb-row.missed {
      animation: sb-dim steps(1) none, sb-carry linear both;
      animation-timeline: --seal, --seal;
      animation-range:
        cover 0% contain calc(var(--lk) * 1%),
        contain 89% contain 95%;
    }
    /* Until its hour runs out it is just another job waiting, not a red one. */
    .sb-row.missed .sb-job > span:first-child {
      animation: sb-plain-text steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc(var(--t1) * 1%);
    }
    .sb-cross {
      animation: sb-hide steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc(var(--t1) * 1%);
    }
    .sb-underline {
      animation: sb-strike-draw linear both;
      animation-timeline: --seal;
      animation-range: contain calc(var(--t1) * 1%) contain calc(var(--t2) * 1%);
    }
    /* The empty slot only shows once the job has actually walked off it. */
    .sb-ghost {
      animation: sb-fade linear both;
      animation-timeline: --seal;
      animation-range: contain 90% contain 95%;
    }

    .sb-sig-path {
      animation: sb-draw linear both;
      animation-timeline: --seal;
      animation-range: contain calc(var(--a) * 1%) contain calc(var(--b) * 1%);
    }
    .sb-sig:first-child .sb-sig-cap b {
      animation: sb-ink-in linear both;
      animation-timeline: --seal;
      animation-range: contain 64% contain 75%;
    }
    .sb-sig:last-child .sb-sig-cap b {
      animation: sb-ink-in linear both;
      animation-timeline: --seal;
      animation-range: contain 77% contain 88%;
    }
    /* The carried-over box is a blank field on the form until something actually has to go in it. */
    .sb-carry {
      animation: sb-field-blank steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain 87%;
    }
    .sb-carry-label {
      animation: sb-label-blank steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain 87%;
    }
    .sb-stamp {
      animation: sb-stamp-in linear both;
      animation-timeline: --seal;
      animation-range: contain calc(var(--a) * 1%) contain calc(var(--b) * 1%);
    }
    .sb-stamp-ring {
      animation: sb-draw linear both;
      animation-timeline: --seal;
      animation-range: contain 88% contain 94%;
    }
    /* The owner's day assembles a cell at a time. Stagger comes from shifting the range, because
       animation-delay does nothing on a scroll timeline: a view timeline has no time in it. */
    /* The day is a row of blank slabs all the way through, and the colour is written into it
       one hour at a time at the end. Cells that faded in from nothing left a hole in the sheet
       for the whole story and read as a rendering fault. */
    .sb-day i {
      animation: sb-cell-blank steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain calc((88 + var(--i) * 0.6) * 1%);
    }
    /* A hard swap with steps(1), not a cross-fade. Fading one line over the other printed both
       sets of words on top of each other for a quarter of a screen. */
    .sb-owner-a {
      animation: sb-hold steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain 94%;
    }
    .sb-owner-b {
      animation: sb-hide steps(1) none;
      animation-timeline: --seal;
      animation-range: cover 0% contain 94%;
    }

    /* The caption column holds still for each beat and moves only across the beat boundaries,
       so text is never in motion for long enough to shimmer while the pane is pinned. The range
       is literal: var() inside animation-range is the common factor in the Safari ghosting. */
    .seal-captions-col {
      animation: sb-captions linear both;
      animation-timeline: --seal;
      animation-range: contain 0% contain 100%;
    }

  }
}

@keyframes sb-grow { from { transform: translateX(-0.5px) scaleY(0); } to { transform: translateX(-0.5px) scaleY(1); } }
@keyframes sb-clock { from { transform: translateY(0); } to { transform: translateY(100%); } }
@keyframes sb-flash { 0%, 100% { opacity: 1; } }
@keyframes sb-hold { 0%, 100% { opacity: 1; } }
@keyframes sb-hide { 0%, 100% { opacity: 0; } }
@keyframes sb-dim { 0%, 100% { opacity: 0.4; } }
@keyframes sb-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes sb-ink-in { from { opacity: 0.35; } to { opacity: 1; } }
/* 1.02, not 1. At exactly 1 the round line cap of a fully offset dash still paints a dot at the
   start of the path, so two stray white pips sat on the blank signature lines for most of the
   story. Pushing the offset just past the path length hides them. */
/* The opacity step is not decoration. A round line cap on a dash that is fully offset still
   paints a dot at the start of the path, so every unticked box had a pip in it and two stray
   marks sat on the blank signature lines. Holding the stroke invisible for the first fraction
   of its own draw is the cheapest fix that keeps the draw itself linear. */
@keyframes sb-draw {
  from { stroke-dashoffset: 1; opacity: 0; }
  4% { opacity: 1; }
  to { stroke-dashoffset: 0; opacity: 1; }
}
@keyframes sb-strike-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes sb-rule-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes sb-carry {
  from { translate: 0 0; }
  to { translate: var(--carry-dx) var(--carry-dy); }
}
@keyframes sb-box-empty {
  0%, 100% { border-color: var(--line); background: transparent; }
}
@keyframes sb-field-blank {
  0%, 100% { border-color: var(--line); border-style: dashed; background: transparent; }
}
@keyframes sb-label-blank {
  0%, 100% { color: color-mix(in srgb, var(--mute) 80%, transparent); }
}
@keyframes sb-mark-locked {
  0%, 100% { background: var(--line); box-shadow: 0 0 0 3px var(--panel); }
}
@keyframes sb-mark-live {
  0%, 100% {
    background: var(--live);
    box-shadow: 0 0 0 3px var(--panel), 0 0 0 5px color-mix(in srgb, var(--live) 26%, transparent);
  }
}
@keyframes sb-stamp-in {
  from { opacity: 0; scale: 1.5; }
  to { opacity: 0.95; scale: 1; }
}
@keyframes sb-cell-blank { 0%, 100% { background: color-mix(in srgb, var(--line) 85%, transparent); } }
@keyframes sb-plain-text { 0%, 100% { color: var(--mute); } }
/* Beats change at 12.5, 26.5, 39.5, 54.5, 65.5, 77.5 and 87.5 per cent, the midpoints of the old
   caption windows, so the words still land on the same moments of the board. */
@keyframes sb-captions {
  0%, 11% { transform: translateY(0); }
  14%, 25% { transform: translateY(-12.5%); }
  28%, 38% { transform: translateY(-25%); }
  41%, 53% { transform: translateY(-37.5%); }
  56%, 64% { transform: translateY(-50%); }
  67%, 76% { transform: translateY(-62.5%); }
  79%, 86% { transform: translateY(-75%); }
  89%, 100% { transform: translateY(-87.5%); }
}

/* Phones get the same eleven beats on a shorter board. The copy scrolls away first and only the
   board pins, which is the only way the board stays big enough to read on a 390px screen.
   Nothing here swaps in JavaScript. */
@media (max-width: 61.9375rem) {
  .seal { height: 470svh; }
  .seal-board {
    --sb-h: 30rem;
    --sb-time-w: 2.5rem;
    --sb-gap: 1rem;
    --sb-pad: 0.8rem;
    --sb-stamp-gutter: 4.4rem;
    --carry-dx: -2.55rem;
    --carry-dy: 10.58rem;
  }
  .sb-rail { height: 56%; padding-bottom: 1.5rem; }
  .sb-sheet { height: 37%; }
  .sb-slot { height: 1.8rem; }
  .sb-job { font-size: 0.71rem; }
  .sb-sig-ink { height: 1.6rem; }
  .sb-carry { height: 2.4rem; }
  .sb-carry-label { top: -0.5rem; font-size: 0.46rem; }
  .sb-day i { height: 0.72rem; }
  .sb-stamp { width: 3.7rem; height: 3.7rem; margin-top: -1.85rem; right: 0.45rem; }
  .sb-stamp span { font-size: 0.5rem; letter-spacing: 0.14em; }
  .seal-captions, .seal-captions p { height: 3.3rem; }
  .seal-captions p { font-size: 0.85rem; }
}

@media (max-width: 61.9375rem) and (max-height: 39rem) {
  .seal-board { --sb-h: 27rem; --carry-dy: 8.94rem; }
}
@media (max-width: 61.9375rem) and (max-height: 36rem) {
  .seal-board { --sb-h: 24rem; --carry-dy: 7.26rem; }
}

/* ---------------------------------------------------------------- ambient motion
 * The rest of the page. Same rules as the scroll story: the hidden start state only ever exists
 * inside keyframes, so with no scroll-timeline support every section is simply there. */
.sh-head { position: relative; }
.sh-rule {
  display: block; height: 1px; width: 3.4rem; background: var(--live);
  transform-origin: left center; margin: 0 0 1.1rem;
}
.sh-num { display: inline-block; transform-origin: 0 100%; color: var(--chalk); font-weight: 600; }

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .sh-reveal {
      animation: sh-reveal-in linear both;
      animation-timeline: view();
      /* Stagger by shifting the range. `animation-delay` is inert on a scroll timeline. */
      animation-range: entry calc(0% + var(--d, 0%)) cover calc(30% + var(--d, 0%));
      will-change: transform, opacity;
    }
    .sh-head {
      animation: sh-head-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 26%;
      will-change: transform, opacity;
    }
    .sh-rule {
      animation: sh-rule-draw linear both;
      animation-timeline: view();
      animation-range: entry 8% cover 30%;
    }
    .sh-num {
      animation: sh-num-pop linear both;
      animation-timeline: view();
      animation-range: entry 12% cover 32%;
    }
    /* The demo phone drifts against the scroll inside its column, so the section has depth
       without a stock photograph in it. */
    .sh-drift {
      animation: sh-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      will-change: transform;
    }
  }
}

@keyframes sh-reveal-in {
  from { opacity: 0; transform: translateY(46px); }
  38% { opacity: 1; }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes sh-head-in {
  from { opacity: 0; transform: translateY(28px); }
  55% { opacity: 1; }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes sh-rule-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes sh-num-pop {
  0% { opacity: 0; transform: translateY(14px) scale(0.6); }
  72% { opacity: 1; transform: translateY(-2px) scale(1.07); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes sh-drift { from { transform: translateY(-3.5%); } to { transform: translateY(3.5%); } }

/* hover craft */
.sh-rise { transition: transform 420ms var(--ease), background-color 420ms var(--ease); }
.sh-rise:hover { transform: translateY(-2px); }
.sh-underline { position: relative; text-decoration: none; }
.sh-underline::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -0.14em; height: 1px;
  background: currentColor; transform: scaleX(0); transform-origin: left center;
  transition: transform 380ms var(--ease);
}
.sh-underline:hover::after, .sh-underline:focus-visible::after { transform: scaleX(1); }

/* ---------------------------------------------------------------- device */
.demo-grid { display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
@media (min-width: 62rem) { .demo-grid { grid-template-columns: 1fr 22rem; } }
.demo-copy p { color: var(--mute); max-width: 52ch; }
.demo-legend { list-style: none; margin: 1.6rem 0 0; padding: 0; display: grid; gap: 0.85rem; max-width: 44ch; }
.demo-legend li {
  font-size: 0.92rem; color: var(--mute);
  border-left: 1px solid var(--line); padding-left: 0.95rem;
}
.demo-legend b {
  display: block; color: var(--chalk); font-weight: 600;
  font-family: var(--font-data); font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase;
  margin-bottom: 0.2rem;
}
.demo-hint { font-family: var(--font-data); font-size: 0.7rem; letter-spacing: 0.04em; margin-top: 1.5rem; }

.device-col { position: static; }
@media (min-width: 62rem) { .device-col { position: sticky; top: 5rem; } }
.device {
  width: 100%; max-width: 22rem; margin: 0 auto;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 1.85rem; padding: 0.5rem;
  box-shadow: 0 2rem 4rem -1.5rem rgba(0, 0, 0, 0.85), 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
}
.screen {
  background: var(--ink); border-radius: 1.4rem; overflow: hidden;
  height: 37rem; display: flex; flex-direction: column;
}
.status-bar {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.7rem 1.1rem 0.35rem;
  font-family: var(--font-data); font-size: 0.66rem; color: var(--mute); letter-spacing: 0.06em;
}
.screen-body {
  flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 0 0 1.5rem;
  scrollbar-width: none;
  /* Fades the last few pixels so a cut-off row reads as "there is more" rather than as broken. */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%);
}
.screen-body::-webkit-scrollbar { width: 0; }

/* view switch */
.views { display: flex; gap: 0.4rem; margin: 1.4rem auto 0; max-width: 22rem; }
.views button {
  flex: 1; background: var(--panel); border: 1px solid var(--line); border-radius: 5px;
  padding: 0.6rem 0.3rem; min-height: 2.6rem; cursor: pointer;
  font-family: var(--font-data); font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--mute); transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.views button:hover { color: var(--chalk); border-color: var(--mute); }
.views button[aria-pressed="true"] { color: var(--ink); background: var(--live); border-color: var(--live); font-weight: 600; }

/* ---------------------------------------------------------------- pin */
/* Centred in the screen rather than pinned to the top: the keypad is shorter than the rail
   it replaces, and left at the top it read as a half empty phone. */
.pin {
  padding: 1.6rem 1.4rem; text-align: center; min-height: 100%;
  display: flex; flex-direction: column; justify-content: center;
}
.pin h2 { font-size: 1.05rem; margin: 0 0 0.3rem; letter-spacing: -0.01em; }
.pin p { color: var(--mute); font-size: 0.82rem; margin: 0 0 1.6rem; }
.pin-dots { display: flex; gap: 0.7rem; justify-content: center; margin-bottom: 1.8rem; }
.pin-dots i {
  width: 0.85rem; height: 0.85rem; border-radius: 50%;
  border: 1px solid var(--line); background: transparent; transition: background 0.12s, border-color 0.12s;
}
.pin-dots i.on { background: var(--live); border-color: var(--live); }
.keys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; }
.keys button {
  aspect-ratio: 1.5; border: 1px solid var(--line); background: var(--panel);
  border-radius: 6px; cursor: pointer; font-family: var(--font-data); font-size: 1.15rem;
  transition: background 0.12s;
}
.keys button:hover { background: var(--panel-2); }
.keys button.ghost { border-color: transparent; background: transparent; color: var(--mute); font-size: 0.72rem; letter-spacing: 0.1em; }
.pin-hint { margin-top: 1.2rem; font-family: var(--font-data); font-size: 0.68rem; color: var(--mute); letter-spacing: 0.06em; }

/* ---------------------------------------------------------------- the rail */
.who {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  padding: 0.9rem 1.15rem 0.85rem; border-bottom: 1px solid var(--line);
}
.who b { font-size: 0.95rem; font-weight: 600; }
.who span { font-family: var(--font-data); font-size: 0.66rem; color: var(--mute); letter-spacing: 0.1em; text-transform: uppercase; }

.rail { padding: 0.4rem 1.15rem 0; }
.block { display: grid; grid-template-columns: var(--rail) 1fr; column-gap: 0.7rem; position: relative; }
/* The track. Drawn once per block so it runs unbroken down the whole shift, and stopped short on
   the last one so the line ends at the final node rather than trailing into nothing. */
.block::before {
  content: ""; position: absolute; left: var(--rail-x); top: 0; bottom: 0;
  width: 1px; background: var(--line); transform: translateX(-0.5px);
}
.block:last-child::before { bottom: auto; height: 1.35rem; }

.tick { padding-top: 0.95rem; }
.tick time {
  font-family: var(--font-data); font-size: 0.72rem; color: var(--mute);
  letter-spacing: 0.02em; display: block; text-align: right; padding-right: 1.15rem;
}
.node {
  position: absolute; left: var(--rail-x); top: 1.02rem;
  width: 0.62rem; height: 0.62rem; border-radius: 50%; transform: translateX(-50%);
  background: var(--ink); border: 1px solid var(--mute); z-index: 1;
}
.block[data-state="sealed"] .node { background: var(--sealed); border-color: var(--sealed); }
.block[data-state="missed"] .node { background: var(--missed); border-color: var(--missed); }
.block[data-state="live"] .node {
  background: var(--live); border-color: var(--live);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--live) 20%, transparent);
}
.block[data-state="sealed"] time, .block[data-state="missed"] time { color: var(--mute); }
.block[data-state="live"] time { color: var(--live); }

.block-body { padding: 0.85rem 0 1.1rem; min-width: 0; }
.block-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.5rem; }
.state {
  font-family: var(--font-data); font-size: 0.6rem; letter-spacing: 0.16em; text-transform: uppercase;
  padding: 0.14rem 0.4rem; border-radius: 3px; border: 1px solid currentColor; flex: none;
}
.block[data-state="sealed"] .state { color: var(--sealed); }
.block[data-state="missed"] .state { color: var(--missed); }
.block[data-state="live"] .state { color: var(--live); }
.block[data-state="locked"] .state { color: var(--mute); }
.count { font-family: var(--font-data); font-size: 0.7rem; color: var(--mute); }

.jobs { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; }
.job { display: flex; align-items: flex-start; gap: 0.6rem; }
.job button {
  display: flex; align-items: flex-start; gap: 0.6rem; width: 100%; text-align: left;
  background: transparent; border: 0; padding: 0.42rem 0.1rem; cursor: pointer; min-height: 2.25rem;
  border-radius: 4px;
}
.job button:disabled { cursor: default; }
.box {
  flex: none; width: 1.1rem; height: 1.1rem; border-radius: 3px;
  border: 1px solid var(--line); margin-top: 0.1rem;
  display: grid; place-items: center; font-size: 0.7rem; color: var(--ink);
  transition: background 0.14s, border-color 0.14s;
}
.job[data-done="true"] .box { background: var(--sealed); border-color: var(--sealed); }
.job[data-missed="true"] .box { border-color: var(--missed); border-style: dashed; }
.job-text { font-size: 0.87rem; line-height: 1.35; min-width: 0; }
.job[data-done="true"] .job-text { color: var(--mute); }
.block[data-state="locked"] .job-text { color: color-mix(in srgb, var(--mute) 62%, transparent); }
.needs-photo {
  font-family: var(--font-data); font-size: 0.58rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--live); border: 1px solid color-mix(in srgb, var(--live) 40%, transparent);
  border-radius: 3px; padding: 0.05rem 0.3rem; margin-left: 0.4rem; white-space: nowrap;
}
.job[data-done="true"] .needs-photo { color: var(--sealed); border-color: color-mix(in srgb, var(--sealed) 40%, transparent); }
.proof {
  margin: 0.15rem 0 0.5rem 1.7rem; border: 1px solid var(--line); border-radius: 5px;
  overflow: hidden; max-width: 9rem;
}
.proof-img { aspect-ratio: 4 / 3; display: grid; place-items: center; font-size: 1.4rem; }
.proof span {
  display: block; font-family: var(--font-data); font-size: 0.56rem; color: var(--mute);
  padding: 0.24rem 0.4rem; border-top: 1px solid var(--line); letter-spacing: 0.04em;
}
.sealed-note {
  font-family: var(--font-data); font-size: 0.63rem; color: var(--mute);
  letter-spacing: 0.04em; margin-top: 0.5rem;
}
.sealed-note.bad { color: var(--missed); }

/* ---------------------------------------------------------------- handover */
.pane { padding: 1.15rem; }
.pane h2 { font-size: 1rem; margin: 0 0 0.3rem; letter-spacing: -0.01em; }
.pane .sub { color: var(--mute); font-size: 0.82rem; margin: 0 0 1.15rem; }
.chain { border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
.leg { padding: 0.95rem 1rem; }
.leg + .leg { border-top: 1px solid var(--line); }
.leg-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.55rem;
}
.leg-who { font-family: var(--font-data); font-size: 0.64rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
.leg b { font-size: 0.92rem; }
.sig {
  border: 1px dashed var(--line); border-radius: 5px; padding: 0.8rem;
  display: grid; place-items: center; min-height: 4rem; text-align: center;
}
.sig.signed { border-style: solid; border-color: color-mix(in srgb, var(--sealed) 45%, transparent); }
.sig-name { font-family: var(--font-hand); font-size: 1.75rem; line-height: 1; color: var(--chalk); }
.sig-meta { font-family: var(--font-data); font-size: 0.58rem; color: var(--mute); margin-top: 0.35rem; letter-spacing: 0.06em; }
.btn {
  width: 100%; min-height: 2.75rem; border-radius: 5px; cursor: pointer;
  font-family: var(--font-data); font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--chalk);
  transition: background 0.14s, border-color 0.14s;
}
.btn:hover { background: var(--panel); }
.btn.primary { background: var(--live); border-color: var(--live); color: var(--ink); font-weight: 600; }
.btn.primary:hover { background: color-mix(in srgb, var(--live) 88%, #fff); }
.btn.danger { color: var(--missed); border-color: color-mix(in srgb, var(--missed) 45%, transparent); }
.btn-row { display: grid; gap: 0.5rem; margin-top: 0.7rem; }
@media (min-width: 24rem) { .btn-row.two { grid-template-columns: 1fr 1fr; } }
.reject-note {
  margin-top: 0.7rem; border-left: 2px solid var(--missed); padding: 0.55rem 0 0.55rem 0.7rem;
  font-size: 0.83rem; color: var(--chalk);
}
.reject-note span { display: block; font-family: var(--font-data); font-size: 0.58rem; color: var(--missed); letter-spacing: 0.14em; text-transform: uppercase; margin-bottom: 0.2rem; }

/* ---------------------------------------------------------------- owner */
.owner-head { padding: 1.15rem 1.15rem 0.9rem; }
.owner-head .big {
  font-family: var(--font-data); font-size: 2.5rem; line-height: 1; letter-spacing: -0.03em;
}
.owner-head .big.ok { color: var(--sealed); }
.owner-head .big.warn { color: var(--live); }
.owner-head .big.bad { color: var(--missed); }
.owner-head p { margin: 0.35rem 0 0; color: var(--mute); font-size: 0.85rem; }
.strip { display: flex; gap: 2px; padding: 0 1.15rem 1rem; }
.strip i { flex: 1; height: 2.1rem; border-radius: 2px; position: relative; }
.strip i[data-s="sealed"] { background: color-mix(in srgb, var(--sealed) 78%, transparent); }
.strip i[data-s="missed"] { background: color-mix(in srgb, var(--missed) 78%, transparent); }
.strip i[data-s="live"] { background: color-mix(in srgb, var(--live) 78%, transparent); }
.strip i[data-s="locked"] { background: var(--panel-2); }
.owner-sec { padding: 0 1.15rem 1.2rem; }
.owner-sec h3 {
  font-family: var(--font-data); font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--mute); margin: 0 0 0.65rem; font-weight: 400;
}
.row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.7rem; padding: 0.48rem 0; border-top: 1px solid var(--line); font-size: 0.87rem; }
.row .v { font-family: var(--font-data); font-size: 0.8rem; color: var(--mute); flex: none; }
.row .v.bad { color: var(--missed); }
.row .v.ok { color: var(--sealed); }
.alert {
  border: 1px solid color-mix(in srgb, var(--missed) 40%, transparent);
  background: color-mix(in srgb, var(--missed) 9%, transparent);
  border-radius: 6px; padding: 0.75rem 0.85rem; margin: 0 1.15rem 1.2rem;
}
.alert b { display: block; font-size: 0.87rem; margin-bottom: 0.15rem; }
.alert p { margin: 0; font-size: 0.8rem; color: var(--mute); }
.alert .when { font-family: var(--font-data); font-size: 0.58rem; color: var(--missed); letter-spacing: 0.14em; text-transform: uppercase; margin-bottom: 0.35rem; }

/* ---------------------------------------------------------------- prose sections */
.section { border-top: 1px solid var(--line); padding: clamp(3rem, 7vw, 5.5rem) 0; }
.section h2 { font-size: clamp(1.5rem, 3vw, 2.05rem); letter-spacing: -0.03em; margin: 0 0 1rem; max-width: 26ch; text-wrap: balance; }
.section p { color: var(--mute); max-width: 54ch; margin: 0 0 1rem; }
.two-col { display: grid; gap: clamp(1.5rem, 4vw, 3rem); }
@media (min-width: 52rem) { .two-col { grid-template-columns: 1fr 1fr; } }

.lead-split { display: grid; gap: clamp(1.75rem, 4vw, 3.5rem); }
@media (min-width: 52rem) { .lead-split { grid-template-columns: 1.05fr 1fr; align-items: start; } }
.lead-split .big {
  font-size: clamp(1.15rem, 2vw, 1.45rem); line-height: 1.42; color: var(--chalk);
  max-width: 34ch; letter-spacing: -0.015em;
}
.claims { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; max-width: 44ch; }
.claims li {
  font-size: 0.95rem; color: var(--mute);
  border-left: 1px solid var(--line); padding-left: 0.95rem;
}
.claims b { color: var(--chalk); font-weight: 600; }

.trade-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 1.6rem 0 2.4rem; }
.trade-chips button {
  background: transparent; border: 1px solid var(--line); border-radius: 999px;
  padding: 0.5rem 1.05rem; min-height: 2.4rem; cursor: pointer; color: var(--mute);
  font-family: var(--font-data); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
  transition: color 0.16s var(--ease), border-color 0.16s var(--ease), background 0.16s var(--ease);
}
.trade-chips button:hover { color: var(--chalk); border-color: var(--mute); }
.trade-chips button[aria-pressed="true"] { color: var(--ink); background: var(--live); border-color: var(--live); font-weight: 600; }

.trade-note {
  border: 1px solid var(--line); border-radius: 8px; padding: 1.25rem 1.35rem; background: var(--panel);
  transition: border-color 0.3s var(--ease), transform 0.4s var(--ease);
}
.trade-note:hover { border-color: color-mix(in srgb, var(--live) 40%, var(--line)); transform: translateY(-3px); }
.trade-note h3 { margin: 0 0 0.4rem; font-size: 0.95rem; }
.trade-note p { font-size: 0.88rem; margin: 0; }
footer { border-top: 1px solid var(--line); padding: 2rem 0 3rem; }
footer p { color: var(--mute); font-size: 0.8rem; margin: 0; }
footer a { color: var(--chalk); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/*
 * AT REST, ONE SCREEN.
 *
 * The scroll story's runway (several screens tall) and its pin only make sense while something is
 * playing. With reduced motion asked for, or in a browser without scroll timelines, the section
 * shows its finished frame, and without this it still stood three or four screens tall around a
 * pinned pane: those readers scrolled through several screens of a still picture. Found on
 * 2026-09-22 capturing portfolio previews with reduced motion on. Last in the file on purpose, so
 * it also beats the phone runway heights above.
 */
@media (prefers-reduced-motion: reduce) {
  .seal {
    height: auto;
  }

  .seal-pane,
  .seal-copy {
    position: relative;
    height: auto;
    min-height: 100svh;
  }
}

@supports not (animation-timeline: view()) {
  .seal {
    height: auto;
  }

  .seal-pane,
  .seal-copy {
    position: relative;
    height: auto;
    min-height: 100svh;
  }
}
