/*
  getopa.app — the whole stylesheet, for every page.

  ## Read this before changing anything

  This file is the single source of truth for the site's styles, and it is
  ALSO inlined verbatim into site/index.html inside a <style> block. Edit THIS
  file, then run

      node scripts/sync_site_css.mjs

  which rewrites the inlined block in index.html. `check_site.mjs` fails if the
  two have diverged. The subpages link this file normally.

  ## The 2026-08-21 restyle, and what it reversed

  The first version of this site used a system font stack and Georgia
  headings. It passed every accessibility gate and looked like a blog. The
  problem was not any single rule — it was that nothing tied it to the product
  it was advertising, and nothing in it had been *designed*.

  Two deliberate reversals of earlier decisions:

  1. **Source Sans 3 is now self-hosted here**, the same face the extension
     ships. SITE.md previously banned webfonts outright, reasoning that a font
     arriving late causes a reflow that is disorienting for this audience.
     That was written when the realistic alternative was Google Fonts on a
     third-party origin. Three same-origin 15KB woff2 files, preloaded, behind
     a CSP that already allows `font-src 'self'`, are a different proposition
     — and the *product* uses this face, so the site using anything else was a
     permanent mismatch. `font-display: swap` plus a metrics-adjacent fallback
     keeps the worst case a swap, not a blank page.

  2. **Type got much bigger and much tighter.** Display sizes now run to 80px
     with -0.035em tracking. Restraint in the copy is a virtue; timidity in the
     typography just read as unfinished.

  ## What is deliberately NOT borrowed

  The direction takes its confidence from editorial/studio work — oversized
  display type, uppercase micro-labels, numbered sections, a full-bleed dark
  block, generous radii. It does not take the parts that would hurt this
  audience: hairline weights, low-contrast greys, scroll-triggered animation,
  or type so large it stops being readable at 200% zoom.

  Every micro-label is 16px, not the 11-13px the pattern usually uses, because
  this file's own floor says 16px is the minimum for anything carrying
  meaning. That is the constraint doing its job against a fashionable choice.

  ## Colours

  --ink / --brand / --ground and friends are lifted verbatim from
  extension/src/popup/popup.css so the page and the product are the same
  colours. Deliberately NOT from docs/DESIGN_SYSTEM.md, which documents a
  palette that no longer exists.

  --night is new and has no counterpart in the extension: it is the dark block
  the safety promises sit on. #f6f6f8 on #0b0b0d is about 19:1, far past AAA,
  which is what makes it safe for the single most important section.
*/

@font-face {
  font-family: "Source Sans 3";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("/fonts/sourcesans400.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url("/fonts/sourcesans600.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("/fonts/sourcesans700.woff2") format("woff2");
}

:root {
  --ink: #1d1d1f;
  /*
    ink-2 and ink-3 were darkened on 2026-09-02 after measuring them against
    the actual rendered pixels of the body gradient rather than against white.

    The gradient's lightest region is about #eaecee, not #ffffff, and that
    ~5% difference was enough to put both tokens below the bar:

      old --ink-3 #6e6e73 on #eaecee = 4.28  — FAILS AA for body text (4.5)
      old --ink-2 #55555b on #eaecee = 6.25  — below this site's stated AAA (7.0)

    axe did not catch either, and that is worth knowing rather than treating
    as a fluke: axe resolves an element's background by walking up for a
    computed `background-color`, and a `linear-gradient` is a background-IMAGE.
    So it compared the text against white and passed it. **A green axe run does
    not prove contrast on any element sitting on this page's gradient.**

    New values, measured against that same #eaecee sample:
      --ink-2 #4c4c52 = 7.20  (AAA body, on the gradient and on white)
      --ink-3 #636368 = 5.04  (AA body, comfortably)
  */
  /*
    #4c4c52 until 2026-09-04, when the ground became ruled paper. A rule at
    3.5% black over the paper drops that value to 6.78 against text sitting on
    it, and this site's bar for body copy is AAA (7.0). #484850 measures 7.20
    on a rule and 7.77 on clear paper -- the same margin #4c4c52 had against
    the old ground, so nothing got tighter, it just moved with the background.
  */
  --ink-2: #484850;
  --ink-3: #636368;
  /*
    Warmed to paper, 2026-09-04. Two reasons, and only one of them is taste.

    The one that is not: a bright blue-white screen is the worst case for glare
    in an eye with any lens clouding, which by 70 is most of them. Light
    scatters off the lens and washes the whole field, and short wavelengths
    scatter hardest. Taking the blue out and dropping the luminance a little is
    a real comfort change for exactly this reader, not a mood.

    The one that is: paper says "a form you fill in". A cool grey-white says
    "an app". This audience trusts the first and is wary of the second.

    --surface stays pure white so cards, notes and the header pill lift off the
    paper rather than blending into it.
  */
  --paper-1: #faf7f2;
  --paper-2: #f5f0e7;
  --paper-3: #f2ede4;
  --rule: rgba(29, 29, 31, 0.035);

  --ground: #f2ede4;
  --surface: #ffffff;
  --hairline: #e6e0d5;
  --brand: #1b6ef3;
  --brand-wash: #eef4ff;
  /* Link and accent text uses the darker brand tone so it stays readable on
     white. The brighter blue is reserved for emphasis, not body copy. */
  --brand-d: #0f4fa8;
  --brand-dd: #0b3d8f;
  --good: #117a3d;
  --bad: #b43c31;
  --gold: #a16207;
  /*
    --gold on --gold-wash is 4.14, which fails AA for the voucher's small
    uppercase label. --gold itself is fine where it is used at display size
    (the hero day count, 4.16 on the gradient, large-text AA needs 3.0) and as
    the focus ring, so it is left alone and a darker tone added for small text.
    #8a5606 on #f8ead0 = 5.18.
  */
  --gold-d: #8a5606;
  --gold-wash: #f8ead0;

  /*
    The editorial accent, for section labels and figure marks.

    becomearoboticist.com uses #e9561c for exactly this job. That value scores
    3.05 against the lightest region of this page's gradient (#eaecee), which
    fails AA for anything at label size -- so it is darkened until it passes.
    #a83c17 measures 5.33 on the gradient and 6.32 on white, and it is close
    enough to the reference to read as the same rust. It also happens to sit
    beside the wax seal's #a81f1a, which is not an accident worth undoing.
  */
  --accent: #a83c17;
  /*
    The accent at about 8%, over paper rather than over white, so a marked
    word sits on the page it is printed on instead of on a card. Added
    2026-09-09 to give `.key` a fill that is not the neutral chip `kbd` and
    `code` already wear -- see the note above `.key`. `--ink` on it measures
    well past the 7.0 this site holds body copy to; check_contrast.mjs samples
    the rendered ground, so it is checked rather than asserted.
  */
  --accent-wash: #f7e7e0;

  /* The dark block. Still blue-black so it feels like the same product family,
     but richer and calmer than the old near-neutral block. */
  --night: #0b1220;
  --night-2: #111a2c;
  --night-line: rgba(255, 255, 255, 0.12);
  --on-night: #f8fafc;
  --on-night-2: #c4cfde;

  --measure: 40rem;
  /* Prose inside a full-width band; see `.ed-main`. Added 2026-09-08. */
  --measure-wide: 46rem;
  --frame: 76rem;

  --gap-xs: 0.5rem;
  --gap-sm: 0.75rem;
  --gap-md: 1rem;
  --gap-lg: 1.5rem;
  --gap-xl: 2rem;
  --gap-2xl: 3rem;
  --gap-3xl: 5rem;

  --radius: 18px;
  --radius-lg: 26px;
  --lift-1: 0 1px 2px rgba(15,23,42,.04), 0 1px 1px rgba(15,23,42,.03);
  --lift-2: 0 10px 26px -18px rgba(15,23,42,.26), 0 2px 6px rgba(15,23,42,.08);
  --lift-3: 0 24px 50px -28px rgba(15,23,42,.28), 0 6px 18px rgba(15,23,42,.08);

  /* Fallback stack chosen for similar metrics, so a swap moves as little as
     possible. */
  --font: "Source Sans 3", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  /*
    RULED PAPER. The rules are set to 1.6rem, which is the body's own
    line-height -- so they run parallel to the text rather than cutting across
    it at some unrelated interval. That is the whole difference between reading
    as paper and reading as noise, and it is why this number must move if the
    body's line-height ever does.

    3.5% black. "Barely visible" is the brief and it is also the constraint:
    contrast sensitivity falls with age, so texture behind text costs this
    reader more than it costs a younger one. Anything heavier and the ruling
    starts competing with the words for a reader who has least to spare.

    It is a background-IMAGE, which is the axe trap documented on --ink-2
    above: axe will compare text against white here and pass pairs that fail.
    check_site.mjs samples the rendered pixel instead. Do not trust a green axe
    run on this page.

    FLAT, and not `background-attachment: fixed`. Both were tried and both are
    wrong for this. `fixed` sizes the background to the VIEWPORT, so the rules
    hold still while the words scroll past them -- the one thing paper never
    does -- and it leaves everything below the first screen unpainted. A
    vertical gradient stretched over a 4000px document is so gradual it just
    reads as an uneven screen. A sheet of paper is one colour.
  */
  background:
    repeating-linear-gradient(
      180deg,
      transparent 0,
      transparent calc(1.6rem - 1px),
      var(--rule) calc(1.6rem - 1px),
      var(--rule) 1.6rem
    ),
    linear-gradient(var(--paper-2), var(--paper-2));
  color: var(--ink);
  font-family: var(--font);
  /* 18px floor, matching the extension panel. This is a product feature. */
  font-size: 1.125rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* See the note on `.contact, kbd` below. A word longer than its column is a
     sideways scrollbar on the whole page, and this audience is the most likely
     to be running a large font size and the least likely to find the content
     that has been pushed off the right edge. */
  overflow-wrap: break-word;
}

/* Keyboard users must always be able to see where they are. Applied globally
   rather than per-component so nothing can be missed. */
:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--brand-d);
  color: #fff;
  padding: var(--gap-sm) var(--gap-md);
  z-index: 20;
  border-radius: 0 0 12px 0;
}
.skip:focus { left: 0; }

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

/*
  `--pad` is the page's side margin, and it is a token rather than a literal
  because three things have to agree on it: `.wrap`, `.bleed`, and the
  full-bleed bands behind `.ed`. When they disagreed, content sat almost
  against the viewport edge on a wide screen -- 24px of air beside a 1216px
  column, which reads as the text having nowhere to go.

  It grows with the viewport. On a phone 24px is already most of the width
  that is not text and taking more would cost a line of prose; past 62em there
  is room to spare and the page should use some of it as margin. The bands
  still run edge to edge underneath -- that is the point of them -- so what
  changes is where the WORDS start, not where the colour stops.
*/
:root { --pad: var(--gap-lg); }
@media (min-width: 62em) { :root { --pad: var(--gap-3xl); } }

.wrap {
  max-width: var(--frame);
  margin: 0 auto;
  padding: 0 var(--pad);
}

/*
  Breaks out of the column to full viewport width, for the dark promises
  block. Bounded by the viewport rather than an arbitrary number, so it
  cannot cause horizontal overflow.
*/
.bleed {
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding-left: max(var(--pad), calc(50vw - var(--frame) / 2 + var(--pad)));
  padding-right: max(var(--pad), calc(50vw - var(--frame) / 2 + var(--pad)));
}

section { padding: var(--gap-3xl) 0; }
section:first-of-type { padding-top: var(--gap-2xl); }

/*
  A centred reading column for the document-shaped subpages (/install/,
  /safety/, /privacy/, /activate/, /404). Without it, `<main>` filled the
  whole 76rem frame while every paragraph inside individually capped itself
  at `--measure` (40rem) — text hugging the left edge with a huge dead strip
  down the right on any screen wider than a laptop, which read as unfinished
  rather than as a design choice. This is the fix Griffin's own audit named:
  a page is not "spacious" just because there is unused space in it.

  WIDTH CORRECTED 2026-09-04, from 46rem to the measure itself. 46rem fixed
  `<main>` against the 76rem frame and then left the identical fault one level
  down: every paragraph still capped itself at 40rem and still sat flush left,
  so the column carried a 6rem dead strip down its right and read as having
  slipped sideways on any wide screen. Matching the column to `--measure` is
  what the paragraph rule was always asking for, and it keeps every left edge
  on the page (h1, h2, prose, lists, notes) on one line instead of two.

  Do not "fix" this by centring the paragraphs inside a wider column. That
  moves the prose 3rem right of the headings above it, which trades a dead
  strip for two ragged left edges, and left edges are what the eye actually
  tracks down a page.
*/
/*
  NARROWED TO WHAT IT IS FOR, 2026-09-08.

  `.doc` caps a page at the reading measure, which is right for a page that IS
  one column of prose -- /activate/ is the only one left. It was also on
  /install/, where every section is an `.ed` band asking for the full frame,
  and the two rules fought: the band lost, the page ran at 640px on a 1280px
  screen, and the prose inside it at ~360px. See the `.ed` block for the
  measurement.

  A page containing `.ed` sections must not carry `.doc`. The sections decide
  their own width, and `.ed-main` decides the line length.
*/
.doc { max-width: var(--measure); margin: 0 auto; }
.doc:has(.ed) { max-width: none; }

/*
  The editorial split. On wide screens the section's label and heading sit in
  a narrow left rail and the prose runs beside them, which is what stops the
  page being a single left-aligned column with half the frame empty — the
  thing that made the first version read as a blog rather than as a designed
  page.

  The rail is `align-self: start` so it never stretches, and the whole layout
  is confined to >=62em, where there is genuinely room for it; below that the
  two halves simply stack in source order.

  It USED to be sticky as well. Removed 2026-09-08 with the editorial grid's
  sticky headings, for the same reason and on the same instruction -- see the
  note beside `.ed-main > h1` further down.
*/
.split { display: grid; gap: var(--gap-lg) 0; }
.split-body > :last-child { margin-bottom: 0; }

@media (min-width: 62em) {
  .split {
    grid-template-columns: minmax(0, 21rem) minmax(0, 1fr);
    gap: 0 var(--gap-3xl);
    align-items: start;
  }
  .split-head { align-self: start; }
  /* The rail is ~300px. The full display size belongs to the h1 and to
     headings that have the whole frame; at this width it would wrap every
     heading to four lines and stop reading as a heading at all. */
  .split-head h2 {
    font-size: 2.125rem;
    max-width: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Sticky is not motion, but it is unexpected movement for someone who has
     asked for less of it. */
  .split-head { position: static; }
}

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

/*
  STICKY HEADER, 2026-09-04, on Griffin's instruction and modelled on
  twotone.au: the wordmark and the page tabs stay reachable from anywhere on
  the page. /privacy/ and /safety/ are long scrolls, and the way out of one
  should not be "scroll all the way back to the top".

  Solid white rather than a translucent blur. `backdrop-filter` costs a
  composited layer on every scroll frame, and this audience is
  disproportionately on old low-end hardware. The bar has one job.

  `z-index: 10` sits under `.skip` (20) on purpose: the skip link is the first
  thing a keyboard user reaches and must never appear behind the bar it is
  trying to skip past.
*/
/*
  A PILL AT REST, A BAR ONCE YOU MOVE. Rebuilt 2026-09-04.

  At the top of the page the header is a floating rounded-full card: large
  mark, large wordmark, room to breathe. The moment the page moves it docks --
  the corners square off, it spans the frame, the shadow goes, and the mark
  shrinks. Griffin's brief, and it earns its place for this audience rather
  than just looking current: the change is the page's way of saying "you have
  moved", which is the thing a nervous reader most often loses track of.

  Everything animated here is a plain CSS transition on a class toggle. No
  library. The two effects that prompted the GSAP question -- this and the
  footer seal -- are about fifty lines between them, and 24KB on the critical
  path is a real cost for a reader on an old phone. See docs/DESIGN.md.

  `z-index: 10` sits under `.skip` (20) on purpose: the skip link is the first
  thing a keyboard user reaches and must never appear behind the bar it is
  trying to skip past.
*/
/*
  FIXED, NOT STICKY. Changed 2026-09-04 after measuring the scroll behaviour,
  and this is the difference between the header feeling considered and feeling
  broken.

  A sticky element stays IN FLOW. This header shrinks from 112px to 57px when
  it docks, so with `position: sticky` the document got 72px shorter the moment
  it docked -- which pulled the scroll position up, which dropped it back below
  the 24px dock threshold, which un-docked it, which made the document tall
  again. Measured with a Playwright scroll sweep in 4px steps: asking for
  scrollY 32, 36, 40, 44 actually produced 26, 22, 24, 17, with the class
  flapping on and off and every bit of page content sliding 72px up and down
  underneath it.

  Out of flow, none of that can happen: the header's size is no longer part of
  the document's height, so shrinking it moves nothing. The space it would have
  occupied is held open by the padding on `.site-bar + .wrap` below, which is a
  CONSTANT -- it must never be animated or made to depend on the docked state,
  or the whole failure comes straight back.
*/
:root {
  /* The resting header's footprint: 1.125rem margin + card + 1.125rem margin.
     Kept in sync with the cards' own metrics at each breakpoint; measured in a
     real browser, not derived. */
  --bar-rest: 8.75rem;
  /* The reference's corner. Larger than --radius-lg (26px) on purpose: these
     are the only free-floating cards on the site, and the curve is most of
     what makes them read as detached objects rather than as a bar. */
  --bar-radius: 28px;
}

.site-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  /* Transparent at rest so the cards read as floating on the paper. It fills
     in on dock, which is also when content starts passing underneath it. */
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 260ms ease, border-color 260ms ease;
}

/* The header's reserved space. Only the wrap immediately after the bar, so no
   other `.wrap` on the page can pick this up by accident. */
.site-bar + .wrap { padding-top: var(--bar-rest); }
.site-bar.is-docked {
  background: #fff;
  border-bottom-color: var(--hairline);
}

/*
  TWO CARDS, NOT ONE BAR -- 2026-09-08, after mindmarket.com, measured off an
  1884px capture rather than eyeballed.

  Their header is two detached white cards on the page's coloured field: a wide
  one running x=140 to x=1424 holding the mark, the wordmark and the nav, and a
  narrow one at x=1452 to x=1736 holding one action. Both 104px tall, 18px from
  the top, ~28px corners. The gap between them is the whole idea -- it says the
  action is not another nav item.

  ## Why it suits this site better than the pill it replaces

  The resting state used to be a single rounded-full pill capped at 66rem,
  centred in a frame of 76rem. It floated, but it floated alone, and the shape
  meant the nav and the wordmark shared one container with nothing separating
  identity from action. There was also no action: this header has never had a
  CTA on it, on a site whose entire job is to get someone to install and buy.
  A sticky nav CTA is the one structural thing the hero-centric landing pattern
  asks for that this page did not have.

  ## What is taken and what is not

  Taken: the two-card split, the 104px height, the 28px corner, the 18px top
  margin, the gap, and the nav's size -- theirs is 21px where ours was 16px,
  and for a 65-80 reader bigger is simply better. Their wordmark is ~35px,
  which is what `.mark-word` was already set at.

  Not taken: the green circle at the right end of their main card. It is a
  hamburger -- a menu trigger for nav that does not fit. Four items fit here,
  so building the circle would mean drawing a control that does nothing, and an
  icon-only round button is the exact thing this audience cannot read. The
  second card's avatar circle is the shape that survives, and it holds the OPA
  mark, which their avatar is doing too.

  ## Geometry

  `--frame` and `--pad` rather than the reference's own margins: the cards line
  up with the content column below them, which is the invariant
  site-layout.spec.js checks and the reason the wordmark does not drift away
  from the page it belongs to. The width no longer changes on dock -- only the
  margin, the gap, the corner and the shadow do -- so there is one less
  transition to fight the layout.
*/
.site-head {
  display: flex;
  /* `nowrap`: a sticky bar that grows to two rows when the nav does not fit
     eats a third of a phone screen and moves the content under it as you
     scroll. The nav scrolls horizontally instead. */
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 1.75rem;
  max-width: var(--frame);
  margin: 1.125rem auto;
  padding-inline: var(--pad);
  transition:
    gap 340ms cubic-bezier(.4, 0, .2, 1),
    margin 340ms cubic-bezier(.4, 0, .2, 1),
    padding 340ms cubic-bezier(.4, 0, .2, 1);
}
/*
  Docked: the two cards butt together into one white bar spanning the frame,
  separated by a hairline. Everything that made them read as objects -- the
  corner, the lift, the gap, the inset -- goes at once, which is what tells you
  you have moved.
*/
.site-bar.is-docked .site-head {
  gap: 0;
  margin-block: 0;
  padding-inline: 0;
}

.bar-card {
  display: flex;
  align-items: center;
  background: #fff;
  border: 1px solid var(--hairline);
  border-radius: var(--bar-radius);
  box-shadow: var(--lift-2);
  /*
    The left padding is not free, and the reason survives the shape change: the
    mark is a circle sitting inside a rounded corner, so at a 28px radius it
    escapes the curve unless the inset clears it. 1.5rem against a 1.05rem
    padding-block does; the old 0.7rem did not.
  */
  padding: 1.05rem 1.5rem;
  transition:
    padding 340ms cubic-bezier(.4, 0, .2, 1),
    border-radius 340ms cubic-bezier(.4, 0, .2, 1),
    border-color 260ms ease,
    box-shadow 260ms ease;
}
/* Identity and navigation. Takes the slack, and holds the two ends apart. */
.bar-card-main {
  flex: 1 1 auto;
  min-width: 0;
  justify-content: space-between;
  gap: var(--gap-lg);
}
/* The action. Content width, never squeezed. */
.bar-card-cta {
  flex: 0 0 auto;
  padding-inline: 1.25rem;
  border-left: 1px solid transparent;
}

.site-bar.is-docked .bar-card {
  border-radius: 0;
  border-color: transparent;
  box-shadow: none;
  padding-block: 0.35rem;
}
.site-bar.is-docked .bar-card-cta { border-left-color: var(--hairline); }

/*
  Anything the browser scrolls to has to clear the bar, or it lands underneath
  it. `#main` covers the skip link, which is the case that matters most: a
  keyboard user pressing "Skip to main content" and arriving at content hidden
  behind a header is worse off than before the link existed.
*/
#main, :target { scroll-margin-top: 5.5rem; }

.mark {
  display: inline-flex;
  align-items: center;
  gap: 0.85rem;
  color: var(--ink);
  text-decoration: none;
  min-height: 44px;
}
/*
  The drawn OPA face in the white disc the artwork frames it with, beside the
  product name. The face is SVG traced from brand/opa-source.png by
  scripts/make_brand_assets.py -- the same file the extension's popup and
  on-page panel use, so someone who has seen the product recognises it here.

  The ring is not decoration. The disc is white and the card is white, so
  without it the badge has no edge at all.

  68px at rest, 44px docked. The artwork is a finely traced drawing of a face,
  and below about 40px it stops being a face and becomes a smudge -- a bad
  outcome for the one mark a nervous user is asked to recognise and trust.
  Docked is 44px rather than smaller for exactly that reason, and it happens
  to be the touch floor too.

  The reference's mark is 46px in a 104px card. Ours is 68px in the same box,
  because theirs is a two-stroke logotype and ours is a portrait.
*/
.mark-badge {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  background: #fff;
  box-shadow: inset 0 0 0 1.5px var(--ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  overflow: hidden;
  transition: width 340ms cubic-bezier(.4, 0, .2, 1), height 340ms cubic-bezier(.4, 0, .2, 1);
}
.site-bar.is-docked .mark-badge { width: 44px; height: 44px; }
.mark-badge img { width: 100%; height: 100%; display: block; }

/*
  Set in type, not drawn. The traced wordmark that used to sit here was
  retired 2026-09-04: at 19-23px it was measurably harder to read than the
  same word in Source Sans 3 Bold, and legibility is the one thing a wordmark
  cannot trade away for an audience that is 65-80. It also scales with the
  docking header for free, where an <img> needed its width and height rewritten.

  2.1rem is 33.6px, which is the reference's own wordmark size to within a
  pixel. That was luck rather than planning, but it means the size needed no
  change to match.
*/
.mark-word {
  font-size: 2.1rem;
  font-weight: 700;
  letter-spacing: 0.005em;
  line-height: 1;
  color: var(--ink);
  transition: font-size 340ms cubic-bezier(.4, 0, .2, 1);
}
.site-bar.is-docked .mark-word { font-size: 1.35rem; }

.site-nav ul {
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: var(--gap-xs);
  max-width: none;
}
.site-nav li { margin: 0; }
/*
  1.25rem, up from 1rem on 2026-09-08. The reference sets its nav at 21px and
  ours was at 16px -- and of all the places to take a reference's larger
  number, a nav read by someone who is 75 is the least arguable one.
  `--ink-2` rather than `--ink-3` keeps it comfortably past 7.0 on white.
*/
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 1.1rem;
  border-radius: 999px;
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--ink-2);
  text-decoration: none;
  white-space: nowrap;
  transition: min-height 340ms cubic-bezier(.4,0,.2,1), font-size 340ms cubic-bezier(.4,0,.2,1);
}
.site-nav a:hover { background: var(--brand-wash); color: var(--brand-d); }
/* Current page is marked by fill and weight, never by colour alone. */
.site-nav a[aria-current="page"] { background: var(--ink); color: #fff; }
.site-bar.is-docked .site-nav a { font-size: 1.0625rem; }

/*
  THE HEADER ACTION, which this site did not have.

  The whole card is the link, so the disc beside the label is never a target on
  its own -- an icon-only round button is exactly what this audience cannot
  read, and the reference's own avatar is decoration beside a worded action for
  the same reason. Label first, disc second, matching their reading order.

  The disc is `--accent`, which is the third place that red appears: the
  stamped day count in the hero, this, and the wax seal at the foot of the
  page. `--ink` would have been safer and would have said nothing.

  IT HOLDS AN ARROW, NOT THE MARK, and the first pass had it the other way
  round. The mark is a fine-line portrait -- black strokes on a white face --
  and DESIGN.md is explicit that it turns to mush below about 40px and needs a
  reduced form rather than a smaller one. Dropped into a 52px red disc it went
  further than mush: the white face fill fought the red, and at the 44px phone
  size it read as a dark smudge with no edge. An arrow at a 2.4px stroke is
  legible at both sizes and says what pressing the card does, which the mark
  did not.
*/
.bar-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.85rem;
  min-height: 44px;
  color: var(--ink);
  font-size: 1.25rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  transition: font-size 340ms cubic-bezier(.4,0,.2,1);
}
.bar-cta:hover .bar-cta-word { text-decoration: underline; text-underline-offset: 4px; }
.bar-cta-disc {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  flex: none;
  background: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: width 340ms cubic-bezier(.4, 0, .2, 1), height 340ms cubic-bezier(.4, 0, .2, 1);
}
/*
  The arrow inside it is Griffin's, from 2026-09-10 -- drawn twice on
  brand/numerals-source.png and traced by scripts/make_numerals.py, the same
  two-take flick as the day count and the signature on the founder card.

  It replaced a 2.4px geometric arrow, which was the right shape and the wrong
  instrument in exactly the way `make_handwriting.py` describes: a constant
  stroke says nothing about the hand that drew it. The disc stays a TRUE
  CIRCLE -- Griffin drew his on a wobbly one and was explicit that only the
  ink was being lifted.

  `width` with `height: auto`, not 46% of both: the drawn arrow is 1.66:1, and
  a square box would have letterboxed it down to a third of the disc. 66% is
  the proportion he drew -- 496px of arrow across a 717px disc -- and it is
  the reason this reads at the 44px phone size where a smaller one would not.
*/
.bar-cta-disc svg {
  width: 66%;
  height: auto;
  display: block;
  /* Paper, not white: the arrow is the same knockout the day count uses, and
     #f2ede4 on #a83c17 is the 5.42 that pairing already measures. */
  color: var(--ground);
}
.site-bar.is-docked .bar-cta-disc { width: 40px; height: 40px; }
.site-bar.is-docked .bar-cta { font-size: 1.0625rem; }

/*
  Reduced motion: the header still docks, because docking is a layout state
  rather than an animation and the alternative is a card sitting over the
  content scrolling under it. What goes away is the tweening between states.
*/
@media (prefers-reduced-motion: reduce) {
  .site-bar,
  .site-head,
  .bar-card,
  .mark-badge,
  .mark-word,
  .bar-cta,
  .bar-cta-disc,
  .site-nav a { transition: none; }
}

/*
  Between the phone and the full two-card header. The gap and the inset shrink
  before anything is taken away, because the two cards are the design and
  losing one of them is the last resort rather than the first.
*/
@media (max-width: 62em) {
  .site-head { gap: 1rem; }
  .bar-card { padding-inline: 1.1rem; }
  .site-nav a { font-size: 1.125rem; padding-inline: 0.8rem; }
  .bar-cta { font-size: 1.125rem; }
  .bar-cta-disc { width: 46px; height: 46px; }
}

@media (max-width: 40em) {
  /*
    The nav used to take `width: 100%` and drop to a second row. That was fine
    for a header that scrolled away and is not fine for one that does not: two
    rows is ~110px of a 667px iPhone SE screen, permanently. It stays on the
    mark's row and scrolls sideways if it ever has to.
  */
  .site-nav { overflow: hidden; min-width: 0; }
  /*
    ONE BAR ON A PHONE, and it starts docked-shaped.

    The floating inset is a desktop luxury; on a 390px bar already holding a
    54px mark, a wordmark and two links it costs real width. The second card
    is not hidden but merged: it keeps its place at the right-hand end, held
    apart by a rule instead of by a gap, so the sticky CTA that is the point
    of it survives on the device most likely to see this page.
  */
  .site-head {
    max-width: none;
    margin: 0;
    padding-inline: 0;
    gap: 0;
  }
  .bar-card {
    padding: 0.35rem var(--gap-md);
    border-radius: 0;
    border-color: transparent;
    box-shadow: none;
    background: #fff;
  }
  .bar-card-main { gap: var(--gap-md); }
  .bar-card-cta {
    border-left-color: var(--hairline);
    padding-inline: 0.75rem;
  }
  .site-bar { background: #fff; border-bottom-color: var(--hairline); }
  /* 54px mark + 2x0.35rem padding + border, and no margin: the phone header
     is docked-shaped from the start. Measured at 67px in Chromium; 4.25rem is
     68px, so the reserve clears it by a pixel rather than by nine. */
  :root { --bar-rest: 4.25rem; }
  .mark { gap: 0.55rem; }
  .mark-badge { width: 54px; height: 54px; }
  .site-bar.is-docked .mark-badge { width: 42px; height: 42px; }
  .mark-word { font-size: 1.45rem; }
  .site-bar.is-docked .mark-word { font-size: 1.2rem; }
  .site-nav ul {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 0.2rem;
  }
  .site-nav ul::-webkit-scrollbar { display: none; }
  .site-nav a,
  .site-bar.is-docked .site-nav a {
    padding-inline: 0.5rem;
    font-size: 1rem;
  }
  /*
    TWO NAV ITEMS ON A PHONE, not four. Measured: at 390px the mark, the
    wordmark, four links at the 16px floor and the action disc come to about
    420px of content in a 390px bar, so the nav scrolled and "Privacy" sat
    off the right edge behind no affordance at all -- worse than not offering
    it, because it looked like a bar that had broken.

    Home goes because the wordmark beside it already links home, and Privacy
    goes because it is the least likely of the four to be reached for here.
    Neither becomes unreachable: the footer carries all four on every page,
    and check_site.mjs asserts that footer exists. The alternative is the
    reference's own answer, a hamburger, and a menu behind an icon-only round
    button is the last thing to give a reader who is 75.
  */
  .site-nav li:nth-child(1),
  .site-nav li:nth-child(4) { display: none; }
  /*
    The disc alone, at the touch floor. The word is what it costs: at 390px
    there is not room for "Get OPA" beside two nav items, and the nav is what
    a reader is more likely to be reaching for. The link keeps its accessible
    name from the visually-hidden span in the markup, so nothing is lost to a
    screen reader -- only to the eye, which has the disc instead.
  */
  .bar-cta { gap: 0; }
  .bar-cta-word { display: none; }
  .bar-cta-disc,
  .site-bar.is-docked .bar-cta-disc { width: 44px; height: 44px; }
}

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

/*
  The eyebrow. Uppercase and tracked-out so it reads as a label rather than a
  sentence — but 16px, not the 11-13px this pattern usually uses, because
  nothing carrying meaning on this site goes below the floor.
*/
.eyebrow {
  display: block;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 var(--gap-md);
}
.eyebrow-brand { color: var(--brand-d); }

h1 {
  /*
    Fluid, 2026-09-04. This was a flat 4.75rem, and the landing page escaped it
    only because `.hero h1` declares a clamp of its own. Every other page did
    not: /install/, /safety/, /privacy/ and /404 all set their headline at 76px
    on a 390px phone, where "Add OPA to Chrome." took three lines and 233px of
    a 667px screen. Nothing about that was visible in the stylesheet; it turned
    up as the install button being pushed off the bottom of an iPhone SE once
    the header became sticky.

    Same shape as the hero's clamp, one step smaller at both ends, so the two
    headings keep their relationship at every width.
  */
  font-size: clamp(2.5rem, 1.6rem + 3.9vw, 4.75rem);
  font-weight: 700;
  line-height: 0.98;
  letter-spacing: -0.03em;
  margin: 0 0 var(--gap-lg);
  /* 18ch was wrapping "Add OPA to Chrome." for no reason; see the note on h2. */
  max-width: 24ch;
  text-wrap: balance;
}

/*
  `max-width: 22ch` was here and it was the single worst thing on the site.

  "Opa means grandpa in Dutch." is 27 characters, so it broke across two lines
  in the middle of a section with 800px of unused width beside it -- and the
  same rule did the same thing to headings on every other page. A measure cap
  is right for a PARAGRAPH, where long lines are genuinely hard to track back
  from. A heading is two seconds of reading; capping it just makes the page
  look like it ran out of room.

  Fluid rather than a flat 2.75rem, so it holds its relationship with the h1's
  clamp instead of staying 44px on a phone. Leading and tracking tighten as it
  grows, which is what makes a large heading read as one object.
*/
h2 {
  /* The reference runs its section headings at 56-64px (4.4-5vw). This tops
     out at 60px, one clear step below the hero so the two keep their order. */
  font-size: clamp(1.875rem, 1rem + 3vw, 3.75rem);
  font-weight: 700;
  line-height: 1.0;
  letter-spacing: -0.03em;
  margin: 0 0 var(--gap-md);
  max-width: 30ch;
  text-wrap: balance;
}

h3 {
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: 0;
  margin: 0 0 var(--gap-xs);
}

.lede {
  font-size: 1.375rem;
  line-height: 1.45;
  color: var(--ink-2);
  /* 44ch, not the 32ch this started at — at 24px a 32ch measure is only about
     420px, which on a wide screen reads as a cramped column rather than as a
     deliberate one. */
  max-width: 44ch;
  margin-bottom: var(--gap-xl);
}

p { margin: 0 0 var(--gap-md); max-width: var(--measure); }

.dim { color: var(--ink-2); }
.small { font-size: 1rem; }

a { color: var(--brand-d); text-underline-offset: 3px; }
a:hover { text-decoration-thickness: 2px; }

ul, ol { padding-left: 1.25rem; margin: 0 0 var(--gap-md); max-width: var(--measure); }
li { margin-bottom: var(--gap-sm); }

strong { font-weight: 700; }

kbd {
  font: inherit;
  font-weight: 600;
  background: var(--ground);
  border: 1px solid var(--hairline);
  border-radius: 7px;
  padding: 0.12em 0.45em;
  /*
    `white-space: nowrap` was here to stop a key label splitting across lines,
    and it also stopped `chrome://extensions` from ever breaking. At a 32px
    root that single token is 351px inside a 279px column, so /install/ scrolled
    sideways while every other page had been fixed. Short labels like Ctrl fit
    on a line and never break anyway, so the guard was only ever protecting the
    case it broke. `overflow-wrap: anywhere` above does the real work.
  */
}

code {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 0.9375em;
  background: var(--ground);
  border-radius: 6px;
  padding: 0.12em 0.4em;
  overflow-wrap: anywhere;
}

hr { border: 0; border-top: 1px solid var(--hairline); margin: var(--gap-2xl) 0; }

/* ---------- the statistic ---------- */

/* ---------- surfaces ---------- */

.card {
  background: var(--surface);
  border-radius: var(--radius);
  padding: var(--gap-xl);
  box-shadow: var(--lift-2);
}
.card > :last-child { margin-bottom: 0; }

.panel {
  background: var(--ground);
  border-radius: var(--radius);
  padding: var(--gap-xl);
  margin: var(--gap-lg) 0;
}
.panel > :last-child { margin-bottom: 0; }

/*
  A note is for something the reader must not miss — usually an honest
  limitation. Marked by a heavy left rule and a bold lead line, not by colour
  alone, so it survives a monochrome print or a colour-blind reader.
*/
.note {
  border-left: 5px solid var(--brand);
  background: var(--brand-wash);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: var(--gap-lg) var(--gap-xl);
  margin: var(--gap-xl) 0;
}
.note > :last-child { margin-bottom: 0; }
.note-title { font-weight: 700; margin-bottom: var(--gap-xs); }

/* ---------- the dark block ---------- */

/*
  Full-bleed near-black. This is where the safety promises live, because for
  this audience they ARE the product, and giving them their own visual
  territory says that louder than a heading can.

  The failure mode of dark sections is grey-on-grey. It is avoided here by
  never using --on-night-2 for anything longer than a short line, and by the
  body colour being 19:1 rather than a fashionable 60% white.
*/
.on-night {
  background: var(--night);
  color: var(--on-night);
  border-radius: var(--radius-lg);
  padding-top: var(--gap-3xl);
  padding-bottom: var(--gap-3xl);
}
.on-night h2, .on-night h3, .on-night strong { color: #fff; }
.on-night .eyebrow { color: var(--on-night-2); }
.on-night p { color: var(--on-night-2); }
.on-night a { color: #9fc2ff; }
.on-night :focus-visible { outline-color: #9fc2ff; }

/* ---------- promises ---------- */

.promises {
  list-style: none;
  padding: 0;
  margin: 0;
  max-width: none;
  display: grid;
  gap: var(--gap-md);
}
@media (min-width: 52em) {
  .promises { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gap-lg); }
}

.promises li {
  display: flex;
  gap: var(--gap-md);
  align-items: flex-start;
  margin: 0;
  padding: var(--gap-lg);
  border-radius: var(--radius);
  background: var(--night-2);
  border: 1px solid var(--night-line);
}
/* The tick is decorative — the sentence carries the whole meaning, so nothing
   here depends on seeing a colour or a shape. */
.promises svg { flex: none; margin-top: 0.15em; }
.promises p { margin: 0; }
.promise-title {
  display: block;
  font-weight: 700;
  color: #fff;
  margin-bottom: 0.15rem;
}

/* On a light background (used on /safety/) the same component inverts. */
.promises-light li {
  background: var(--surface);
  border: 1px solid var(--hairline);
  box-shadow: var(--lift-1);
}
.promises-light .promise-title { color: var(--ink); }
.promises-light p { color: var(--ink-2); }

/* ---------- calls to action ---------- */

/*
  Sized well past the 44px touch floor because the audience includes people
  with reduced dexterity, and because a timid CTA on a page asking someone
  nervous to try something is the wrong signal. One primary per page.
*/
.cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  background: linear-gradient(180deg, var(--brand) 0%, var(--brand-dd) 100%);
  color: #fff;
  text-decoration: none;
  font-size: 1.1875rem;
  font-weight: 700;
  letter-spacing: 0;
  padding: 1.05rem 2rem;
  min-height: 60px;
  border-radius: 999px;
  box-shadow: 0 12px 28px rgba(18,49,79,.22);
  transition: transform .12s ease, background .15s ease, box-shadow .15s ease;
}
.cta:hover { box-shadow: 0 16px 32px rgba(18,49,79,.28); }
.cta:active { transform: scale(.985); }
.cta svg { flex: none; }

.cta-2 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  color: var(--ink);
  border: 2px solid rgba(100,116,139,.24);
  text-decoration: none;
  font-size: 1.1875rem;
  font-weight: 700;
  letter-spacing: 0;
  padding: 1.05rem 2rem;
  min-height: 60px;
  border-radius: 999px;
  transition: border-color .15s ease, transform .12s ease;
}
.cta-2:hover { border-color: var(--ink); }
.cta-2:active { transform: scale(.985); }

.on-night .cta-2 { background: transparent; color: #fff; border-color: var(--night-line); }
.on-night .cta-2:hover { border-color: #fff; }

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-md);
  margin: 0 0 var(--gap-md);
  max-width: none;
}

/*
  `text-align: center` does not reach flex children, so inside the two centred
  sections on this site the button packed itself against the left edge while
  every other line above it was centred. Nothing in the markup asked for that;
  `justify-content` simply defaulted to `flex-start` and won. The document
  pages are deliberately not listed here: their prose is left-aligned, so a
  left-aligned button is correct there.
*/
.demo .cta-row,
.install-hero .cta-row { justify-content: center; }


/* ---------- chips ---------- */

/* The three short promises in the hero. Pills rather than a list, so they
   read as a row of facts and not as body copy to be waded through. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
  list-style: none;
  padding: 0;
  margin: 0 0 var(--gap-xl);
  max-width: none;
}
.chips li {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.55rem 1.1rem 0.55rem 0.85rem;
  border-radius: 999px;
  background: var(--ground);
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}
.chips svg { flex: none; }

/* ---------- numbered steps ---------- */

/*
  Editorial numbering: the figure sits in its own rail and the text keeps a
  clean left edge, rather than the number being a bullet the paragraph wraps
  around. These are instructions someone reads off a second screen while
  doing them.
*/
.steps { list-style: none; padding: 0; margin: var(--gap-xl) 0 0; max-width: none; counter-reset: step; }
.steps > li {
  counter-increment: step;
  display: grid;
  grid-template-columns: 3.5rem 1fr;
  gap: 0 var(--gap-lg);
  padding: var(--gap-lg) 0;
  border-top: 1px solid var(--hairline);
  margin: 0;
}
.steps > li:last-child { border-bottom: 1px solid var(--hairline); }
.steps > li::before {
  content: counter(step, decimal-leading-zero);
  grid-row: span 2;
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--brand-d);
  font-variant-numeric: tabular-nums;
  padding-top: 0.2rem;
}
.steps h3 { margin-top: 0; }
.steps p:last-child { margin-bottom: 0; }

@media (max-width: 34em) {
  .steps > li { grid-template-columns: 2.5rem 1fr; gap: 0 var(--gap-md); }
}

/* ---------- three-up rows ---------- */

/*
  auto-fit rather than a fixed three columns behind a viewport media query.
  These sit inside .split-body, which is much narrower than the viewport, so a
  `min-width: 52em` rule would give three ~200px columns at 1280 and wrap
  every heading onto three lines. auto-fit responds to the space the grid
  actually has, which is the only measurement that matters here.
*/
.rows {
  display: grid;
  gap: var(--gap-lg);
  margin: var(--gap-xl) 0 0;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.row-item { padding: var(--gap-xl); border-radius: var(--radius); background: var(--ground); }
.row-item > :last-child { margin-bottom: 0; }
.row-num {
  display: block;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  color: var(--brand-d);
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--gap-md);
}

/* ---------- the schematic ---------- */

/*
  The one picture on the site: a diagram of where the panel sits relative to
  the form. Inline SVG, so it costs no request, cannot arrive late or fail,
  and scales rather than blurring at 200% zoom. Deliberately a diagram and not
  a screenshot — a screenshot of software a reader cannot yet install shows
  them something they cannot have.
*/
.figure { margin: var(--gap-xl) 0 0; max-width: none; }

.figure svg {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  box-shadow: var(--lift-3);
}
.figure figcaption {
  margin-top: var(--gap-md);
  color: var(--ink-2);
  font-size: 1rem;
  max-width: var(--measure);
}

/* ---------- Q&A, and headed items generally ---------- */

/*
  A RUN OF HEADED ITEMS, ACROSS THE FRAME -- 2026-09-09.

  Griffin: "in the sections where it has multiple headers and body with
  dividers in between them find a better way of organising that information
  whether it be a list or a different organisation ... so it fits better on the
  screen possibly stretches to the edge of the screen."

  Two places did this and both did it the same wrong way: a stack of headings
  each followed by a paragraph, ruled apart, inside a 40rem column. On a 1440
  screen that is a 640px ribbon of text with 500px of empty paper beside it and
  a scroll that never ends -- /install/'s six troubleshooting answers, and
  /privacy/'s tail, which stacked "If you email us", "Children" and "If this
  page changes" under a heading that named only the first of the three.

  ## Why a grid and not a taller column

  A rule between two items says "these are separate". Fine for two. By the
  sixth, the reader is being told six times in a row that things are separate
  while scrolling past all of them looking for one, and the divider has stopped
  being information. Set side by side, the separation is carried by the LAYOUT
  and each item is a thing you can look at rather than a thing you scroll
  through. Six answers become one screen.

  `auto-fit` with a `min(100%, 20rem)` floor rather than a breakpoint: one
  column on a phone, two on a laptop, three on a wide monitor, and the `min()`
  is what stops the track demanding 20rem inside a 320px viewport, which is a
  horizontal scrollbar at the width the layout spec checks hardest.

  ## The rule moved from between the items to the top of each

  Same hairline, same idea, but a rule that caps each item reads as the top of
  a card. It also means the first item gets one too, so nothing is a special
  case -- the old `:first-of-type` reset existed only because a divider before
  the first thing is a divider between it and nothing.
*/
.qa,
.ed-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--gap-xl) var(--gap-2xl);
  margin: var(--gap-xl) 0 0;
  /* Full frame. The whole point is the width the old column was not using. */
  max-width: none;
}

/*
  Each item is one grid cell, which is what the `div` wrappers in the markup
  are for. A bare `dt`/`dd` pair cannot be a cell -- grid makes every child an
  item, so the term and its description would land in different columns and the
  list would read as two unrelated lists. HTML5 allows `div` inside `dl` for
  exactly this.
*/
.qa > div,
.ed-grid > div {
  padding-top: var(--gap-md);
  border-top: 1px solid var(--hairline);
}

.qa dt,
.ed-grid h3 {
  font-weight: 700;
  font-size: 1.1875rem;
  line-height: 1.35;
  margin: 0;
}
.qa dd,
.ed-grid p {
  margin: var(--gap-xs) 0 0;
  max-width: none;
  color: var(--ink-2);
}

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

/*
  Long unbreakable tokens: an email address, a `chrome://` URL.

  At the browser's default root size they fit and nobody notices. At a 32px
  root, which is what a reader who has turned their font size up actually has,
  `griff.devs@gmail.com` is 387px of a single word inside a 375px viewport, and
  every document page gains a horizontal scrollbar. No `max-width` helps: the
  box is not what is too wide, the word is. `break-word` on the body catches
  the general case; the two elements that are always one long token get
  `anywhere`, which is also allowed to break when working out min-content
  width, and is therefore the one that holds inside a flex or grid track.
*/
.contact, kbd { overflow-wrap: anywhere; }

.contact {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-weight: 700;
  font-size: 1.25rem;
  letter-spacing: 0;
}

/* ---------- the editorial grid ---------- */

/*
  THE LAYOUT SYSTEM, added 2026-09-04 after becomearoboticist.com, and after
  Griffin pointed at the founder section and said it was formatted badly. It
  was, and so was everything shaped like it.

  What was actually wrong is worth naming, because it is not a matter of taste
  and it repeated on every page: nothing shared a gridline. The heading sat at
  one left edge, its body text began at another 270px further in, the figure's
  caption wrapped to two lines in a column too narrow to hold it, and the whole
  block stopped two thirds of the way across the frame leaving a wedge of dead
  space at the bottom right. Each of those is small. Together they read as a
  page nobody laid out.

  The fix is one rule with no exceptions: **a section has two columns, and
  everything in a column shares its left edge.** The narrow column carries the
  section label and any figure or spec block; the wide column carries the
  heading and the prose. Because the heading and the prose are siblings in the
  same column, they cannot drift apart.

  Below 56em the two columns stack in source order, which puts the label and
  the figure above the heading -- the right reading order on a phone.
*/
/*
  REBUILT 2026-09-08. The version this replaces is described in the block
  above, and its reasoning was sound in isolation -- everything in a column
  shares a left edge. What it did on a real screen was another matter, and
  Griffin named all three symptoms: the numbered label pushes the content
  right, everything sits in a strip down the middle, and it does that on every
  page.

  ## The measurement, because it is worse than it sounds

  On a 1280px screen the install page used 640px of it. Two rules were
  fighting and the page got the worse half of each: `.doc` caps a document at
  `--measure` (40rem) and `main` on that page carries it, while `.ed` asks for
  64rem and cannot have it. Inside that 640px the 15rem rail took 240px, so
  the prose ran in a ~360px column -- narrower than a phone -- under a header
  and above a footer that both span the full 76rem frame. That mismatch is the
  thing that reads as unfinished: the furniture is wide and the content is a
  ribbon.

  ## What replaces it

  Two changes, and the second is what actually fixes the page.

  1. THE LABEL SITS ABOVE THE HEADING, not beside it. It was a rail because a
     rail looks composed in a screenshot; it costs 15rem of every section for
     four words, and it is the reason the content began a third of the way
     across. Above the heading it costs one line, the heading starts at the
     frame's left edge, and on a phone -- where the rail stacked anyway -- the
     reading order is unchanged.

  2. SECTIONS ARE FULL-BLEED BANDS THAT ALTERNATE. This is the device taken
     from mindmarket.com, and it is the one that answers "fixed in the middle".
     A single continuous ground with hairlines between sections gives the eye
     nothing to measure the page by, so a narrow column in the centre of it
     looks stranded. Bands that run edge to edge give the page a spine: the
     content can then be any width it likes, because the SECTION is full
     width and the reader sees structure rather than a floating strip.

     The bleed is a pseudo-element rather than a wrapper, so it works on
     sections already nested inside `.wrap` and no page's markup had to move
     for it. `inset-inline: -50vw` with `width: 100vw` is the standard trick;
     `left: 50%; margin-left: -50vw` would introduce a horizontal scrollbar on
     any viewport that already has one.

  Prose still ends at a readable measure -- see `.ed-main` below, which is
  where the line length is now decided. The width of the SECTION and the width
  of the LINE are different questions, and conflating them is what produced
  the old layout.
*/
.ed {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gap-md);
  padding-block: var(--gap-3xl);
  max-width: var(--frame);
  margin-inline: auto;
}

/*
  The band. Painted behind the section, edge to edge, on alternate sections.

  `--surface` against the ruled paper rather than a second paper tone: the
  ruling is 3.5% black, and a band that also carried it would read as a
  printing fault. A clean white block on ruled paper reads as a card laid on
  it, which is the right metaphor here and costs no contrast -- every pair on
  white was already measured by check_contrast.mjs.
*/
.ed::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline: -50vw;
  width: 100vw;
  margin-inline: auto;
  z-index: -1;
  background: transparent;
}

/*
  `100vw` INCLUDES THE SCROLLBAR, and every page here has one.

  So the band is a dozen pixels wider than the space actually available, and
  that overflow is real: `site-layout.spec.js` caught it twice on the first
  run -- the document grew a horizontal scrollbar, which changed the document
  width, which broke both "scrolling does not resize the document" and
  "reflows and stays centred at every width" in WebKit.

  `overflow-x: clip` rather than `hidden`, and the difference is the whole
  reason this works: `hidden` makes the element a scroll container, which
  would break `position: sticky` on the header. `clip` refuses the scroll
  without becoming one. (Section headings were sticky too when this was
  written; they are not any more, but the header still is and the rule still
  matters.)

  Set on `html`, not `body`. On `body` it makes the body a scroll container
  in its own right, and `document.documentElement.scrollHeight` then measures
  a different box -- which the scroll test caught as a 373px jump.
*/
html { overflow-x: clip; }
.ed:nth-of-type(even)::before { background: var(--surface); }

/*
  The hairline goes with the rail. A band boundary and a rule on top of it is
  one edge drawn twice. It survives only between two consecutive UNbanded
  sections, which happens on a page that opens with a hero that is not an
  `.ed`.
*/
.ed:nth-of-type(odd) + .ed:nth-of-type(odd) { border-top: 1px solid var(--hairline); }

@media (min-width: 56em) {
  /*
    THE SPLIT, AND WHY IT IS NOT THE RAIL AGAIN.

    First attempt at this rebuild put the label above the heading and ran the
    whole section as one left-aligned column. The bands were right and that
    was wrong: prose caps at ~72 characters, so on a 1280px screen every
    section ended two-thirds of the way across and left a 30rem wedge of
    nothing down the right. DESIGN.md names that exact failure -- "a page is
    not 'spacious' just because there is unused space in it" -- and it is what
    the previous layout was already guilty of.

    The wedge only closes if something occupies the right, so the heading goes
    left and the prose goes beside it. The difference from the rail this
    replaces is what the narrow column CARRIES: 15rem of eight-point label,
    versus 24rem holding the section heading at full size. One is a margin
    with a word in it; the other is half a composition. The label sits above
    its heading inside that column, which is what Griffin actually objected
    to -- the number was pushing the content right, and now it pushes nothing.

    24rem + 46rem + the gap fills the 76rem frame, so the section, the header
    and the footer finally share an edge on both sides.
  */
  .ed {
    /*
      20rem, down from 24rem on 2026-09-08. The heading column was sized when
      the page had 24px of side padding; widening `--pad` to 5rem took 6rem out
      of the frame and the body column absorbed all of it, dropping to about 40
      characters -- a narrower line than the phone layout. The heading gives
      the 4rem back: it only ever holds two or three words at 2.125rem, and it
      had the room to spare.
    */
    grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
    gap: 0 var(--gap-2xl);
    align-items: start;
  }

  /*
    A SECTION WHOSE BODY IS A SCANNABLE LIST TAKES THE WHOLE FRAME.

    `.ed-cols` splits a list into two columns so a reader looking for the one
    entry that applies to them does not walk the whole thing. Nested inside the
    heading split it was two columns of ~20rem -- about 30 characters each,
    which is a newspaper classified, not something a 75-year-old reads
    comfortably.

    So these sections drop the heading column and put the heading back above,
    which is the right shape for a list anyway: there is no argument being
    built beside it, just a thing to scan. The list then gets the full 66rem
    and its two columns are ~31rem each.
  */
  .ed:has(.ed-cols) { grid-template-columns: minmax(0, 1fr); }
  .ed:has(.ed-cols) .ed-main { display: block; }
  /* Same reasoning for a run of headed items -- see `.ed-grid`. */
  .ed:has(.ed-grid) { grid-template-columns: minmax(0, 1fr); }
  .ed:has(.ed-grid) .ed-main { display: block; }
  .ed:has(.qa) { grid-template-columns: minmax(0, 1fr); }
  .ed:has(.qa) .ed-main { display: block; }

  /*
    The heading joins the label in the left column. It is `.ed-main`'s child,
    not `.ed-side`'s, on every page -- moving it in the markup would have
    meant editing five pages, and `display: contents` lets the grid see
    through `.ed-main` to place them instead.
  */
  .ed-main { display: contents; }
  .ed-main > h1,
  .ed-main > h2 { grid-column: 1; margin-top: 0; }
  .ed-main > :not(h1):not(h2) { grid-column: 2; }

  /*
    NOT STICKY, and it was -- removed 2026-09-08 at Griffin's instruction,
    on every page.

    The argument for it was that a heading alone in a column has nothing to
    sit under, so following the reader keeps the answer to "what am I
    reading" on screen. The argument against it is that he watched it and
    said he really does not like it, which settles it -- and he is right for
    a reason worth writing down: on a page of short sections the heading
    detaches from its own block and slides over the one below it, so the
    thing meant to tell you where you are spends half its time labelling
    something else. DESIGN.md's own line is that motion which happens *at*
    the reader reads as out of control, and a heading crawling down the page
    is that whether or not it is technically a layout state rather than an
    animation.
  */

  .ed-side {
    grid-column: 1;
    display: flex;
    align-items: baseline;
    /*
      Tight row gap, looser column gap. In a 20rem column a long label
      ("01 · BEFORE YOU START") and its chip do not share a line, so they wrap
      -- and at a uniform 1rem gap the chip then floated midway between the
      label above it and the heading below, belonging to neither. Halving the
      row gap groups label, chip and heading as one block, which is what they
      are.
    */
    gap: var(--gap-xs) var(--gap-sm);
    flex-wrap: wrap;
  }

  /*
    A FIGURE IN THE SIDE COLUMN TAKES THE WHOLE LINE, and skipping this cost a
    test failure worth keeping the note for.

    `.ed-side` became a flex row so the label and its badge sit on one line.
    The founder section also puts a `<figure>` in there, and as a flex item
    beside the label it was sized by its content instead of by its column --
    which mattered because that image is `loading="lazy"`. A lazy image whose
    box has collapsed reserves nothing, so the section was 374px short until
    scrolling brought it into range, at which point the document grew by
    exactly that much. `site-layout.spec.js` caught it as "the document
    changed height while scrolling: 3255, 3628", which is the same class of
    fault as the header-docking loop that test was originally written for.

    `flex: 1 0 100%` puts the figure back on its own line at the column's full
    width, so the `width`/`height` attributes resolve to a real aspect ratio
    and the space is reserved before the image arrives.
  */
  .ed-side .ed-figure { flex: 1 0 100%; }

  /*
    The hero opts out. It is a landing call to action rather than a document
    section, and a headline that owns the page should not be boxed into 24rem
    of it.
  */
  .ed-head { grid-template-columns: minmax(0, 1fr); }
  .ed-head .ed-main { display: block; }
  .ed-head .ed-side { grid-column: 1; }

  /*
    A SECTION WITH A PICTURE IN ITS SIDE COLUMN KEEPS ITS HEADING WITH ITS
    PROSE, and the founder section is why this rule exists.

    The split above moves the heading into column 1 to sit under its label.
    That is right when the label is all that is there. On the founder section
    the side column also carries the portrait -- so the heading was placed in
    the next free row of column 1, which is UNDERNEATH the photo, while the
    prose started at the top of column 2. The heading ended up orphaned below
    a picture with its own first paragraph a column away and 400px higher:
    Griffin's "the text has gone stray from the image".

    So a figure in the side column reverts that section to the plain
    arrangement -- side rail carries label and picture, main carries heading
    and prose together. `:has()` rather than a hand-applied class, because the
    condition IS the markup: any section built this way needs it, and one
    added later gets it without anybody remembering.
  */
  .ed:has(.ed-side .ed-figure) .ed-main,
  .ed:has(.ed-side .founder-media) .ed-main { display: block; }

  /*
    A SECTION WITH NO RAIL AND NO HEADING runs the full frame. The four
    statements on the landing page are the only one: with nothing in the side
    column and no heading to put there, the default grid would leave 24rem of
    nothing down the left of the thing the page is actually for.
  */
  .ed-wide { grid-template-columns: minmax(0, 1fr); }
  .ed-wide .ed-main { display: block; }
}

/*
  WHERE THE LINE LENGTH IS DECIDED, now that the section no longer decides it.

  ~72 characters. The old grid let prose run the full width of its column for
  the same reason it had a rail: the column WAS the measure. With the section
  at full frame that would be ~120 characters, past where the eye reliably
  finds the start of the next line -- and much worse for a reader already
  tracking with difficulty.

  `--measure-wide` rather than `--measure`: 40rem is a document column with
  nothing beside it; inside a band, under a heading, a little more reads
  better and leaves less dead space to its right.
*/
.ed-main > p,
.ed-main > ul,
.ed-main > ol,
.ed-main > dl,
.ed-main > .ed-note,
.ed-main > figure { max-width: var(--measure-wide); }

/*
  ...except the full-frame runs, which exist precisely to escape it.

  This needs the `.ed-main >` prefix to win: `.qa { max-width: none }` on its
  own is one class against a class-plus-element and silently loses, which
  measured as a 736px grid sitting in a 1056px column and looking, wrongly,
  like the grid itself was refusing to expand. `--measure-wide` is 46rem and
  736px is exactly 46rem, which is how the cause was found.

  `.fact-list` JOINED THEM 2026-09-09, having lost the identical argument in
  silence since the day it was written. It declares `max-width: none` on
  itself, which is (0,1,0) against this rule's (0,1,1) -- so the four
  statements the landing page exists to make ran in 736px of a 1056px column.
  Measured at 1440: two columns of 328px, which is narrower than the same list
  gets on a phone, so two of the four claims wrapped mid-sentence and 320px of
  band sat empty beside them. The list is a scannable run, not prose; the same
  reasoning that gives `.qa` the frame gives it the frame.
*/
.ed-main > .qa,
.ed-main > .ed-grid,
.ed-main > .fact-list { max-width: none; }

/*
  Two columns for a section whose body is a LIST rather than an argument.

  A troubleshooting list is read by scanning for the one entry that applies,
  not by reading through -- so a single 72-character column makes the reader
  travel a very long way to discover none of it is for them. Opt in with
  `.ed-cols`; it is wrong for prose that builds.

  `break-inside: avoid` keeps a question with its answer. Without it a column
  break lands between them and the answer looks like it belongs to the next
  question.
*/
@media (min-width: 62em) {
  .ed-cols { columns: 2; column-gap: var(--gap-2xl); max-width: none; }
  .ed-cols > * { break-inside: avoid; }
}

/*
  ONE right edge as well as one left edge.

  `p` carries `max-width: var(--measure)` globally, which is correct in a
  single-column document and wrong here: it stopped the prose about 100px
  short of where the notes and spec blocks ended, so the column had a ragged
  right edge that read as an accident. Inside the grid the COLUMN is the
  measure, and everything in it ends at the same place.
*/
/*
  SUPERSEDED 2026-09-08, and kept only for what is nested.

  This used to remove the global `max-width: var(--measure)` from everything
  inside a section, because the old grid's COLUMN was the measure and a
  paragraph stopping 100px short of the notes beside it read as an accident.
  With the section now running the full frame, the same rule let prose run to
  ~120 characters a line.

  So the measure comes back on the direct children -- `.ed-main > p` and
  friends, above -- and this stays only for content nested deeper, inside a
  note, a card or a definition, where the container already constrains it.
*/
.ed-main dd,
.ed-note p,
.ed-note ul { max-width: none; }

.ed-side > :last-child,
.ed-main > :last-child { margin-bottom: 0; }

/*
  The section label. Numbered, because a number tells a reader where they are
  in a document, which is the whole point of the pattern.

  16px, not the reference's 10px. This site's own rule is a 16px floor for
  anything carrying meaning, and a section label carries meaning. The wide
  tracking is what does the work the small size does there.
*/
.ed-label {
  display: flex;
  align-items: center;
  /*
    `wrap`, and it is not optional. Making this a flex row to hold the disc
    turned the label text into an anonymous flex item, and an anonymous item
    cannot be given `min-width: 0` -- there is no selector for it. At a 32px
    root font size on a 390px screen (which is 200% zoom on a phone, and a
    setting this audience actually uses) a tracked uppercase label plus a
    68px disc no longer fits on one line, and with `nowrap` the row simply
    grew past the viewport and took the document with it.
    site-layout.spec.js caught it; nothing else would have.
  */
  flex-wrap: wrap;
  /*
    And wrapping the ROW is not enough on its own. At a 32px root the single
    word "Troubleshooting", tracked at 0.16em, measures 362px in a column
    342px wide -- so it overflowed the page by 20px with the row already
    wrapped. Breaking a word is ugly and it is what WCAG's reflow criterion
    asks for at this zoom; `anywhere` only ever fires when the word genuinely
    cannot fit, which at ordinary sizes is never.
  */
  overflow-wrap: anywhere;
  gap: 0.6rem;
  margin: 0 0 var(--gap-md);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}
/*
  THE NUMBER IS A DISC, 2026-09-08, after the runbook artifact Griffin pointed
  at -- its steps are numbered into a bordered circle rather than set as text
  with a middot after them.

  It is a small change that does a specific thing: as text, `01 . CONSENT` is
  one label the eye reads left to right, and the number is just the first word
  of it. As a disc it is an object beside a label, so the number is countable
  at a glance down the page and the words beside it are free to be words. On a
  page whose whole argument is "there are five of these and here they are",
  being countable is the point.

  The middot went with it: a separator between an object and a label is a
  separator between two things that were not touching.
*/
.ed-label .ed-num {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 2.15rem;
  height: 2.15rem;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: inset 0 0 0 1.5px var(--hairline);
  color: var(--ink-2);
  letter-spacing: 0.04em;
  /* Tabular so a column of section numbers lines up. */
  font-variant-numeric: tabular-nums;
}

/*
  A figure on the grid: hard edges, not a soft circle.

  The circle-and-drop-shadow treatment is friendly and it is also what every
  template does. A squared frame with a hairline reads as a document rather
  than as a profile card, and it holds the gridline the rest of the column is
  aligned to -- a circle's optical left edge is inside its box, so it never
  quite lines up with the text above it.
*/
.ed-figure { margin: 0 0 var(--gap-md); }
/*
  The portrait lifts off the paper, 2026-09-08. Square corners flush against a
  ruled ground read as a photo that has been pasted on; a radius and a soft
  shadow read as one that has been placed. Same reasoning as the callout above
  and the header pill: `--surface` things sit ON this paper, they are not part
  of it.
*/
.ed-figure img {
  border-radius: 12px;
  box-shadow: var(--lift-2);
}
.ed-figure img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border: 1px solid var(--ink);
  background: var(--ground);
}
.ed-figure figcaption {
  margin-top: 0.6rem;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* The line under a heading: who, what, when. Never a paragraph. */
.ed-meta {
  margin: 0 0 var(--gap-lg);
  max-width: none;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--brand-d);
}


/*
  A claim about the build, set apart from the prose that led to it.

  Every promise on /safety/ ends with one of these, and they are the reason
  that page is different from a page of good intentions: each says the refusal
  is enforced in two independent places. CLAUDE.md constraints 1 to 3. They
  are ruled rather than boxed so they read as a footnote to the paragraph
  above, not as a separate card competing with it.
*/
/*
  THE CALLOUT, restyled 2026-09-08 after Griffin asked why the runbook page
  looked better than the site. Most of the answer was here.

  It was white with a hard left edge and square corners, sitting on a white
  band -- so on every even section it was invisible, and on an odd one it read
  as a rule someone had left behind rather than as a thing to stop and read.
  A callout has one job: to say "this is the part you must not miss", to a
  reader who is nervous and skimming.

  Three changes, all of them the same idea: a WASH rather than white, so it
  separates from either band; a radius on the three corners the accent bar
  does not own, so the bar reads as a spine rather than as a border that
  failed to go round; and a soft lift, so it sits ON the page instead of
  being drawn on it.

  `--gold-wash` and not the blue: gold is this site's colour for the thing
  that is time-limited or conditional, which is what a callout almost always
  carries here.
*/
.ed-note {
  margin: var(--gap-lg) 0 0;
  padding: var(--gap-md) var(--gap-lg);
  max-width: none;
  border-left: 3px solid var(--accent);
  border-radius: 0 10px 10px 0;
  background: var(--gold-wash);
  box-shadow: var(--lift-1);
  color: var(--ink-2);
}

/*
  The reassuring variant. Green is confirmation everywhere else on this site
  (`--good`), and a note that says "nothing was sent" or "you do not have to
  write anything down" is doing the opposite job to a warning -- it should not
  be wearing a warning's colour.
*/
.ed-note-good {
  border-left-color: var(--good);
  background: #e9f3ec;
}
.ed-note > :last-child { margin-bottom: 0; }
.ed-note-title {
  display: block;
  margin: 0 0 var(--gap-xs);
  max-width: none;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}

/* A tag in the side rail: the fact the section is really about, in two words. */
/*
  A CHIP, not a boxed word. It was a hard 1px black rectangle, which at
  0.9375rem uppercase is heavier than the section heading beside it -- so
  "FIVE MINUTES" competed with "What you need" for the same glance.

  A chip states a fact about the section (a price, a duration, a promise); the
  heading says what the section is. Reducing it to a tinted pill with no
  border puts them back in the right order without losing it. The 16px floor
  for anything carrying meaning still applies to prose; this is a label beside
  a label, at the same size as the one it sits next to.
*/
.ed-badge {
  display: inline-block;
  margin: 0;
  padding: 0.25rem 0.6rem;
  border-radius: 6px;
  background: var(--ground);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/*
  A KEY WORD, BOXED -- 2026-09-08.

  Griffin asked for the runbook artifact's treatment of key words, "bolding
  and boxing them", brought into body copy. That artifact does two separate
  things and only one of them is this: it has chips, which are labels ABOUT a
  block, and it has words inside sentences set bold and a shade darker than
  the prose around them. `.ed-badge` is already the first. This is the second,
  with a box round it.

  ## Why one class and not a semantic set

  The obvious next move is `.key-good` and `.key-bad` in green and red, and it
  is the wrong one. These sit inside running prose, and a paragraph with three
  differently-coloured boxes in it is a paragraph nobody finishes. The word
  carries the meaning -- "cannot", "nothing", "Stop" -- and the box only says
  *this is the word*. One treatment, used sparingly, is legible; a palette of
  them is decoration competing with the sentence.

  ## IT MUST NOT LOOK LIKE A KEY -- 2026-09-09

  It did, exactly. `.key` was `--ink` on `--ground` with a 1px `--hairline`
  ring at a 6px radius; `kbd` is `--ink` on `--ground` with a 1px `--hairline`
  border at a 7px radius, and `code` is the third of the same chip. So three
  meanings wore one costume: press this key, type this literal string, notice
  this phrase. On /install/ `chrome://extensions` and on /safety/ `cannot
  undo` are the same object to look at and nothing about either says which.
  Things that look the same have to behave the same, or the reader stops
  trusting the difference between them -- and this audience is the one that
  can least afford to wonder whether a word in a sentence is something to
  press.

  The box stays, because the box is the brief. What changes is the FILL and
  the RING. A ring at a radius on a neutral fill is what makes a chip read as
  a raised object; a flat tinted wash with no edge reads as a phrase somebody
  went over with a marker, which is what this is. The tint is the accent the
  site already stamps the day count and the wax seal with, so a marked word
  belongs to something rather than being a fourth grey.

  `--ink` on the wash rather than the accent itself: coloured text inside a
  coloured box is two claims on the same word, and the pairing has to clear
  7.0 at body size where `--accent` would not.

  ## The rule for using it

  At most one or two to a section, on the phrase the section would be wrong
  without. If a paragraph has three, two of them are not key.

  `box-decoration-break: clone` so a boxed phrase that wraps renders as two
  complete boxes rather than as one box with its right end missing and its
  left end reopened on the next line.
*/
.key {
  padding: 0.06em 0.34em 0.1em;
  border-radius: 6px;
  background: var(--accent-wash);
  color: var(--ink);
  font-weight: 700;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
/*
  The opening paragraph of a page. Larger than body, and NOT capped to
  `--measure` -- it sits in the wide column of the grid and is meant to run
  the width of it, which is what makes the top of a page read as a masthead
  rather than as the first of many paragraphs.
*/
.ed-lede {
  max-width: 46rem;
  font-size: clamp(1.1875rem, 1.05rem + 0.5vw, 1.4375rem);
  line-height: 1.45;
  color: var(--ink-2);
}

/*
  A page's opening block. Same grid as every section below it, so the h1 sits
  on the identical gridline the h2s do -- which is the thing that makes a page
  look laid out rather than assembled.
*/
.ed-head { padding-top: var(--gap-xl); }
.ed-head .ed-label { color: var(--brand-d); }

/*
  Spans both columns. For the one thing per page that is genuinely wider than
  the prose -- the demo clip -- so it can break the grid without leaving it.
*/
.ed-full { grid-column: 1 / -1; }

/*
  Full-span, but indented to the prose column on wide screens. For a button
  that follows a full-width figure: spanning the grid keeps it out of the
  rail's flow, and the indent keeps it under the text it belongs to rather
  than floating out beside the section label.
*/
@media (min-width: 56em) {
  .ed-indent { grid-column: 2; }
}

/* A call to action that belongs to a left-aligned column rather than to a
   centred page. `.cta-row` centres by default, which is right under a
   centred hero and wrong inside the editorial grid. */
.cta-row-start { justify-content: flex-start; text-align: left; }

/*
  A reader who has asked their OS for more contrast has told us that subtle is
  costing them. The ruling is the first thing to go: it carries no information
  and its whole design brief was to sit at the edge of perception.
*/
@media (prefers-contrast: more) {
  body { background: var(--paper-1); }
}

/* --------------------------------------------------------------------------
   The founder section
   --------------------------------------------------------------------------
   Built after a screenshot of alaris.studio, then revised on Griffin's notes.
   What he kept from the reference: the handwritten name inside the photograph,
   and the credential set under it as a caption. What he changed: the section
   runs IMAGE LEFT, TEXT RIGHT rather than centred, the numbered label is gone,
   and the photograph is in colour.

   ## It is the ordinary grid, not a special case

   The first version left the editorial grid for a centred axis. It does not
   need to: `.ed` already puts a rail on the left and a prose column on the
   right, and there is already a rule saying a section with a picture in its
   rail keeps its heading with its prose instead of pulling it left. So this is
   a normal section whose rail happens to hold a portrait, which means one less
   layout in the stylesheet and one less thing to keep in step.

   ## No greyscale, and no wash

   Both are gone at his instruction. The first pass desaturated the photograph
   and laid a dark gradient over the top of it, because the name was drawn in
   white and a white school-formal backdrop gives white strokes nothing to sit
   on. Writing the name in `--accent` instead solves that at the source: it
   reads on a light ground without darkening the picture, and it is the same
   red as the stamped day count in the hero and the wax seal in the footer.

   That is three appearances of one colour on one page, which is the point of
   having an accent at all.
*/
.founder-media {
  display: flex;
  flex-direction: column;
  gap: var(--gap-md);
  flex: 1 0 100%;
  min-width: 0;
}

/*
  The portrait. A rounded card rather than the hard-edged `.ed-figure` frame
  the other rails use -- that frame exists to hold a gridline, and this one is
  holding a face.
*/
/*
  THE PHOTOGRAPH NEEDS AN EDGE -- 2026-09-09.

  The portrait was shot against a pale curtain, and the founder section sits
  on the white band, so the top corners of the image and the page behind them
  were within a few percent of each other and the card's top edge simply
  dissolved. `--lift-2` is a shadow, and a shadow under a light object on a
  light ground is not an edge; it is a hint that there might be one.

  A 1px ring at 10% PURE BLACK, drawn as an `outline` with a negative offset
  so it takes no layout and hugs the 22px corner from the inside. Pure black
  and not `--hairline`: a tinted near-white ring picks up the cream around it
  and reads as dirt along the image edge, which is the failure this is
  fixing, not a variant of it. The same ring goes on the demo clip below for
  the same reason.
*/
.founder-card {
  position: relative;
  border-radius: 22px;
  overflow: hidden;
  background: var(--paper-3);
  box-shadow: var(--lift-2);
  outline: 1px solid rgba(0, 0, 0, 0.1);
  outline-offset: -1px;
  aspect-ratio: 4 / 5;
}
.founder-card img.founder-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/*
  THE NAME, IN HIS OWN HAND.

  Traced from `brand/handwriting-source.png` by scripts/make_handwriting.py --
  see that file for why it is a trace of the ink rather than a font, a drawn
  alphabet, or the vector export of the same drawing.

  ## The flick

  He wrote the name twice, and the site alternates between the two takes the
  way alaris.studio does. It only works because the two are genuinely
  different hands: the same name written twice by the same person, half a
  second apart, is a kind of movement nothing else on this page can do.

  Both files are centred on a shared box by the generator, so the swap reads as
  one name redrawn rather than as something jumping.

  `visibility`, NOT `opacity`, and for two reasons that both matter.

  The first is that this is a swap and not a fade. `visibility` is a discrete
  property, so there is no interpolated middle where both hands are half
  drawn -- showing two versions of the same name at once is the single thing
  this effect must never do, and with opacity it is one mistimed keyframe away.

  The second is that check_site.mjs fails on any `opacity: 0` that is not
  scoped under `.js`, because a rule that hides content when the script fails
  is the worst bug this site could ship. Its regex reads a keyframe selector as
  an ordinary one, so an opacity animation trips it -- and the right answer was
  not to loosen the gate but to notice it was pointing at the wrong property.

  ## Reduced motion stops it dead

  Not slower: stopped, on take A. That is also why the animation lives here in
  the stylesheet rather than inside the SVG files. A CSS animation inside an
  `<img>`-loaded SVG does run, but whether that document sees the reader's
  motion preference is not something to gamble on for this audience.
*/
/*
  ONE LINE, 2026-09-08. He wrote GRIFFIN over HUDSON because that is what fits
  on a page you are writing on; stacked it is a 2:1 block that reads as a logo
  sitting on the photograph. Side by side it is a signature, which is what the
  thing actually is. The recomposition happens in the GENERATOR, not here,
  because both takes have to land on one shared box or the flick between them
  jumps -- see scripts/make_handwriting.py.

  The lockup is 8.4:1 now instead of 2:1, so the width had to grow or the
  letters would have come out a third of their old height. 52% of the card gave
  a cap height of about 12% of the card's width when the name was two lines;
  84% gives about 9.4% on one. That is what the layout costs, and it is worth
  paying -- a name read left to right in one pass is easier than a name read
  downwards, and this reader is 65-80.

  It sits across the TOP of the portrait, where the school-formal backdrop is
  plain. `left: 8%` rather than centred, because a signature starts at a margin.
*/
.founder-name {
  position: absolute;
  left: 8%;
  top: 6%;
  width: 84%;
  height: auto;
  pointer-events: none;
  /* A whisper of shadow so the strokes hold their edge wherever the
     photograph happens to be light. */
  filter: drop-shadow(0 1px 3px rgba(30, 8, 4, .3));
}
.founder-name-a { animation: opa-hand-a 900ms step-end infinite; }
.founder-name-b { animation: opa-hand-b 900ms step-end infinite; }

/* Also driving `.hand-a`/`.hand-b` -- the day count and the header arrow. */
@keyframes opa-hand-a {
  0%   { visibility: visible; }
  50%  { visibility: hidden; }
  100% { visibility: visible; }
}
@keyframes opa-hand-b {
  0%   { visibility: hidden; }
  50%  { visibility: visible; }
  100% { visibility: hidden; }
}

@media (prefers-reduced-motion: reduce) {
  .founder-name-a,
  .founder-name-b { animation: none; }
  .founder-name-a { visibility: visible; }
  .founder-name-b { visibility: hidden; }
}

/*
  The credential, under the card. A caption in tracked capitals rather than a
  sentence: it labels the photograph, it does not make a claim in prose.

  Griffin's own words, and CLAUDE.md's note on this section says not to
  embellish them.
*/
.founder-role {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  line-height: 1.6;
  color: var(--ink-2);
}
.founder-role b {
  display: block;
  font-size: 1.0625rem;
  letter-spacing: 0.08em;
  color: var(--ink);
}

/*
  "OPA is added to Chrome. One step left."

  Written into /install/ by opa.js when the extension opens this page itself
  after being added -- see `justInstalled()` there, and the `onInstalled`
  handler in the extension's background.js.

  Green, and it is the only place on the site that uses `--good` for anything
  other than a confirmation inside the panel. That is exactly what this is: the
  page confirming that the thing the reader just did worked. The one emotion
  this audience needs at this moment is "that went right", and they are looking
  at a page whose headline still tells them to do it.

  Full width of the content column and above everything, because a reassurance
  that has to be found is not a reassurance.
*/
.install-done {
  margin: 0 0 var(--gap-xl);
  padding: var(--gap-md) var(--gap-lg);
  max-width: none;
  border-radius: 999px;
  background: #e8f5ed;
  border: 2px solid rgba(17, 122, 61, .28);
  color: #0e5c2e;
  font-size: 1.125rem;
  font-weight: 700;
  text-align: center;
}
@media (max-width: 40em) {
  /* A pill that has wrapped to two lines is a lozenge with its ends cut off. */
  .install-done { border-radius: var(--radius); text-align: left; }
}

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

/*
  THE FOOTER, ON A SHEET -- 2026-09-08, second pass.

  ## Where this came from, and what it stopped being

  Six treatments were built as a live artifact with their contrast measured in
  the browser rather than asserted, and Griffin picked F, letterpress cream.
  The artifact's own note on F was that it is the safest of the six and
  "answers your question least, because from four feet away it is the footer
  you already have." He picked it anyway and said what to do about that: make
  it an actual letter, tilt the box, separate it from the page, and give the
  seal maximum effect.

  The first pass took "letter" literally -- letterhead, return address, "To
  whoever is reading this,", "Yours," and a signature -- and he cut all of it:
  "i dont want the text to be literally in a letter format ... as it still has
  to be a footer."

  He is right, and the reason is worth keeping so nobody tries it again. A
  letter is addressed to one person and signed by one person, and this block is
  neither: it is a list of links and a legal disclosure that every visitor sees
  on every page. Dressing that as correspondence made the disclosure read as a
  personal assurance, which is the opposite of what a disclosure is for --
  a statement about what OPA *is*, true whoever is reading and whoever wrote it.

  ## What survives, and why it is still not just a footer

  The OBJECT stays and the CONTENT goes back to being a footer. Cream
  letterpress stock, a slight tilt, separated from the page so the ruling shows
  around its corners, and a wax seal pressed across the bottom edge. Ordinary
  footer furniture -- three link groups, the non-affiliation statement under a
  rule, a stamp line -- printed on something that was clearly made rather than
  laid out.

  That is the blend. The paper is the character; the words are a footer.

  F's own values are kept verbatim, because they are the part that was
  measured: `linear-gradient(#f7f2e6, #f2ebdb)`, a hairline top edge at 10%
  ink, and a 90%-white inset highlight.

  ## The seal is the letterhead

  Removing the wordmark block from the top left leaves the sheet with no
  identity on it -- except that it does not, because the seal IS the mark,
  pressed into wax at 272px. A wordmark in the corner as well would be the
  brand stated twice on the quietest block of the page.

  ## The tilt is gone -- 2026-09-08

  It was 0.7deg on the sheet, dropping to 0.4deg on a phone, and Griffin asked
  for it removed. It is worth writing down why it was there and why losing it
  costs nothing, because the reasoning that put it there was not wrong -- it
  was answering a question this block no longer asks.

  The tilt came from the letter version. A letter is a thing somebody put down
  on a desk, and a thing put down on a desk is never quite square. Once the
  letter went (the salutation, the sign-off, the signature -- see above), what
  is left is a footer printed on good stock, and a printed sheet in a layout is
  square to the layout. The tilt was the last surviving gesture of an idea that
  had already been cut, and with the rest of it gone it read as a rendering
  fault rather than as a hand.

  What actually carries "this is a separate object" is the SEPARATION and the
  SEAL, not the angle: ruled ground visible all the way round the sheet, and a
  wax seal pressed across its bottom edge. Both survive, and the block reads as
  an object without leaning.

  Removing it also removes a constraint. A rotated block widens its own
  bounding box by `height * sin(angle)` -- about 6px here -- which had to fit
  inside the wrap's padding at 320px. Square, that budget is free, which is
  what pays for the even margin below.

  ## Separated from the page, evenly -- 2026-09-08

  Griffin: the lined paper around it must have "even spacing all around the
  main footer box". It did not. The sheet was capped at 64rem and centred, so
  the ruled band beside it was whatever the viewport had left over -- 208px a
  side on a 1440 screen -- while above and below it was the footer's own
  padding, a different number entirely. At one width it looked deliberate; at
  every other width it looked like the sheet had been dropped in.

  It is one number now, on all four sides, and the number is `--pad`: the same
  page margin every other section on the site is set in. Horizontally that gap
  is the wrap's own padding, so the sheet simply fills the content column and
  lines up with the band above it. Vertically it is `padding-block` on the
  footer, set to the same token. Even by construction rather than by
  coincidence, at every width, with nothing to re-tune when `--pad` changes at
  62em.

  The 64rem cap went with it. It was what made the gap uneven, and the sheet
  filling the frame is the thing the rest of the page already does.
*/
.foot-sheet {
  position: relative;
  /*
    No `max-width` and no `margin-inline: auto`. The sheet fills the wrap's
    content column, which is what makes the ruled band beside it exactly
    `--pad` -- the same gap the footer sets above and below. See the note above.
  */
  padding: clamp(1.75rem, 3.4vw, 3.25rem);
  /*
    LIFTED OFF THE PAGE -- 2026-09-09.

    This was F's gradient verbatim, #f7f2e6 to #f2ebdb, and the reference is
    not the problem: the GROUND it was dropped onto is. `--paper-2` is
    #f5f0e7, so the top of the sheet was two points away from the page behind
    it and the bottom was actually DARKER than it. The whole conceit of this
    block -- a cream sheet lying on ruled paper, closed with a seal that hangs
    off its edge -- was being carried by a 1px line at 10% black, and a reader
    with any loss of contrast sensitivity, which is most of this audience, was
    not seeing a sheet at all. The seal then reads as a sticker on the page
    rather than as wax on an object, because there is no object.

    So the stock goes near-white while keeping its cream cast, which is what a
    fresh sheet on an older page actually looks like, and it is the same move
    the `.ed` bands already make against this ground with `--surface`. It only
    RAISES the contrast of every word printed on it, so nothing measured here
    got tighter; check_contrast.mjs samples the rendered ground and says so.
  */
  background: linear-gradient(#fffcf3, #f9f2e1);
  border: 1px solid rgba(29, 29, 31, .12);
  /* Paper is cut, not moulded. 2px, not `--radius`. */
  border-radius: 2px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .9),
    var(--lift-3);
}

/*
  THE RULED BAND AROUND THE SHEET, AND WHY IT IS A FORMULA.

  The first attempt at "even spacing all around" was `padding-block: var(--pad)`
  on the reasoning that the wrap already spends `--pad` on the inline axis, so
  four equal gaps came for free. Measured, that was wrong, and the picture
  showed it immediately: 80px above the sheet against 192px beside it on a
  1440 screen.

  The reason is that the ruled ground is the PAGE, not the footer. It runs to
  the viewport edge, so the band beside the sheet is the wrap's padding PLUS
  whatever the `--frame` cap leaves over -- (100vw - --frame) / 2 -- and that
  second term is zero on a laptop and 112px on a 1440 monitor. The gap above
  had no such term, so the two could only agree at one width.

  So the block padding carries the same two terms:

      (100vw - --frame) / 2  +  --pad

  which is the visible side band by construction, at every width. Below the
  frame the first term goes negative and `clamp`'s floor takes over at `--pad`,
  which is correct: there the sheet is `--pad` from the edge on all four sides
  because there is nothing left over.

  ## The cap, and why it is not a fudge

  12rem, which is exactly the value the formula produces at 1440px.

  Past that, honesty about the constraint: a full-bleed ground means the side
  band grows without bound, and a 2560 monitor would want 752px of empty paper
  above the footer. Nobody wants that, and "even" stops being the goal well
  before it -- what a reader sees on a very wide screen is a sheet in a large
  field, not four gaps to compare. The band is exactly even from 320px to
  1440px, which is every width this audience actually uses, and generous
  beyond it.

  ## 100vw includes the scrollbar

  Deliberately not corrected. It makes the block padding about 7px larger than
  the inline band -- half a scrollbar -- and the alternative is measuring the
  viewport in JavaScript and writing a custom property from it, which is a
  script dependency for half a scrollbar on the quietest block of the page.
  See the note on `.ed::before` for where this same fact actually mattered.

  ## The seal is not spacing

  It hangs about 56px past the sheet's bottom edge, and that is an object lying
  across the boundary rather than a margin -- the entire point of a seal. It
  needs no allowance here: the smallest band this formula produces is `--pad`,
  and at those widths the seal has already come inside the sheet (see `.seal`).
*/
footer {
  padding-block: clamp(
    var(--pad),
    calc((100vw - var(--frame)) / 2 + var(--pad)),
    12rem
  );
}

/*
  The three link groups, at the top where a footer's links go.

  `.foot-col a` holds the 44px touch floor, and check_site.mjs asserts that by
  name -- it is one of four selectors in that gate, so the class cannot be
  renamed without updating it.
*/
.foot-cols {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-lg) var(--gap-3xl);
  margin-bottom: var(--gap-xl);
}
/*
  LETTERPRESS, and it is only ever on display type.

  A one-pixel white shadow directly under dark type on cream is what reads as
  ink pressed into paper. On body copy it would be a legibility cost for
  exactly the reader this site is built around -- a soft edge under every glyph
  is the last thing tired eyes need -- so it goes on the column headings and
  the stamp line, and nowhere else. It does not touch the measured contrast
  either way: the glyph itself is still its own colour on cream, and the shadow
  sits behind it.
*/
.foot-col h2,
.foot-stamp {
  text-shadow: 0 1px 0 rgba(255, 255, 255, .9);
}
/* 1rem, up from 0.85rem on 2026-09-09. These are the labels that say what the
   two links under each of them are FOR, on the block a reader who has scrolled
   the whole page arrives at -- and 13.6px is the size this file's own comments
   twice refuse for anything carrying meaning. The columns have an 80px gutter;
   there was never a width argument for it, only a habit. */
.foot-col h2 {
  margin: 0 0 var(--gap-xs);
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.foot-col ul {
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: none;
}
.foot-col li { margin: 0; }
.foot-col a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.foot-col a:hover { text-decoration: underline; }
/* The page you are on, marked the way the header marks it: by weight and a
   rule, never by colour alone. */
.foot-col a[aria-current="page"] {
  color: var(--ink);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 2px;
}

/*
  THE NON-AFFILIATION STATEMENT, which is what this whole block is really for.

  It is the most important sentence on the site for a reader worried about who
  they are dealing with, and check_site.mjs requires it on every page. Set as
  fine print under a wall of links it is a disclosure designed not to be read,
  so it gets a rule of its own, real space, and a size above the body floor
  rather than below it.

  The impressed rule is F's: a hairline with a highlight beneath it, which is
  the edge a press leaves in cotton stock. Written as an inset shadow so the
  highlight lands under the rule at the top of the box rather than under the
  box.

  Capped so it never runs under the seal, and capped as a PERCENTAGE as well
  as an absolute: the seal sits at a percentage inset from the sheet's right
  edge, so a fixed rem cap is only correct at one sheet width. 46rem was that
  mistake -- at 1440 the statement ran straight under the wax and "helps you
  fill the form" disappeared into it.
*/
.foot-legal {
  max-width: min(40rem, 62%);
  padding-top: var(--gap-lg);
  border-top: 1px solid rgba(29, 29, 31, .18);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .85);
}
.foot-legal p {
  margin: 0 0 var(--gap-md);
  max-width: none;
  /* Body size, 2026-09-09. The note above says this gets "a size above the
     body floor rather than below it" and 1.0625rem was one pixel BELOW it --
     the intent was right and the number contradicted it. For the sentence
     this file calls the most important on the site, set at least as large as
     the sentences around it. */
  font-size: 1.125rem;
  line-height: 1.6;
}
.foot-legal p:last-child { margin-bottom: 0; }
.foot-legal strong { color: var(--ink); }

/* Where it was made and when. A stamp, not a signature. */
.foot-stamp {
  margin: var(--gap-xl) 0 0;
  /* 1rem, up from 0.85rem, 2026-09-09 -- the floor, same as the column
     headings it sits under. Where a thing was made and by whom is the last
     line a wary reader checks, not fine print. */
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  /* --ink-2 rather than --ink-3: on this cream, with the ruling's worst case,
     --ink-3 measures 4.66 and an unrecognised <p> is held to 7.0. Darkening it
     costs nothing; widening the checker would have cost the reader. */
  color: var(--ink-2);
}

/* ---------- the wax seal ---------- */

/*
  A red wax seal of the OPA mark, with a highlight that tracks the pointer.
  Griffin's brief, after twotone.au's masked brand mark.

  WHY THE SUNGLASSES AND NOT THE WHOLE FACE. The mark is a hand-traced
  portrait: 64 subpaths, most of them hair and wrinkle strokes. Ranked by area
  against their own bounding box, the split is unambiguous -- three shapes fill
  65-78% of their box (the lenses and the bridge) and everything else fills
  under 20% because it is a hairline. An emboss destroys hairlines; it is a
  relief, not a print. Pressing the full drawing into wax produced mush at any
  size, and the partial head outline that survived read as a broken jaw. The
  sunglasses are the part that is recognisable anyway.

  The blob outline is generated by summed sine lobes rather than hand-drawn, so
  the wobble is smooth and the ring is closed -- a hand-typed bezier ring is
  exactly where this sort of thing goes wrong.

  Ring text sits on TWO arcs, not one circle. Glyphs are placed on the left of
  the direction of travel, so a single closed circle renders the bottom half
  upside down. The top arc runs left-to-right over the top (sweep 1) and the
  bottom arc left-to-right under the bottom (sweep 0).

  ## MAXIMUM EFFECT -- Griffin's words, 2026-09-08

  It is pressed across the bottom right of the sheet and hangs off the bottom
  edge, which is where a seal goes and what a seal does: it closes the sheet,
  so it has to be ON the edge rather than inside the margin. The part that
  overhangs is also the part that proves the sheet is a separate object --
  ruled page behind it, wax on top.

  Since the letterhead came off this block it is also the only mark on it, so
  it is carrying the identity as well as the drama.

  It hangs DOWN and not sideways, and that is a constraint rather than a
  choice. Vertical overhang costs nothing but the footer's bottom padding;
  horizontal overhang would push a 320px page into sideways scroll, which
  site-layout.spec.js checks at eight widths.

  The shadow is warmer and deeper than the old one because it is now casting
  onto a lit cream sheet a few millimetres below it rather than onto the page
  ground -- and because a seal that does not look raised is a sticker.
*/
.seal {
  position: absolute;
  right: clamp(1.25rem, 4%, 3.5rem);
  bottom: -3.5rem;
  width: min(272px, 34vw);
  aspect-ratio: 1;
  /* the two properties the shine reads; centred until a pointer moves */
  --mx: 50%;
  --my: 38%;
  filter: drop-shadow(0 22px 34px rgba(74, 12, 12, .46));
}
.seal-svg { display: block; width: 100%; height: 100%; }
.seal-ring-text {
  font-family: "Shadow Light", "Source Sans 3", system-ui, sans-serif;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.17em;
}

/*
  The shine. A soft highlight that tracks the pointer, blended onto the wax so
  it lifts the red rather than washing it out, and clipped to a circle inside
  the blob so it never spills past the molten edge.

  Nothing here carries information: the seal is complete with the pointer
  nowhere near it, and on a touch device this never fires at all.
*/
/*
  Scoped under `.js` deliberately, and check_site.mjs enforces that: an
  `opacity: 0` rule outside that scope would leave this hidden forever if the
  script never runs. With scripting off the span simply has no styles, which is
  the correct outcome -- it is inert without the pointer handler that feeds it.
*/
.js .seal-shine {
  position: absolute;
  inset: 7%;
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: soft-light;
  opacity: 0;
  transition: opacity 300ms ease;
  background:
    radial-gradient(circle 30% at var(--mx) var(--my),
      rgba(255,255,255,.98), rgba(255,255,255,.4) 40%, transparent 66%);
}
.js .seal:hover .seal-shine { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .js .seal-shine { display: none; }
}

/*
  A reader who has asked their OS for more contrast has told us that subtle is
  costing them. The letterpress highlights are the first thing to go: they
  carry no information and their whole design brief was to sit at the edge of
  perception.
*/
@media (prefers-contrast: more) {
  .foot-col h2,
  .foot-stamp { text-shadow: none; }
  .foot-legal { box-shadow: none; }
}

@media (max-width: 60em) {
  /*
    The seal comes inside the sheet and sits under the signature, where a seal
    on a narrow letter would be. Off the corner it would either overhang into
    a 24px page margin -- which is sideways scroll -- or shrink to a red dot,
    and it is the emotional centre of this block rather than a bullet point.

    It stays below the letter rather than above it. The old footer put it
    first on a phone, reasoning that a reader scrolling for a link should not
    have to pass a 340px graphic; that reasoning is gone with the links, which
    are now below the seal either way, and a seal above the letter it seals is
    the wrong way round.
  */
  .seal {
    position: static;
    width: min(240px, 56vw);
    /* Held to the right, the same corner it occupies on a wide screen, so the
       block does not appear to reorganise itself between breakpoints. */
    margin: var(--gap-lg) 0 0 auto;
  }
  .foot-legal { max-width: none; }
}

@media (max-width: 40em) {
  .foot-cols { gap: var(--gap-md) var(--gap-xl); }
}

/* ---------- motion ---------- */

/*
  The hero's entrance, and nothing else.

  ## What used to be here, and why it went

  A `.reveal` class faded and lifted every heading, paragraph and figure as it
  crossed into the viewport, driven by an IntersectionObserver. Removed
  2026-09-08 at Griffin's instruction -- his words, "it looks gross". He is
  right, and the reason is in DESIGN.md's own brief: motion that happens *at*
  the reader reads as out of control, and this was the site doing it to every
  section on every page. A header that docks because you scrolled is a
  response; a paragraph that appears because you scrolled is a page that was
  hiding something from you.

  What survives is the one entrance that is not scroll-triggered: the hero
  settling once, on load, when it is already on screen. That is the page
  arriving, not the page reacting.

  ## The rule this breaks, and how it is made safe

  SITE.md said "no JavaScript on /". That is reversed here deliberately, and
  the reversal is only defensible because of the two guards below.

  **Nothing is hidden unless JavaScript is running.** The rule is scoped under
  `.js`, a class opa.js sets on <html> as its first act. If the script fails
  to load, is blocked, or the browser has JS off, it never matches and the
  page is simply visible. Hiding content by default and revealing it with
  script is the single worst way to do this -- it means a failed script leaves
  a blank page -- and it is structurally impossible here. check_site.mjs
  fails on any `opacity: 0` outside that scope.

  **Reduced motion wins outright.** The query below does not merely shorten
  the transition, it forces the end state, so someone who has asked their OS
  for less motion gets a static page with everything present rather than a
  fast version of the animation.

  Movement is small on purpose: 18px and a fade, once. It reads as the page
  settling rather than as things flying around, which for a 65-80 audience is
  the difference between "considered" and "disorienting".
*/
.js .hero-in { opacity: 0; transform: translateY(18px); }
.js .hero-in.is-in {
  opacity: 1;
  transform: none;
  transition: opacity .6s cubic-bezier(.2,.75,.2,1), transform .6s cubic-bezier(.2,.75,.2,1);
  transition-delay: var(--reveal-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* Force the resting state rather than a fast animation. */
  .js .hero-in,
  .js .hero-in.is-in {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   THE LANDING LAYER — only `/` uses anything below this line.
   ==========================================================================

   ## Why this is one block at the bottom, and why it was rewritten

   Everything above is the shared base: four subpages (`/safety/`, `/install/`,
   `/privacy/`, `/404`) are built entirely out of it. Everything below styles
   the landing page and nothing else.

   That seam is load-bearing. It was rewritten on 2026-09-02, replacing about
   1300 lines that had accumulated as two successive redesign layers stacked on
   top of each other — `.deadline-hero`, then `.product-hero` overriding it,
   with the losing layer still present and still shipping. The rule now is: if
   a selector below is not used in site/index.html, delete it rather than leave
   it for the next person to reason about.

   The base was NOT rewritten with it. Doing so would have restyled four pages
   nobody asked to change, to fix a problem that only existed on one.

   ## What this layer is allowed to assume

   It reuses the base wherever the base already does the job: `.on-night` for
   the dark block, `.promises` for the safety list, `.steps` for the numbered
   list, `.stat`/`.stat-note` for the statistic, `.cta`/`.cta-2`, `.note`,
   `.contact`, `.eyebrow`. New selectors here exist only for things the base
   genuinely does not have — the hero, the countdown, the video frame, the
   screenshot row and the founder card.
*/

/* --------------------------------------------------------------------------
   Utilities the landing page needs
   -------------------------------------------------------------------------- */

/*
  The countdown is announced to screen readers through this, once per day,
  rather than through the visible strip. See the `[data-countdown-text]` note
  in opa.js: a timer that re-reads itself every second is not urgency, it is
  an unusable page.
*/
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The rhythm between sections. Generous on purpose — this audience reads. */
.section-block {
  padding: var(--gap-3xl) 0;
  border-top: 1px solid var(--hairline);
}
.section-block > .eyebrow { margin-bottom: var(--gap-sm); }
.section-block > h2 { margin-top: 0; }
.section-block > .lede { max-width: var(--measure); }

/* --------------------------------------------------------------------------
   Hero
   --------------------------------------------------------------------------
   REBUILT 2026-09-04 for the two-page site. There is no second column any
   more — no propped-open product screenshot, no pinned voucher chip — because
   there is no longer a second thing on this page competing with the
   deadline for attention. One centred column, generous air above and below,
   reads as considered rather than as a page waiting for content that never
   arrived.
*/
/*
  THE DEADLINE OWNS THE FIRST SCREEN. Rebuilt 2026-09-04, Griffin's brief,
  measured against alaris.studio: their display headline runs ~8vw at weight
  900 on 0.84 leading and fills ~95% of the viewport height.

  Why this is the right trade for a 65-80 reader, and not just fashion: the
  deadline is the only fact on this page that is both time-limited and about
  money. For someone deciding whether to bother at all, it IS the argument.
  A headline that has to share the fold with a button is neither a headline
  nor a button.

  The cost is that the install CTA now sits below the fold. That is paid for
  by `.hero-cue` below -- this audience will not scroll unless something tells
  them to, which is the single most common failure for this age group and is
  entirely fixable with an affordance. The old
  "install button is on the first screen" assertion in site-layout.spec.js was
  deleted with this change rather than left to fail quietly.

  `svh` not `vh`: on mobile Safari `100vh` is the LARGEST viewport, so a
  100vh hero is always taller than the visible screen and the cue is always
  under the address bar. `svh` is the small viewport, which is what is
  actually on screen.
*/
.hero {
  /*
    WIDER THAN THE READING COLUMN, from 2026-09-08.

    This was capped at 68rem (1088px) inside a `.wrap` that already spends
    `--pad` on each side, so on any screen past about 1250px the headline had a
    fixed 1056px to wrap in while its font size kept growing with the viewport.
    That is what actually limited it: at 9.8vw a 1920 screen wants 188px type
    in a 1056px column, which is five lines, and five lines do not fit above
    the fold at any size. The height clamp was getting the blame for a width
    problem.

    So the hero reclaims the wrap's padding above 62em and runs the full frame.
    Below that it stays exactly where it was -- at 24px of page margin there is
    nothing to reclaim, and a headline touching the screen edge on a phone is
    not the same idea at a smaller size.
  */
  max-width: 68rem;
  margin: 0 auto;
  min-height: 82svh;
  display: flex;
  flex-direction: column;
  /*
    `flex-start`, NOT `center`. A centred flex column whose content is taller
    than its box overflows at BOTH ends, and the overflow above the container
    cannot be scrolled to -- so the eyebrow simply vanished off the top of the
    page at 1440x900. Starting at the top and pushing the cue down with
    `margin-top: auto` gives the same look when it fits and degrades safely
    when it does not.
  */
  justify-content: flex-start;
  padding-top: var(--gap-lg);
  padding-bottom: var(--gap-lg);
  text-align: center;
}
@supports not (min-height: 1svh) {
  .hero { min-height: 82vh; }
}

@media (min-width: 62em) {
  .hero {
    max-width: none;
    /* Reclaims the wrap's own padding and nothing more, so this can never
       reach past the frame or cause horizontal scroll. */
    margin-inline: calc(-1 * var(--pad));
  }
}

/*
  An auto top margin on the FIRST item as well as the last.

  With only the cue carrying `margin-top: auto`, every pixel the headline did
  not use piled up in one gap between the last paragraph and the cue -- 160px
  of nothing in the middle of the first screen at 1440x900. Auto margins on
  both ends split the slack evenly instead, which centres the content group
  while still pinning the cue to the foot of the box.

  It also degrades correctly: when the content is taller than the hero, both
  auto margins resolve to zero, so nothing is pushed off the top. That is the
  failure `justify-content: center` caused and the reason it is not used here.

  It is on the H1 now, because the eyebrow that used to be the first item left
  the hero on 2026-09-08. If anything is ever put above the headline again,
  this moves with it -- the rule belongs to whatever is first, not to a
  particular element.

  WRITTEN INTO THE SHORTHAND on `.hero h1` below, not as a longhand up here.
  A separate `.hero h1 { margin-top: auto }` was the first attempt, and the
  `margin: 0 auto ...` shorthand twenty-five lines further down silently reset
  it -- which is the same ordering bug this file already records against the
  scroll cue, found the same way, twenty minutes apart.
*/

/*
  The offer line. `clamp()` runs from 2.125rem (34px) to 4.25rem (68px) so it
  reads as the biggest thing on the page at every width, and the whole thing
  still fits at 200% zoom without the day count wrapping away from its
  sentence.
*/
/*
  `auto`, not 0, on the horizontal margin: the shorthand is more specific than
  the centring net at the foot of this file, so writing 0 here silently pinned
  the biggest thing on the page to the left edge of a centred section.

  Weight 700, not the 900 the reference uses: Source Sans 3 is self-hosted at
  400/600/700 only, and a fourth face is another ~15KB on the critical path of
  a page whose whole argument is that it loads instantly on a bad connection.
  At this size 700 with tight tracking reads as very bold. If a genuine 900 is
  wanted it is one woff2 and one `@font-face` -- a deliberate call, not a
  default.

  Leading below 1 and negative tracking are what make a headline this large
  read as one object rather than as stacked lines. Both go back to normal at
  body size; tight leading on a paragraph is hostile to tired eyes.
*/
.hero h1 {
  /* `auto` top: see the note above -- the h1 is the hero's first item now and
     carries the slack that used to sit on the eyebrow. */
  margin: auto auto var(--gap-lg);
  /*
    9.8vw, 2026-09-08, up from 7.32vw. That is mindmarket.com's own hero
    ratio, measured off an 1884px capture: their headline is 185px with a
    ~135px cap height and ~170px of line spacing, which is 9.82vw. Griffin
    asked for that treatment on this sentence and this is it, arrived at by
    dividing rather than by taste.

    The three numbers that make it work are unchanged, because both references
    agree on them: leading below 1, tracking around -0.045em, and a weight
    heavy enough to hold at size. Those are what make a headline this large
    read as one object instead of as stacked lines.

    Bounded by the viewport's HEIGHT as well as its width, which is what
    "fill 80-85% of the screen" actually means. A width-only clamp overflowed
    the fold on any short viewport -- and short viewports are not an edge case
    here: layout.spec.js tests 614px and 560px because a 1366x768 laptop at
    125% Windows scaling gives ~614px of CSS height, and that is an ordinary
    machine belonging to an ordinary person.

    `min()` inside the clamp lets whichever axis is tighter win, so the height
    term is what actually binds on most screens and the vw term only reaches
    its number on a tall one. Measured, at every viewport the spec names:

      1920x1080  140px  3 lines  cue clears the fold by 78px
      1440x900   117px  3 lines  clears by 46px
       390x844    38px  3 lines  clears by 108px
       390x667    38px  3 lines  clears by 76px

    Anything shorter than 720px is the block further down, not this one.

    ## The second pass, and what actually limited it

    9.8vw / 13vh was the first attempt and Griffin said it was still not big
    enough. Walking the height term up did almost nothing, and the reason is
    that HEIGHT WAS NEVER THE BINDING CONSTRAINT. The hero was capped at 68rem
    inside a wrap that already spends `--pad` a side, so past about 1250px the
    column stopped growing at 1056px while the font size kept tracking the
    viewport. At 9.8vw a 1920 screen wants 188px of type in a 1056px column,
    which is five lines, and five lines never clear the fold. The clamp was
    getting the blame for a width problem.

    Widening the hero to the full frame (see the note on `.hero`) changed what
    is reachable completely. Measured, at every viewport the spec names:

      1920x1080  167px  4 lines  cue clears the fold by 31px
      1440x900   140px  3 lines  clears by 46px
       390x844    41px  3 lines  clears by 108px
       390x667    41px  3 lines  clears by 76px

    1280x720 is now 112px on TWO lines where it was 50px on two. That is the
    ordinary laptop, and it is where this pass changed the most.

    THE FLOOR DID NOT MOVE. 2.25rem is what the narrowest phone gets, and
    raising it was tried: at 320x568 the sentence is already four lines at
    39px and every point of type there comes out of the cue's clearance.

    The countdown sits under this and the two clear the fold together, so the
    height term is still the thing to touch if this ever needs to grow -- and
    site-layout.spec.js will tell you when you have gone too far. 15.5vh was
    found by walking it up until 1920x1080 stopped clearing.

    ## The leading, 2026-09-10

    0.92, up from 0.86. Griffin: the lines read "a little too squished". They
    did, and the handwritten day count is why -- the red block is now taller
    than the typeset digits were, so at 0.86 the line above it very nearly
    touched it. A headline this size still wants leading under 1 to read as
    one object rather than as stacked lines, which is what both references
    agree on above, so this is the top of that range rather than a departure
    from it.

    It costs about 6% of the headline's height, which comes out of the cue's
    clearance below. `.hero-cue` gives some of it back; site-layout.spec.js is
    what says whether the trade still clears the fold at every viewport.
  */
  font-size: clamp(2.25rem, min(10.6vw, 15.5vh), 12rem);
  font-weight: 700;
  line-height: 0.92;
  letter-spacing: -0.045em;
  text-wrap: balance;
}

/*
  The scroll affordance, and it is deliberately not subtle.

  A hairline that runs off the bottom edge plus a labelled arrow. The line is
  what does the work: an element visibly continuing past the fold is read as
  "there is more" by people who ignore arrows entirely.

  Present and legible with JavaScript off -- the nudge is a CSS animation on
  the arrow alone, and it stops under prefers-reduced-motion.
*/
.hero-cue {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  /*
    `margin: auto 0 0`, written as the shorthand ON PURPOSE. It was
    `margin-top: auto` followed by `margin: 0` a few lines later in this same
    rule, and the shorthand silently reset the longhand above it -- so the cue
    sat 212px short of the bottom of the hero with a slab of empty page under
    it. The auto top margin is what pins it to the foot of the 82svh box and
    lets the gap above absorb whatever the headline does not use.
  */
  margin: auto 0 0;
  padding-top: var(--gap-lg);
  /*
    `--ink-2`, not `--ink-3`, and this one is not a preference.

    At `--ink-3` the cue measured 4.88 against the paper with a rule behind
    it, where the bar is 7. `check_contrast.mjs` has said so since it was
    written; nobody saw it, because the CI step that runs it sat behind a
    browser-test failure and had never once executed. Found 2026-09-08, the
    first time that step reached the end.

    It is the worst possible element to have got wrong. DESIGN.md is explicit
    that this audience will not scroll unless something tells them to, and
    that this cue is what pays for the install CTA sitting below the fold on
    the landing page -- so a reader who cannot see it does not scroll, and
    never reaches the button the page exists for. Contrast sensitivity falls
    with age; 4.88 is exactly the range where that bites.

    `--ink-2` measures 7.20 on a rule, which is the case this sits in.
  */
  color: var(--ink-2);
  /*
    0.9375rem. It was 0.95rem, raised to 1rem on 2026-09-09 for the same
    reason `--ink-2` is here rather than `--ink-3`: this cue is what pays for
    the install button sitting below the fold, so a reader who does not
    register it never reaches the thing the page exists for.

    Griffin asked for it smaller on 2026-09-10, to buy back the fold space the
    headline's new leading spends. RAISED AS A CONCERN AND OVERRULED, which is
    his call -- but the trade is only safe because none of what makes the cue
    work is being touched: it keeps `--ink-2` at 7.20, weight 600, the
    tracking, the arrow and the hairline that does most of the actual work. 15px
    is still above the 14px floor DESIGN.md sets for a label, and it is a
    3.3% cut against a 7% gain in leading.

    If this ever needs to go lower, it is the wrong lever. Take the height
    term in the h1 clamp instead.
  */
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.hero-cue svg {
  width: 20px;
  height: 20px;
  animation: opa-nudge 2.1s ease-in-out infinite;
}
.hero-cue::after {
  content: "";
  width: 1px;
  height: var(--gap-md);
  background: linear-gradient(var(--hairline), transparent);
}
@keyframes opa-nudge {
  0%, 62%, 100% { transform: translateY(0); }
  78%           { transform: translateY(5px); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-cue svg { animation: none; }
}

/*
  SHORT VIEWPORTS. The hero's `min-height: 82svh` says nothing about the
  header sitting above it, so on a short screen the two together are taller
  than the fold and the scroll cue -- the thing that pays for the CTA being
  below it -- drops off the bottom.

  This is not an edge case. A 1366x768 laptop at 125% Windows scaling gives
  about 614px of CSS viewport, and less again once the toolbar and bookmarks
  bar are there; layout.spec.js already tests 614 and 560 for exactly that
  reason. It is an ordinary machine belonging to an ordinary person.

  Rather than shrink the headline further -- and this audience is the last one
  to take type away from -- the space comes out of the padding, which nobody
  has ever noticed the absence of, and out of the resting header, which is
  oversized precisely because there was room.

  RETUNED 2026-09-08, from `min(6.4vw, 7vh)` to `min(10.6vw, 13vh)`. The old
  numbers gave a 1280x614 laptop a 43px headline, which is barely larger than
  its own body copy, and they were correct when they were written: the 7vh
  term was set by 1280x560 landing 4px past the fold in a hero capped at
  68rem. Widening the hero to the full frame took the same sentence from three
  lines to two at that viewport, and two lines is a whole line of headroom.

  Measured on the real cascade, not with the clamp forced: 1280x614 goes from
  43px to 80px and 1280x560 from 39px to 73px, both still clearing the cue.
  The vw term deliberately matches the tall-screen clamp above, so the only
  thing that changes at the 720px boundary is the height cap.
*/
@media (max-height: 720px) {
  .hero { padding-top: var(--gap-sm); }
  .hero h1 {
    margin-bottom: var(--gap-sm);
    font-size: clamp(2rem, min(10.6vw, 13vh), 9rem);
  }
  .countdown { margin-bottom: var(--gap-sm); }
  .hero-cue { padding-top: var(--gap-sm); }
  .hero-cue::after { height: var(--gap-sm); }
}

/*
  The floating cards are a luxury of tall screens. Scoped above the phone
  breakpoint because the mobile block already replaces this header wholesale,
  and the two must not fight.

  Nothing is taken away here -- the two cards, the gap and the corner all
  survive. They just stop being 104px tall, because on a 1366x768 laptop at
  125% scaling the header and the hero have to clear the fold together and the
  header is the half nobody reads.

  Keep the mark's inset clear of the corner here too, or it escapes the curve
  -- see the note on `.bar-card`.
*/
@media (max-height: 720px) and (min-width: 40.0625em) {
  /* 52px mark + 2x0.55rem padding + border + 2x0.5rem margin. */
  :root { --bar-rest: 6.25rem; }
  .site-head { margin-block: var(--gap-xs); }
  .bar-card { padding-block: 0.55rem; }
  .mark-badge { width: 52px; height: 52px; }
  .mark-word { font-size: 1.6rem; }
  .bar-cta-disc { width: 44px; height: 44px; }
}

/*
  The day count is the one number on the page that changes, so it is the one
  thing set apart -- and since 2026-09-04 it is a solid block rather than
  coloured text, after becomearoboticist.com, whose hero does exactly this to
  the "28" in "CREATE ROBOTS IN 28 DAYS."

  Their values, measured: `background: #1a1a1a`, text in the paper colour,
  `padding: 0 20px`, square corners, at the full headline size.

  It was `--gold` text before. Gold reads as emphasis; a filled block reads as
  a fact that has been stamped, which is what a countdown is.

  THE BLOCK IS `--accent`, NOT `--ink`, since 2026-09-08. Griffin asked for a
  colour from the site's own theme instead of the black knockout, and of the
  four candidates in `:root` this is the only one that is both a real colour
  and clears the bar:

    --brand  #1b6ef3   3.94 against --ground   fails
    --gold   #a16207   4.22                    fails
    --accent #a83c17   5.42                    passes
    --night  #0b1220  16.06                    passes, but reads as black

  The bar here is 4.5, not the site's usual 7.0, and that is not a relaxation
  -- check_contrast.mjs applies the large-text ruling at >=24px, and this
  block never renders below 36px. Run it after touching either colour.

  It also does something the black did not. The footer's wax seal is #a81f1a,
  and --accent was chosen in the first place because it sits beside that red.
  The one stamped fact on the first screen and the one seal at the foot of the
  page are now the same gesture in the same colour, which is worth more than
  the contrast the knockout gave up.

  `0.06em` of horizontal padding rather than a flat 20px, so the block grows
  with the type instead of looking tight at 112px and bloated at 36px. The
  negative inline margin cancels the padding's effect on the sentence's
  rhythm, so "have 118 days" still reads as three words.
*/
.hero-days {
  display: inline-block;
  padding: 0 0.11em 0.04em;
  margin-inline: -0.03em;
  background: var(--accent);
  color: var(--ground);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/*
  ...AND THE NUMBER INSIDE IT IS GRIFFIN'S OWN, from 2026-09-10.

  Traced from brand/numerals-source.png by scripts/make_numerals.py, the same
  way his signature on the founder card is traced. Two takes of every digit,
  alternated, so the count reads as something written rather than something
  printed. The block behind it does not change: same --accent, same knockout,
  same padding, same measured 5.42 -- "so it appears how it is now just
  handwritten" was the brief, and the geometry was never the part in question.

  The rules above still apply. They describe the block, and opa.js swaps only
  what is inside it -- so with the script blocked or scripting off, the
  authored digits render exactly as they did before this existed.

  ## The three numbers this needs

  `--hand-cap` is the handwriting's cap height in em, and it is the only one
  chosen by eye. Source Sans caps are 0.66em; the writing is set a little
  larger because a marker stroke carries less black than a typeface does at
  the same cap height, and matched exactly it read as shrunken beside the
  words either side of it.

  `--hand-box` and `--hand-drop` are MEASURED, and opa.js sets them from the
  glyph table -- how tall the box is in caps, and how much of it hangs below
  the baseline. They are not written here because they are outputs of
  scripts/make_numerals.py, and a constant copied out of a generator is a
  constant that goes quietly wrong the next time the generator runs. The
  fallbacks are what the current trace measures, so the layout is still sane
  in the instant before the script has run.

  ## The drawing has to have a baseline, or the headline grows

  A block-level `<svg>` has none, and an inline-block wrapped round something
  with no baseline takes its BOTTOM EDGE as one. Done that way the red block
  hangs entirely above the line -- ~120px of it where the typeset digits put
  ~99 -- and the h1 opens up by 20px to make room, on the one screen in this
  site that is already fighting for fold space.

  So the drawing is inline, lowered by exactly the part of its box that sits
  below the writing's own baseline. That puts the WRITING's baseline on the
  line's, which is what the eye reads and what the typeset digits did. The
  parent takes `line-height: 0` so the invisible strut of the h1's own font
  cannot pad the block out past the ink it is drawn around.

  All of it is scoped to `[data-hand-drawn]`, the attribute opa.js sets once
  it has actually drawn something. With the script blocked the typeset digits
  get none of this and sit exactly where they always did.
*/
.hero-days[data-hand-drawn] {
  --hand-cap: 0.60em;
  line-height: 0;
  /*
    The red is given its own breathing room, because the drawing is cropped to
    its ink and brings none. Griffin asked for more of it, 2026-09-10, and for
    the writing to come down slightly to make room rather than the block
    growing to hold it -- so `--hand-cap` fell from 0.68em with this.

    opa.js crops the drawing to the ink on all four sides, so these two are
    the whole of the red and can be read against each other. Slightly more
    at the sides than above and below, which is what a block of any kind
    wants: equal measurements read as tight at the sides, because the eye
    compares the gap beside a stem against the gap above a full-width run of
    them.
  */
  padding-block: 0.14em;
  padding-inline: 0.17em;
}
.hand-num {
  display: inline-block;
  vertical-align: calc(var(--hand-cap) * var(--hand-box, 1.22) * var(--hand-drop, 0.12) * -1);
  height: calc(var(--hand-cap) * var(--hand-box, 1.22));
  width: auto;
  fill: currentColor;
}

/*
  The glyph table itself. It draws nothing: it is twenty <path>s in a <defs>,
  referenced by <use>. Sized to nothing in CSS as well as in its attributes,
  because a bare <svg> is a replaced element and would otherwise reserve the
  300x150 the spec gives one with no intrinsic size.
*/
.hand-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/*
  The A/B flick, shared with the signature on the founder card -- same
  keyframes, defined once beside `.founder-name`. Two takes of one hand
  alternating reads as the mark being redrawn; it is a trick lifted from
  alaris.studio and it only works because both takes are genuinely his.

  Every glyph flicks on the SAME clock, so a three-digit count changes hand
  all at once rather than rippling. That matters more here than on the
  signature: a number whose digits disagree about which take they are in
  reads as a glitch.
*/
.hand-a { animation: opa-hand-a 900ms step-end infinite; }
.hand-b { animation: opa-hand-b 900ms step-end infinite; }

@media (prefers-reduced-motion: reduce) {
  .hand-a,
  .hand-b { animation: none; }
  .hand-a { visibility: visible; }
  .hand-b { visibility: hidden; }
}

/* --------------------------------------------------------------------------
   Countdown
   --------------------------------------------------------------------------
   One connected strip, not four dashboard cards — four boxes read as a
   dashboard, and this is a sentence. The numerals are 30px on a phone, which
   is the floor the design doc sets, and tabular so the strip does not jitter
   as digits change width.
*/

/*
  IN THE HERO, directly under the headline. Griffin's call, 2026-09-04,
  reversing a pass that had moved it to the foot of the demo section on the
  argument that it merely repeated the headline's number.

  It does repeat it, and that is the point: the headline states the fact, the
  strip makes it tick. A number that moves does something a sentence cannot,
  and urgency is the one job this page's first screen has.

  What the decision costs is fold space, so the hero is sized around it rather
  than the other way round -- see the height term in the `.hero h1` clamp, and
  the compact treatment below. If anything else is ever added to the hero, the
  scroll cue is what falls off the bottom first.
*/
.countdown { margin: 0 0 var(--gap-md); }

.countdown-strip {
  display: inline-flex;
  align-items: baseline;
  gap: var(--gap-sm);
  margin: 0;
  /*
    Tightened twice. It was 107px when it came back into the hero, then 95px,
    and it is ~74px now -- 2026-09-08, to buy the headline its height.

    Griffin wanted the headline at the reference's scale AND the countdown on
    the first screen. Those compete for the same pixels, and once the eyebrow
    had gone and the sub-line had come down this was the only compressible
    thing left. What it costs is padding, which nobody has ever noticed the
    absence of; what it buys is about 24px of headline, which is the
    difference between three lines and three larger lines.

    The numerals did not shrink. They are the thing the strip is for.
  */
  padding: 0.45rem var(--gap-lg);
  /*
    Light, since 2026-09-04. It was a filled dark card, which put two dark
    blocks directly on top of each other once the headline's day count became
    a knockout block -- and both of them showing the same number. One thing on
    this screen gets stamped, and it is the sentence.

    A PILL, 2026-09-08. It was a 1px black rectangle, which is the one shape
    on this site that belongs to nothing else: every other outlined object --
    the secondary button, the nav card's action, the badges -- is a 999px pill
    with a 2px slate border. A hard black rectangle in the middle of that reads
    as a browser default rather than as a decision, and on the first screen
    that is the worst place for it.

    The values are `.cta-2`'s verbatim, because they are the ones already
    measured. The horizontal padding went up a step with the radius: a pill
    needs its content clear of the curve or the first and last numerals look
    like they are falling out of it.
  */
  border: 2px solid rgba(100, 116, 139, .24);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
}

.countdown-unit {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  min-width: 3.4ch;
}
.countdown-unit b {
  font-size: clamp(1.5rem, 1rem + 1.6vw, 2.25rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
/*
  BLACK, 2026-09-08, at Griffin's instruction, reversing the accent it carried.

  The reason it is right: the headline directly above already stamps the same
  number in `--accent` on a filled block, and it is the same number. Two reds
  showing one fact on one screen is the accent spending itself -- the second
  one cannot add emphasis, it can only dilute the first. The strip's job is to
  make the number tick, and `--ink` does that at full contrast.

  So the days figure is not styled apart from hours and minutes any more. It
  does not need to be: it is the first thing in the row and the widest number
  in it, and the row is read left to right.
*/
.countdown-unit:first-child b { color: var(--ink); }
.countdown-unit i {
  margin-top: 0.2rem;
  /* 16px floor for anything carrying meaning — the site's own rule, held here
     against the 11px this pattern usually uses. */
  font-size: 1rem;
  font-style: normal;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.countdown-sep {
  font-size: clamp(1.5rem, 1rem + 1.4vw, 2rem);
  font-weight: 700;
  line-height: 1;
  color: var(--ink-3);
  transform: translateY(-0.35rem);
}

/*
  Only the seconds page moves, and only for a sixth of a second. The days,
  hours and minutes hold still so the deadline stays readable rather than
  becoming something that has to be caught between flickers.
*/
.is-seconds b { display: inline-block; }
.countdown-unit.is-ticking b { animation: opa-tick 160ms ease-out; }
@keyframes opa-tick {
  from { transform: translateY(-22%); opacity: 0.45; }
  to   { transform: none;             opacity: 1; }
}

/*
  The sub-line, and it is deliberately NOT a second headline.

  It went to 1.5rem on 2026-09-08 and back down the same day: Griffin's words
  were that it "doesn't really need to have any boldened text nor does it have
  to be as large as the main text". Both halves of that are now true -- the
  boxed `always asks` is gone, and this is 1.25rem under a headline four to
  six times its size.

  The reference's own sub-line is a cliff rather than a step: 39px under 185px,
  about a fifth. This is nearer a seventh, because our headline is nine words
  where theirs is three and the gap between the two has to be unmistakable at a
  glance. 20px is still four above this site's body floor.
*/
.hero-solution {
  margin: 0 auto;
  max-width: 34rem;
  font-size: 1.25rem;
  line-height: 1.5;
  color: var(--ink-2);
}

/*
  The closing date, under the countdown. It is what the blue eyebrow above the
  headline used to say, at a fifth of the prominence: the countdown answers
  "how long", and this answers "until when", which a ticking number never does
  on its own. Small, tracked, and `--ink-2` rather than `--ink-3` so it clears
  the 7.0 bar an unrecognised paragraph is held to.
*/
/* 1rem, up from 0.9375rem on 2026-09-09. This is a DATE, on the first screen,
   answering the one question the ticking number cannot -- and the floor this
   file states twice and holds for `.countdown-unit i` directly above and for
   `.ed-label` is 16px for anything carrying meaning. Tracking comes down a
   step to pay for the width, which is what tracking is for at this size. */
.hero-closes {
  margin: 0.6rem 0 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* The 40-second promise rides inside the button rather than beside it, so the
   button says exactly what pressing it costs. */
.cta .cta-sub {
  display: block;
  font-size: 0.9375rem;
  font-weight: 600;
  opacity: 0.82;
  letter-spacing: 0.01em;
}

/* --------------------------------------------------------------------------
   The demo
   --------------------------------------------------------------------------
   The only other thing on the home page. Centred like the hero above it, and
   sized generously — this is the whole selling point now, not one section
   among several.
*/

.demo {
  text-align: center;
}
.demo h2, .demo .lede { margin-left: auto; margin-right: auto; }

/*
  The demo section's heading and paragraph sit LOWER in their band than every
  other section's do -- 2026-09-08, the other half of the change under
  `.demo-frame`.

  The reason is that this section is one object rather than a heading with prose
  under it: the clip is the content, and the two lines above it are its title.
  Giving them the same leading space as a section of running prose left them
  floating at the top of a mostly empty band with the thing they name a long way
  below. Pushing them down attaches them to it.

  Only the top moves. The space between the text and the clip is set by
  `.demo-frame`'s margin, which the tape constrains.
*/
#see-it > .ed-main { padding-top: var(--gap-xl); }

/*
  WIDER, 2026-09-04. The walkthrough is the product demonstration, and it was
  sitting at 60rem inside a 76rem frame -- narrower than the text above it,
  which reads as an embedded clip rather than as the thing the page is for.

  It breaks the frame to near-viewport width and is bounded by the viewport
  rather than by a fixed number, so it cannot cause horizontal overflow at any
  width. The 76rem cap keeps it from becoming a letterbox on a very wide
  monitor, where a 2500px-wide video is worse, not better.
*/
.demo-frame {
  position: relative;
  /*
    THE HOLE ABOVE THE CLIP -- 2026-09-08.

    This was `--gap-3xl`, 80px, on top of the section's own 80px of padding and
    the paragraph's margin: about 210px of page between the top of the band and
    the top of the video, most of it doing nothing. Griffin asked for the empty
    space fixed and the heading and its paragraph moved down into it, and he is
    right that it read as a gap rather than as spacing.

    It is `--gap-2xl` now, 48px, and the tape was re-cut to fit inside that.
    The two numbers are coupled and the coupling is the trap: the strips hang
    ABOVE the clip, so this margin is their headroom, and at `--gap-xl` an
    earlier pass landed them on the paragraph. Measured at 1440px, the top
    strip reaches about 40px above the clip, which clears 48px. If the strips
    are ever lengthened or steepened, this grows with them -- both of a rotated
    box's dimensions feed its bounding height.
  */
  margin: var(--gap-2xl) auto var(--gap-lg);
  /*
    Full width of the frame, not wider. An earlier pass used
    `min(94vw, 76rem)`, which is wider than the `.wrap` column it lives in --
    so it hung off the right edge at every width and pushed the page into
    horizontal scroll at 320px. Breaking out of a centred column needs the
    `.bleed` pattern above, not a bigger width.

    76rem minus the wrap padding is ~1168px on a 1440 screen, which is still
    84% of the viewport and a third wider than the 60rem it replaced.
  */
  width: 100%;
  max-width: none;
  text-align: left;
}
/*
  The clip's own box: the video and the one control that sits on it, and
  nothing else. It is what the tape is positioned against.
*/
.demo-clip {
  position: relative;
  /*
    Reserved before the poster loads, so the tape is never briefly positioned
    against a zero-height box. Matches the video element's own width and
    height attributes.

    NO ANGLE BRACKETS IN THIS FILE'S COMMENTS, and this one earned the rule
    twice over. The stylesheet is inlined verbatim into index.html, and
    check_site.mjs finds the demo clip by regex on the whole document. A
    bracketed tag name written in a comment up here becomes the FIRST match in
    the file, so the gate inspects the comment, finds no autoplay attribute in
    it, and reports that the video does not autoplay -- true of the comment,
    false of the page. The first fix was a comment explaining the trap, which
    quoted the regex, which contains the same bracketed tag name, and tripped
    it again.
  */
  aspect-ratio: 1440 / 810;
}

/*
  The border was `--hairline`, which is #e6e0d5 -- a warm cream line drawn
  round a clip whose own first frame is a white government form, sitting on
  the white band. Measured against both it is under 4% of contrast, so the
  centrepiece of the page had no visible edge at its top or its left and the
  shape only became readable where the player's own dark control bar starts.

  Same fix as the portrait above and the same reasoning: 1px of pure black at
  10%, as an `outline` with a negative offset so it costs no layout and hugs
  the 26px corner from the inside rather than boxing it. A tinted ring belongs
  to the surface it borrowed its colour from; a neutral one belongs to the
  picture. 2026-09-09.
*/
.demo-frame video {
  display: block;
  height: 100%;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
  background: var(--night);
  outline: 1px solid rgba(0, 0, 0, 0.1);
  outline-offset: -1px;
  box-shadow: var(--lift-3);
}

/*
  One tap to turn sound on. See index.html's comment: the video autoplays
  muted because no browser will autoplay sound without a prior gesture, and
  this is that gesture, offered plainly rather than left for someone to guess
  at the native player's own icon.
*/
.sound-toggle {
  position: absolute;
  /*
    TOP-LEFT, not top-right. The OPA panel sits on the right of the frame in
    the clip, and that panel is the entire thing the viewer is being asked to
    watch -- a control parked on top of it hides the product in its own demo.
    The left of the frame is the government form's own chrome, which is
    context rather than content.
  */
  /*
    2.75rem -- 2026-09-08, and it is a measured value between two things it
    must not touch.

    It was 3.5rem to clear a tape strip that was then at the TOP-LEFT. The
    strips moved to opposite corners (top-right and bottom-left), so that
    reason is gone and this came up. It went all the way to `--gap-md` first,
    which was too far: the poster's top strip is the browser chrome, and at
    16px this button sat straight across "Government of Western Australia --
    Department of Transport", which is the line that tells a viewer what they
    are looking at.

    44px clears that title and lands on the form's blue header band, which
    carries no text at that end. Below it is the form's own heading, which is
    what 4.5rem hit in an earlier pass. The window is about 30px wide and this
    is the middle of it.
  */
  top: 2.75rem;
  left: var(--gap-md);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.6rem 1.1rem;
  border: 0;
  border-radius: 999px;
  background: rgba(11, 18, 32, 0.82);
  color: #fff;
  font-family: var(--font);
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
}
.sound-toggle:hover { background: rgba(11, 18, 32, 0.95); }
/*
  `display: inline-flex` above beats the UA stylesheet's
  `[hidden] { display: none }`, so opa.js setting `button.hidden = true` on a
  silent clip did nothing at all and the control stayed on screen.

  It is the second time this exact pairing has bitten in one day — the panel's
  own `.opa-btn` hid Stop and Undo the same way, see docs/DESIGN.md. Any rule
  that sets a display value on something JavaScript can hide needs this line
  beside it, and neither instance was visible by reading the CSS: both were
  found by rendering the page and noticing something that should not be there.
*/
.sound-toggle[hidden] { display: none; }

/*
  A recording light, modelled on twotone.au's archive page: a red dot that is
  LIT while audio is playing and dark while it is muted.

  The reason it is a light rather than a speaker icon is the audience. Someone
  who is 70 has spent a lifetime around tape decks and camcorders, where a red
  light means "this is live, sound is happening". That reading is immediate and
  needs no learning; a crossed-out speaker glyph has to be decoded.

  It never carries the state alone -- the button's label changes, and
  aria-pressed changes with it -- so nothing here is colour-only information.
*/
.sound-dot {
  width: 11px;
  height: 11px;
  margin-right: 0.6rem;
  border-radius: 50%;
  flex: none;
  background: #6b4340;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22);
  transition: background 220ms ease, box-shadow 220ms ease;
}
.sound-toggle[aria-pressed="true"] .sound-dot {
  background: #ff3b30;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.35),
    0 0 0 4px rgba(255, 59, 48, 0.22),
    0 0 12px 2px rgba(255, 59, 48, 0.75);
  animation: opa-rec 1.8s ease-in-out infinite;
}
@keyframes opa-rec {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.62; }
}
@media (prefers-reduced-motion: reduce) {
  .sound-toggle[aria-pressed="true"] .sound-dot { animation: none; }
}

/*
  THE SHORT VERSION -- 2026-09-08.

  Griffin: "overall text size and effect needs to be changed so we can get all
  the important info out on the home page and can keep the descriptive aspects
  to the trust pages etc. and the demonstration and explanation of the process
  can be left to the video."

  Three instructions in one sentence, and this block is the first of them. The
  landing page carried the deadline, a clip and a founder -- and not one of the
  four facts a person actually decides on. What it refuses to do lived only on
  /safety/, and the price lived only on /install/, which is a page you reach by
  having already decided.

  ## The heading went, and that was the test

  It read "Four things worth knowing." Griffin's instruction was to write the
  statements so clearly "that u dont even need the words", which is a good test
  and this block now passes it: a heading that only announces that a list is
  about to happen is a heading doing no work.

  So each item leads with the CLAIM, set as a small heading in its own right,
  and follows with the proof underneath. The claim is the part someone scanning
  reads; the line under it is there for the one who stops. That split is what
  makes the section legible without a title on top of it, and it is why the
  bold is on a whole sentence rather than on a phrase in the middle of one --
  a boxed word inside a paragraph makes you read the paragraph to find out
  what it modifies.

  Two columns above 62em. Four full-width rows made a 26px claim look like a
  heading for the row under it rather than a statement of its own.

  Ruled above each item rather than bulleted. A bullet says "here is a list of
  features"; a rule says "here are four separate statements", which is what
  they are, and it is the same hairline the rest of the page divides with.

  If a fifth is ever added, one of the five is not important.
*/
.fact-list {
  display: grid;
  gap: var(--gap-xl);
  list-style: none;
  margin: 0 0 var(--gap-2xl);
  padding: 0;
  max-width: none;
}
@media (min-width: 62em) {
  .fact-list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--gap-2xl) var(--gap-3xl);
  }
}
.fact-list li {
  margin: 0;
  padding-top: var(--gap-md);
  border-top: 1px solid var(--hairline);
  max-width: 30rem;
  /* Body size, from 2026-09-09. It was 1.0625rem, which is one pixel under
     the body floor this site treats as a product feature -- a step too small
     to read as hierarchy and big enough to read as a mistake. The proof line
     under each claim is already quieter than the claim by weight, size and
     colour; it does not also need to be a pixel short. */
  font-size: 1.125rem;
  line-height: 1.55;
  color: var(--ink-2);
}
.fact-list b {
  display: block;
  margin-bottom: 0.3rem;
  /* "It keeps nothing about you." otherwise breaks after "about", which
     orphans one word under a three-word line. */
  text-wrap: balance;
  font-size: clamp(1.375rem, 1.05rem + 1vw, 1.875rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   The packaging tape
   --------------------------------------------------------------------------
   Two strips across OPPOSITE corners -- top right and bottom left -- angled,
   holding the clip down. Griffin's brief, 2026-09-08: "slightly shiny and
   translucent, so the video is clearly visible beneath the corners", and then,
   on seeing both of them at the top, "move the tape top right and bottom left
   so opposite ends".

   Diagonally opposed is also how a thing actually gets taped down: two strips
   on the same edge hold one edge and the sheet still lifts. Opposite corners
   pin it.

   ## Why tape, on this site

   It is the same argument as the wax seal in the footer and the ruled paper
   under everything. This audience is being asked to trust a piece of software
   with a government form, and every object on this page that behaves like a
   physical thing -- paper, a stamp, a seal, now tape -- is doing the same
   quiet work. A clip that is taped down is a clip somebody put there.

   ## How it is built

   Two pseudo-elements, so there is no markup for it and nothing to hide from
   assistive tech: pseudo-elements are not in the accessibility tree and this
   carries no information. `pointer-events: none` so the strips cannot swallow
   a click meant for the video underneath.

   Three layers make it read as tape rather than as a grey rectangle:

     1. A translucent body, warm and slightly GREYED rather than white. The
        first pass was mostly white and it disappeared: the clip's poster is a
        screenshot of a government form, which is white, so white tape over a
        white frame is nothing at all, and over the paper beside it read as a
        smear. Tape is not white -- it is a warm grey you can see through --
        and getting that right is what makes it read on both grounds at once.
     2. A specular streak running across it at a slightly different angle from
        the strip itself, which is what "shiny" actually looks like -- a
        highlight that does not run parallel to the edges.
     3. Darker lines along the long edges, where real tape thickens and
        catches shadow.

   `backdrop-filter` is what makes it translucent rather than transparent: a
   1px blur plus a touch of brightness diffuses whatever is under it, which is
   the difference between tape and tinted glass. It degrades to plain
   translucency where it is unsupported, which is fine -- nothing depends on
   it.

   ## The zigzag

   `clip-path` cuts teeth into both short ends, because tape off a dispenser
   is never cut straight and a straight-cut strip reads as a rectangle someone
   drew. The teeth are on the SHORT edges only; teeth along the long edges
   would be a serrated ribbon, not tape.

   `filter: drop-shadow()` rather than `box-shadow`, because `clip-path` clips
   a box-shadow away with the box -- the shadow has to be applied to the
   already-clipped shape, which is what the filter pipeline does and the box
   model does not.

   ## What it cost, and what it must not cost

   The strips hang above the clip, so `.demo-frame` gained top margin to make
   room; without it the tape overlapped the paragraph above. They are also
   shorter than the first pass, which is the horizontal constraint below
   showing up as a length: a strip long enough to centre on the corner cannot
   fit, so it is short enough to cross it instead.

   HORIZONTALLY THEY STAY INSIDE THE FRAME, and that is a hard constraint
   rather than a preference. Real tape would hang off both sides, but the
   figure already spans the full width of the content column, so anything past
   its edge lands in the wrap's padding -- and at 320px that padding is 24px,
   so a strip reaching further would put the whole page into horizontal
   scroll. site-layout.spec.js checks exactly that at eight widths. -12px of
   overhang against a 24px floor is the margin this is allowed.
*/
.demo-clip::before,
.demo-clip::after {
  content: "";
  position: absolute;
  z-index: 3;
  /*
    SHORTER, AND HUNG OFF THE CORNERS -- 2026-09-08.

    Griffin: the tape "is hiding user functionality such as the pause button
    and some of the walkthrough itself". Both were true, and the reason was the
    placement rather than the tape. The strips were 12.5% of the clip's width
    lying almost flat ALONG THE INSIDE of each corner, so the bottom-left one
    ran straight across Chrome's own play button and the top-right one sat over
    the OPA panel's header -- the one thing in the frame a viewer is being
    asked to watch. `pointer-events: none` meant the button still worked, which
    is not the same as the reader being able to see it, and on this site of all
    sites that distinction is the entire argument.

    ## What the geometry has to satisfy

    Chrome draws its play button about 25px in from the left and 12-36px up
    from the bottom edge. The strip has to be clear of that box at x = 25.

    With the strip centred near the corner at 30 degrees, its high end is the
    one pointing OUT of the clip, so by the time the ink is over x = 25 it has
    dropped to about 9px above the bottom edge -- under the button rather than
    over it. That is the whole trick, and it is why the angle came DOWN rather
    than up: a steeper strip reaches further vertically at the same length
    (`w*sin + h*cos` is its bounding height), and vertical reach is exactly
    what puts it back on the controls.

    Shorter helps twice over: 9.5% instead of 12.5%, about 106px on a 1120px
    clip. A strip holds a corner down; it does not need to reach a third of
    the way along an edge.
  */
  width: clamp(96px, 9.5%, 148px);
  height: clamp(26px, 2.5vw, 36px);
  pointer-events: none;
  background:
    /* the highlight, deliberately off-axis from the strip */
    linear-gradient(101deg,
      transparent 6%,
      rgba(255, 255, 255, .46) 24%,
      rgba(255, 255, 255, .12) 40%,
      transparent 56%),
    /* the body, and the thickened long edges */
    linear-gradient(180deg,
      rgba(126, 114, 96, .40) 0,
      rgba(224, 217, 204, .46) 9%,
      rgba(205, 197, 182, .36) 50%,
      rgba(228, 222, 210, .44) 91%,
      rgba(126, 114, 96, .36) 100%);
  backdrop-filter: blur(1.2px) brightness(1.02) saturate(1.08);
  -webkit-backdrop-filter: blur(1.2px) brightness(1.02) saturate(1.08);
  filter: drop-shadow(0 2px 5px rgba(24, 18, 12, .22));
  /* Teeth on the two short ends: a dispenser cut, not a scissor cut. */
  clip-path: polygon(
    3% 0%, 100% 0%,
    97% 11%, 100% 22%, 97% 33%, 100% 44%,
    97% 56%, 100% 67%, 97% 78%, 100% 89%, 97% 100%,
    0% 100%,
    3% 89%, 0% 78%, 3% 67%, 0% 56%,
    3% 44%, 0% 33%, 3% 22%, 0% 11%
  );
}
/*
  THE HORIZONTAL OVERHANG IS THE ONE THAT CAN BREAK THE PAGE.

  The figure spans the full content column, so anything past its edge lands in
  the wrap's padding -- and a strip reaching further than that puts the whole
  document into horizontal scroll. `site-layout.spec.js` checks exactly that at
  eight widths.

  The old rule answered this with a flat -12px, which is the budget of the
  NARROWEST viewport applied to every viewport. That is why the tape never
  looked hung off anything: at 1440px there are 80px of page margin sitting
  unused beside a strip allowed to use twelve of them.

  So the overhang tracks `--pad`, which is the padding that is actually there:
  24px on a phone, 5rem past 62em. `min()` caps it at 46px, because past about
  half the strip's length it stops reading as tape on a corner and starts
  reading as tape beside a video. The `- 12px` keeps a margin against the page
  edge at the narrow end, where --pad is 24 and the answer is 12 -- exactly
  what the flat value used to be, which is the point: nothing gets tighter,
  the wide case simply stops being punished for the narrow one.

  Vertical overhang is nearly free by comparison -- it spends the figure's own
  margin, which `.demo-frame` carries for this -- but it is NOT unlimited: it
  is what the strips reach across into the picture, and above the clip it is
  bounded by the paragraph. -18px is about half the strip's thickness, which
  is enough to sit the strip on the edge rather than inside it.
*/
.demo-clip::before,
.demo-clip::after {
  --tape-out: min(46px, calc(var(--pad) - 12px));
}
/* Top right, across the panel's outside corner rather than its header. */
.demo-clip::before {
  top: -18px;
  right: calc(-1 * var(--tape-out));
  transform: rotate(30deg);
}
/*
  Bottom left, and this is the corner the change was made for.

  Chrome draws its play/pause button at the bottom-left of the video, about
  25px in and 20px up. The strip used to lie along that edge and cross straight
  over it. It was `pointer-events: none`, so the button still WORKED -- but a
  control you cannot see is a control this audience does not have, and that is
  the whole argument of this site applied to its own demo.

  Hung below the corner it clears the control bar's left end instead of lying
  on it.

  An earlier pass tried to place this against `.demo-frame` with a percentage
  offset derived from the clip's 16:9 ratio. It landed 200px high, because a
  percentage `top` resolves against the containing block's HEIGHT -- and the
  figure's height is the clip plus the caption under it. `.demo-clip` exists so
  that `bottom` means the bottom of the video.
*/
.demo-clip::after {
  bottom: -18px;
  left: calc(-1 * var(--tape-out));
  transform: rotate(30deg);
}

/*
  A reader who has asked for more contrast has told us that translucent things
  sitting over content are costing them. The tape is the most decorative object
  on the page and carries nothing, so it is the first to go -- the same call
  the ruling gets.
*/
@media (prefers-contrast: more) {
  .demo-clip::before,
  .demo-clip::after { display: none; }
}

/*
  The honesty caveat, as a CAPTION -- 2026-09-08.

  It was a gold panel with a 4px rule, a wash and a "Real in this clip / Not
  real" split across four lines. That treatment came from a true worry -- a
  caveat set in 12px grey is a caveat designed not to be read -- and it
  overshot: a warning-coloured slab directly under the product demonstration is
  the loudest object in that half of the page, and what it shouts is that
  something is wrong with the clip.

  Nothing is wrong with the clip. The sentence is a fact about how it was made,
  and a caption is what a fact about a figure is.

  So: the wash, the gold rule and the two bold labels are gone. What holds it
  above fine print is size and colour, not decoration -- 17px, which is only one
  step under the 18px body floor, in `--ink-2`, which is the body's own colour
  and clears 7.0 on a rule. It reads as the figure's caption, which is what it
  is, and it is still the first thing under the video.

  Left edge flush with the clip above it, and capped to a readable measure
  rather than running the full width of a 1168px video.
*/
.demo-frame figcaption {
  /*
    Clears the bottom-left strip, which now hangs about 46px below the clip at
    its lowest point. This margin is that number rounded up to a token; if the
    tape is ever lengthened, measure again before shortening this.
  */
  margin-top: var(--gap-2xl);
  max-width: 44rem;
  margin-inline: 0;
  text-align: left;
  /* 1rem, 2026-09-09: the size the site's other two captions already use
     (`.figure figcaption` and `.ed-figure figcaption`). At 1.0625rem this was
     a third caption size, a pixel off the other two and a pixel off body --
     a distinction nobody can see and a rung nobody needs. */
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink-2);
}

/* --------------------------------------------------------------------------
   Responsive
   --------------------------------------------------------------------------
   The base sets type sizes for the subpages; the landing page's display sizes
   want more headroom on a wide screen. Kept global rather than scoped because
   the subpages benefit from the same scale and previously got it from the
   layer this one replaced.
*/

/*
  TWO HEADING SIZES, NOT ONE -- corrected 2026-09-09.

  This block used to be one line, `h2 { font-size: 2.25rem }`, under a comment
  saying the landing page's display sizes "want more headroom on a wide
  screen". It did the opposite. The base `h2` two thousand lines up is a fluid
  clamp topping out at 60px, written with an explicit note that 60 is "one
  clear step below the hero so the two keep their order" -- and this rule
  meant it never once reached it on a desktop. Every h2 on the site was a flat
  36px past 992px wide.

  What that cost is a rung. Measured on the landing page at 1440: the headline
  is 139px and the next thing down is 36px, with nothing in between, so a
  section heading arrived at barely twice body size and read as a label rather
  than as the start of something. It is the reason the middle of that page
  went slack.

  The flat size was not wrong, though -- it was right for the place it was
  written for and wrong everywhere else. A heading in the editorial grid
  shares a 20rem rail with its section label, and 20rem is 320px:
  "Watch how it works." is exactly one line at 36px there and three lines at
  53px. So the rail keeps 36.

  A section that OPTED OUT of the rail is the other case. `.ed-wide`,
  `:has(.qa)`, `:has(.ed-cols)`, `:has(.ed-grid)` and the founder section all
  set `.ed-main { display: block }`, which hands their heading the column's
  full width -- and a heading with 688-1056px to sit in should not be wearing
  a size cut to fit 320. Those get the scale back, capped at 52px rather than
  the base clamp's 60 so the subpage h1 at 76px keeps its order.

  The result is a scale with no hole in it: 139 / 76 / 52 / 36 / 22 / 18.
*/
@media (min-width: 62em) {
  /* The rail's size. Two or three words in 20rem. */
  h2 { font-size: 2.25rem; }

  /* A heading that was given the frame is set for the frame. */
  .ed-wide .ed-main > h2,
  .ed:has(.qa) .ed-main > h2,
  .ed:has(.ed-cols) .ed-main > h2,
  .ed:has(.ed-grid) .ed-main > h2,
  .ed:has(.ed-side .founder-media) .ed-main > h2 {
    font-size: clamp(2.25rem, 1.35rem + 2.15vw, 3.25rem);
  }
}

@media (max-width: 60em) {
  .hero { padding-top: var(--gap-2xl); }
}

@media (max-width: 40em) {
  .section-block { padding: var(--gap-2xl) 0; }

  .countdown-strip {
    display: flex;
    width: 100%;
    justify-content: space-between;
    gap: var(--gap-xs);
    padding: var(--gap-md);
    /*
      `min-width: 0` is load-bearing, not tidiness. `.hero` is a flex column,
      and a flex item's default `min-width: auto` refuses to shrink below its
      min-content width -- so this strip, whose four labels cannot wrap, pushed
      the hero to 338px inside a 320px viewport and put the whole page into
      horizontal scroll. The offender the reflow test names in that state is
      the off-screen skip link, which is a red herring; this is the cause.
    */
    min-width: 0;
    /*
      And it must be allowed to wrap. `min-width: 0` lets the strip itself
      shrink, but the units inside it still cannot: their labels are single
      words that do not break. Under text-only zoom -- an older reader who has
      raised their browser font size, which is exactly this audience -- a 32px
      root turns "MINUTES" into ~140px and three of those never fit a 390px
      phone on one row. Wrapping is the correct reflow; overflowing is not.
    */
    flex-wrap: wrap;
    row-gap: var(--gap-sm);
    /*
      The pill is a one-row shape. Here the strip is full width and allowed to
      wrap, and a 999px radius on a box that has become two rows tall puts a
      65px curve where the first numeral sits. `--radius-lg` is the card
      radius the header already uses, so the shape stays part of the same
      family without eating its own content.
    */
    border-radius: var(--radius-lg);
  }
  .countdown-unit { min-width: 0; }
  /* A separator stranded at the end of a wrapped row reads as a typo. */
  .countdown-sep { align-self: center; }

  .hero-solution { font-size: 1.1875rem; }

  .sound-toggle { font-size: 0.9375rem; padding: 0.5rem 0.9rem; }
}

/*
  ON A PHONE THE SECONDS UNIT GOES -- widened from 22.5em to 40em, 2026-09-09.

  Four labels at the 16px floor come to 237px of text before any padding.
  That was reasoned about at 320px and it is wrong by 390px too, which is the
  width most of this audience is actually holding: measured on an iPhone-sized
  viewport the strip wrapped to two rows, put SECONDS alone on the second one,
  and left the third colon stranded at the right-hand end of the first --
  which the rule directly above this one exists to prevent, and calls "a
  typo". A pill that has become two ragged rows under the biggest sentence on
  the site is the worst-placed thing on the page to get wrong.

  Seconds is the right thing to drop, and the argument is unchanged: on a
  deadline measured in months it is the one unit that tells a reader nothing
  they can act on. Three units and two separators fit one row at 320px with
  room to spare, so this also retires the two-row case rather than tidying it.
  The separator before it goes with it, via `:has()` -- and if a browser does
  not support that, the result is a stray colon rather than a broken layout,
  which is the correct way for this to fail.
*/
@media (max-width: 40em) {
  .countdown-unit.is-seconds,
  .countdown-sep:has(+ .countdown-unit.is-seconds) { display: none; }
}

/* ---------- the install page ---------- */

/*
  REBUILT 2026-09-04: one clear button rather than a three-step guide with
  inline diagrams of the Chrome Web Store flow. Centred like the home page's
  hero, for the same reason — this page now has exactly one job above the
  fold, and everything else (what you need, troubleshooting, uninstalling)
  is ordinary `.section-block` prose below it.
*/
.install-hero {
  max-width: 36rem;
  margin: 0 auto;
  padding-top: var(--gap-2xl);
  padding-bottom: var(--gap-xl);
  text-align: center;
}
.install-hero .lede { margin: 0 auto var(--gap-lg); }
.install-hero .note { text-align: left; }

/*
  Sized past the ordinary `.cta` floor — this is the one button the whole
  page exists to lead someone to, and a page whose entire job is one press
  should make that press impossible to miss.
*/
.cta-install,
.cta-buy {
  font-size: 1.3125rem;
  padding: 1.3rem 2.75rem;
  min-height: 68px;
}

/*
  `.cta-buy` is `.cta-install`'s twin in every visual respect and a different
  class on purpose. `.cta-install` is documented above as "the one button the
  whole page exists to lead someone to", and site-layout.spec.js pins exactly
  that -- it asserts `.cta-install` is on the first screen at 390x844 and
  390x667. Giving the purchase button the same class made that locator match
  two elements and the assertion died on a strict-mode violation, which is the
  test correctly noticing that the page now had two "one button"s.

  They are steps, not alternatives: add the extension, then buy the key. Only
  the first has to be above the fold.
*/

/*
  Printing matters on this page more than anywhere else on the site. A
  70-year-old being helped by an adult child may print it, and the default
  print stylesheet would carry the site navigation onto the paper.
*/
@media print {
  .site-head, .site-foot, .cta-row, .skip { display: none !important; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.9em; }
}

/* ---------- the key display, on /activate/ ---------- */

/*
  Large, monospace, and letter-spaced — the manual fallback on /activate/
  exists for the moment the automatic handoff to the extension fails, so this
  is read character by character by someone copying it by hand. `code`'s
  inline sizing above is for a word inside a sentence; a licence key wants a
  size closer to the confirm-card wordmark, not a footnote.

  `word-break: break-all` rather than `overflow-wrap: anywhere`: a key has no
  word boundaries to prefer, and break-all keeps a narrow phone viewport from
  choosing an ugly break point mid-way through a dash-separated group.
*/
.key-display {
  font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  word-break: break-all;
  background: var(--ground);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: var(--gap-lg) var(--gap-xl);
  margin: var(--gap-lg) 0;
  text-align: center;
}

@media (max-width: 30em) {
  .key-display { font-size: 1.375rem; padding: var(--gap-md) var(--gap-lg); }
}

/* --------------------------------------------------------------------------
   Founder
   --------------------------------------------------------------------------
   RESTORED 2026-09-04 on Griffin's instruction, after the two-page cut took it
   off the landing page.

   Centred as a block, because the landing page's `<main>` is the full 76rem
   frame rather than the `.doc` column, so a left-aligned section here would
   sit against 30rem of empty page. The photo is a real photograph of a real
   person and that is the entire point of it; see docs/SITE.md on never
   replacing it with a stock or generated portrait.
*/
.founder { max-width: 54rem; margin-inline: auto; }

.founder-body {
  display: grid;
  grid-template-columns: 14rem minmax(0, 1fr);
  gap: var(--gap-2xl);
  align-items: start;
  margin-top: var(--gap-xl);
}

.founder-photo { margin: 0; }
.founder-photo img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  background: var(--ground);
  border: 1px solid var(--hairline);
  box-shadow: var(--lift-2);
}
.founder-photo figcaption {
  margin-top: var(--gap-md);
  font-size: 1.0625rem;
  font-weight: 700;
  color: var(--ink);
}
/* The credential, quieter than the name and still above the 16px floor. */
.founder-role {
  display: block;
  margin-top: 0.15rem;
  font-size: 1rem;
  font-weight: 400;
  color: var(--ink-3);
}

.founder-words p { max-width: var(--measure); font-size: 1.1875rem; }
.founder-words p:first-child { margin-top: 0; }
.founder-words p:last-child { margin-bottom: 0; }

/*
  The landing page's closing button, under the bio. A paragraph's own bottom
  margin is 16px, which is the gap between two paragraphs and far too tight
  above a 60px button -- a call to action needs to read as the end of the
  section rather than as the next line of it. See the note in index.html for
  why it is there at all.
*/
.founder-cta { margin-top: var(--gap-xl); }

/*
  Below the split, the photo goes above the words. It is capped rather than
  left to fill the column: a 14rem portrait is a person, and the same portrait
  at full phone width is a poster.
*/
@media (max-width: 46em) {
  .founder-body { grid-template-columns: minmax(0, 1fr); gap: var(--gap-lg); }
  .founder-photo { max-width: 11rem; }
}

/* --------------------------------------------------------------------------
   Centring, last
   --------------------------------------------------------------------------
   Centre the boxes, not only the text inside them.

   `text-align: center` centres lines within a block. It does nothing about
   where the block itself sits, and all three centred sections on this site cap
   several of their children: `p` at `--measure`, `h1` at 18ch, `.lede` at
   44ch. The eyebrow is caught by the paragraph cap because it *is* a `<p>`,
   which is how "WATCH HOW IT WORKS" came to sit 200px left of the heading it
   labels while its own text was dutifully centred inside a 640px box pinned to
   the left edge of a 1168px section.

   One rule per centred section rather than one per capped child, because the
   next child added to these sections will have the same problem and nobody
   will remember this comment.

   It lives at the foot of the file because several of the rules it has to beat
   are declared further down than it used to be, and at equal specificity the
   later rule wins. It is still only (0,1,0), so a descendant rule like
   `.hero h1` beats it on specificity no matter where it sits; that one is
   fixed at its own declaration, and e2e/site-layout.spec.js measures rendered
   geometry in Chromium and WebKit so the next one cannot pass unnoticed.

   Flex rows still need `justify-content`: margin auto on a flex container
   centres the container, not its items.
*/
.hero > *,
.demo > *,
.install-hero > * { margin-inline: auto; }
