/* app.css — everything that is not a colour.
 *
 * COLOURS COME FROM tokens.css AND NOWHERE ELSE. That file is generated from
 * palettes/solve-ent.json, which is the file the hub's palette gate measures —
 * so a hex code written here would be a colour nothing has checked. There are
 * none, and `tools/a11y.mjs` refuses one.
 *
 * Sizes are in rem against a base the reader controls, so "large" is genuinely
 * larger and not a scale factor applied to something already fixed in pixels.
 */

:root {
  --base: 1.0625rem;      /* 17px, which is the smallest this asks anybody to read */
  --tap: 44px;            /* the floor for anything a finger has to hit */
  --gap: 1rem;
  --measure: 34rem;       /* a line length that can be read without losing the place */
  --radius: 10px;
  --track: 0em;
  --leading: 1.55;
}

:root[data-text-size='large']   { --base: 1.25rem; }
:root[data-text-size='largest'] { --base: 1.5rem; }

/* Letter and line spacing, for readers who need it and nobody who does not. */
:root[data-spacing='open'] { --track: 0.06em; --leading: 1.9; }

/* AND SWITCHED OFF WHERE THE SETTING WOULD WORK BACKWARDS.
 *
 * Loosened tracking helps a sentence and hurts a string that is read character
 * by character — an equation, a number being typed, a value written down, a
 * report copied by hand. MoleBridge reached this first and states it plainly:
 * those are read one character at a time, and loosening them makes them harder
 * rather than easier, which is the whole setting backwards.
 *
 * ONLY WHERE THE WHOLE ELEMENT IS THE STRING. The equations inside a question's
 * prose — "n × M = m relates moles, molar mass and mass" — are NOT covered, and
 * that is deliberate rather than missed: the engine produces that sentence as
 * plain text, and wrapping its symbols would put presentation inside the part of
 * this app that must not carry any. The completion code needs no entry here; it
 * sets its own tracking and never reads `--track`. */
:root[data-spacing='open'] .choice,
:root[data-spacing='open'] .wrote,
:root[data-spacing='open'] .calc-result,
:root[data-spacing='open'] .diagnostic,
:root[data-spacing='open'] input[type='text'] {
  letter-spacing: normal;
  word-spacing: normal;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: linear-gradient(180deg, var(--page), var(--page-alt));
  background-attachment: fixed;
  color: var(--text);
  font: var(--base)/var(--leading) system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  letter-spacing: var(--track);
  min-height: 100svh;
  /* A TAIL, so `scroll-margin-top` can actually be honoured. A browser cannot
     scroll past the end of the document, so an element near the bottom lands
     wherever the last scroll position puts it — under the sticky bar, with the
     margin ignored because there is nowhere further to go. The floor is the
     bar's height with air, which is what the diagnosis needed. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right) max(env(safe-area-inset-bottom), 5rem) env(safe-area-inset-left);
}

/* ---------- focus ----------
 * One rule, on :focus-visible, and it is never removed anywhere. A 3px outline
 * with an offset, so it is visible against both the surface it sits on and the
 * control's own edge. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ---------- the skip link ----------
 * Off-canvas until focused, which is a KEYBOARD route. It is not the only route
 * to the work: the work is the first thing under the bar and a finger reaches
 * it by looking at it. A control nobody can reach is worse than a missing one. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--surface-raised);
  color: var(--text);
  padding: 0.75rem 1rem;
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  z-index: 10;
}
.skip:focus { left: 0.5rem; top: 0.5rem; }

main:focus { outline: none; }

/* ---------- the bar ---------- */
/* THE TOOLS GO WHERE THE READER GOES.
 *
 * The bar was static, so the moment anything scrolled — and a wrong answer
 * scrolls the page to the diagnosis on purpose — the calculator and the report
 * went off the top with it. `tools/fold-check.mjs` measured the calculator at
 * 533px above the screen at the exact moment a reader has just got something
 * wrong and most wants to work something out. Putting a control in the chrome
 * only makes it always-available if the chrome is always there.
 *
 * IT NEEDS ITS OWN BACKGROUND. The body is a gradient, so a sticky bar with a
 * transparent background has the page scrolling visibly THROUGH it. `--page-alt`
 * is the gradient's own far end, so the bar reads as part of the same surface
 * rather than as a panel laid over it.
 *
 * The cost is about 52px of a 380px screen, and it is worth it: that screen is
 * where both of this app's reachability defects lived. */
.bar {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--page-alt);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--hairline);
}
/* THE RUNNING VERSION (Doctrine §7b). A serial number, not a badge: quiet, and
   dimmed with a colour token rather than opacity so the contrast gate measures
   what a reader actually sees. Tabular figures so it does not wobble between
   releases, and selectable so it can be pasted instead of transcribed. */
.stamp {
  color: var(--text-3);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  user-select: text;
  margin-inline-start: 0.5rem;
  margin-inline-end: auto;
}

.wordmark {
  /* The level-one heading. It is the app's name, which is what the page is,
     and every page needs one — axe is right about that and a decorative span
     was not doing the job. */
  font-size: 1em;
  font-weight: 650;
  letter-spacing: 0.01em;
  margin: 0;
}

/* ROOM FOR THE BAR WHEN SOMETHING SCROLLS ITSELF INTO VIEW.
 *
 * `focus()` and `scrollIntoView` put an element at the top of the viewport,
 * which under a sticky header means BEHIND it. The diagnosis landed with its
 * first 57px covered — on screen by arithmetic, invisible to a reader, and in
 * an app whose thesis is attribution that is the product being hidden by the
 * fix that was meant to keep the tools reachable. `scroll-margin-top` is the
 * one-property answer, and it belongs on everything anything ever scrolls to.
 *
 * The value is the bar's height plus a little air, expressed the same way the
 * bar is built so the two move together. */
:target,
#main,
#diagnosis,
#drill-diagnosis,
#entry,
#drill-entry,
#working,
#run-note {
  scroll-margin-top: calc(var(--tap) + 1.6rem);
}

.icon {
  min-width: var(--tap);
  min-height: var(--tap);
  display: grid;
  place-items: center;
  font: inherit;
  font-weight: 700;
  font-style: italic;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--rail);
  border-radius: 50%;
  cursor: pointer;
}
/* A DRAWN GLYPH ON A BUTTON STYLED FOR A LETTER. The (i) is an italic bold
 * character and the calculator is a picture, so the picture carries its own
 * size and inherits the colour — `currentColor` means the a11y gate reverse-maps
 * it to the same text token as everything else rather than meeting a stroke
 * colour it has never heard of. */
.icon-glyph {
  width: 1.15rem;
  height: 1.15rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon:hover { background: var(--surface-raised); }
.icon:active { background: var(--surface-pressed); }

/* ---------- a newer version is ready ----------
 * A STRIP, not a dialog. It sits under the bar and pushes the page down rather
 * than covering it, so nothing a reader is part-way through is hidden behind it
 * and nothing has to be dismissed before answering a question.
 *
 * The inset matches `main`. It has to: this block was omitted once by a silent
 * no-op edit and the strip rendered flush to both screen edges while every other
 * word on the page was indented — and every gate stayed green, because contrast,
 * target size, landmarks and axe are all perfectly happy with text touching the
 * bezel. `tools/a11y.mjs` measures the inset now. */
.update-strip {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 0.85rem 0.9rem;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--rail);
}
.update-strip p { margin: 0 0 0.6rem; }
.update-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* ---------- layout ---------- */
main {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 1.25rem 0.9rem 4rem;
}

h2 {
  font-size: 1.35em;
  line-height: 1.25;
  margin: 0 0 0.75rem;
}
/* The topic names are written lowercase in the engine, where they are read
   mid-sentence. As a heading that looks like a mistake. Capitalised HERE, in the
   presentation, rather than by changing data that other sentences depend on —
   and only the first letter, since `capitalize` would title-case every word. */
h2::first-letter { text-transform: uppercase; }
h2:focus { outline: none; }
h3 { font-size: 1.1em; margin: 1.5rem 0 0.4rem; }
p { margin: 0 0 0.85rem; }
.aside { color: var(--text-3); font-size: 0.94em; }

/* ---------- controls ---------- */
button {
  font: inherit;
  letter-spacing: inherit;
  min-height: var(--tap);
  padding: 0.6rem 1rem;
  border-radius: var(--radius);
  cursor: pointer;
}

.primary {
  background: var(--accent-soft);
  color: var(--text);
  border: 1px solid var(--rail);
  font-weight: 600;
}
.primary:hover { background: var(--surface-raised); }
.primary:active { background: var(--surface-pressed); }

/* `hidden` HAS TO MEAN HIDDEN, and in this sheet it did not.
 *
 * The user agent hides the attribute with `[hidden] { display: none }` from its
 * OWN stylesheet, which any author rule setting `display` beats regardless of
 * specificity — so `.ghost { display: inline-flex }` below quietly made the
 * attribute inert on every ghost button in the app. The calculator's "put this
 * in the answer" control was set hidden on a step with nowhere to put a number
 * and painted anyway, directly above the sentence explaining that it was not
 * being offered.
 *
 * `!important` is right here rather than sloppy: this is the one declaration
 * that must outrank every layout rule in the file, and the alternative is
 * remembering, at each new control, that hiding it needs a class as well. */
[hidden] { display: none !important; }

/* Also used on links. `min-height` does nothing to an inline box, so this is
   inline-flex — a 138x20 link was the target the gate caught. */
.ghost {
  display: inline-flex;
  align-items: center;
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  min-height: var(--tap);
  padding: 0.6rem 1rem;
  text-decoration: none;
}
a.ghost { color: var(--accent); }
.ghost:hover { background: var(--surface); }

/* ---------- choosing a topic, or a move ---------- */
.topics { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.6rem; }
/* A control that FOLLOWS the list needs the same gap the list has inside it.
   Without this, "Go back to whole questions" sat flush against the last move,
   reading as a seventh option. The inset check cannot see this — it measures
   where words start horizontally, and this is vertical. */
.topics + button { margin-top: 1.1rem; }
.topic {
  width: 100%;
  text-align: left;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--rail);
}
.topic:hover { background: var(--surface-raised); }
.topic:active { background: var(--surface-pressed); }

/* ---------- the question ---------- */
.question {
  background: var(--surface);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  margin-bottom: 1rem;
}
.question-body { margin: 0; }
.question .ghost { margin-top: 0.25rem; }

/* ---------- what the reader has already written on this question ----------
 *
 * A RAIL AND A HEADING, like the diagnosis, and for the same reason: what this
 * block is has to be readable to somebody who sees no colour at all. Every
 * value in it is something the reader typed and the grader accepted, so it is a
 * record of their own work rather than anything the app is handing them. */
.working {
  background: var(--surface);
  border: 1px solid var(--rail);
  border-left-width: 4px;
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}
.working h3 { font-size: 0.95em; color: var(--text-2); margin: 0 0 0.4rem; font-weight: 600; }
.working ol { margin: 0; padding-left: 1.2rem; }
.working li { margin-bottom: 0.25rem; }
.working li:last-child { margin-bottom: 0; }
.working .what { color: var(--text-2); }
.working .wrote { font-weight: 600; }

.step-place {
  color: var(--text-2);
  font-size: 0.92em;
  letter-spacing: 0.02em;
  margin-bottom: 0.3rem;
}
.step { font-weight: 600; margin-bottom: 0.35rem; }
.unit-hint { color: var(--text-2); font-size: 0.95em; }

/* ---------- working it out, without leaving the question ----------
 *
 * A KEYPAD RATHER THAN THE SYSTEM KEYBOARD ALONE. This is used on a tablet, by
 * finger, and × and ÷ are two keyboard layers away on most of them — far enough
 * that a reader picks up a phone instead, which is the thing this exists to
 * stop. Every key is on the same `--tap` floor as everything else a finger has
 * to hit, and the field still takes typed input for whoever has a keyboard.
 *
 * IN A DIALOG NOW rather than under the answer box. The rules below stopped
 * needing a wrapper and a body of their own the moment the panel became the
 * wrapper — the disclosure they were laid out for is what a reader could not
 * find. */
/* `[open]` IS LOAD-BEARING. A closed dialog is hidden by the user agent's own
 * `dialog:not([open]) { display: none }`, and an id selector outranks it — so
 * `#calc { display: grid }` would put the calculator on the screen permanently,
 * on top of everything, with no way to close it. */
#calc[open] { display: grid; gap: 0.15rem; }
.calc-result {
  margin: 0.5rem 0;
  min-height: 1.4em;
  color: var(--text-2);
  font-weight: 600;
}
.calc-keys {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0.4rem;
  margin: 0.6rem 0;
}
.calc-key {
  font: inherit;
  min-height: var(--tap);
  min-width: var(--tap);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  background: var(--surface-raised);
  color: var(--text);
  cursor: pointer;
}
.calc-key:active { background: var(--surface-pressed); }

/* ---------- the completion code ----------
 *
 * BIG, SPACED AND GROUPED, because it is copied by hand onto paper and read
 * back by somebody else. The grouping is in the text rather than in the layout
 * so that copying it takes the dashes with it. */
.completion-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 1.5rem;
  letter-spacing: 0.08em;
  word-break: break-all;
  margin: 0.3rem 0 0.6rem;
}

/* ---------- a stack of codes, read back ----------
 *
 * BLOCKS, NOT ROWS. This is read on a tablet with paper beside it, and a grid
 * is where that falls apart first — the columns go off the edge and what is
 * lost is silent. Each code is its own block with its number as the heading, so
 * a finger can find one and the reading order is the paste order. */
.codes { margin: 0.4rem 0 0; padding-left: 1.3rem; }
.codes > li { margin-bottom: 1rem; }
.codes > li:last-child { margin-bottom: 0; }
.code-head { font-weight: 600; margin: 0 0 0.2rem; }
.codes ul { margin: 0; padding-left: 1.1rem; color: var(--text-2); }
/* A refusal is carried by the words, not by a colour — the heading says "did
   not read" whether or not anybody can see the rail beside it. */
.code-refused { border-left: 3px solid var(--rail); padding-left: 0.6rem; }

textarea {
  font: inherit;
  letter-spacing: inherit;
  padding: 0.55rem 0.7rem;
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  width: 100%;
  resize: vertical;
}

/* ---------- answering ---------- */
.entry { display: grid; gap: 0.55rem; margin: 1rem 0; }
.entry-label { color: var(--text-2); font-size: 0.95em; }

/* A SELECT IS PAINTED BY THE BROWSER UNTIL IT IS NOT.
 *
 * Left alone it arrives 19px tall — under half the tap floor — in the system's
 * own greys, which map to no token in this palette and were therefore never
 * measured for contrast in either mode. `appearance: none` is what hands it
 * back, and the arrow has to be drawn again because removing the appearance
 * removes that too. Hub LESSONS §151 is the same defect on a button. */
input[type='text'],
select {
  font: inherit;
  letter-spacing: inherit;
  min-height: var(--tap);
  padding: 0.55rem 0.7rem;
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  width: 100%;
}
select {
  appearance: none;
  /* The arrow, in the ink colour rather than the browser's. `currentColor`
     cannot be used inside a data URI, so this is drawn with a border trick that
     inherits the text colour and therefore both modes. */
  padding-right: 2.2rem;
  background-image: none;
}
.select-holder { position: relative; display: block; }
.select-holder::after {
  content: '';
  position: absolute;
  right: 1rem;
  top: 50%;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--text-2);
  border-bottom: 2px solid var(--text-2);
  transform: translateY(-70%) rotate(45deg);
  pointer-events: none;
}

.choices { display: grid; gap: 0.6rem; }
.choice {
  width: 100%;
  text-align: left;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--rail);
}
.choice:hover { background: var(--surface-raised); }
.choice:active { background: var(--surface-pressed); }

/* ---------- what happened at this step ----------
 * A RAIL AND A HEADING, not a colour. Whether a step went wrong is carried by
 * the words, so this reads the same to somebody who sees no colour at all. */
.diagnosis {
  background: var(--surface-raised);
  border: 1px solid var(--rail);
  border-left-width: 4px;
  border-radius: var(--radius);
  padding: 0.85rem 1rem;
  margin-top: 1rem;
}
.diagnosis h3 { margin-top: 0; font-size: 1.05em; }
.why { font-weight: 600; }
.remedy { margin: 0.5rem 0 0; color: var(--text-2); }
.remedies { margin: 0.5rem 0 0; padding-left: 1.2rem; color: var(--text-2); }
.remedies li { margin-bottom: 0.3rem; }
/* THE NEXT ACTION, and it is the last thing in the panel because it is the
   thing a reader leaves the panel to do. */
.next { margin-top: 0.7rem; font-weight: 600; }

.run-note {
  margin-top: 1rem;
  padding: 0.85rem 1rem;
  background: var(--surface);
  border: 1px dashed var(--rail);
  border-radius: var(--radius);
}
.run-note p { margin: 0; }

.closing { margin: 0 0 1.25rem; padding-left: 1.2rem; }
.closing li { margin-bottom: 0.5rem; }

/* ---------- dialogs ---------- */
dialog {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  padding: 1.1rem 1.1rem 1.4rem;
  max-width: min(38rem, calc(100vw - 1.5rem));
  max-height: min(90svh, calc(100svh - 2rem));
  overflow-y: auto;
  margin: auto;
}
dialog::backdrop { background: rgb(0 0 0 / 0.55); }
dialog h2 { margin-top: 0; }
dialog fieldset {
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  margin: 0 0 0.85rem;
  padding: 0.6rem 0.75rem 0.75rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
dialog legend { color: var(--text-2); font-size: 0.92em; padding: 0 0.3rem; }
dialog fieldset button {
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--rail);
}
dialog fieldset button[aria-pressed='true'] {
  background: var(--accent-soft);
  font-weight: 650;
}
/* A CHECK MARK, so "on" is not carried by the fill alone. */
dialog fieldset button[aria-pressed='true']::before { content: '\2713\00a0'; }
dialog ul { padding-left: 1.2rem; }
dialog li { margin-bottom: 0.35rem; }
dialog a { color: var(--accent); }

.releases { list-style: none; padding: 0; }
.releases > li { margin-bottom: 1.4rem; }
.releases h3 { margin-top: 0; }
.still-missing { color: var(--text-2); }

.diagnostic {
  font: 0.9em/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: var(--surface-raised);
  border: 1px solid var(--rail);
  border-radius: var(--radius);
  padding: 0.7rem 0.8rem;
  white-space: pre-wrap;
  word-break: break-word;
  overflow-x: auto;
}

/* ---------- for a screen reader only ---------- */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
