/* ===========================================================================
   The learning interface · design language
   ---------------------------------------------------------------------------
   The five roles, the five states and the floors of src/learning/contract.ts,
   drawn. Colors, faces, shadows and easings come from public/css/tokens.css,
   which stays the only file in the repository allowed to hold a literal.

   THIS IS NOT THE DEMO CANVAS AND IT DELIBERATELY BORROWS NOTHING FROM IT.
   src/demo/assets/scale.css sizes type for a deck read on a screenshare from
   the back of a room, and src/demo/layout.ts measures a fixed stage and shrinks
   any object that does not fit inside it. Both are right there and wrong here.
   An application is operated at a desk, at whatever zoom its reader needs, and
   the failure the demo's clamp protects against — an object drawn at 0.6 with
   its labels at 5.6px — is exactly what a clamp CAUSES when the viewport is a
   browser window somebody resized. So there is no clamp. When the frame runs
   out of room it reflows, and two named panels scroll.

   WHERE THE NUMBERS COME FROM. The five type roles below are the five floors
   in src/learning/contract.ts, and no size anywhere in this file is under the
   smallest of them: src/learning/contract.test.ts parses the file and fails on
   one that is. The floor is .8125rem. The site's reference pages set tracked
   uppercase mono eyebrows at .58rem, which is 9.3px, and that size is the
   reason this file states a floor at all.
   =========================================================================== */

.ln {
  /* -- type: five roles, largest to smallest ------------------------------
     prose        the question, the result, the inspector body
     prose-quiet  a hint, a definition
     label        the name of a control, in the body face
     eyebrow      tracked uppercase mono: a region name, a state word
     scene-text   a label inside the drawing, at 1:1
     --------------------------------------------------------------------- */
  --ln-prose: 1rem;
  --ln-prose-quiet: .9375rem;
  --ln-label: .875rem;
  --ln-eyebrow: .8125rem;
  --ln-scene-text: .8125rem;

  --ln-lh-prose: 1.55;
  --ln-lh-quiet: 1.5;
  --ln-lh-label: 1.3;
  --ln-lh-eyebrow: 1.25;

  --ln-measure: 68ch;
  --ln-measure-quiet: 74ch;

  /* The case name is a nameplate, in the serif the site sets row titles in. It
     is the one size here that is not a floor, because it is the only thing on
     the screen that is larger than body copy rather than smaller. */
  --ln-case: 1.25rem;

  /* -- spacing: five steps ----------------------------------------------- */
  --ln-1: .35rem;   /* inside one control: a label and its input */
  --ln-2: .6rem;    /* between the parts of one statement */
  --ln-3: 1rem;     /* between rows */
  --ln-4: 1.5rem;   /* between the bands of a region */
  --ln-5: 2rem;     /* between regions */

  /* -- targets ------------------------------------------------------------
     44 for anything a learner submits or resets with; 24 for a marker inside
     a row and for a hit area over a drawing, which is the WCAG 2.5.8 minimum
     and the largest a diagram can carry without the hit areas touching. */
  --ln-target: 44px;
  --ln-target-inline: 24px;

  /* -- motion -------------------------------------------------------------
     Four purposes. Multiplied by --motion, which public/js/motion.js resolves
     before first paint; calm mode sets it to 0 and the travel goes while the
     order stays. Nothing here loops and nothing starts without a gesture. */
  --ln-relate: calc(320ms * var(--motion));
  --ln-change: calc(460ms * var(--motion));
  --ln-attend: calc(160ms * var(--motion));
  --ln-ack: calc(120ms * var(--motion));

  font-family: var(--sans);
  color: var(--ink);
  background: var(--paper);
}

/* ── the frame ──────────────────────────────────────────────────────────────
   Orientation across the top, the scene and the working column beside each
   other, evidence across the foot. `minmax(0, …)` on both tracks and both rows,
   because a grid item defaults to min-content and a drawing wider than its
   column would otherwise push the working column off the screen rather than
   scroll inside its own panel.

   100svh RATHER THAN 100vh: on a phone the dynamic toolbar makes vh taller than
   the window, which puts the submit control under the browser chrome — the one
   place the acceptance criterion for this frame says it may not be.
   ────────────────────────────────────────────────────────────────────────── */
.ln-frame {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(22rem, 26rem);
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--ln-4) var(--ln-5);
  height: 100svh;
  padding: var(--ln-4) var(--ln-5);
  box-sizing: border-box;
}

.ln-orient { grid-column: 1 / -1; }
.ln-scene { grid-row: 2; grid-column: 1; display: grid; grid-template-rows: auto minmax(0, 1fr); gap: var(--ln-3); }
/* THE INSPECTOR GETS A FLOOR AND A CEILING AND THE QUESTION GETS THE REST.
   Both halves are a decision. Without the floor the inspector collapsed to its
   heading, which is the region a learner selects a part in order to read.
   Without the ceiling it took the slack and left the submitted result 38px of
   an 1800px screen, which is a reading panel nobody can read. */
.ln-work { grid-row: 2; grid-column: 2; display: grid; grid-template-rows: minmax(0, 1fr) minmax(9rem, 11rem); gap: var(--ln-4); }

/* `min-width: 0` AND `min-height: 0` ON EVERY REGION, in both layouts. A grid
   item and a flex item both default to min-content in the axis they are laid
   out along, so a drawing wider than its column pushes the working column off
   the screen instead of scrolling inside its own panel, and a long inspector
   grows the frame instead of scrolling. Neither `overflow: auto` on the panel
   nor `minmax(0, 1fr)` on the track is enough on its own; the item's own
   minimum is the third thing, and it is the one that gets forgotten. */
.ln-orient, .ln-scene, .ln-work, .ln-ask, .ln-inspect, .ln-evidence { min-width: 0; min-height: 0; }
.ln-evidence { grid-column: 1 / -1; }

/* ── the region names ───────────────────────────────────────────────────────
   Every region says what it is, in the eyebrow, at the floor. On a real screen
   this is a heading; on the specimen it is also the thing being demonstrated,
   which is why it is not visually hidden. */
.ln-role {
  font-family: var(--mono);
  font-size: var(--ln-eyebrow);
  line-height: var(--ln-lh-eyebrow);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--brass);
  margin: 0 0 var(--ln-2);
}

/* ── type, applied ────────────────────────────────────────────────────────── */
.ln-prose { font-size: var(--ln-prose); line-height: var(--ln-lh-prose); max-width: var(--ln-measure); margin: 0; }
.ln-quiet { font-size: var(--ln-prose-quiet); line-height: var(--ln-lh-quiet); max-width: var(--ln-measure-quiet); color: var(--ink-soft); margin: 0; }
.ln-label { font-size: var(--ln-label); line-height: var(--ln-lh-label); color: var(--ink); }
.ln-figure { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--ln-prose); color: var(--ink); }

/* ── orientation ────────────────────────────────────────────────────────────
   What is fixed and what is currently set. It never changes when a part is
   selected, which is what makes it the thing a learner can look back at. */
.ln-orient__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ln-2) var(--ln-4); }
/* `position: relative` is set below, beside the definition it is the containing
   block for. */
.ln-orient__case { font-family: var(--serif); font-size: var(--ln-case); line-height: 1.2; margin: 0; }
.ln-orient__step { font-family: var(--mono); font-size: var(--ln-eyebrow); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }

/* ── controls ───────────────────────────────────────────────────────────────
   A control's name is beside it, in the body face, at the label floor — never
   in tracked mono at .6rem, which is how the reviewed donor set the four
   controls a learner has to operate.

   THE LABEL IS THE INSTRUCTION AND THE HELP IS A DEFINITION. No required action
   is explained only by the help: a learner who never hovers anything can still
   tell what every control does. */
/* THE LABEL IS BESIDE THE CONTROL AND NOT ABOVE IT. A stacked label is fine in
   a form; in a row of orientation it makes the row two lines tall and puts the
   control below the thing it belongs to, which is one more place a reader's eye
   has to travel to answer "what is this set to". */
.ln-ctl { display: flex; align-items: center; gap: var(--ln-2); }
.ln-ctl__h { display: flex; align-items: center; gap: var(--ln-2); }
.ln-ctl__h label { font-size: var(--ln-label); line-height: var(--ln-lh-label); color: var(--ink); font-weight: 600; }
/* SQUARE, AND SAID HERE RATHER THAN INHERITED. `public/css/base.css` zeroes
   border-radius on `*`, and `public/css/reference.css` states the rule twice:
   no border-radius anywhere, and the one exception in the system is a circle.
   The first version of this file set 2px on the controls, copied from the
   donor, and it showed: the specimen loads tokens and faces and not base.css,
   so nothing was zeroing the browser's own rounding either.

   A stylesheet that is square only because another file reset it is a
   stylesheet that stops being square the first time it is loaded on its own.
   The declaration is explicit on every control this file draws, and
   src/learning/contract.test.ts refuses any radius here that is not 0 or the
   circle. */
.ln-ctl select, .ln-ctl input[type='text'] {
  font: inherit; font-size: var(--ln-label); min-height: var(--ln-target);
  padding: var(--ln-1) var(--ln-2);
  background: var(--card); border: 1px solid var(--rule-2); color: var(--ink); border-radius: 0;
}
.ln-ctl select:hover { border-color: var(--brass); }

.ln-btn {
  font: inherit; font-size: var(--ln-label); font-weight: 600;
  min-height: var(--ln-target); min-width: var(--ln-target);
  padding: var(--ln-2) var(--ln-3);
  background: var(--card); border: 1px solid var(--rule-2); color: var(--ink);
  border-radius: 0; cursor: pointer;
  transition: background var(--ln-ack) linear, border-color var(--ln-ack) linear;
}
.ln-btn:hover { border-color: var(--brass); }
.ln-btn--go { background: var(--brass); border-color: var(--brass); color: var(--on-brass); }
.ln-btn--go:hover { background: var(--brass-hi); border-color: var(--brass-hi); }
.ln-btn[disabled] { opacity: .55; cursor: not-allowed; }

/* ── focus ──────────────────────────────────────────────────────────────────
   The site's ring, not a new one: public/css/base.css sets 2px --brass at 3px
   offset on :focus-visible and this inherits it. What is added here is the SVG
   case, where an outline draws round the whole <a> or <g> bounding box and says
   nothing about which part is focused. */
.ln :focus-visible { outline: 2px solid var(--brass); outline-offset: 3px; }

/* ── help ───────────────────────────────────────────────────────────────────
   A DEFINITION, on hover AND on keyboard focus AND on a tap, with no script.

   The marker is a real button, so it takes focus from the keyboard and takes
   focus from a tap. The reveal is on the group's :hover and :focus-within, for
   the two reasons written beside the rule.

   WHAT IS NOT HERE. Interpretation, sources, and anything a learner has to do.
   A definition is one sentence; PROSE_WORDS.definition is 25 words and the
   specimen test counts them. Everything longer is the inspector's. */
.ln-help { display: inline-flex; }
.ln-help__m {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ln-target-inline); height: var(--ln-target-inline);
  padding: 0; background: none; cursor: pointer;
  /* THE ONE EXCEPTION THE SYSTEM ALLOWS. A circle, and it is a circle because
     that is what a help marker is on every screen a reader has used, not
     because a corner was softened for decoration. */
  border: 1px solid var(--rule-2); border-radius: 50%;
  font-family: var(--mono); font-size: var(--ln-eyebrow); line-height: 1; color: var(--ink-soft);
}
.ln-help__m:hover, .ln-help__m:focus { border-color: var(--brass); color: var(--brass); }

/* THE DEFINITION OPENS ACROSS THE WHOLE ROW rather than beside its own marker,
   which is recovered from the reviewed donor and is the reason there is no
   positioning logic anywhere in this file. A box anchored to a marker near the
   right edge collides with the viewport, and the usual fix is a script that
   measures and flips it. A box that is as wide as the row it belongs to has no
   edge to collide with.

   `display: none` AND NOT `visibility: hidden`. The hidden box still takes part
   in the scroll area, and measured at 384px it needed 481px of window on its
   own. The fade it cost is not one of the four motion purposes in contract.ts:
   nothing is being related, changed, attended to or acknowledged by a
   definition appearing where a pointer already is.

   A screen reader still gets it. `aria-describedby` computes its description
   from the referenced node whether or not that node is displayed, so the
   definition is in the accessible description at all times and on the screen
   only when somebody asked for it. */
.ln-orient__row { position: relative; }
.ln-help__d {
  display: none;
  position: absolute; z-index: 3; left: 0; right: 0; top: calc(100% + var(--ln-2));
  font-size: var(--ln-prose-quiet); line-height: var(--ln-lh-quiet); color: var(--ink-soft);
  background: var(--card); border: 1px solid var(--brass); padding: var(--ln-2) var(--ln-3);
}
/* ON THE GROUP AND NOT ON THE MARKER, so that the definition survives the
   pointer travelling onto it — WCAG 1.4.13 asks for that, and the tooltip is
   inside the group, so one `:hover` covers both. `:focus-within` is the
   keyboard and the tap in one selector: a tap gives the button `:focus` and not
   `:focus-visible`, and "touch can explicitly open it" is the requirement this
   pattern exists for. Tapping elsewhere blurs it closed. */
.ln-help:hover > .ln-help__d,
.ln-help:focus-within > .ln-help__d { display: block; }

/* ── help, with a controller attached ───────────────────────────────────────
   EVERYTHING ABOVE IS THE SCRIPTLESS PATH AND IT STAYS. A frame rendered with
   no controller — the specimen, a page whose script did not arrive — still
   shows a definition on hover, on focus and on a tap, out of CSS alone.

   `src/scene/help.ts` sets `data-help-live` on the root it is attached to, and
   from that moment the decisions below are the controller's rather than the
   selector's. Two things then become possible that a selector cannot do: the
   definition can be placed against the viewport instead of across a row, so a
   term inside a drawing can carry one; and Escape can close it.

   THE HOVER RULES ARE TURNED OFF RATHER THAN LEFT TO RACE. A `:hover` rule and
   a controller both deciding whether a box is displayed is a box that flickers
   at the edge of the grace period, and the one the learner sees is whichever
   the cascade happened to favour.

   POSITIONED AGAINST THE VIEWPORT, WHICH IS WHAT `position: fixed` MEANS HERE.
   The controller writes `left` and `top` in client coordinates, having already
   flipped the side and slid along the edge; `src/scene/overlay.ts` is that
   arithmetic and it is the only place it happens. */
[data-help-live] .ln-help:hover > .ln-help__d,
[data-help-live] .ln-help:focus-within > .ln-help__d { display: none; }

[data-help-live] .ln-help__d {
  position: fixed; left: 0; top: 0; right: auto; z-index: 4;
  /* BORDER BOX, AND SAID HERE RATHER THAN INHERITED. `public/css/base.css` sets
     it on `*`, and this stylesheet is loaded without it by the specimen and by
     the fixture — measured at 420px, a content-box max-width of
     `100vw - 2 * --ln-3` produced a 422px box in a 420px window, which is the
     one failure a viewport-aware placement cannot fix, because the box was
     already wider than the room before anything was placed. */
  box-sizing: border-box;
  width: max-content; max-width: min(28rem, calc(100vw - 2 * var(--ln-3)));
}
/* WEIGHED AGAINST THE RULE ABOVE AND NOT ONLY WRITTEN AFTER IT. Both selectors
   name an attribute, the group and the tooltip, so they carry the same
   specificity and the later one wins. The first version of this block was one
   simple selector lighter, lost the cascade to a `:hover` it had just turned
   off, and the definition never appeared. */
[data-help-live] .ln-help > .ln-help__d[data-shown] { display: block; }
/* PINNED READS AS HELD OPEN. A definition a learner asked for and a definition
   under their pointer are the same sentence, and the difference between them is
   whether moving the pointer away takes it. The channel is the rule down the
   left edge, which is the same channel `--ln-state` uses for a draft: weight
   and a rule are separable from hue, and this screen has already spent hue. */
[data-help-live] .ln-help > .ln-help__d[data-shown='pinned'] { border-left-width: 3px; }

/* ── the definition affordance ──────────────────────────────────────────────
   TWO FORMS, ONE AFFORDANCE. A circle beside a label that is already on the
   screen, and the word itself where the word is in prose. A reader learns "this
   can be explained" once.

   THE TERM FORM IS A BUTTON THAT DOES NOT LOOK LIKE ONE, which is the whole
   reason it is spelled out here: a word in a sentence cannot carry a 24px box
   without breaking the line it is in, so the circle moves to the marker inside
   it and the word keeps a dotted underline. The hit area is still the button,
   and it is still the inline minimum tall. */
.ln-help__m--term {
  width: auto; height: auto; min-height: var(--ln-target-inline);
  gap: var(--ln-1); padding: 0;
  border: 0; border-radius: 0;
  font-family: inherit; font-size: inherit; color: inherit;
}
.ln-help__m--term .ln-help__w { text-decoration: underline dotted; text-underline-offset: .2em; }
.ln-help__m--term .ln-help__i {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--ln-target-inline); height: var(--ln-target-inline);
  border: 1px solid var(--rule-2); border-radius: 50%;
  font-family: var(--mono); font-size: var(--ln-eyebrow); line-height: 1; color: var(--ink-soft);
}
.ln-help__m--term:hover .ln-help__i,
.ln-help__m--term:focus .ln-help__i { border-color: var(--brass); color: var(--brass); }

/* ── the panel ──────────────────────────────────────────────────────────────
   THE RICHER COUNTERPART, AND NOT A BIGGER TOOLTIP. Interpretation, a unit and
   the published page that carries the term are a reading surface: it scrolls,
   it holds a link, and it stays until the learner closes it.

   NONMODAL, AND NOTHING IS TRAPPED. It is inserted immediately after the
   trigger that opened it, so a Tab walks in and another walks out; there is no
   backdrop, because what is under it is still the learner's to operate.

   ITS HEIGHT IS BOUNDED AND IT SCROLLS, which is `docs/LEARNING-INTERFACE.md`'s
   rule for a reading panel applied to one that floats. A long explanation in an
   unbounded floating box is a box taller than the window with its last sentence
   off the bottom of the screen and nothing saying so. */
.ln-help__p {
  position: fixed; left: 0; top: 0; z-index: 5; box-sizing: border-box;
  display: flex; flex-direction: column; gap: var(--ln-2);
  width: min(30rem, calc(100vw - 2 * var(--ln-3)));
  max-height: min(22rem, calc(100vh - 2 * var(--ln-3)));
  overflow: auto; overscroll-behavior: contain;
  background: var(--card); border: 1px solid var(--brass); border-radius: 0;
  padding: var(--ln-3);
}
.ln-help__h { margin: 0; font-size: var(--ln-label); line-height: var(--ln-lh-label); font-weight: 600; }
.ln-help__b, .ln-help__u, .ln-help__s {
  margin: 0; font-size: var(--ln-prose-quiet); line-height: var(--ln-lh-quiet);
  color: var(--ink-soft); max-width: var(--ln-measure-quiet);
}
.ln-help__u b { color: var(--ink); }
.ln-help__s a { color: var(--brass); }
/* A PRIMARY TARGET AND NOT AN INLINE ONE. The marker that opens this is 24px
   because it sits in a dense row; the way out of a panel is a control a learner
   uses deliberately, and TARGETS says 44 for those. */
.ln-help__x {
  align-self: start;
  min-height: var(--ln-target); min-width: var(--ln-target);
  padding: var(--ln-2) var(--ln-3);
  font: inherit; font-size: var(--ln-label); font-weight: 600;
  background: var(--card); border: 1px solid var(--rule-2); border-radius: 0; color: var(--ink);
  cursor: pointer;
}
.ln-help__x:hover { border-color: var(--brass); }

/* ── the scene ──────────────────────────────────────────────────────────────
   THE DRAWING IS NEVER SCALED BELOW 1:1. A viewBox that shrinks takes its text
   with it, and text inside an SVG is not exempt from the floor because a reader
   cannot tell which pixels came from a stylesheet. So the drawing keeps its
   intrinsic size and the panel scrolls sideways under it, which is the one
   horizontal scroll this contract permits and OVERFLOW says so. */
/* `align-self: start` WITH A CEILING. The row is `minmax(0, 1fr)` so that a
   drawing taller than the room gets the room and scrolls inside it; without
   this the panel also STRETCHED to the row when the drawing was short, and a
   480x220 diagram sat in the top-left corner of an 880px empty card. */
.ln-scene__art { align-self: start; max-height: 100%; min-height: 0; overflow: auto; overscroll-behavior: contain; border: 1px solid var(--rule); background: var(--card); padding: var(--ln-3); }
/* BOUNDED ABOVE AS WELL AS BELOW. `width: 100%` alone stretched a 480 unit
   viewBox across an 890px panel, which is a scale of 1.86 and a 13px label
   drawn at 24px: legible, and three sizes larger than the sentence beside it
   asking the question. The floor keeps it from shrinking and the ceiling keeps
   it from becoming the only thing on the screen. */
.ln-scene__art svg { display: block; width: 100%; min-width: 30rem; max-width: 40rem; height: auto; }

.ln-node-l { font-family: var(--sans); font-size: var(--ln-scene-text); fill: var(--ink); }
.ln-node-v { font-family: var(--mono); font-size: var(--ln-scene-text); fill: var(--brass); }
.ln-edge-l { font-family: var(--sans); font-size: var(--ln-scene-text); fill: var(--ink-soft); }

/* The hit area is transparent and 24px, and is not the visible shape. A node
   drawn 18px tall is still a 24px target; an edge drawn as a 1.5px line is a
   24px target because its hit stroke says so. */
.ln-node__hit, .ln-edge__hit { fill: transparent; stroke: transparent; stroke-width: 24; }
/* A marker does not inherit from the element that references it, so its fill is
   named here. A `currentColor` in a <marker> is the arrowhead that silently
   goes black. */
.ln-scene__art marker path { fill: var(--rule-2); }

.ln-node, .ln-edge { cursor: pointer; }
.ln-node rect { fill: var(--paper); stroke: var(--rule-2); stroke-width: 1; transition: stroke var(--ln-attend) linear, stroke-width var(--ln-attend) linear; }
.ln-edge__line { fill: none; stroke: var(--rule-2); stroke-width: 1.5; transition: stroke var(--ln-attend) linear, stroke-width var(--ln-attend) linear; }

/* WHICH PARTS CAN BE OPENED, BEFORE ANYBODY HOVERS ANYTHING. A learner who has
   to sweep a pointer across a drawing to discover it is interactive has already
   been told the drawing is a picture. Recovered from the reviewed donor. */
.ln-node__dot { fill: var(--brass); opacity: .45; transition: opacity var(--ln-attend) linear; }

/* hover — a boundary in --brass and the dot at full opacity. */
.ln-node:hover rect { stroke: var(--brass); }
.ln-node:hover .ln-node__dot { opacity: 1; }
.ln-edge:hover .ln-edge__line { stroke: var(--brass); }

/* focus — the ring, drawn on the shape rather than on the group's bounding box. */
.ln-node:focus-visible, .ln-edge:focus-visible { outline: none; }
.ln-node:focus-visible rect { stroke: var(--brass); stroke-width: 2; stroke-dasharray: 4 3; }
.ln-node:focus-visible .ln-node__dot { opacity: 1; }
.ln-edge:focus-visible .ln-edge__line { stroke: var(--brass); stroke-width: 2; stroke-dasharray: 4 3; }

/* selected — weight, not hue, is the cue that survives being hovered and
   focused at the same time. The dot is filled solid and stays without a
   pointer, so a screenshot of a selection is still a picture of a selection. */
.ln-node[aria-pressed='true'] rect { stroke: var(--brass); stroke-width: 2.5; stroke-dasharray: none; }
.ln-node[aria-pressed='true'] .ln-node__dot { opacity: 1; r: 5; }
.ln-node[aria-pressed='true'] .ln-node-v { fill: var(--brick); }
.ln-edge[aria-pressed='true'] .ln-edge__line { stroke: var(--brass); stroke-width: 2.5; stroke-dasharray: none; }

/* ── the question ───────────────────────────────────────────────────────────
   The prompt, the hint, the draft, the submit, and the result, in that order
   and always in that order. */
/* FIXED BANDS AND ONE READING PANEL. The question, the hint, the options and
   the actions are as tall as they are; the result is whatever the answer needed
   to say, and it is the band that takes the slack and scrolls.

   FLEX AND NOT A GRID TEMPLATE, and the reason is a bug this file shipped for
   an hour. `grid-template-rows: repeat(4, auto) minmax(0, 1fr)` miscounted the
   region's own heading, so the ACTIONS row was given the 1fr, computed to zero
   height, and the submit button was drawn underneath the result. A template
   that has to agree with how many children a region happens to have breaks the
   next time one is added. `flex: 1` on the one band that stretches does not
   care, and tools/specimen-audit.mjs now fails on a band of zero height. */
.ln-ask { display: flex; flex-direction: column; gap: var(--ln-3); }
.ln-ask > * { flex: 0 0 auto; }
.ln-ask__result { flex: 1 1 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: grid; gap: var(--ln-3); align-content: start; }
.ln-ask__prompt { font-size: var(--ln-prose); line-height: var(--ln-lh-prose); max-width: var(--ln-measure); margin: 0; font-weight: 600; }
.ln-ask__opts { display: grid; gap: var(--ln-2); margin: 0; padding: 0; border: 0; }
.ln-ask__opt { display: flex; align-items: center; gap: var(--ln-2); min-height: var(--ln-target); font-size: var(--ln-label); line-height: var(--ln-lh-label); cursor: pointer; }
.ln-ask__opt input { width: 1.1rem; height: 1.1rem; accent-color: var(--brass); }
.ln-ask__acts { display: flex; flex-wrap: wrap; gap: var(--ln-2); }

/* draft and submitted — a rule down the left edge, dashed and solid. The word
   is in the markup beside it and is not decoration: it is the channel that
   works when the rule and the hue have both been spent on something else. */
.ln-state { border-left: 3px solid var(--rule-2); padding-left: var(--ln-3); }
.ln-state__word { font-family: var(--mono); font-size: var(--ln-eyebrow); line-height: var(--ln-lh-eyebrow); letter-spacing: .06em; text-transform: uppercase; display: block; margin-bottom: var(--ln-1); }
.ln-state--draft { border-left-style: dashed; border-left-color: var(--ink-soft); }
.ln-state--draft .ln-state__word { color: var(--ink-soft); }
.ln-state--submitted { border-left-style: solid; border-left-color: var(--closed); }
.ln-state--submitted .ln-state__word { color: var(--closed); }
.ln-state--submitted .ln-miss { color: var(--brick); }

/* ── the answer controls ────────────────────────────────────────────────────
   Five native controls and no invented one. A radio group, a checkbox group, a
   range beside a number field, a select, and a textarea; every rule below is
   about a form element a browser already focuses, announces and operates.

   THE THIRD RULE STYLE IS THE POINT OF `--invalid`. Draft is dashed and
   submitted is solid, which is `STATES` in src/learning/contract.ts. A refused
   draft is a third thing a learner has to be able to tell from both of them,
   and `double` is separable from either in greyscale and on a screenshot — the
   same argument this file makes everywhere for not spending a fourth hue.

   AN UNAVAILABLE OPTION IS DRAWN WITH ITS REASON UNDER IT. The reason is a
   paragraph in the document rather than a `title`, so a keyboard, a
   touchscreen and a screen reader all reach it; the option's input points at it
   with aria-describedby. Fading the label and saying nothing is a screen
   refusing something and declining to say why.
   ────────────────────────────────────────────────────────────────────────── */
.ln-q { display: grid; gap: var(--ln-3); }
.ln-q__prompt { font-size: var(--ln-prose); line-height: var(--ln-lh-prose); max-width: var(--ln-measure); margin: 0; font-weight: 600; padding: 0; }
.ln-q__rule { margin: 0 0 var(--ln-2); }
.ln-q__one, .ln-q__num { display: grid; gap: var(--ln-1); }

.ln-opt { display: grid; gap: 0; }
.ln-opt__no { font-size: var(--ln-prose-quiet); line-height: var(--ln-lh-quiet); color: var(--ink-soft); margin: 0 0 var(--ln-2) calc(1.1rem + var(--ln-2)); max-width: var(--ln-measure-quiet); }
.ln-opt--no .ln-ask__opt { color: var(--ink-soft); cursor: not-allowed; }
.ln-opt--no .ln-ask__opt span { text-decoration: line-through; text-decoration-color: var(--rule-2); }

.ln-q__num-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--ln-2); }
.ln-q__range { flex: 1 1 12rem; min-width: 8rem; min-height: var(--ln-target); accent-color: var(--brass); }
.ln-q__exact { font-size: var(--ln-label); line-height: var(--ln-lh-label); color: var(--ink); font-weight: 600; }
.ln-q__unit { font-family: var(--mono); font-size: var(--ln-eyebrow); line-height: var(--ln-lh-eyebrow); color: var(--ink-soft); }

.ln-q__field, .ln-q__text, .ln-q__one select {
  font: inherit; font-size: var(--ln-label); min-height: var(--ln-target);
  padding: var(--ln-1) var(--ln-2);
  background: var(--card); border: 1px solid var(--rule-2); color: var(--ink); border-radius: 0;
}
.ln-q__field { width: 7rem; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.ln-q__text { width: 100%; max-width: var(--ln-measure); resize: vertical; }
.ln-q__one select { justify-self: start; min-width: 12rem; max-width: 100%; }
.ln-q__field:hover, .ln-q__text:hover, .ln-q__one select:hover { border-color: var(--brass); }

/* A refused entry marks the control it is about as well as the block below it,
   because the block is the explanation and the control is where the learner is
   looking. Both channels are non-color: a doubled border here, a doubled rule
   there. */
[data-answer-input][aria-invalid='true'] { border-style: double; border-width: 3px; border-color: var(--brick); }
.ln-state--invalid { border-left-style: double; border-left-width: 5px; border-left-color: var(--brick); }
.ln-state--invalid .ln-state__word { color: var(--brick); }

/* ── the inspector ──────────────────────────────────────────────────────────
   EMPTY IS A STATE AND IT IS DRAWN. A model a learner cannot put down is a
   model that is always asserting something, so nothing selected has to have a
   picture. It is also the only region here that may scroll vertically: what it
   holds is an explanation, and bounding the panel would cut the explanation
   rather than the layout. */
.ln-inspect { min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--rule); background: var(--card); padding: var(--ln-3); }
.ln-inspect__body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: grid; gap: var(--ln-2); align-content: start; }
.ln-inspect__none { font-size: var(--ln-prose-quiet); line-height: var(--ln-lh-quiet); color: var(--faint); margin: 0; }

/* ── evidence ───────────────────────────────────────────────────────────────
   The document a figure was read out of, and when. Slate, because the site
   already uses --filed for a figure somebody else published and has done since
   the token was added. */
.ln-evidence__src { font-family: var(--mono); font-size: var(--ln-eyebrow); line-height: var(--ln-lh-eyebrow); color: var(--ink-soft); margin: 0; border-top: 1px solid var(--filed); padding-top: var(--ln-2); }
.ln-evidence__src b { font-weight: 400; color: var(--filed); letter-spacing: .06em; text-transform: uppercase; }
.ln-evidence__src a { color: inherit; }

/* Said to a screen reader and to nobody else. */
.ln-vh { position: absolute; width: 1px; height: 1px; margin: 0; padding: 0; clip-path: inset(50%); overflow: hidden; white-space: nowrap; }

/* ── the narrow and zoomed frame ────────────────────────────────────────────
   AT 60rem THE FRAME STOPS BEING VIEWPORT-BOUNDED. 60rem is 960px at a 16px
   root, which a 1280px window reaches at 135% zoom and a 1920px window reaches
   at 200% — so a zoom and a small window resolve to the same layout, which is
   what WCAG 1.4.10 is asking for and the reason there is no px breakpoint in
   this file.

   THE ORDER IS THE READING ORDER AND THE QUESTION COMES SECOND. A learner needs
   to know what they are looking for before a tall drawing, and orientation
   before either. Scene, then inspector, then evidence.

   NOTHING SHRINKS. Every size above is a rem, so 200% zoom makes all of them
   twice as large and the frame reflows underneath them. That is the opposite of
   what the demo canvas does, and it is why none of its machinery is here. */
@media (max-width: 60rem) {
  /* Flex rather than grid, so the order below is the whole layout and there is
     no second statement of it in a template. `display: contents` on the working
     column lets the question and the inspector be re-ordered independently
     while staying one region in the markup. */
  .ln-frame {
    display: flex;
    flex-direction: column;
    height: auto;
    min-height: 100svh;
    padding: var(--ln-3);
    gap: var(--ln-4);
  }
  .ln-work { display: contents; }
  .ln-frame > *, .ln-ask, .ln-inspect { min-width: 0; }
  .ln-orient { order: 0; }
  .ln-ask { order: 1; }
  .ln-scene { order: 2; }
  .ln-inspect { order: 3; }
  .ln-evidence { order: 4; }
  /* Both reading panels stop being scroll containers. On one column the page is
     the scroll container, and a panel keeping its own scrollbar inside it is
     the trap where a reader cannot reach the end of an explanation. The
     drawing keeps its sideways scroll, because the alternative is scaling it
     below 1:1 and taking its labels under the floor. */
  .ln-ask > * { flex: 0 0 auto; }
  .ln-inspect, .ln-inspect__body, .ln-ask__result, .ln-scene__art { overflow: visible; min-height: 0; }
  .ln-scene__art { overflow-x: auto; }
}
