/* Figtree, self-hosted. Serving it from Google would put a request to a third
   party on every page load, for 61KB of font we can hold ourselves. One
   variable file per subset covers every weight from 300 to 900, so the whole
   family costs less than a photograph. Licensed under the OFL - see
   brand/fonts/OFL.txt, which is why self-hosting is ours to do. */
@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url(/static/fonts/figtree-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url(/static/fonts/figtree-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Figtree';
  font-style: italic;
  font-weight: 300 900;
  font-display: swap;
  src: url(/static/fonts/figtree-italic-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Figtree';
  font-style: italic;
  font-weight: 300 900;
  font-display: swap;
  src: url(/static/fonts/figtree-italic-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* The brand palette. Four colours are Daniel's; everything else is derived from
   them, and every derived value is marked. Change a brand colour here and it
   changes everywhere, including the printed booklet - see instructions.py,
   which holds the same four as print colours. */
:root {
  --paper:  #F5F0E7;   /* brand: cream */
  --accent: #820C23;   /* brand: burgundy - the colour that carries the identity */
  --ink:    #171717;   /* brand: charcoal */
  --tan:    #C3A477;   /* brand: kraft, for secondary marks and rules */

  --card:   #FFFFFF;   /* derived: white lifts a panel off the cream */
  --muted:  #6B665C;   /* derived: charcoal warmed and lightened, ~5:1 on cream */
  --line:   #E2D9C8;   /* derived: kraft mixed back into the cream */
  --warn:   #8A6A3C;   /* derived: kraft darkened until it reads as caution */
  --alarm:  #B4402F;   /* derived: deliberately NOT the brand burgundy. If errors
                          were burgundy, every button would look like an error */
  --on-accent: #F5F0E7; /* derived: what text sits on a FILLED accent surface.
                          Its own token rather than `--paper`, because those two
                          agree in light mode and are opposites in dark, and a
                          button reading `color: var(--paper)` is quietly asking
                          for the page colour when it wants the opposite of its
                          own background. That mistake shipped: when dark mode's
                          accent went from pale rose to burgundy on 2026-09-11,
                          every filled button became charcoal-on-burgundy at
                          1.73:1 and nobody could read a word. Whenever --accent
                          changes, check this. scripts/contrast.py measures it */

  --measure: 1080px;   /* the page */
  --narrow: 640px;     /* anything that is mostly words */
  --gutter: clamp(1.25rem, 5vw, 3rem);
  --rhythm: clamp(3rem, 7vw, 5rem);
  --radius: 4px;
  --ease: 180ms cubic-bezier(.4, 0, .2, 1);
}
/* One dark palette, not two. The inline script in _head.html resolves the
   system setting and writes the answer onto <html>, so this block covers both
   "the OS is dark" and "the reader chose dark" - rather than a media query and
   an override that have to be kept identical by hand. */
:root[data-theme="dark"] {
  --paper:  #171717;   /* brand charcoal, now carrying the page */
  --ink:    #F5F0E7;   /* brand cream, now carrying the text */
  --tan:    #C3A477;
  --accent: #820C23;   /* BRAND BURGUNDY, the same value light mode uses.
                          Daniel asked for this on 2026-09-11 to see it.
                          Know what it costs: burgundy on charcoal is 1.73:1,
                          where 4.5:1 is the floor for text and 3:1 for the edge
                          of a control. It is fine as a *fill* under cream text
                          (9.13:1, the page behind it is irrelevant) and close
                          to invisible as text, a thin border, or a progress
                          bar. The previous value was #D85A72 - same hue, 349
                          against 348, lifted to 60% lightness to clear 4.8:1 -
                          and it is one line away if this is not the look.
                          scripts/contrast.py prints all of it. */
  --card:   #201F1D;
  --muted:  #A39B8C;
  --line:   #322F2A;
  --warn:   #C3A477;
  --alarm:  #D66651;
  --on-accent: #F5F0E7; /* cream, exactly as in light mode - because the accent
                          is now the same burgundy. Were the accent ever put
                          back to a pale rose, this becomes #171717 again. */
}

/* Dark mode stops using the accent as a FOREGROUND colour.
   Burgundy is a fine fill - 9.13:1 under --on-accent - and hopeless as text or
   as a hairline on this page: 1.59:1 for a label on a card, 1.73:1 for a
   border. So anything unfilled hands its text to the page's own ink and its
   border to --muted, the lightest thing in the palette that is not the text.
   Light mode is untouched; there, burgundy on cream is 9.13:1 and carries the
   identity, which is the whole reason it is the brand colour.
   Kept in one block on purpose: every difference the dark theme makes is here
   and in the palette above it, rather than scattered through the file as
   per-component overrides that drift apart. */
:root[data-theme="dark"] .btn.second,
:root[data-theme="dark"] .autobtn:not(.on),
:root[data-theme="dark"] .voicepick .linkish,
:root[data-theme="dark"] .helpline a,
:root[data-theme="dark"] table a,
:root[data-theme="dark"] .pdf:hover,
:root[data-theme="dark"] .journey-back:hover,
:root[data-theme="dark"] .ghost:hover,
:root[data-theme="dark"] .themetoggle:hover,
:root[data-theme="dark"] .panel button[type=submit]:hover:not(:disabled) {
  color: var(--ink);
}
:root[data-theme="dark"] .btn.second,
:root[data-theme="dark"] .autobtn:not(.on),
:root[data-theme="dark"] .pdf:hover,
:root[data-theme="dark"] .journey-back:hover,
:root[data-theme="dark"] .ghost:hover,
:root[data-theme="dark"] .themetoggle:hover,
:root[data-theme="dark"] .panel button[type=submit]:hover:not(:disabled),
/* not a button, the same defect: this is how a chosen photo says it is chosen,
   and an invisible outline says nothing. */
:root[data-theme="dark"] .film .on img {
  border-color: var(--muted);
}

* { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--paper); color: var(--ink);
  font: 17px/1.6 'Figtree', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
button, input, select { font: inherit; }
.wrap { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); }
.narrow { max-width: var(--narrow); margin: 0 auto; }
/* A utility, so it has to beat whatever display the element would otherwise
   have - `.frame-grid` is a grid, and a later rule with equal specificity was
   quietly winning. */
.hidden { display: none !important; }
.centred { text-align: center; }
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ---- type ---- */
h1, .display {
  margin: 0; font-size: clamp(2rem, 4.6vw, 3rem);
  font-weight: 500; line-height: 1.08; letter-spacing: -0.03em;
}
h2, .h2 { margin: 0; font-size: clamp(1.4rem, 2.6vw, 1.9rem); font-weight: 500; letter-spacing: -0.02em; }
h3, .h3 { margin: 0; font-size: 1.05rem; font-weight: 600; }
.eyebrow {
  margin: 0 0 1.1rem; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .22em; color: var(--muted);
}
.lead { margin: 1.2rem 0 0; font-size: 1.1rem; line-height: 1.55; color: var(--muted); }
.sub, .hint, .note { color: var(--muted); margin: .5rem 0 0; font-size: .92rem; }
.hairline { height: 1px; background: var(--line); border: 0; margin: 0; }

/* ---- the logo -------------------------------------------------------
   `scripts/brand_assets.py` cuts the supplied JPEG out into a transparent
   PNG and writes it in the two colours it is ever drawn in. Two files and
   an `<img>` rather than one file and a CSS mask: a mask a browser
   declines to apply leaves nothing on the page at all. */
.logo { display: block; width: var(--wordmark, 190px); height: auto; }
.logo-paper { display: none; }
:root[data-theme="dark"] .logo-ink { display: none; }
:root[data-theme="dark"] .logo-paper { display: block; }
.disc { width: 26px; height: 26px; flex: none; }

/* ---- the bar at the top of every page ---- */
.topbar { display: flex; align-items: center; gap: 1.2rem; padding: 1.3rem 0; }
.mark { display: flex; align-items: center; gap: .75rem; text-decoration: none; }
.mark .logo { --wordmark: 170px; }
.topbar nav {
  margin-left: auto; display: flex; align-items: center; gap: 1.4rem;
  flex-wrap: wrap; justify-content: flex-end;
  font-size: .88rem; color: var(--muted);
}
.topbar nav a { color: var(--muted); text-decoration: none; }
/* Hover, on a device that cannot hover.
 *
 * A phone has no pointer that arrives and leaves: tapping synthesises a hover
 * on what you tapped, and the back/forward cache then restores the page with
 * that state intact. Confirmed on a handset 2026-09-23 - tap *Browse
 * ready-made designs*, press Back, and it comes back solid where it should be
 * outlined, while returning by an in-page link (a fresh page) looks right.
 * PRD §6 issue 11.
 *
 * `@media (hover: hover)` is true only where a pointer can really hover, so on
 * a phone these never apply and there is no state left to stick. Each is
 * wrapped where it stands rather than gathered into one block at the end,
 * because source order decides between rules of equal specificity and these sit
 * with the components they belong to.
 *
 * `tests/test_templates.py` fails on an unguarded one. Two things are
 * deliberately left alone: the `[data-theme="dark"]` palette rules, which only
 * recolour a state something else created and are simply dead when hover never
 * matches; and `:focus-visible`, which a keyboard user needs on a device with
 * no pointer at all - which is why `.pick` below is two rules and not one
 * selector list. */
@media (hover: hover) { .topbar nav a:hover { color: var(--ink); } }
.topbar code { color: var(--ink); font-weight: 600; letter-spacing: .08em; font-size: .85rem; }
.topbar form { display: inline; }
.topbar form button {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: .88rem; text-decoration: underline;
}

/* The theme control names its state - Light, Dark or System - rather than
   showing an icon whose meaning depends on whether it means "you are here" or
   "go here", which is the oldest unresolved argument in interface design. */
.themepick { color: var(--muted); }
.ghost, .themetoggle {
  border: 1px solid var(--line); border-radius: var(--radius);
  background: none; color: var(--muted);
  padding: .25rem .6rem; margin-left: .35rem; font-size: .78rem; cursor: pointer;
  transition: border-color var(--ease), color var(--ease);
}
@media (hover: hover) { .ghost:hover, .themetoggle:hover { border-color: var(--accent); color: var(--accent); } }

/* ---- buttons ---- */
.btn, .filebtn span, button.primary {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 3.2rem;
  border: 1px solid var(--accent); border-radius: var(--radius);
  background: var(--accent); color: var(--on-accent);
  padding: .6rem 1.7rem; font-size: 1rem; font-weight: 600;
  text-decoration: none; cursor: pointer;
  transition: transform var(--ease), background var(--ease), color var(--ease);
}
@media (hover: hover) { .btn:hover, .filebtn span:hover, button.primary:hover:not(:disabled) { transform: translateY(-1px); } }
/* The one irreversible decision on the site, asked in our own voice. */
.ask {
  max-width: 27rem; width: calc(100% - 2.5rem); padding: 1.9rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--card); color: var(--ink);
}
.ask::backdrop { background: rgba(23, 23, 23, .55); }
.ask h2 { margin: 0 0 .8rem; font-size: 1.25rem; line-height: 1.25; }
.ask p { margin: 0 0 .7rem; line-height: 1.55; }
.ask-row { display: flex; gap: .7rem; margin-top: 1.5rem; }
/* Confirm is the wider of the two and the only filled one: leaving should be
   easy to find and never easier to hit by accident than staying. */
.ask-row button {
  flex: 1; padding: .9rem; border-radius: var(--radius);
  border: 1px solid var(--line); background: none; color: var(--muted);
  font: inherit; font-size: .95rem; cursor: pointer;
}
.ask-row button.primary {
  flex: 1.6; border: 0; background: var(--accent); color: var(--on-accent);
  font-weight: 600;
}
.btn.second { background: transparent; color: var(--accent); }
@media (hover: hover) { .btn.second:hover { background: var(--accent); color: var(--on-accent); } }
.btn.wide { width: 100%; }
.btn.hero-btn { min-width: 12.5rem; min-height: 3.4rem; font-size: 1.1rem; font-weight: 700; }
/* The decision a page exists for, set a step above the way out of it sitting
   alongside. Size only, and the same 1.1rem step `.hero-btn` already uses: the
   fill against the outline carries most of the difference already, and a
   heavier weight on top of it read as shouting. */
.btn.decide { font-size: 1.1rem; }
button.primary:disabled { opacity: .5; cursor: default; transform: none; }
/* Two entry points side by side, wrapping to a column on a phone rather than
   letting the second one run off the edge. The margin moves from `.filebtn` to
   the row that now holds it, or the two buttons sit on different baselines. */
.choices {
  display: flex; flex-wrap: wrap; gap: .8rem;
  align-items: center; margin-top: 1.6rem;
}
.choices .filebtn { margin-top: 0; }
.filebtn { display: inline-block; margin-top: 1.6rem; }
.filename { color: var(--muted); font-size: .88rem; margin: .8rem 0 0; }
.back { color: var(--muted); font-size: .9rem; text-decoration: none; }
/* Sized and coloured like the footer beneath it, deliberately: deleting a kit
   should read as an option on the page, not as the thing the page is for. The
   friction is the dialog it opens, not how loudly this button asks for it. */
.linklike {
  display: inline-block; margin: 2rem 0 0;
  background: none; border: 0; padding: 0;
  font: inherit; font-size: .82rem; color: var(--muted); cursor: pointer;
}
@media (hover: hover) { .linklike:hover { color: var(--accent); } }

/* ---- the reveal: drag to compare the photo with the thread ----------
   One source of truth (--split) driven three ways: the pointer from
   anywhere inside the circle, a range input for the keyboard, and one slow
   sweep the first time it is seen - see static/reveal.js. */
.reveal {
  --split: 55;
  position: relative; aspect-ratio: 1; width: 100%;
  border-radius: 50%; overflow: hidden;
  touch-action: none;                /* the drag is ours, not the page's */
  cursor: ew-resize; user-select: none;
  background: #fff;                  /* a white board, in either theme */
  border: 1px solid var(--line);
}
.reveal img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; pointer-events: none;
}
.reveal-art { position: absolute; inset: 0; clip-path: inset(0 0 0 calc(var(--split) * 1%)); }
.reveal-divider {
  position: absolute; top: 0; bottom: 0; left: calc(var(--split) * 1%);
  width: 2px; transform: translateX(-1px);
  background: #fff; box-shadow: 0 0 0 1px rgba(0, 0, 0, .12);
  pointer-events: none;
}
.reveal-handle {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 3rem; height: 3rem; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--ink); color: var(--paper);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .28); font-size: .9rem;
}
.reveal.is-dragging .reveal-handle { transform: translate(-50%, -50%) scale(.94); }
.reveal.is-focused .reveal-handle { outline: 3px solid var(--accent); outline-offset: 3px; }
.reveal-range { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
.caption {
  margin: 1rem 0 0; text-align: center;
  font-size: .78rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}

/* ---- the front door -------------------------------------------------
   Nine visitors in ten already have a kit, so this is not a shop window.
   A logo, the thing the product makes, and two ways on. */
.hero { padding: clamp(1rem, 3vw, 2rem) 0 var(--rhythm); text-align: center; }
.hero .logo { --wordmark: min(430px, 84vw); margin: 0 auto; }
.hero-art { width: min(460px, 80vw); margin: clamp(1.2rem, 3vw, 2rem) auto 0; }
.hero-line {
  margin: 1.4rem auto 0; max-width: 46ch;
  font-size: clamp(1.15rem, 2.2vw, 1.45rem); line-height: 1.35;
  letter-spacing: -0.02em; color: var(--muted);
}
.cta { display: flex; flex-wrap: wrap; gap: .9rem; justify-content: center; margin-top: 1.8rem; }
.hero .note { margin-top: 1.4rem; }
.beats {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 2.5rem;
  padding: var(--rhythm) 0; border-top: 1px solid var(--line);
}
.beats .step-no {
  font-size: .72rem; font-weight: 700; letter-spacing: .22em;
  text-transform: uppercase; color: var(--tan); margin-bottom: .7rem;
}
.beats p { margin: .35rem 0 0; color: var(--muted); font-size: .93rem; }
.helpline {
  display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: baseline;
  padding: 1.6rem 0; border-top: 1px solid var(--line);
  font-size: .93rem; color: var(--muted);
}
.helpline a { color: var(--accent); text-decoration: none; font-weight: 600; }
/* A helpline whose answer is a button rather than a link. `baseline` above is
   right for two lines of text, but a 3.2rem button shares only its text
   baseline with the sentence beside it and hangs below the row. */
.helpline.action { align-items: center; }

footer {
  display: flex; flex-wrap: wrap; gap: 1rem 1.8rem; align-items: center;
  padding: 2rem 0 3.5rem; border-top: 1px solid var(--line);
  font-size: .82rem; color: var(--muted);
}
footer a { text-decoration: none; }
.footer-mark { --wordmark: 190px; opacity: .75; }
/* One flex item, not three. A wrapping row cannot be told "keep these two
   together", so the links are their own row that either sits beside the
   wordmark or drops beneath it whole - and `.spacer` then pushes Theme to the
   right edge of that row rather than to the right edge of a stray line.
   No media query: the break happens when the words stop fitting, which is the
   only width that matters. */
.footer-links {
  flex: 1 1 auto;
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.8rem;
}
.spacer { margin-left: auto; }

/* ---- the journey, on every signed-in page ---- */
.journey {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1.6rem;
  padding: 1.2rem 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  font-size: .74rem; text-transform: uppercase; letter-spacing: .16em; color: var(--muted);
}
/* The four steps as one object. `nowrap` is the whole point: they are a
   sequence, and a sequence broken across two rows stops reading as one. */
.journey .steps { display: flex; flex-wrap: nowrap; align-items: center; gap: 1.6rem; }
.journey span { display: flex; align-items: center; gap: .5rem; white-space: nowrap; }
/* The way out, living in the bar that already carries the way forward. Above
   the heading it was a stack of its own in the top-left corner, which is a
   third column the page does not have. `margin-right: auto` is what pushes the
   four steps across to make room. Sentence case and no tracking, so it reads as
   a control rather than as a fifth step in the sequence beside it. */
.journey-back {
  margin-right: auto; align-self: center; padding: .3rem .7rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--ink); text-decoration: none;
  font-size: .82rem; font-weight: 600;
  text-transform: none; letter-spacing: normal;
}
@media (hover: hover) { .journey-back:hover { border-color: var(--accent); color: var(--accent); } }
.journey i {
  font-style: normal; width: 1.4rem; height: 1.4rem; border-radius: 50%;
  display: grid; place-items: center; border: 1px solid var(--line); font-size: .68rem;
}
.journey .now { color: var(--ink); }
.journey .now i { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.journey .done i { border-color: var(--tan); color: var(--tan); }

/* ---- forms ---- */
.field { display: block; margin-bottom: 1.1rem; }
.field span {
  display: block; margin-bottom: .4rem;
  font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .16em; color: var(--muted);
}
.field input {
  width: 100%; padding: .9rem 1rem;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  font: inherit; font-size: 1.05rem;
}
.field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
#code { text-transform: uppercase; letter-spacing: .18em; font-weight: 600; }
select {
  padding: .6rem; border-radius: var(--radius); font-size: .95rem;
  border: 1px solid var(--line); background: var(--card); color: var(--ink);
}
input[type=range] { width: 100%; margin: 1.2rem 0; accent-color: var(--accent); }

.row { display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap; }
.row label { display: flex; flex-direction: column; gap: .3rem; font-size: .85rem; color: var(--muted); }

/* ---- choosing and framing a photo ---- */
.page-head { padding: var(--rhythm) 0 2rem; }
/* A page reached from a list carries a way back at the top, and then the
   heading does not need the full rhythm above it as well - the two together
   left a hand's width of nothing between the link and the title. */
/* "Design 1" is a slot number rather than a title - it says which of three
   this is and nothing more - so it does not need the full display size the
   pages that ask something of a customer use. */
h1.slot { font-size: clamp(1.7rem, 3.4vw, 2.3rem); }
/* Guidance before the photo is chosen, and findings after it. Both are advice,
   styled as reading matter rather than as an alert somebody has to dismiss. */
ul.advice, .tips { list-style: none; margin: 1.8rem 0 0; padding: 0; max-width: 62ch; }
ul.advice li, .tips li {
  padding: .9rem 0; border-top: 1px solid var(--line);
  color: var(--muted); font-size: .95rem;
}
ul.advice strong, .tips b { color: var(--ink); font-weight: 600; }
.reassure { margin: 1.1rem 0 0; font-size: .87rem; color: var(--muted); }
#notes p { margin: .5rem 0 0; font-size: .92rem; color: var(--muted); }
.detected { font-weight: 600; color: var(--ink); margin: 0 0 .8rem; }

.frame-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 4rem); align-items: start; padding-bottom: var(--rhythm);
}
.cropwrap { width: 100%; max-width: 440px; }
canvas#crop {
  width: 100%; height: auto; touch-action: none; cursor: grab;
  border-radius: 50%; border: 1px solid var(--line); background: #fff;
}
canvas#crop:active { cursor: grabbing; }

/* Kraft track, burgundy fill - the two secondary brand colours doing the one
   job they are obviously for. */
.bar { height: 4px; background: var(--line); border-radius: 99px; overflow: hidden; margin: 1rem 0 .6rem; }
.fill { height: 100%; width: 0; background: var(--accent); transition: width .25s ease; }
.stage { font-size: .9rem; color: var(--muted); margin: 0; }

/* ---- the five designs ---- */
.chosen { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
/* /pattern, where the left column carries the heading as well as the artwork.
   Centred, the right column did not begin until the progress section and the
   whole page hung off its top-left corner. `start` gives the two columns one
   top edge, so "Started ..." sits level with "Design 1". */
.chosen.top { align-items: start; padding: var(--rhythm) 0; }
.chosen.top .artwork { margin-top: 1.3rem; }
/* First thing in its column, so it carries no inherited gap above it. */
.chosen.top .started { margin: 0 0 1rem; }
/* Same circular treatment as .design-card img: once a design is committed
   there is nothing left to compare it against, only the artwork to show. */
.chosen .artwork {
  width: 100%; aspect-ratio: 1; border-radius: 50%;
  background: #fff; border: 1px solid var(--tan);
}
.spec { list-style: none; margin: 1.6rem 0 2rem; padding: 0; }
.spec li {
  display: flex; justify-content: space-between; gap: 2rem;
  padding: .8rem 0; border-bottom: 1px solid var(--line); font-size: .92rem;
}
.spec li b { font-weight: 600; font-variant-numeric: tabular-nums; }
.spec li span { color: var(--muted); }
.film { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1.1rem; padding: var(--rhythm) 0 0; }
/* Inside `.chosen` it is a third grid item rather than a section of its own, so
   that a phone can put it between the preview and the details. Spanning both
   columns keeps the two-column desktop layout exactly as it was; the row gap
   replaces the padding it used to carry. */
.chosen .film { grid-column: 1 / -1; padding: 0; }
/* Centred, because the thing the label names is a circle. Ranged left, each
   label started at the corner of a bounding box the artwork never reaches, so
   the text sat visibly off to one side of the disc it belonged to. */
.film button { background: none; border: 0; padding: 0; cursor: pointer; text-align: center; color: inherit; }
.film img {
  width: 100%; border-radius: 50%; border: 1px solid var(--line);
  background: #fff; transition: border-color var(--ease);
}
@media (hover: hover) { .film button:hover img { border-color: var(--tan); } }
.film .on img { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.film b { display: block; margin-top: .7rem; font-size: .85rem; font-weight: 600; }
.film small { display: block; color: var(--muted); font-size: .78rem; }
.blurb { display: block; color: var(--muted); }
.ident { display: block; margin-top: .55rem; color: var(--muted); font-size: .72rem; letter-spacing: .04em; }
.warn { font-size: .85rem; color: var(--warn); margin: .7rem 0 0; }

/* Something went wrong before the page was even asked for - an expired sign-in
   link, most of the time. Distinct from `.warn`, which is a note beside a field
   somebody is filling in: this is the reason the page opened, so it goes above
   the heading and takes `--alarm` rather than the kraft caution colour.
   `--alarm` exists precisely so an error is never the brand burgundy, which
   every button already is. */
.alert {
  margin: 0 0 2.2rem; padding: 1.1rem 1.25rem;
  background: var(--card);
  border: 1px solid var(--line); border-left: 3px solid var(--alarm);
  border-radius: var(--radius);
}
.alert h2 {
  margin: 0 0 .3rem; font-size: 1.02rem; font-weight: 600; color: var(--alarm);
}
.alert p { margin: 0; font-size: .95rem; color: var(--muted); }

/* The same box, for a failure that arrives *after* the page is up: the answer
   to a form the customer has just submitted. It sits with the button rather
   than above the heading, because that is where their eye already is, and it
   carries its own text in --alarm rather than a heading plus a muted line.
   Measured, because this one shipped wrong: it was `.warn`, which is 4.4:1 on
   cream at .85rem - below AA, and *less* legible than the --muted note it sat
   under at 5.03:1. The most important sentence on the page was the faintest
   thing on it. --alarm on a card is 5.65:1 light, 4.61:1 dark. */
.alert.reply {
  margin: 1.2rem 0 0;
  color: var(--alarm);
  font-size: .95rem;
}
.pdf {
  display: inline-block; margin-top: .8rem; padding: .55rem .95rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--ink); font-size: .88rem; font-weight: 600; text-decoration: none;
  background: none; cursor: pointer; font-family: inherit;
}
/* ---- the artworks a kit is building --------------------------------
   `auto-fill` rather than `auto-fit` on purpose: with one design `auto-fit`
   collapses the empty tracks and lets that single card stretch to the full
   measure, which is the poster-sized picture this grid exists to stop. */
.designs {
  display: grid; gap: clamp(1.8rem, 3vw, 2.6rem);
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-auto-rows: auto;
  margin-top: 1rem;
}
/* The page rhythm is built for pages that are read. This one is scanned - three
   artworks, three states, and a decision about whether to make another - and at
   the full rhythm the second block was below the fold on a laptop with nothing
   between the two but air. */
.page-head.tight { padding: 2.4rem 0 1.2rem; }
/* The end of the road: every design made, or not enough string for another.
   Filled and centred at every width, because at this point buying a kit is not
   the lesser of two options - it is the only one left. An outlined button on
   the left read as an afterthought sitting under the sentence explaining why
   there is nothing else to do.
   Centred is also what separates it from `.closing.offer` below, which puts
   its button beside the heading on a laptop: an offer sits next to what it
   offers, an ending is a full stop under the last sentence. */
.endcap { margin-top: 1.6rem; text-align: center; }

/* The branch that *offers* something, rather than closing the page: the
   heading and the button share the first line, and the sentence explaining the
   offer sits under the heading, where a sentence belongs. A button stranded
   below a line of prose, with the right half of the measure empty beside it,
   reads as something left over rather than as the invitation it is.
   Only this branch. The two endings keep `.endcap` above: a full stop is
   centred on purpose, and an ending has nothing to offer alongside it.

   A flex row rather than two grid columns, which is the second attempt. Grid
   with a `1fr auto` pair pushed the button to the far edge of the measure,
   where it was the only thing on the page reaching that far and read as an
   island. The button belongs *beside* its heading. `max-width` ties the pair
   to the same measure as the sentence beneath, so it can never end further
   right than the text it is explained by, and `flex-wrap` drops it to its own
   line rather than breaking that promise. */
.closing.offer .offerhead {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 1rem 1.5rem; max-width: 60ch;
}
.closing.offer .lead { margin-top: 1.1rem; max-width: 60ch; }
/* Each card is a subgrid of six rows - picture, name, status, bar, buttons,
   ident - so every card's third row is the same height as every other card's
   third row, and the bars line up because they are literally in the same row.
 *
 * This replaces an earlier attempt that made the card a flex column and let the
 * status line absorb the slack. That aligns the *bottoms* of the cards, which
 * is the same thing as aligning the bars only while everything below the status
 * is identical in height - and the moment one card's "Chosen 11 September 2026
 * · Pattern B970A8DD" wrapped where another's did not, or one status ran to two
 * lines, the bars drifted apart by exactly that much. Rows are the thing being
 * aligned, so rows are what should be shared.
 *
 * `row-gap: 0` is the point of the override: a subgrid inherits the parent's
 * gaps, and the 2.6rem meant for the space between cards would otherwise open
 * up between every element inside one. Internal rhythm stays on the children's
 * own margins, where it was. */
.design-card {
  min-width: 0;
  display: grid; grid-template-rows: subgrid; grid-row: span 6; row-gap: 0;
}
/* Circular, like the board. The preview is a square PNG with the artwork drawn
   inside a circle on white, so squaring it off framed the picture in a white
   box that is not part of anything. */
.design-card img {
  width: 100%; aspect-ratio: 1; border-radius: 50%;
  background: #fff; border: 1px solid var(--tan);
}
.design-card h2 { margin-top: 1.3rem; }
.design-card .bar { margin: .7rem 0 1.1rem; }
/* Equal halves. One is where you were going and the other is the paper copy;
   sizing them by their label length would rank them by the length of a word. */
/* `align-items: stretch` undoes `.row`'s `flex-end`, which survived the switch
   to grid and left the shorter button hanging off the bottom of the taller one
   the moment a label wrapped. The trimmed padding is what stops it wrapping:
   `.btn` reserves 1.7rem each side, which is generous on a page and more than
   "View design" can spare inside a 145px column. */
.design-card .row.minor {
  display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; align-items: stretch;
}
.design-card .row.minor .btn { width: 100%; padding-left: .8rem; padding-right: .8rem; }
.design-card .ident { margin-top: .8rem; }

/* ---- the ready-made gallery ----------------------------------------------
   Reuses `.designs` for the grid and deliberately *not* `.design-card` for the
   card. That one is a six-row subgrid whose span is pinned to the child count
   of `designs.html`, and a test compares the two; sharing the selector would
   mean this page's anatomy silently decided whether the artwork cards on
   another page line up.

   The whole tile is the button. Picking a design is one tap on the picture,
   which is the only thing on the card anybody wants to tap, and it keeps the
   page to one form with no JavaScript. `all: unset` is avoided on purpose -
   it would take the button's keyboard focus behaviour with it. */
.pick-card { min-width: 0; }
.pick {
  display: grid; gap: .45rem; width: 100%; padding: 0;
  border: 0; background: none; cursor: pointer;
  font: inherit; color: inherit; text-align: center;
}
/* Circular, like the board and like `.design-card img`, for the same reason:
   the preview is a square PNG with the artwork drawn inside a circle on white,
   so squaring it off frames the picture in a white box that is not part of
   anything. */
.pick img {
  width: 100%; aspect-ratio: 1; border-radius: 50%;
  background: #fff; border: 1px solid var(--tan);
}
.pick:focus-visible img { border-color: var(--accent); }
@media (hover: hover) { .pick:hover img { border-color: var(--accent); } }
.pick .h3 { margin-top: .6rem; }

@media (hover: hover) { .pdf:hover { border-color: var(--accent); color: var(--accent); } }
.pdf:disabled { opacity: .45; cursor: default; }
/* Carry on, and the booklet: a filled primary and an outlined secondary of the
   same width, which is what a pair of equal choices looks like. `.pdf` keeps
   its small inline shape everywhere else - the console's Export CSV is the
   same class and must not become a banner. */
.actions { display: grid; gap: .7rem; margin-top: 1.6rem; }
.actions .advance.start, .actions .pdf { margin: 0; width: 100%; }
.actions .pdf { padding: .95rem 1rem; font-size: 1rem; text-align: center; }

/* ---- the guided player ------------------------------------------------
   One screen, the height of the viewport, and nothing below it. Someone
   winding has thread in both hands and the phone propped against the board;
   a page they have to scroll is a page they have to put the thread down for.

   So: the nail, the artwork and what comes next in the middle, the two
   controls they touch while winding on either side of it, and everything else
   behind the two bottom corners. `100dvh` rather than `100vh` because mobile
   Safari's `vh` includes the browser chrome that is covering the bottom of
   the page, which is exactly where the Next step button is. */
.wrap.player {
  max-width: 640px; height: 100dvh;
  display: flex; flex-direction: column; overflow: hidden;
}
/* The gutter is on `.wrap`; `main`'s own would inset this screen twice, and
   every pixel here is spent on the board.

   `width: 100%` and no auto margin, both load-bearing. `main` carries
   `margin: 0 auto` from the rule above, and an auto margin on a flex item
   turns off the stretch it would otherwise get - so this was sized by its own
   contents, and its contents are a board sized as a percentage *of it*. The
   board settled wherever that loop happened to land: 208px on a phone when the
   arithmetic said 226, and 300px on a laptop when it said 361. Give the column
   a width that does not depend on what is inside it. */
.wrap.player main {
  flex: 1 1 auto; min-height: 0; display: flex;
  width: 100%; margin: 0; padding: 0;
}
.playerbar {
  display: flex; align-items: center; gap: 1rem;
  padding: .6rem 0 .5rem; font-size: .82rem; color: var(--muted);
}
/* Both halves of this cell hold words now, which is why there is one rule and
   not a chevron rule with an override under it. A bare "‹" asked the customer
   to guess, and this screen is read from a metre away with thread in both
   hands. The chevron keeps a size of its own: at .78rem it reads as a comma. */
.playerbar .back {
  display: inline-flex; align-items: center; gap: .3rem;
  text-decoration: none; font-size: .78rem; line-height: 1;
  padding: .25rem .55rem .3rem; margin-left: -.55rem;
}
.playerbar .back .chev { font-size: 1.3rem; line-height: .8; }
/* Underlined, because it does something rather than goes somewhere and the
   only thing on this row that says so is the underline. Muted like everything
   else in this bar: it is the least urgent control on the screen. */
.playerbar .tourlink {
  padding: .25rem 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: .78rem; line-height: 1;
  color: var(--muted); text-decoration: underline;
}
@media (hover: hover) { .playerbar .tourlink:hover { color: var(--accent); } }
.playerbar .saved { flex: 1 1 auto; text-align: right; font-size: .72rem; }
.playerbar .saved.warn { color: var(--warn); margin: 0; }

/* The rows: what to do, where you have been, the board, where you are going,
   the count, the transport, the corners, the progress hairline. The two lists are
   the `1fr`s, so a tall phone spends its extra height on them and a short one
   takes it back from them - and the Next step button stays on screen
   either way. */
.screen {
  flex: 1 1 auto; min-height: 0; width: 100%;
  display: grid; grid-template-rows: auto auto 1fr auto 1fr auto auto auto;
}
/* Reserved even when empty, so the board does not jump up the screen at step
   two and back down again at the end. */
.verb {
  margin: 0; min-height: 1.5rem; text-align: center;
  color: var(--muted); font-size: .95rem;
}

/* Four behind and four ahead, fading away from the present. `--d` is the
   distance in steps, so the ramp is one declaration rather than eight
   hand-written opacities, and the far ones clip rather than push when the
   screen is short - the list is bottom-aligned above the board and
   top-aligned below it, so what gets cut is always the furthest away. */
.past, .next {
  display: flex; flex-direction: column; align-items: center;
  min-height: 0; overflow: hidden; font-variant-numeric: tabular-nums;
  color: var(--muted); letter-spacing: .06em;
}
.past { justify-content: flex-end; padding-bottom: .5rem; font-size: .85rem; }
.next { justify-content: flex-start; padding-top: .5rem; font-size: .95rem; }
.past span, .next span { opacity: calc(1 - var(--d) * .19); line-height: 1.5; }
/* The very next nail is the one a hand is already reaching for. */
.next span:first-child { color: var(--ink); font-weight: 600; }

/* A step moves rather than swaps. Both columns are rebuilt from nothing on
   every render, so without this the four labels behind and the four ahead
   vanish and reappear one row along - and at a nail every two seconds for six
   hours, that reads as a flicker rather than as a list moving on.

   One row is `1.5em` because that is the `line-height` these spans are set in,
   and `em` resolves against each column's own font size - so the two columns,
   which are set at different sizes, each slide by their own row without either
   number being written down twice.

   Transform only, and deliberately. The resting opacity of these spans is a
   ramp computed from `--d`, and a keyframe that touched opacity would have to
   know that ramp in order to hand it back. It is also the quieter half of the
   effect: what moves is the column, and nothing blinks.

   Turned off with the rest of the motion by the `prefers-reduced-motion` block
   near the end of this file, which is why the duration is an animation rather
   than a hand-rolled one. */
@keyframes step-along { from { transform: translateY(1.5em); } }
.past.stepped span, .next.stepped span { animation: step-along var(--ease); }

/* The number in the middle has its text replaced rather than being rebuilt, so
   this one is restarted by hand - see `flick()` in player.js. An order of
   magnitude smaller than the columns: it is three rem of type at the centre of
   the screen, where barely visible is plenty. */
@keyframes step-arrive { from { transform: translateY(.1em); opacity: .55; } }
.nail.stepped { animation: step-arrive var(--ease); }

/* The pace, the board and hands-free on one line. The two controls flank the
   artwork because they are the only two things touched without putting the
   thread down, and the middle column takes whatever is left. */
/* `minmax(0, 1fr)`, not `1fr`. A plain `1fr` is `minmax(auto, 1fr)`, and the
   board's auto size is whatever the script last wrote onto the canvases as a
   pixel width - so the column refused to shrink below the size it had already
   been given, the row grew past the viewport, and hands-free and the voice
   corner slid off the right-hand edge. The board is measured from this column,
   so a column measured from the board is a loop. */
/* 3rem columns, which is exactly the hands-free button: anything wider was
   space the board could have had, and the board gives some of its own back to
   the quadrant arc below. */
.winding {
  display: grid; grid-template-columns: 3rem minmax(0, 1fr) 3rem;
  align-items: center; gap: .3rem;
}

/* Eight speeds, 0.25x to 3x, on a reel with three of them in view. Labelled as
   a multiple of the usual pace rather than in seconds: see player.js.

   This was five fixed choices on a rail, and the argument for that was the
   glance - the whole set visible at once, nothing hidden, every one of them a
   thumb-sized target. Eight of them will not fit beside the board at a size
   anybody can hit with thread between their fingers, so the glance is spent
   whatever we do here. Spending it on a reel at least leaves a control that
   says what it is.

   Which is what the fade at the two ends is for, and it is the one thing on
   screen reporting that there is more above and below. Against five fixed
   choices it would have been wrong - dimming four options to say they are
   further away when they are simply not chosen - and against a reel it is the
   whole affordance. It reaches about a third of the way into the end rows,
   which is enough to read as unfinished and not enough to make them unreadable.

   Five rows rather than four, and that is not a rounding: three read clearly
   and the two at the ends are dimmed by the mask, which is what says there is
   more of this above and below. An odd number, because the chosen speed rests
   in the *middle* row - with four there is no such row, and the top of the
   list has no resting position where the first speed is not half cut off.

   The two rows of padding are the other half of that. They are inside the
   scrollable area rather than around it, so they are blank space the reel can
   scroll into - which is what lets 3x and 0.25x reach the middle row at all.
   Without them the two speeds at each end can be seen but never chosen from
   the centre, and a picker whose ends behave differently from its middle is a
   picker people stop trusting. `box-sizing: border-box` is global here, so the
   height is the window and the padding is scrolled content, not extra height. */
.pacewrap { display: flex; flex-direction: column; align-items: center; }
.pace {
  --pace-step: 2.15rem;
  display: flex; flex-direction: column; align-items: center;
  height: calc(var(--pace-step) * 5);
  padding-block: calc(var(--pace-step) * 2);
  overflow-y: auto;
  overscroll-behavior: contain;      /* spinning the reel must not move the page */
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;           /* turned off with the rest of the motion below */
  scrollbar-width: none;
  /* One fifth is one row. The middle three are untouched and the outer two are
     taken down to about a third, which reads as *there is more here* without
     making either of them unreadable. */
  mask-image: linear-gradient(to bottom,
    rgba(0, 0, 0, .18), rgba(0, 0, 0, .5) 14%, #000 22%,
    #000 78%, rgba(0, 0, 0, .5) 86%, rgba(0, 0, 0, .18));
}
.pace::-webkit-scrollbar { display: none; }

/* The affordance, and the desktop control. A reel with nothing above or below
   it looks like a short list that happens to be cut off; two arrows say it
   moves. They move the *choice* rather than the view, because a pair of arrows
   that only scrolled would leave the reel showing one speed while the player
   wound at another - and because on a trackpad or a mouse there is no flick to
   make. Greyed out and inert at the ends, so the reel says where its ends are
   rather than swallowing a press. */
.pacestep {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 1.35rem; padding: 0;
  border: 0; background: none; color: var(--muted); cursor: pointer;
}
.pacestep .ic { width: .8rem; height: .8rem; fill: currentColor; }
.pacestep:disabled { opacity: .25; cursor: default; }
.pace button {
  /* `flex: none` because the reel is shorter than its contents by design, and
     without it the eight buttons squash to fit the three rows instead. */
  width: 100%; height: var(--pace-step); flex: none; padding: 0 .2rem;
  scroll-snap-align: center;
  border: 1px solid transparent; border-radius: var(--radius);
  background: none; color: var(--muted); cursor: pointer;
  font: inherit; font-size: .72rem; font-variant-numeric: tabular-nums;
}
.pace button.on {
  color: var(--ink); font-weight: 600;
  border-color: var(--line); background: var(--card);
}

/* Round, and the size of a thumb: this is reached for with thread between the
   fingers. Filled while it runs, so a glance from a metre away says whether
   the player is moving. */
.autobtn {
  display: flex; align-items: center; justify-content: center;
  width: 3rem; height: 3rem; padding: 0; margin: 0 auto;
  border: 1px solid var(--accent); border-radius: 50%;
  background: none; color: var(--accent); cursor: pointer;
}
.autobtn.on { background: var(--accent); color: var(--on-accent); }
.autobtn .ic { width: 1.3rem; height: 1.3rem; fill: currentColor; }
.autobtn .pause, .autobtn.on .play { display: none; }
.autobtn.on .pause { display: block; }

/* Capped against the viewport's *height* as well as its width: the canvas is
   square, so a board given the full width of a 390px phone is 390px tall and
   the Next step button falls off the bottom. */
/* The quadrant arc hangs `--rim` pixels outside the board, so the board asks
   for that much less than its column and the arc lands flush with the column's
   edge instead of in the gap beyond it. It went out without this and the arc's
   ends - which fall at three and nine o'clock, exactly where the pace rail and
   hands-free sit - ran under both of them.

   Written as a variable rather than the same number in three declarations,
   because the three are one decision: change it here and the board, the arc
   and the space reserved for it all move together. */
.boardwrap {
  --rim: 9px;
  position: relative; aspect-ratio: 1; margin: 0 auto;
  width: min(calc(100% - var(--rim) * 2), 430px, 42vh);
}
/* Everything from here to `.corner` is deliberately outside the theme. It is a
   picture of a physical object - a white board with black thread on it - and
   a themed version paints near-black thread onto a near-black board, which is
   invisible. The same reasoning already governs the canvases; the white they
   used to carry themselves now lives on this layer, underneath them.

   `.dial` and `.corner-a`, not `.disc` and `.qa`. Both of those were taken -
   `.disc` is the favicon in the top bar and `.qa` is the help page's question
   list - and a stylesheet does not warn you: the board quietly came out 26px
   across and its letters 40px low, because the *other* rules won on source
   order. Grep before naming anything in a single-file stylesheet. */
.dial {
  position: absolute; inset: 0; box-sizing: border-box;
  background: #fff; border: 1px solid var(--tan); border-radius: 50%;
}
.boardwrap canvas {
  display: block; position: relative;
  background: none; border: 0; border-radius: 50%;
}
.boardwrap #overlay { position: absolute; inset: 0; }

/* Which quarter of the board to look at, as a thick arc on the rim.
   It was a tinted wedge under the thread first, which is what the reference
   does and which looked right on an empty board - and vanished completely
   under four thousand chords, the exact moment somebody most needs it. The
   rim is the one part of the picture that never fills up.

   A circle with only its top border coloured draws an arc of exactly ninety
   degrees centred on twelve o'clock, so the quadrant is one rotation: B runs
   from twelve to three, hence 45deg, and round the rim from there in the
   board's own order - A at nine o'clock, then B, D, C clockwise. That order
   looks like a mistake and is not; see engine/labels.py.

   Brand burgundy, and thick enough to be a marker rather than a rim that has
   swollen in one place - which is what it was at 4px of kraft. Kraft on cream
   is 2.08:1, under the 3:1 a graphic element needs, so it was faint for an
   arithmetic reason and no amount of extra width would have fixed it. The
   board keeps its own 1px kraft outline underneath, so the rim still reads as
   gold with a burgundy quarter on it.

   Burgundy on the *dark* page is 1.73:1 and this arc all but disappears there.
   Known and chosen (Daniel, 2026-09-11), on the same grounds as `--accent`
   itself: the two cues that carry the same fact without colour - the selected
   letter is larger and in ink rather than muted - read identically in both
   themes, and the chord marker is on the white board rather than on the page,
   so it is 10.37:1 either way. */
.rim {
  position: absolute; inset: calc(var(--rim) * -1); border-radius: 50%;
  border: var(--rim) solid transparent; border-top-color: var(--accent);
  opacity: 0; transition: transform var(--ease), opacity var(--ease);
}
/* One attribute, on the board, driving both the arc and the letter. Two
   would be two things to keep in step. */
.boardwrap[data-quad] .rim { opacity: 1; }
.boardwrap[data-quad="B"] .rim { transform: rotate(45deg); }
.boardwrap[data-quad="D"] .rim { transform: rotate(135deg); }
.boardwrap[data-quad="C"] .rim { transform: rotate(225deg); }
.boardwrap[data-quad="A"] .rim { transform: rotate(315deg); }

/* The nail, in a hole in the middle of the artwork. It is the densest part of
   the picture, which is the price of putting the number where it can be read
   without moving your eyes; tapping it lifts the hole away. */
.hole {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 44%; height: 44%; padding: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: #fff; border: 1px solid var(--tan); color: #171717;
  cursor: pointer; transition: opacity var(--ease);
}
/* The hole lifts and nothing else does. The arc used to fade with it, from
   when the quadrant was a tint painted *under* the thread and peeking meant
   getting everything off the picture. It is on the rim now, outside the board
   entirely, so it was never in the way - and a marker that dims when you look
   at your own artwork is just a marker you have to find twice. */
.boardwrap.peek .hole { opacity: 0; }
.nail {
  font-size: clamp(1.5rem, 8vw, 3rem); line-height: 1; font-weight: 500;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
/* In the corners of the square, which are the empty parts outside the circle,
   at the diagonals - where they are on the real board.

   Bold at 1.1rem is over WCAG's 18.66px large-text threshold, so these clear
   the bar on 3:1 rather than 4.5:1 - which is what lets them stay `--muted`
   (5.03 on cream, 3.14 on charcoal) instead of shouting. At the 0.72rem they
   were, the same colour would have failed in dark mode.

   Each is a fixed round box with a transparent border, so the ring appearing
   around the selected one does not nudge the letter, and each is placed by its
   centre rather than its corner, so the box can change size without moving.

   The page-coloured background is not there to be seen. A letter sits at the
   middle of its quadrant, which is exactly where that quadrant's arc is, and
   on a phone the two overlap by a few pixels. Unmarked letters sit on the page
   and it changes nothing; the selected one is a filled burgundy disc landing
   on a burgundy band, which without it is one shape with a bite out of it. */
.corner {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border: 2px solid transparent; border-radius: 50%;
  font-size: 1.1rem; font-weight: 700; letter-spacing: .06em;
  color: var(--muted); background: var(--paper);
  transition: color var(--ease), border-color var(--ease);
}
.corner-a { left: 5.5%; top: 6.5%; }
.corner-b { left: 94.5%; top: 6.5%; }
.corner-c { left: 5.5%; top: 93.5%; }
.corner-d { left: 94.5%; top: 93.5%; }
/* Filled, not outlined - the same burgundy-under-cream the Next step button is, and
   the same `--on-accent` token, which is what stops it becoming charcoal text
   on burgundy the next time the accent changes. The ring is the page colour
   again, set in a shadow rather than a border so it sits outside the disc:
   without it the badge welds itself to the arc it lands on. */
.boardwrap[data-quad="A"] .corner-a,
.boardwrap[data-quad="B"] .corner-b,
.boardwrap[data-quad="C"] .corner-c,
.boardwrap[data-quad="D"] .corner-d {
  color: var(--on-accent);
  background: var(--accent); border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--paper);
}

/* Only when the quadrant changes, which is once every sixty nails - about once
   a minute at 1x - and exactly when the thing to say is "stop looking at that
   corner of the board, start looking at this one". On every step it would be
   four thousand pulses in an evening and wallpaper inside ten minutes.
   The global `prefers-reduced-motion` block near the end of this file turns
   every animation here into a jump cut, so this needs no case of its own. */
@keyframes quadpulse {
  50% { opacity: .15; }
}
.boardwrap.moved[data-quad="A"] .corner-a,
.boardwrap.moved[data-quad="B"] .corner-b,
.boardwrap.moved[data-quad="C"] .corner-c,
.boardwrap.moved[data-quad="D"] .corner-d,
.boardwrap.moved .rim {
  animation: quadpulse 460ms ease-in-out 2;
}

/* One row, both directions, rather than a row each - a second row is a whole
   line of vertical budget on a screen whose premise is that nothing scrolls.
   28% and the rest: enough of the width that going back is a thumb's target
   rather than a fingernail's, and far enough from half that the forward
   action is never in doubt. Outlined against filled says the same thing
   again in colour. */
.transport { display: flex; gap: .5rem; }
.stepback {
  flex: 0 0 28%;
  display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: none; color: var(--muted);
  font: inherit; font-size: 1rem; line-height: 1; cursor: pointer;
}
.stepback .chev { font-size: 1.4rem; line-height: .7; }
.stepback:disabled { opacity: .4; cursor: default; }
/* Deliberately large: it is pressed a few thousand times, sometimes with a
   knuckle. Deliberately not the whole width: a brush past must not advance,
   and the button beside it is the one that undoes a nail already wound. */
.advance {
  display: block; width: 100%; margin: 0 0 .8rem;
  min-height: 5.5rem; border: 0; border-radius: var(--radius);
  background: var(--accent); color: var(--on-accent);
  font: inherit; font-size: 1.15rem; font-weight: 600; cursor: pointer;
}
.advance:disabled { opacity: .45; cursor: default; }
.transport .advance { flex: 1 1 auto; width: auto; margin: 0; min-height: 4.2rem; }
a.advance.start {
  min-height: 0; padding: .95rem 1rem; margin: 1.6rem 0 0;
  text-align: center; text-decoration: none; font-size: 1rem;
}
.row.minor { display: flex; gap: .8rem; }
.row.minor button {
  flex: 1; padding: .85rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: none; color: var(--muted); font: inherit; font-size: .9rem; cursor: pointer;
}
.row.minor button:disabled { opacity: .4; cursor: default; }

/* The three corners. Where a control belongs when it is touched twice in an
   evening rather than four thousand times.

   A grid rather than `space-between`, because the left-hand button is not
   always there: music appears only where a track has been prepared and filed,
   and under `space-between` its absence would have slid the finder into the
   corner. Three columns hold the middle one in the middle either way. */
.dock {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 1rem; padding: .35rem 0;
}
.dock > :first-child { justify-self: start; }
.dock > :last-child { justify-self: end; }
.dockbtn {
  display: flex; align-items: center; justify-content: center;
  width: 2.75rem; height: 2.75rem; padding: 0; margin: 0 -.7rem;
  border: 0; background: none; color: var(--muted); cursor: pointer;
}
.dockbtn svg {
  width: 1.3rem; height: 1.3rem;
  fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round;
}
.dockbtn svg .solid { fill: currentColor; stroke: none; }
/* A size up from the .76rem it had in the dock, where it was one of three
   things sharing a row. On a line of its own it can be read rather than
   squinted at, and it keeps the tabular figures so the digits do not jitter as
   the number climbs. */
.counter {
  margin: 0; font-size: .88rem; text-align: center;
  text-transform: uppercase; letter-spacing: .18em; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* A hairline across the foot of the screen rather than a bar under a heading:
   it is the one thing here that nobody needs to read, only to notice. */
.screen > .bar {
  height: 2px; margin: 0 calc(var(--gutter) * -1); border-radius: 0;
}

/* The sheets. Up from the bottom rather than open in the middle, so the board
   stays in view - finding your place is looking at the artwork, and you cannot
   see you have found it under a dialog.

   The scrim is shared with `.paused` below, which is the one overlay here that
   does take the middle. One dim for four things, because a second one would be
   a second opacity to keep in step with the first. */
.scrim { position: fixed; inset: 0; z-index: 5; background: rgba(0, 0, 0, .4); }
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 6;
  margin-inline: auto; max-width: 640px; max-height: 70vh; overflow-y: auto;
  padding: 1.4rem var(--gutter) 1.8rem;
  background: var(--card); border-top: 1px solid var(--line);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -8px 30px rgba(0, 0, 0, .18);
  animation: rise 180ms cubic-bezier(.4, 0, .2, 1);
}
@keyframes rise { from { transform: translateY(100%); } }
/* Tighter than a field on a page of its own. Every row this sheet spends is a
   row of the board it is covering. */
.sheet .field { margin-bottom: .8rem; }
.sheet .field:last-of-type { margin-bottom: .4rem; }
/* Full width, like the text inputs two hundred lines up and like the voice
   picker this used to be the only half of. A select sized to its own longest
   option is a control whose width reports what is in the list - Music was a
   third of the card because "No music" is short, and it would have grown a
   little every time a genre was added. Both sheets are one tap on a phone,
   and a tap target that moves is worse than a wide one. */
.sheet .field select { width: 100%; }
.sheet .check { margin-bottom: 1.2rem; }

/* Where you got to, on stopping hands-free.

   In the middle rather than up from the bottom, which is the opposite of the
   argument for the sheets above: they are worked with the board still in view,
   and this one *is* the board - a still of it, with the number the screen
   behind cannot give you, because that one is always a nail ahead. It is read
   and dismissed, so it may have the middle of the screen for the two seconds
   it is there.

   Left and right to nothing with `margin-inline: auto` and a width, rather
   than a transform on both axes: what is either side of the card has to be
   scrim, because clicking off it is one of the two ways out. */
.paused {
  position: fixed; left: 0; right: 0; top: 50%; z-index: 6;
  transform: translateY(-50%);
  width: min(20rem, calc(100vw - 32px)); margin-inline: auto;
  padding: 1.2rem; text-align: center;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .3);
}
/* Outside the theme, for the same reason `.dial` is: a picture of a white
   board with black thread on it, and a themed version of that paints
   near-black onto near-black. `40vh` is what keeps the card off the ends of a
   phone held sideways, where the height is the scarce direction. */
.pausedart {
  aspect-ratio: 1; width: min(11rem, 40vh); margin: 0 auto .9rem;
  background: #fff; border: 1px solid var(--tan); border-radius: 50%;
}
.pausedart canvas {
  display: block; width: 100%; height: 100%; border-radius: 50%;
}
.paused .hint { margin: 0; font-size: .88rem; }
/* The one line anybody opened this for, so it is the one line that is not
   grey. Tabular figures because the step number is four digits by the end of
   an evening and this is a number to read rather than to skim. */
.pausedstep {
  margin: .2rem 0 1.1rem; font-size: 1.15rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.paused .advance { min-height: 0; margin: 0; padding: .8rem 1rem; font-size: 1rem; }

/* The list is the device's, so the control has to cope with two voices or
   twenty-two. A plain select is the right answer for both, and "Hear it" sits
   on the label line because choosing a voice by name alone is guesswork. */
.voicepick { margin-top: 1.3rem; }
.voicepick span { display: flex; justify-content: space-between; align-items: baseline; }
.voicepick .linkish {
  font-size: .78rem; font-weight: 600; text-transform: none; letter-spacing: 0;
  color: var(--accent);
}

/* The tour, over everything, once per browser.

   The dim and the highlight are one element. `.tourring` is moved over
   whichever control is being named, and its box-shadow - spread far enough to
   reach the corner of any screen - is the darkness over everything else. One
   element means the hole is always exactly where the ring is, which two
   elements would have to be kept agreeing about on every reposition.

   The ring is cream rather than burgundy: it is drawn on a darkened screen, in
   both themes, and the brand colour is a fill rather than a line - it reads as
   a shadow at 2px over a dimmed board. */
.tour { position: fixed; inset: 0; z-index: 20; }
.tourring {
  position: fixed; pointer-events: none;
  border: 2px solid var(--on-accent); border-radius: 10px;
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .62);
}
/* Fixed and full-screen, so the line is drawn in viewport pixels and the
   script can set its ends from the same rectangles it positions everything
   else with, rather than converting into some box's own coordinates. */
.tourline {
  position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none;
  stroke: var(--on-accent); fill: var(--on-accent); stroke-width: 1.5;
}
.tourbubble {
  position: fixed; width: min(19rem, calc(100vw - 24px));
  padding: 1rem 1.1rem 1.1rem;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line); border-radius: 12px;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .3);
}
.tourcount {
  margin: 0 0 .35rem; font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .18em; color: var(--muted);
}
.tourbubble .hint { margin: .35rem 0 0; font-size: .88rem; }
.tourrow { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: 1rem; }
.tourrow .linkish { font-size: .85rem; font-weight: 400; color: var(--muted); }
/* The same filled button as Next step, at the size a control pressed six times
   deserves rather than the size of one pressed four thousand times. */
.tourrow .advance {
  width: auto; min-height: 0; margin: 0; padding: .55rem 1.3rem; font-size: 1rem;
}
.check { display: flex; align-items: center; gap: .6rem; font-size: .9rem; color: var(--muted); }
.check input { width: 1.05rem; height: 1.05rem; accent-color: var(--accent); }
.check input:disabled + span { opacity: .5; }
.progress { display: flex; align-items: baseline; gap: .6rem; margin: .9rem 0 0; font-size: .85rem; color: var(--muted); }
button.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--ink); font: inherit; font-weight: 600; text-decoration: underline;
}

/* ---- help ---- */
.qa { max-width: 62ch; margin-top: 2.5rem; }
.qa div { padding: 1.4rem 0; border-top: 1px solid var(--line); }
.qa h3 { margin: 0 0 .3rem; }
.qa p { margin: 0; color: var(--muted); font-size: .95rem; }
/* An answer long enough to need two paragraphs needs the gap between them. */
.qa p + p { margin-top: .7rem; }

/* ---- panels: used by the working pages and the console ---- */
.panel {
  background: var(--card); border: 1px solid var(--line);
  border-radius: 10px; padding: 1.5rem; margin-bottom: 1rem;
  overflow-x: auto;      /* tables are the one thing that can outgrow a phone */
}
.panel.error { border-color: var(--alarm); }
.panel.tools { margin-top: .8rem; }
.panel.tools .field:last-of-type { margin-bottom: .4rem; }
.panel.locked { display: flex; flex-wrap: wrap; gap: 2rem; align-items: flex-start; }
.panel.locked > div { flex: 1 1 280px; }
.panel form + form { border-top: 1px solid var(--line); padding-top: 1.2rem; margin-top: 1.2rem; }
.panel button[type=submit] {
  background: none; border: 1px solid var(--line); border-radius: var(--radius);
  padding: .55rem .95rem; cursor: pointer; color: var(--ink);
  font: inherit; font-size: .88rem; font-weight: 600;
}
@media (hover: hover) { .panel button[type=submit]:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); } }
.panel button[type=submit]:disabled { opacity: .4; cursor: default; }
/* `.panel button[type=submit]` above blanks the background for the console's
   plain forms, and it is as specific as this rule - so the primary button has
   to put its own back or it is cream text on nothing. */
.panel button.primary { background: var(--accent); border: 0; color: var(--on-accent); }

/* ---- the console -----------------------------------------------------
   Dense tables and plain forms. This is the screen someone opens when a
   customer is on the phone, so it optimises for finding a fact quickly and
   for nothing else. */
.whoami {
  display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: baseline;
  margin: .6rem 0 0; color: var(--muted); font-size: .85rem;
}
.whoami code { color: var(--ink); font-weight: 600; letter-spacing: .08em; }
.whoami form { display: inline; }
.whoami button {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: .85rem; text-decoration: underline;
}
/* The console's video screen picks a style by looking at it, which is the
   whole reason that stage exists - nobody can choose between Soft and Bold as
   words. `auto-fill` rather than a column count, because five previews and the
   All five box wrap differently on a laptop and on the phone somebody is
   holding while they upload the photograph. */
.stylegrid {
  display: grid; gap: 1rem; margin-top: .6rem;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.stylepick {
  display: flex; flex-direction: column; gap: .3rem; cursor: pointer;
  padding: .7rem; border: 1px solid var(--line); border-radius: 10px;
  background: var(--card);
}
.stylepick:has(input:checked) { border-color: var(--accent); }
.stylepick img { width: 100%; height: auto; border-radius: 50%; display: block; }
.stylepick .hint { margin: 0; font-size: .78rem; }

header { max-width: var(--measure); margin: 0 auto; padding: 2rem var(--gutter) 1rem; }
main { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter); }
table {
  width: 100%; border-collapse: collapse; margin-top: .6rem;
  font-size: .86rem; font-variant-numeric: tabular-nums;
}
th, td {
  text-align: left; padding: .45rem .6rem .45rem 0;
  border-bottom: 1px solid var(--line); vertical-align: top;
}
th { color: var(--muted); font-weight: 600; font-size: .78rem;
     text-transform: uppercase; letter-spacing: .05em; }
td code { font-size: .85rem; letter-spacing: .04em; }
table a { color: var(--accent); }
table .warn { font-size: .78rem; margin: 0; }
/* Console figures: right-aligned and tabular, so a column of counts can be
   compared by eye down the page rather than read one at a time. */
.num { text-align: right; font-variant-numeric: tabular-nums; }
/* A bar drawn with a width, because a chart library is a dependency and this
   is a rectangle. Enough to see a distribution's shape, which is the only
   thing being asked of it. */
.bar-cell {
  display: block; height: .55rem; min-width: 2px;
  border-radius: 2px; background: var(--accent); opacity: .75;
}
dl { display: grid; grid-template-columns: 1fr 1fr; gap: .35rem .8rem; margin: 0; font-size: .9rem; }
dl div { display: flex; justify-content: space-between; gap: .5rem; }
dt { color: var(--muted); } dd { margin: 0; font-variant-numeric: tabular-nums; }
dl.wide { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
dl.wide dd { font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  /* Not covered by the rule above: scrolling is neither an animation nor a
     transition, and the pace reel scrolls itself whenever the speed changes. */
  .pace { scroll-behavior: auto; }
}
@media (max-width: 820px) {
  .beats { grid-template-columns: 1fr; gap: 1.8rem; }
  .frame-grid, .chosen { grid-template-columns: 1fr; }
  .film { grid-template-columns: repeat(3, 1fr); gap: 1rem; }
  .cta .btn { flex: 1 1 100%; }

  /* One column, so the picker goes directly under the preview it changes and
     the details follow it. Source order stays right for the two-column
     layout above; only the phone reorders. */
  .chosen .film { order: 2; }
  .chosen .detail { order: 3; }

  /* Centred once the page is a single column, under a centred artwork and a
     centred row of thumbnails. `width: fit-content` because a block-level flex
     container would otherwise stretch the full measure, which is a different
     decision from centring it.

     On `/designs` the whole closing section is centred instead - heading, the
     line about string, and the button - because it is an ending rather than a
     heading, and a centred button under a left-aligned sentence reads as a
     stray. Its two other branches, the out-of-designs and out-of-string
     endings, have been centred at every width since they were written by
     `.endcap`, so this makes all three agree on a phone. */
  .chosen .detail .btn { display: flex; width: fit-content; margin-inline: auto; }
  .closing { text-align: center; }
  /* One column again, so the heading-and-action row unwinds back into the
     stack it came from - and in reading order, which is why the wrapper stops
     being a box at all: `display: contents` hands its two children straight to
     this flex column, where `order` can put the sentence between them. */
  .closing.offer { display: flex; flex-direction: column; align-items: center; }
  .closing.offer .offerhead { display: contents; }
  .closing.offer h2 { order: 1; }
  .closing.offer .lead { order: 2; max-width: none; }
  .closing.offer .btn { order: 3; margin-top: 1.6rem; }
}

/* Below this the five thumbnails go back to one row - the *whole* point of
   moving them up. Three across wrap to two rows 435px tall, which pushes the
   second row off the screen again and leaves the picker half-visible under the
   preview; five across at 64px fit in 110px, so the artwork, all five choices
   and the caption share one screen with room to spare. The thumbnails are for
   switching, not for judging - that is what the 310px preview above them is -
   so small is the right trade, and nothing is hidden behind a horizontal
   scroll where a customer could miss that Bold exists. */
@media (max-width: 560px) {
  .film { grid-template-columns: repeat(5, 1fr); gap: .5rem; }
  .film b { margin-top: .45rem; font-size: .72rem; }
  .film small { font-size: .68rem; line-height: 1.35; }
  .film .ofstring { display: none; }
  /* A 64px column breaks "33% — too much" after the dash, which reads as a
     typo. Keeping the phrase together moves the whole of it to the second
     line instead. */
  .film .over { white-space: nowrap; }
}

/* The width below which the bar and the footer stop being rows.
 *
 * Both are a fixed-width wordmark beside text that will not shrink either - an
 * image keeps its width, and a flex item will not go below its min-content - so
 * `flex-wrap` alone does not save them. It only lets the *text* fold onto a
 * second line while the wordmark stays put and the row stays centred, which is
 * how the bar came to read as three overlapping levels. This is a change of
 * layout, not a change of line, so it takes a real breakpoint.
 *
 * 660px is measured against the content rather than any device. The bar needs
 * 26px of roundel, 170px of wordmark and four nav items - about 590px all
 * told - and the footer with its kit note on /results needs about 580px; the
 * gutter is 5vw either side. Both give out in the low 600s, and this clears it.
 * Check these again if a nav item is added: the bar is the tighter of the two. */
/* The width below which the two ways into a design stop being a row.
 *
 * Measured against the content, like 660px below: the row is 0.8 of the
 * viewport (5vw of gutter each side), *Upload photo* is 154px, *Browse
 * ready-made designs* is 261px, and the gap is 13px - so the pair needs 429px
 * of row and gives out at about 536px of screen.
 *
 * Once they stack, they are one column sized to the wider of the two and both
 * fill it. Two buttons of different widths stacked read as a main action and
 * an afterthought, which is the wrong ranking: a customer with no photograph
 * ready wants the second one. `max-content` rather than `100%` so the column
 * is the *Browse* button's own width and neither grows past what it needs;
 * `minmax(0, ...)` so that on a 320px screen, where the longer label wraps,
 * the column is capped by the screen instead of overflowing it.
 *
 * Centred rather than left-aligned once stacked. Hard against the left edge
 * the pair reads as a column that has collapsed; centred it reads as a choice
 * between two things - the same argument the masthead makes at 660px below.
 *
 * Check this again if either label changes: the numbers above are the labels. */
@media (max-width: 536px) {
  .choices {
    display: grid; grid-template-columns: minmax(0, max-content);
    justify-content: center;
  }
  .choices .filebtn,
  .choices .filebtn span,
  .choices .btn { width: 100%; }
}

@media (max-width: 660px) {
  /* Each on its own line, both centred. Left-aligned it lines up with the page
     beneath, which is the tidier argument on paper and the worse one on a
     phone: two short rows hard against one edge read as a column that has
     collapsed rather than a masthead. Centred they read as one. */
  .topbar { flex-wrap: wrap; gap: .9rem; }
  .topbar .mark, .topbar nav { flex: 1 0 100%; }
  .topbar .mark { justify-content: center; }
  /* The size and the gap are what make the centring visible. At .88rem the four
     items fill a 390px phone exactly, and a centred row with no slack in it is
     indistinguishable from a justified one - the first item hard left, the last
     hard right, and the whole point lost. */
  .topbar nav { margin-left: 0; justify-content: center; gap: .45rem 1rem; font-size: .82rem; }

  /* The footer is a sign-off rather than navigation, so its mark centres over
     the row instead of sitting at the left of it. */
  footer { flex-direction: column; align-items: stretch; }
  .footer-mark { align-self: center; }

  /* Secondary navigation, and it must not wrap like ordinary content. Sharing
     one wrapping row, the way back took the first line and left STYLE and
     STRING stranded on a second - two bars that look unrelated rather than one
     sequence of four. So the link takes its own line and the steps hold a
     single row beneath it, spread across the full width.

     Sized off the viewport rather than at a fixed size, because `nowrap` turns
     "too wide" into overflow rather than a second row: these have to shrink to
     fit whatever phone they land on. Four small steps read as a sequence; two
     and two do not, which is the whole reason the bar is on the page. */
  .journey { flex-direction: column; align-items: stretch; gap: .9rem; }
  /* Under the steps on a phone, over them on a laptop. `order` rather than a
     second copy in the markup, so there is still one link for a screen reader
     to meet and one place to change the words. */
  .journey-back { order: 2; margin-right: 0; align-self: flex-start; }
  .journey .steps {
    justify-content: space-between; gap: .4rem;
    font-size: clamp(.55rem, 2.5vw, .74rem); letter-spacing: .06em;
  }
  .journey .steps span { gap: clamp(.25rem, 1.2vw, .5rem); }
  .journey .steps i {
    width: clamp(1rem, 4.2vw, 1.4rem); height: clamp(1rem, 4.2vw, 1.4rem);
    font-size: clamp(.55rem, 2.2vw, .68rem);
  }
}
