/* The two pages the customer uses to make and manage their game: the create
 * wizard and the status page. Assumes `brand.css` is already loaded.
 *
 * The rule this file follows throughout: **an ordinary form is an ordinary
 * form.** The pixel vocabulary appears as a heart in a rail and a little grain
 * in the frames; it does not turn a text field into an inventory slot or a
 * fieldset into a quest log. Everything here is sized for a thumb first.
 *
 * ## Two surfaces, one file, one of them redesigned
 *
 * `create.html` wears `body class="creator creator-page"`; `status.html` wears
 * `creator` alone. Everything the creator redesign changes about the page's
 * canvas and its display face hangs off `.creator-page`, so the status page
 * keeps the tokens and the typography it shipped with until its own pass.
 * Anything shared between the two — `.panel`, `.summary`, `.note-box`, the
 * buttons — is left exactly as it was, and the creator simply stopped using
 * `.panel`.
 *
 * ## Three surfaces, and a reason for each
 *
 * The form used to be one shape repeated: every group of fields was a bordered
 * card on a dark page, so five steps read as five stacks of the same box. What
 * a section *is* now decides how it is drawn.
 *
 *   `.sect`   No box at all. A hairline across the column, the legend in the
 *             display face beneath it, and the controls on the page's own
 *             ground. This is most of the form — the occasion, the princess
 *             style, the photos, the notes, the two recordings — because most
 *             of the form is a heading followed by some controls, which is a
 *             section, not an object.
 *
 *   `.plate`  A solid velvet card, used only where the content genuinely is a
 *             thing in its own right: the prince, the princess, the written
 *             message, the summary, and the card that asks where to send it.
 *             Five of them across five steps.
 *
 *   The keepsakes. A parchment note, a gilded frame, a portrait ring: warm
 *   physical objects on a nocturnal page. They are the only surfaces allowed a
 *   texture, and they are all real game art or a direct echo of it.
 *
 * No glass anywhere. `backdrop-filter` appears on the landing page's sticky
 * header and nowhere in the creator: a form is read and typed into, and a
 * blurred plane under a text field costs legibility for atmosphere.
 */

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

/* `hidden` has to beat the layout modes below, all of which set `display`. */
[hidden] {
  display: none !important;
}

.creator {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* --- the creator's own canvas ---------------------------------------------
 *
 * Overrides on the page's own `body`, not edits to `brand.css`: the status page
 * loads the same two stylesheets and is not part of this pass, so a token moved
 * upwards would move it too.
 *
 * The palette barely shifts. What changes is the light: the canvas goes as dark
 * as the landing page's, which is what makes art read as lit rather than as a
 * picture pasted on a grey, and the display face becomes the one the landing
 * page set the product's voice in. */
.creator-page {
  /* A form wants a column, not a spread — but the column now has a companion
     beside it, and 68rem left the two fighting over the same 1100px. */
  --shell: 74rem;

  --night: #0a0712;
  --surface: #140e21;
  --surface-sunken: #0d0918;

  /* Fraunces, declared once in `brand.css`. The system serif stays behind it as
     the whole of the fallback plan: if the WOFF2 never arrives, every heading
     renders in the face it rendered in yesterday. */
  --display: var(--display-fraunces);
}

.header-note {
  margin: 0;
  color: var(--ink-faint);
  font-size: 0.82rem;
  text-align: right;
}

@media (max-width: 34rem) {
  .header-note {
    display: none;
  }
}

/* The header is the same bar the landing page carries, and on the creator it
   stays in flow: a sticky bar over a form is a bar over the field somebody is
   typing into. */
.creator-page .site-header {
  background: rgb(10 7 18 / 88%);
}

/* The language control, at a size a finger can actually hit.
   ==========================================================================
   `brand.css` gives it 27px on a desktop and raises it to the 44px floor only
   below the tablet break, which was fine while it was a small piece of chrome
   on a page nobody had to operate. The creator is a form, this is the one
   control in its header, and a creator who picked the wrong language has to be
   able to get back with one press.

   41.6px painted rather than 27px painted with an invisible box around it: a
   hit area a customer cannot see is a hit area they do not know is there, and
   the pill still reads small against a 68px bar — it is the same height the
   landing page gives its own header button.

   Scoped to `.creator-page`, so the status page's header is untouched. */
.creator-page .lang-trigger {
  min-height: 2.6rem;
  padding-inline: 0.75rem;
}

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

/* Five stations on one thread, and where the creator is on it.
 *
 * The rail sits **in the flow**, under the site header and above the step. It
 * was `position: sticky; top: 0` with a z-index, and that is what put it over
 * the content on every step: `wizard.js` moves to a new step by scrolling the
 * top of the document to the top of the viewport, so the eyebrow ("3 OF 5")
 * and, on the shorter steps, the title itself landed underneath a 67px bar that
 * was painted on top of them. Nothing was wrong with the offsets — the bar was
 * a layer, and a layer over the thing it introduces cannot be offset out of the
 * way without inventing a second set of numbers to keep in sync.
 *
 * In flow, the scroll target lands exactly below it and every step measures the
 * same: the rail, then the eyebrow, then the title, then the form.
 *
 * The thread is drawn by the steps themselves rather than by a bar behind them,
 * so the rail is five buttons and nothing else — and it cannot fall out of step
 * with a list `wizard.js` builds at runtime. */
.rail {
  position: relative;
  z-index: 0;
  border-bottom: 1px solid var(--line-soft);
  background: transparent;
}

.rail-inner {
  padding-block: 1.05rem 1.15rem;
}

.rail-steps {
  display: flex;
  align-items: flex-start;
  gap: 0;
  list-style: none;
  margin: 0;
  padding: 0;
}

.rail-step {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
}

/* The thread. One hairline per step, drawn from the previous mark to this one,
   at the height the marks sit at — so it passes behind them rather than under
   the labels. It stops at the current step: a lit line running ahead of where
   the creator has actually got would be a picture of progress nobody made. */
.rail-step + .rail-step::before {
  content: '';
  position: absolute;
  top: 1.02rem;
  right: 50%;
  left: -50%;
  height: 1px;
  background: var(--line);
}

.rail-step:has(.rail-button[data-state='done'])::before,
.rail-step:has(.rail-button[data-state='current'])::before {
  background: linear-gradient(90deg, var(--gold-dim), var(--rose-deep));
}

.rail-button {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.45rem;
  width: 100%;
  min-height: var(--touch);
  padding: 0 0.3rem;
  border: 0;
  background: transparent;
  color: var(--ink-faint);
  font: inherit;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  text-align: center;
  cursor: pointer;
  transition: color 160ms ease;
}

.rail-button:disabled {
  cursor: default;
}

/* The station itself. Filled, so it sits on the thread rather than letting it
   run through — the page's own ground would show the line straight across. */
.rail-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.05rem;
  height: 2.05rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--night);
  font-family: var(--display);
  font-size: 0.95rem;
  line-height: 1;
  transition:
    border-color 160ms ease,
    background-color 160ms ease,
    color 160ms ease;
}

.rail-label {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Three states, told three different ways rather than by three shades of the
   same one: a step that is done is filled in gold, the step being worked on is
   filled in rose and is the only one whose name is bright, and a step not yet
   reached is a hairline ring. */
.rail-button[data-state='done'] {
  color: var(--ink-dim);
}

.rail-button[data-state='done'] .rail-mark {
  border-color: var(--gold-dim);
  background: rgb(224 183 116 / 16%);
  color: var(--gold);
}

.rail-button[data-state='current'] {
  color: var(--ink);
  font-weight: 600;
}

.rail-button[data-state='current'] .rail-mark {
  border-color: var(--rose);
  background: var(--rose);
  color: var(--rose-ink);
  box-shadow: 0 0 0 4px rgb(228 101 127 / 14%);
}

.rail-button:hover:not(:disabled):not([data-state='current']) {
  color: var(--ink);
}

.rail-button:hover:not(:disabled):not([data-state='current']) .rail-mark {
  border-color: var(--rose-deep);
}

.rail-button:focus-visible {
  outline: none;
}

/* The ring goes round the station rather than round a full-width button, so a
   keyboard user sees a mark, not a stripe across the rail. */
.rail-button:focus-visible .rail-mark {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

/* On a phone the five names cannot all be read, so they are dropped and the
   rail becomes what it actually is there: five stations on a thread, showing
   where you are. Nothing is lost — every step's own eyebrow says "3 of 5" in
   words directly below, and the buttons keep their full height. */
@media (max-width: 44rem) {
  .rail-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }

  .rail-inner {
    padding-block: 0.8rem;
  }

  .rail-mark {
    width: 1.8rem;
    height: 1.8rem;
    font-size: 0.85rem;
  }

  .rail-step + .rail-step::before {
    top: 0.9rem;
  }
}

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

.wizard {
  flex: 1;
  padding-block: clamp(2rem, 4.5vw, 3.4rem) clamp(2rem, 5vw, 4rem);
}

/* The thing being made on the left, the little world it is becoming on the
   right. Asymmetric on purpose: the form is the work and gets the width, and
   the companion is a glimpse, not a second panel of controls. */
.wizard-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21rem;
  align-items: start;
  gap: clamp(1.5rem, 4vw, 3.25rem);
}

/* Step five is the exception, and deliberately so: the review panel says
   everything the companion *card* was saying, in full, and it reads far better
   across the whole column than squeezed beside a picture of a place. The last
   step is the payoff, and the payoff gets the page.

   The total is the one thing the review panel does not repeat, so it stays:
   step five is where the creator submits, and it is the last place the price
   should go quiet. */
/* Capped rather than stretched, and left-aligned rather than centred: a
   summary read across 1180px is a spreadsheet, and re-centring the column on
   the last step would slide the whole page sideways under the creator. */
.wizard-body[data-step='5'] {
  grid-template-columns: minmax(0, 64rem);
}

.wizard-body[data-step='5'] .preview-card {
  display: none;
}

/* Margins rather than a flex `gap`, and that is the whole reason: a gap is one
   number for every pair of children, and the three parts of a step's opening —
   the eyebrow, the title and the one sentence under it — belong far closer
   together than two sections do. With a gap there is no way to say so. */
.wizard-step {
  display: flex;
  flex-direction: column;
}

.wizard-step > * {
  margin-top: clamp(1.7rem, 3.6vw, 2.6rem);
}

.wizard-step > :first-child {
  margin-top: 0;
}

.wizard-step > .step-counter,
.wizard-step > h1,
.wizard-step > .lede {
  margin-bottom: 0;
}

.wizard-step > h1 {
  max-width: 15ch;
  margin-top: 0.7rem;
  font-size: clamp(2.4rem, 5.6vw, 3.9rem);
  letter-spacing: -0.015em;
}

.wizard-step > .lede {
  max-width: 44ch;
  margin-top: 0.95rem;
  font-size: clamp(1.02rem, 1.5vw, 1.15rem);
  line-height: 1.6;
}

.wizard-step h1:focus-visible {
  outline: none;
}

.step-counter {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin: 0;
  color: var(--gold-dim);
  font-size: 0.76rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* Localizing wraps a line's nodes in one `[data-i18n-text]` span, which would
   otherwise put the badge and the words inside a single flex item and drop the
   gap between them. The row is laid out on whichever of the two is the flex
   container, so it looks the same before and after this page is translated —
   and identical with scripting off, where the wrapper never appears. */
.step-counter [data-i18n-text] {
  display: inline-flex;
  align-items: center;
  gap: inherit;
}

/* The circled digit. `((3))` in the catalog line rather than a `<span>` in the
   markup, because the number moves: "1 of 5" opens with it and "5 adimdan 1."
   closes with it, and a fixed span would have pinned it to the English order. */
.step-counter .numeral {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.55rem;
  height: 1.55rem;
  border: 1px solid var(--rose-deep);
  border-radius: 999px;
  background: var(--wash-rose);
  color: var(--rose-bright);
  font-family: var(--display);
  font-size: 0.88rem;
  font-weight: 400;
  letter-spacing: 0;
}

/* --- sections and plates ---------------------------------------------------- */

/* A section: a hairline, a name, and the controls under it.
 *
 * No border, no background, no shadow. The rule at the top is what separates
 * one group of questions from the next, and it is the same device the landing
 * page uses above its numbered columns. Most of this form is a heading followed
 * by some controls, and that is a section — drawing each one as a floating card
 * is what made five steps read as five stacks of the same box. */
.sect {
  min-width: 0;
  /* Only the inline default a <fieldset> ships with. The block rhythm belongs
     to `.wizard-step`, and a `margin: 0` here silently out-ordered it. */
  margin-inline: 0;
  padding: clamp(1.1rem, 2.4vw, 1.5rem) 0 0;
  border: 0;
  border-top: 1px solid var(--line);
  background: none;
}

/* A plate: a card, for content that is a thing rather than a group of fields.
 *
 * Five on the whole form — the prince, the princess, the written message, the
 * summary and the address — and each of them is an object the creator is making
 * or checking, not a heading with inputs under it. */
.plate {
  min-width: 0;
  margin-inline: 0;
  padding: clamp(1.15rem, 3vw, 1.7rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--lift);
}

/* `float` is what makes a <legend> stylable at all, and it is used for nothing
   else. It was briefly applied to `.review-title` as well — an ordinary <h2>
   that needs no such trick — and a full-width float above a grid collapsed the
   status page's stage rail and both summary lists into a one-character column.
   The two cases are separated here so that cannot happen again. */
.sect > legend,
.plate > legend {
  float: left;
  width: 100%;
  margin: 0;
  padding: 0;
  color: var(--ink);
  font-family: var(--display);
  font-weight: 400;
  line-height: 1.2;
}

.sect > legend {
  margin-bottom: 0.5rem;
  font-size: clamp(1.3rem, 2.4vw, 1.62rem);
}

/* The same weight as a section's name: a plate is one of the step's own
   groups, not something nested inside one, and a smaller heading on the one
   required field of a step inverted the hierarchy it was meant to carry. */
.plate > legend {
  margin-bottom: 0.85rem;
  font-size: clamp(1.25rem, 2.2vw, 1.5rem);
}

/* `float` above takes the legend out of flow, which is what lets it be styled
   at all; this puts the section's own content back below it. */
.sect > legend + *,
.plate > legend + * {
  clear: both;
}

/* A section's own opening line sits directly under its name at reading size
   rather than at hint size: on a page with no boxes it is the sentence that
   says what the controls below are for, and at 0.85rem grey it was
   disappearing. */
.sect > .hint:first-of-type {
  max-width: 56ch;
  margin-top: 0;
  color: var(--ink-dim);
  font-size: 0.95rem;
}

.plate > .hint:first-of-type {
  margin-top: 0;
}

/* Only a section's own plain labels. `:not([class])` is doing real work: as
   `.sect label` this rule out-specified every labelled component inside one —
   `.storm-axe-toggle` in particular, whose checkbox and text stopped sitting on
   one line and became a stray square above a heading. A component that has been
   given a class has already said how it lays itself out. */
.sect > label:not([class]),
.plate > label:not([class]) {
  display: block;
  margin: 0 0 var(--gap);
}

.sect > label:not([class]):last-child,
.plate > label:not([class]):last-child {
  margin-bottom: 0;
}

/* Two plates side by side where there is room: the prince and the princess are
   the same question asked twice and should look it. */
.pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: var(--gap);
}

/* --- the one adventure there is ---------------------------------------------- */

/* Presented rather than chosen, and presented the way the landing page presents
   a scene: the art at full width across the top of a frame, the words in a band
   under it. It was a thumbnail beside a paragraph, which is how a product is
   listed in a table, not how a place is shown. */
.story-card {
  display: block;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--lift);
}

/* `height: auto` is load-bearing here and on every image below it. The markup
   carries width/height attributes (they reserve layout space and stop the page
   jumping as art loads), and those set a used height that `aspect-ratio` cannot
   override on its own — without this the card renders 540px tall. */
.story-card img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  object-position: center 60%;
  border: 0;
  border-radius: 0;
  image-rendering: pixelated;
}

.story-card-body {
  padding: clamp(1rem, 2.4vw, 1.35rem) clamp(1rem, 2.4vw, 1.5rem)
    clamp(1.05rem, 2.4vw, 1.4rem);
  border-top: 1px solid var(--line-soft);
}

.story-card h3 {
  margin-bottom: 0.45rem;
  font-size: clamp(1.25rem, 2.4vw, 1.6rem);
}

.story-card .hint {
  max-width: 62ch;
  margin-top: 0;
  color: var(--ink-dim);
  font-size: 0.95rem;
}

/* What the customer's own material becomes, in one line. Gold rather than rose:
   it is a statement of fact about the game, not a control. */
.story-card-meta {
  margin: 0.8rem 0 0;
  color: var(--gold-dim);
  font-size: 0.82rem;
  letter-spacing: 0.02em;
}

@media (max-width: 34rem) {
  .story-card img {
    aspect-ratio: 16 / 9;
  }
}

/* The language picker lives in `brand.css`. It is one component drawn by one
   module into both headers, so it is styled once; it was duplicated here and in
   `landing.css` while it was two hand-written buttons, and the copies drifted
   the way copies do. The status page loads `brand.css` too and simply has no
   `[data-locale-picker]` to draw into — unstyled rules cost it nothing. */

/* --- wizard navigation ----------------------------------------------------- */

/* Navigation sits **in the flow**, at the end of the step, and never over it.
   It used to be `position: sticky; bottom: 0` with a fade behind it, which put
   Back and Continue on top of whatever happened to be under them — the Princess
   style row on Characters, Little notes on Memories, the heading on Review. A
   translucent bar over real content is not a layer problem to tune; the bar has
   to not be a layer. The cost is a thumb reaching one scroll further on a
   phone, and that is the right trade against covering the thing being read.

   Nothing else in the creator is sticky except the companion, which is a column
   of its own and overlaps nothing. */
.wizard-nav {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  margin-top: var(--gap-lg);
  padding-top: var(--gap);
  border-top: 1px solid var(--line);
  background: transparent;
}

.wizard-nav-spacer {
  flex: 1;
}

@media (max-width: 34rem) {
  /* Back shrinks to its label and Continue takes the rest of the row: the
     forward action is the one a thumb reaches for. */
  .wizard-nav .btn-primary {
    flex: 1;
  }
}

/* --- the companion --------------------------------------------------------- */

/* A keepsake, not a sidebar.
 *
 * The one thing that still follows the scroll, and it can: it is a separate
 * grid column that overlaps nothing.
 *
 * Two things live in it, and only two — **a real place in the game**, and
 * **what the creator has actually typed so far**. There is no rendering of a
 * character that has not been drawn yet, no picture of a finished game, and no
 * progress bar for work nobody has done. `wizard.js` moves the picture on as
 * the story moves indoors and then out under the moon, and swaps the rows for
 * whatever the creator is filling in at that moment.
 *
 * The frame is the one place in the creator gold is allowed to be structural:
 * a hairline rim, because the thing inside it is meant to read as something
 * kept rather than as a panel of data. */
.preview {
  position: sticky;
  top: var(--gap);
}

.preview-card {
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow:
    0 0 0 1px rgb(224 183 116 / 12%),
    var(--lift);
}

.preview-art {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  image-rendering: pixelated;
}

/* The picture's own caption, in the display face, on the band under it — the
   same way the landing page names a scene. It says where this is, which is the
   whole reason the picture is allowed to be here: it is a place in the game,
   not decoration. */
.preview-place {
  margin: 0;
  padding: 0.6rem 1.1rem 0.7rem;
  border-top: 1px solid var(--line-soft);
  color: var(--ink-dim);
  font-family: var(--display);
  font-size: 0.95rem;
  line-height: 1.35;
}

.preview-body {
  padding: 1rem 1.1rem 1.25rem;
  border-top: 1px solid var(--line-soft);
  background: var(--surface-sunken);
}

.preview-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
  font-family: var(--display);
  font-size: 1.05rem;
  font-weight: 400;
}

/* Rows inside the companion are read at a glance, not studied, so they are one
   line each and the value is allowed to be cut. */
.preview .summary {
  font-size: 0.86rem;
}

.preview .summary dd {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* The total, under the card rather than inside it.
 *
 * Its own block, because it outlives the card: the picture and the rows stand
 * down on a phone and again on step five, and this does not — the figure a
 * creator is about to be charged has to survive exactly the two moments they
 * are deciding whether to pay. Same rim, same radius, same surface as the card
 * above it, so the two read as one column rather than as a panel and a badge. */
.preview-total {
  margin-top: 0.75rem;
  padding: 0.85rem 1rem 0.95rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow:
    0 0 0 1px rgb(224 183 116 / 12%),
    var(--lift);
}

.preview-total-label {
  margin: 0;
  color: var(--ink-dim);
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* The one figure on the page that is a total. `tabular-nums` so 5.99 and 6.99
   do not shift the line as the creator changes their mind, and it may wrap on a
   narrow phone rather than push the column wider. */
.preview-total-amount {
  margin: 0.1rem 0 0;
  font-family: var(--display);
  font-size: clamp(1.9rem, 6vw, 2.3rem);
  line-height: 1.05;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}

.preview-total-once {
  margin: 0.15rem 0 0;
  color: var(--ink-dim);
  font-size: 0.82rem;
}

/* Below the desktop break the companion *card* is dropped rather than stacked.
   It is a glimpse of the story beside the form, and a phone has no beside;
   stacked, it became a screenful of decoration between the heading and the
   first control, on every step, and step five says all of it again in full
   anyway.

   The aside itself stays, because the total lives in it and one line of price
   is not decoration — it is the thing a phone most needs to be told. It lands
   at the end of the column, under the step and beside the submit. */
@media (max-width: 62rem) {
  .wizard-body {
    grid-template-columns: minmax(0, 1fr);
  }

  .preview-card {
    display: none;
  }
}

/* --- summary lists (review, preview, and the status page) ------------------- */

.summary {
  display: grid;
  grid-template-columns: minmax(7rem, auto) minmax(0, 1fr);
  gap: 0;
  margin: 0;
  font-size: 0.9rem;
}

.summary dt {
  padding: 0.5rem 1rem 0.5rem 0;
  border-top: 1px solid var(--line-soft);
  color: var(--ink-faint);
}

.summary dd {
  margin: 0;
  padding: 0.5rem 0;
  border-top: 1px solid var(--line-soft);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.summary dt:first-of-type,
.summary dt:first-of-type + dd {
  border-top: 0;
}

.summary dd.is-blank {
  color: var(--ink-faint);
}

.review-title {
  font-size: 1.3rem;
}

/* --- the review step --------------------------------------------------------- */

/* One panel, four groups, two columns.
 *
 * The previous pass boxed each group in its own `.panel`, which turned a receipt
 * into a dashboard: four borders, four shadows, and three of them between the
 * story and the ending. A card is a thing in its own right; these are four parts
 * of one answer to "is this right?", so they share one surface and are separated
 * by nothing heavier than a hairline.
 *
 * Density is the point. Rows are label-left/value-right at 0.88rem, muted label
 * against cream value, one hairline between them, no pill, no icon per row, and
 * no nested card anywhere inside. */
.review-panel {
  padding: clamp(1.15rem, 2.5vw, 1.6rem);
}

.review-panel-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 var(--gap-lg);
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.2vw, 1.7rem);
  font-weight: 400;
}

/* Two columns where there is room, one continuous summary where there is not.
 *
 * `columns` rather than a grid, and the difference is the dead space. A grid
 * places Story and Characters in one row and makes that row as tall as
 * Characters, so the short group sat above a hand's width of empty panel. A
 * column flow balances the two heights instead: the groups fall in reading
 * order — Story, Characters, Memories, Finale — and the browser decides where
 * to break so both columns end at about the same place. Below the break there
 * is one column and the same reading order, which is exactly what a phone
 * wants. */
.review-groups {
  columns: 2;
  column-gap: clamp(1.5rem, 4vw, 2.75rem);
  column-rule: 1px solid var(--line-soft);
}

@media (max-width: 48rem) {
  .review-groups {
    columns: 1;
    column-rule: none;
  }
}

/* A group is one thing; the flow may put it in either column but may never cut
   it in half. */
.review-group {
  min-width: 0;
  margin-bottom: var(--gap-lg);
  break-inside: avoid;
}

.review-group:last-child {
  margin-bottom: 0;
}

/* The address closes the summary, under both columns rather than inside one. */
.review-group-email {
  margin-top: var(--gap-sm);
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}

.review-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-sm);
  min-width: 0;
}

/* The group's name, in the page's own voice.
   It was 0.78rem uppercase sans with wide tracking — the typography of a table
   header, four times over, which is most of why the summary read as an admin
   receipt rather than as the thing the customer had just made. Four short
   words in the display face say the same thing and belong to the same page as
   the heading above them. */
.review-group-title {
  min-width: 0;
  margin: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: 1.08rem;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}

/* Typography, not a call to action. One per group — never one per row — and
   deliberately lighter than Create Game, which is the only thing on this page
   the customer is being asked to press. The hit target is still the touch
   floor; only the paint is small, and the negative margins keep that target
   from pushing the heading's baseline around. The arrow is decorative and drawn
   by the stylesheet, so it is never part of the accessible name. */
.review-edit {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 0.28rem;
  min-height: var(--touch);
  margin: calc(var(--touch) / -2 + 0.55rem) -0.35rem;
  padding: 0 0.35rem;
  border: 0;
  background: none;
  color: rgb(228 101 127 / 78%);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 500;
  cursor: pointer;
  transition: color 160ms ease;
}

.review-edit::after {
  content: '→';
  font-size: 0.95em;
  transition: transform 160ms ease;
}

.review-edit:hover {
  color: var(--rose-bright);
}

.review-edit:focus-visible {
  outline: 2px solid var(--rose);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

@media (prefers-reduced-motion: no-preference) {
  .review-edit:hover::after {
    transform: translateX(2px);
  }
}

/* The rows. Deliberately not `.summary`: that rule is shared with the status
   page and the preview aside, and this needs its own density and its own
   two-column sizing without dragging those two along. */
.review-rows {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  gap: 0;
  margin: 0.55rem 0 0;
  font-size: 0.9rem;
}

.review-rows dt {
  padding: 0.5rem 1rem 0.5rem 0;
  border-top: 1px solid var(--line-soft);
  color: var(--ink-faint);
  white-space: nowrap;
}

.review-rows dd {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.45rem;
  min-width: 0;
  margin: 0;
  padding: 0.5rem 0;
  border-top: 1px solid var(--line-soft);
  color: var(--ink);
  text-align: right;
}

/* The value's words. One line, cut with an ellipsis rather than wrapped: a
   280-character finale message must not be able to stretch the panel, and the
   whole value is still in the element for anyone who inspects or copies it. */
.review-value {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Grey rather than cream, so what is still blank can be seen without reading
   every line. Never the only signal: the words say "Not added" too. */
.review-value.is-blank,
.is-blank {
  color: var(--ink-faint);
}

/* The email is the one row allowed to wrap: an address is not prose, and
   truncating the thing the customer is checking would defeat the row. */
.review-group-email .review-rows dd {
  overflow-wrap: anywhere;
}

.review-group-email .review-value {
  overflow: visible;
  white-space: normal;
}

/* An inline confirmation accent, not a profile picture: small enough to sit in
   a row of text, big enough to recognise a face in. Circular, like the portrait
   ring on step two these come from. */
.review-thumb {
  flex: none;
  width: 2.2rem;
  height: 2.2rem;
  /* The same gold rim the portrait wears on step two, at a twelfth of the size.
     It is the customer's own photo, borrowed from the object URL `create.js`
     already owns — nothing is re-read, re-encoded or fetched for it. */
  border: 1px solid var(--gold-dim);
  border-radius: 50%;
  background: var(--surface-sunken);
  box-shadow: 0 0 0 2px rgb(10 7 18 / 90%);
  object-fit: cover;
}

/* The same circle with nothing in it, so the row keeps its shape before a photo
   is chosen and does not jump when one is. */
.review-thumb.is-empty {
  display: block;
  border-style: dashed;
}

/* The memory photos are hung in frames, not worn as portraits, so theirs keep
   the frame's shape. Same borrowed object URL, same gold rim, one per photo the
   creator actually chose. */
.review-thumbs {
  display: inline-flex;
  flex: none;
  gap: 0.25rem;
}

.review-thumb.is-frame {
  width: 2.1rem;
  height: 1.5rem;
  border-radius: 3px;
}

/* At the narrowest width the words win. The frames are decoration — they are
   `aria-hidden` and the row says "2 of 3 added" beside them — and on a 320px
   phone the 64 pixels they take are the difference between reading that
   sentence and reading most of it. A 390px phone has the room and keeps them,
   which is why this is 22rem and not the 26rem the rest of the file uses. */
@media (max-width: 22rem) {
  .review-thumbs {
    display: none;
  }
}

/* --- where to send it --------------------------------------------------------- */

/* The last card, and the only one that asks rather than reports. The rose
   hairline is the same emphasis the finale's required message carries, for the
   same reason: this is the field that has to be filled in. Compact — the
   address is already read back twice, in the summary above and in the field
   itself, so there is no third copy of it here. */
.review-send {
  /* Narrower than the summary above it, and deliberately: the receipt is the
     wide thing on this step and this is the one small question left. Capped
     rather than centred, so the left edge of the page never moves. */
  max-width: 46rem;
  border-color: rgb(228 101 127 / 38%);
  background:
    linear-gradient(180deg, rgb(228 101 127 / 7%), transparent 55%),
    var(--surface);
}

.review-send input[type='email'] {
  background: var(--surface-sunken);
}

/* The acknowledgement. Two sizes rather than one wall: the commitment in the
   page's ordinary reading size, the explanation under it in the hint size. Both
   are inside the label, so both are still the checkbox's accessible name, and
   both are on screen before the form can be sent. */
/* The acknowledgement. Nothing is shortened, hidden, folded behind a
   disclosure or moved off the page — every sentence that was here is still
   here, still inside the label, and still the checkbox's accessible name. What
   changed is that it can be read: more room above it, a comfortable measure,
   and the leading a paragraph of small type needs. */
.review-consent {
  margin-top: var(--gap-lg);
  padding-top: var(--gap-lg);
  border-top: 1px solid var(--line);
}

.review-consent-body {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
}

.review-consent-lead {
  color: var(--ink);
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5;
}

.review-consent-detail {
  /* Comfortable measure: the old block ran the full width of the panel, which
     on a desktop is about 110 characters a line. */
  max-width: 62ch;
  color: var(--ink-dim);
  font-size: 0.88rem;
  line-height: 1.75;
}

/* --- the final action ---------------------------------------------------------- */

/* Create Game is the end of the whole form, and is allowed to be heavier than
   the Continue it replaces — same family, same rose, a little more of it. */
.wizard-submit {
  padding-inline: 2rem;
  /* On a phone the nav gives the primary action the rest of the row, and the
     extra padding above was enough to break "Create Game" across two lines. */
  white-space: nowrap;
  background: linear-gradient(180deg, var(--rose-bright), var(--rose-deep));
  box-shadow: 0 8px 22px rgb(194 65 95 / 32%);
}

.wizard-submit:hover {
  box-shadow: 0 10px 26px rgb(194 65 95 / 42%);
}

/* --- the review step on a phone ------------------------------------------------ */

@media (max-width: 34rem) {
  /* The label gets its own line.
     Two columns inside a 320px card left "Duvardaki fotoğraflar" a nowrap label
     and 23 pixels for its value, so every row on the summary was an ellipsis.
     Stacked, the value has the whole width — and the row still reads
     label-then-value, in the same order, with the same hairline between
     entries. */
  .review-rows {
    grid-template-columns: minmax(0, 1fr);
    font-size: 0.88rem;
  }

  .review-rows dt {
    padding: 0.5rem 0 0;
    white-space: normal;
  }

  .review-rows dd {
    justify-content: flex-start;
    padding: 0.1rem 0 0.6rem;
    border-top: 0;
    text-align: left;
  }

  .review-thumb {
    width: 1.9rem;
    height: 1.9rem;
  }

  .review-thumb.is-frame {
    width: 1.9rem;
    height: 1.35rem;
  }

  .review-consent-detail {
    font-size: 0.85rem;
    line-height: 1.7;
  }

  .wizard-submit {
    padding-inline: 1.25rem;
  }
}

/* --- princess body style picker -------------------------------------------- */

/* Centred as a group. The legend and the hint above stay left-aligned — only
   the two fixed-width cards are centred, which is what stops a two-card row
   from sitting off to one side of a full-width panel. */
/* Left, with the section they belong to. They were centred while the section
   was a full-width card and centring was what stopped two cards floating in one
   corner of it; there is no card now, and a pair centred under a left-aligned
   heading reads as unrelated to it. */
.body-styles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 0.9rem;
}

/* Fixed box, so the two cards are identical whatever their artwork does. They
   were previously sized by their contents, which left each PNG's own silhouette
   deciding how big its card looked. */
.body-style {
  position: relative;
  flex: none;
  width: 8.5rem;
  height: 9.5rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  cursor: pointer;
  color: var(--ink-faint);
  font-size: 0.85rem;
  text-align: center;
}

/* The radio itself carries the state for assistive tech; the card shows it. */
.body-style input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

/* A fixed preview area both cards share. `contain` is what actually preserves
   each PNG's aspect ratio inside it — the old rule only looked right because
   both files happen to be square, and would have stretched anything else. */
.body-style img {
  width: 96px;
  height: 96px;
  object-fit: contain;
  object-position: center;
  display: block;
  image-rendering: pixelated;
}

/* Inset, so selecting a card repaints inside its own box. An outer ring would
   grow past the border and make the chosen card read as the larger one. */
.body-style:has(input:checked) {
  border-color: var(--rose);
  color: var(--rose-bright);
  box-shadow: inset 0 0 0 1px var(--rose-deep);
}

.body-style:has(input:focus-visible) {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* --- a row of choices ------------------------------------------------------ */

/* Three values, shown as three cards instead of hidden inside a dropdown.
 *
 * Real radios do the work: same field name, same values, same `required`, same
 * default, same keyboard behaviour (arrow keys move within the group). The
 * cards are the label, so the whole card is the hit area. Deliberately lighter
 * than `.body-style` — these carry a sentence, not artwork, so they are a wide
 * row rather than a tall tile. */
.choice-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11.5rem), 1fr));
  gap: 0.75rem;
  margin-block: 0.5rem var(--gap-sm);
}

.choice {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  /* A grid item will not shrink below its content without this, which is how a
     long line would push the three-across row into a horizontal scrollbar. */
  min-width: 0;
  min-height: var(--touch);
  margin: 0;
  padding: 0.75rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  cursor: pointer;
  transition:
    border-color 160ms ease,
    background-color 160ms ease;
}

/* The radio itself carries the state for assistive tech; the card shows it. */
.choice input {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
}

.choice-count {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  color: var(--ink-dim);
  font-family: var(--display);
  font-size: 0.92rem;
  line-height: 1;
}

.choice-body {
  min-width: 0;
}

.choice-title {
  display: block;
  color: var(--ink);
  font-size: 0.9rem;
  font-weight: 600;
}

.choice-note {
  display: block;
  color: var(--ink-faint);
  font-size: 0.8rem;
}

/* The one-time price for this card's option count.
 *
 * Brighter than the note above it and on its own line: it is the fact that
 * changes with the choice, and it was previously not on the page at all. Empty
 * when the tier is unpriced, and the empty span then occupies nothing. */
.choice-price {
  display: block;
  margin-top: 0.3rem;
  color: var(--gold);
  font-size: 0.9rem;
  font-weight: 600;
}

.choice-price:empty {
  display: none;
}

.choice:hover {
  border-color: var(--line-strong);
}

/* Inset, like the body-style cards: an outer ring would grow past the border
   and make the chosen card read as the larger one. */
.choice:has(input:checked) {
  border-color: var(--rose);
  background: linear-gradient(180deg, rgb(228 101 127 / 10%), transparent);
  box-shadow: inset 0 0 0 1px var(--rose-deep);
}

.choice:has(input:checked) .choice-count {
  border-color: var(--rose);
  color: var(--rose-bright);
}

.choice:has(input:focus-visible) {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* The occasion, in the same selected-state language as the variation cards but
   a far richer shape: the artwork is the option, and the words name it.
 *
 * **No copy is baked into the artwork.** Every visible string is DOM text, so a
 * translated build swaps the words and keeps the same three pictures. The
 * images are decorative (`alt=""`) because the title beside them already says
 * what each one is, and a screen reader should hear it once. */
.choice.occasion {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  padding: 0;
  /* The art reaches the card's edges, so its corners follow the card's. */
  overflow: hidden;
}

.occasion-art {
  display: block;
  aspect-ratio: 3 / 2;
  background: var(--surface-sunken);
}

/* `cover` rather than `contain`: the runtime files are already cropped to the
   illustration, so this only ever trims a little of the painted background —
   never the subject, and never by stretching it. */
.occasion-art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Unchosen options sit back a little; the chosen one comes forward. The
     picture is dimmed, never recoloured or covered. */
  opacity: 0.82;
  transition: opacity 160ms ease;
}

.occasion-copy {
  display: block;
  padding: 0.7rem 0.8rem 0.85rem;
  text-align: center;
}

/* No fixed height anywhere in the copy: "Telling them how you feel" already
   wraps to two lines, and a longer translation has to be able to take a third
   without the artwork moving or the text being clipped. */
.occasion-description {
  display: block;
  margin-top: 0.15rem;
  color: var(--ink-faint);
  font-size: 0.8rem;
}

/* The state indicator, drawn in the corner rather than into the artwork: an
   empty ring while unchosen, a filled rose tick once chosen. */
.occasion-check {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  background: rgb(21 15 36 / 65%);
  color: transparent;
}

.occasion-check svg {
  width: 0.85rem;
  height: 0.85rem;
}

.choice.occasion:hover .occasion-art img {
  opacity: 0.94;
}

.choice.occasion:has(input:checked) {
  /* The inset ring the other cards use, plus one soft rose halo. Restrained on
     purpose: the artwork is already bright, and a strong glow around three
     paintings reads as neon. */
  box-shadow:
    inset 0 0 0 1px var(--rose-deep),
    0 0 18px rgb(228 101 127 / 18%);
}

.choice.occasion:has(input:checked) .occasion-art img {
  opacity: 1;
}

.choice.occasion:has(input:checked) .occasion-check {
  border-color: var(--rose);
  background: var(--rose);
  color: var(--rose-ink);
}

/* One column on a narrow screen, and the card turns on its side: a thumbnail
   beside the words keeps three options reachable without three tall pictures
   to scroll past. */
@media (max-width: 34rem) {
  .choice-row.occasions {
    grid-template-columns: 1fr;
  }

  .choice.occasion {
    flex-direction: row;
    align-items: center;
  }

  .occasion-art {
    flex: none;
    width: 6.5rem;
    aspect-ratio: 4 / 3;
  }

  .occasion-copy {
    /* Room kept clear of the corner indicator. */
    padding: 0.6rem 2.4rem 0.6rem 0.85rem;
    text-align: left;
  }
}

/* The number beside a section name.
 *
 * Editorial rather than pictorial: three small drawn icons said nothing the
 * headings did not already say, and read as decoration applied to the page from
 * outside it. A numeral says which of the three optional sections this is, and
 * says it in the page's own voice. Decorative in the accessibility tree — the
 * heading is the name — and deliberately quieter than the words beside it. */
.section-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  margin-right: 0.55rem;
  border: 1px solid rgb(228 101 127 / 45%);
  border-radius: 50%;
  color: var(--ink-dim);
  font-family: var(--body);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  vertical-align: 0.08em;
}

/* --- optional couple photos ------------------------------------------------ */

.memory-slots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--gap-sm);
  margin-block: var(--gap-sm);
}

/* The gilded frame is the object; the slot around it is not.
   The slot used to be a bordered box with a bordered frame inside it, which is
   a picture in a frame in a box — and three of them in a row read as three
   cards rather than as three pictures on a wall. */
.memory-slot {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  /* A grid item's default `min-width: auto` refuses to shrink below its
     content, which is how a long filename would push the three-across row into
     a horizontal scrollbar. */
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
}

.memory-slot label {
  margin: 0;
}

/* The whole card is the label, so a click anywhere in it opens the file dialog
   — picture, number or button — with or without script. */
.memory-field {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* The frame on the wall, drawn in CSS so the empty state and the chosen photo
   sit in exactly the same one. A double hairline — gold outside, rose inside —
   echoes the portrait rings on the characters step without repeating them. */
.memory-frame {
  position: relative;
  display: block;
  padding: 5px;
  border: 1px solid var(--gold-dim);
  border-radius: var(--radius-sm);
  background: linear-gradient(160deg, rgb(224 183 116 / 22%), rgb(20 14 33 / 90%) 60%);
  box-shadow: 0 6px 18px rgb(0 0 0 / 45%);
}

/* `height: auto` is load-bearing. The markup carries width/height attributes —
   they reserve the space while the art loads — and those set a used height that
   `aspect-ratio` cannot override on its own; without this the frame renders a
   portrait box with the couple stretched inside it. */
.memory-frame img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--rose-deep);
  background: var(--surface-sunken);
}

/* Inside a memory card the card is already the surface, so the upload loses its
   own box and keeps only its button: one frame per slot, not two. */
.memory-slot .upload {
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
}

.memory-slot .upload:hover {
  border-color: transparent;
}

.memory-slot .upload-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--touch);
  border-color: var(--line-strong);
}

.memory-slot .upload:hover .upload-cta {
  border-color: var(--rose);
  color: var(--rose-bright);
}

.memory-slot .upload-name {
  flex: none;
  max-width: 100%;
  text-align: center;
  font-size: 0.76rem;
}

/* Subdued on purpose: it is an illustration of the kind of picture that works,
   and it must never be mistaken for the photo the creator chose. It is an
   `<img>` in the page and never a file — nothing here reaches the form. */
.memory-example {
  opacity: 0.42;
  filter: saturate(0.7);
}

.memory-slot[data-filled='true'] .memory-frame {
  border-color: var(--rose);
}

.memory-slot[data-filled='true'] .memory-example {
  display: none;
}

/* The number under the frame, in the display face: a small plate on a picture
   hung on a wall, rather than a form label above a control. */
.memory-slot > .memory-field > .label-text {
  margin: 0;
  color: var(--ink-dim);
  font-family: var(--display);
  font-size: 1rem;
}

/* One short line per slot rather than the same three sentences three times. */
.memory-tip {
  margin: 0;
  color: var(--ink-faint);
  font-size: 0.78rem;
}

/* Fixed box, so three slots stay the same size whatever shape the photos are
   and the row does not jump as each one is chosen. Built in script and moved
   into the frame above, where it replaces the example. */
.memory-preview {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--rose-deep);
  background: var(--surface-sunken);
}

/* Built in script, so it is styled here rather than given a class in markup. */
.memory-remove,
.note-clear {
  align-self: flex-start;
  min-height: 0;
  padding: 0.35rem 0.85rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--ink-dim);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.memory-remove:hover,
.note-clear:hover {
  border-color: var(--line-strong);
  color: var(--ink);
}

/* On a narrow screen the card turns on its side. Three full-width frames stacked
   is a long scroll for one optional section, and the picture stays perfectly
   readable at a third of the width. */
@media (max-width: 34rem) {
  .memory-field {
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr);
    grid-template-areas:
      "art head"
      "art cta";
    align-items: center;
    column-gap: 0.75rem;
    row-gap: 0.35rem;
  }

  .memory-frame {
    grid-area: art;
  }

  .memory-field > .label-text {
    grid-area: head;
    align-self: end;
    margin: 0;
  }

  .memory-slot .upload {
    grid-area: cta;
    align-self: start;
    align-items: stretch;
  }
}

/* --- optional notes -------------------------------------------------------- */

.note-slots {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
  gap: var(--gap);
  margin-block: var(--gap-sm);
}

/* A keepsake rather than a form field: warm paper inside the dark panel, with
   the writing surface still a real `<textarea>`.
 *
 * **The paper is a gradient, not a picture.** A photographed parchment behind
 * live text is the one way this idea fails — the words stop being readable the
 * moment the texture is busier than they are — so the warmth is painted in CSS
 * and the ink is a plain dark brown that holds its contrast at any size. */
.note-slot {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
  padding: 0.7rem 0.75rem 0.6rem;
  border: 1px solid var(--gold-dim);
  border-radius: var(--radius-sm);
  background:
    radial-gradient(120% 90% at 15% 0%, #fdf3e2 0%, #f3e3ca 55%, #e8d3b4 100%);
  box-shadow:
    inset 0 0 0 1px rgb(255 255 255 / 45%),
    0 1px 8px rgb(0 0 0 / 25%);
  color: #4a3527;
}

.note-slot label {
  margin: 0;
}

/* The number reads as written on the paper, not as a form label above it. */
.note-slot .label-text {
  color: #7a5a3c;
  font-family: var(--display);
  font-size: 0.95rem;
  letter-spacing: 0.01em;
}

.note-slot textarea {
  min-height: 6.5rem;
  padding: 0.15rem 0;
  border: 0;
  border-top: 1px solid rgb(122 90 60 / 30%);
  border-radius: 0;
  background: transparent;
  color: inherit;
  resize: vertical;
}

.note-slot textarea::placeholder {
  color: rgb(122 90 60 / 65%);
}

.note-slot textarea:focus-visible {
  outline: 2px solid var(--rose-deep);
  outline-offset: 3px;
}

/* Under its own field and right-aligned, so three counters do not read as a
   column of numbers with nothing to attach them to. */
.note-count {
  align-self: flex-end;
  color: var(--ink-faint);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* On the paper, the muted browns take over from the page's inks. */
.note-slot .note-count {
  color: #8a684a;
}

.note-slot .note-clear {
  align-self: flex-start;
  border-color: rgb(122 90 60 / 35%);
  background: rgb(255 255 255 / 35%);
  color: #6b4d34;
}

.note-slot .note-clear:hover {
  border-color: rgb(122 90 60 / 60%);
  color: #4a3527;
}

/* Only when the creator has actually gone over. The counter is information
   until then, not a warning. */
.note-count.over {
  color: var(--alarm);
  font-weight: 600;
}

/* --- optional recordings ---------------------------------------------------- */

/* Record and its timer on one line, so the elapsed seconds read as belonging to
   the button rather than floating under the section. */
.voice-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-block: var(--gap-sm);
}

/* The Record button and its recording state are drawn with the finale step's
   own artwork, further down. They were plain rules here; two of them set
   `background` as a shorthand, which would have blanked the orb the moment a
   recording started. */

.voice-timer {
  color: var(--ink-faint);
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
}

.voice-timer.over {
  color: var(--alarm);
  font-weight: 600;
}

/*
 * A refused clip, said beside the control that refused it.
 *
 * The same treatment the timer above uses when a recording runs past the limit,
 * so "too long" reads the same wherever a creator meets it. It is the slot's own
 * `role="status"` line, so the finale voice and the Battle Cry announce their
 * own refusals and never each other's.
 */
.hint.over {
  color: var(--alarm);
  font-weight: 600;
}

.voice-preview {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-block: var(--gap-sm);
}

.voice-preview audio {
  flex: 1 1 15rem;
  min-width: 0;
  max-width: 100%;
  height: 2.6rem;
}

/* --- the finale step --------------------------------------------------------- */

/* Three studio drawings appear on this step and nowhere else: a quill beside the
 * written message, an orb on the record button, and an icon on the upload card.
 * The rule they all follow is the same one the rest of this file follows — the
 * artwork decorates a real control, it never *is* one.
 *
 * So: every one of them is decorative in the accessibility tree, none carries a
 * word of text (the runtime WebPs are text-free so a translated build reuses the
 * same three files), and not one of them is the only thing announcing what a
 * control does. The record button keeps its own DOM text as its accessible name;
 * the upload card keeps its label, its button and its filename. Remove all three
 * images and the section still reads, still records and still submits. */

/* The step's shape, in spacing.
   ==========================================================================
   Three sections, and they are not three equal things: the message is the one
   required field and the reason the step exists, and the voice and the shout
   are two optional touches that go with it. So the gap after the message is a
   full section, and the gap between the two recordings is half of one — they
   read as a pair rather than as two more sections of the same weight.

   Nothing is hidden, reordered or renamed to say this. It is two numbers. */
#voice-section {
  margin-top: clamp(2rem, 4.2vw, 3.1rem);
}

#cry-section {
  margin-top: clamp(1rem, 2.2vw, 1.5rem);
}

/* The written message is the one required field on the page and the one thing
   the step is named after, so it carries a rose hairline the two optional
   sections under it do not. That is the whole of the emphasis: same panel, same
   radius, same rhythm — a card built to look more important than its
   neighbours would have made the optional ones look broken. */
.finale-message {
  position: relative;
  border-color: rgb(228 101 127 / 38%);
  background:
    linear-gradient(180deg, rgb(228 101 127 / 7%), transparent 55%),
    var(--surface);
}

.finale-message textarea {
  min-height: 10rem;
  background: var(--surface-sunken);
  /* The one field on the form somebody will read back to themselves before
     they send it. Set a little larger and a little looser than the rest, which
     is the whole of the emphasis — it is still the same control, the same
     `maxlength` and the same `required`. */
  font-size: max(1.06rem, 17px);
  line-height: 1.65;
  resize: vertical;
}

/* The live count, built in script like the notes' own, under its own field.
   Under it rather than laid inside its bottom corner, because the field is
   resizable and a number pinned over a box the customer can drag is a number
   that ends up in the middle of their words. */
.finale-message .note-count {
  display: block;
  margin-top: 0.35rem;
  text-align: right;
}

/* Top corner, out of the reading path and out of the pointer path. Sized so it
   is an accent beside the heading rather than a second thing to look at, and
   held back to 80% so it never competes with the field below it. */
.finale-quill {
  position: absolute;
  top: 0.6rem;
  right: 0.75rem;
  width: 3.25rem;
  height: 3.25rem;
  opacity: 0.8;
  pointer-events: none;
  user-select: none;
}

/* The legend must not run underneath it on a narrow panel. */
.finale-message > legend {
  padding-right: 3.75rem;
}

/* Two ways in, one under the other: record it here, or upload a file. Stacked
   at every width rather than side by side — as two columns they read as a
   choice that has to be settled before either control can be touched, and
   neither column fits a 390px phone without squeezing the upload surface into
   a strip. */
.voice-ways {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
  margin-block: var(--gap-sm);
}

/* Each way is one card holding its own control and its own limit, so "up to 15
   seconds" is attached to the thing it bounds rather than floating under the
   section. Nothing inside either card is a player, a level meter or a
   waveform: the controls are the same button, file input and native <audio>
   the section has always used. */
.voice-way {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  min-width: 0;
  margin: 0;
  padding: 0.85rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

/* --- the record card ------------------------------------------------------- */

/* The orb is the button's own background, not an image beside it. That is the
   whole point: the glowing circle a creator reaches for is inside the real
   control's hit area, it presses when they press it, and it takes its state
   from the same `.recording` class `create.js` already toggles. An <img> next
   to a separate button would have been a picture of a record button sitting
   beside a record button.
   The left padding is what reserves the circle's room; the button's own text —
   "Tap to record", swapped to "Stop recording" by the script — sits after it
   and remains the accessible name. */
.voice-record {
  flex: 1 1 auto;
  /* `.btn` centres its label; here the label follows the orb, so it starts
     where the orb ends rather than floating in the middle of the button. */
  justify-content: flex-start;
  min-width: 0;
  min-height: 5rem;
  padding: 0.7rem 1.1rem 0.7rem 5.1rem;
  border-color: var(--line-strong);
  border-radius: var(--radius);
  background-color: var(--surface);
  background-image: url('/creator/assets/marketing/finale/finale-record-mic-orb.webp');
  background-repeat: no-repeat;
  background-position: left 0.6rem center;
  background-size: 4rem 4rem;
  color: var(--ink);
  font-size: 0.95rem;
  font-weight: 600;
  text-align: left;
  transition:
    border-color 160ms ease,
    background-color 160ms ease,
    box-shadow 160ms ease;
}

.voice-record:hover {
  border-color: var(--rose);
  background-color: var(--surface-raised);
}

/* Red while recording, and the only colour change in the section: nothing else
   here needs to shout. The ring is drawn by the button rather than by a
   separate element, so there is one thing on screen and it is the control. */
.voice-record.recording {
  border-color: var(--rose-deep);
  background-color: rgb(194 65 95 / 22%);
  box-shadow: 0 0 0 1px var(--rose-deep) inset, 0 0 22px rgb(228 101 127 / 35%);
}

/* A real state deserves a real signal, and this one is genuinely live: the
   class is on the element for exactly as long as the recorder is running. It
   is a glow on the button, not a picture of a sound — nothing here is derived
   from the microphone, and nothing pretends to be. */
@media (prefers-reduced-motion: no-preference) {
  .voice-record.recording {
    animation: voice-recording-pulse 1.6s ease-in-out infinite;
  }
}

@keyframes voice-recording-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 1px var(--rose-deep) inset, 0 0 18px rgb(228 101 127 / 28%);
  }
  50% {
    box-shadow: 0 0 0 1px var(--rose-deep) inset, 0 0 30px rgb(228 101 127 / 52%);
  }
}

/* The limit and the elapsed clock, to the right of the button on a desktop row
   and under it on a phone. */
.voice-take-meta {
  display: flex;
  flex: none;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.3rem;
  min-width: 0;
  text-align: right;
}

.voice-limit {
  color: var(--ink-faint);
  font-size: 0.8rem;
}

/* A hairline over the clock. Static and decorative by construction: it is a
   gradient, it is the same six pixels whatever is happening, and it could not
   represent a level if it wanted to. */
.voice-take-meta .voice-timer::before {
  content: '';
  display: block;
  width: 100%;
  height: 3px;
  margin-bottom: 0.35rem;
  border-radius: 2px;
  background-image: radial-gradient(circle, var(--line-strong) 1px, transparent 1px);
  background-size: 6px 3px;
  opacity: 0.8;
}

/* Recording is an enhancement over upload, never a requirement: `create.js`
   reveals the Record button only where the browser can actually record, and
   renames the upload label when it does. Where it cannot — an insecure origin,
   a browser without MediaRecorder, a denied device — the card that would hold
   the button collapses instead of standing empty, and one quiet line takes its
   place. The stylesheet reads the script's own decision off the button; no
   second opinion about the environment is formed anywhere. */
.voice-way-record:has(> .voice-record[hidden]) {
  display: none;
}

.voice-fallback {
  margin: 0;
  padding: 0.7rem 0.9rem;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--ink-faint);
  font-size: 0.85rem;
  line-height: 1.5;
}

.voice-ways:not(:has(> .voice-way-record > .voice-record[hidden])) > .voice-fallback {
  display: none;
}

/* --- the upload card ------------------------------------------------------- */

/* One <label> around the real file input, so a click anywhere on the card —
   icon, title, button, filename or format line — opens the browser's own file
   dialog, with or without script. The icon is inside that label and therefore
   inside the target; it is not a second control. */
.voice-way-upload {
  align-items: flex-start;
  cursor: pointer;
}

.voice-upload-icon {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  margin-top: 0.1rem;
  opacity: 0.9;
  pointer-events: none;
  user-select: none;
}

.voice-upload-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}

/* The card's title, drawn as the divider the two ways need between them: the
   script rewrites this one string to "Or upload an audio file" wherever a
   Record button is actually offered, so the "or" appears exactly when there is
   something to be an alternative to. */
.voice-way-upload .label-text {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0;
  color: var(--ink-dim);
  font-size: 0.88rem;
  font-weight: 500;
}

.voice-way-upload .label-text::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--line);
}

/* Inside a card, the button is the target and the card is the surface.
   The dashed rectangle used to sit inside the bordered card, inside the
   section — three boxes deep for one file input, which is most of why this
   section read as an upload form rather than as a place to leave your voice.
   The same treatment the portrait fields and the memory slots already use:
   the surface keeps the input, the focus ring and the filename, and gives up
   its own box. */
.voice-way-upload .upload {
  padding: 0;
  border: 0;
  background: none;
}

.voice-way-upload .upload:hover {
  border-color: transparent;
}

.voice-way-upload .upload:focus-within {
  border-style: none;
  border-radius: var(--radius-sm);
  outline-offset: 4px;
}

.voice-way-upload .upload-cta {
  min-height: var(--touch);
  padding-inline: 1.1rem;
  border-color: var(--line-strong);
}

.voice-way-upload .upload:hover .upload-cta {
  border-color: var(--rose);
  color: var(--rose-bright);
}

.voice-way-note {
  color: var(--ink-faint);
  font-size: 0.8rem;
  line-height: 1.5;
}

/* --- what follows the two cards -------------------------------------------- */

/* The clip, once there is one.
   ==========================================================================
   The browser's own player, in a row that says it belongs to this section.
   **Nothing about the playback is reimplemented**: no custom transport, no
   drawn scrubber, no picture of a sound. The control is the real `<audio
   controls>`, it keeps its keyboard behaviour and its accessible name, and all
   this does is decide what it sits on.

   Rose rather than the section's grey, because this row is the only one in the
   section that means "you have made something": it appears when a clip exists
   and disappears with it. */
.voice-preview {
  padding: 0.75rem 0.9rem;
  border: 1px solid rgb(228 101 127 / 30%);
  border-radius: var(--radius);
  background:
    linear-gradient(180deg, rgb(228 101 127 / 8%), transparent 70%),
    var(--surface-sunken);
}

/* The one declaration that fixes the native `<audio>` player.
   ==========================================================================
   The finale's playback control is the browser's own, deliberately: it is
   accessible, it is keyboard-operable, and it is the same control the customer
   already knows. What it was not was dark — Chrome painted a white pill on a
   plum page, and it read as something pasted in from another site.
   `color-scheme` is how an element says which way round it is, and the browser
   repaints its own widget accordingly. Nothing is redrawn here and no part of
   the player is replaced.

   **On the element, not on the page.** It was briefly `color-scheme: dark` on
   `.creator-page`, which worked and also repainted every other native control
   the creator has — the four Storm Axe radios on step three went from white
   discs to dark ones for a fix that was about the finale. Chrome honours the
   declaration on the `<audio>` element itself, which was checked in a browser
   before this was narrowed: the player is dark, and nothing else in the form
   moved.

   `max-width` is unrelated housekeeping — it stops a twelve-second clip being
   given the scrubber of a feature film across a desktop column. */
.creator-page audio {
  color-scheme: dark;
}

.voice-preview audio {
  max-width: 26rem;
  border-radius: 999px;
}

/* Secondary to the two ways in, and separated from them: it is a check on a
   clip that already exists, not a third way to make one. */
.voice-in-action {
  margin-bottom: 0;
  padding-top: var(--gap-sm);
  border-top: 1px solid var(--line-soft);
}

.voice-preview-action:disabled {
  opacity: 0.55;
  cursor: default;
}

/* What happens to the file, said once per section and kept quiet. */
.voice-note {
  margin-top: var(--gap-sm);
  font-size: 0.8rem;
}

/* --- the finale step on a phone --------------------------------------------- */

/* At 390px and below the record button takes the full width and its clock drops
   under it, so neither is squeezed and both stay comfortably above the 44px
   touch floor. Nothing is hidden and nothing scrolls sideways. */
@media (max-width: 34rem) {
  .voice-way {
    padding: 0.75rem;
  }

  /* Only the record card wraps: its button takes the full width and the clock
     drops under it. The upload card keeps its icon beside its body — wrapped,
     the icon floated alone above the title and read as an unrelated ornament. */
  .voice-way-record {
    flex-wrap: wrap;
  }

  .voice-record {
    flex: 1 1 100%;
    min-height: 4.5rem;
    padding-left: 4.5rem;
    background-size: 3.5rem 3.5rem;
  }

  .voice-take-meta {
    flex: 1 1 100%;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    text-align: left;
  }

  /* Side by side on one line, so the dotted rule above the clock would be a
     rule across half a row. It earns its place on the desktop column only. */
  .voice-take-meta .voice-timer::before {
    display: none;
  }

  .voice-upload-icon {
    width: 2.5rem;
    height: 2.5rem;
  }

  .finale-quill {
    width: 2.75rem;
    height: 2.75rem;
  }

  .finale-message > legend {
    padding-right: 3.25rem;
  }
}

/* --- optional Storm Axe Challenge -------------------------------------------- */

/* The whole section is one decision followed by five fields, and the fields are
   hidden until the decision is made. A creator who does not want a question
   should see a sentence and a checkbox, not an empty quiz. */
/* Art on one side, the question on the other: the reward is visible whether or
   not the challenge is switched on, so what the words are for is never in
   doubt. */
.storm-axe-body {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  gap: var(--gap);
  align-items: start;
  margin-top: var(--gap-sm);
}

/* No box. The axe is a prop lying on the page under its own light, which is
   what makes the section read as a reward rather than as a fifth panel. */
.storm-axe-art {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  min-width: 0;
  padding: 0.6rem 0.5rem 0.2rem;
  border: 0;
  background: radial-gradient(
    62% 46% at 50% 34%,
    rgb(228 101 127 / 16%),
    transparent 72%
  );
}

/* `height: auto` against the width/height attributes, which are there to
   reserve the space while the art loads. */
.storm-axe-art img {
  width: auto;
  height: 9rem;
  object-fit: contain;
  image-rendering: pixelated;
  filter: drop-shadow(0 4px 10px rgb(0 0 0 / 45%));
}

.storm-axe-reward {
  margin: 0;
  color: var(--gold);
  font-size: 0.82rem;
  text-align: center;
}

.storm-axe-form {
  min-width: 0;
}

/* The one decision the section asks for, given a row of its own so it reads as
   a switch rather than as the first of six fields. Still a real checkbox: the
   box is the control, the row is its label, and nothing else gates the form. */
.storm-axe-toggle {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.65rem;
  min-height: var(--touch);
  margin: 0;
  padding: 0.5rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  cursor: pointer;
  transition: border-color 160ms ease;
}

.storm-axe-toggle:hover {
  border-color: var(--line-strong);
}

.storm-axe-toggle:has(input:focus-visible) {
  outline: 2px solid var(--rose);
  outline-offset: 2px;
}

.storm-axe-toggle:has(input:checked) {
  border-color: var(--rose);
  background: linear-gradient(180deg, rgb(228 101 127 / 10%), transparent);
}

.storm-axe-toggle input {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
  accent-color: var(--rose);
}

.storm-axe-toggle .label-text {
  margin: 0;
  color: var(--ink);
}

.storm-axe-fields {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  margin-top: var(--gap-sm);
  padding-top: var(--gap-sm);
  border-top: 1px solid var(--line-soft);
}

.storm-axe-fields label {
  margin: 0;
}

/* Question and answers inside one bordered block, so five controls read as one
   thing being built rather than as five unrelated fields. */
.storm-axe-builder {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-sunken);
}

.storm-axe-question {
  display: block;
  margin: 0;
}

/* Inset, so the question reads as written into the panel rather than laid on
   top of it. */
.storm-axe-question textarea {
  background: var(--surface);
  border-color: var(--line-strong);
}

/* The label is a block, so the counter needs telling where to sit; in the flex
   note slots `align-self` already does it. */
.storm-axe-question .note-count {
  display: block;
  text-align: right;
}

.storm-axe-builder-hint {
  margin: 0;
  color: var(--ink-faint);
  font-size: 0.8rem;
}

.storm-axe-answers {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
}

/* The radio sits beside its answer rather than above it, so "which of these is
   right" reads as one row per answer instead of eight stacked controls. */
/* One card per answer: the letter, the words, and the control that says this is
   the right one. The text field is the widest thing in the row on purpose — it
   is what the creator came here to write. */
.storm-axe-answer {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  transition:
    border-color 160ms ease,
    background-color 160ms ease;
}

.storm-axe-letter {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.7rem;
  height: 1.7rem;
  border: 1px solid var(--line-strong);
  border-radius: 50%;
  color: var(--ink-dim);
  font-family: var(--display);
  font-size: 0.9rem;
  line-height: 1;
}

/* Marked. The radio is what changed; this only shows it. */
.storm-axe-answer:has(input[type='radio']:checked) {
  border-color: var(--rose);
  background: linear-gradient(180deg, rgb(228 101 127 / 12%), transparent);
}

.storm-axe-answer:has(input[type='radio']:checked) .storm-axe-letter {
  border-color: var(--rose);
  color: var(--rose-bright);
}

/* The word is always there so the column is legible before anything is chosen;
   only its colour changes. The radio inside it is the authority. */
.storm-axe-correct {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex: none;
  gap: 0.4rem;
  min-width: var(--touch);
  min-height: var(--touch);
  margin: 0;
  padding-left: 0.3rem;
  cursor: pointer;
}

.storm-axe-correct-mark {
  color: var(--ink-faint);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.storm-axe-answer:has(input[type='radio']:checked) .storm-axe-correct-mark {
  color: var(--rose-bright);
  font-weight: 600;
}

/* The counters under four short answers were four numbers nobody reads; the
   `maxlength` still stops the field, and the question above keeps its count. */
.storm-axe-answer .note-count {
  display: none;
}

.storm-axe-correct input {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  margin: 0;
  accent-color: var(--rose);
}

/* The widest thing in the row. */
.storm-axe-text {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
}

.storm-axe-text input {
  border-color: var(--line);
  background: var(--surface-sunken);
}

/* A tick that has not been placed yet is the one thing in this section that can
   stop a submission, so it is the one thing that is allowed to be loud. */
.storm-axe-answers.needs-correct .storm-axe-correct input {
  outline: 2px solid var(--alarm);
  outline-offset: 3px;
}

/* The card wraps rather than squeezing: the answer keeps a full line, and the
   letter and the Correct control sit under it. Longer translations of "Correct"
   have the width of the card to grow into. */
@media (max-width: 26rem) {
  .storm-axe-answer {
    flex-wrap: wrap;
    row-gap: 0.35rem;
  }

  .storm-axe-text {
    flex: 1 1 100%;
    order: 3;
  }
}

@media (max-width: 40rem) {
  .storm-axe-body {
    grid-template-columns: 1fr;
  }

  .storm-axe-art {
    flex-direction: row;
    justify-content: flex-start;
    gap: var(--gap-sm);
  }

  .storm-axe-art img {
    height: 6.5rem;
  }

  .storm-axe-reward {
    text-align: left;
  }
}

/* ==========================================================================
   Status page
   ==========================================================================

   The third surface, and the last one through the redesign. It carries the
   same language the landing page set and the creator adopted — Fraunces for
   display type, a deep plum canvas, rose for the one action that matters and
   gold used structurally — over an information architecture that does not
   change, because the states it shows are the pipeline's own.

   **Everything below is either status-only or scoped to `.status-page`.**
   `.panel`, `.panel-split`, `.review-title`, `.stage*`, `.option*`,
   `.mirror-toggle`, `.link-*`, `.email-line`, `.ready-actions` and `.status-*`
   are worn by this page and nothing else, so they are edited directly. The
   handful that are shared with the creator — `.summary`, `.note-box`,
   `.eyebrow`, `.hint`, `.lede`, `.header-note` — are only ever reached through
   `.status-page`, so the creator keeps exactly the appearance it was approved
   with.
   ========================================================================== */

/* --- the canvas ------------------------------------------------------------- */

/* The same overrides the creator makes on its own body, for the same reasons:
   art reads as lit against something closer to black, and the display face is
   the one the product now speaks in. Scoped to this page's own class, so
   nothing here reaches the creator or the landing page. */
.status-page {
  --shell: 74rem;

  --night: #0a0712;
  --surface: #140e21;
  --surface-sunken: #0d0918;

  --display: var(--display-fraunces);
}

.status-page .site-header {
  background: rgb(10 7 18 / 88%);
}

/* The language control, at a size a finger can hit — the same 41.6px the
   creator gives it, and for the same reason: this page has one control in its
   header and somebody who picked the wrong language has to be able to get
   back. */
.status-page .lang-trigger {
  min-height: 2.6rem;
  padding-inline: 0.75rem;
}

/* The line that tells a creator to keep this page.
   ==========================================================================
   It is the most load-bearing sentence in the header on this surface: the
   status URL carries the only capability they have, and losing the tab is
   losing the way back. At 0.82rem in `--ink-faint` it was set like a footnote.
   One size step, the page's ordinary quiet ink, and a little tracking — still
   well under the wordmark beside it, and still a sentence rather than a
   badge. */
.status-page .header-note {
  color: var(--ink-dim);
  font-size: 0.88rem;
  letter-spacing: 0.01em;
}

/* About a tenth tighter than the first pass, top and bottom.
   The generating screen is deliberately sparse — there is genuinely little to
   say while a request is being worked on — but sparse and adrift are not the
   same thing, and four separated blocks over a tall page had drifted. Nothing
   was added to close the gaps; the gaps closed. Selection and ready are denser
   to begin with and take the same proportional trim without noticing it. */
.status-main {
  flex: 1;
  padding-block: clamp(1.75rem, 4vw, 3.05rem) clamp(1.9rem, 4.5vw, 3.5rem);
}

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

/* What is happening, in the largest type on the page, with a real place from
   the game beside it. The art is atmosphere and says so: it is one of two
   authored backgrounds, it is captioned with which one, and it changes exactly
   once — from night to first light — when the game actually exists. */
.status-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.25rem);
  margin-bottom: clamp(1.75rem, 4vw, 2.9rem);
}

.status-hero-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap);
}

/* The status page's eyebrow is the shared editorial one (`brand.css`): the
   rule, then the label. Its pixel heart is left out of the drawing there. */

.status-hero h1 {
  max-width: 16ch;
  font-size: clamp(2.4rem, 5.6vw, 3.9rem);
  letter-spacing: -0.015em;
}

.status-page .status-hero .lede {
  max-width: 44ch;
  font-size: clamp(1.02rem, 1.5vw, 1.15rem);
  line-height: 1.6;
}

/* The gold rim the creator's companion wears, on the one picture this page
   shows. */
.status-art {
  overflow: hidden;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow:
    0 0 0 1px rgb(224 183 116 / 12%),
    var(--lift);
}

.status-art img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  image-rendering: pixelated;
}

/* Names the place, the same way the landing page names a scene and the
   creator's companion names the one beside the form. It is why the picture is
   allowed to be here: it is somewhere in their game, not decoration. */
/* Stopped. The picture stays — it is still the world the story was being made
   in — but the colour comes down and the caption naming the place is hidden by
   `status.js`, because a place-name under "Something went wrong" reads as a
   postcard. No error illustration is invented, and nothing about why is shown
   here: the sentence above says what happened. */
.status-art[data-state='stopped'] img {
  filter: saturate(0.5) brightness(0.78);
}

.status-art[data-state='stopped'] {
  box-shadow: var(--lift);
}

.status-art figcaption {
  padding: 0.6rem 1.1rem 0.7rem;
  border-top: 1px solid var(--line-soft);
  color: var(--ink-dim);
  font-family: var(--display);
  font-size: 0.95rem;
  line-height: 1.35;
}

@media (max-width: 56rem) {
  .status-hero {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--gap-lg);
  }

  /* The picture is atmosphere, and atmosphere goes under the sentence that
     explains what is happening — never above it on a phone. */
  .status-art {
    order: 2;
  }

  /* And it gives up height there. A 16:9 crop on a 390px phone is most of a
     screen of decoration between the headline and the thing the page is
     actually for — the private link, or the faces to choose from. The same
     picture, a wider slice of it. */
  .status-art img {
    aspect-ratio: 21 / 9;
  }
}

/* --- what the page is made of ------------------------------------------------ */

/* `.panel` is the status page's own surface. It used to be the creator's too —
   every group of fields on the form was one — and when the creator moved to
   `.sect` and `.plate` this was left exactly as it had been, so its own
   redesign could decide what it should be. This is that decision: the same
   solid velvet plate the creator uses for a thing in its own right, because
   that is what each of these is — where the game is, the faces to choose from,
   the private link, the address, the order. */
.panel {
  min-width: 0;
  margin: 0;
  padding: clamp(1.2rem, 2.8vw, 1.75rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--lift);
}

/* `float` is what makes a <legend> stylable at all, and it is used for nothing
   else. It was briefly applied to `.review-title` as well — an ordinary <h2>
   that needs no such trick — and a full-width float above a grid collapsed the
   stage rail and both summary lists into a one-character column. The two cases
   are separated here so that cannot happen again. */
.panel > legend {
  float: left;
  width: 100%;
  margin: 0 0 0.85rem;
  padding: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: 1.2rem;
  font-weight: 400;
}

/* Every panel's own heading, in the display face at the size the creator gives
   a section — this page's headings are step-level, not captions. */
.panel > .review-title {
  display: block;
  margin: 0 0 0.9rem;
  padding: 0;
  color: var(--ink);
  font-family: var(--display);
  font-size: clamp(1.25rem, 2.2vw, 1.55rem);
  font-weight: 400;
}

.panel > legend + * {
  clear: both;
}

.panel > label:not([class]) {
  display: block;
  margin: 0 0 var(--gap);
}

.panel > label:not([class]):last-child {
  margin-bottom: 0;
}

.panel > .hint:first-of-type,
.panel > .review-title + .hint {
  max-width: 62ch;
  margin-top: 0;
  color: var(--ink-dim);
  font-size: 0.95rem;
}

.status-panels {
  display: flex;
  flex-direction: column;
  gap: clamp(1.4rem, 2.6vw, 1.8rem);
}

.panel-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: clamp(1.4rem, 2.6vw, 1.8rem);
}

/* The one notice this page raises. It is used for two different things — a
   request that stopped, and a link that will not open — and both are worth
   reading, so it is a considered block rather than a form error strip. */
.status-page .problems {
  margin: 0 0 var(--gap-lg);
  padding: 1.05rem 1.25rem;
  border-radius: var(--radius);
  font-size: 0.98rem;
  line-height: 1.6;
}

/* --- the stage rail ---------------------------------------------------------- */

/* Named stages, completed / current / upcoming, drawn as the same five-station
   thread the creator's wizard uses — one product, one way of showing where you
   are.
   **There is no percentage anywhere in this file**, because the pipeline does
   not produce one: it moves between six named statuses, and inventing a number
   to sit under them would be the one dishonest thing on the page. */
.stages {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0;
  margin: 0;
  padding: 0;
}

/* Four stages when nobody has to choose anything, five when they do. The rail
   is built from `stagesForRequest`, so the column count follows the list rather
   than a number written here. */
.stages:has(.stage:nth-child(4):last-child) {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.35rem;
  text-align: center;
}

/* The thread, drawn from each stage back to the one before it so the rail has
   no rule hanging off the end. It lights only as far as the request has
   actually got. */
.stage + .stage::before {
  content: '';
  position: absolute;
  left: -50%;
  top: 1.37rem;
  width: 100%;
  height: 1px;
  background: var(--line);
}

.stage[data-state='done'] + .stage::before {
  background: linear-gradient(90deg, var(--gold-dim), var(--rose-deep));
}

.stage-mark {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.05rem;
  height: 2.05rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--night);
  color: var(--ink-faint);
  font-family: var(--display);
  font-size: 0.95rem;
  line-height: 1;
}

.stage-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
}

.stage-name {
  color: var(--ink-faint);
  font-size: 0.84rem;
  line-height: 1.35;
}

.stage-note {
  color: var(--ink-faint);
  font-size: 0.75rem;
}

/* Three states, told three different ways rather than by three shades of one:
   a finished stage is filled in gold, the stage the request is on is filled in
   rose and is the only one whose name is bright, and a stage not yet reached is
   a hairline ring with its own number in it. */
.stage[data-state='done'] .stage-mark {
  border-color: var(--gold-dim);
  background: rgb(224 183 116 / 16%);
  color: var(--gold);
}

.stage[data-state='done'] .stage-name {
  color: var(--ink-dim);
}

.stage[data-state='current'] .stage-mark {
  border-color: var(--rose);
  background: var(--rose);
  color: var(--rose-ink);
  box-shadow: 0 0 0 4px rgb(228 101 127 / 14%);
}

.stage[data-state='current'] .stage-name {
  color: var(--ink);
  font-weight: 600;
}

.stage[data-state='current'] .stage-note {
  color: var(--rose-bright);
}

.stage[data-state='failed'] .stage-mark {
  border-color: var(--alarm-line);
  background: var(--alarm-bed);
  color: var(--alarm);
}

.stage[data-state='failed'] .stage-name {
  color: var(--ink);
}

.stage[data-state='failed'] .stage-note {
  color: var(--alarm);
}

/* The one live thing on the page, and it is honest: the current stage breathes
   while the request is genuinely on it. Nothing is derived from a clock, a
   percentage or an estimate — the class is on the element for exactly as long
   as the pipeline reports that state. */
@media (prefers-reduced-motion: no-preference) {
  .stage[data-state='current'] .stage-mark {
    animation: stage-current-pulse 2.6s ease-in-out infinite;
  }
}

@keyframes stage-current-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 4px rgb(228 101 127 / 14%);
  }
  50% {
    box-shadow: 0 0 0 7px rgb(228 101 127 / 8%);
  }
}

/* On a phone the stages become a vertical list — the same states, read
   downwards, with the thread running between the marks. */
@media (max-width: 44rem) {
  .stages,
  .stages:has(.stage:nth-child(4):last-child) {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .stage {
    flex-direction: row;
    align-items: flex-start;
    gap: 0.9rem;
    padding: 0.5rem 0;
    text-align: left;
  }

  .stage + .stage::before {
    left: 1.02rem;
    top: -0.4rem;
    width: 1px;
    height: 1rem;
  }

  .stage[data-state='done'] + .stage::before {
    background: var(--gold-dim);
  }

  .stage-body {
    align-items: flex-start;
  }
}

/* The one real count the pipeline does produce — jobs finished out of jobs
   seeded — kept clear of the rail above it so it does not read as a caption on
   the last stage. */
/* Full width, deliberately: it is the panel's only `.hint`, so the reading
   measure above would otherwise cap its rule at 62 characters and leave a
   hairline that stops halfway across the rail. */
#stage-detail {
  max-width: none;
}

#stage-detail:not(:empty) {
  margin-top: var(--gap-lg);
  padding-top: var(--gap-sm);
  border-top: 1px solid var(--line-soft);
  color: var(--ink-dim);
}

/* --- the private link --------------------------------------------------------- */

/* Once the game exists, sharing it is what the page is for, so this panel rises
   above the stage rail rather than sitting under a row of finished ticks. The
   rail stays — a completed journey is worth seeing — it just stops being the
   first thing.

   The rose hairline is the same one the creator gives its required field and
   its delivery card: on a page of five panels, this is the one that is the
   point. */
/* The two panels that are an action rather than a report come first. They are
   never on screen together — a game is either waiting to be chosen for or
   already made — so both may claim the same place. */
#picker {
  order: -1;
}

#ready {
  order: -1;
  border-color: rgb(228 101 127 / 32%);
  background:
    linear-gradient(180deg, rgb(228 101 127 / 7%), transparent 60%),
    var(--surface);
}

/* The link itself, set in the mono face on an inset well. It is a string
   somebody has to recognise, not prose. */
.link-field {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  margin-block: var(--gap) var(--gap-sm);
}

.link-field input {
  padding: 0.85rem 1rem;
  border-color: var(--line-strong);
  background: var(--night-deep);
  color: var(--ink-dim);
  font-family: var(--mono);
  font-size: 0.86rem;
}

.link-field input:focus-visible {
  color: var(--ink);
}

.ready-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  margin-top: var(--gap);
}

/* Play and Copy are the two actions anybody came here for, and they share the
   row. Scoped to the share panel: the picker reuses this row for one confirm
   button, which should be its own width rather than half the panel's. */
#ready .ready-actions .btn-primary,
#ready .ready-actions .btn-ghost {
  flex: 1 1 13rem;
}

#ready .ready-actions .btn-primary {
  box-shadow: 0 8px 22px rgb(194 65 95 / 32%);
}

#link-state:not(:empty) {
  margin-top: var(--gap);
  padding: 0.85rem 1rem;
  border: 1px solid var(--alarm-line);
  border-radius: var(--radius-sm);
  background: var(--alarm-bed);
  color: var(--alarm);
  font-size: 0.9rem;
  line-height: 1.55;
}

/* --- link management ---------------------------------------------------------- */

/* Revoke and Generate, kept out of the way. They are rare, one of them destroys
   something the customer already sent, and standing them beside Play and Copy
   made a keepsake page read like an admin console. */
.link-admin {
  margin-top: var(--gap-lg);
  padding-top: var(--gap);
  border-top: 1px solid var(--line);
}

/* `inline-flex` drops the browser's own triangle, so the disclosure draws its
   own — rotated by the open state, which is the state the element already
   carries. Without it this reads as a sentence nobody can press. */
.link-admin > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--touch);
  color: var(--ink-faint);
  font-size: 0.9rem;
  cursor: pointer;
  list-style: none;
}

.link-admin > summary::-webkit-details-marker {
  display: none;
}

.link-admin > summary::before {
  content: '';
  width: 0.42rem;
  height: 0.42rem;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 160ms ease;
}

.link-admin[open] > summary::before {
  transform: rotate(45deg);
}

.link-admin > summary:hover {
  color: var(--ink-dim);
}

.link-admin > summary:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: 4px;
}

.link-admin .hint {
  max-width: 62ch;
}

.link-admin-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: var(--gap-sm);
}

.link-admin-actions .btn-quiet {
  min-height: var(--touch);
}

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

/* The faces are the screen. Everything else on it is a label for them. */
#picker .panel-split {
  margin-top: var(--gap-lg);
}

.role {
  min-width: 0;
}

/* The character's part in the story, in the display face — not a form label
   over a control. */
.role-name {
  margin: 0 0 var(--gap-sm);
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--line-soft);
  color: var(--ink);
  font-family: var(--display);
  font-size: 1.1rem;
  font-weight: 400;
}

/* However many the server actually offered. `auto-fit` with a floor rather than
   a fixed column count: one candidate fills its row, two sit side by side,
   three wrap where they must — and a role that received only one is never given
   an invented sibling to keep a grid tidy. */
.options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 8.5rem), 1fr));
  gap: 0.75rem;
  margin-block: 0 var(--gap);
}

.option {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  min-width: 0;
  padding: 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-sunken);
  color: var(--ink-faint);
  font: inherit;
  line-height: 1.2;
  cursor: pointer;
  transition:
    border-color 160ms ease,
    background-color 160ms ease;
}

.option:hover {
  border-color: var(--line-strong);
}

.option img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: contain;
  /* The face sits on the same dark plum the game draws it over, so what the
     creator judges is the head and not a checkerboard. */
  background:
    radial-gradient(70% 60% at 50% 42%, rgb(228 101 127 / 10%), transparent 72%),
    var(--night-deep);
  border-radius: var(--radius-sm);
  image-rendering: pixelated;
}

/* The letter, and — when this is the one — the word that says so. Selection is
   never carried by colour alone: the card gains a ring, the caption gains
   weight, and `aria-pressed` says it outright. */
.option-label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  font-size: 0.82rem;
}

.option[aria-pressed='true'] {
  border-color: var(--rose);
  background: linear-gradient(180deg, rgb(228 101 127 / 12%), transparent);
  box-shadow: inset 0 0 0 1px var(--rose-deep);
}

.option[aria-pressed='true'] .option-label {
  color: var(--rose-bright);
  font-weight: 600;
}

/* A tick in the corner of the chosen card, drawn by the stylesheet from the
   state the button already carries. Decorative: the caption beside it and
   `aria-pressed` are what actually say this one is chosen. */
.option[aria-pressed='true'] .option-label::before {
  content: '✓';
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--rose);
  color: var(--rose-ink);
  font-size: 0.7rem;
  line-height: 1;
}

.option:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

/* Mirror toggle. The pressed state carries the meaning; the label never
   changes, so the control does not appear to rename itself as it is used. */
.mirror-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--touch);
  padding: 0.4rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-sunken);
  color: var(--ink-dim);
  font: inherit;
  font-size: 0.88rem;
  cursor: pointer;
  transition:
    border-color 160ms ease,
    color 160ms ease;
}

.mirror-toggle:hover {
  border-color: var(--line-strong);
  color: var(--ink);
}

.mirror-toggle[aria-pressed='true'] {
  border-color: var(--rose);
  color: var(--rose-bright);
}

/* The separator only. The word beside it is a catalog line written by
   `status.js`; punctuation is not a word in any language, and this is the one
   part of the pair a stylesheet may own. */
.mirror-state:not(:empty)::before {
  content: '·';
  margin-right: 0.35rem;
  color: var(--ink-faint);
}

/* The confirm commits both roles in one request, so it sits under both of them
   rather than under whichever column happened to come first. */
.picker-actions {
  margin-top: var(--gap-lg);
  padding-top: var(--gap);
  border-top: 1px solid var(--line);
}

.picker-actions .btn-primary {
  padding-inline: 2rem;
}

@media (max-width: 34rem) {
  .picker-actions .btn-primary {
    flex: 1 1 100%;
  }
}

/* --- the two summary panels ---------------------------------------------------- */

/* On the ready state, and only there, the two lower panels step back.
   ==========================================================================
   They are truthful and they stay — where we email you, and what you sent us —
   but on the one screen that has a payoff they were competing with it. The
   selector is the state: `#ready` and `#detail` are siblings in
   `.status-panels`, and `#ready` is only ever un-hidden once a game exists, so
   this reaches the ready page and no other. Nothing is removed, nothing is
   folded away, and every word stays at a readable size — the border softens,
   the surface sinks, the headings drop a step and the padding tightens. The
   eye belongs on the private link. */
#ready:not([hidden]) ~ #detail .panel {
  padding: clamp(1.05rem, 2.4vw, 1.5rem);
  border-color: var(--line-soft);
  background: var(--surface-sunken);
  box-shadow: none;
}

#ready:not([hidden]) ~ #detail .review-title {
  margin-bottom: 0.7rem;
  font-size: clamp(1.1rem, 1.8vw, 1.3rem);
  color: var(--ink-dim);
}

#ready:not([hidden]) ~ #detail .email-line {
  background: var(--night-deep);
}

.email-line {
  margin: var(--gap) 0 0;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--night-deep);
  color: var(--ink);
  font-size: 0.95rem;
  overflow-wrap: anywhere;
}

.status-page .note-box {
  margin-top: var(--gap-lg);
}

/* The order, read rather than audited: a little more room per row than a table
   would give, and the label in the page's own quiet ink. */
.status-page .summary {
  margin-top: var(--gap-sm);
  font-size: 0.92rem;
}

.status-page .summary dt,
.status-page .summary dd {
  padding-block: 0.6rem;
}

@media (max-width: 34rem) {
  /* The label gets its own line. Two columns inside a phone-width panel left
     the value a few characters, and every row read as an ellipsis. */
  .status-page .summary {
    grid-template-columns: minmax(0, 1fr);
  }

  .status-page .summary dt {
    padding: 0.55rem 0 0;
  }

  .status-page .summary dd {
    padding: 0.1rem 0 0.6rem;
    border-top: 0;
  }
}

/* --- the upload surface ----------------------------------------------------- */

/* A real `<input type="file">`, presented as part of the page instead of as the
   operating system's grey button and "No file chosen".
 *
 * The input is **still there and still the control**: it keeps its `required`,
 * its `accept`, its name, its validation and its keyboard behaviour, and the
 * browser's own file dialog is what opens. It is laid over the surface at zero
 * opacity rather than hidden, so a click anywhere on the surface is a click on
 * the input itself — no label-for indirection, no script standing between the
 * customer and the control, and nothing to go wrong if the script never loads.
 * Without JavaScript this is a styled file input that works. */
.upload {
  position: relative;
  display: flex;
  /* The filename needs a whole line of its own once the surface is narrow — in
     a three-across memory slot there are about 30px left beside the button, and
     a name squeezed into 30px is what produced "No / file / chos / en". */
  flex-wrap: wrap;
  align-items: center;
  gap: 0.85rem;
  min-width: 0;
  min-height: var(--touch);
  padding: 0.7rem 0.9rem;
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  transition:
    border-color 160ms ease,
    background-color 160ms ease;
}

.upload:hover {
  border-color: var(--rose);
}

/* The focus ring has to follow the input, which is invisible — so the surface
   wears it. */
.upload:focus-within {
  outline: 2px solid var(--rose);
  outline-offset: 2px;
  border-style: solid;
}

.upload input[type='file'] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
}

.upload-cta {
  flex: none;
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font-size: 0.88rem;
  font-weight: 600;
}

/* One line, truncated with an ellipsis. `overflow-wrap: anywhere` was here and
   it is exactly the wrong tool: given a narrow box it breaks inside the word,
   so `1000001553.jpg` came out as three stacked fragments. A filename is not
   prose — it either fits or it is cut, and `min-width: 0` is what lets a flex
   item actually be cut instead of forcing its container wider. */
.upload-name {
  flex: 1 1 8rem;
  min-width: 0;
  overflow: hidden;
  color: var(--ink-dim);
  font-size: 0.88rem;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Filled state, so "chosen" is visible at a glance across a form of six of
   these rather than only by reading each filename. */
.upload[data-filled='true'] {
  border-style: solid;
  border-color: var(--rose);
}

.upload[data-filled='true'] .upload-name {
  color: var(--ink);
}

/* The chosen photo itself, small and inside the surface: the customer's own
   check that they picked the right face, without a second panel. Built in
   script, so it is styled here rather than given a class in markup. */
.upload-thumb {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}

/* --- the portrait upload --------------------------------------------------- */

/* The prince and the princess are asked for a face, not for a file, so the
   question is drawn as a framed portrait rather than as a dashed rectangle.
 *
 * **This is presentation only.** The `<input type="file">` underneath is the
 * same one every other slot uses, with the same name, `accept` and `required`;
 * the circle is a CSS crop over an `<img>`, and no byte of the chosen file is
 * touched by any of it. The whole field is inside its `<label>`, so a click
 * anywhere — crown, circle, caption or button — opens the file dialog, with or
 * without script. */
.portrait-field {
  display: block;
  margin: 0 0 var(--gap);
  text-align: center;
}

.portrait-field:last-child {
  margin-bottom: 0;
}

/* The form still needs the word; the picture is what should carry the meaning.
   Kept in the accessibility tree rather than removed. */
.portrait-field-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The crown hangs over the top of the ring, so the frame reserves the room it
   overlaps into — otherwise the first card in a stacked column clips it. */
.portrait-frame {
  position: relative;
  display: block;
  width: min(100%, 11.5rem);
  margin: 0 auto;
  padding-top: 1.5rem;
}

.portrait-crown {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 1;
  width: 3.6rem;
  height: auto;
  transform: translateX(-50%);
  color: var(--gold);
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / 45%));
}

/* The double ring: the border is the outer one, the padding is the gap, and the
   portrait's own ring below is the inner one. Two elements, no extra markup. */
.portrait-ring {
  position: relative;
  display: block;
  aspect-ratio: 1;
  padding: 6px;
  border: 1px solid var(--gold-dim);
  border-radius: 50%;
  background: var(--surface-sunken);
  transition:
    border-color 160ms ease,
    box-shadow 160ms ease;
}

/* Both the example and the chosen photo. `object-fit: cover` is the entire
   crop: the file keeps its own aspect ratio and its own bytes, and only the
   window over it is round. */
.portrait-ring img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--rose-deep);
}

/* Subdued on purpose. It is a demonstration of framing, and it must not be
   mistaken for the photo the customer just chose. */
.portrait-example {
  opacity: 0.3;
  filter: saturate(0.5) contrast(0.95);
}

.portrait-tag {
  position: absolute;
  bottom: 0.35rem;
  left: 50%;
  padding: 0.15rem 0.55rem;
  transform: translateX(-50%);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-faint);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.portrait-guide {
  display: block;
  margin: 0.7rem 0 0.55rem;
}

.portrait-guide-title {
  display: block;
  color: var(--rose-bright);
  font-size: 0.88rem;
  font-weight: 600;
}

.portrait-guide-note {
  display: block;
  color: var(--ink-faint);
  font-size: 0.82rem;
}

/* One title is showing at any moment, and both occupy one line, so the two
   cards stay the same height whichever state each of them is in. */
.portrait-guide-title.is-chosen {
  display: none;
}

/* The surface itself, with the box taken off it: the ring above is now what
   says "attach a photo here", and a second dashed rectangle under it read as a
   second, separate control. */
.portrait-field .upload {
  flex-direction: column;
  align-items: stretch;
  gap: 0.35rem;
  padding: 0;
  border: 0;
  background: none;
}

.portrait-field .upload:hover {
  border-color: transparent;
}

.portrait-field .upload:focus-within {
  border-style: none;
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

/* The one primary action in the card, sized for a thumb. */
.portrait-field .upload-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch);
  padding: 0.5rem 1rem;
  border: 0;
  background: linear-gradient(180deg, var(--rose-bright), var(--rose-deep));
  color: var(--rose-ink);
  font-size: 0.94rem;
}

.portrait-field .upload:hover .upload-cta {
  background: linear-gradient(180deg, #f68fa3, var(--rose));
}

.portrait-field .upload-name {
  flex: none;
  text-align: center;
  font-size: 0.8rem;
}

/* Chosen. The ring warms, the example and its caption give way to the real
   photo, and the caption above it stops calling the picture a recommendation. */
.portrait-field[data-filled='true'] .portrait-ring {
  border-color: var(--rose);
  box-shadow: 0 0 0 1px rgb(228 101 127 / 22%);
}

.portrait-field[data-filled='true'] .portrait-example,
.portrait-field[data-filled='true'] .portrait-tag,
.portrait-field[data-filled='true'] .portrait-guide-title.is-empty {
  display: none;
}

.portrait-field[data-filled='true'] .portrait-guide-title.is-chosen {
  display: block;
}

/* The face check's answer, under the button that caused it.

   Field-local on purpose: the summary at the top of the form is where a
   submission's problems land, and this is a problem with the picture the
   creator is looking at right now. Out of flow while empty, so an accepted
   photo leaves the two cards exactly the height they already had, and wrapping
   rather than truncating, because the sentence is the whole point. */
.portrait-problem {
  display: block;
  margin-top: 0.5rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--alarm-line);
  border-radius: var(--radius-sm);
  background: var(--alarm-bed);
  color: var(--alarm);
  font-size: 0.82rem;
  line-height: 1.35;
  text-align: left;
  /* A long sentence in a narrow card must wrap, never widen it: the two
     portraits sit in a grid, and one card pushing wider is a horizontal
     scrollbar on a 390px phone. */
  overflow-wrap: anywhere;
}

.portrait-problem:empty {
  display: none;
}

/* Checking, not refusing. Quiet, in the card's own ink, so a photo that is
   about to be accepted does not flash an alarm-coloured box first. */
.portrait-problem[data-state='checking'] {
  border-color: var(--line-strong);
  background: var(--surface-sunken);
  color: var(--ink-faint);
}

/* The chosen photo, built in script and moved into the ring. The small square
   thumbnail rules above do not apply to it. */
.portrait-ring .upload-thumb {
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: 50%;
  background: none;
}

/* --- link management -------------------------------------------------------- */

/* Revoke and Generate, kept out of the way. They are rare, one of them destroys
   something the customer already sent, and standing them beside Play and Copy
   made a keepsake page read like an admin console. */
.link-admin {
  margin-top: var(--gap);
  padding-top: var(--gap-sm);
  border-top: 1px solid var(--line-soft);
}

.link-admin > summary {
  color: var(--ink-dim);
  font-size: 0.88rem;
  cursor: pointer;
}

.link-admin > summary:hover {
  color: var(--ink);
}

.link-admin-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: var(--gap-sm);
}

/* --- the payment step ------------------------------------------------------

   Phase 5 (D-191, `docs/PAYMENT-ARCHITECTURE.md` § 21). The final action of the
   creator flow, and deliberately dressed as the last step of a gift rather than
   as a billing screen: the same panel, the same button, one line of emphasis
   for the price. No card marks, no basket, no totals table — the hosted
   checkout does all of that, and imitating it here would only invite a creator
   to look for a card field that is not there. */

.pay-panel {
  border-color: var(--line-strong);
  background: linear-gradient(180deg, var(--wash-rose), transparent 60%),
    var(--surface-raised);
}

/* The one number on the panel, and the only thing on it set larger than the
   body. It is presentation of a server-decided figure — see `pricing.js`. */
.pay-price {
  margin: 0;
  font-family: var(--display-book, inherit);
  font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2rem);
  line-height: 1.2;
  color: var(--ink);
}

.pay-price + .hint {
  margin-top: 0.35rem;
}

.pay-actions .btn-primary {
  flex: 1 1 16rem;
  box-shadow: 0 8px 22px rgb(194 65 95 / 32%);
}

/* Empty until the button has something to say, so it takes no vertical space
   in the resting state and does not leave a gap under the CTA. */
.pay-status:not(:empty) {
  margin-top: var(--gap);
  padding: 0.85rem 1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--wash-ink);
  color: var(--ink-dim);
}

/* A button mid-flight must look unavailable as well as be unavailable: the
   disabled attribute alone is announced but not obviously seen. */
.pay-actions .btn-primary[aria-busy='true'] {
  cursor: progress;
}

/* `/create/paid` has no art and no stage rail, so the hero would otherwise sit
   in a column sized for a picture that is not there. */
.paid-hero {
  grid-template-columns: minmax(0, 1fr);
}

/* **The challenge, and the smallest amount of layout it needs.**
   `interaction-only` means the widget is usually zero-height and invisible, so
   this is mostly about the case where Cloudflare does ask for something: it
   gets its own line under the wizard buttons rather than squeezing them, and it
   never becomes a flex item that steals width from the submit control. */
.creator-challenge {
  flex-basis: 100%;
  margin-top: 12px;
}

/* The sentence beside it — an expired or unloadable challenge. Same voice as
   the form's other problems, and it takes no space when there is nothing to
   say. */
.creator-challenge-problem {
  flex-basis: 100%;
  margin: 8px 0 0;
  color: var(--rose);
  font-size: 0.9375rem;
  line-height: 1.5;
}

/* The pre-payment policy links (create review step, status payment panel).
   Cream on the dark panel rather than the browser's default blue, underlined so
   they still read as links, with a wrap-friendly gap for a phone. */
.policy-links a {
  color: var(--ink-dim);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
.policy-links a:hover,
.policy-links a:focus-visible {
  color: var(--ink);
}
