/* knocklet.css
   The palette, the two faces, and the components every Knocklet screen is built from. A screen
   file adds layout; nothing outside this file spells a colour, a face or a radius.

   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.

   THE PALETTE, by job.
     Ground, back to front — --bg, the page; --surface, cards, bands and fields; --wash, the warm
       tint a waiting card carries.
     Type, loudest first — --ink, --ink2 for secondary copy, --ink3 for marks nobody has to read.
     Rules — --line, a divider meant to be seen; --hairline, one that only has to separate.
     State, one meaning each — --accent (brass) is somebody waiting, --ack (slate blue) is
       somebody answered. There is no third state colour, and nothing in the palette means
       trouble: a failure says so in words.
     Controls — --btn-bg / --btn-fg / --btn-line dress the pill, --switch-on / --switch-off a
       switch.
     Identity — --room, one hue per room, written by the console via CSSOM.

   THE COMPONENTS, so a screen composes instead of restyling.
     .btn + .btn--primary | .btn--ghost | .btn--plain | .btn--sm   the pill, 44px tall
     .card, .card--waiting                                         the 14px panel
     .eyebrow                                                      the label over a block
     .ticker                                                       mono figures: codes, clocks
     .wordmark + .mark, .wordmark--start                           the mark and the name
     .field, .field--code, .hint                                   text entry
     .foot, .guard, .theme                                         the band every document ends on
     .page, .page > .spacer, .room-swatch                          the column the button page sits in

   THE SCREENS, each in its own file on top of this one: presser/presser.css, landing.css,
   console/console.css. A rule that names one screen's element or one screen's data-state belongs
   there, not here — this file is what the three have in common. */

/* ── The faces ──────────────────────────────────────────────────────────── */

/* Self-hosted rather than fetched from a font host: the presser page promises there is nothing to
   install and nobody watching, and a webfont on somebody else's origin is a third party on it.
   swap everywhere, because the sentence under the button has to be readable before a face lands.
   The italic is a static 400 — the product has almost no italic and a second variable file would
   cost a third of the type budget to carry one. */
@font-face {
  font-family: 'Work Sans';
  src: url('/fonts/work-sans-var.woff2') format('woff2');
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Work Sans';
  src: url('/fonts/work-sans-italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/fonts/plex-mono-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/fonts/plex-mono-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

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

/* Every token is a light-dark() pair, so the two appearances are one line apart and neither can
   be edited without the other in view. What picks between them is `color-scheme` and nothing
   else: the reader's own answer is [data-theme] on <html> (theme.js writes it before the first
   paint), and the two rules under this block re-answer it with one declaration instead of a
   second copy of the palette. @media print does the same, which is what keeps a page printed
   from a dark machine on white paper in dark ink. */
:root {
  color-scheme: light dark;

  /* Ground, back to front */
  --bg:               light-dark(#F2F2F0, #19242D);
  --surface:          light-dark(#FAFAF8, #202C36);
  --wash:             light-dark(#F3EDE2, #2A333A);

  /* Type. The ratios are read against --bg light / --surface dark, which is where each one
     spends its life. */
  --ink:              light-dark(#1A2630, #EBE7E0);
  --ink2:             light-dark(#55636D, #A2ABB0);   /* 5.52:1 light, 6.75:1 dark */
  --ink3:             light-dark(#8A949B, #77828A);   /* 2.96 / 3.62 — marks and meta, never a sentence */
  /* Palette.emphasisRule, mixed off the ink rather than spelled beside it: emphasis is type at a
     lower weight, and a divider colour disappears in that job. */
  --emphasis-rule:    color-mix(in srgb, var(--ink) 35%, transparent);

  /* Rules */
  --line:             light-dark(#DEDDD7, #2C3843);
  --hairline:         light-dark(#EAE9E4, #24303A);

  /* State. Two accents, one meaning each. Stated in oklch, which is how they were chosen: the
     lightness is the term being held level between the schemes and hex hides it. The colorsets
     carry the sRGB each one resolves to, and design-tokens.test.js converts to compare. */
  --accent:           light-dark(oklch(0.56 0.09 65), oklch(0.79 0.06 72));    /* brass: 4.54:1 light, 7.31:1 dark */
  --ack:              light-dark(oklch(0.52 0.05 215), oklch(0.75 0.035 215)); /* slate blue: 5.14:1 light, 6.48:1 dark */

  /* Controls */
  --btn-bg:           light-dark(#1A2630, #35434E);
  --btn-fg:           light-dark(#F2F2F0, #EFECE5);   /* 8.62:1 on --btn-bg dark */
  --btn-line:         light-dark(#1A2630, #4A5964);
  --switch-on:        light-dark(#1A2630, #C7CFD4);
  --switch-off:       var(--line);   /* --line by another name: an off track is a groove, not a state */

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

/* The reader's answer, and the whole of what an override does. Native controls — the settings
   form's time and date fields, a select — read color-scheme too, so a chosen scheme has to move
   this rather than the tokens or the console ends up with light steppers on a dark panel. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

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

:root {
  /* Work Sans for everything that is words, IBM Plex Mono for everything that is a figure: a join
     code, a clock time, an elapsed count. Nothing else gets a face. */
  --face:      'Work Sans', system-ui, sans-serif;
  --face-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, 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 */
  /* One step above the phone's largest, and the only one: the landing's question and a legal
     document's title are read at a desk, where 34pt is a heading rather than an opening. */
  --t-display:     clamp(2.125rem, 5vw, 3.25rem);
  --t-title1:      1.5625rem;  /* 25 */
  --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);
  color: var(--ink);
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
}

/* The mark and the name, one block:
   <h1 class="wordmark"><svg class="mark" …></svg> Knocklet</h1>
   The mark is an inline SVG whose one path is punched, so it draws in currentColor and the button
   is a hole: the fill falls out of the block's own type colour, nothing has to restate it, and the
   mark can never take a hue. */
.wordmark { display: flex; align-items: center; justify-content: center; gap: .5em; margin: 0;
            font: 500 var(--t-title2)/1.1 var(--face); letter-spacing: -.02em; color: var(--ink); }
.wordmark--start { justify-content: flex-start; }
/* Sized off the type it stands beside rather than off its width/height attributes, so the pair
   scales together: 1.55em is the mock's 34 against 22. */
.mark { flex: none; inline-size: 1.55em; block-size: 1.55em; }

/* 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. --ink2 rather than the mock's --ink3, which is a 2.96:1 mark and this is a label
   somebody has to read. */
.eyebrow { font: 500 var(--t-caption)/1.3 var(--face); letter-spacing: .12em;
           text-transform: uppercase; color: var(--ink2); margin: 0 0 var(--sp-2); }

/* Every figure on every screen: elapsed counts, clock times, join codes. */
.ticker { font-family: var(--face-mono); font-variant-numeric: tabular-nums; }

/* JoinCode. Tracking is stated rather than inherited, because it does not scale with the size a
   screen sets: the console's share block draws the same class at --t-large-title. */
.join-code { font: 500 var(--t-callout) var(--face-mono); letter-spacing: .156em; color: var(--ink2); }

/* ── 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;    /* a width, not a colour — the hairline colour is --hairline */

  --gutter:  clamp(var(--sp-4), 5vw, var(--sp-6));
  --column:  26rem;
  /* The measure a full-bleed band holds its content to. Every document sets its own — the legal
     pages read at 42rem, the console runs to its own column — and the band itself always reaches
     both edges of the window. */
  --band:    64rem;

  /* 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);
}

.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.
   Only where a `.page` is the document — the button and the sign sheet, which are a phone and a
   sheet of paper whatever they are opened on. The landing, the legal pages and the console are
   documents rather than columns and lay themselves out full-bleed. */
@media (min-width: 48rem) {
  body:has(> .page) { --flow: var(--sp-5); 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; }
}

/* ── Components ─────────────────────────────────────────────────────────── */

/* The pill, one shape at three weights. --primary is the filled one and a screen has at most one;
   --ghost is the hairline one and carries everything else; --plain drops the stroke for the third
   choice in a row, where a third outline would read as a pair plus an afterthought. */
.btn {
  box-sizing: border-box;
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-block-size: 44px;                  /* the target floor, and it is a floor */
  padding: 11px 22px;
  font: 500 var(--t-callout)/1.2 var(--face);
  text-align: center; text-decoration: none;
  color: var(--ink); background: none;
  border: var(--hair) solid transparent; border-radius: var(--r-pill);
  cursor: pointer; appearance: none;
  -webkit-tap-highlight-color: transparent;
}
.btn:active { opacity: .82; }
.btn:focus-visible { outline: 2px solid var(--switch-on); outline-offset: 3px; }
/* Both spellings. A form control that is genuinely unavailable takes `disabled`; anything that
   still has to say why it will not act takes `aria-disabled`, which keeps it in the focus order —
   a control that drops out of it moves a reader somewhere they did not ask to be. */
.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--hairline); color: var(--ink3); border-color: var(--hairline); cursor: default;
}

.btn--primary { background: var(--btn-bg); color: var(--btn-fg); border-color: var(--btn-line); }
.btn--ghost   { border-color: var(--line); color: var(--ink); }
.btn--plain   { color: var(--ink2); }
.btn--sm { min-block-size: 38px; padding: 8px 16px; font-size: var(--t-subheadline); }

/* The panel everything on the console sits in. */
.card {
  box-sizing: border-box;
  background: var(--surface);
  border: var(--hair) solid var(--line);
  border-radius: var(--r-card);
  padding: var(--sp-5);
}
/* Somebody is waiting in this one. The warm ground says so and the accent goes on the figure
   inside; a card outlined in a colour reads as a warning, and nothing here is one. */
.card--waiting { background: var(--wash); }

/* KnockField: surface fill, line hairline, switch-on 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);        /* >=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(--switch-on); border-width: 1.5px; padding: 11.5px; }
.field::placeholder { color: var(--ink3); }

/* 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-mono);
  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); color: var(--ink2); }

/* A label that is read but not seen, for a field whose placeholder is the whole of what it wants.
   Nothing a sighted reader needs may hide behind this. */
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

/* ── The presser's own screen rules live in public/presser/presser.css ──── */

/* An author `display` beats the user agent's rule for [hidden], and several blocks on both
   surfaces declare one. Without this, `hidden` is a no-op on exactly the elements a script
   toggles most. */
[hidden] { display: none !important; }

/* ── Room identity ──────────────────────────────────────────────────────── */

/* One hue per room, and the only place it is allowed to appear. PaletteSheet's own move — a line
   stroke over the swatch — so an arbitrary hue can never collapse into either ground. */
.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);
}

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

/* Not for emergencies. Static in the HTML shell, never injected, never behind a toggle. Plain
   type at the foot of the document: Palette.emphasisRule means "notice this sentence" where a
   sentence has just arrived and has to be read now, and a standing term of the product read on
   every page is not that. It is the last line of every document instead, which is a stronger
   claim on the reader than a rule beside one paragraph of small print. */
.guard {
  margin: 0;
  font: var(--t-footnote)/1.5 var(--face);
  color: var(--ink2);
}

/* ── The foot ───────────────────────────────────────────────────────────── */

/* The band every browser document ends on: the landing, both legal pages and the console. A real
   <footer>, full-bleed, holding its own content to --band — which each document sets to its own
   measure, so the wordmark down here lines up with the words above it.
   The order is the name and the way out, then the small print, then the one sentence the product
   is bounded by. Nothing here is a control except the scheme picker. */
.foot {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  padding: var(--sp-8) var(--gutter) calc(var(--sp-7) + env(safe-area-inset-bottom));
  border-block-start: var(--hair) solid var(--line);
}
.foot > * { inline-size: 100%; max-inline-size: var(--band); margin-inline: auto; }

/* Wordmark left, the way out right, stacked until there is room for the pair. */
.foot__top {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--sp-3) var(--sp-6);
}
.foot__top .wordmark { font-size: var(--t-headline); color: var(--ink); text-decoration: none; }
.foot__top .wordmark:focus-visible { outline: 2px solid var(--switch-on); outline-offset: 4px; }

.foot__nav { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-5); }
.foot__link {
  display: inline-flex; align-items: center; min-block-size: 44px;
  font: var(--t-subheadline)/1.4 var(--face); color: var(--ink2); text-decoration: none;
}
.foot__link:hover { color: var(--ink); }
.foot__link[aria-current] { color: var(--ink); }
.foot__link:focus-visible { outline: 2px solid var(--switch-on); outline-offset: 3px; }

/* The small print, under a hairline of its own: the company line, the scheme picker, and the
   sentence that closes every page. The picker is small print too — a preference set once, not a
   control the page is about — so with room it sits at the end of the company line rather than on
   a band of its own, and the guard keeps the last word at full width. */
.foot__fine {
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding-block-start: var(--sp-5);
  border-block-start: var(--hair) solid var(--hairline);
}
@media (min-width: 40rem) {
  .foot__fine { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
  .foot__fine .guard { grid-column: 1 / -1; }
}
/* Who runs this and where to write, side by side with nothing between them: a separator glyph in
   the markup is a word the copy table does not hold. */
.foot__legal { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); margin: 0;
               font: var(--t-footnote)/1.5 var(--face); color: var(--ink3); }
.foot__legal a { color: var(--ink3); }
.foot__legal a:hover { color: var(--ink2); }

/* ── The scheme picker ──────────────────────────────────────────────────── */

/* Three answers, one of them "whatever this machine says", which is the one nothing has to be
   done to keep. theme.js writes the choice to <html> before the first paint and marks the button
   that won; with no script at all the group is inert and the machine's answer stands. */
.theme { display: inline-flex; align-items: center; gap: var(--sp-2); }
.theme__set {
  display: inline-flex;
  padding: 2px;
  border: var(--hair) solid var(--line);
  border-radius: var(--r-pill);
  background: var(--surface);
}
.theme__pick {
  min-block-size: 32px;
  padding: 4px 12px;
  border: var(--hair) solid transparent;
  border-radius: var(--r-pill);
  font: var(--t-caption)/1.3 var(--face);
  letter-spacing: .04em;
  color: var(--ink2);
  background: none;
  cursor: pointer;
  appearance: none;
}
.theme__pick[aria-pressed="true"] { background: var(--wash); border-color: var(--line); color: var(--ink); }
.theme__pick:hover { color: var(--ink); }
.theme__pick:focus-visible { outline: 2px solid var(--switch-on); outline-offset: 2px; }

@media (forced-colors: active) {
  .room-swatch { forced-color-adjust: none; box-shadow: 0 0 0 1px CanvasText; }
  /* The chosen scheme is a fill in the palette and a fill is dropped here, so it is said with a
     stroke as well. */
  .theme__pick[aria-pressed="true"] { border-color: 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); 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); color: var(--ink2); }

/* 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); 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); color: var(--ink2); }

/* 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); color: var(--ink2); }

/* ── 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; }
}
