/* sieve.css — route-local styles for the solver pages.
   Loaded by: `/word-unscrambler/`.

   EXTERNAL BECAUSE THE POLICY IS. sites/d9-word-solvers/_headers pins
   style-src to 'self' with no 'unsafe-inline', so there is no <style> block and
   no style= attribute anywhere on this site — page CSS lives here for the same
   reason page JS lives in an external file. tests/structure-d9.js asserts both
   forms are absent from the markup, which is what keeps the policy satisfiable
   rather than something everyone works around.

   NO NEW TOKENS. Every colour, face and radius below is a var() out of
   kit/base.css. CLAUDE.md: the design tokens are law — instrument gray-white
   paper, charcoal ink, carbon blue accent, mono for all readouts and data, and
   pass/fail green/red reserved for actual pass/fail states. Nothing here
   invents a colour, and the one place a "found/not found" distinction appears
   it uses the kit's own .r-value.pass / .r-value.fail on the readout, which IS
   an actual pass/fail state: the dictionary either contains words for this rack
   or it does not. */

/* ---------- The rack entry ---------- */

.rack-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
  margin: 0 0 0.5rem;
}

/* Mono and tracked, because the thing being typed is a set of tiles rather
   than a word — the letters must read as separable. Uppercase is a DISPLAY
   transform only; sieve.js lowercases whatever arrives, so what the visitor
   sees and what the engine reads never disagree about case. */
.rack-input {
  font-family: var(--mono);
  font-size: 1.25rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  width: 18ch;
  max-width: 100%;
}

.rack-input::placeholder {
  letter-spacing: 0.08em;
  text-transform: none;
  color: var(--ink-soft);
  opacity: 0.75;
}

.rack-hint {
  font-size: 0.86rem;
  color: var(--ink-soft);
  margin: 0.2rem 0 0.9rem;
}

/* ---------- Messages ---------- */

/* The container the engine's refusals land in. kit/base.css styles the
   individual .note; this is only the region around them, and it exists as a
   class of its own because it is also the announced region (role="status" in
   the markup) — every refusal this site can produce is rendered here, so the
   element has to be present and empty rather than created on demand. */
.notes:empty { margin: 0; }
.notes { margin: 0.4rem 0 0; }

/* ---------- Results ---------- */

.results {
  margin: 0.9rem 0 0;
}

/* One block per word length, longest first. The heading is the instrument
   label for the block, so it takes the same mono/uppercase/tracked treatment
   as .eyebrow and .r-label rather than a new one. */
.len-group {
  border-top: 1px solid var(--line);
  padding: 0.7rem 0 0.2rem;
}

.len-group:first-child { border-top: 0; }

.len-head {
  font-family: var(--mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-soft);
  margin: 0 0 0.45rem;
}

.len-head .len-count { color: var(--ink); }

.word-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 0.75rem;
  margin: 0 0 0.6rem;
  padding: 0;
  list-style: none;
  font-family: var(--mono);
  font-size: 0.95rem;
}

.word-list li { white-space: nowrap; }

/* Every word carries .word as well, and it is a rule rather than a bare hook:
   the class is what page.js and the suites address a result by, and
   tests/orphan-classes.js requires a class in shipped markup to have a rule
   in a stylesheet the page loads. The declaration is the inherited value made
   explicit rather than a second appearance of the same styling. */
.word { font-family: var(--mono); }

/* The cap line. It is styled as a note rather than as small print because a
   truncated list that does not say it is truncated is the same defect class as
   a solver that invents a word: the page would be showing a partial answer as
   if it were the whole one. */
.more {
  font-size: 0.88rem;
  color: var(--ink-soft);
  margin: 0.2rem 0 0.9rem;
}

/* ---------- The index disclosure ---------- */

/* The lazily-loaded shards are the one thing this site fetches, and their size
   is disclosed on the page rather than buried. Mono, because it is a
   measurement. */
.disclosure {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--ink-soft);
  letter-spacing: 0.02em;
  margin: 0.9rem 0 0;
}

/* ---------- Working state ---------- */

/* No spinner, no animation: a shard parse is tens of milliseconds and a
   moving element would need a prefers-reduced-motion branch to say nothing.
   The word is the state. */
.working {
  font-family: var(--mono);
  font-size: 0.86rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin: 0.6rem 0;
}

/* ---------- A3.11: zero CLS across a solve ---------------------------------
   CLAUDE.md's build standard says Zero CLS, flat — that is the standing
   ruling the review's "layout decision" was waiting on. The review measured
   CLS 0.1098 at 1280×800 across a throttled solve (the shard fetch landing
   ~14 s after the click, outside the recent-input window), so every region
   JavaScript writes after load keeps constant geometry. Five regions, all
   figures measured 2026-08-16 (probe table in the build doc's A3.11
   close-out; 375 is the house mobile floor).

   1. The readout strip: geometry must not depend on what the values say.
      Each variable value gets a box sized to its widest possible content —
      the rack input is maxlength=15 (15ch), "Longest" prints "NN letters"
      (10ch), the count is comma-grouped out of a 168,548-word index (7ch).
      With every item's width constant the flex wrap is identical for every
      value; it measured 123→96 px at 375×812 before the boxes. */
#r-rack, #r-words, #r-longest { display: inline-block; }
#r-rack { min-width: 15ch; }
#r-words { min-width: 7ch; }
#r-longest { min-width: 10ch; }

/* 2. "sieving…" appears on the click but clears when the solve lands —
      outside the input window on a slow network. The line keeps its height
      (22 px at every tested width) across both states. */
.working { min-height: 1.375rem; }

/* 3. Refusals OVERLAY the reserved results panel instead of taking flow
      space: absolute positioning takes .notes out of the flow (aria-live
      role and DOM position untouched), so a no-words verdict landing 14 s
      late displaces nothing. Every refusal path clears the results first,
      so a note only ever paints over the EMPTY panel, and the panel
      (288 px) is taller than the tallest refusal (159 px at 375×812). The
      1rem insets mirror .tool-panel's own padding in kit/base.css —
      structure-d9 asserts the pairing so a kit repad cannot silently skew
      the overlay. */
.tool-panel { position: relative; }
.notes { position: absolute; left: 1rem; right: 1rem; margin: 0.4rem 0 0; }

/* 4. The results viewport is reserved from first paint and scrolls inside
      itself. scrollbar-gutter keeps the words from reflowing the day the
      scrollbar appears; the border is always drawn — dashed while idle so
      the reservation reads as an intentional instrument panel, transparent
      when filled — so the box never changes size. */
.results {
  height: 18rem;
  overflow-y: auto;
  scrollbar-gutter: stable;
  border: 1px dashed var(--line);
  border-radius: 4px;
}
.results:not(:empty) { border-color: transparent; }

/* 5. The disclosure grows by a wrapped line when the pull figure replaces
      the idle sentence. Reserved at the tallest of its two states per width
      band, measured at 375/480/600/700/820/900/1024/1280 — blank space is
      the accepted cost of the ruling, movement is not. */
.disclosure { min-height: 3.75rem; }                                /* >=820: 60px */
@media (max-width: 819px) { .disclosure { min-height: 5rem; } }     /* 80px */
@media (max-width: 599px) { .disclosure { min-height: 6.25rem; } }  /* 100px */
@media (max-width: 479px) { .disclosure { min-height: 8.75rem; } }  /* 140px */
