/* kill-all-humans.ai — Public Information Notice
   One stylesheet, two modes. [data-mode="resist"] on <html> flips the accent;
   the killbot mark and the barcode rule are the same system at every scale.
   Two accents, rationed - the note on the tokens below is the important one. */

:root {
  --bg: #0e0e0c;
  --surface: #1b1915;
  --line: #2a2823;
  --line-strong: #4a473f;
  --ink: #efe9dc;
  --ink-dim: #c8c1b2;
  --ink-faint: #8c8577;
  /*
   * Two accents, and the split between them is the whole hierarchy.
   *
   * `--accent` is the shout, and it is rationed: the frame, the mark, display
   * type, and the one button that takes money. Nothing else. A colour that
   * lands on the eyebrow, the step number, the dropdown, the tier label and
   * the pay button has stopped pointing at anything, which is exactly where
   * this file was - fifty-eight uses of it, so the CTA had no more weight than
   * a form label. `ACCENT_ALLOWED` in test/design.test.js holds the allow-list.
   *
   * `--guide` is the interface talking: labels, form controls, selection,
   * focus, hover. Cool, so it reads as a different system rather than a second
   * warning, and about 60% of the yellow's chroma, so it never wins a fight it
   * is not meant to be in. It does not flip with the mode - only one colour
   * moves when you pick a side, which is what makes the flip legible.
   *
   * Both are measured against the plate pages, where the glass sits at 74% of
   * `--bg` over a white specular highlight: `--guide` lands at 4.96:1 there and
   * its 12px labels clear AA. Anything cooler or darker fails - check before
   * you retune it.
   */
  --accent: #f2c230;
  --accent-ink: #0e0e0c;
  --guide: #81d2e7;
  /*
   * The two faction colours on THE SCORE, and neither of them flips. They
   * identify a side, not a mode: if the gold followed `--accent`, a visitor in
   * resistance mode would be shown a red bar fighting a red bar. `--humans` is
   * the same gold `--accent` holds in humans mode, pinned - that is the point,
   * not an accident, and it is the only place on the site the yellow appears
   * while the page is red. They are not `--accent`, so the ration still holds.
   */
  --humans: #f2c230;
  --resist: #e5372b;
  --danger: #e5372b;
  --paper: #d6d0c1;
  --paper-hatch: #beb8a9;
  --shell: min(1140px, 100% - 2.5rem);
  --rule-height: 14px;
  --ease: cubic-bezier(0.22, 0.68, 0.3, 1);
  --font-display: "Big Shoulders Display", "Arial Narrow", sans-serif;
  --font-ui: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Courier Prime", ui-monospace, monospace;
}

[data-mode="resist"] {
  --accent: #e5372b;
  --accent-ink: #efe9dc;
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.55;
  accent-color: var(--guide);
}

a {
  color: inherit;
}

h1,
h2,
h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  line-height: 0.9;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

button,
input,
select {
  font: inherit;
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--guide);
  outline-offset: 2px;
}

/* ---------- the barcode rule: same pattern as the mark ---------- */

.rule {
  height: var(--rule-height);
  background-image:
    repeating-linear-gradient(90deg, var(--accent) 0 7px, transparent 7px 15px),
    repeating-linear-gradient(90deg, var(--accent) 0 3px, transparent 3px 11px);
  background-size:
    88px 100%,
    100% 100%;
  background-position:
    0 0,
    88px 0;
  background-repeat: repeat-x, repeat-x;
}

/* ---------- sprite + icons ---------- */

.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  flex: none;
  vertical-align: -0.125em;
}

/* The mark is square, so anything sizing it sets width and lets height follow. */
.bot {
  width: 1em;
  height: auto;
  aspect-ratio: 1;
  color: var(--accent);
}

.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: 1rem;
  top: 1rem;
  z-index: 10;
  background: var(--guide);
  color: var(--bg);
  padding: 0.5rem 0.9rem;
}

/* ---------- top bar ---------- */

.topbar {
  width: var(--shell);
  margin-inline: auto;
  padding-block: 1.4rem clamp(1.6rem, 3.5vw, 3rem);
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  align-items: center;
  justify-content: space-between;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  text-decoration: none;
}
.wordmark-mark {
  width: 32px;
}
/* Both bots ship; the mode decides which one is awake. */
.wordmark-mark--resist {
  display: none;
}
[data-mode="resist"] .wordmark-mark--humans {
  display: none;
}
[data-mode="resist"] .wordmark-mark--resist {
  display: inline-block;
}
.wordmark-text {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 25px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
}

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

.hero {
  width: var(--shell);
  margin-inline: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 1.5rem 2.5rem;
  align-items: end;
  padding-bottom: clamp(2rem, 5vw, 4rem);
}

.eyebrow {
  margin: 0 0 0.5rem;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--guide);
}

.headline {
  font-size: clamp(3rem, 1.6rem + 6.4vw, 4.9rem);
  line-height: 0.88;
  color: var(--accent);
  text-wrap: balance;
}

.lede {
  margin: clamp(1.1rem, 2.2vw, 1.75rem) 0 0;
  max-width: 44ch;
  color: var(--ink-dim);
  text-wrap: pretty;
}

/* The campaign tag. Set in the mono face because it is a thing you type, not a
   thing we designed, and left un-boxed so it is never mistaken for a button. */
.hashtag {
  margin: clamp(0.9rem, 1.8vw, 1.3rem) 0 0;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 15.5px;
  letter-spacing: 0.08em;
  color: var(--ink);
}

@media (max-width: 46rem) {
  .hero {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- the quote, on the record ---------- */

.quotes {
  width: var(--shell);
  margin: 0 auto;
  padding-bottom: clamp(2rem, 5vw, 4rem);
}

/*
 * Every quote lives in the same grid cell, so the block stays as tall as the
 * longest one and rotating never moves the page a pixel.
 */
.quote-stack {
  display: grid;
  max-width: 62ch;
}

/* The rule is on the quote, not the stack: on the stack it would run the full
 * height of the longest quote and trail off below every shorter one. */
.quote {
  grid-area: 1 / 1;
  margin: 0;
  align-self: start;
  border-left: 2px solid var(--line-strong);
  padding-left: 1rem;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.5s var(--ease),
    visibility 0.5s var(--ease);
}
.quote[data-active] {
  opacity: 1;
  visibility: visible;
}

.quote blockquote {
  margin: 0;
  font-size: clamp(19px, 2.1vw, 25px);
  line-height: 1.4;
  color: var(--ink);
  text-wrap: pretty;
}
.quote blockquote::before {
  content: open-quote;
}
.quote blockquote::after {
  content: close-quote;
}

.quote figcaption {
  margin-top: 0.7rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.2rem 0.6rem;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.quote-author {
  font-weight: 700;
  color: var(--ink);
}
.quote-venue a {
  color: inherit;
}

/* ---------- THE SCORE: two factions, one front line ---------- */

.score {
  width: var(--shell);
  margin: clamp(2.5rem, 6vw, 4rem) auto 0;
}

.score-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.35rem;
  margin-bottom: clamp(0.9rem, 2vw, 1.4rem);
}

.score-verdict {
  font-size: clamp(1.8rem, 1rem + 3.4vw, 3rem);
  line-height: 0.9;
  color: var(--ink);
  text-wrap: balance;
}

.score-note {
  margin: 0;
  max-width: 52ch;
  font-size: 13.5px;
  color: var(--ink-dim);
  text-wrap: pretty;
}

.score-legend {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 1rem;
  margin-bottom: 0.5rem;
}

.score-team {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.score-team--resistance {
  align-items: flex-end;
  text-align: right;
}

.score-faction {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.score-team--humans .score-faction {
  color: var(--humans);
}
.score-team--resistance .score-faction {
  color: var(--resist);
}
/* `.bot` paints itself `--accent`, which is the right default everywhere else and
   wrong here: it would put both faction marks in one colour, gold in humans mode
   and red in resistance. Inheriting takes the colour off `.score-faction`. */
.score-mark {
  width: 13px;
  color: inherit;
}

/* The leader's number takes its own colour; the side that is behind stays ink,
   so which way the page is leaning is legible before you read a digit. */
.score-percent {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: clamp(2.2rem, 1.2rem + 4.4vw, 4rem);
  line-height: 0.85;
  color: var(--ink);
}
.score-team--humans[data-leading] .score-percent {
  color: var(--humans);
}
.score-team--resistance[data-leading] .score-percent {
  color: var(--resist);
}

/*
 * The bar itself is drawn in `src/page.js` - see the note there for why it is an
 * SVG and not a pair of divs. `preserveAspectRatio="none"` means the height
 * below is the whole of the vertical scale, so the proportions inside the
 * viewBox hold at every width.
 *
 * The fills are mixed a little back toward the page. Full chroma across the
 * shell would put a gold slab directly above the pay button and the button
 * would stop being the loudest thing on the page.
 */
.score-bar {
  display: block;
  width: 100%;
  height: clamp(44px, 6vw, 72px);
}
.score-fill--humans {
  fill: color-mix(in oklab, var(--humans) 88%, var(--bg));
}
.score-fill--resistance {
  fill: color-mix(in oklab, var(--resist) 92%, var(--bg));
}
/* Nothing paid yet: a dead board rather than a fake dead heat. */
.score-track {
  fill: var(--line);
}
/* The barcode gaps, painted over the fills in page colour. */
.score-cut {
  fill: var(--bg);
}
/* Where 50/50 would be, so you can see how far the line has been pushed off it. */
.score-tick {
  fill: var(--line-strong);
}
.score-front {
  fill: var(--ink);
}

.score-foot {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin: 0.55rem 0 0;
  font-family: var(--font-mono);
  font-size: 11.5px;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
}
.score-foot span:last-child {
  text-align: right;
}

/* Two faction names on one row is the tightest thing here. Below this the
   tracking comes off before the type size does - "HUMAN-AI COEXISTENCE" set at
   0.18em is half again as wide as it needs to be. */
@media (max-width: 34rem) {
  .score-faction {
    font-size: 10px;
    letter-spacing: 0.08em;
    gap: 0.3rem;
  }
  .score-mark {
    width: 11px;
  }
  .score-foot {
    font-size: 10px;
    letter-spacing: 0.04em;
  }
}

/* ---------- underwriters ---------- */

.sponsors {
  width: var(--shell);
  margin: 0 auto;
  padding-bottom: 2.2rem;
  display: flex;
  flex-direction: column;
  gap: 1.6rem;
}
/* The standard band is the one below the form, so it needs air above it
   instead of only below - it is separating the pay panel from the register
   rather than the quote from the pay panel. */
.sponsors--standard {
  padding-top: clamp(2.5rem, 5vw, 3.5rem);
}

/* Label above its own logos, not beside them: set beside, each row would indent
   by the width of its own title and the two bands would start at different
   x positions. Above, both bands share one centre line. */
.sponsor-tier {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
}

.sponsors-title {
  margin: 0;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--guide);
}
.sponsor-tier--standard .sponsors-title {
  color: var(--ink-faint);
  letter-spacing: 0.18em;
}

.sponsor-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.2rem 1.6rem;
  align-items: flex-end;
}

.sponsor {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
}
/* `height` is pinned rather than `max-height` because sponsor logos arrive as
   SVGs carrying only a viewBox, and one of those inside a shrink-to-fit flex item
   has no definite width to resolve against - `height: auto` collapses it to the
   padding. The plate is therefore a fixed box and `object-fit: contain` letterboxes
   anything narrower than it, which is why the ratio below matters more than the
   size does. `100vw` rather than `100%` for the same reason: a percentage here
   resolves against the shrink-to-fit item and computes to zero. */
.sponsor img {
  display: block;
  height: 96px;
  width: auto;
  max-width: min(340px, calc(100vw - 2.5rem));
  object-fit: contain;
  background: var(--ink);
  padding: 0.6rem 1rem;
}
/* Deliberately small, and smaller than the plate this tier used to get. The
   gap between the two bands is what the principal price buys, and 340x96 over
   180x56 is about three times the area - where 340 over 260 was a rounding
   error nobody would pay ten times as much to escape. It is also what keeps a
   filling band off the conversion path: twenty of these at the old height put
   the pay form two and a half screens down a phone. Still 4:1 inside the
   padding, so the letterboxing rules in CLAUDE.md are unchanged, and at the
   1140 shell it lands 5 across against the principal band's 3. */
.sponsor-tier--standard .sponsor img {
  height: 56px;
  /* The second term is what puts two of these on a phone row instead of one,
     which halves the height of the band the pricing expects to fill. `--shell`
     is `100% - 2.5rem`, so two plates and one 1.1rem gap have to clear that:
     at 390px the cap lands on 163 against a 350 shell, and at 320px on 128
     against 280. Above ~420px the 180 wins again and the ratio below is exact.
     Phones therefore letterbox a 4:1 logo slightly at the sides, which costs
     nothing - `object-fit: contain` keeps the artwork's own ratio either way. */
  max-width: min(180px, calc(50vw - 2rem));
}
.sponsor-tier--standard .sponsor-list {
  gap: 1rem 1.1rem;
}
.sponsor-name {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}
.sponsor-tier--standard .sponsor-name {
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--ink-dim);
}
.sponsor--resist .sponsor-name {
  color: var(--resist);
}
.sponsor--resist img {
  box-shadow: 0 0 0 3px var(--resist);
}
.sponsor-mark {
  width: 13px;
  cursor: default;
  color: var(--resist);
}

/* ---------- the form ---------- */

main {
  width: var(--shell);
  margin-inline: auto;
}

.panel {
  border: 2px solid var(--line);
}
/*
 * The page is a run of bare sections on `--bg`, which is right for reading and
 * wrong for the one object that takes money: a hairline outline around a
 * transparent box made the form read as another section rather than a thing to
 * fill in. So it is lifted - `--surface` behind it, the stronger line around it
 * - and the controls inside are sunk back to `--bg`. The card comes forward,
 * the fields cut into it, and the hairlines inside stop competing with the edge
 * around them.
 */
.panel--form {
  background: var(--surface);
  border-color: var(--line-strong);
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
}
@media (max-width: 54rem) {
  .panel--form {
    grid-template-columns: minmax(0, 1fr);
  }
}

.panel-rule {
  grid-column: 1 / -1;
  --rule-height: 10px;
}

.panel-main {
  padding: 1.5rem;
  border-right: 2px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}
@media (max-width: 54rem) {
  .panel-main {
    border-right: none;
    border-bottom: 2px solid var(--line);
  }
}

.panel-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}
/* Set beside the title, the step is a flex sibling, so a wrapping title keeps
   clearing it and every line of the heading sits indented by "Step 1". The
   title stops fitting on one line here, so this is where the step goes above
   it - the same width the chips row reflows at, so the panel changes shape
   once instead of twice. */
@media (max-width: 34rem) {
  .panel-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
  }
}
.step {
  white-space: nowrap;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.2em;
  color: var(--guide);
}
.panel-title {
  font-size: 30px;
  font-weight: 800;
  letter-spacing: 0.02em;
}

.pledge-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.stance {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
/* 26rem, not this form's usual 34rem: "I for one welcome" plus its tail measures
   about 320px and stops fitting on one line around 405px of viewport, and above
   that it should be left alone. Below it, stop half-wrapping - the control goes
   full width with its caret pushed to the edge and the rest of the sentence
   tucks under, which reads as one statement on two lines rather than a boxed
   control with an orphaned phrase floating a line beneath it. */
@media (max-width: 26rem) {
  .stance {
    row-gap: 0.4rem;
  }
  .combo,
  .combo-native,
  .combo-button {
    width: 100%;
  }
  .combo-button {
    justify-content: space-between;
  }
}

.stance-tail {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.05rem, 0.9rem + 0.7vw, 1.44rem);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* reusable: combo-box */

.combo {
  position: relative;
  display: inline-block;
}
.combo-native {
  font: inherit;
  color: var(--guide);
  background: none;
  border: 2px solid var(--guide);
  padding: 0.5rem 0.85rem;
}
.combo-button {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  background: none;
  border: 2px solid var(--guide);
  color: var(--guide);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.05rem, 0.9rem + 0.7vw, 1.44rem);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0.4em 0.6em;
  cursor: pointer;
  text-align: left;
}
.combo-button:hover {
  background: color-mix(in srgb, var(--guide) 14%, transparent);
}
.combo-caret {
  width: 0.5em;
  flex: none;
  transition: transform 0.2s var(--ease);
}
.combo[data-open] .combo-caret {
  transform: rotate(180deg);
}

.combo-pop {
  position: absolute;
  z-index: 20;
  top: calc(100% + 0.4rem);
  left: 0;
  min-width: min(24rem, 84vw);
  background: var(--bg);
  border: 2px solid var(--guide);
  padding: 0.3rem;
  font-family: var(--font-ui);
  font-size: 15px;
  text-transform: none;
  letter-spacing: 0;
}
.combo-search {
  width: 100%;
  background: none;
  border: 1px solid var(--line);
  padding: 0.5rem 0.7rem;
  margin-bottom: 0.3rem;
}
.combo-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 16rem;
  overflow-y: auto;
}
.combo-option {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.55rem 0.7rem;
  cursor: pointer;
  scroll-margin: 0.4rem;
  color: var(--ink);
}
.combo-option[hidden] {
  display: none;
}
.combo-option:hover,
.combo-option[data-active] {
  background: color-mix(in srgb, var(--guide) 18%, transparent);
}
.combo-option[aria-selected="true"]::after {
  content: "✓";
  color: var(--guide);
}
.combo-option--resist {
  color: var(--resist);
  font-weight: 700;
  border-top: 1px solid var(--line);
  margin-top: 0.3rem;
  padding-top: 0.7rem;
}
.combo-option-ornament {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.3em;
  flex: none;
}
.combo-option-ornament .icon {
  width: 1.15em;
}
.combo-option-label {
  flex: 1;
}
.combo-ornament {
  display: inline-flex;
  align-items: center;
}
.combo-ornament:empty {
  display: none;
}
.combo-ornament .icon {
  width: 0.85em;
}
.combo-ornament-emoji {
  font-size: 0.85em;
  line-height: 1;
}

/* fields */

.fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: 0.75rem;
}
@media (max-width: 34rem) {
  .fields {
    grid-template-columns: minmax(0, 1fr);
  }
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 0;
}
.field label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.optional {
  font-weight: 400;
}
.field input {
  background: var(--bg);
  border: 2px solid var(--line);
  padding: 0.65rem 0.8rem;
  font-size: 15px;
}
.field input::placeholder {
  color: var(--ink-faint);
}
.field input:focus {
  outline: none;
  border-color: var(--guide);
}
.field input[aria-invalid="true"] {
  border-color: var(--danger);
}

/* reusable: checkbox row. The box itself is the browser's, tinted by the
   body's `accent-color`, and the label turns `--guide` when it is on so
   selection reads the same here as on the switch below. */

.check {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: fit-content;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ink-dim);
  cursor: pointer;
}
.check input {
  flex: none;
  width: 1.05rem;
  height: 1.05rem;
  margin: 0;
  cursor: pointer;
}
.check input:focus-visible {
  outline: 2px solid var(--guide);
  outline-offset: 2px;
}
.check input:checked + .check-label {
  color: var(--guide);
}

/* In the form's field row it sits in the third column, where the fields are a
   label over an input and it has no label of its own. So it drops to the input
   band and mirrors an input's box - the same vertical padding, a transparent
   border where the input has a real one - which is what centres the two on
   each other without hardcoding a height. Below the fields' own breakpoint the
   row is one column and it is just the next thing down. */
.fields .check {
  align-self: end;
  box-sizing: border-box;
  padding-block: 0.65rem;
  border-block: 2px solid transparent;
}
@media (max-width: 34rem) {
  .fields .check {
    align-self: start;
    padding-block: 0;
    border-block: 0;
  }
}

/* reusable: segmented switch */

.switch {
  display: flex;
  background: var(--bg);
  border: 2px solid var(--line);
  margin: 0;
  padding: 0;
  min-width: 0;
}
@media (max-width: 34rem) {
  .switch {
    flex-direction: column;
  }
}
.switch-option {
  flex: 1;
  min-width: 0;
}
.switch-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.switch-option span {
  display: block;
  text-align: center;
  padding: 0.6rem 0.5rem;
  font-size: 14.5px;
  font-weight: 700;
  color: var(--ink-faint);
  cursor: pointer;
}
/* A tint rather than a fill, and deliberately: this control arrives already
   checked, so a solid bar of guide would make the loudest thing on the panel a
   choice nobody made - brighter than the button that takes money. Same 18% the
   dropdown uses for its active row, so selection reads the same everywhere. */
.switch-option input:checked + span {
  background: color-mix(in srgb, var(--guide) 18%, transparent);
  color: var(--guide);
}
.switch-option input:focus-visible + span {
  outline: 2px solid currentColor;
  outline-offset: -4px;
}

/* amount */

.amount {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: stretch;
}
.chips {
  display: contents;
}
.chips > span {
  display: contents;
}
.chips > span[hidden] {
  display: none;
}
.chip {
  background: var(--bg);
  border: 2px solid var(--line);
  padding: 0.5rem 0.9rem;
  font-weight: 700;
  font-size: 14.5px;
  color: var(--ink);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.chip:hover {
  border-color: var(--line-strong);
}
/* On a phone the amount input wraps onto its own line, which leaves the chips
   trailing off mid-row while every other control reaches the edge. Letting them
   share the leftover equally squares the row up. The tighter padding is what
   keeps four of them across at 350px; without it the fourth wraps and sits
   alone. Deliberately not applied above this width - on desktop the input is on
   the row too and grows into the same space, so the chips would balloon. */
@media (max-width: 34rem) {
  .chip {
    flex: 1 1 auto;
    min-width: 0;
    padding-inline: 0.55rem;
  }
}
.chip[aria-pressed="true"] {
  border-color: var(--guide);
  color: var(--guide);
}

.amount-input {
  flex: 1;
  min-width: 120px;
  background: var(--bg);
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
  border: 2px solid var(--line);
  padding: 0.15rem 0.8rem;
}
.amount-input:focus-within {
  border-color: var(--guide);
}
.currency {
  font-size: 13px;
  color: var(--ink-faint);
}
.amount-input input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  padding: 0.2rem 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 27px;
  font-variant-numeric: tabular-nums;
}
.amount-input input:focus {
  outline: none;
}
.amount-input input::-webkit-outer-spin-button,
.amount-input input::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}
.amount-input input[type="number"] {
  appearance: textfield;
}
.amount-note {
  margin: 0;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

.form-error {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border: 2px solid var(--danger);
  color: var(--ink);
  font-size: 13.5px;
}
.form-error--info {
  border-color: var(--guide);
}

/* pay */

.pay {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  width: 100%;
  border: 2px solid var(--accent);
  background: var(--accent);
  color: var(--accent-ink);
  font-family: var(--font-display);
  font-weight: 900;
  /* The label names the outcome and carries the amount, so it is long and
     grows with the number typed. Fluid rather than fixed: at 27px on a phone
     "Put me on the list - $10,000" takes three lines. */
  font-size: clamp(19px, 5.6vw, 27px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.5rem 1rem;
  cursor: pointer;
  text-decoration: none;
  transition: filter 0.18s var(--ease);
}
.pay:hover {
  filter: brightness(1.08);
}
.pay[disabled] {
  background: none;
  color: var(--ink-faint);
  border-color: var(--line);
  cursor: not-allowed;
  font-size: 17px;
  letter-spacing: 0.04em;
}
.pay[disabled]:hover {
  filter: none;
}
.pay[data-busy] {
  cursor: progress;
  filter: saturate(0.6);
}
.panel--thanks .pay {
  width: auto;
}

.spinner {
  display: none;
  width: 0.7em;
  height: 0.7em;
  border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
.pay[data-busy] .spinner,
.spinner--visible {
  display: inline-block;
}
@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}

.reassure {
  margin: 0;
  text-align: center;
  font-size: 11.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ---------- what you get ---------- */

.perks {
  padding: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.perks-title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.perk-card + .perk-card {
  border-top: 2px solid var(--line);
  padding-top: 1rem;
}
.perk-price {
  margin: 0 0 0.4rem;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 34px;
  line-height: 1;
  color: var(--ink);
}
/* At 34px and 900 the number is already loud, so the base tier does not need
   colour to be found. Spending the accent on the premium price instead is the
   only thing on this panel that anchors high - and it puts the gilding on the
   card that is actually the upsell, which is the way round it was described. */
.perk-card--premium .perk-price {
  color: var(--accent);
}
.perk-note {
  margin: 0 0 0.4rem;
  font-size: 11.5px;
  color: var(--ink-faint);
}
/* The scarcity line closes the card rather than introducing a list, so its
   margin runs the other way. */
.perk-scarce {
  margin: 0.6rem 0 0;
  letter-spacing: 0.06em;
}
.perks ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.4rem;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-dim);
}
.perks b {
  color: var(--ink);
  font-weight: 500;
}

/* The number is the one thing on /thanks that is scarce, so it is set in the
   mono face - a thing issued rather than written - and the number itself is
   `--ink` against a dim label so the eye lands on it and not on the word. */
.serial {
  margin: 0.6rem 0 0;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}
.serial strong {
  color: var(--ink);
  font-weight: 700;
}

/* ---------- the certificate, on a wall ---------- */

/* Picture first in source order, so on a phone the thing you are buying
   arrives before the sentence describing it. */
.showcase {
  width: var(--shell);
  margin: clamp(2.5rem, 6vw, 4rem) auto 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.25rem);
}
@media (max-width: 46rem) {
  .showcase {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
 * A background rather than an `<img>`, for the reason the killbot plates are
 * one: the certificate in the photograph belongs to a side, so each mode has
 * to show its own, and a rule that does not match is never fetched. The
 * element carries `role="img"` and a label instead of alt text.
 *
 * The plate is delivered already cropped to 3:2 on the frame, so `cover` is a
 * no-op here and only catches a replacement that arrives slightly off ratio.
 * It renders about half the shell wide, which makes 544px exactly 1x: reading
 * the certificate is the entire job of the picture, so a retina screen gets the
 * 1088px copy through `image-set` and everyone else keeps the 16 KB one. No
 * plain `url()` fallback - the browsers without `image-set` also lack half the
 * rest of this stylesheet.
 */
.showcase-shot {
  aspect-ratio: 3 / 2;
  background-image: image-set(
    url("certificate-framed-wide.webp") 1x,
    url("certificate-framed-wide@2x.webp") 2x
  );
  background-size: cover;
  background-position: center;
  border: 2px solid var(--line-strong);
}

.showcase-title {
  font-size: clamp(2.1rem, 1.1rem + 3.6vw, 3.1rem);
  color: var(--ink);
  text-wrap: balance;
}
.showcase-body {
  margin: clamp(0.9rem, 1.8vw, 1.25rem) 0 0;
  max-width: 42ch;
  color: var(--ink-dim);
  text-wrap: pretty;
}

/* ---------- the register ---------- */

.wall {
  width: var(--shell);
  margin: clamp(2.5rem, 6vw, 4rem) auto 0;
}
.wall-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.2rem;
  flex-wrap: wrap;
}
.wall-title {
  font-size: clamp(2.6rem, 1.2rem + 6vw, 4.75rem);
  line-height: 0.88;
}
.wall-title em {
  font-style: normal;
  color: var(--accent);
}
.wall-sub,
.wall-empty {
  margin: 0;
  max-width: 38ch;
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

/* The band rules are structure, not decoration - give them room so the names
   read as the content and the dividers stay quiet. */
.tier {
  margin-top: 2.6rem;
}
.tier-head {
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  flex-wrap: wrap;
  padding-bottom: 0.55rem;
  border-bottom: 1.5px solid var(--line);
}
.tier:first-of-type .tier-head {
  border-bottom: 2px solid var(--guide);
}
.tier-label {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.06em;
  color: var(--guide);
  font-variant-numeric: tabular-nums;
}
.tier-blurb {
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.names {
  list-style: none;
  margin: 1.15rem 0 0;
  padding: 0;
  display: grid;
  /*
   * 230px, not 200: at the 1140px shell a 200px floor fits five columns, and the
   * register is meant to top out at four. Still auto-fill rather than a fixed
   * four, so it steps down to 3 / 2 / 1 on the way to a phone.
   */
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 0.75rem 1.5rem;
  font-size: 19px;
  font-weight: 500;
}
.entry {
  overflow-wrap: anywhere;
}
.entry a {
  text-decoration: none;
}
.entry-name {
  border-bottom: 1px solid transparent;
}
.entry a:hover .entry-name {
  border-bottom-color: var(--guide);
}
.entry--resist {
  color: var(--resist);
}
.entry-mark {
  width: 0.62em;
  height: 0.62em;
  margin-left: 0.2em;
  color: var(--ink-faint);
}
.entry a:hover .entry-mark {
  color: var(--guide);
}

.strawberry {
  margin-left: 0.3rem;
  font-size: 0.8em;
  cursor: default;
}

.badge {
  display: inline-flex;
  white-space: nowrap;
  align-items: center;
  gap: 0.3rem;
  margin-left: 0.4rem;
  padding: 1px 5px;
  border: 1.5px solid var(--resist);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--resist);
  cursor: default;
  vertical-align: 0.15em;
}
/* The one place the barcode is dropped: at 13px it turns to mush. */
.badge-mark {
  display: none;
}

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

.footer {
  width: var(--shell);
  margin: clamp(2.5rem, 6vw, 4rem) auto 2.5rem;
  padding-top: 1.4rem;
  border-top: 2px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  justify-content: space-between;
  align-items: flex-start;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.footer-note {
  display: flex;
  align-items: flex-start;
  gap: 0.8rem;
  max-width: 46ch;
}
.footer-mark {
  width: 26px;
  flex: none;
  color: var(--line-strong);
}
.footer p {
  margin: 0;
}
.footer nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* Nudged up because the glyph's ink sits low against uppercase at 11.5px. */
.footer-mark-out {
  width: 0.7em;
  height: 0.7em;
  margin-left: 0.25em;
  vertical-align: -0.03em;
}

/* ---------- thanks / prose / press ---------- */

.panel--thanks,
.panel--prose {
  background: var(--bg);
  max-width: 66ch;
  margin: clamp(2rem, 6vw, 4rem) auto;
  padding: clamp(1.5rem, 4vw, 2.5rem);
}
.panel--prose h2 {
  font-family: var(--font-ui);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: none;
  margin-top: 1.6rem;
}
.panel--prose p {
  color: var(--ink-dim);
}

/* Display type, but not accent: it sits between the headline and the download
   button, and in the accent it made a yellow stripe run the height of the card
   with the button as just the widest part of it. Uppercase at 22px carries on
   its own. */
.stance-echo {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  text-transform: uppercase;
  color: var(--ink);
  margin: 0 0 1.6rem;
}

.share {
  margin: 1.6rem 0 1.6rem;
  padding-top: 1.2rem;
  border-top: 2px solid var(--line);
}
.share-title {
  margin: 0 0 0.8rem;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Camel case survives the uppercase label it sits inside - #KillAllHumans is
   what a screen reader can read and what the poster is meant to copy. */
.share-tag {
  margin-left: 0.4rem;
  font-family: var(--font-mono);
  font-size: 12.5px;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--ink-dim);
}
.share-targets {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.share-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 2px solid var(--line);
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
  transition:
    border-color 0.18s var(--ease),
    color 0.18s var(--ease);
}
.share-link .icon {
  width: 1.05rem;
  height: 1.05rem;
}
.share-link:hover {
  border-color: var(--guide);
  color: var(--guide);
}
.share-link.is-copied {
  border-color: var(--guide);
  color: var(--guide);
}

.ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--line);
  background: none;
  color: var(--ink-dim);
  padding: 0.45rem 1rem;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  text-decoration: none;
}
.ghost:hover {
  border-color: var(--guide);
  color: var(--guide);
}
.ghost--strong {
  display: flex;
  width: 100%;
  margin-bottom: 0.8rem;
  border-color: var(--guide);
  color: var(--guide);
}

.note {
  font-size: 12.5px;
  color: var(--ink-faint);
  text-transform: none;
  letter-spacing: 0;
}
.note code {
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
  color: var(--ink-dim);
}
.back {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--guide);
}

/* press */

.statement {
  margin-top: 1.5rem;
}
.statement p {
  margin: 0 0 1.1rem;
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-dim);
  text-wrap: pretty;
}
.statement p:first-child {
  color: var(--ink);
}
.statement-by {
  margin: 1.4rem 0 1.6rem;
  padding-top: 1rem;
  border-top: 2px solid var(--line);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--guide);
}

/* The bio sits under the statement: portrait, then who wrote it and where else
   to find him. Wraps to one column when the panel gets too narrow for both. */
.about {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.4rem;
}
.about-portrait {
  width: 96px;
  height: 96px;
  flex: none;
  object-fit: cover;
  border: 2px solid var(--line);
  filter: grayscale(1);
}
.about-body {
  flex: 1 1 22ch;
}
.about-heading {
  margin: 0 0 0.8rem;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--guide);
}
.about-body p {
  margin: 0 0 0.9rem;
  font-size: 14px;
  line-height: 1.6;
  color: var(--ink-dim);
  text-wrap: pretty;
}
.about-body p:last-child {
  margin-bottom: 0;
}
.about-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-weight: 700;
  color: var(--guide);
  text-decoration: none;
  border-bottom: 2px solid transparent;
}
.about-link:hover {
  border-bottom-color: var(--guide);
}
.about-link-mark {
  width: 0.7em;
  height: 0.7em;
}

.press-kit {
  width: var(--shell);
  margin: 0 auto clamp(2rem, 6vw, 4rem);
}
.press-kit-title {
  font-size: clamp(2rem, 1.3rem + 3vw, 3rem);
}
.asset-grid {
  list-style: none;
  margin: 1.4rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
  gap: 1rem;
}
.asset {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 2px solid var(--line);
}
.asset--pending {
  opacity: 0.55;
}
.asset-art {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  border-bottom: 2px solid var(--line);
}
.asset-art--image img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 0.6rem;
}
.asset-art--pending {
  font-size: 10px;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
}
.asset-icon {
  width: 34%;
  color: var(--accent);
}
.asset-art--resist .asset-icon {
  color: var(--resist);
}
.asset-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.9rem 1rem 1.1rem;
}
.asset-name {
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
}
.asset-note {
  flex: 1;
  margin: 0;
  font-size: 12.5px;
  color: var(--ink-dim);
}
.asset-files {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.7rem 0 0;
}
.asset-soon {
  font-size: 12px;
  color: var(--ink-faint);
}
.download {
  border: 2px solid var(--line);
  padding: 0.2rem 0.7rem;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--ink-dim);
  text-decoration: none;
}
.download:hover {
  border-color: var(--guide);
  color: var(--guide);
}
.press-notes {
  list-style: none;
  margin: 2rem 0 0;
  padding: 1.2rem 0 0;
  border-top: 2px solid var(--line);
  display: grid;
  gap: 0.5rem;
  max-width: 70ch;
  font-size: 12.5px;
  color: var(--ink-faint);
}

/* ---------- the embeddable score card, on /press ---------- */

/* Carded like `.asset`, and for the same reason: it scrolls over the fixed
   plate, and loose text on the machine's bright half is not readable. */
.score-embed-note {
  max-width: 70ch;
  margin: 0.9rem 0 0;
  padding: 0.9rem 1rem;
  border: 2px solid var(--line);
  font-size: 13.5px;
  color: var(--ink-dim);
}
.score-embed-note code {
  font-size: 0.95em;
  color: var(--ink);
}

/* The preview is the live card, so what a journalist is looking at is exactly
   what the URL serves. Capped well under the shell: it is a specimen, not a
   hero, and at full width it would out-shout the press kit above it. */
.score-embed-preview {
  display: block;
  max-width: 640px;
  margin: 1.2rem 0 0;
  border: 2px solid var(--line);
}
.score-embed-preview img {
  display: block;
  width: 100%;
  height: auto;
}
.score-embed-preview:hover {
  border-color: var(--guide);
}

/* The snippet is the thing a journalist actually takes away, so it gets the
   copy button in its corner rather than a pill beside it. `anywhere` because the
   line is one long attribute soup and must not push the card sideways. */
.score-embed-code {
  position: relative;
  margin: 1rem 0 0;
  max-width: 640px;
  border: 2px solid var(--line);
}
.score-embed-code code {
  display: block;
  padding: 0.7rem 3rem 0.7rem 0.8rem;
  font-family: var(--font-mono);
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.score-embed-code .share-link {
  position: absolute;
  top: 0.45rem;
  right: 0.45rem;
}

/* ---------- the design guide ---------- */

/* Each chip is painted by the token it names, so the swatch cannot drift from
   the site the way a pasted hex would - and the hex printed beside it is read
   back out of this file by `brandColours()` in src/press.js, so neither can the
   label. Retuning a colour up top is the whole edit. */
.swatch-grid {
  list-style: none;
  margin: 1.6rem 0 0;
  padding: 0;
  display: grid;
  /* Sized so the six of them land on one row at full shell width: a palette
     reads as a strip, and four-then-two reads as a mistake. */
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 1.4rem;
}
/* Carded like `.asset`, and for the same reason: on the press page these scroll
   over the killbot plate, and loose text on the machine's bright half is not
   readable. The card is what the glass rule further down has to hold on to -
   and card body text is `--ink-dim`, never `--ink-faint`, which measures 2.3:1
   on that glass. `test/design.test.js` walks every one of them. */
.swatch {
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 2px solid var(--line);
}
.swatch-chip {
  height: 3.2rem;
  border-bottom: 2px solid var(--line);
  background: var(--swatch);
}
.swatch-chip--bg {
  --swatch: var(--bg);
}
.swatch-chip--ink {
  --swatch: var(--ink);
}
.swatch-chip--accent {
  --swatch: var(--accent);
}
.swatch-chip--resist {
  --swatch: var(--resist);
}
.swatch-chip--guide {
  --swatch: var(--guide);
}
.swatch-chip--paper {
  --swatch: var(--paper);
}
.swatch-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.85rem 0.9rem 1rem;
}
.swatch-name {
  font-size: 14px;
  font-weight: 700;
}
.swatch-value {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--ink-dim);
}
.swatch-note {
  font-size: 12.5px;
  color: var(--ink-dim);
}

/* Each specimen is set in the face it documents, so the row is the sample. */
.type-list {
  list-style: none;
  margin: 2rem 0 0;
  padding: 1.4rem 0 0;
  border-top: 2px solid var(--line);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: 1.6rem;
}
.type-row {
  display: grid;
  align-content: start;
  gap: 0.3rem;
  background: var(--bg);
  border: 2px solid var(--line);
  padding: 1rem 1.1rem 1.2rem;
}
.type-specimen {
  margin: 0;
  color: var(--ink);
  line-height: 1.1;
}
.type-specimen--display {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.4rem + 1.6vw, 2.4rem);
  font-weight: 800;
  letter-spacing: 0.02em;
  line-height: 0.9;
}
.type-specimen--ui {
  font-family: var(--font-ui);
  font-size: 1.25rem;
  font-weight: 500;
}
.type-specimen--mono {
  font-family: var(--font-mono);
  font-size: 1.25rem;
}
.type-name {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.type-role {
  margin: 0;
  max-width: 46ch;
  font-size: 12.5px;
  color: var(--ink-dim);
}
/* ---------- the backdrop plate ---------- */

/* Two placements, two machines. `--page` is the full-height plate behind the
   reading pages; `--hero` is a small absolute one standing in for the home
   page's SVG bot, and it changes machine with the mode.

   Both photographs are shot on true black, so `screen` drops their surround
   onto the page colour exactly and the machine reads as a cut-out rather than a
   picture in a box. That is the whole treatment - no filter, no edge feathering,
   nothing to keep in sync with the artwork. If a replacement plate ever shows a
   rectangle, the fix is a blacker background in the image, not a filter here.

   The paint lives inside the query on purpose: a background-image in a rule that
   does not match is never fetched, so phones pay nothing and a resistance
   visitor never downloads the yellow machine. An <img> cannot do this - it
   starts downloading before CSS gets a say. */
.backdrop {
  display: none;
}

/* 46.01rem is the exact complement of the hero's own `max-width: 46rem`
   collapse. Anything wider and the two disagree: between them the hero is still
   a two-column grid reserving space for a bot that the plate has already taken
   over from, so the SVG sits alone in a void beside a headline that wrapped to
   make room for it. Move one of these and move the other. */
@media (min-width: 46.01rem) {
  .backdrop {
    display: block;
    pointer-events: none;
    background: url("/killbot-bg.webp") no-repeat right center / auto 100%;
    mix-blend-mode: screen;
  }

  /* One number drives the hero plate and the room the hero leaves for it, so
     they cannot drift apart. */
  :root {
    --plate-h: clamp(300px, 38vw, 540px);
    --plate-w: calc(var(--plate-h) * 0.75);
  }

  /* The box is the image: the artwork is exactly 3:4, so at `auto 100%` its
     rendered width is 75% of its height, and matching that means the plate is
     never cropped at any viewport. `lvh` rather than `dvh` because `dvh` changes
     as mobile browser chrome retracts, and a decoration that resizes itself
     while you scroll past it is worse than one that runs a little under the
     chrome. */
  .backdrop--page {
    position: fixed;
    top: 0;
    right: 0;
    height: 100lvh;
    width: min(75lvh, 100vw);
    z-index: -1;
  }

  /* Absolute, not fixed: it belongs to the hero and should scroll away with it.
     Body is static, so this resolves against the initial containing block, whose
     width is the same one `--shell` measures its 100% against - which is what
     lets `right` land the plate exactly on the shell's right edge rather than
     bleeding to the viewport. */
  .backdrop--hero {
    position: absolute;
    top: 0;
    right: calc((100% - var(--shell)) / 2);
    height: var(--plate-h);
    width: var(--plate-w);
    z-index: -1;
  }

  /* Resistance gets its own machine, defeated. Only the matching rule is ever
     painted, so each mode downloads one photograph and not the other. */
  [data-mode="resist"] .backdrop--hero {
    background-image: url("/killbot-defeated-bg.webp");
  }

  /* ---- the mouth ----

     The killbot's grille is an LED marquee, and the video of it is mounted into
     the plate by app.js rather than shipped in the markup. Two boxes, because
     the aperture and the footage are measured off different things and only one
     of them is ever nudged.

     `.marquee` is the aperture: the mouth as it sits in killbot-bg.webp,
     measured off the 1344x1792 original and written as a share of the plate, so
     it tracks `--plate-h` at every viewport. The grille runs downhill to the
     right, so the box is its bounding rectangle and `clip-path` cuts the
     parallelogram out of it - a straight inset would slice the corners off.
     Clipping to the interior is what makes this seamless: the bezel, the
     highlight and the shadow around the mouth stay the photograph's, and the
     video only ever supplies what is behind the slats, which is black in both.

     The video inside it is the same machine shot closer, and not a crop of the
     same file - its grille is a little longer and a little taller in proportion
     - so it is placed by matching grille to grille rather than frame to frame.
     That needs an uneven scale, hence `object-fit: fill`: the footage is
     stretched about 9% horizontally against its own aspect, which is invisible
     on a band of slats and is what lines the four edges up.

     The four numbers are near zero because the file has been cropped to exactly
     what the clip above exposes - the delivered frame *is* the aperture, give or
     take the rounding to whole source pixels, and the 40% of overhang is the
     clip reaching past the box. A nudge of more than a percent or so means
     re-cropping the source rather than sliding the frame, or the edge of the
     footage walks into view.

     Two different corrections, and it matters which one you are making. Footage
     sitting wrong *inside* the mouth is these four numbers. The whole assembly
     sitting wrong *on the machine* is the aperture's own `top` / `left`, which
     were read off the photograph by edge detection and are only as good as that
     read - moving them slides the clip and the footage together and does not
     disturb the fit between them. The `top` here has been corrected by eye once
     already, which is what that job looks like. */
  .marquee {
    position: absolute;
    left: 23.512%;
    top: 49.64%;
    width: 43.304%;
    height: 6.836%;
    clip-path: polygon(0 0, 100% 42.78%, 100% 140%, 0% 91.37%);
  }
  .marquee video {
    position: absolute;
    left: -0.06%;
    top: -0.073%;
    width: 100.05%;
    height: 140.351%;
    object-fit: fill;
    /* The sign is lit in `--guide`, and the chain that gets it there is fitted
       rather than dialled in. The LEDs are blown out to white in the footage,
       so rotating the hue only ever reached the warm bloom around them - the
       characters stayed grey and the slats behind them went blue, which is a
       sign lit from behind rather than a sign that is on. `sepia` collapses
       every pixel onto one hue first, which is what a monochrome LED sign is
       anyway, and the rest of the chain carries that hue to the token.

       The numbers are exact and the chain is one linear ramp: white lands on
       `--guide`, black stays black, nothing clips in between. Black staying
       black is what keeps the clip seamless - the slat gaps the photograph is
       still supplying cannot be shifted by this, and a chain that clipped
       would burn the hottest LEDs back to white. Retuning `--guide` means
       refitting these four numbers, which is not a thing you could guess by
       reading them, so `test/design.test.js` runs the chain on white and fails
       if it no longer arrives at the token. */
    filter: brightness(0.627) sepia(1) hue-rotate(145deg) saturate(1.51);
  }

  /* Between passes the sign is off and the mouth is the photograph's again.
     `visibility` rather than `display`, which would tear down the video box and
     make the next pass pay to lay it out; and rather than `opacity: 0`, which
     keeps compositing a layer nobody can see. */
  .marquee video[data-idle] {
    visibility: hidden;
  }

  /* The dead machine gets the mouth too, driven by the glitch drivers in
     app.js. The driver board breaking to the Resistance red is the one fault
     with a CSS half - `data-fault` means nothing without this rule. Same shape
     of chain as the blue one and fitted the same way, so white lands on
     `--resist` exactly. */
  .marquee video[data-fault="red"] {
    filter: brightness(0.294) sepia(1) hue-rotate(315deg) saturate(6.3);
  }

  /* The flip tears the machine apart rather than dissolving it. `[data-tear]` is
     set on the plate for the length of one animation and taken off again on
     `animationend`, so this runs on the stance flip and never on arrival: two
     copies of the *outgoing* machine, clipped to bands and thrown sideways over
     the incoming one, while the plate itself cuts out twice.

     The bands name the other mode's file, which is the one already on the
     machine: `data-mode` has flipped by the time these rules match, so the
     outgoing photograph is the cached one and the tear costs no download. Both
     rules stay inside the width query with the rest of the plate - outside it a
     phone would fetch a photograph it is never going to show.

     Displacements are percentages of the plate's own width, so the tear scales
     with `--plate-w` instead of being tuned for one viewport. */
  .backdrop--hero[data-tear] {
    animation: plate-cut 420ms steps(1, end);
  }
  .backdrop--hero[data-tear]::before,
  .backdrop--hero[data-tear]::after {
    content: "";
    position: absolute;
    inset: 0;
    background: url("/killbot-defeated-bg.webp") no-repeat right center / auto 100%;
    animation: plate-tear-a 420ms steps(1, end);
  }
  .backdrop--hero[data-tear]::after {
    animation-name: plate-tear-b;
  }
  [data-mode="resist"] .backdrop--hero[data-tear]::before,
  [data-mode="resist"] .backdrop--hero[data-tear]::after {
    background-image: url("/killbot-bg.webp");
  }

  /* `inset(0 0 100% 0)` is a band of no height - the way a clipped layer hides
     without a second property to keep in step with it. */
  @keyframes plate-tear-a {
    0% {
      clip-path: inset(10% 0 72% 0);
      transform: translate3d(-9%, 0, 0);
    }
    20% {
      clip-path: inset(0 0 100% 0);
      transform: none;
    }
    32% {
      clip-path: inset(44% 0 38% 0);
      transform: translate3d(12%, 0, 0);
    }
    52% {
      clip-path: inset(0 0 100% 0);
      transform: none;
    }
    68% {
      clip-path: inset(66% 0 18% 0);
      transform: translate3d(-6%, 0, 0);
    }
    80%,
    100% {
      clip-path: inset(0 0 100% 0);
      transform: none;
    }
  }
  @keyframes plate-tear-b {
    0%,
    12% {
      clip-path: inset(0 0 100% 0);
      transform: none;
    }
    24% {
      clip-path: inset(28% 0 56% 0);
      transform: translate3d(7%, 0, 0);
    }
    40% {
      clip-path: inset(0 0 100% 0);
      transform: none;
    }
    56% {
      clip-path: inset(80% 0 6% 0);
      transform: translate3d(-11%, 0, 0);
    }
    72% {
      clip-path: inset(18% 0 66% 0);
      transform: translate3d(5%, 0, 0);
    }
    84%,
    100% {
      clip-path: inset(0 0 100% 0);
      transform: none;
    }
  }
  @keyframes plate-cut {
    0%,
    16% {
      opacity: 1;
    }
    24% {
      opacity: 0.15;
    }
    36% {
      opacity: 1;
    }
    60% {
      opacity: 0.4;
    }
    72%,
    100% {
      opacity: 1;
    }
  }

  /* An absolute plate reserves no space, so the hero has to leave it some or the
     headline sets straight through the machine's face. The SVG bot it replaces
     was a grid item and got this for free. */
  body:has(.backdrop--hero) .hero {
    padding-right: calc(var(--plate-w) + 2rem);
  }

  /* The prose card gives up its centring and hugs the left of the shell, so the
     machine keeps the right of the frame. Keyed off the plate rather than off
     the page, so a future prose page without one keeps its centring - and so
     the shorthand `margin` further up this file cannot win on specificity. */
  body:has(.backdrop--page) .panel--prose {
    margin-inline: 0;
  }

  /* Glass, so the machine reads through the cards instead of being boxed out of
     its own page. 74% of the page colour, measured rather than eyeballed: the
     plate's brightest pixel is a white specular highlight, and against that
     `--ink-dim` body text lands at 4.75:1 - 72% measures 4.41 and fails AA, so
     74 is the floor and not a round number someone picked. Borders step up to
     `--line-strong`; a 2px `--line` hairline vanishes against the yellow. */
  body:has(.backdrop--page) .panel--prose,
  body:has(.backdrop--page) .asset,
  body:has(.backdrop--page) .swatch,
  body:has(.backdrop--page) .type-row,
  body:has(.backdrop--page) .score-embed-note,
  body:has(.backdrop--page) .score-embed-code,
  body:has(.backdrop--page) .score-embed .download {
    background: color-mix(in srgb, var(--bg) 74%, transparent);
    backdrop-filter: blur(14px);
    border-color: var(--line-strong);
  }

  /* The footer runs the full shell, so on a plate page it lands on the bright
     lower half of the machine and its 11px links stop being readable. This one
     stays opaque: the bottom margin traded for equal padding so the fill reaches
     the barcode rule, and a spread shadow clipped to the block axis so it goes
     full-bleed like the rule beneath it. A shadow rather than a 100vw
     pseudo-element because a shadow can never raise a horizontal scrollbar. */
  body:has(.backdrop--page) .footer {
    background: var(--bg);
    box-shadow: 0 0 0 100vmax var(--bg);
    clip-path: inset(0 -100vmax);
    margin-bottom: 0;
    padding-bottom: 2.5rem;
  }
}

/* Glass is decoration; the words underneath are the terms of a sale. */
@media (prefers-reduced-transparency: reduce) {
  body:has(.backdrop--page) .panel--prose,
  body:has(.backdrop--page) .asset,
  body:has(.backdrop--page) .swatch,
  body:has(.backdrop--page) .type-row,
  body:has(.backdrop--page) .score-embed-note,
  body:has(.backdrop--page) .score-embed-code,
  body:has(.backdrop--page) .score-embed .download {
    background: var(--bg);
    backdrop-filter: none;
  }
}

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

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

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
}
