/* Hallmark · genre: modern-minimal · macrostructure: Split Studio
 * design-system: design.md (locked) · surface: marketing
 * theme: the locked system (custom · "cool graphite instrument panel, signal blue")
 * paper oklch(19% 0.0094 256) dark · display grotesk-sans (Inter) · accent cool (254°)
 * nav: N9 edge-aligned · footer: Ft5 statement
 * enrichment: real product captures (no illustration, no re-drawn chrome)
 * motion: 1 primitive (hover lift) — design.md § Motion, reveal pattern: none
 * pre-emit critique: P5 H5 E5 S5 R5 V4
 */

@import url("tokens.css");
/* The footer is the one part of this page with a device of its own, so it gets
   its own stylesheet. Imported rather than `<link>`ed, to match `tokens.css`
   above and keep the head to a single stylesheet. The cost is that the request
   is serialized behind this file rather than running beside it; at ~5 KB of
   render-blocking CSS that is the cheaper of the two prices. */
@import url("footer.css");

/* ═══════════════════════════════════════════════════════════════════════════
   Reset + page frame
   ═══════════════════════════════════════════════════════════════════════════ */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  /* `clip`, never `hidden`: `hidden` on a root element silently creates a
     scroll container and breaks `position: sticky` further down the tree. */
  overflow-x: clip;
}

/* `color-scheme` tells the browser which way to paint the things this
   stylesheet does not own — form controls, scrollbars, the canvas behind the
   page. It follows the same three states as the palette: the OS decides until
   the toggle says otherwise. */
html {
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
}
@media (prefers-color-scheme: light) {
  html:not([data-theme="dark"]) {
    color-scheme: light;
  }
}
html[data-theme="light"] {
  color-scheme: light;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: 1.6;
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img,
svg {
  display: block;
  max-width: 100%;
  height: auto;
}

/* Headings are roman throughout — no italic display type anywhere in this
   system. `overflow-wrap: anywhere` lets a long word break inside itself
   rather than pushing the page sideways at 320 px. */
h1,
h2,
h3 {
  margin: 0;
  font-weight: 600;
  font-style: normal;
  line-height: 1.05;
  letter-spacing: -0.025em;
  overflow-wrap: anywhere;
  min-width: 0;
}

p {
  margin: 0;
}

a {
  color: inherit;
}

/* Tabular numerals wherever a number is a measurement — the same rule the app
   applies to every column of times. */
.tnum {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum", "cv05";
}

/* The focus ring is an outline at 2 px offset so it lands on the page rather
   than inside a control's own fill. It is NEVER transitioned — a ring that
   fades in leaves keyboard users with no indicator for the length of the
   transition. */
:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: var(--radius-ctl);
}

.shell {
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}

/* Anything a link or a "find in page" can jump to has to clear the pinned
   dock, or it arrives underneath it. ~5rem is the dock's height plus air. */
#main,
.row,
.spec,
.needs {
  scroll-margin-block-start: 5rem;
}

/* Skip link — visible the moment it takes focus, off-screen otherwise. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  /* Above the sticky dock (z-index 20). A skip link that lands *behind* the
     thing it is meant to skip is worse than no skip link. */
  z-index: 30;
  padding: var(--space-2xs) var(--space-sm);
  background: var(--color-surface);
  border: var(--rule-hair) solid var(--color-border-strong);
  border-radius: var(--radius-ctl);
}
.skip:focus {
  left: var(--page-gutter);
  top: var(--space-2xs);
}

/* Read by a screen reader, invisible to everyone else. Used once — for the
   footer wordmark, whose visible copy is a cropped shape rather than a string
   and is `aria-hidden` for it. `clip-path` rather than a negative offset so
   the text is never a target for a "find in page" scroll. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Instrument voice — the mono micro-label
   The one typographic device this page borrows wholesale from the product.
   Mono, uppercase, tracked, `faint`: the quietest voice in the system.
   ═══════════════════════════════════════════════════════════════════════════ */

.micro {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-faint);
  line-height: 1;
}

/* ═══════════════════════════════════════════════════════════════════════════
   N9 · Edge-aligned nav
   Wordmark hard-left, actions hard-right, and nothing in between. The empty
   middle IS the archetype; filling it with four links makes it N1 with extra
   steps.
   ═══════════════════════════════════════════════════════════════════════════ */

/*
 * The dock: the nav pins to the top of the viewport and carries a scroll
 * readout along its bottom edge.
 *
 * It pins because the theme toggle is the only control on the page now and a
 * control you have to scroll back up to reach is a control you do not use. It
 * is opaque `bg` rather than translucent glass — the page's own paper, so the
 * pinned row is indistinguishable from the unpinned one at the top of the
 * page, and copy never shows through it.
 *
 * The rule along its bottom is a **progress readout**, not a decoration: it
 * fills left to right with how far down the page you are. This is the same
 * instrument voice as the session strip in the product — a bar that reports a
 * measured quantity — which is the only reason a progress indicator belongs on
 * a page in this system at all.
 */
.nav-dock {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--color-bg);
}

.nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-md);
  border-bottom: var(--rule-hair) solid var(--color-border);
}

.nav-dock::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: 2px;
  background: var(--color-primary);
  transform: scaleX(0);
  transform-origin: 0 50%;
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .nav-dock::after {
      animation: scroll-progress linear both;
      /* The document's own scroller, so the bar reports the whole page rather
         than one section of it. */
      animation-timeline: scroll(root block);
    }
  }
}

@keyframes scroll-progress {
  to {
    transform: scaleX(1);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   The page's session strip
   The product's § Session strip at page scale: a row of mono fields, one of
   them filled, reporting the state that matters. In the overlay that state is
   the flag; here it is the section you are reading.
   ═══════════════════════════════════════════════════════════════════════════ */

.readout {
  display: flex;
  align-items: stretch;
  gap: var(--space-md);
  /* Narrow viewports scroll it rather than wrapping it: a strip that becomes
     two strips has stopped being one line of state. Fields drop right-to-left
     in the product; here they run off the same edge and can be dragged back. */
  overflow-x: auto;
  scrollbar-width: none;
  padding-block: var(--space-2xs);
  border-top: var(--rule-hair) solid var(--color-border);
}
.readout::-webkit-scrollbar {
  display: none;
}

.readout__field {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  line-height: 1;
  padding: 0.4rem 0.5rem;
  border-radius: var(--radius-ctl);
  color: var(--color-faint);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-instant) var(--ease-out);
}

.readout__field:hover {
  color: var(--color-text);
}

/* The active field. `primary` is the token for "informational / selection"
   (§ Theme), which is exactly what this is — and filled ink takes `on-accent`,
   never white, per § Theme rule 1. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* Scoped on the root, not on `main`: the sections that *define* these
       timelines live in `main`, but the strip that *references* them is up in
       the dock. The scope has to cover both. */
    :root {
      timeline-scope: --sec-standings, --sec-relative, --sec-fuel,
        --sec-manager, --sec-spec, --sec-needs;
    }

    #standings {
      view-timeline-name: --sec-standings;
    }
    #relative {
      view-timeline-name: --sec-relative;
    }
    #fuel {
      view-timeline-name: --sec-fuel;
    }
    #manager {
      view-timeline-name: --sec-manager;
    }
    #spec {
      view-timeline-name: --sec-spec;
    }
    #needs {
      view-timeline-name: --sec-needs;
    }

    .readout__field {
      animation: readout-active linear both;
      /* Lit while the section owns the middle of its pass through the
         viewport, so exactly one field is filled at a time. */
      animation-range: cover 22% cover 78%;
    }

    .readout__field:nth-child(1) {
      animation-timeline: --sec-standings;
    }
    .readout__field:nth-child(2) {
      animation-timeline: --sec-relative;
    }
    .readout__field:nth-child(3) {
      animation-timeline: --sec-fuel;
    }
    .readout__field:nth-child(4) {
      animation-timeline: --sec-manager;
    }
    .readout__field:nth-child(5) {
      animation-timeline: --sec-spec;
    }
    .readout__field:nth-child(6) {
      animation-timeline: --sec-needs;
    }
  }
}

/* Off at both ends of the range, on through the middle. `fill: both` clamps to
   the nearest endpoint outside the range, so both endpoints must be the *off*
   state or the field stays lit for the rest of the page. */
@keyframes readout-active {
  0% {
    background: transparent;
    color: var(--color-faint);
  }
  1%,
  99% {
    background: var(--color-primary);
    color: var(--color-on-accent);
  }
  100% {
    background: transparent;
    color: var(--color-faint);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Section band
   The product's § Class band at page scale: a chip naming the subject, then
   micro-label/value pairs carrying that subject's own numbers. Every value is
   a fact about the thing in the section — the band is structure that reports,
   not structure that decorates.
   ═══════════════════════════════════════════════════════════════════════════ */

.band {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs) var(--space-md);
  padding-inline-start: var(--space-sm);
  /* The leading edge is the row's left border one scale up — the same device
     at a different size, not a second one. On the site there is no identity
     colour to carry (§ Two colour systems is a product-surface rule), so it
     takes `border-strong`: structure, not status. */
  border-inline-start: 3px solid var(--color-border-strong);
}

.band__chip {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  line-height: 1;
  padding: 0.34rem 0.45rem;
  border-radius: var(--radius-ctl);
  background: var(--color-surface-2);
  color: var(--color-text);
}

.band__field {
  display: inline-flex;
  align-items: baseline;
  gap: 0.45rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  line-height: 1;
}

.band__k {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-faint);
}

.band__v {
  font-weight: 700;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  /* The wordmark is a link, so it carries a link's hit target — 27 px of cap
     height is a fine logo and a poor tap target. */
  min-height: 44px;
  font-weight: 600;
  font-size: 1.0625rem;
  letter-spacing: -0.02em;
  text-decoration: none;
  white-space: nowrap;
}

/* The TL monogram. One shape in one colour: it takes `primary` here because
   the wordmark's own text is white and a white mark beside white text is not a
   mark, it is a ligature. `em` rather than px, so the mark tracks the wordmark
   if its type size ever moves. */
.wordmark__mark {
  flex: none;
  /* 1.45em of box, of which the ink is 62.5 % — so the mark stands about a
     fifth taller than the wordmark's cap height, which is where a monogram
     beside its own name wants to sit. */
  width: 1.45em;
  height: 1.45em;
  fill: var(--color-primary);
  /* The mark's optical centre sits above its box centre — the L's foot is the
     only thing in the bottom fifth — so it needs nudging down to sit on the
     wordmark's midline. */
  margin-block-start: 0.06em;
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Theme toggle
   An icon button, and the only control on the page. It ships the same states
   as the chips — `border-width` never changes, state goes to background and
   colour — and it keeps a 44 px hit target while painting a 34 px box, via
   the inset pseudo-element the app's controls use for the same reason.
   ═══════════════════════════════════════════════════════════════════════════ */

.theme-toggle {
  position: relative;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: var(--rule-hair) solid var(--color-border-strong);
  border-radius: var(--radius-ctl);
  background: transparent;
  color: var(--color-muted);
  cursor: pointer;
  transition:
    background-color var(--dur-short) var(--ease-out),
    border-color var(--dur-short) var(--ease-out),
    color var(--dur-short) var(--ease-out);
}

/* The painted box is 34 px; the thing you can hit is 44. */
.theme-toggle::before {
  content: "";
  position: absolute;
  inset: -5px;
}

.theme-toggle:hover {
  background: var(--color-surface-2);
  border-color: var(--color-faint);
  color: var(--color-text);
}

.theme-toggle:active {
  transform: translateY(1px);
}

.theme-toggle svg {
  width: 17px;
  height: 17px;
}

/*
 * Which glyph shows is a statement about what the button *does*, not about
 * what the page currently is: on dark paper it offers the sun, on light paper
 * the moon. Both are in the markup and CSS picks one, so there is no flash of
 * the wrong icon and no second script to swap them.
 */
.theme-toggle__moon {
  display: none;
}
@media (prefers-color-scheme: light) {
  html:not([data-theme="dark"]) .theme-toggle__sun {
    display: none;
  }
  html:not([data-theme="dark"]) .theme-toggle__moon {
    display: block;
  }
}
html[data-theme="light"] .theme-toggle__sun {
  display: none;
}
html[data-theme="light"] .theme-toggle__moon {
  display: block;
}
html[data-theme="dark"] .theme-toggle__sun {
  display: block;
}
html[data-theme="dark"] .theme-toggle__moon {
  display: none;
}

/* ═══════════════════════════════════════════════════════════════════════════
   C1 · Outlined chip + its filled sibling
   Both ship the full state set. `border-width` never changes between states —
   state goes to background, outline or colour, never to geometry, or the
   control shifts its neighbours by a pixel on hover.
   ═══════════════════════════════════════════════════════════════════════════ */

.chip,
.chip-solid {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  min-height: 44px; /* WCAG AA hit target, at every width */
  padding: 0.6rem 1.05rem;
  border-radius: var(--radius-ctl);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap; /* a two-line button is a broken button */
  cursor: pointer;
  transition:
    background-color var(--dur-short) var(--ease-out),
    border-color var(--dur-short) var(--ease-out),
    transform var(--dur-instant) var(--ease-out);
}

.chip {
  border: var(--rule-hair) solid var(--color-border-strong);
  background: transparent;
  color: var(--color-text);
}
.chip:hover {
  background: var(--color-surface-2);
  border-color: var(--color-faint);
}

.chip-solid {
  border: var(--rule-hair) solid transparent;
  background: var(--color-primary);
  color: var(--color-on-accent);
  font-weight: 600;
}
.chip-solid:hover {
  background: var(--color-primary-dim);
}

.chip:active,
.chip-solid:active {
  transform: translateY(1px);
}

/* A chip that leads with a glyph. `1em` so the icon tracks the label's size,
   and `flex: none` so it never gets squeezed when the row wraps on a phone. */
.chip__icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
}

/* The support chip sits beside the download CTA, so it must not compete with
   it: outlined like any secondary action, and its warmth arrives only on
   hover. `warning` is the closest the palette has to a coffee, and this is the
   one place it is not carrying its § Theme meaning — it is a brand colour on a
   link off-site, not a caution about anything on this page. */
.chip--coffee:hover {
  border-color: color-mix(in oklab, var(--color-warning) 55%, transparent);
  color: var(--color-warning);
  background: color-mix(in oklab, var(--color-warning) 10%, transparent);
}

.chip[aria-disabled="true"],
.chip-solid[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

/* A quiet text link that still reads as an action. The rule under it is drawn
   with a border, so it sits clear of the descenders. */
.link-q {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: var(--space-3xs);
  color: var(--color-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: var(--rule-hair) solid transparent;
  transition: color var(--dur-short) var(--ease-out);
}
.link-q:hover {
  color: var(--color-text);
}

/* A link inside running prose.
   `.link-q` is a standalone target — 44 px tall with its own padding — which
   is right in the footer and wrong mid-sentence: the padding opens a gap
   before the next comma and the min-height inflates the line box around it.
   This one takes the paragraph's own metrics and carries an underline, so it
   is identifiable as a link without relying on colour alone. */
.link-inline {
  color: var(--color-text);
  text-decoration: underline;
  text-decoration-color: var(--color-border-strong);
  text-underline-offset: 0.2em;
  transition: text-decoration-color var(--dur-short) var(--ease-out);
}
.link-inline:hover {
  text-decoration-color: currentColor;
}

/* ═══════════════════════════════════════════════════════════════════════════
   H2 · Split diptych hero
   ═══════════════════════════════════════════════════════════════════════════ */

.hero {
  display: grid;
  /* 6/6 rather than 7/5. `minmax(0, …)` on both tracks: a bare `1fr` refuses to
     shrink below its content's intrinsic width, which is how a wide screenshot
     pushes a grid past the viewport.

     The even split is not a style preference — the capture is a timing table,
     and below about 600 px its 12 px type stops being legible. A hero image you
     cannot read is decoration, and this system does not use decoration. */
  grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
  gap: var(--space-2xl);
  align-items: center;
  padding-block: var(--space-3xl) var(--space-2xl);
}

.hero__copy {
  display: grid;
  gap: var(--space-md);
  justify-items: start;
  /* The leading edge, one more time. The product quarantines identity colour
     to a 3 px left border; the site has no identity to carry, so the same
     geometry carries structure instead — hero, band and section all hang off
     one vertical. */
  padding-inline-start: var(--space-sm);
  border-inline-start: 3px solid var(--color-border-strong);
}

.band--hero {
  /* Already inside the hero's own leading edge — a second rule here would be
     the device stacked on itself. */
  border-inline-start: 0;
  padding-inline-start: 0;
  margin-block-end: calc(var(--space-md) * -1 + var(--space-2xs));
}

.hero h1 {
  font-size: var(--text-display);
  max-width: 17ch; /* two lines, no widow */
  letter-spacing: -0.035em;
  font-weight: 700;
}

.hero__lede {
  font-size: var(--text-lede);
  color: var(--color-muted);
  max-width: 46ch;
  text-wrap: pretty;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin-block-start: var(--space-2xs);
}

.hero__note {
  color: var(--color-faint);
  font-size: var(--text-sm);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Product captures
   A real screenshot in a <figure> with a hairline border. No drawn browser
   bar, no traffic-light dots, no phone frame — the reader's own window already
   supplies real chrome, and a fake one only ever reads as fake.
   ═══════════════════════════════════════════════════════════════════════════ */

.shot {
  margin: 0;
  border: var(--rule-hair) solid var(--color-border);
  border-radius: var(--radius-panel);
  background: var(--color-timing-bg);
  padding: var(--space-3xs);
  overflow: hidden;
  box-shadow: var(--shadow-raise);
}

.shot img {
  width: 100%;
  border-radius: calc(var(--radius-panel) - 5px);
}

.shot figcaption {
  padding: var(--space-2xs) var(--space-3xs) var(--space-3xs);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Split Studio rows — the page's spine
   Text on one side, its proof on the other; the pairing alternates direction
   down the page. The alternation is done with `direction`-free explicit column
   placement so the DOM order stays reading order for screen readers.
   ═══════════════════════════════════════════════════════════════════════════ */

.row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  /* Row 1 is the band, row 2 the diptych. Declared rather than implicit
     because the flipped rows place their children by explicit `grid-row`, and
     an auto-placed band would land in the same cell as the copy. */
  grid-template-rows: auto auto;
  gap: var(--space-2xl);
  align-items: center;
  padding-block: var(--space-xl) var(--space-2xl);
  border-top: var(--rule-hair) solid var(--color-border);
}

/* The band is a heading device, so it sits apart from its section rather than
   on top of it — the same `CLASS_BAND_H` + `BAND_GAP` reasoning the product
   uses, at page scale. */
.row > .band {
  grid-column: 1 / -1;
  grid-row: 1;
  /* The grid gap is sized for the diptych; the band wants a heading's air
     under it, not a column's. */
  margin-block-end: calc(var(--space-2xl) * -1 + var(--space-xl));
}

/* Everything after the band auto-places from row 2 onward — two columns in a
   diptych row, stacked in `--wide`. Only the flipped rows place explicitly. */

.row__copy {
  display: grid;
  gap: var(--space-sm);
  align-content: start;
}

.row h2 {
  font-size: var(--text-h2);
  max-width: 16ch;
}

.row__body {
  color: var(--color-muted);
  max-width: 52ch;
  text-wrap: pretty;
}

/* Alternate rows put the proof on the left. Grid placement, not `order` on a
   flex container, so the tab order and the reading order stay identical. */
.row--flip {
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
}
.row--flip > .row__copy {
  grid-column: 2;
  grid-row: 2;
}
.row--flip > .shot {
  grid-column: 1;
  grid-row: 2;
}

/* One row breaks the diptych: the full standings table.
 *
 * Split Studio pairs each claim with a proof at half width, which works for
 * every capture here except this one — the wide table carries twelve columns of
 * 12 px type, and at half width that renders at around 5 px. The rule the
 * archetype is really enforcing is "every claim gets its proof"; a proof too
 * small to read fails that rule while satisfying its letter. So this row stacks:
 * copy at reading measure, then the capture at the full width of the shell.
 *
 * Used exactly once. A second full-width row and the page has stopped being a
 * diptych at all.
 */
.row--wide {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-lg);
}
.row--wide .row__copy {
  max-width: 62ch;
}

/* A short list of what the capture beside it actually shows. Every item is
   verifiable in the image — the list is a caption, not a claim. */
.reads {
  display: grid;
  gap: var(--space-2xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

.reads li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-2xs);
  align-items: baseline;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.reads li::before {
  content: "";
  width: 10px;
  height: 2px;
  border-radius: 1px;
  background: var(--color-border-strong);
  transform: translateY(-0.28em);
}

/* ═══════════════════════════════════════════════════════════════════════════
   F3 · Tabular spec sheet
   The page's signature move: the site becomes the instrument it is selling.
   Hairline rules between rows, mono keys, tabular figures, no vertical rules.
   ═══════════════════════════════════════════════════════════════════════════ */

.spec {
  padding-block: var(--space-xl) var(--space-2xl);
  border-top: var(--rule-hair) solid var(--color-border);
  display: grid;
  gap: var(--space-lg);
}

.spec__intro {
  max-width: 54ch;
  display: grid;
  gap: var(--space-sm);
}

.spec__intro h2 {
  font-size: var(--text-h2);
}

.spec__intro p {
  color: var(--color-muted);
}

/* A table caption is centred and inherits the table's alignment by default;
   here it is a micro-label naming the readout, so it aligns with the first
   column like every other label on the page. */
.spec__cap {
  text-align: left;
  padding-block-end: var(--space-xs);
}

.spec__scroll--stacked {
  margin-block-start: var(--space-xl);
}

.spec__scroll {
  overflow-x: auto;
  /* The table is the one element allowed to scroll sideways — inside its own
     container, never taking the page with it. */
  -webkit-overflow-scrolling: touch;
}

.spec table {
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  text-align: left;
}

.spec thead th {
  padding: 0 var(--space-sm) var(--space-2xs) 0;
  border-bottom: var(--rule-hair) solid var(--color-border-strong);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-faint);
}

.spec tbody th {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--color-text);
  white-space: nowrap;
  padding-right: var(--space-md);
}

.spec tbody td {
  color: var(--color-muted);
  font-size: var(--text-sm);
  padding-right: var(--space-sm);
}

.spec tbody th,
.spec tbody td {
  padding-block: var(--space-xs);
  border-bottom: var(--rule-hair) solid var(--color-border);
  vertical-align: baseline;
}

.spec tbody tr:last-child th,
.spec tbody tr:last-child td {
  border-bottom: 0;
}

/* Status swatches inside the table, carrying the app's own colour meanings.
   Coloured ink, never a filled cell — the fill budget belongs to the product. */
.sw {
  font-family: var(--font-mono);
  font-weight: 700;
  white-space: nowrap;
}
.sw--pb {
  color: var(--color-accent);
}
.sw--best {
  color: var(--color-sector-purple);
}
.sw--warn {
  color: var(--color-warning);
}
.sw--bad {
  color: var(--color-danger);
}

/* S5 · Bottom-anchored section label — the table is the act, the label is its
   footer. Inverts the usual hierarchy and keeps the page clear of the
   tag-above-heading pattern that reads as templated editorial. */
.spec__end {
  margin-block-start: var(--space-md);
  padding-block-start: var(--space-xs);
  border-top: var(--rule-hair) solid var(--color-border);
}

/* ═══════════════════════════════════════════════════════════════════════════
   Requirements
   ═══════════════════════════════════════════════════════════════════════════ */

/* Heading above its content, never beside it. A heading parked in a left
   column with the body in a right one is the hanging-header pattern, and it is
   the single most reliable templated-editorial tell — banned outright here even
   though the content would technically fit. */
.needs {
  padding-block: var(--space-xl) var(--space-2xl);
  border-top: var(--rule-hair) solid var(--color-border);
  display: grid;
  gap: var(--space-lg);
}

.needs h2 {
  font-size: var(--text-h2);
}

.needs__list {
  display: grid;
  /* `auto-fit`, not a fixed 3: the list gained a fourth entry and a hard
     column count would have parked it alone on a second row with two empty
     cells beside it. */
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-lg);
  margin: 0;
}

.needs__list > div {
  display: grid;
  gap: var(--space-3xs);
  /* Without this the two auto rows stretch to the tallest column in the row,
     which pushes each `dd` down by half the difference and leaves the short
     entries floating clear of their own headings. */
  align-content: start;
}

.needs__list dt {
  font-weight: 600;
}

.needs__list dd {
  margin: 0;
  color: var(--color-muted);
  max-width: 46ch;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Closing CTA — one button, not two. The repetition is the call to action.
   ═══════════════════════════════════════════════════════════════════════════ */

.close {
  padding-block: var(--space-2xl);
  border-top: var(--rule-hair) solid var(--color-border);
  display: grid;
  gap: var(--space-md);
  justify-items: start;
}

.close h2 {
  font-size: var(--text-h2);
  max-width: 18ch;
}

.close p {
  color: var(--color-muted);
  max-width: 50ch;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Responsive — verified at 320 / 375 / 414 / 768
   Below 60rem every two-column block becomes one column; below 40rem the page
   is a phone and space is a luxury.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 60rem) {
  .hero,
  .row,
  .row--flip,
  .needs__list {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-lg);
  }

  .hero {
    padding-block: var(--space-2xl) var(--space-lg);
  }

  /* The flipped rows lose their flip: on one column, DOM order wins and the
     copy always leads. */
  .row--flip .row__copy,
  .row--flip .shot {
    grid-column: 1;
    grid-row: auto;
  }

  .row {
    padding-block: var(--space-xl);
  }
}

@media (max-width: 40rem) {
  /* The nav used to wrap here, because two actions plus the wordmark would not
     fit a phone without one of them shrinking below its hit target. There is
     one action now, and the pair fits a 320 px screen with room to spare — so
     it stays a single edge-aligned row, which is what N9 is. */

  .hero__actions .chip,
  .hero__actions .chip-solid {
    flex: 1 1 100%;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Scroll reveal — opacity only
   The one place this system animates on scroll, and it is fenced tightly.

   · **Opacity and nothing else.** No rise, no scale, no stagger. A section
     arrives at full strength by the time it is a third of the way up the
     viewport; the effect is the arrival, not a performance.
   · **Zero JavaScript.** `animation-timeline: view()` drives it from the
     scroller itself, so the page keeps having no script at all. Under
     `@supports`, because the fallback has to be "visible", never "stuck at
     zero" — a browser without scroll-driven animations shows the page exactly
     as it always did.
   · **Never the hero, never the nav.** Anything above the fold at load must be
     readable at load. The range ends inside `entry`, so an element already on
     screen when the page opens is past its own animation and renders at 1.
   · **Off under reduced motion**, via the guard below rather than by relying
     on the blanket duration override — a scroll-driven animation has no
     duration to collapse.
   ═══════════════════════════════════════════════════════════════════════════ */

@keyframes reveal-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/*
 * The second device: the captures drift.
 *
 * Each product shot travels about 26 px against the page over its whole pass
 * through the viewport, which reads as the image sitting slightly *behind* the
 * copy beside it. It is transform-only, it is tiny, and it is deliberately
 * applied to the `<figure>` rather than to the `<img>` inside it — moving the
 * image within its own frame would look like a bug, moving the framed picture
 * against the paragraph next to it looks like depth.
 *
 * Amplitude is the whole argument here. At 26 px it is felt rather than
 * watched; at the 80–120 px a parallax library would default to, a screenshot
 * whose entire job is to be *read* would be sliding while you read it.
 */
@keyframes reveal-drift {
  from {
    transform: translateY(13px);
  }
  to {
    transform: translateY(-13px);
  }
}

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .row,
    .spec,
    .needs,
    .foot__meta {
      animation: reveal-fade linear both;
      animation-timeline: view();
      /* Start as the top edge clears the fold, finish well before the section
         is centred — the copy is readable long before it stops moving. */
      animation-range: entry 5% entry 55%;
    }

    /* The hero's own capture is exempt: it is on screen at load, and a hero
       that arrives already mid-animation reads as a page still loading. */
    .row .shot {
      animation: reveal-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Reduced motion
   Spatial movement collapses and the two short durations go to zero. The
   scroll reveal above is not disabled here — it is never declared in the
   first place, because a scroll-driven animation has no duration for this
   block to shorten.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .chip:active,
  .chip-solid:active {
    transform: none;
  }
}
