/*
 * Abracalc — "Lamplight".
 *
 * The design idea, stated once so it stops anyone diluting it later:
 *
 *   The competition all look like shops. Wonderbly is a premium gift retailer,
 *   the AI entrants are SaaS landing pages with a book on them. All of them are
 *   bright, white and transactional, because they are selling an object.
 *
 *   We are not selling an object, we are selling eight o'clock at night. So the
 *   page IS a dark room, and every card is a PAGE UNDER A LAMP — warm paper
 *   floating on deep blue, with the light falling off at the edges. Forms stay
 *   comfortable to fill in because they sit on paper; the room around them
 *   carries the mood.
 *
 * Everything else follows from that: the gold is lamplight, the glow behind the
 * active card is the lamp, the star field is the window, and the type is
 * Fraunces — the same face the books are set in, so the site and the product
 * are visibly one thing.
 *
 * Fonts are self-hosted. No CDN, no third-party request, nothing to leak a
 * visitor to. That is also why the CSP can stay as tight as it is.
 */

@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/fraunces.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Fraunces';
  src: url('/fonts/fraunces-italic.woff2') format('woff2');
  font-weight: 100 900;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'Literata';
  src: url('/fonts/literata.woff2') format('woff2');
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /* the room */
  --night: #0E1424;
  --night-2: #151D33;
  --night-3: #1E2842;
  --hairline: rgba(246, 192, 90, .16);

  /* the page under the lamp */
  --paper: #FAF4E4;
  --paper-2: #F1E8D2;
  --paper-3: #E4D8BC;
  --ink: #1A2133;
  --ink-2: #4E566B;
  /* Was #838A9C: 3.15:1 on --paper, 2.83:1 on --paper-2, 3.39:1 on the input
     fill. That is a fail, and not on decoration — this one token carries every
     .hint (the instructional text that is the whole product), every h3,
     input::placeholder (the placeholders ARE the worked examples), .cast .role,
     .outline-card .tag, .beats .slot, the beat numerals, .shelf-card .gone and
     the download-code label. Same cool slate hue (223°), taken down until the
     worst ground clears 4.5:1: 5.13:1 on --paper, 4.62:1 on --paper-2, 5.49:1
     on the card gradient's lit top, 5.53:1 on the input fill. It sits closer to
     --ink-2 (6.67:1) than it used to; that is the price of rank three being
     readable, and size, weight and tracking carry the rest of the hierarchy. */
  --ink-3: #5C6784;

  /* the lamp */
  --lamp: #F6C05A;
  --lamp-hot: #FFD98A;
  --lamp-glow: rgba(246, 192, 90, .22);
  /* The lamp is 11:1 on the night ground and 1.52:1 on paper, so a gold link or
     a gold ring on a lit page is not dim, it is absent — the shelf's "Remove"
     was gold on --paper. Same lamplight taken down to ink: 5.13:1 on --paper,
     4.62:1 on --paper-2. */
  --lamp-ink: #8A5F14;

  --rose: #E4785E;
  /* --rose is 6.25:1 on the night ground, which is where .error was designed.
     But #cast-error and #failed-why sit inside .gate, which is paper, and there
     it measured 2.68:1 — the one message that tells a customer their redraw or
     their extra photo failed, and it is hidden again after nine seconds. This is
     the same coral carried into ink: 5.16:1 on --paper, 4.64:1 on --paper-2. */
  --rose-ink: #AA4931;
  --sage: #86B08A;
  /* .privacy-note strong is --sage, and inside a .card or .gate that is sage on
     tinted paper: 2.03:1, on the sentence about a child's photographs. 4.60:1 on
     the tinted note, 5.05:1 on bare --paper. */
  --sage-ink: #3E7444;
  --star: #FFF6DD;

  /* Type on the dark ground, where the paper ink would vanish. */
  --on-night: #EDE6D6;
  --on-night-2: #A9B2C9;
  /* Was #6E7893: 4.17:1 on --night, and worse than that everywhere it is
     actually read. The room is a gradient — its top stop is #101728 (4.06:1) —
     and the lamp's throw lifts the ground further still, to 3.65:1 where the
     step rail sits. This token carries the whole progress .feed, .steps li, the
     ETA caption, .dropzone .hint (the format and size rules) and the footer.
     #8790AB measures 5.77:1 on --night, 5.62:1 on the top stop, 5.05:1 under
     the lamp throw, 5.24:1 inside the dropzone and 4.60:1 on --night-3. */
  --on-night-3: #8790AB;

  /*
   * Four colours that measure on the night ground and fail on paper. Rather than
   * every component overriding them one at a time, they are variables here and
   * the lit surfaces redefine them once, further down, next to .card. The room
   * is the default because most of the app is the room.
   */
  --focus: var(--lamp);      /* 8.6–11.4:1 on every ground in the room */
  --error: var(--rose);      /* 6.25:1 on --night */
  --good: var(--sage);       /* 7.51:1 on --night */
  --link: var(--lamp);       /* 11.02:1 on --night */
  --link-hover: var(--lamp-hot);

  --r-sm: 6px;
  --r: 10px;
  --r-lg: 16px;

  --display: 'Fraunces', 'Iowan Old Style', Georgia, serif;
  --book: 'Literata', Georgia, serif;
  --ui: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  /* A page lifted off a dark surface needs a shadow AND a warm rim. */
  --lift:
    0 1px 0 rgba(255, 255, 255, .5) inset,
    0 18px 40px -18px rgba(0, 0, 0, .7),
    0 2px 6px rgba(0, 0, 0, .3);
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--night);
  color: var(--on-night);
  font: 400 17px/1.65 var(--book);
  -webkit-font-smoothing: antialiased;
  /* The window: a scatter of stars, and the lamp's throw from upper left. */
  background-image:
    radial-gradient(1px 1px at 12% 18%, var(--star), transparent),
    radial-gradient(1px 1px at 28% 9%, rgba(255,246,221,.7), transparent),
    radial-gradient(1.5px 1.5px at 46% 22%, var(--star), transparent),
    radial-gradient(1px 1px at 63% 12%, rgba(255,246,221,.55), transparent),
    radial-gradient(1.5px 1.5px at 79% 26%, var(--star), transparent),
    radial-gradient(1px 1px at 91% 14%, rgba(255,246,221,.6), transparent),
    radial-gradient(1px 1px at 8% 44%, rgba(255,246,221,.4), transparent),
    radial-gradient(1px 1px at 88% 52%, rgba(255,246,221,.35), transparent),
    radial-gradient(ellipse 70% 50% at 22% -8%, rgba(246,192,90,.13), transparent 70%),
    linear-gradient(180deg, #101728 0%, var(--night) 42%, #0A1020 100%);
  background-attachment: fixed;
}

/* ---------- chrome ---------- */

.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 18px clamp(18px, 5vw, 52px);
  border-bottom: 1px solid var(--hairline);
}
.wordmark {
  font-family: var(--display); font-weight: 600; font-size: 23px;
  letter-spacing: -.02em; color: var(--paper); text-decoration: none;
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
}
/* The star is the brand mark: bedtime, and a bit of magic. */
.wordmark::after {
  content: '✦'; color: var(--lamp); font-size: .62em;
  margin-left: .18em; vertical-align: .22em;
}

.foot {
  margin-top: auto; padding: 26px clamp(18px, 5vw, 52px);
  border-top: 1px solid var(--hairline);
  font-family: var(--ui); font-size: 12.5px; color: var(--on-night-3);
}

/* ---------- screens ---------- */

.screen { display: none; }
.screen.active { display: block; animation: rise .4s cubic-bezier(.2,.7,.3,1); }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } }
@media (prefers-reduced-motion: reduce) { .screen.active { animation: none; } }

.wrap { max-width: 780px; margin: 0 auto; padding: clamp(26px, 5vw, 56px) 20px 90px; }
.wrap.wide { max-width: 1060px; }
.wrap.narrow { max-width: 540px; }

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

.hero {
  max-width: 720px; margin: 0 auto;
  padding: clamp(46px, 10vw, 118px) 20px 96px;
  text-align: center;
  position: relative;
}
/* The lamp itself, thrown behind the headline. */
.hero::before {
  content: ''; position: absolute; top: -6%; left: 50%;
  width: min(760px, 96vw); height: 460px; transform: translateX(-50%);
  background: radial-gradient(ellipse at center, var(--lamp-glow), transparent 68%);
  pointer-events: none; z-index: 0;
}
.hero > * { position: relative; z-index: 1; }

.eyebrow {
  font-family: var(--ui); font-size: 11.5px; font-weight: 600;
  letter-spacing: .22em; text-transform: uppercase; color: var(--lamp);
  margin-bottom: 22px;
}
.eyebrow::before, .eyebrow::after {
  content: '·'; margin: 0 .8em; opacity: .55;
}

h1 {
  font-family: var(--display);
  font-size: clamp(36px, 6.4vw, 62px); line-height: 1.02;
  letter-spacing: -.028em; font-weight: 600; color: var(--paper);
  margin-bottom: 22px; text-wrap: balance;
  font-variation-settings: 'SOFT' 40, 'WONK' 1, 'opsz' 96;
}
h1 em {
  font-style: italic; color: var(--lamp-hot);
  font-variation-settings: 'SOFT' 60, 'WONK' 1;
}

h2 {
  font-family: var(--display); font-size: clamp(25px, 3.5vw, 35px);
  line-height: 1.12; letter-spacing: -.022em; font-weight: 600;
  color: var(--paper); margin-bottom: 10px; text-wrap: balance;
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
}
h3 {
  font-family: var(--ui); font-size: 12px; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3);
  margin-bottom: 16px;
}

.lede {
  font-size: 19.5px; line-height: 1.62; color: var(--on-night-2);
  max-width: 54ch; margin: 0 auto 36px;
}
.sub { color: var(--on-night-2); margin-bottom: 26px; max-width: 62ch; font-size: 16.5px; }

/* ---------- controls ---------- */

button, input, select, a.primary { font: inherit; }

/*
 * The keyboard indicator. There was none: no :focus-visible rule existed for any
 * button in this file, and .dropzone shared one declaration with :hover that set
 * `outline: none` — so the only control that acknowledged focus at all did it by
 * being indistinguishable from a mouse hover. Shape matches the prototype in
 * intake-demo.html:121-123 (2.5px solid, offset 2px); the colour comes from
 * --focus, which is the lamp in the room and ink on a lit page, because the lamp
 * measures 1.52:1 against paper and a ring nobody can see is not a ring.
 */
:focus-visible { outline: 2.5px solid var(--focus); outline-offset: 2px; }

/*
 * Buttons are printed, not inflated: a small radius and a hard bottom edge, so
 * they read as a pressed key rather than the pill every AI product ships.
 *
 * min-height is the 44px floor from WCAG 2.5.5. The padding already cleared it
 * here; it is declared anyway so shrinking the padding later cannot quietly
 * take a control back under the floor.
 */
.primary {
  background: linear-gradient(180deg, var(--lamp-hot), var(--lamp));
  color: #2A1E06; border: 0; border-radius: var(--r-sm);
  padding: 13px 26px; cursor: pointer;
  font-family: var(--ui); font-weight: 700; font-size: 15px; letter-spacing: -.005em;
  box-shadow: 0 2px 0 #B8862C, 0 8px 20px -8px rgba(246,192,90,.6);
  transition: transform .1s, box-shadow .1s, filter .12s;
  text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px;
}
.primary:hover { filter: brightness(1.05); transform: translateY(-1px);
                 box-shadow: 0 3px 0 #B8862C, 0 12px 26px -8px rgba(246,192,90,.7); }
.primary:active { transform: translateY(1px); box-shadow: 0 1px 0 #B8862C; }
.primary:disabled { opacity: .35; cursor: not-allowed; transform: none;
                    box-shadow: 0 2px 0 #B8862C; filter: grayscale(.4); }
.primary.big { padding: 17px 42px; font-size: 17px; }

.ghost {
  background: transparent; color: var(--ink-2);
  border: 1px solid var(--paper-3); border-radius: var(--r-sm);
  padding: 11px 20px; cursor: pointer;
  font-family: var(--ui); font-weight: 600; font-size: 14px;
  transition: border-color .15s, color .15s, background .15s;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px;
}
.ghost:hover { border-color: var(--ink-3); color: var(--ink); background: rgba(0,0,0,.03); }
/* .small keeps the small type — it is "Copy" beside a download code and "Draw
   again" under a face — but not the ~27px box it used to have. The floor holds
   through min-height above; only the padding shrinks. */
.ghost.small { padding: 7px 13px; font-size: 12.5px; }
/* On the dark ground the paper-coloured ghost would disappear. */
.gate-actions .ghost, .actions > .ghost { }
.on-night .ghost, .hero .ghost {
  color: var(--on-night-2); border-color: rgba(237,230,214,.22);
}

.linkish {
  background: none; border: 0; cursor: pointer; color: var(--on-night-2);
  font-family: var(--ui); font-size: 14px;
  border-bottom: 1px solid var(--hairline); padding-bottom: 2px;
}
.linkish:hover { color: var(--lamp); border-color: var(--lamp); }

/*
 * Not in the audit, and worse than anything that was: no rule in this file ever
 * gave an inline <a> a colour, so the two real links on the site — "Exactly what
 * happens to your photos" in the upload screen's privacy note, and the same link
 * in the footer — rendered in the browser's default blue on a near-black ground.
 * That measures 2.02:1 unvisited and 1.71:1 once visited, on the sentence about
 * where a child's photographs go. They take --link like everything else, so on
 * paper they become ink automatically.
 *
 * .primary, .ghost and .wordmark are excluded because they are already controls
 * with their own colour; without the exclusion the shelf's Download button — an
 * <a class="ghost"> — turned into an underlined amber link.
 */
a:not(.primary):not(.ghost):not(.wordmark) {
  color: var(--link); text-decoration: underline; text-underline-offset: 2px;
}
a:not(.primary):not(.ghost):not(.wordmark):hover { color: var(--link-hover); }

input, select, textarea {
  width: 100%; padding: 11px 13px;
  border: 1px solid var(--paper-3); border-radius: var(--r-sm);
  background: #FFFDF6; color: var(--ink);
  font-family: var(--ui); font-size: 15px;
  transition: border-color .15s, box-shadow .15s;
}
/* The gold glow is the field lighting up and it stays. What is gone is the
   `outline: none` that used to accompany it: on a card that glow is gold on
   paper at 1.6:1, so it was decoration standing in for an indicator. The
   :focus-visible rule above now draws the real ring on top of it. */
input:focus, select:focus, textarea:focus {
  border-color: var(--lamp);
  box-shadow: 0 0 0 3px rgba(246,192,90,.28);
}
input::placeholder { color: var(--ink-3); }

label {
  display: block; font-family: var(--ui); font-size: 12.5px; font-weight: 600;
  color: var(--ink-2); margin-bottom: 15px;
}
label input, label select { margin-top: 6px; }
.hint { display: block; font-weight: 400; color: var(--ink-3); font-size: 12px; margin-top: 3px; }

.grid { display: grid; gap: 0 16px; }
.grid.two { grid-template-columns: 1fr 1fr; }
@media (max-width: 640px) { .grid.two { grid-template-columns: 1fr; } }

/*
 * The one place the room becomes a lit page. Every colour that measures on the
 * night ground and fails on paper is swapped here, once, for its ink counterpart
 * — so a new component that sits on paper inherits legible error text, a legible
 * focus ring and a legible link without knowing anything about any of this.
 * .outline-card is deliberately NOT in this list: it is itself a button, so its
 * focus ring lands on the room outside the card, where gold is 11:1 and ink
 * would be 1.2:1.
 */
.card, .gate, .shelf-card, .code-block, .cast figure {
  --focus: var(--ink);          /* 13.2–15.8:1 on every paper ground */
  --error: var(--rose-ink);     /* 5.16:1 on --paper, was 2.68:1 */
  --good: var(--sage-ink);      /* 4.60:1 on the tinted note, was 2.03:1 */
  --link: var(--lamp-ink);      /* 5.13:1 on --paper, was 1.52:1 */
  --link-hover: var(--ink);     /* 14.61:1 — on paper a link darkens */
}

/*
 * The lit page. This is the one component the whole design rests on: warm stock
 * lifted off the dark room, with a faint gold rim where the lamp catches the
 * edge.
 */
.card {
  background: linear-gradient(180deg, #FFFCF2, var(--paper));
  border-radius: var(--r-lg); padding: 24px; margin-bottom: 18px;
  color: var(--ink); box-shadow: var(--lift);
  position: relative;
}
.card::after {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-lg);
  border: 1px solid rgba(246,192,90,.35); pointer-events: none;
}
.card .sub, .card p { color: var(--ink-2); }

/* The padding/negative-margin pair is the 44px floor without a visual change:
   measured at 1440px this row was 28px tall (at 390px the hint wraps and it
   already cleared), and it is the only way to open "Your home and your street" —
   the section the copy calls the best question on the page. The padding grows the
   box, the negative margin puts the text back exactly where it was. */
.card.collapsible summary {
  cursor: pointer; list-style: none; display: flex; align-items: baseline;
  gap: 12px; flex-wrap: wrap;
  min-height: 44px; padding: 8px 0; margin: -8px 0;
}
.card.collapsible summary::-webkit-details-marker { display: none; }
.card.collapsible summary h3 { margin: 0; }
.card.collapsible summary::after {
  content: '＋'; margin-left: auto; color: var(--ink-3); font-family: var(--ui);
}
.card.collapsible[open] summary::after { content: '－'; }
.card.collapsible[open] summary { margin-bottom: 20px;
  padding-bottom: 16px; border-bottom: 1px solid var(--paper-3); }

/* flex-wrap, because on the ready screen this row holds .primary.big ("Download
   the PDF", 17px, 17px/42px padding) plus a ghost — about 361px of content in a
   350px content box at 390px wide. Without it the payoff screen either overflows
   or squashes the one button the whole flow exists to reach. .gate-actions
   already wrapped; this is the same fix. */
.actions { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-top: 28px; }
.actions.center { justify-content: center; }
[data-screen="ready"] .actions .primary, [data-screen="redeem"] .actions .primary { margin-left: 0; }
.actions .primary { margin-left: auto; }
.actions.center .primary { margin-left: 0; }
.actions > .ghost { color: var(--on-night-2); border-color: rgba(237,230,214,.22); }
.actions > .ghost:hover { color: var(--paper); border-color: rgba(237,230,214,.45);
                          background: rgba(255,255,255,.04); }

/* --error, not --rose: see the token. On the night ground this is still the same
   coral at 6.25:1; inside a .gate (#cast-error, #failed-why) it becomes the ink
   coral at 5.16:1 instead of 2.68:1. */
.error { color: var(--error); font-family: var(--ui); font-size: 14px; margin-top: 10px; }

/* ---------- landing ---------- */

.code-form {
  max-width: 430px; margin: 0 auto;
  background: rgba(255,255,255,.04); border: 1px solid var(--hairline);
  border-radius: var(--r-lg); padding: 20px;
  backdrop-filter: blur(2px);
}
.code-form label {
  color: var(--on-night-2); text-align: left;
  font-size: 11.5px; letter-spacing: .12em; text-transform: uppercase;
  margin-bottom: 9px;
}
.code-form .row { display: flex; gap: 10px; }
.code-form .row input {
  text-transform: uppercase; letter-spacing: .14em; font-weight: 600;
  font-family: var(--ui);
}

.assurances {
  list-style: none; margin-top: 46px; display: grid; gap: 13px;
  max-width: 430px; margin-left: auto; margin-right: auto; text-align: left;
}
.assurances li {
  font-family: var(--ui); font-size: 14px; color: var(--on-night-2);
  padding-left: 28px; position: relative; line-height: 1.5;
}
.assurances li::before {
  content: '✦'; position: absolute; left: 0; top: -1px;
  color: var(--lamp); font-size: 13px;
}
.assurances strong { color: var(--paper); font-weight: 600; }

/* ---------- step rail ---------- */

.steps {
  list-style: none; display: flex; gap: 0; margin-bottom: 36px;
  font-family: var(--ui); font-size: 12px; flex-wrap: wrap;
  align-items: center;
}
.steps li {
  color: var(--on-night-3); letter-spacing: .1em; text-transform: uppercase;
  font-weight: 600; display: flex; align-items: center;
}
.steps li + li::before {
  content: ''; width: 26px; height: 1px; background: var(--hairline); margin: 0 12px;
}
.steps li.on { color: var(--lamp); }
.steps li.done { color: var(--sage); }

/* ---------- photos ---------- */

.dropzone {
  border: 1.5px dashed rgba(246,192,90,.4); border-radius: var(--r-lg);
  padding: 54px 24px; text-align: center; cursor: pointer;
  background: rgba(255,255,255,.03);
  transition: border-color .18s, background .18s, transform .18s;
}
.dropzone:hover, .dropzone.over {
  border-color: var(--lamp); background: rgba(246,192,90,.09);
  transform: translateY(-2px);
}
/* :focus-visible used to share the rule above, including its `outline: none` —
   so the keyboard indicator on the only control on this screen was a 2px lift
   identical to a mouse hover. It keeps the lit look and gets the real ring from
   :focus-visible in the controls section (gold, 9.99:1 on this ground). */
.dropzone:focus-visible { border-color: var(--lamp); background: rgba(246,192,90,.09); }
.dropzone svg {
  width: 32px; height: 32px; stroke: var(--lamp); fill: none;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; margin-bottom: 15px;
}
.dropzone p { font-family: var(--ui); font-size: 15px; color: var(--on-night-2); }
.dropzone strong { color: var(--paper); }
.dropzone .hint { color: var(--on-night-3); }

.thumbs {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 10px; margin-top: 18px;
}
.thumbs figure {
  position: relative; aspect-ratio: 1; border-radius: var(--r); overflow: hidden;
  background: var(--night-3); box-shadow: 0 6px 16px -8px rgba(0,0,0,.7);
}
.thumbs img { width: 100%; height: 100%; object-fit: cover; display: block; }
/*
 * "Remove photo" was 22×22px in the corner of a 92px thumbnail — a quarter of the
 * 44×44 floor, on the phone the photographs were just taken on, for the one
 * control that undoes an accidental upload. The button IS 44×44 now; the visible
 * chip is drawn by ::before at 28px so the target could grow without a black
 * disc eating the picture. `isolation` makes the button its own stacking
 * context, which is what keeps the z-index:-1 chip behind the × and the whole
 * button above the photograph instead of behind it.
 */
.thumbs button {
  position: absolute; top: 0; right: 0; width: 44px; height: 44px;
  border: 0; padding: 0; background: transparent; color: var(--paper);
  display: grid; place-items: center; isolation: isolate;
  cursor: pointer; font-size: 15px; line-height: 1; font-family: var(--ui);
}
.thumbs button::before {
  content: ''; position: absolute; z-index: -1; inset: 8px;
  border-radius: 50%; background: rgba(14,20,36,.88);
}
/*
 * The ring sits on the chip rather than on the transparent 44px box, and carries
 * its own dark halo: the surround here is a photograph, so neither gold nor ink
 * can be assumed to contrast with whatever is behind it. Against the chip the
 * gold measures 7.9:1 over a white photograph and 11.2:1 over a black one, and
 * the halo is what guarantees the outer edge whatever the picture is doing.
 */
.thumbs button:focus-visible { outline: none; }
.thumbs button:focus-visible::before {
  outline: 2.5px solid var(--lamp); outline-offset: 1px;
  box-shadow: 0 0 0 6px rgba(14,20,36,.92);
}

/* The privacy promises are the brand, so they are typeset, not disclaimed. */
.privacy-note {
  font-family: var(--ui); font-size: 13px; color: var(--on-night-2);
  background: rgba(134,176,138,.09); border: 1px solid rgba(134,176,138,.26);
  border-radius: var(--r); padding: 13px 16px; margin-top: 22px; line-height: 1.55;
}
/* --good, not --sage: the same green is 7.51:1 on the night ground and 2.03:1 on
   the tinted note inside a .card or .gate, which is where it makes the promise
   about a child's photographs. */
.privacy-note strong { color: var(--good); font-weight: 600; }
.card .privacy-note, .gate .privacy-note {
  background: rgba(134,176,138,.13); border-color: rgba(134,176,138,.4); color: var(--ink-2);
}
.privacy-note.small { font-size: 12px; margin-top: 16px; }

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

.outlines {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
  margin-bottom: 26px;
}
.outline-card {
  text-align: left; cursor: pointer; position: relative;
  background: linear-gradient(180deg, #FFFCF2, var(--paper));
  border: 0; border-radius: var(--r-lg); padding: 20px;
  box-shadow: var(--lift);
  display: flex; flex-direction: column; gap: 9px;
  transition: transform .18s cubic-bezier(.2,.7,.3,1), box-shadow .18s;
}
.outline-card::after {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-lg);
  border: 1px solid rgba(246,192,90,.3); pointer-events: none; transition: border-color .18s;
}
.outline-card:hover { transform: translateY(-3px) rotate(-.35deg); }
.outline-card.sel { transform: translateY(-3px); }
.outline-card.sel::after { border: 2px solid var(--lamp); }
.outline-card h4 {
  font-family: var(--display); font-size: 19px; font-weight: 600;
  letter-spacing: -.016em; line-height: 1.16; color: var(--ink);
  font-variation-settings: 'SOFT' 40, 'WONK' 1;
}
.outline-card p { font-family: var(--book); font-size: 14px; color: var(--ink-2); line-height: 1.5; }
.outline-card .tag {
  font-family: var(--ui); font-size: 10.5px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3); margin-top: auto;
}
/* Was #7A5410 on --lamp: 4.06:1, at 10px bold with .14em tracking, i.e. small
   text held to the large-text threshold it does not qualify for. #6B4A0C is the
   same warm brown two steps darker — 4.83:1 on --lamp, 5.96:1 on --lamp-hot. */
.outline-card .suggested {
  font-family: var(--ui); font-size: 10px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; color: #6B4A0C;
  background: var(--lamp); padding: 3px 8px; border-radius: 999px;
  align-self: flex-start;
}

.radio {
  display: flex; gap: 12px; align-items: flex-start; margin-bottom: 14px;
  font-family: var(--ui); font-size: 14px; color: var(--ink-2); line-height: 1.55;
}
.radio input { width: auto; margin-top: 3px; flex: none; accent-color: var(--lamp); }
.radio strong { color: var(--ink); }

/* The price, on the one card that says it. No new colour: the amount is --ink like
   every other thing a customer is asked to read carefully, and the line under it is a
   .hint like every other explanation on this form. `.hint` is inline-block by default,
   which is why the amount and the caption sit on one line here. */
.pay .price { font-family: var(--ui); margin-bottom: 10px; }
.pay .price strong { font-size: 26px; color: var(--ink); margin-right: 8px; }
.pay .price .hint { display: inline; }

.depth { flex: 1; height: 4px; background: rgba(255,255,255,.1); border-radius: 999px; overflow: hidden; }
.depth span {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--lamp), var(--lamp-hot));
  box-shadow: 0 0 10px var(--lamp-glow);
  transition: width .45s cubic-bezier(.2,.7,.3,1);
}

/* ---------- working ---------- */

/* No wrap here meant the h2 (clamp(25px, 3.5vw, 35px)) and a 28px tabular ETA
   numeral fought over a 350px content box at 390px wide, and both lost. The
   flex-basis is what stops it wrapping on a desktop where there is room. */
.working-head {
  display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between;
  gap: 20px; margin-bottom: 24px;
}
.working-head > div:first-child { flex: 1 1 240px; }
/* margin-left keeps the ETA on the right once it has wrapped onto its own line,
   where justify-content: space-between no longer has anything to space it from. */
.eta { text-align: right; font-family: var(--ui); margin-left: auto; }
.eta span {
  display: block; font-size: 28px; font-weight: 700; letter-spacing: -.03em;
  font-variant-numeric: tabular-nums; color: var(--lamp);
}
.eta small { color: var(--on-night-3); font-size: 11px; letter-spacing: .1em;
             text-transform: uppercase; }

.rail { display: flex; gap: 5px; margin-bottom: 30px; }
.rail div {
  flex: 1; height: 4px; border-radius: 999px; background: rgba(255,255,255,.09);
  position: relative; overflow: hidden;
}
.rail div.done { background: var(--sage); }
.rail div.now { background: rgba(246,192,90,.2); }
.rail div.now::after {
  content: ''; position: absolute; inset: 0; border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--lamp), transparent);
  animation: sweep 1.8s ease-in-out infinite;
}
@keyframes sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
  .rail div.now::after { animation: none; background: var(--lamp); opacity: .7; }
}
/*
 * A stalled stage stops sweeping.
 *
 * The sweep means "this is happening now", and for a long time it went on meaning
 * that over a dead connection: the fallback poll fired exactly once, ever, so a
 * dropped Wi-Fi or a slept phone left the customer watching an animation that said
 * work was in progress while nothing was being received. The client says so in words
 * now and freezes the estimate; leaving the motion running would contradict both.
 * Dimmed rather than hidden, because the stage genuinely is the current one — it is
 * the liveness that is in doubt, not the position.
 */
.rail.stalled div.now::after { animation: none; background: var(--paper-3); opacity: .45; }

.working-body { display: grid; grid-template-columns: 290px 1fr; gap: 30px; align-items: start; }
@media (max-width: 800px) { .working-body { grid-template-columns: 1fr; } }

.feed {
  font-family: var(--ui); font-size: 13px; color: var(--on-night-3);
  max-height: 430px; overflow-y: auto; display: flex; flex-direction: column; gap: 11px;
  padding-left: 20px; position: relative;
}
.feed::before {
  content: ''; position: absolute; left: 3px; top: 4px; bottom: 4px;
  width: 1px; background: var(--hairline);
}
.feed p { position: relative; animation: fadein .32s ease; line-height: 1.5; }
.feed p::before {
  content: ''; position: absolute; left: -20px; top: 7px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--on-night-3);
}
/* The newest line is the one being lived through, so it gets the lamp. */
.feed p:last-child { color: var(--paper); font-weight: 500; }
.feed p:last-child::before {
  background: var(--lamp); box-shadow: 0 0 0 3px rgba(246,192,90,.22);
}
@keyframes fadein { from { opacity: 0; transform: translateX(-5px); } }

.preview-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(114px, 1fr)); gap: 12px;
}
/* Each finished page arrives as a lit plate — the book assembling itself. */
.preview-grid img {
  width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--r);
  background: var(--night-3);
  box-shadow: 0 1px 0 rgba(246,192,90,.28) inset, 0 10px 22px -12px rgba(0,0,0,.85);
  animation: plate .5s cubic-bezier(.2,.7,.3,1);
}
@keyframes plate { from { opacity: 0; transform: scale(.92) translateY(6px); } }

/*
 * When .working-body's columns collapse, the feed lands above the previews in
 * source order: a 430px nested scroll region that auto-scrolls itself, filling a
 * phone viewport, pushing the pages the customer is actually waiting for below
 * the fold, and taking over touch scrolling for any finger that starts inside it.
 * Here it goes below the previews and stops being a scroll region at all, so the
 * page scrolls — which is what the finger expects.
 *
 * This block has to sit after .feed and .preview-grid rather than beside the
 * grid-template-columns switch above: a media query carries no extra
 * specificity, so up there `max-height: none` lost to the plain `.feed` rule.
 */
@media (max-width: 800px) {
  .preview-grid { order: 1; }
  .feed { order: 2; max-height: none; overflow: visible; }
}

/* ---------- gates ---------- */

/*
 * A gate is the moment the process stops and asks. It gets the strongest lamp
 * on the page, because it must not be possible to miss it.
 */
.gate {
  margin-top: 36px; position: relative;
  background: linear-gradient(180deg, #FFFCF2, var(--paper));
  border-radius: var(--r-lg); padding: 28px; color: var(--ink);
  box-shadow:
    0 0 0 1px var(--lamp),
    0 0 0 6px rgba(246,192,90,.16),
    0 30px 60px -28px rgba(0,0,0,.9);
  animation: rise .4s cubic-bezier(.2,.7,.3,1);
}
.gate-head { margin-bottom: 24px; padding-bottom: 18px; border-bottom: 1px solid var(--paper-3); }
.gate-head h3 {
  font-family: var(--display); font-size: 25px; font-weight: 600;
  letter-spacing: -.02em; text-transform: none; color: var(--ink);
  margin-bottom: 7px; font-variation-settings: 'SOFT' 40, 'WONK' 1;
}
.gate-head p { color: var(--ink-2); font-size: 15.5px; max-width: 60ch; line-height: 1.55; }
.gate-actions { display: flex; gap: 12px; margin-top: 24px; flex-wrap: wrap; align-items: center; }
.gate-actions .primary { margin-left: auto; }

.cast { display: grid; grid-template-columns: repeat(auto-fill, minmax(258px, 1fr)); gap: 15px; }
.cast figure {
  border: 1px solid var(--paper-3); border-radius: var(--r);
  overflow: hidden; background: #FFFDF6;
}
.cast img { width: 100%; display: block; background: #FFFDF6; }
.cast figcaption { padding: 13px; border-top: 1px solid var(--paper-2); }
.cast .who { font-family: var(--display); font-weight: 600; font-size: 17px; color: var(--ink);
             font-variation-settings: 'SOFT' 40, 'WONK' 1; }
.cast .role {
  font-family: var(--ui); font-size: 11px; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .1em; margin-top: 1px;
}
/* A COLUMN, not a row. It was a row when the card held one text box and one
   button; it now holds a note, an upload control, a button and a line of status,
   and as a row those four were squeezed into narrow vertical strips with the
   status text wrapping over eight lines. */
.cast .redraw { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.cast .redraw input { font-size: 13px; padding: 8px 10px; width: 100%; }
.cast figure.busy { opacity: .45; pointer-events: none; }

.beats { list-style: none; counter-reset: b; display: flex; flex-direction: column; gap: 9px; }
.beats li {
  display: grid; grid-template-columns: 28px 1fr; gap: 13px; align-items: start;
  counter-increment: b;
}
.beats li::before {
  content: counter(b); font-family: var(--ui); font-size: 11px; font-weight: 700;
  color: var(--ink-3); background: var(--paper-2); border-radius: var(--r-sm);
  padding: 8px 0; text-align: center;
}
.beats .slot {
  font-family: var(--ui); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 4px;
}
/* Beats are book text, so they are set in the book's reading face. */
.beats input { font-family: var(--book); font-size: 15px; }
.beats input.edited {
  border-color: var(--lamp); background: rgba(246,192,90,.09);
  box-shadow: inset 2px 0 0 var(--lamp);
}
.note-field { margin-top: 22px; }

/* ---------- ready ---------- */

.done-mark {
  font-size: 40px; color: var(--lamp); margin-bottom: 14px;
  text-shadow: 0 0 24px var(--lamp-glow);
}

/*
 * The code is the only credential and there is no email to fall back on, so it
 * is typeset like something worth writing down — a ticket, not a toast.
 */
.code-block {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  background: linear-gradient(180deg, #FFFCF2, var(--paper));
  border-radius: var(--r-lg); padding: 24px 26px; margin: 28px 0 16px;
  box-shadow: var(--lift); position: relative;
}
.code-block::after {
  content: ''; position: absolute; inset: 6px; border-radius: var(--r);
  border: 1.5px dashed rgba(246,192,90,.55); pointer-events: none;
}
.code-label {
  font-family: var(--ui); font-size: 10.5px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink-3);
}
#download-code {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 31px;
  letter-spacing: .13em; color: var(--ink); font-weight: 600;
}
.retention {
  font-family: var(--ui); font-size: 13.5px; color: var(--on-night-2);
  margin-bottom: 28px; line-height: 1.55;
}

.full { display: block; }
/* The name says "full" and inside a two-column grid it was one column wide, which is
   why the description of an imagined creature — the box the whole drawing is built
   from — came out the same size as "Or in cm, if you happen to know". */
.grid.two > .full { grid-column: 1 / -1; }

/*
 * Present to a screen reader, absent to everyone else.
 *
 * Used by #rows-live, which announces a removed row. `display: none` and
 * `visibility: hidden` both take an element out of the accessibility tree, so a live
 * region hidden either of those ways announces nothing at all.
 */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

/*
 * A repeated row: one grown-up, one pet, one imagined character.
 *
 * The rule and the heading are not decoration. Until this round these sections could
 * only ever grow, so the boundary between the second grandparent and the third was
 * invisible and did not matter much; with a Remove button on each one it decides which
 * person a customer is about to delete. Same reasoning, and the same rule, as
 * .person-photos on the upload screen.
 */
.person-row + .person-row {
  margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--paper-3);
}
.row-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
}
.row-title {
  font-family: var(--ui); font-size: 12px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3);
  margin: 0;
}
/* .ghost.small keeps the small type; min-height on .ghost keeps the 44px floor. The
   colour is the error ink rather than the body ink because this is the one control in
   the card that destroys what the customer typed. */
.row-remove { color: var(--error); border-color: var(--paper-3); flex: 0 0 auto; }
.row-remove:hover { color: var(--error); border-color: var(--error); background: rgba(0,0,0,.03); }

/* rows="3" gives it a sensible opening size; this stops it collapsing below the point
   where a customer can see the whole description they have written. */
textarea { min-height: 92px; resize: vertical; line-height: 1.5; }


/* ------------------------------------------------------------------ shelf
   A returning customer's own books. Each card has two very different jobs on
   it — fetch the finished PDF, or start a new book from the same answers — and
   they must not look alike: one is finished business, the other is the reason
   the shelf exists. So the reorder action is the printed button and the
   download is a quiet link beside it, and an expired book loses the link
   entirely rather than offering a 404. */
.shelf { display: grid; gap: 14px; margin-top: 26px; }

.shelf-card {
  display: grid; grid-template-columns: 1fr auto; gap: 18px 22px; align-items: center;
  background: var(--paper); color: var(--ink);
  border-radius: var(--r-lg); box-shadow: var(--lift); padding: 20px 22px;
}
@media (max-width: 620px) { .shelf-card { grid-template-columns: 1fr; } }

.shelf-card .title {
  font-family: var(--display); font-weight: 500; font-size: 20px; line-height: 1.2;
  letter-spacing: -.005em;
}
.shelf-card .meta {
  margin-top: 5px; font-size: 14px; color: var(--ink-2);
  display: flex; flex-wrap: wrap; gap: 6px 14px;
}
.shelf-card .who { display: flex; gap: 6px; margin-top: 11px; flex-wrap: wrap; }
.shelf-card .who img,
.shelf-card .who span {
  width: 34px; height: 34px; border-radius: 50%; object-fit: cover;
  background: var(--paper-2); border: 1.5px solid #fff;
  box-shadow: 0 1px 3px rgba(26, 33, 51, .18);
  display: grid; place-items: center;
  font-family: var(--display); font-size: 14px; color: var(--ink-2);
}
.shelf-card .card-actions {
  display: flex; flex-direction: column; gap: 9px; align-items: stretch;
}
/* flex-wrap for the same reason .actions needed it: as a row at 390px this holds
   "Make another book", "Download (2 days left)" and "Remove" in a 306px box, and
   without wrapping the primary button collapsed to three stacked words. */
@media (max-width: 620px) {
  .shelf-card .card-actions { flex-direction: row; flex-wrap: wrap; align-items: center; }
}
.shelf-card .gone { font-size: 13.5px; color: var(--ink-3); max-width: 26ch; }

/* A text-weight button. Real <button> for the keyboard and the screen reader,
   because a div that looks like a link is neither. */
.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: var(--link); text-decoration: underline;
  text-underline-offset: 2px; position: relative;
}
.linkish:hover { color: var(--link-hover); }
/*
 * .linkish appears inline inside sentences — mid-paragraph in the shelf's privacy
 * note, between middots in the footer, inside the reuse banner — so it was
 * padding: 0 and about 19px tall, and it cannot simply be padded to 44px without
 * pushing the line apart. The target is grown by an overlay instead: absolutely
 * positioned, centred on the line box, 44px tall, invisible, and outside layout
 * entirely, so the visual line is exactly what it was. Only 4px is added
 * sideways, which keeps the two footer links from swallowing each other's edges.
 */
.linkish::after {
  content: ''; position: absolute;
  top: 50%; left: -4px; right: -4px; height: 44px;
  transform: translateY(-50%);
}
/* In the shelf card this one is a block-level item in a column with room around
   it, so it takes the height honestly and the overlay above becomes a no-op —
   which matters, because a 44px overlay there would have reached 3px into the
   Download button stacked above it. */
.shelf-card .card-actions .linkish { min-height: 44px; }
/* The footer is deliberately quiet, so its links match its own text rather than
   taking the gold: 8.93:1 on the ground down there, 11.02:1 on hover. */
.foot .linkish, .foot a { color: var(--on-night-2); }
.foot .linkish:hover, .foot a:hover { color: var(--lamp); }
/*
 * The footer's first line is a two-item nav, not a sentence, so it gets honest
 * height instead of an overlay — and it has to. Measured by hit-testing: on 20px
 * footer lines the 44px overlay reached 12px down into the line below and
 * intercepted the top third of the "photos deleted when you approve your
 * characters" link, so tapping that link fired "Your books" instead. As a flex
 * row the two buttons are 44px tall for real and nothing overlaps anything.
 */
.foot p:first-child {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0 7px;
}
.foot p:first-child .linkish { min-height: 44px; }
.foot p:first-child .linkish::after { content: none; }

/* The banner that says a form was filled in from a previous book. Without it,
   a prefilled form looks like a bug — the customer does not remember telling
   us any of this. */
.reused {
  display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap;
  background: rgba(134, 176, 138, .12); border-left: 3px solid var(--sage);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: 13px 16px; margin-bottom: 22px; font-size: 15px; color: var(--on-night);
}
.reused b { color: var(--on-night); }


/* ------------------------------------------------- character refinement
   Two changes per character. The controls sit under each drawing rather than
   in one shared row, because a note applies to one face and a shared box
   invites the customer to describe three at once. */
.cast figcaption .redraw-row {
  display: flex; gap: 8px; align-items: center; margin-top: 8px;
}
/* min-height for the 44px floor — this was ~30px, and it is the control for
   adding a clearer photo of a face that came out wrong. */
.cast .addphoto {
  flex: 1 1 auto; text-align: center; cursor: pointer;
  border: 1.5px dashed var(--paper-3); border-radius: var(--r-sm);
  padding: 8px 10px; font: 600 12.5px/1.2 var(--ui); color: var(--ink-2);
  background: var(--paper-2);
  display: flex; align-items: center; justify-content: center;
  min-height: 44px; position: relative;
}
/*
 * The file input inside this label carries the `hidden` attribute in app.js, and
 * a hidden input is not focusable — so "Add a photo" could not be reached by
 * keyboard at all, on either gate. Overriding `hidden` from CSS is unusual enough
 * to be worth saying out loud: it is here because the alternative is a control
 * only a mouse can operate. The selector deliberately does not require [hidden],
 * so it keeps working if app.js drops the attribute (which is the better fix).
 */
/*
 * A file input that is off-screen rather than display:none — in both places.
 *
 * `hidden` is `display:none`, and a display:none input cannot take focus, so the
 * control was unreachable by keyboard entirely. There is a second reason to prefer
 * this technique over `hidden`, and it is the older one: some versions of iOS Safari
 * refuse to open the picker for a display:none input at all, so on those phones the
 * only way to add a photograph did not work by any means, mouse included. The
 * prototype used the off-screen form for exactly that reason.
 *
 * It lives in CSS rather than on the element because the page runs under
 * `default-src 'self'` with no inline styles, so a `style` attribute would be blocked.
 */
.cast .addphoto input[type="file"],
.dropzone input[type="file"] {
  display: block; position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}

/*
 * Per-person photo boxes.
 *
 * The upload screen used to be one anonymous pool under copy that said "we need to be
 * able to tell who is who" and then never asked — so the extraction had to label every
 * photograph with which face to draw, and a family of five was hoping the model
 * guessed. Each person gets their own box now, which is only useful if it reads as a
 * box: without a rule these render as unspaced blocks and the boundary between one
 * person and the next disappears.
 */
.person-photos { display: grid; gap: 10px; }
.person-photos > * { margin: 0; }
.person-photos + .person-photos {
  margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--paper-3);
}
.person-photos .thumbs { margin-top: 2px; }
.cast .addphoto:focus-within { outline: 2.5px solid var(--focus); outline-offset: 2px; }
.cast .addphoto:hover { border-color: var(--lamp); background: #fff; color: var(--ink); }
.cast .addphoto.spent { opacity: .5; cursor: not-allowed; }
.cast figcaption .left {
  margin-top: 7px; font-size: 12.5px; line-height: 1.4; color: var(--ink-3);
}
.cast figure.busy { opacity: .55; pointer-events: none; }


/* --------------------------------------------------- reduced motion
   Three of these already sit next to their own @keyframes, where they read
   better: scroll-behavior, .screen.active and the rail sweep. What is collected
   here is the movement that is spread across components and was missed.

   The two that matter most on this app: the rail sweep runs for the whole
   ~7-minute build, and `rise` fires on every screen change and every gate — so
   somebody who asked their phone for no motion got a moving page for seven
   minutes and a slide-up at each of the two moments they are being asked to
   make a decision. Nothing here removes a state: hover and selection still
   change colour, border and shadow, only the travel goes. */
@media (prefers-reduced-motion: reduce) {
  .gate { animation: none; }
  .feed p { animation: none; }
  .preview-grid img { animation: none; }
  .primary, .primary:hover, .primary:active,
  .outline-card, .outline-card:hover, .outline-card.sel,
  .dropzone, .dropzone:hover, .dropzone.over {
    transform: none; transition: none;
  }
  .depth span { transition: none; }
}


/* --------------------------------------------------- print
   The ready screen is the one page here a customer plausibly prints, because the
   download code is the only route back to the book and there is no email to fall
   back on. Printed as designed it is a dark room — a full cartridge of ink — with
   the code itself set in gold on near-white. So print gets ink on white, drops
   the chrome and the buttons, and keeps the three things worth having on paper:
   the code, the date it expires, and what happens to the photographs. */
@media print {
  body {
    background: #fff; background-image: none; color: #000;
    font-size: 12pt;
  }
  .topbar { border-bottom: 1px solid #999; }
  .wordmark { color: #000; }
  .topbar nav, .foot, .steps, .rail, .feed, .preview-grid,
  .actions, .depth, .done-mark, .code-block .ghost { display: none; }
  .wrap { max-width: none; padding: 0; }
  .screen.active { animation: none; }
  h1, h2, .card, .gate, .shelf-card, #download-code { color: #000; }
  .sub, .lede, .retention, .code-label, .hint, h3 { color: #333; }
  .card, .gate, .code-block, .shelf-card {
    background: #fff; box-shadow: none; border: 1px solid #999;
  }
  .card::after, .outline-card::after, .code-block::after, .hero::before { display: none; }
  .privacy-note { background: none; border: 1px solid #999; color: #333; }
  .privacy-note strong { color: #000; }
}
