/*
 * Bittner AI — intake automation for solo and small law firms.
 *
 * One stylesheet, no external assets, no web fonts. The reader is a managing
 * partner between hearings, often on a phone, and the page has to look like a
 * document rather than a pitch deck.
 *
 * Palette and type carried over from the business plan: paper ground, one
 * muted green, a serif for display and a system sans for reading. Newsreader
 * and IBM Plex are the intended faces; the stacks below fall back to Georgia
 * and system-ui, which is what the plan's own CSS does. Loading them from a
 * font host would be the only external request on the site, so we don't.
 */

/* ---------- display face ----------
 * Newsreader, self-hosted so the site still makes zero external requests.
 * Variable, subset to latin + the punctuation this site actually sets, and
 * the axes clipped to the range in use (wght 500-650, opsz 18-56) — 58KB
 * rather than the 132KB Google serves.
 *
 * The second @font-face is not a font: it is Georgia re-declared with
 * Newsreader's metrics, so the fallback occupies the same space and swapping
 * costs no layout shift.
 */
@font-face {
  font-family: Newsreader;
  src: url("fonts/newsreader-var.9ec3e78c.woff2") format("woff2-variations");
  font-weight: 500 650;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Newsreader Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Times");
  size-adjust: 88.48%;
  ascent-override: 83.07%;
  descent-override: 29.95%;
  line-gap-override: 0%;
}

:root {
  color-scheme: light dark;

  --paper:      #F6F8F7;
  --surface:    #FFFFFF;
  --surface-2:  #EDF1EF;
  --ink:        #121D1A;
  --ink-2:      #4E5B57;
  --ink-3:      #616C67;
  --rule:       #B9C4BF;
  --rule-soft:  #DCE3E0;
  --field:      #7A8984;
  --accent:     #0E5A4E;
  --accent-ink: #0B453C;
  --accent-soft:#E0EDE8;
  --on-accent:  #FFFFFF;
  --warn:       #8A5C13;
  --warn-soft:  #F5EBD9;

  --f-display: Newsreader, "Newsreader Fallback", Georgia, "Times New Roman", serif;
  --f-body:    system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --f-mono:    ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --wrap: 72rem;
  --measure: 68ch;
  --pad: 1.5rem;
  --radius: 4px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:      #0D1412;
    --surface:    #131C19;
    --surface-2:  #1A2521;
    --ink:        #E7EDEA;
    --ink-2:      #A0AEA9;
    --ink-3:      #8A9893;
    --rule:       #354742;
    --rule-soft:  #232F2B;
    --field:      #5E726B;
    --accent:     #52C0A8;
    --accent-ink: #7FD4C0;
    --accent-soft:#15302A;
    --on-accent:  #08211C;
    --warn:       #D9A64A;
    --warn-soft:  #2C2416;
  }
}

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (min-width: 48rem) { html { scroll-padding-top: 5rem; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--f-body);
  font-size: 1.0625rem;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------- type ---------- */

h1, h2, h3 {
  font-family: var(--f-display);
  font-weight: 500;
  margin: 0 0 0.5em;
  text-wrap: balance;
}

/* Display type is the loudest element on a page with no photography, so it
   carries the weight instead: larger, tighter tracking, tighter leading. */
h1 {
  font-size: clamp(2.5rem, 7vw, 4.5rem);
  line-height: 0.99;
  letter-spacing: -0.032em;
}
h2 {
  font-size: clamp(1.85rem, 4.4vw, 2.9rem);
  line-height: 1.06;
  letter-spacing: -0.026em;
}
h3 {
  font-size: 1.22rem;
  font-weight: 600;
  line-height: 1.28;
  letter-spacing: -0.008em;
}

.side-head {
  font-family: var(--f-body);
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.08em;
  line-height: 1.35;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 0.7rem;
}

p, ul, ol { margin: 0 0 1.15rem; }

a { color: var(--accent); text-underline-offset: 2px; }
a:hover { color: var(--accent-ink); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }

strong { font-weight: 650; }

code, .mono { font-family: var(--f-mono); font-size: 0.92em; }

.measure { max-width: var(--measure); }
.lede {
  font-size: clamp(1.14rem, 2.1vw, 1.4rem);
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 52ch;
  letter-spacing: -0.008em;
}
.lead-in { color: var(--ink-2); }
.fine  { font-size: 0.9rem; color: var(--ink-3); }
.mb0 { margin-bottom: 0; }

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad);
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  padding: 0.7rem 1rem;
  z-index: 100;
}
.skip:focus { left: 0; }

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

.site-head {
  border-bottom: 1px solid var(--rule);
  background: var(--surface);
  /* Positioned for the accent rule drawn by ::after; the media query below
     promotes this to sticky. Do not re-declare position later in the file —
     a later declaration silently wins and the header quietly stops sticking. */
  position: relative;
}

/* Sticky everywhere the bar is one row tall. On a phone that is only true once
   the links are behind the toggle — .has-toggle is set by the script, so a
   no-JS visitor keeps the taller, non-sticky header rather than losing a third
   of the screen to a pinned one. */
@media (min-width: 48rem) {
  .site-head { position: sticky; top: 0; z-index: 50; }
}
@media (max-width: 47.99rem) {
  .site-head.has-toggle { position: sticky; top: 0; z-index: 50; }
}
@supports (backdrop-filter: blur(1px)) {
  @media (min-width: 48rem) {
    .site-head {
      background: color-mix(in srgb, var(--surface) 88%, transparent);
      backdrop-filter: saturate(1.4) blur(12px);
    }
  }
  @media (max-width: 47.99rem) {
    .site-head.has-toggle {
      background: color-mix(in srgb, var(--surface) 92%, transparent);
      backdrop-filter: saturate(1.4) blur(12px);
    }
  }
}
.site-head .wrap {
  display: flex;
  align-items: center;
  gap: 0.6rem 2.25rem;
  flex-wrap: wrap;
  padding-block: 0.95rem;
}

.brand {
  font-family: var(--f-display);
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink);
  text-decoration: none;
  margin-right: auto;
  /* The home link is the most-tapped thing in the masthead on a phone, and
     at a 1.3rem line-height it was a 34px target — inside WCAG 2.5.8's 24px
     AA floor, but under the 44px that 2.5.5 and every platform guideline
     ask for, which is the figure the rest of this site's controls are built
     to. inline-flex rather than a padding bump so the box grows around the
     word without moving it, and margin-right:auto keeps working. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
.brand:hover { color: var(--ink); }
.brand em { font-style: normal; color: var(--accent); }

.nav {
  display: flex;
  align-items: center;
  gap: 0.3rem 1.75rem;
  flex-wrap: wrap;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.nav a {
  color: var(--ink-2);
  text-decoration: none;
  padding-block: 0.55rem;
  border-bottom: 1px solid transparent;
}
.nav a:hover { color: var(--ink); border-bottom-color: var(--rule); }
.nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }

/* ---------- buttons ---------- */

.btn {
  display: inline-block;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  font-size: 1rem;
  line-height: 1.3;
  padding: 0.95rem 1.85rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  text-decoration: none;
  min-height: 50px;
  letter-spacing: -0.005em;
}
.btn:hover, .btn:focus-visible {
  background: var(--accent-ink);
  border-color: var(--accent-ink);
  color: var(--on-accent);
}

.btn-quiet {
  background: transparent;
  color: var(--ink);
  border-color: var(--field);
}
.btn-quiet:hover, .btn-quiet:focus-visible {
  background: var(--surface-2);
  border-color: var(--ink-3);
  color: var(--ink);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  align-items: center;
  margin: 2.5rem 0 0.6rem;
}
.cta-note { font-size: 0.9rem; color: var(--ink-3); margin: 0.35rem 0 0; }

/* ---------- sections ---------- */

section { padding-block: clamp(4rem, 9vw, 8rem); }
.hero {
  padding-block: clamp(4.5rem, 11vw, 9rem);
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
}
.alt { background: var(--surface); border-block: 1px solid var(--rule); }

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

/* The hero carries two things: the claim, and the demo that backs it. Stacked,
   the demo sat below the fold on a laptop while half the viewport stayed empty,
   so the one piece of evidence on the page read as a footnote to the headline.
   Side by side, the claim and its proof are visible at once.

   The split only happens once there is room for BOTH a readable measure and a
   usable textarea. Below that the demo returns under the copy, which is the
   right order: read the claim, then try it. */

.hero-grid { display: grid; gap: clamp(2.5rem, 5vw, 3.5rem); }

@media (min-width: 64rem) {
  .hero-grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    gap: clamp(3rem, 5vw, 5rem);
  }
  /* The global h1 scales off the viewport and tops out at 4.5rem, which is
     right across a full-width measure and far too large in a half-width
     column — about seven characters a line, and the headline runs to four.
     Size it to the column instead. */
  .hero-copy h1 {
    font-size: clamp(2.5rem, 4.2vw, 3.4rem);
    max-width: 20ch;
  }
  .hero-copy .lede { max-width: 44ch; }
  /* Optical alignment — the demo's label sits level with the lede rather than
     with the top of a display-size headline. */
  .hero-demo { padding-top: 0.35rem; }
}

/* The demo is an instrument, so it gets an edge and a surface of its own
   instead of floating on the hero wash. */
.hero-demo .demo {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 10px;
  padding: clamp(1.25rem, 3vw, 1.75rem);
}
.hero-demo .demo-foot { margin-bottom: 0; }
.hero-demo [data-demo-noscript] { margin-bottom: 0; }

@media (max-width: 63.99rem) {
  .hero-demo { margin-top: 0.5rem; }
}

.eyebrow {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 1.5rem;
}

/* Numbered section marker, like the plan's own contents. */
.marker {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
  display: block;
  margin-bottom: 1.35rem;
  padding-bottom: 1.15rem;
  border-bottom: 1px solid var(--rule);
}

/* ---------- grid + cards ---------- */

.grid {
  display: grid;
  gap: clamp(2rem, 4vw, 3.25rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr)); }

/* No panel, no fill, no shadow. A single rule across the top sets the column
   and whitespace does the rest — the page reads as one surface rather than a
   tray of boxes. */
.card {
  border-top: 2px solid var(--ink);
  padding: 1.15rem 0 0;
}
.card h3 { margin-bottom: 0.5rem; }
.card p { color: var(--ink-2); }
.card p:last-child { margin-bottom: 0; }

/* ---------- the chain diagram ---------- */

.chain {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: step;
  border-top: 1px solid var(--rule);
}
.chain > li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 3.25rem 1fr;
  gap: 0 1.5rem;
  padding: 2rem 0;
  border-bottom: 1px solid var(--rule);
}
.chain > li::before {
  content: counter(step, decimal-leading-zero);
  font-family: var(--f-mono);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  color: var(--accent);
  padding-top: 0.4rem;
}
.chain h3 { margin-bottom: 0.3rem; }
.chain p { margin-bottom: 0; color: var(--ink-2); }
.chain .tool {
  font-family: var(--f-mono);
  font-size: 0.76rem;
  color: var(--ink-3);
  display: block;
  margin-top: 0.4rem;
}

/* ---------- lists ---------- */

.ticks, .nots { list-style: none; padding: 0; margin: 0 0 1.15rem; }
.ticks li, .nots li {
  position: relative;
  padding-left: 1.6rem;
  margin-bottom: 0.6rem;
  color: var(--ink-2);
}
.ticks li strong, .nots li strong { color: var(--ink); }
.ticks li::before {
  content: "";
  position: absolute;
  left: 0.15rem;
  top: 0.52em;
  width: 0.38rem;
  height: 0.72rem;
  border: solid var(--accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) translateY(-50%);
}
.nots li::before {
  content: "\00D7";
  position: absolute;
  left: 0.2rem;
  top: -0.05em;
  color: var(--ink-3);
  font-size: 1.05rem;
}

/* ---------- callouts ---------- */

.note {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  padding: 1.15rem 1.35rem;
  border-radius: 0 var(--radius) var(--radius) 0;
  margin: 1.75rem 0;
}
.note p:last-child { margin-bottom: 0; }
.note .side-head { color: var(--accent-ink); }

.note-warn { border-left-color: var(--warn); background: var(--warn-soft); }
.note-warn .side-head { color: var(--warn); }

/* ---------- tier panels ---------- */

.tier {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface);
  padding: clamp(1.4rem, 3.5vw, 2rem);
  margin-bottom: 1.25rem;
}
.tier-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  border-bottom: 1px solid var(--rule-soft);
  padding-bottom: 0.9rem;
  margin-bottom: 1.15rem;
}
.tier-head h3 { margin: 0; font-size: 1.35rem; }
.status {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  padding: 0.25rem 0.6rem;
  border-radius: 99px;
  border: 1px solid var(--accent);
  color: var(--accent-ink);
  background: var(--accent-soft);
  white-space: nowrap;
}
.status-soon {
  border-color: var(--warn);
  color: var(--warn);
  background: var(--warn-soft);
}

/* ---------- definition table ---------- */

.facts {
  display: grid;
  grid-template-columns: minmax(8rem, auto) 1fr;
  gap: 0.55rem 1.5rem;
  margin: 0 0 1.15rem;
  font-size: 0.97rem;
}
.facts dt { font-weight: 600; color: var(--ink-3); }
.facts dd {
  margin: 0;
  color: var(--ink-2);
  /* Grid items default to min-width:auto, so an unbreakable string such as an
     email address would push the track — and the page — past the viewport. */
  min-width: 0;
  overflow-wrap: break-word;
}

/* A 128px label column is not worth having on a phone. */
@media (max-width: 30rem) {
  .facts { grid-template-columns: 1fr; gap: 0.15rem; }
  .facts dd { margin-bottom: 0.55rem; }
  .facts dd:last-child { margin-bottom: 0; }
}

/* ---------- FAQ ---------- */

details { border-bottom: 1px solid var(--rule); padding: 0.9rem 0; }
details summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 1.05rem;
  list-style: none;
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  min-height: 44px;
  padding-block: 0.3rem;
}
details summary::-webkit-details-marker { display: none; }
details summary::before {
  content: "+";
  font-family: var(--f-mono);
  color: var(--accent);
  flex: none;
  width: 0.8rem;
}
details[open] summary::before { content: "\2212"; }
details p { margin: 0.6rem 0 0 1.55rem; color: var(--ink-2); }

/* ---------- closing band ---------- */

.band { background: var(--surface); border-top: 1px solid var(--rule); }

/* ---------- contact form ---------- */

.contact-form { max-width: 34rem; margin-top: 1.75rem; }
.contact-form .field { margin-bottom: 1rem; }
.contact-form label {
  display: block;
  font-weight: 600;
  font-size: 0.95rem;
  margin-bottom: 0.35rem;
}
.contact-form input,
.contact-form textarea {
  width: 100%;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--field);
  border-radius: var(--radius);
  padding: 0.7rem 0.8rem;
  min-height: 46px;
}
.contact-form input::placeholder,
.contact-form textarea::placeholder { color: var(--ink-3); }
.contact-form textarea { resize: vertical; }
.contact-form input:focus-visible,
.contact-form textarea:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.contact-form .field-row {
  display: grid;
  gap: 0 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
}
.contact-form button { margin-top: 0.35rem; cursor: pointer; }

/* Honeypot: off-screen rather than display:none, which bots detect. */
.contact-form .hp {
  position: absolute;
  left: -5000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------- footer ---------- */

.site-foot {
  border-top: 1px solid var(--rule);
  background: var(--surface);
  padding-block: 2.5rem;
  font-size: 0.93rem;
  color: var(--ink-2);
}
.site-foot .cols {
  display: grid;
  gap: 1.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}
.site-foot ul { list-style: none; padding: 0; margin: 0; }
.site-foot li { margin-bottom: 0.4rem; }
.site-foot a { color: var(--ink-2); text-decoration: none; }
.site-foot a:hover { color: var(--accent); text-decoration: underline; }
.colophon {
  margin: 2rem 0 0;
  padding-top: 1.35rem;
  border-top: 1px solid var(--rule-soft);
  color: var(--ink-3);
  font-size: 0.85rem;
}

.spaced { margin-top: 2.75rem; }

/* ==========================================================================
   Motion and depth
   --------------------------------------------------------------------------
   Everything below is progressive enhancement. No JavaScript, no library.
   Two rules hold throughout:

   1. Nothing here may hide content. The scroll reveals live inside an
      @supports block, so a browser without view() timelines simply renders
      the page — it never gets an opacity:0 element it cannot animate back.
   2. Every transition and animation is off under prefers-reduced-motion.
   ========================================================================== */

/* ---------- ambient accent wash ---------- */

/* One soft field of the brand green behind the hero. No new colour is
   introduced — it is --accent at low alpha, so it follows the theme. */
.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.hero::before {
  content: "";
  position: absolute;
  inset: -40% -15% auto -15%;
  height: 170%;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(58% 52% at 20% 8%,
      color-mix(in srgb, var(--accent) 13%, transparent), transparent 70%),
    radial-gradient(42% 44% at 92% 2%,
      color-mix(in srgb, var(--accent) 8%, transparent), transparent 72%);
}

/* A hairline of accent under the masthead, so the header reads as a rule
   rather than a border. */
.site-head::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg,
    var(--accent), color-mix(in srgb, var(--accent) 22%, transparent) 38%, transparent 72%);
}

/* ---------- micro-interactions ---------- */

.card, .tier {
  transition: border-color 180ms ease, box-shadow 180ms ease;
}

@media (hover: hover) {
  .card:hover { border-top-color: var(--accent); }
  .tier:hover {
    border-color: color-mix(in srgb, var(--accent) 42%, var(--rule));
    box-shadow: 0 14px 30px -18px color-mix(in srgb, var(--accent) 45%, transparent);
  }
}

.btn {
  transition:
    background-color 160ms ease,
    border-color     160ms ease,
    box-shadow       160ms ease,
    transform        110ms ease;
}
.btn:hover { box-shadow: 0 8px 20px -12px color-mix(in srgb, var(--accent) 70%, transparent); }
.btn:active { transform: translateY(1px); }

/* Chain rows: the step number picks up weight as you read down it. */
.chain > li { transition: background-color 160ms ease; }
.chain > li::before { transition: color 160ms ease, transform 160ms ease; }
@media (hover: hover) {
  .chain > li:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
  .chain > li:hover::before { color: var(--accent-ink); transform: translateX(2px); }
}

/* Nav: the underline grows from the left rather than blinking on. */
.nav a { position: relative; }
.nav a,
.nav a:hover,
.nav a[aria-current="page"] { border-bottom-color: transparent; }
.nav a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  height: 1px;
  width: 100%;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 180ms ease;
}
.nav a:hover::after,
.nav a[aria-current="page"]::after { transform: scaleX(1); }

.status { transition: border-color 160ms ease, background-color 160ms ease; }

.contact-form input,
.contact-form textarea { transition: border-color 140ms ease, box-shadow 140ms ease; }
.contact-form input:hover,
.contact-form textarea:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--field)); }

/* ---------- scroll reveals ---------- */

/* Scroll-driven animations, CSS only. Wrapped in @supports so that a browser
   without view() timelines never sees the opacity:0 start state. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal,
    .card,
    .tier,
    .spec,
    .chain > li {
      animation: reveal-in linear both;
      animation-timeline: view();
      /* The range must END somewhere every element can actually reach. A range
         ending in the `cover` phase is not reachable for anything near the foot
         of the page — there is no scroll left to complete it — and the element
         stays stranded at opacity 0. `entry` completes as soon as the element
         is fully in the viewport, which is reachable for all of them. */
      animation-range: entry 0% entry 55%;
    }
    @keyframes reveal-in {
      from { opacity: 0; transform: translateY(16px); }
      to   { opacity: 1; transform: none; }
    }
  }
}

/* ---------- scroll motion: travel ---------- */

/* The reveals above only fade things in. This block gives the page a sense of
   travel: the masthead rule fills as you read, the hero wash drifts as it
   leaves, section intros arrive in sequence rather than as one block, grids
   cascade, and the step you are reading in a chain is marked.

   Three things to know before editing any of it.

   1. `view()` resolves against the element's nearest SCROLLPORT, and
      `overflow: hidden` makes an element a scrollport. .hero is
      overflow:hidden, so a `view()` animation on anything inside it would
      resolve against a box that never scrolls and freeze at its start — which
      for a fade means a permanently invisible element. Anything inside .hero
      is therefore driven from `scroll(root block)` instead, which names the
      document scroller explicitly and does not care where the element lives.
   2. A scroll-driven animation with `both` fill holds its end state forever
      after the range, so an effect that must come back down (the reading
      marker) is written as 0% → 50% → 100% with the two ends identical,
      never as a one-way from/to.
   3. `animation-delay` is a TIME and does nothing on a scroll timeline. The
      stagger below is done by shifting `animation-range`, which is the only
      thing that works.

   test/motion.mjs measures every animated element at several scroll positions
   and fails if any of them is left stranded under full opacity. That test is
   the reason this block can be edited with any confidence at all. */

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {

    /* The hairline under the masthead doubles as a read-progress bar. ::after
       already draws the static rule; this is a second, denser line over it. */
    .site-head::before {
      content: "";
      position: absolute;
      left: 0; right: 0; bottom: -1px;
      height: 2px;
      z-index: 1;
      pointer-events: none;
      background: linear-gradient(90deg, var(--accent), var(--accent-ink));
      transform: scaleX(0);
      transform-origin: left center;
      animation: head-progress linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes head-progress {
      from { transform: scaleX(0); }
      to   { transform: scaleX(1); }
    }

    /* The wash behind the hero lags the content, so the hero gains depth as it
       leaves. Driven from the root scroller — see note 1 above. */
    .hero::before {
      animation: wash-drift linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 110vh;
    }
    @keyframes wash-drift {
      from { transform: translate3d(0, 0, 0); opacity: 1; }
      to   { transform: translate3d(0, -14%, 0); opacity: 0.35; }
    }
  }
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* Section intros: marker, then heading, then the lead-in paragraph. Same
       keyframes as the base reveal, three offset ranges. */
    section > .wrap > .marker,
    section > .wrap > h2,
    section > .wrap > .lead-in,
    .rail-band .rail > li {
      animation: reveal-in linear both;
      animation-timeline: view();
    }
    section > .wrap > .marker  { animation-range: entry 2%  entry 42%; }
    section > .wrap > h2       { animation-range: entry 6%  entry 50%; }
    section > .wrap > .lead-in { animation-range: entry 10% entry 58%; }

    /* Cascades. A grid that arrives all at once reads as a page repaint; the
       same grid arriving across a third of its entry reads as motion. Capped
       at six because past that the last card is still waiting when the reader
       has moved on. */
    .grid > .card:nth-child(1),
    .rail > li:nth-child(6n + 1) { animation-range: entry 0%  entry 50%; }
    .grid > .card:nth-child(2),
    .rail > li:nth-child(6n + 2) { animation-range: entry 5%  entry 55%; }
    .grid > .card:nth-child(3),
    .rail > li:nth-child(6n + 3) { animation-range: entry 10% entry 60%; }
    .grid > .card:nth-child(4),
    .rail > li:nth-child(6n + 4) { animation-range: entry 15% entry 65%; }
    .grid > .card:nth-child(5),
    .rail > li:nth-child(6n + 5) { animation-range: entry 20% entry 70%; }
    .grid > .card:nth-child(n + 6),
    .rail > li:nth-child(6n)     { animation-range: entry 25% entry 75%; }

    /* Specs rise further than cards, because they sit in wider bands where a
       16px lift is invisible. */
    .spec {
      animation-range: entry 0% entry 60%;
    }

    /* The step you are reading takes an accent edge, and loses it again on the
       way out. ::after is used because ::before is the step number and the li
       itself carries a hover background — animating either would hold its end
       state and kill the hover. */
    .chain > li { position: relative; }
    .chain > li::after {
      content: "";
      position: absolute;
      left: -0.9rem;
      top: 1.75rem;
      bottom: 1.75rem;
      width: 2px;
      background: var(--accent);
      transform: scaleY(0);
      transform-origin: center;
      animation: step-mark linear both;
      animation-timeline: view();
      animation-range: cover 28% cover 72%;
    }
    @keyframes step-mark {
      0%, 100% { transform: scaleY(0); opacity: 0; }
      50%      { transform: scaleY(1); opacity: 1; }
    }
  }
}

/* Printing has no scroll timeline to drive, so opt out of the reveal entirely
   rather than print a page of invisible boxes. */
@media print {
  .reveal, .card, .tier, .spec, .chain > li,
  section > .wrap > .marker,
  section > .wrap > h2,
  section > .wrap > .lead-in,
  .rail > li {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  /* Decoration with nothing to drive it on paper. */
  .site-head::before,
  .chain > li::after { display: none !important; }
  .hero::before { animation: none !important; transform: none !important; }
}

/* ---------- the reduced-motion contract ---------- */

@media (prefers-reduced-motion: reduce) {
  .card, .tier, .btn, .chain > li, .chain > li::before, .nav a::after,
  .status, details summary::before, .contact-form input, .contact-form textarea {
    transition: none;
  }
  .btn:active { transform: none; }
  .nav a:hover::after, .nav a[aria-current="page"]::after { transform: scaleX(1); }
  .reveal, .card, .tier, .spec, .chain > li {
    animation: none; opacity: 1; transform: none;
  }
}

/* ==========================================================================
   Hero diagram
   --------------------------------------------------------------------------
   Inline SVG rather than an image: it is text at any zoom, it themes with the
   palette, it costs one request fewer than a PNG, and its aspect ratio is
   fixed by the viewBox so it cannot shift layout.
   ========================================================================== */

/* The diagram used to sit in the hero, in a .hero-grid / .hero-copy /
   .hero-fig layout. It now sits mid-page in .stage-grid, and those three
   selectors stayed behind as dead rules — until markup elsewhere used the name
   .hero-grid again and silently inherited a layout meant for a figure. Only
   what the diagram still needs is kept here; the hero's own layout lives with
   the hero, near the top of this file. Do not reintroduce a generic .hero-*
   layout rule in this section. */
.chain-dia {
  width: 100%;
  max-width: 252px;
  height: auto;
  display: block;
}

.dia-spine path,
.dia-fan path {
  fill: none;
  stroke: var(--rule);
  stroke-width: 1.5;
  stroke-linecap: round;
}
.dia-fan path { stroke: color-mix(in srgb, var(--accent) 50%, transparent); }
.dia-dot circle { fill: var(--paper); stroke: var(--accent); stroke-width: 2; }
.dia-dot .dia-dot-end { fill: var(--accent); }
.hero .dia-dot circle { fill: var(--surface); }
.hero .dia-dot .dia-dot-end { fill: var(--accent); }

.dia-label text {
  font-family: var(--f-body);
  font-size: 12px;
  font-weight: 650;
  fill: var(--ink);
}
.dia-sub text {
  font-family: var(--f-mono);
  font-size: 8px;
  fill: var(--ink-3);
}

/* A single accent stroke travelling the spine. Decorative only — the diagram
   reads identically without it. */
.dia-pulse {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .dia-pulse {
    stroke-dasharray: 46 700;
    animation: dia-travel 4.6s ease-in-out infinite;
  }
  @keyframes dia-travel {
    0%        { stroke-dashoffset: 60;   opacity: 0; }
    12%       { opacity: 0.9; }
    78%       { opacity: 0.9; }
    88%, 100% { stroke-dashoffset: -420; opacity: 0; }
  }
}

/* ==========================================================================
   Specification tiles
   --------------------------------------------------------------------------
   The figures here are structural facts about how the system is built — route
   count, field count, the number of ways it can report a cleared conflict
   (zero). They are not performance metrics, because nothing is measured yet.
   Never put a result in this grid.
   ========================================================================== */

.spec-head { margin-top: 2.5rem; }

.specs {
  display: grid;
  gap: clamp(2rem, 4vw, 3.25rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  list-style: none;
  margin: 2.5rem 0 3rem;
  padding: 0;
}

.spec {
  border-top: 1px solid var(--rule);
  padding: 1.15rem 0 0;
  transition: border-color 180ms ease;
}

/* The numeral carries the panel. Nothing boxes it in. */
.spec-fig {
  font-family: var(--f-display);
  font-size: clamp(3.6rem, 7vw, 5rem);
  font-weight: 500;
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--accent);
  margin: 0 0 0.45rem;
  font-variant-numeric: lining-nums tabular-nums;
}

.spec-label {
  font-family: var(--f-body);
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.07em;
  line-height: 1.35;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 0.7rem;
}

.spec p:last-child { margin-bottom: 0; color: var(--ink-2); font-size: 0.97rem; }

@media (hover: hover) {
  .spec:hover { border-top-color: var(--accent); }
}

@media (prefers-reduced-motion: reduce) {
  .spec { transition: none; }
}

/* ==========================================================================
   Interactive sections
   --------------------------------------------------------------------------
   Every panel below is authored into the HTML and visible by default. The
   script hides all but one and reveals the picker. If it never runs, the
   section is a plain readable stack — which is also why it is crawlable.
   ========================================================================== */

/* ---------- picker ---------- */

.picker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 2.5rem;
  padding: 0;
  list-style: none;
}
.picker button {
  font: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--ink-2);
  background: transparent;
  border: 1px solid var(--field);
  border-radius: 999px;
  padding: 0.6rem 1.1rem;
  min-height: 44px;
  cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
@media (hover: hover) {
  .picker button:hover { color: var(--ink); border-color: var(--ink); }
}
.picker button.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* Panels stack until the script switches the group to one-at-a-time. */
.panels > [data-panel] + [data-panel] { margin-top: 3.5rem; }
.panels.is-switched > [data-panel] + [data-panel] { margin-top: 0; }
.panels > [data-panel]:focus { outline: none; }

/* ---------- the workflow diptych ---------- */

.wf { display: grid; gap: clamp(2rem, 4vw, 3.5rem); }
@media (min-width: 54rem) { .wf { grid-template-columns: 1fr 1fr; } }

.wf-head {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin: 0 0 1.25rem;
  padding-bottom: 0.9rem;
  border-bottom: 1px solid var(--rule);
}
.wf-today .wf-head { color: var(--ink-3); }
.wf-with  .wf-head { color: var(--accent); border-bottom-color: var(--accent); }

.wf-steps { list-style: none; margin: 0; padding: 0; counter-reset: wfstep; }
.wf-steps li {
  counter-increment: wfstep;
  position: relative;
  padding: 0 0 1.15rem 2.15rem;
  font-size: 0.98rem;
  line-height: 1.45;
  color: var(--ink-2);
}
.wf-steps li::before {
  content: counter(wfstep);
  position: absolute;
  left: 0;
  top: 0.1em;
  font-family: var(--f-mono);
  font-size: 0.7rem;
  color: var(--ink-3);
}
/* The connector between steps, so the column reads as a sequence. */
.wf-steps li::after {
  content: "";
  position: absolute;
  left: 0.42rem;
  top: 1.5em;
  bottom: 0.35rem;
  width: 1px;
  background: var(--rule);
}
.wf-steps li:last-child { padding-bottom: 0; }
.wf-steps li:last-child::after { display: none; }

.wf-with .wf-steps li { color: var(--ink); }
.wf-with .wf-steps li::before { color: var(--accent); }
.wf-with .wf-steps li::after { background: color-mix(in srgb, var(--accent) 45%, transparent); }

/* The cost of a manual step, stated where it happens. */
.wf-steps .who {
  display: block;
  font-family: var(--f-mono);
  font-size: 0.72rem;
  color: var(--ink-3);
  margin-top: 0.2rem;
}
.wf-with .wf-steps .who { color: var(--accent); }

.wf-note {
  grid-column: 1 / -1;
  margin: 0.5rem 0 0;
  color: var(--ink-3);
  font-size: 0.9rem;
}

/* Stagger on selection. Delays are per-position because an inline style
   attribute would be refused by the CSP, and rightly so. */
@media (prefers-reduced-motion: no-preference) {
  .is-entering .wf-steps li { animation: step-in 320ms ease both; }
  .is-entering .wf-steps li:nth-child(1) { animation-delay: 0ms; }
  .is-entering .wf-steps li:nth-child(2) { animation-delay: 55ms; }
  .is-entering .wf-steps li:nth-child(3) { animation-delay: 110ms; }
  .is-entering .wf-steps li:nth-child(4) { animation-delay: 165ms; }
  .is-entering .wf-steps li:nth-child(5) { animation-delay: 220ms; }
  .is-entering .wf-steps li:nth-child(6) { animation-delay: 275ms; }
  .is-entering .wf-steps li:nth-child(7) { animation-delay: 330ms; }
  .is-entering .wf-steps li:nth-child(8) { animation-delay: 385ms; }
  .is-entering .wf-steps li:nth-child(9) { animation-delay: 440ms; }
  .is-entering .wf-steps li:nth-child(n+10) { animation-delay: 495ms; }
  @keyframes step-in {
    from { opacity: 0; transform: translateY(7px); }
    to   { opacity: 1; transform: none; }
  }
}

/* ---------- calculator ---------- */

.calc { margin: 2.5rem 0 0; }
.calc-grid {
  display: grid;
  gap: 1.25rem 1.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
  margin-bottom: 2.5rem;
}
.calc-field label {
  display: block;
  font-size: 0.88rem;
  font-weight: 600;
  margin-bottom: 0.4rem;
}
.calc-field input {
  width: 100%;
  font: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--field);
  border-radius: var(--radius);
  padding: 0.65rem 0.75rem;
  min-height: 46px;
}
.calc-field input:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.calc-out {
  border-top: 2px solid var(--ink);
  padding-top: 1.35rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.75rem;
}
.calc-fig {
  font-family: var(--f-display);
  font-size: clamp(3.4rem, 8vw, 5.5rem);
  font-weight: 500;
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--accent);
  margin: 0;
  font-variant-numeric: lining-nums tabular-nums;
}
.calc-unit {
  font-family: var(--f-body);
  font-size: 0.78rem;
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0;
}
.calc-note { margin: 1.15rem 0 0; color: var(--ink-2); max-width: 46ch; }

/* ---------- the five-stage centrepiece ---------- */

.stage-grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: start;
}
@media (min-width: 54rem) {
  .stage-grid { grid-template-columns: auto minmax(0, 1fr); }
}
.stage-fig { margin: 0; justify-self: start; }
.stage-copy h3 { margin-top: 0; }
.stage-copy h3 + p { color: var(--ink-2); }
.stage-copy > h3 ~ h3 { margin-top: 2.25rem; }
.stage-copy p:last-child { margin-bottom: 0; }

/* The status pill sits inline with a panel heading. */
h3 .status { margin-left: 0.6rem; vertical-align: 0.18em; }


/* ---------- mobile navigation ---------- */

.navtoggle {
  display: none;
  align-items: center;
  gap: 0.55rem;
  font: inherit;
  font-size: 0.74rem;
  font-weight: 650;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--field);
  border-radius: 999px;
  padding: 0.5rem 0.95rem;
  min-height: 44px;
  cursor: pointer;
  margin-left: auto;
}
.navtoggle-bars {
  position: relative;
  width: 15px;
  height: 9px;
  flex: none;
}
.navtoggle-bars::before,
.navtoggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1.5px;
  background: var(--ink);
  transition: transform 180ms ease, opacity 140ms ease;
}
.navtoggle-bars::before { top: 0; }
.navtoggle-bars::after  { bottom: 0; }
.nav-open .navtoggle-bars::before { transform: translateY(3.75px) rotate(45deg); }
.nav-open .navtoggle-bars::after  { transform: translateY(-3.75px) rotate(-45deg); }

@media (prefers-reduced-motion: reduce) {
  .navtoggle-bars::before, .navtoggle-bars::after { transition: none; }
}

/* Below the breakpoint, and only once the script has revealed the toggle, the
   links collapse behind it. Without .has-toggle nothing here applies and the
   nav renders as the plain wrapping list it has always been. */
@media (max-width: 47.99rem) {
  .site-head.has-toggle .navtoggle { display: inline-flex; }
  .site-head.has-toggle .brand { margin-right: 0; }
  .site-head.has-toggle .wrap { gap: 0; }
  .site-head.has-toggle .nav {
    display: none;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-top: 0.85rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--rule);
  }
  .site-head.has-toggle.nav-open .nav { display: flex; }
  .site-head.has-toggle .nav a {
    padding-block: 0.95rem;
    font-size: 0.82rem;
  }
  .site-head.has-toggle .nav a::after { display: none; }
  .site-head.has-toggle .nav a[aria-current="page"] { color: var(--accent); }
}

/* ---------- footer legal links ---------- */

.legal-links {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0 1.15rem;
  margin-left: 0.35rem;
}
.legal-links a {
  color: var(--ink-3);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--rule);
  /* The colophon runs at 0.85rem; these need a 44px target of their own
     without pushing the line apart on a phone. */
  padding-block: 0.72rem;
}
.legal-links a:hover { color: var(--accent); text-decoration-color: var(--accent); }

/* ==========================================================================
   Film
   --------------------------------------------------------------------------
   A 60-second render, self-hosted. preload="none" keeps the MP4 off the wire
   until someone presses play, so the poster is the only cost on page load.
   The margin is set explicitly: an unstyled <figure> falls back to its 40px
   UA margin, which is how the hero diagram once broke.
   ========================================================================== */

.film { margin: 2rem 0 0; }
.film video {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  background: var(--surface-2);
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius);
}
.film figcaption {
  max-width: var(--measure);
  margin-top: 0.7rem;
  font-size: 0.92rem;
  color: var(--ink-3);
}
.film details { max-width: var(--measure); margin-top: 0.4rem; }
.film details ol { margin: 0.6rem 0 0 1.55rem; padding-left: 1.2rem; color: var(--ink-2); }
.film details li { margin-bottom: 0.35rem; }

/* ==========================================================================
   Hero demo
   --------------------------------------------------------------------------
   Hidden in the markup and revealed by the script, like every other control
   here. Without JavaScript the visitor sees a sentence pointing at the worked
   examples instead of a form that cannot submit.
   ========================================================================== */

.demo {
  border-top: 2px solid var(--ink);
  padding-top: 1.5rem;
  max-width: 44rem;
}

.demo-form label {
  display: block;
  font-size: 0.88rem;
  font-weight: 650;
  margin-bottom: 0.5rem;
}
.demo-form textarea,
.demo-form input[type="text"] {
  width: 100%;
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--field);
  border-radius: var(--radius);
  padding: 0.75rem 0.85rem;
  min-height: 46px;
}
.demo-form textarea { resize: vertical; line-height: 1.5; }
.demo-form textarea::placeholder,
.demo-form input::placeholder { color: var(--ink-3); }
.demo-form textarea:focus-visible,
.demo-form input:focus-visible {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.demo-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.85rem;
  align-items: flex-end;
  margin-top: 1rem;
}
.demo-biz { flex: 1 1 14rem; min-width: 0; }
.demo-row .btn { flex: none; }
.demo-form button[disabled] { opacity: 0.55; cursor: progress; }

.demo-status {
  font-family: var(--f-mono);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  margin: 1.1rem 0 0;
  min-height: 1.4em;
  color: var(--ink-3);
}
.demo-status.is-ok   { color: var(--accent); }
.demo-status.is-warn { color: var(--warn); }
.demo-status.is-busy { color: var(--ink-2); }

/* ---------- the result ---------- */

.demo-out {
  margin-top: 1.25rem;
  border-left: 2px solid var(--accent);
  padding: 0.35rem 0 0.35rem 1.35rem;
}
.demo-summary {
  font-family: var(--f-display);
  font-size: 1.3rem;
  line-height: 1.3;
  letter-spacing: -0.015em;
  margin: 0 0 1.15rem;
}
.demo-fields {
  display: grid;
  grid-template-columns: minmax(7rem, auto) 1fr;
  gap: 0.4rem 1.25rem;
  margin: 0 0 1.25rem;
  font-size: 0.95rem;
}
.demo-fields dt {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding-top: 0.25em;
}
.demo-fields dd { margin: 0; color: var(--ink); min-width: 0; overflow-wrap: break-word; }

.demo-decision {
  display: inline-block;
  font-family: var(--f-mono);
  font-size: 0.74rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-accent);
  background: var(--accent);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  margin: 0 0 0.9rem;
}
.demo-reason { color: var(--ink-2); margin: 0 0 1.15rem; }

.demo-missing-head {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 0.5rem;
}
.demo-missing { margin: 0; padding-left: 1.1rem; color: var(--ink-2); font-size: 0.95rem; }
.demo-missing li { margin-bottom: 0.3rem; }

/* A fallback must look like a fallback. */
.demo-fallback-note {
  margin: 1.25rem 0 0;
  padding: 0.9rem 1.1rem;
  background: var(--warn-soft);
  border-left: 3px solid var(--warn);
  border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--ink);
  font-size: 0.92rem;
}

.demo-foot { margin-top: 1.5rem; }

/* ---------- film narration ---------- */

/* The control is hidden in the markup and revealed by the script only when the
   device actually has an on-device voice, so this styles something that may
   never appear — which is the point. */
.film-narrate {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.1rem;
  margin: 1rem 0 0;
}
.film-narrate-note { max-width: 46ch; margin: 0; }

/* The line being spoken. A left edge and a weight change rather than a
   background, so it reads at a glance without the transcript turning into a
   highlighter exercise. */
.film details li { transition: color 160ms ease; }
.film details li.is-speaking {
  color: var(--ink);
  font-weight: 500;
}
@media (prefers-reduced-motion: reduce) {
  .film details li { transition: none; }
}

/* ---------- integrations rail ---------- */

.rail-band {
  padding-block: clamp(2.5rem, 5vw, 4rem);
  background: var(--surface);
  border-block: 1px solid var(--rule);
}
.rail-label {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 1.35rem;
}
.rail {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 0.75rem;
  list-style: none;
  margin: 0 0 1.35rem;
  padding: 0;
}
.rail li {
  font-family: var(--f-display);
  font-size: clamp(1.05rem, 2.2vw, 1.45rem);
  letter-spacing: -0.018em;
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.4rem 1.05rem;
}
.rail-note { max-width: 56ch; margin: 0; }

/* ==========================================================================
   The premium layer
   --------------------------------------------------------------------------
   Frosted glass, gradient edges, the bento grids, the hero spheres, the
   per-section progress rails and the pointer micro-interactions.

   Everything in this block is additive. Nothing here is required for the page
   to read, and all of it is written so that removing the block leaves the
   editorial layout underneath intact. Four rules hold throughout, and the
   first two are the ones that will bite if ignored:

   1. ONLY transform and opacity animate. No animated width, height, top,
      left, filter or box-shadow — those run on the main thread, repaint on
      every frame and are what turns a 98 into a 71. Where an effect looks
      like it needs a layout property, it is drawn at full size once and
      revealed with a transform instead.
   2. NOTHING here may reserve or move layout. Every decorative layer is
      position:absolute inside an isolated stacking context with
      pointer-events:none, so CLS stays at 0 no matter when it paints.
   3. Glass degrades. backdrop-filter is behind @supports; without it the
      cards are opaque surfaces, which is the same design with less light.
   4. Reduced motion stops all of it, and so does print.
   ========================================================================== */

:root {
  /* Glass is the surface colour at partial alpha — not a new colour. It has
     to be opaque enough that --ink-2 body copy still clears 4.5:1 against
     whatever shows through, which is why it is this high. */
  --glass:      color-mix(in srgb, var(--surface) 72%, transparent);
  --glass-edge: color-mix(in srgb, var(--rule) 70%, transparent);
  --glass-blur: 14px;

  /* The gradient edge. Accent at the top-left corner falling to nothing by
     the middle, so a grid of cards reads as lit from one direction rather
     than as eight independently glowing boxes. */
  --edge-lit:   color-mix(in srgb, var(--accent) 55%, transparent);
  --edge-dim:   color-mix(in srgb, var(--rule) 60%, transparent);

  /* Cursor spotlight strength. Overridden to 0 under reduced motion, which
     is how one custom property turns the whole effect off. */
  --spot:       0.10;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Dark glass sits lower, because a blur over a dark ground reads as fog
       rather than glass if the alpha is too high. */
    --glass:      color-mix(in srgb, var(--surface) 58%, transparent);
    --glass-edge: color-mix(in srgb, var(--rule) 85%, transparent);
    --edge-lit:   color-mix(in srgb, var(--accent) 62%, transparent);
    /* The spotlight has to work harder over a dark surface to be seen. */
    --spot:       0.16;
  }
}

/* ---------- frosted glass ----------
 *
 * The gradient edge is drawn with two stacked backgrounds rather than a
 * border-image, because border-image cannot follow a border-radius and the
 * mask-composite approach needs two pseudo-elements per card. Here the first
 * layer is clipped to the padding box and paints the card face; the second is
 * clipped to the border box and shows through the 1px transparent border as
 * the edge. One element, no pseudo, and it inherits the radius for free. */
.glass {
  position: relative;
  border: 1px solid transparent;
  border-radius: calc(var(--radius) * 3);
  background-image:
    linear-gradient(var(--glass), var(--glass)),
    linear-gradient(135deg, var(--edge-lit), var(--edge-dim) 42%, transparent 78%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  isolation: isolate;
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass {
    -webkit-backdrop-filter: saturate(1.3) blur(var(--glass-blur));
    backdrop-filter: saturate(1.3) blur(var(--glass-blur));
  }
}

/* Without backdrop-filter the translucent face would show raw background
   through it, so fall back to the solid surface. Same design, no light. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass {
    background-image:
      linear-gradient(var(--surface), var(--surface)),
      linear-gradient(135deg, var(--edge-lit), var(--edge-dim) 42%, transparent 78%);
  }
}

/* ---------- the cursor spotlight ----------
 *
 * app.js writes --px/--py as percentages on the card under the pointer. This
 * paints a soft radial at that point. The gradient is drawn once at full size
 * and only its opacity animates, so moving the cursor costs a composite and
 * not a repaint.
 *
 * Both custom properties default to 50% so that a card which has never been
 * hovered — or a page with no JavaScript at all — is simply a card. */
.glass::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  border-radius: inherit;
  opacity: 0;
  transition: opacity 220ms ease;
  background: radial-gradient(
    18rem 18rem at var(--px, 50%) var(--py, 50%),
    color-mix(in srgb, var(--accent) calc(var(--spot) * 100%), transparent),
    transparent 68%);
}

@media (hover: hover) and (pointer: fine) {
  .glass:hover::after { opacity: 1; }
}

/* Depth on hover. translate3d rather than translateY to force the layer onto
   the compositor before the pointer ever arrives, so the first hover of the
   session is as smooth as the fiftieth. */
@media (hover: hover) {
  .glass {
    transition:
      transform   260ms cubic-bezier(0.22, 1, 0.36, 1),
      box-shadow  260ms ease,
      border-color 260ms ease;
  }
  .glass:hover {
    transform: translate3d(0, -4px, 0);
    box-shadow:
      0 18px 40px -24px color-mix(in srgb, var(--accent) 50%, transparent),
      0 2px 10px -6px rgb(0 0 0 / 0.12);
  }
}

/* Keyboard users get the elevation too — a focused card that stays flat while
   a hovered one lifts is a two-tier interface. */
.glass:focus-within {
  box-shadow: 0 18px 40px -24px color-mix(in srgb, var(--accent) 50%, transparent);
}

/* ---------- bento grid ----------
 *
 * Varying cell sizes with one focal card, which is the whole point: a grid of
 * identical boxes is a table, and the eye has nowhere to land. The focal card
 * is always the first child, so the reading order and the visual hierarchy
 * agree and no tabindex is needed to repair it.
 *
 * Spans are set with grid-column/grid-row rather than grid-template-areas so
 * that adding or removing a card degrades to a plain cell instead of
 * collapsing the named template. */
.bento {
  display: grid;
  gap: clamp(0.85rem, 1.8vw, 1.25rem);
  margin: 0;
  padding: 0;
  list-style: none;
  grid-template-columns: 1fr;
}

.bento > .card {
  /* Undo the editorial card: in bento the panel IS the container, so the
     top rule and the zero side padding both have to go. */
  border-top: 0;
  padding: clamp(1.25rem, 2.6vw, 1.75rem);
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.bento > .card > h3 {
  font-size: 1.12rem;
  margin-bottom: 0.45rem;
}
.bento > .card > p:last-child { margin-bottom: 0; }

/* The focal card. Larger type, and the accent edge at full strength rather
   than the 55% the others carry. */
.bento > .card:first-child {
  --edge-lit: color-mix(in srgb, var(--accent) 85%, transparent);
}
.bento > .card:first-child > h3 {
  font-family: var(--f-display);
  font-size: clamp(1.3rem, 2.6vw, 1.85rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
}

/* Two columns from 46rem: the focal card goes full width, everything else
   pairs up. */
@media (min-width: 46rem) {
  .bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bento > .card:first-child { grid-column: span 2; }
}

/* Three columns from 64rem for the six-card grid: the focal card takes a
   2x2 block in the top left and the remaining five fill around it.
   3 columns x 3 rows = 9 cells; the focal takes 4 and five cards take 5. */
@media (min-width: 64rem) {
  .bento-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .bento-6 > .card:first-child {
    grid-column: span 2;
    grid-row: span 2;
    justify-content: center;
  }

  /* Four columns for the eight-card grid. 4 x 3 = 12 cells: the focal takes
     4, six cards take one each, and the last takes 2 so the bottom row
     closes flush instead of leaving a hole. */
  .bento-8 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .bento-8 > .card:first-child {
    grid-column: span 2;
    grid-row: span 2;
    justify-content: center;
  }
  .bento-8 > .card:last-child { grid-column: span 2; }
}

/* ---------- ambient light spheres ----------
 *
 * Three soft spheres behind the hero, drifting on a long loop. They are the
 * one piece of pure decoration on the page, so they are also the piece most
 * carefully fenced: aria-hidden in the markup, pointer-events:none, z-index
 * below content, and a transform-only animation.
 *
 * .hero is overflow:hidden, which both clips them and makes it impossible for
 * them to extend the scroll height. */
.spheres {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}
.sphere {
  position: absolute;
  border-radius: 50%;
  /* filter: blur() on a large element is expensive and repaints; a radial
     gradient that fades to transparent is free and looks the same. */
  background: radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--accent) 26%, transparent),
    color-mix(in srgb, var(--accent) 9%, transparent) 45%,
    transparent 70%);
}
.sphere-1 { width: 38rem; height: 38rem; top: -16rem; left: -10rem; }
.sphere-2 { width: 30rem; height: 30rem; top: -6rem;  right: -12rem;
            background: radial-gradient(circle at 50% 50%,
              color-mix(in srgb, var(--accent) 18%, transparent),
              transparent 68%); }
.sphere-3 { width: 24rem; height: 24rem; bottom: -14rem; left: 42%;
            background: radial-gradient(circle at 50% 50%,
              color-mix(in srgb, var(--accent) 14%, transparent),
              transparent 66%); }

@media (prefers-reduced-motion: no-preference) {
  .sphere-1 { animation: drift-a 34s ease-in-out infinite alternate; }
  .sphere-2 { animation: drift-b 28s ease-in-out infinite alternate; }
  .sphere-3 { animation: drift-c 41s ease-in-out infinite alternate; }
}

@keyframes drift-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(5%, 7%, 0) scale(1.08); }
}
@keyframes drift-b {
  from { transform: translate3d(0, 0, 0) scale(1.05); }
  to   { transform: translate3d(-7%, 5%, 0) scale(1); }
}
@keyframes drift-c {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-9%, -6%, 0); }
}

/* ---------- kinetic tags ----------
 *
 * The integrations rail floats. Each tag gets the same keyframes with a
 * different duration and negative delay, which is enough to stop the row
 * reading as one rigid object without any of them visibly bobbing.
 *
 * The float is on an inner <span>, not on the <li>. The <li> already carries
 * the scroll-driven reveal declared in the motion block, and a second
 * `animation` declaration on the same element would replace that reveal
 * rather than add to it — the cascade does not merge animation lists. So the
 * li stays the reveal target and the span becomes the visible pill.
 *
 * These are TIME delays on a TIME animation, so unlike the scroll-driven
 * blocks above, animation-delay works here.
 */
.rail li {
  border: 0;
  padding: 0;
}
.rail li > span {
  display: inline-block;
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.4rem 1.05rem;
}

@media (prefers-reduced-motion: no-preference) {
  .rail li > span {
    animation: tag-float 6s ease-in-out infinite alternate;
  }
  .rail li:nth-child(3n + 2) > span { animation-duration: 7.5s; animation-delay: -1.2s; }
  .rail li:nth-child(3n + 3) > span { animation-duration: 8.5s; animation-delay: -2.6s; }
  .rail li:nth-child(4n + 1) > span { animation-delay: -0.6s; }
}
@keyframes tag-float {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -5px, 0); }
}

@media (hover: hover) {
  .rail li > span {
    transition: border-color 200ms ease, color 200ms ease;
  }
  .rail li > span:hover { border-color: var(--accent); color: var(--accent-ink); }
}

/* ---------- magnetic buttons ----------
 *
 * app.js writes --mx/--my in pixels while the pointer is near a button. The
 * translation is applied here rather than in script so that the rest state,
 * the transition back and the reduced-motion opt-out all live in CSS with
 * everything else. Both default to 0, so a button nothing has touched — or a
 * page with no JavaScript — sits exactly where the layout puts it. */
.btn {
  --mx: 0px;
  --my: 0px;
}

@media (hover: hover) and (pointer: fine) {
  .btn {
    transform: translate3d(var(--mx), var(--my), 0);
    /* Overrides the 110ms transform transition declared in the motion block
       above: a magnetic button needs to track the pointer with almost no lag
       while it is being followed, and ease back when released. app.js drops
       .is-magnetic on release, which restores the slower curve. */
    transition:
      background-color 160ms ease,
      border-color     160ms ease,
      box-shadow       160ms ease,
      transform        420ms cubic-bezier(0.22, 1, 0.36, 1);
  }
  .btn.is-magnetic { transition-duration: 160ms, 160ms, 160ms, 80ms; }
  /* :active sets translateY in the motion block, which a transform here would
     override. Fold the press into the same transform instead. */
  .btn:active { transform: translate3d(var(--mx), calc(var(--my) + 1px), 0); }
}

/* ---------- section progress rails ----------
 *
 * A hairline down the left edge of a section that fills as the section
 * passes, so a long page reads as distinct movements rather than one
 * continuous scroll. Drawn at full height and revealed with scaleY, because
 * animating height would relayout on every frame.
 *
 * Nested inside @supports because a browser without scroll timelines would
 * otherwise show a permanently empty rail. */
.has-rail { position: relative; }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .has-rail::before {
      content: "";
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      width: 2px;
      z-index: 1;
      pointer-events: none;
      background: linear-gradient(180deg,
        var(--accent), color-mix(in srgb, var(--accent) 25%, transparent));
      transform: scaleY(0);
      transform-origin: top center;
      animation: rail-fill linear both;
      animation-timeline: view();
      /* contain, not entry: the rail should track the reader's progress
         THROUGH the section, which is the whole time any part of it is on
         screen — not just its arrival. */
      animation-range: contain 0% contain 100%;
    }
    @keyframes rail-fill {
      from { transform: scaleY(0); }
      to   { transform: scaleY(1); }
    }
  }
}

/* The rail sits in the page gutter on a wide screen and is suppressed on a
   narrow one, where there is no gutter to put it in and it would collide
   with the text. */
@media (max-width: 63.99rem) {
  .has-rail::before { display: none; }
}
@media (min-width: 64rem) {
  .has-rail::before { left: max(0px, calc((100vw - var(--wrap)) / 2)); }
}

/* ---------- reveal masks ----------
 *
 * A wipe across the top edge of a section as it enters, which gives the
 * sections a visible seam without the harshness of a border. clip-path is
 * not used: a transform on a 2px bar is cheaper and cannot be janked by a
 * browser that rasterises clip-path on the main thread. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .has-rail::after {
      content: "";
      position: absolute;
      left: 0; right: 0; top: 0;
      height: 1px;
      z-index: 1;
      pointer-events: none;
      background: linear-gradient(90deg,
        transparent,
        color-mix(in srgb, var(--accent) 70%, transparent) 20%,
        color-mix(in srgb, var(--accent) 70%, transparent) 80%,
        transparent);
      transform: scaleX(0);
      transform-origin: left center;
      animation: seam-wipe linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 70%;
    }
    @keyframes seam-wipe {
      from { transform: scaleX(0); }
      to   { transform: scaleX(1); }
    }
  }
}

/* ---------- bento reveal cascade ----------
 *
 * The base .card reveal already covers these; this only re-times them. A
 * bento cascade runs on a diagonal rather than in source order, because the
 * cards are not in a single row and a left-to-right stagger looks wrong when
 * the first card is four cells tall. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .bento > .card:nth-child(1) { animation-range: entry 0%  entry 45%; }
    .bento > .card:nth-child(2) { animation-range: entry 6%  entry 51%; }
    .bento > .card:nth-child(3) { animation-range: entry 10% entry 55%; }
    .bento > .card:nth-child(4) { animation-range: entry 14% entry 59%; }
    .bento > .card:nth-child(5) { animation-range: entry 18% entry 63%; }
    .bento > .card:nth-child(6) { animation-range: entry 22% entry 67%; }
    .bento > .card:nth-child(7) { animation-range: entry 26% entry 71%; }
    .bento > .card:nth-child(n + 8) { animation-range: entry 30% entry 75%; }
  }
}

/* ---------- picker: smooth switching ----------
 *
 * The panel content itself is NOT animated here. Selecting a tab already
 * staggers that panel's steps through the .is-entering block above, and
 * fading the panel as a whole on top of that animates the same change twice —
 * the second one arriving late enough to read as a stutter.
 *
 * What was missing is the indicator. The selected pill scales up from under
 * the label instead of the background colour snapping on. It is drawn on the
 * button rather than as one shared sliding element, because a shared
 * indicator has to have its position measured in script on every resize and
 * every font load, and gets it wrong once in between.
 */
.picker button {
  position: relative;
  isolation: isolate;
  transition:
    color 180ms ease,
    border-color 180ms ease,
    transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.picker button::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: var(--accent);
  transform: scale(0.82);
  opacity: 0;
  transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms ease;
}
.picker button.is-on::before { transform: scale(1); opacity: 1; }
/* The .is-on rule in the picker block sets a flat accent background. With the
   animated pill underneath it that background would hide the transition, so
   it is cleared here and the pill carries the fill. */
.picker button.is-on { background: transparent; }
@media (hover: hover) {
  .picker button:not(.is-on):hover { transform: translate3d(0, -1px, 0); }
}

/* ---------- glass on the existing panels ---------- */

/* .tier already has a border, radius and surface fill, so it only needs the
   lit edge. Its own border-color transition on hover is left alone. */
.tier {
  background-image:
    linear-gradient(var(--surface), var(--surface)),
    linear-gradient(135deg, var(--edge-lit), var(--edge-dim) 48%, transparent 82%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  border-color: transparent;
}

/* ---------- print ---------- */

@media print {
  .spheres { display: none !important; }
  .has-rail::before, .has-rail::after { display: none !important; }
  .glass {
    background-image: none !important;
    border-color: var(--rule) !important;
    box-shadow: none !important;
    transform: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
  }
  .glass::after { display: none !important; }
  .bento { display: block !important; }
  .bento > .card { break-inside: avoid; margin-bottom: 1rem; }
  .rail li > span { animation: none !important; transform: none !important; }
}

/* ---------- the reduced-motion contract, extended ----------
 *
 * The decorative layers stay — they are not motion, they are light — but
 * everything that moves stops, and the spotlight is turned off at the token
 * so that no hover produces a change of state either. */
@media (prefers-reduced-motion: reduce) {
  :root { --spot: 0; }

  .sphere,
  .rail li > span,
  .has-rail::before,
  .has-rail::after {
    animation: none !important;
    transform: none !important;
  }
  /* The rails are drawn by a scaleY/scaleX of 0 and filled by animation. With
     the animation off, transform:none would leave them at full size, which is
     correct for the seam and correct for the rail: a static hairline. */
  .glass,
  .glass::after,
  .picker button,
  .picker button::before {
    transition: none !important;
  }
  .glass:hover { transform: none !important; box-shadow: none !important; }
  .btn { transform: none !important; }
}

/* ---------- the hero demo as a glass panel ----------
 *
 * .demo is authored as an editorial block: a 2px rule across the top and no
 * side padding. .glass replaces the border and the radius but not the
 * padding, so without this the panel face would run straight into its own
 * edge. */
.demo.glass {
  padding: clamp(1.25rem, 3vw, 1.75rem);
}

/* Bento headings sit in narrow cells where the default greedy line-breaking
   leaves one word stranded on the last line. `balance` is a no-op on browsers
   that lack it and costs nothing on four-word headings. */
.bento > .card > h3 { text-wrap: balance; }
