/* knocklet.css
   Tokens mirror ios/Knocklet/Resources/Assets.xcassets/*.colorset one for one, and
   test/design-tokens.test.js fails when the two drift. A contrast fix belongs in the
   colorset, then here. Never a hex at a call site. */

/* ── Tokens ─────────────────────────────────────────────────────────────── */

:root {
  color-scheme: light dark;

  /* Ground and type */
  --ink:              #1C1B19;
  --ink-rgb:          28 27 25;
  --muted:            #6F6A63;
  --paper:            #F2F2F0;
  --surface:          #FFFFFF;
  --line:             #E2DFD9;
  /* Palette.emphasisRule. Ink, not line: this is emphasis beside type, and a divider
     colour disappears in that job. */
  --emphasis-rule:    rgb(var(--ink-rgb) / .35);

  /* Structure */
  --slate:            #2F4858;   /* fill under type. Never a state. */
  --control-accent:   #2F4858;   /* a mark read against the ground */
  --slate-panel:      #2F4858;
  --on-slate:         #F2F2F0;

  /* State. Two accents, one meaning each. */
  --waiting:          #B08D57;   /* brass: graphic only, 3.09:1 on surface */
  --waiting-rgb:      176 141 87;
  --waiting-ink:      #7A5F30;   /* brassInk: the mark that gets read, 5.99:1 */
  --acknowledged:     #6F9473;   /* sage */
  --acknowledged-rgb: 111 148 115;
  --acknowledged-ink: #4C6E51;   /* sageInk, 5.74:1 */

  /* Room identity. Overwritten per room by app.js via CSSOM, hex-validated first. */
  --room:             #4A6FA5;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink:              #EDE9E2;
    --ink-rgb:          237 233 226;
    --muted:            #9AA5AC;
    --paper:            #1A2630;
    --surface:          #22333B;
    --line:             #33454F;

    --slate:            #3C5666;
    --control-accent:   #7194AD;
    --slate-panel:      #223642;
    --on-slate:         #EDE9E2;

    --waiting:          #C29A62;
    --waiting-rgb:      194 154 98;
    --waiting-ink:      #D8B57C;
    --acknowledged:     #86AE8A;
    --acknowledged-rgb: 134 174 138;
    --acknowledged-ink: #9CC5A1;
  }
}

/* ── Type ───────────────────────────────────────────────────────────────── */

:root {
  /* Font.display — status, room labels, button labels, the wordmark.
     ui-rounded FIRST; -apple-system would win and hand back SF Pro. */
  --face-display: ui-rounded, "SF Pro Rounded", -apple-system, BlinkMacSystemFont,
                  system-ui, "Segoe UI Variable Display", "Segoe UI", sans-serif;
  /* Body copy is the system face and gets no helper, same as KnockType. */
  --face-body:    -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI",
                  Roboto, Helvetica, Arial, sans-serif;
  /* Font.ticker — elapsed figures, clock times, join codes. */
  --face-ticker:  ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* iOS TextStyles at default Dynamic Type, in rem so 200% zoom carries them. */
  --t-large-title: clamp(1.75rem, 7.5vw, 2.125rem); /* 34pt */
  --t-title2:      1.375rem;   /* 22 */
  --t-title3:      1.25rem;    /* 20 */
  --t-headline:    1.0625rem;  /* 17 semibold */
  --t-body:        1.0625rem;  /* 17 */
  --t-callout:     1rem;       /* 16 */
  --t-subheadline: .9375rem;   /* 15 */
  --t-footnote:    .8125rem;   /* 13 */
  --t-caption:     .75rem;     /* 12 */
  --t-caption2:    .6875rem;   /* 11 */
}

html { font-size: 100%; }            /* never px: a px root breaks text-only zoom */
body {
  margin: 0;
  font: var(--t-body)/1.5 var(--face-body);
  color: var(--ink);
  background: var(--paper);
  -webkit-text-size-adjust: 100%;
}

/* HeroTitle. The rounded face sets loose at largeTitle; -0.5pt, once, here. */
.hero-title { font: 600 var(--t-large-title)/1.15 var(--face-display);
              letter-spacing: -.015em; text-wrap: balance; margin: 0; }
.wordmark   { font: 700 var(--t-large-title)/1.1 var(--face-display); letter-spacing: -.015em;
              margin: 0; text-align: center; }

/* Eyebrow: uppercase and wide-tracked on screen, sentence case to a screen reader. The HTML
   holds sentence case; text-transform does the shouting, so VoiceOver reads "Earlier today"
   and not E-A-R-L-Y. */
.eyebrow { font: 600 var(--t-caption2)/1.3 var(--face-body); letter-spacing: .11em;
           text-transform: uppercase; color: var(--muted); margin: 0 0 var(--sp-2); }

.ticker { font-family: var(--face-ticker); font-variant-numeric: tabular-nums; }

/* JoinCode, both sizes. Tracking is stated per size because it does not scale. */
.join-code      { font: 500 var(--t-callout) var(--face-ticker); letter-spacing: .156em; color: var(--muted); }
.join-code--row { font: 400 var(--t-caption)  var(--face-ticker); letter-spacing: .167em; }

/* ── Spacing, radii, layout ─────────────────────────────────────────────── */

:root {
  --sp-1: .25rem;  --sp-2: .5rem;   --sp-3: .75rem;  --sp-4: 1rem;
  --sp-5: 1.25rem; --sp-6: 1.5rem;  --sp-7: 2rem;    --sp-8: 2.75rem; --sp-9: 3.5rem;

  --r-field: 12px;   /* KnockField */
  --r-card:  14px;
  --r-pill:  999px;
  --hair:    1px;

  --gutter:  clamp(var(--sp-4), 5vw, var(--sp-6));
  --column:  26rem;

  /* The distance between blocks, named because the spacer has to cancel one of them, and small
     because the fullest screen sets it. A room, the disc, its sentence, an overlay line, the note
     field, a history row, Leave and the guard line come to about 720pt of blocks at 390; after the
     padding that leaves 75 for the five gaps between them, and the guard line is last. */
  --flow: var(--sp-3);

  --stage: min(78vw, 19rem);              /* the ripple field */
  --disc:  calc(var(--stage) * .55);      /* THE BUTTON */
}

.page {
  box-sizing: border-box;
  width: 100%;
  max-width: var(--column);
  margin-inline: auto;
  min-height: 100svh;                      /* svh: the iOS URL bar must not crop the button */
  padding: var(--sp-6) var(--gutter) calc(var(--sp-6) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--flow);
}
/* Exactly one, and at the foot. It takes all the free space, so everything above it packs from
   the top and the disc keeps its y while blocks below it come and go. The negative margin cancels
   the gap it would otherwise open above itself: the spacer is the distance, so a rhythm on both
   sides of it is the rhythm counted twice, and the guard line at the floor is what pays. */
.page > .spacer { flex: 1 1 auto; margin-block-start: calc(-1 * var(--flow)); }
/* Focus is moved here once on first render so the room name is heard first. Nobody can tab to
   it, so the ring would only ever be an artefact of that one scripted move. */
.page:focus { outline: none; }

/* Wide enough for the column to become a card, which never overflows: min-height goes, the
   spacer goes with it, and the rhythm is free to open back up. */
@media (min-width: 48rem) {
  :root { --flow: var(--sp-5); }
  body { display: grid; place-items: center; padding: var(--sp-7) 0; }
  .page {
    min-height: 0;
    background: var(--surface);
    border: var(--hair) solid var(--line);
    border-radius: var(--r-card);
    padding-block: var(--sp-8);
  }
  .page > .spacer { display: none; }
}

/* 390px at 200% zoom lands here; 320px at 100% does not. The disc label moves out of the
   disc and the disc becomes a pure target. */
@media (max-width: 18rem) {
  .knock__label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .knock-status { font-size: var(--t-title3); }
}

/* A 568pt phone is 276pt shorter than the modal one and has the same blocks to fit, so the ripple
   field is sized off the height it has: 45svh draws a 249pt stage at 320x568 and a 281pt one at
   360x640, and 19rem keeps the baseline ceiling for a screen that is wide and short. The same
   term does the landscape card's work, where 78vw is most of a viewport with no height to spend.

   The band above the heading is what puts the disc in thumb reach. `.page` packs from the top, so
   a heading at least 33svh tall — less the padding above it and the gap below it — lands the
   stage's top edge at a third of the viewport. `min-` rather than a fixed height, so a room name
   that wraps to three lines pushes the disc down instead of being clipped. Whichever heading is
   up takes it: the room block when there is a room, the wordmark when there is not, which is the
   `off` state.

   The guard line pays for that third: at 320 and 360 it now falls a scroll below the fold on the
   states carrying the most blocks. It still renders in every state, which the render, no-hide and
   DOM-order tests hold it to, and bin/shoot.mjs photographs the foot of the page on every short
   viewport so the trade is looked at rather than assumed. */
@media (max-height: 40rem) {
  :root { --stage: min(78vw, 45svh, 19rem); --flow: var(--sp-2); }
  .page { padding-block: var(--sp-3) calc(var(--sp-3) + env(safe-area-inset-bottom)); }
  .page:not(:is([data-state="boot"], [data-state="code"], [data-state="ended"]))
    :is(.room:not([hidden]), .room[hidden] ~ .wordmark) {
    min-block-size: calc(33svh - var(--sp-3) - var(--flow));
  }
}

/* ── What each state puts on screen ─────────────────────────────────────── */

/* Two mechanisms decide what is visible and both of them only ever hide: the rules below say
   what the state allows, and the `hidden` attribute says what the view actually carried. Nothing
   here un-hides, so a page whose script never ran shows chrome instead of empty labels — and a
   reviewer can set data-state by hand and read all eleven states with no server behind them.
   app.js owns `hidden` on .room, #note-field and #history and never touches the rest. */

/* An author `display` beats the user agent's rule for [hidden], and .room declares one. Without
   this, `hidden` is a no-op on the one block app.js toggles most. */
[hidden] { display: none !important; }

:is([data-state="boot"], [data-state="code"], [data-state="ended"]) .stage { display: none; }

/* off arrives bare — no label, no colour, no window — because the owner shut the room off and
   re-serving what it was called defeats that. The page discards what it was holding. */
:is([data-state="boot"], [data-state="code"], [data-state="ended"], [data-state="off"]) .room { display: none; }

/* Two headings is one too many. The room is the heading once there is a room; the wordmark is
   the heading when there is not, which includes `off`, where the label is withheld and there is
   nothing to put in its place. */
.page:not(:is([data-state="boot"], [data-state="code"], [data-state="ended"], [data-state="off"]))
  .room:not([hidden]) ~ .wordmark { display: none; }

:is([data-state="boot"], [data-state="code"], [data-state="ended"],
    [data-state="off"], [data-state="notYet"], [data-state="over"]) #history { display: none; }

/* The code field is the entry screen and the exit screen, and nowhere in between. */
.page:not(:is([data-state="code"], [data-state="ended"])) #code-form { display: none; }

/* Leaving does not close a ring, so the way out is hidden while one is open: the owner would
   keep being pushed about someone who has gone. A note cannot be attached to an open ring
   either — a re-press is deduped and the second note is discarded — so the field goes with it. */
.page:not(:is([data-state="ready"], [data-state="closed"], [data-state="sent"]))
  :is(#leave, #note-field) { display: none; }

.room-window:empty, #elapsed:empty, #notice:empty, #note-count:empty { display: none; }

/* ── The button ─────────────────────────────────────────────────────────── */

/* The disc, its sentence and its figure are one block. The column gap belongs between blocks,
   so .knock-status's own margin is the whole distance from the ripple field to the sentence. */
.knock-block { display: flex; flex-direction: column; }

.stage { position: relative; width: var(--stage); height: var(--stage);
         margin-inline: auto; display: grid; place-items: center; }

.knock {
  position: relative; z-index: 1;
  /* Said out loud because a button is content-box in Chrome and Safari and border-box in
     Firefox: unstated, the padding lands outside the disc on two engines out of three and the
     167px measurement the stage is sized against holds on neither. */
  box-sizing: border-box;
  inline-size: var(--disc); block-size: var(--disc);
  border-radius: 50%;
  border: 0;
  padding: var(--sp-4);
  background: var(--slate);
  color: var(--on-slate);
  font: 600 clamp(var(--t-subheadline), calc(var(--disc) * .13), var(--t-title3))/1.2 var(--face-display);
  text-align: center;
  overflow-wrap: anywhere;
  cursor: pointer;
  touch-action: manipulation;            /* kills the 300ms double-tap wait, keeps pinch-zoom */
  -webkit-tap-highlight-color: transparent;
  appearance: none;
  transition: background-color 250ms ease, color 250ms ease,
              box-shadow 450ms cubic-bezier(.25,.46,.45,.94), opacity 90ms linear;
}

/* Press: opacity only. No scale — that is a micro-animation and the motion budget is the
   ripple and the settle. KnockButtonStyle uses 0.82. */
.knock:active { opacity: .82; }
.knock:focus-visible { outline: 2px solid var(--control-accent); outline-offset: 3px; }

/* WAITING — brass rim, brassInk word, arcs live behind it. */
[data-state="waiting"] .knock {
  background: var(--surface); color: var(--waiting-ink);
  box-shadow: 0 0 0 2px var(--waiting); cursor: default;
}

/* ACKNOWLEDGED — the settle lands here; the rim warms brass to sage over 450ms. */
[data-state="acknowledged"] .knock {
  background: var(--surface); color: var(--acknowledged-ink);
  box-shadow: 0 0 0 2px var(--acknowledged); cursor: default;
}

/* Inert: outside the window, switched off, or held after a 429. KnockButtonStyle's disabled:
   line fill, muted label. 4.03:1 light / 3.97:1 dark, below the text floor and allowed,
   because an inactive control is exempt and the sentence that matters is not in here. */
[data-state="notYet"] .knock,
[data-state="over"]   .knock,
[data-state="off"]    .knock,
[data-held="true"]    .knock {
  background: var(--line); color: var(--muted); box-shadow: none; cursor: default;
}

/* The sentence under the disc. Always present, always the copy that carries the state. */
.knock-status {
  margin: var(--sp-3) 0 0;
  min-height: 2.6em;                     /* the two lines it says: 2 x 1.3, no jump on transition */
  font: 500 var(--t-title3)/1.3 var(--face-display);
  color: var(--muted);
  text-align: center;
}
[data-state="waiting"]      .knock-status strong { color: var(--waiting-ink); font-weight: 600; }
[data-state="acknowledged"] .knock-status strong { color: var(--acknowledged-ink); font-weight: 600; }
.knock-status .figure { font-family: var(--face-ticker); font-variant-numeric: tabular-nums; }

/* The elapsed phrase is a sibling of the live region rather than a child: with aria-atomic on a
   region holding "4 min", every minute would re-announce the whole sentence. */
#elapsed {
  margin: var(--sp-2) 0 0;
  font: var(--t-subheadline)/1.4 var(--face-ticker);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  text-align: center;
}

/* Held, offline, back online, a press that did not go through: the sentence that composes onto
   a render rather than replacing it. Its own polite region, for the same reason. */
#notice {
  margin: var(--sp-2) 0 0;
  font: var(--t-subheadline)/1.45 var(--face-body);
  color: var(--muted);
  text-align: center;
}

/* ── The knock ripple ───────────────────────────────────────────────────── */

.ripple { position: absolute; inset: 0; width: 100%; height: 100%;
          pointer-events: none; overflow: visible; }
.wave {
  fill: none;
  stroke: var(--waiting);
  stroke-linecap: round;
  /* fill-box, not view-box: Chrome resolves a percentage origin against the viewBox's size with
     its min-x and min-y dropped, so `50% 50%` on a -50 -50 100 100 box lands on user (50,50) —
     the bottom-right corner — and every front expands up and to the left off the screen. The two
     arcs are symmetric about the user-space origin, so their own box centres on the disc, which
     is the point a knock has to come out of. */
  transform-box: fill-box;
  transform-origin: 50% 50%;
  transform: scale(1);
  opacity: 0;
  stroke-width: 4.4;
  transition: opacity 450ms cubic-bezier(.25,.46,.45,.94),
              transform 450ms cubic-bezier(.25,.46,.45,.94);
}

/* Someone is waiting. Three fronts, one 2.5s cycle, a third of a cycle apart. */
[data-state="waiting"] .wave { animation: knock-wave 2.5s linear infinite; }
[data-state="waiting"] .wave:nth-child(2) { animation-delay: -.8333s; }
[data-state="waiting"] .wave:nth-child(3) { animation-delay: -1.6667s; }

@keyframes knock-wave {
  0%   { transform: scale(1);     opacity: 0;    stroke-width: 4.40; }
  18%  { transform: scale(1.236); opacity: .417; stroke-width: 2.78; }
  40%  { transform: scale(1.450); opacity: .269; stroke-width: 1.77; }
  60%  { transform: scale(1.578); opacity: .153; stroke-width: 1.29; }
  80%  { transform: scale(1.648); opacity: .058; stroke-width: 1.07; }
  100% { transform: scale(1.667); opacity: 0;    stroke-width: 1.01; }
}

/* The held frame, phases 0.30 / 0.60 / 0.90. Not a degradation: it is literally the app icon,
   which is why it is a first-class state. `sent` wears it permanently — a press landed in a
   room that does not report back, so the knock is out there and the page claims nothing about
   what happened next. `waiting` wears it only under reduce motion. */
[data-state="sent"] .wave:nth-child(1) { transform: scale(1.363); opacity: .334; stroke-width: 2.14; }
[data-state="sent"] .wave:nth-child(2) { transform: scale(1.578); opacity: .153; stroke-width: 1.29; }
[data-state="sent"] .wave:nth-child(3) { transform: scale(1.663); opacity: .022; stroke-width: 1.01; }

@media (prefers-reduced-motion: reduce) {
  .knock, .wave { transition-duration: 250ms; }   /* Apple's substitution: fade, don't remove */
  [data-state="waiting"] .wave { animation: none; }
  [data-state="waiting"] .wave:nth-child(1) { transform: scale(1.363); opacity: .334; stroke-width: 2.14; }
  [data-state="waiting"] .wave:nth-child(2) { transform: scale(1.578); opacity: .153; stroke-width: 1.29; }
  [data-state="waiting"] .wave:nth-child(3) { transform: scale(1.663); opacity: .022; stroke-width: 1.01; }
}

/* ── Field, quiet button, history, the guard line ───────────────────────── */

/* KnockField: surface fill, line hairline, controlAccent at 1.5 on focus. */
.field {
  display: block; inline-size: 100%; box-sizing: border-box;
  padding: 12px;
  font: var(--t-body)/1.4 var(--face-body);   /* >=16px or iOS Safari zooms on focus */
  color: var(--ink);
  background: var(--surface);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-field);
  appearance: none;
  resize: vertical;
}
.field:focus-visible { outline: none; border-color: var(--control-accent); border-width: 1.5px; padding: 11.5px; }
.field::placeholder { color: var(--muted); }

/* knockCodeEntry(). letter-spacing adds a gap after the last glyph too, so a centred code sits
   one gap left without the matching indent. Same bug in CSS as in SwiftUI. */
.field--code {
  font: var(--t-title2)/1 var(--face-ticker);
  letter-spacing: .45em; text-indent: .45em; text-align: center;
  block-size: 68px;
}

/* The helper sentence under a field, and the character counter. */
.hint { margin: var(--sp-2) 0 0; font: var(--t-footnote)/1.4 var(--face-body); color: var(--muted); }

/* KnockButtonStyle .primary — the slate capsule. The only one on this page is Continue; the
   disc is its own thing. */
.btn-primary {
  box-sizing: border-box;
  min-block-size: 44px; padding: 10px 20px;
  font: 600 var(--t-headline) var(--face-display);
  color: var(--on-slate); background: var(--slate);
  border: 0; border-radius: var(--r-pill);
  cursor: pointer;
}
.btn-primary:active { opacity: .82; }

/* KnockButtonStyle .quiet — the only other button on the page. */
.btn-quiet {
  box-sizing: border-box;
  min-block-size: 44px; padding: 10px 20px;
  font: 500 var(--t-subheadline) var(--face-display);
  color: var(--ink); background: none;
  border: var(--hair) solid var(--line); border-radius: var(--r-pill);
  cursor: pointer;
}
.btn-quiet:active { opacity: .82; }
#leave { align-self: center; }

#code-form { display: flex; flex-direction: column; }
#code-form .btn-primary { align-self: center; margin-block-start: var(--sp-4); }

/* Room identity: a swatch and a label, and the colour appears nowhere else on the page.
   PaletteSheet's own move — a line stroke over the swatch — so an arbitrary hue can never
   collapse into either ground. */
.room { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); }
.room-swatch {
  inline-size: 2.5rem; block-size: .375rem; border-radius: var(--r-pill);
  background: var(--room);
  box-shadow: 0 0 0 var(--hair) var(--line);
}
.room-label  { font: 600 var(--t-title2)/1.25 var(--face-display); color: var(--ink); margin: 0; text-align: center; }
.room-window { font: var(--t-subheadline)/1.4 var(--face-body); color: var(--muted); margin: 0; }

/* Own-ring history. A word and a time, no badge, no dot, no count. */
.history ul { list-style: none; margin: 0; padding: 0; }
.history li {
  display: flex; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) 0; border-block-start: var(--hair) solid var(--line);
  font-size: var(--t-subheadline);
}
.history .when { font-family: var(--face-ticker); color: var(--muted); }
.history .note { flex: 1 1 auto; color: var(--muted); overflow-wrap: anywhere; }
.history .verb { font-weight: 500; }
.history .verb--waiting      { color: var(--waiting-ink); }
.history .verb--acknowledged { color: var(--acknowledged-ink); }

/* The only thing on screen with scripting off, besides the wordmark and the guard line. */
noscript { font: var(--t-callout)/1.5 var(--face-body); color: var(--muted); text-align: center; }

/* Not for emergencies. Static in the HTML shell, never injected, never behind a toggle, and
   Palette.emphasisRule is the one thing in the app that means "notice this sentence". */
.guard {
  margin: 0;
  padding-inline-start: var(--sp-4);
  border-inline-start: 2px solid var(--emphasis-rule);
  font: var(--t-footnote)/1.5 var(--face-body);
  color: var(--muted);
}

@media (forced-colors: active) {
  .knock { forced-color-adjust: none; background: Canvas; color: CanvasText;
           box-shadow: 0 0 0 2px CanvasText; }
  [data-state="ready"] .knock, [data-state="closed"] .knock, [data-state="sent"] .knock {
    background: ButtonFace; color: ButtonText; }
  .wave { stroke: CanvasText; }
  .room-swatch { forced-color-adjust: none; box-shadow: 0 0 0 1px CanvasText; }
}

/* ── The sign sheet ─────────────────────────────────────────────────────── */

/* A poster: read at arm's length and from across a corridor, so everything on it is centred
   except the guard line, which keeps its rule and its left edge on paper as on screen. */
.sign > :not(.guard) { text-align: center; }

/* The sentence somebody standing at the door reads, and the loudest thing on the sheet after the
   wordmark. */
.sign-lead { margin: 0; font: 500 var(--t-title2)/1.3 var(--face-display); color: var(--ink); }

/* Everything the path decides — both symbols and the code under them — in one block, because a
   sheet opened at a link with no room in it has to lose them together. */
.sign-sheet { display: flex; flex-direction: column; gap: var(--flow); }

.sign-qr { margin: 0; }                 /* figure's UA margin, which fights the column's rhythm */
/* The quiet zone is inside the symbol, so nothing may crop it, round it or sit on it: a QR with
   no margin around it does not scan. @media print pins these at 40mm; on screen they take
   whatever the column can spare. */
.sign-qr img { display: block; margin-inline: auto; inline-size: min(100%, 12.5rem); block-size: auto; }
.sign-qr figcaption { margin-block-start: var(--sp-2);
                      font: var(--t-footnote)/1.5 var(--face-body); color: var(--muted); }

/* Label, code, hostname: the way in for anyone who would rather type than scan. Plain text and
   not a link, because this block is printed far more often than it is clicked. */
.sign-code { display: flex; flex-direction: column; gap: var(--sp-2); }
.sign-code p { margin: 0; }
.sign-type { font: var(--t-callout)/1.4 var(--face-body); color: var(--ink); }

/* Addressed to the owner at the screen, so @media print takes it off the paper it is talking
   about. */
.sign-hint { margin: 0; font: var(--t-footnote)/1.4 var(--face-body); color: var(--muted); }

/* No code in the path, which is a link somebody edited rather than a room that went away. It says
   the little it can see and leaves the sheet blank, rather than printing half a sign. */
.sign-miss { margin: 0; font: var(--t-body)/1.5 var(--face-body); color: var(--muted); }

/* ── Print ──────────────────────────────────────────────────────────────── */

/* The sign sheet is the only thing anybody prints, and its two rules land on classes that arrive
   with sign.html. They sit here because the print ground and the card reset are this file's job
   whichever document is on the paper. */
@media print {
  :root { color-scheme: light; }
  body { background: #fff; }                /* white paper, not paper-the-token */
  .page { border: 0; box-shadow: none; max-width: none; }
  .sign-hint { display: none; }
  .sign-qr img { width: 40mm; height: 40mm; }
}
