/* =========================================================================
   Open Psychology Interactives — Simplified Edition
   patterns.css — optional visual patterns
   -------------------------------------------------------------------------
   Load after `simplified.css`. Every rule here is optional: nothing in this
   file is required for an activity to work, and no pattern assumes anything
   about how an activity is structured or scored.

   The patterns are drawn from the golden references in
   `docs/simplified-reference/`, generalised so that a pattern is available
   to any activity rather than tied to the one that first needed it. The
   reference class names are kept, so a golden example could be pasted in and
   would look right — with one deliberate exception, described in
   `simplified.css`: interactive state moves from classes (`.correct`,
   `.wrong`, `.active`) to `data-state`, because each state also needs a
   glyph, a border style and a text label to stop colour carrying meaning on
   its own.

   Pick the pattern that fits the teaching job. Do not reach for the choice
   grid by default: three of the seven references use no choices at all.
   ========================================================================= */

/* -------------------------------------------------------------------------
   1. Notice

   A short flagged aside. The project requires simulated, fictional and
   illustrative values to be labelled as such (CLAUDE.md), and this is where
   that label goes. Use `--caution` for content notes and anything a reader
   should know before they start.
   ---------------------------------------------------------------------- */

.notice {
  margin-top: var(--stack);
  padding: 1rem 1.125rem;
  border-radius: var(--radius-sm);
  background: var(--blue-light);
  border: 1px solid rgb(61 90 128 / 0.25);
  font-size: var(--text-sm);
}

.notice--caution {
  background: var(--gold-light);
  border-color: var(--gold-strong);
}

.notice > p {
  margin: 0;
}

.notice > p + p {
  margin-top: 0.5rem;
}

/* -------------------------------------------------------------------------
   2. Casebox

   The stimulus: the case, extract, result or scenario the learner is working
   on. Set slightly larger than body text and ruled in teal, so it reads as
   the material rather than as instructions about the material.
   ---------------------------------------------------------------------- */

.casebox {
  margin-top: var(--stack);
  padding: 1.375rem 1.5rem;
  border-left: 5px solid var(--teal);
  border-radius: var(--radius-sm);
  background: var(--card);
  font-size: var(--text-md);
}

.casebox > p {
  margin: 0;
}

.casebox > p + p {
  margin-top: 0.5rem;
}

/* -------------------------------------------------------------------------
   3. Tag and chip strip

   `.tag` names the thing currently on screen — which measure, which lens,
   which topic. `.chip-strip` lists attributes that all stay true at once.
   ---------------------------------------------------------------------- */

.tag {
  display: inline-flex;
  align-items: center;
  margin-top: 1rem;
  padding: 0.5625rem 0.75rem;
  border-radius: var(--radius-pill);
  background: var(--gold-light);
  border: 1px solid var(--gold-strong);
  font-size: var(--text-sm);
  font-weight: 800;
}

.chip-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5625rem;
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}

.chip {
  padding: 0.5625rem 0.75rem;
  border-radius: var(--radius-pill);
  background: #FAF8F4;
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--text-sm);
  font-weight: 800;
}

/* -------------------------------------------------------------------------
   4. Lens head

   A banner naming the frame currently being applied, and the question it
   asks. Used where the material stays fixed and the interpretation changes.
   ---------------------------------------------------------------------- */

.lens-head {
  margin-top: var(--stack);
  padding: 1.125rem 1.25rem;
  border-radius: var(--radius-md);
  background: var(--teal-light);
  border: 1px solid rgb(28 114 147 / 0.22);
}

.lens-head strong {
  display: block;
  margin-bottom: 0.25rem;
  font-size: 1.25rem;
}

.lens-head span {
  font-size: 0.875rem;
  color: var(--muted);
}

/* The instruction, kept with the frame it belongs to rather than floating
   above the options as a separate block. */
.lens-head__task {
  margin: 0.625rem 0 0;
  color: var(--navy);
  font-weight: 800;
}


/* -------------------------------------------------------------------------
   Requirement checklist

   A list of stated conditions with their current status, for activities that
   judge something against criteria rather than against a right answer. Each
   item's own text must already say whether it is met; the edge colour is
   reinforcement and never the signal, because a checklist read aloud has to
   carry the same information as one on a screen.

     <ul class="checklist">
       <li data-state="correct">Met: 20 minutes or less. This battery: 20 minutes.</li>
       <li data-state="incorrect">Not met: burden of 8 or less. This battery: 10.</li>
     </ul>
   ---------------------------------------------------------------------- */

.checklist {
  margin: 0.625rem 0 0;
  padding: 0;
  list-style: none;
}

.checklist li {
  padding: 0.375rem 0 0.375rem 0.75rem;
  border-left: 3px solid var(--border);
}

.checklist li + li {
  margin-top: 0.375rem;
}

.checklist li[data-state="correct"] {
  border-left-color: var(--green-strong);
}

.checklist li[data-state="incorrect"] {
  border-left-color: var(--coral);
}

/* -------------------------------------------------------------------------
   5. Choice grid

   Markup contract:

     <div class="option-grid" data-workbook-choices>
       <button class="option" type="button" data-choice="key">
         <strong>Short label</strong>
         <span>One line saying what this option claims.</span>
       </button>
     </div>

   State is set through `data-state`, not a class:

     data-state="chosen"     selected, not yet judged   teal   solid    dot
     data-state="correct"    the best-supported answer  green  solid    tick
     data-state="partial"    defensible, not the best   blue   dotted   plus-minus
     data-state="incorrect"  the answer that was given  gold   dashed   cross

   `partial` exists because several activities in this collection judge an
   answer on three levels rather than two: an option can be a defensible
   reading that another frame nonetheless explains better. Collapsing that to
   right-or-wrong throws away the overlap, which is often where the teaching
   is. Use it where the material genuinely supports a middle answer, not to
   soften a wrong one.

   Three signals, not one: hue, border style and a corner glyph. A reader who
   cannot distinguish the hues still has a solid-versus-dashed edge and a
   tick-versus-cross, and workbook.js adds a visually hidden sentence naming
   the state for anyone using a screen reader.

   Answered options are locked with `aria-disabled="true"`, never the
   `disabled` attribute. `disabled` removes a button from the tab order, so a
   keyboard reader who has just answered loses their place and cannot go back
   over the options to read why. `aria-disabled` keeps them reachable and
   readable; workbook.js swallows the activation.

   Gold rather than red for an incorrect choice is deliberate and matches the
   references: the feedback these activities give is "another option is more
   central here", not "you failed".
   ---------------------------------------------------------------------- */

.option-grid {
  display: grid;
  grid-template-columns: repeat(var(--option-columns, 3), minmax(0, 1fr));
  gap: var(--gap);
  margin-top: var(--stack);
}

.option {
  position: relative;
  display: block;
  width: 100%;
  min-height: var(--option-min-height, 7.5rem);
  padding: 1.125rem;
  padding-right: 2.75rem;
  text-align: left;
  color: var(--navy);
  background: var(--card);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: border-color 120ms ease, background-color 120ms ease;
}

.option:hover {
  border-color: var(--teal);
}

.option strong {
  display: block;
  margin-bottom: 0.375rem;
  font-size: var(--text-md);
}

.option span {
  display: block;
  color: var(--muted);
  font-size: var(--text-sm);
  line-height: 1.45;
}

/* A single-line variant, for recap questions where the option is the whole
   answer and a supporting line would only pad it out. */
.option--compact {
  display: flex;
  align-items: center;
  min-height: var(--option-min-height, 6.5rem);
}

.option--compact strong {
  margin-bottom: 0;
  font-size: 1rem;
  line-height: 1.4;
}

/* A single-line variant. Where every option is one item from a fixed list
   that is on screen every round, a card each is mostly empty space; a row
   keeps the whole list visible at once, which is what lets a learner compare
   them. Put it in a one-column grid: `style="--option-columns: 1"`. */
.option--row {
  display: grid;
  grid-template-columns: minmax(10rem, 16rem) 1fr;
  gap: 0.125rem 1rem;
  align-items: baseline;
  min-height: 0;
  padding: 0.75rem 2.75rem 0.75rem 1rem;
}

.option--row strong {
  margin-bottom: 0;
  font-size: 1rem;
}

.option--row span {
  font-size: var(--text-sm);
}

/* --- States --- */

.option[data-state="chosen"] {
  border-color: var(--teal);
  background: var(--teal-light);
}

.option[data-state="correct"] {
  border-color: var(--green);
  border-style: solid;
  background: var(--green-light);
}

.option[data-state="partial"] {
  border-color: var(--blue);
  border-style: dotted;
  background: var(--blue-light);
}

.option[data-state="incorrect"] {
  border-color: var(--gold-strong);
  border-style: dashed;
  background: var(--gold-light);
}

/* The corner glyph. Decorative by construction — a CSS pseudo-element is not
   dependable in assistive technology, which is why workbook.js writes a real
   visually hidden sentence as well. */
.option[data-state]::after {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  font-size: 0.875rem;
  font-weight: 800;
  line-height: 1;
}

.option[data-state="chosen"]::after {
  content: "\2022";
  background: var(--teal);
  color: #FFFFFF;
}

.option[data-state="correct"]::after {
  content: "\2713";
  background: var(--green);
  color: #FFFFFF;
}

.option[data-state="partial"]::after {
  content: "\00B1";
  background: var(--blue);
  color: #FFFFFF;
}

.option[data-state="incorrect"]::after {
  content: "\2715";
  background: var(--navy);
  color: #FFFFFF;
}

/* On a row the glyph belongs beside the text, not above it. */
.option--row[data-state]::after {
  top: 50%;
  transform: translateY(-50%);
}

/* Locked, but still focusable and still readable. Opacity is left alone on
   purpose: the text is the thing the learner has come back to re-read. */
.option[aria-disabled="true"] {
  cursor: default;
}

.option[aria-disabled="true"]:hover {
  border-color: var(--border);
}

.option[aria-disabled="true"][data-state="chosen"]:hover,
.option[aria-disabled="true"][data-state="correct"]:hover {
  border-color: var(--green);
}

.option[aria-disabled="true"][data-state="partial"]:hover {
  border-color: var(--blue);
}

.option[aria-disabled="true"][data-state="incorrect"]:hover {
  border-color: var(--gold-strong);
}

@media (forced-colors: active) {
  .option[data-state] {
    border-width: 2px;
  }
}

/* -------------------------------------------------------------------------
   6. Paired reveal

   Two boxes side by side, for the move the references use more than any
   other: say what the evidence supports, then say what it does not.

     "What it brings into focus"  /  "What it backgrounds"
     "What we can say"            /  "What we still cannot say"

   `.mini--limit` tints the second box so the pair reads as a pair rather
   than as two unrelated notes.
   ---------------------------------------------------------------------- */

.reveal-grid {
  display: grid;
  grid-template-columns: repeat(var(--reveal-columns, 2), minmax(0, 1fr));
  gap: var(--gap);
  margin-top: 0.875rem;
}

.mini {
  padding: 1rem;
  border-radius: var(--radius-sm);
  background: #FAF8F4;
  border: 1px solid var(--border);
}

.mini strong {
  display: block;
  margin-bottom: 0.3125rem;
}

.mini p {
  margin: 0;
  color: var(--muted);
  font-size: 0.875rem;
}

.mini--limit {
  background: var(--gold-light);
  border-color: var(--gold-strong);
}

/* -------------------------------------------------------------------------
   7. Evidence strip

   Small labelled facts about the case as it currently stands. Where a stage
   adds evidence, this is what visibly grows.
   ---------------------------------------------------------------------- */

.evidence-grid {
  display: grid;
  grid-template-columns: repeat(var(--evidence-columns, 3), minmax(0, 1fr));
  gap: var(--gap);
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}

.evidence {
  padding: 0.9375rem;
  border-radius: var(--radius-sm);
  background: #FAF8F4;
  border: 1px solid var(--border);
}

.evidence strong {
  display: block;
  margin-bottom: 0.25rem;
}

.evidence span {
  font-size: var(--text-sm);
  color: var(--muted);
}

/* -------------------------------------------------------------------------
   8. Result tiles

   Large figures with a caption above and a reference point below. The
   caption below is not optional: a number with nothing to compare it to
   teaches nothing (LEARNER_COPY_STYLE.md §5.1).
   ---------------------------------------------------------------------- */

/* A result tile that carries an outcome. The tile's own text always says
   which outcome it is, so the edge is reinforcement rather than the signal:
   nothing here is readable only by colour. */
.result[data-state="correct"] {
  border-left: 4px solid var(--green-strong);
}

.result[data-state="incorrect"] {
  border-left: 4px solid var(--coral);
}

.result[data-state="partial"] {
  border-left: 4px solid var(--gold-strong);
}

.results-grid {
  display: grid;
  grid-template-columns: repeat(var(--results-columns, 4), minmax(0, 1fr));
  gap: var(--gap);
  margin: var(--stack) 0 0;
  padding: 0;
  list-style: none;
}

.result {
  padding: 1.125rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: #FAF8F4;
}

.result strong {
  display: block;
  margin-bottom: 0.25rem;
  font-size: var(--text-sm);
  color: var(--muted);
}

.result .big {
  font-size: var(--text-2xl);
  font-weight: 800;
  color: var(--teal);
  font-variant-numeric: tabular-nums;
}

.result .big--small {
  font-size: var(--text-xl);
}

.result span {
  font-size: var(--text-xs);
  color: var(--muted);
}

/* -------------------------------------------------------------------------
   9. Voices

   Several accounts of the same situation, shown together so they can be
   compared rather than read in turn.
   ---------------------------------------------------------------------- */

.voice-grid {
  display: grid;
  grid-template-columns: repeat(var(--voice-columns, 3), minmax(0, 1fr));
  gap: var(--gap);
  margin-top: var(--stack);
}

.voice {
  padding: 1.125rem;
  border-radius: var(--radius-md);
  background: var(--card);
  border: 1px solid var(--border);
}

.voice strong {
  display: block;
  margin-bottom: 0.4375rem;
}

.voice p {
  margin: 0;
  color: var(--muted);
  font-size: 0.875rem;
}

/* -------------------------------------------------------------------------
   10. Cascade

   A consequence chain: what follows from the step just taken, and what
   follows from that. The arrows are decorative; the steps are a list, so the
   sequence is available without seeing them.
   ---------------------------------------------------------------------- */

.continuation {
  margin-top: 0.875rem;
  padding: 1.125rem;
  border-radius: var(--radius-sm);
  background: #FAF8F4;
  border: 1px solid var(--border);
}

.continuation > strong {
  display: block;
  margin-bottom: 0.375rem;
}

.cascade {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
  counter-reset: cascade;
}

.cascade-step {
  padding: 0.5625rem 0.6875rem;
  border-radius: 10px;
  background: var(--card);
  border: 1px solid var(--border);
  font-size: var(--text-sm);
  font-weight: 700;
}

/* Between the steps, not inside them, so the arrow is never read as content.
   `aria-hidden` on a separate element would also work; this needs no markup
   at all. */
.cascade-step + .cascade-step::before {
  content: "\2192";
  margin-right: 0.5rem;
  color: var(--coral);
  font-weight: 900;
}

/* -------------------------------------------------------------------------
   11. Comparison bars

   A bar is a second reading of a number that is already on the page in text.
   `.value` is required, not decorative: it is what a screen reader and a
   greyscale printout both fall back to.
   ---------------------------------------------------------------------- */

.compare-bar {
  margin-top: var(--stack);
  padding: 1.25rem;
  border-radius: var(--radius-md);
  background: #FBFAF7;
  border: 1px solid var(--border);
}

.bar-row {
  display: grid;
  /* Tokenised so a row can carry a word — "partly reached" — in the value
     column rather than only a figure. */
  grid-template-columns:
    var(--bar-label, 9.375rem) 1fr var(--bar-value, 3.4375rem);
  gap: 0.625rem;
  align-items: center;
  margin-top: 0.75rem;
}

.bar-row:first-child {
  margin-top: 0;
}

.track {
  height: 0.75rem;
  border-radius: var(--radius-pill);
  background: #E8E4DD;
  overflow: hidden;
}

.fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--bar-colour, var(--teal));
}

.value {
  text-align: right;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.legend {
  margin: 0.75rem 0 0;
  color: var(--muted);
  font-size: var(--text-xs);
}

/* -------------------------------------------------------------------------
   12. Stimulus stage

   A fixed-height area where something is shown, briefly or otherwise.

   If what appears here is timed, the activity must also offer a route that
   is not timed — a replay, a step control, or a static version with the same
   information. A one-second exposure with no alternative fails WCAG 2.2
   SC 2.2.1, and several of these demonstrations are the whole point of the
   activity, so losing them is not an option either.
   ---------------------------------------------------------------------- */

.stage {
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
  min-height: var(--stage-height, 17.8125rem);
  margin-top: var(--stack);
  border-radius: var(--radius-md);
  background: #FBFAF7;
  border: 1px solid var(--border);
}

.stage__message {
  padding: 1.25rem;
  color: var(--muted);
  font-weight: 800;
  text-align: center;
}

.stage__mark {
  position: absolute;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--blue);
}

/* A performed task departs from the workbook here, and should. A speeded
   response is worse against a bright page: the ground has to be dark and
   constant so that ink colour is the only thing changing, and nothing else
   may sit inside the panel competing for the eye. Every ink used against
   this ground clears 7:1. */
.stage--focus {
  background: #15181D;
  border-color: #15181D;
}

.stage__word {
  font-size: clamp(2.5rem, 8vw, 4.25rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0.02em;
}

.stage__fixation {
  color: #FFFFFF;
  font-size: 2.5rem;
  font-weight: 400;
  line-height: 1;
}

/* An SVG stimulus field filling the stage. Several performed tasks need a
   spatial display rather than a single word: two boxes at known positions, an
   array of search items, a scene that alternates, a stream in the centre. The
   SVG carries its own geometry in its viewBox, so all this has to do is size
   it and stop the stage's centring from shrinking it to its intrinsic size. */
.stage__field {
  display: block;
  justify-self: stretch;
  align-self: stretch;
  width: 100%;
  height: 100%;
}

.stage__feedback {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.875rem;
  margin: 0;
  color: #E9EDF3;
  font-size: var(--text-sm);
  font-weight: 700;
  text-align: center;
}

/* -------------------------------------------------------------------------
   12b. Response keypad

   The fixed set of responses in a performed task. Not a choice grid: there
   is no correct option in the abstract, only a correct response to the
   stimulus currently on screen, and nothing here is ever marked.

   The whole key is the target, the response is named in text, and the
   keyboard equivalent is printed on the key rather than hidden in the
   instructions. Where a key carries a colour swatch it is bordered, because
   a pale swatch on a white key has almost no contrast on its own and the
   name beside it is what actually identifies the response.
   ---------------------------------------------------------------------- */

.keypad {
  display: grid;
  grid-template-columns: repeat(var(--keypad-columns, 4), minmax(0, 1fr));
  gap: var(--gap);
  margin: var(--stack) 0 0;
  padding: 0;
  border: 0;
}

.keypad__key {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-height: 4rem;
  padding: 0.75rem 0.5rem;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--card);
  color: var(--navy);
  font-size: 1rem;
  font-weight: 800;
  cursor: pointer;
}

.keypad__key:hover {
  border-color: var(--teal);
}

.keypad__key[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
}

.keypad__swatch {
  width: 1.5rem;
  height: 0.875rem;
  border: 1px solid var(--navy);
  border-radius: 3px;
}

.keypad__hint {
  color: var(--muted);
  font-size: var(--text-xs);
  font-weight: 600;
}

@media (max-width: 47.5em) {
  .keypad {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* -------------------------------------------------------------------------
   13. Comparison values

   Other people's responses, other groups' figures, or any small set of
   values shown for comparison. Label the source in text: where these are
   simulated, say so beneath them.
   ---------------------------------------------------------------------- */

.peers {
  display: grid;
  grid-template-columns: repeat(var(--peers-columns, 3), minmax(0, 1fr));
  gap: var(--gap);
  margin: var(--stack) 0 0;
  padding: 0;
  list-style: none;
}

.peer {
  padding: 0.9375rem;
  border-radius: var(--radius-sm);
  background: var(--teal-light);
  border: 1px solid rgb(28 114 147 / 0.2);
  text-align: center;
}

.peer strong {
  display: block;
  font-size: var(--text-xl);
  font-variant-numeric: tabular-nums;
}

.peer span {
  font-size: var(--text-sm);
  color: var(--muted);
}

/* -------------------------------------------------------------------------
   14. Paired blocks

   Two plain blocks side by side, for comparing one set of things with
   another — before and after, one condition and the other.
   ---------------------------------------------------------------------- */

.pair-grid {
  display: grid;
  grid-template-columns: repeat(var(--pair-columns, 2), minmax(0, 1fr));
  gap: var(--gap);
  margin-top: var(--stack);
}

.block {
  padding: 1.125rem;
  border-radius: var(--radius-sm);
  background: var(--card);
  border: 1px solid var(--border);
}

.block ol,
.block ul {
  margin: 0;
  padding-left: 1.25rem;
  color: var(--muted);
}

.block li + li {
  margin-top: 0.3125rem;
}

/* -------------------------------------------------------------------------
   15. Controls

   For activities that manipulate something rather than choose between
   options. `<output for="...">` pairs the current value with its input;
   workbook.js keeps the two in step through `bindRange`.
   ---------------------------------------------------------------------- */

.controls {
  display: grid;
  grid-template-columns: repeat(var(--controls-columns, 2), minmax(0, 1fr));
  gap: 1.125rem;
  margin: var(--stack) 0 0;
  padding: 0;
  border: 0;
}

.control__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}

.control__label {
  font-weight: 800;
}

.control__value {
  color: var(--teal);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  /* The readout is the control's current value and has to stay one token. At
     narrow widths a long label squeezes the header's other cell until a value
     like "8 of 10" wraps to "8 of" over "10", which reads as two numbers. The
     label wraps instead, which is what a label is for. */
  white-space: nowrap;
}

.control__hint {
  margin: 0.375rem 0 0;
  color: var(--muted);
  font-size: var(--text-sm);
}

.control select {
  width: 100%;
}

.control input[type="range"] {
  width: 100%;
  min-height: var(--target);
  accent-color: var(--teal);
}

.control select,
.control input[type="number"],
.control input[type="text"],
.control textarea,
.entry input,
.entry textarea {
  max-width: 100%;
  min-height: var(--target);
  padding: 0.6875rem 0.75rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--navy);
}

/* Free text. Sized for a sentence or two, never for an essay: an activity
   that wants a paragraph is asking the learner to do the wrong amount. */
.entry textarea,
.control textarea {
  width: 100%;
  min-height: 4.5rem;
  resize: vertical;
  font-family: inherit;
  line-height: 1.5;
}

/* A label, a small input and a button on one line. */
.entry {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.625rem;
  margin-top: var(--stack);
}

.entry label {
  font-weight: 800;
}

.entry input {
  width: 9.375rem;
}

/* An inline validation message. Referenced from the input with
   `aria-describedby`, so it is read as part of the field rather than
   appearing silently beside it. */
.entry__error {
  flex-basis: 100%;
  margin: 0;
  color: var(--coral);
  font-size: var(--text-sm);
  font-weight: 700;
}

.entry__error::before {
  content: "\26A0\FE0E";
  margin-right: 0.375rem;
}

/* -------------------------------------------------------------------------
   15b. Toggle set

   Several independent things switched on and off at once — measures in a
   plan, conditions in a design — where the consequence appears elsewhere on
   the page rather than as a verdict on the choice itself. Not a quiz: there
   is no correct subset, and nothing here marks one.

   Markup:

     <fieldset class="toggle-grid" style="--toggle-columns: 1">
       <legend class="visually-hidden">…</legend>
       <label class="toggle" data-checked="false">
         <input type="checkbox" value="…">
         <span>
           <strong>Name</strong>
           <span>What it is</span>
         </span>
       </label>
     </fieldset>

   `data-checked` is written by the activity rather than styled with `:has()`,
   so the selected state does not depend on a selector that older browsers in
   a teaching lab may not have. The native checkbox is the non-colour signal:
   a tick is a tick whatever the palette does. The whole label is the target,
   so the row — not the 18px box — is what has to be hit.
   ---------------------------------------------------------------------- */

/* A fieldset legend that is meant to be seen. Most groups here hide theirs
   because a nearby heading already names them, but a group that is the pivot
   of its activity should say so in its own right. `legend` ignores most box
   properties, so this stays deliberately typographic. */
.field-legend {
  padding: 0;
  margin: 0 0 0.5rem;
  color: var(--navy);
  font-weight: 800;
  /* A legend inside a grid fieldset is a grid item like any other. Without
     this it takes the first cell and shunts the first option into the second
     column, which is invisible in a one-column group and obvious the moment a
     group has two or more. */
  grid-column: 1 / -1;
}

.toggle-grid {
  display: grid;
  grid-template-columns: repeat(var(--toggle-columns, 2), minmax(0, 1fr));
  gap: var(--gap);
  margin: var(--stack) 0 0;
  padding: 0;
  border: 0;
}

.toggle {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.75rem;
  align-items: start;
  min-height: var(--target);
  padding: 0.75rem 1rem;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--card);
  cursor: pointer;
}

.toggle:hover {
  border-color: var(--teal);
}

/* A toggle whose option will not fit within the activity's budget. Dimmed,
   never disabled: its description says it will not fit, and the control stays
   in the tab order so a keyboard reader still meets it. */
.toggle[data-full="true"] {
  opacity: 0.6;
}

.toggle[data-checked="true"] {
  border-color: var(--teal);
  background: var(--teal-light);
}

.toggle input {
  width: 1.125rem;
  height: 1.125rem;
  margin: 0.3125rem 0 0;
  accent-color: var(--teal);
}

/* The ring goes round the whole row rather than the small box inside it. */
.toggle:focus-within {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.toggle input:focus-visible {
  outline: none;
}

.toggle strong {
  display: block;
  font-size: 1rem;
}

.toggle span span {
  display: block;
  color: var(--muted);
  font-size: var(--text-sm);
}

@media (forced-colors: active) {
  .toggle[data-checked="true"] {
    border-width: 2px;
  }
}

/* -------------------------------------------------------------------------
   15c. Interval plot

   A value with its uncertainty, plotted against a reference range: a score
   against a control group, an estimate against a null, an effect against a
   benchmark. One row per observation.

   Two things carry meaning and neither is colour. The marker is filled when
   the whole interval clears the reference band and hollow when it does not,
   so the distinction survives greyscale. The band itself is dashed, because
   a pale fill on a pale plot ground has almost no contrast on its own and
   the edge is the part that means something.

   A plot always needs its numbers somewhere. Put a data table beside or
   below it; do not print the values into the SVG and call that the text
   alternative.
   ---------------------------------------------------------------------- */

.plot {
  margin-top: var(--stack);
  padding: 1rem;
  border-radius: var(--radius-md);
  background: #FBFAF7;
  border: 1px solid var(--border);
  /* A figure drawn in a 900-unit viewBox and stretched to fit renders its
     12px labels at 12px only when it is 900px wide. At a 320px viewport that
     is three pixels, which is not a small figure but an unreadable one.
     Below the floor the figure keeps its designed proportions and the region
     scrolls instead, which is the same answer .table-scroll gives for a wide
     table. workbook.js makes the region keyboard reachable when it scrolls. */
  overflow-x: auto;
}

.plot svg {
  display: block;
  width: 100%;
  min-width: 46rem;      /* labels never render below about 10px */
  height: auto;
}

/* A figure label that has to sit over data marks. The stroke is painted
   first and in the figure's own background colour, so the glyphs keep a clear
   edge against whatever is behind them without a filled box hiding marks. */
.plot__over {
  paint-order: stroke;
  stroke: #FBFAF7;
  stroke-width: 3.5px;
  stroke-linejoin: round;
}

@media (forced-colors: active) {
  .plot__over {
    stroke: Canvas;
  }
}

.plot:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.plot__band {
  fill: var(--teal-light);
  stroke: var(--teal);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}

.plot__band--alert {
  fill: var(--coral-light);
  stroke: var(--coral);
}

.plot__zero {
  stroke: var(--teal);
  stroke-width: 1.5;
}

.plot__axis {
  stroke: var(--border);
  stroke-width: 1;
}

.plot__label {
  font: 700 var(--text-sm) var(--font-body);
  fill: var(--navy);
}

.plot__sub,
.plot__tick {
  font: 500 var(--text-xs) var(--font-body);
  fill: var(--muted);
}

.plot__interval {
  stroke: var(--blue);
  stroke-width: 3;
  stroke-linecap: round;
}

/* Dashed, not merely recoloured: whether an interval covers its reference is
   the whole point of a coverage plot and must survive greyscale. */
.plot__interval--miss {
  stroke: var(--coral);
  stroke-dasharray: 5 3;
}

.plot__point {
  fill: var(--card);
  stroke: var(--blue);
  stroke-width: 2.5;
}

.plot__point--filled {
  fill: var(--blue);
}

.plot__bar {
  fill: var(--blue-light);
  stroke: var(--blue);
  stroke-width: 1.5;
}

/* A bar on the wrong side of zero. Direction already carries the sign, so
   this only makes it easier to find; never rely on the tint alone. */
.plot__bar--negative {
  fill: var(--coral-light);
  stroke: var(--coral);
}

/* Three roles in one figure, each a different mark as well as a different
   hue, and each named in text on the page: a population (filled curve), one
   sample from it (ticks), and the distribution of a statistic (histogram). */
.plot__density {
  fill: var(--teal-light);
  stroke: var(--teal);
  stroke-width: 2;
}

.plot__sample {
  stroke: var(--coral);
  stroke-width: 2;
}

.plot__hist {
  fill: var(--blue-light);
  stroke: var(--blue);
  stroke-width: 1;
}

/* Key elements. Drawn exactly as the data is drawn, so the key needs no
   separate styling; the class exists so that a key marker is never counted
   as an observation. */
.plot__key {
  pointer-events: none;
}

@media (forced-colors: active) {
  .plot__band {
    stroke-width: 2;
  }
}

/* -------------------------------------------------------------------------
   15d. Disclosure

   A labelled `<details>`, for material that must be available but does not
   have to be on screen: most often the data table behind a chart.

   Only use it where the essential information is already reachable another
   way. A plot's `<desc>` should state what the plot shows before anything is
   opened; the table behind this is the full set of values, not the only route
   to the point. Never put an activity's actual content in here.
   ---------------------------------------------------------------------- */

.disclosure {
  margin-top: var(--stack);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--card);
}

.disclosure > summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-height: var(--target);
  padding: 0.6875rem 1rem;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 800;
  cursor: pointer;
}

.disclosure > summary::marker {
  color: var(--teal);
}

.disclosure[open] > summary {
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}

.disclosure__body {
  padding: 0 1rem 1rem;
}

.disclosure__body .table-scroll {
  margin-top: 0.75rem;
}

/* -------------------------------------------------------------------------
   15e. Key terms

   A glossary inside a `.disclosure`, collapsed by default, placed next to the
   controls a learner is about to operate:

     <details class="disclosure">
       <summary>Key terms</summary>
       <div class="disclosure__body">
         <dl class="glossary">
           <dt>Term</dt><dd>One or two plain sentences.</dd>
         </dl>
       </div>
     </details>

   For activities where the task cannot be done without the vocabulary:
   statistical and psychometric ones, mostly. Not a default. An activity whose
   terms are explained by doing it does not need one, and a glossary that
   restates what the page already says is furniture.

   Keep each definition to one or two sentences. This is a reminder for
   someone mid-task, not a teaching section.
   ---------------------------------------------------------------------- */

.glossary {
  margin: 0;
}

.glossary dt {
  font-size: var(--text-sm);
  font-weight: 800;
}

.glossary dd {
  margin: 0.125rem 0 0.75rem;
  color: var(--muted);
  font-size: var(--text-sm);
}

.glossary dd:last-child {
  margin-bottom: 0;
}

/* -------------------------------------------------------------------------
   16. Data table

   Every chart needs one of these, or an equivalent readout. It is the text
   alternative, and it is also what a lecturer reads out loud.
   ---------------------------------------------------------------------- */

.table-scroll {
  overflow-x: auto;
  margin-top: var(--stack);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.data-table caption {
  padding: 0.75rem 0.75rem 0;
  color: var(--muted);
  font-size: var(--text-sm);
  text-align: left;
}

.data-table th,
.data-table td {
  padding: 0.625rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
}

.data-table thead th {
  background: #FBFAF7;
  font-weight: 800;
}

.data-table td {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.data-table tbody tr:last-child th,
.data-table tbody tr:last-child td {
  border-bottom: 0;
}

/* -------------------------------------------------------------------------
   17. Responsive

   Every grid in this file collapses to a single column at the shell's one
   breakpoint, which is what the references do. The bar rows narrow rather
   than stack, because a label above a bar loses the comparison.
   ---------------------------------------------------------------------- */

@media (max-width: 47.5em) {
  .option--row {
    grid-template-columns: 1fr;
  }

  /* minmax(0, 1fr), not 1fr. A bare 1fr keeps an automatic minimum, so a
     grid item with a wide intrinsic width, a select whose longest option is
     long, pushes its track past the viewport and the page scrolls sideways
     at 320px. */
  .option-grid,
  .reveal-grid,
  .evidence-grid,
  .results-grid,
  .voice-grid,
  .peers,
  .pair-grid,
  .toggle-grid,
  .controls {
    grid-template-columns: minmax(0, 1fr);
  }

  .bar-row {
    grid-template-columns:
      var(--bar-label-narrow, 6.875rem) 1fr var(--bar-value-narrow, 2.8125rem);
  }

  .entry input {
    width: 100%;
  }
}

/* ---------------------------------------------------- perceptual stimulus
   A dark, low-reference field for a judgement about apparent movement.

   This deliberately does NOT use .plot. A .plot figure carries text labels,
   so it holds a 46rem minimum width and lets the region scroll rather than
   shrinking its type to nothing. A perceptual stimulus has the opposite
   requirement: there is no text in it, the learner is judging how far a point
   of light appeared to move, and a figure that scrolls sideways could carry
   the moving point out of view. So the field keeps a set height, renders its
   coordinate space at roughly 1:1, and crops its width at narrow viewports
   instead (preserveAspectRatio="xMidYMid slice" in the markup). The motion is
   centred, so cropping removes only empty field. The height is in rem, so a
   learner who raises their browser's text size gets a larger field and a
   proportionally larger displacement rather than an unchanged one. */
.stimulus {
  margin-top: var(--stack);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.stimulus svg {
  display: block;
  width: 100%;
  height: clamp(12.5rem, 26vw, 15rem);
  background: #080D18;
}

.stimulus__caption {
  padding: 0.8125rem 1rem 0.875rem;
  background: #FBFAF7;
  border-top: 1px solid var(--border);
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--ink-soft, #4A5568);
}

.stimulus__caption strong {
  color: var(--ink, #1A2744);
}

@media (forced-colors: active) {
  /* The field's contrast comes from fill attributes, which forced-colors
     leaves alone; the border is what stops it merging into the page. */
  .stimulus {
    border: 2px solid CanvasText;
  }
}
