/* Design tokens. This file is the source of truth for the web app and the
   marketing site. Components reference variables, never raw hex values. */

:root {
  /* ── Surfaces ────────────────────────────────────────────────────────────
     Pearl white, not cream. Separation between a card and the page comes
     from this pair alone, which is why cards carry no border. */
  --pearl: #f4f6f8;
  --surface: #ffffff;
  --surface-sunk: #eaeef2;

  /* ── Text ─────────────────────────────────────────────────────────────── */
  --graphite: #16181c;
  --graphite-2: #5c626b;
  --hairline: #e2e7ec;

  /* ── Brand ────────────────────────────────────────────────────────────────
     Petrol, chosen because it sits outside the green/red/amber band that the
     grading result owns. A brand colour that reads as a result is a bug. */
  --ink: #0f3d52;
  --ink-hover: #0a2c3c;
  --ink-wash: #e4edf2;

  /* ── Result colours, reserved ─────────────────────────────────────────────
     These three appear only in a grading outcome. Never on a button, a link,
     a nav item or an icon. */
  --correct: #1f7a4d;
  --wrong: #b3261e;
  --review: #8a6410;

  /* ── Radius ───────────────────────────────────────────────────────────────
     The sheet is rectangular. Circles belong to bubbles alone, so nothing
     else in the interface is fully round. */
  --r-1: 4px;
  --r-2: 8px;
  --r-3: 12px;

  /* ── Spacing, 4px rhythm ──────────────────────────────────────────────── */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --s-8: 64px;
  --s-9: 96px;

  /* ── Type ─────────────────────────────────────────────────────────────────
     One superfamily across every supported language: IBM Plex ships Latin,
     Arabic, Devanagari and Chinese SC drawn to match, so a page never mixes
     unrelated designs and line boxes stay the same height when the locale
     changes.

     Each stack ends in a system font that actually contains the script. That
     last entry is what stops a failed download from rendering tofu, or from
     rendering Arabic as disconnected letters. */
  --font-latin: 'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif;
  --font-ar: 'IBM Plex Sans Arabic', 'Geeza Pro', 'Noto Naskh Arabic', 'Segoe UI', sans-serif;
  --font-hi: 'IBM Plex Sans Devanagari', 'Noto Sans Devanagari', 'Nirmala UI', sans-serif;
  --font-zh: 'IBM Plex Sans SC', 'PingFang SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', monospace;

  /* The document sets --font-ui per locale; components only read this. */
  --font-ui: var(--font-ar);

  --t-display: 34px;
  --t-h1: 26px;
  --t-h2: 20px;
  --t-body: 16px;
  --t-sm: 14px;
  --t-xs: 12px;

  /* Arabic needs more leading than Latin at the same size. */
  --lh-tight: 1.4;
  --lh-body: 1.75;

  /* ── Motion ───────────────────────────────────────────────────────────────
     One rhythm for the whole product. Exit is faster than enter. */
  --dur-fast: 150ms;
  --dur-base: 240ms;
  --dur-slow: 400ms;
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in: cubic-bezier(0.55, 0.06, 0.68, 0.19);

  /* ── Elevation ───────────────────────────────────────────────────────────
     Two steps only: resting and lifted. Anything needing a third step is
     really asking for a different layout. */
  --lift-1: 0 1px 2px rgb(22 24 28 / 6%);
  --lift-2: 0 8px 24px rgb(22 24 28 / 10%);

  /* ── Layers ──────────────────────────────────────────────────────────── */
  --z-sticky: 10;
  --z-popover: 40;
  --z-modal: 100;
}

/* ── Production font loading ───────────────────────────────────────────────
   Self-hosted on our own origin, WOFF2 only, one file per script per weight,
   and only two weights ship: 400 and 600.

   Three rules exist specifically to stop text from rendering broken on real
   devices. Each one has bitten real products:

   1. NEVER split a joining script across several subset files. Latin can be
      split safely because letters do not connect. Arabic and Devanagari can
      not: if one word is served partly from file A and partly from file B the
      shaper cannot join it, and the word renders as disconnected letters.
      Arabic ships as ONE file. Devanagari ships as ONE file.

   2. NEVER subset without keeping the OpenType layout tables. A subsetter run
      with default settings drops GSUB, and GSUB is exactly what selects the
      initial, medial, final and isolated form of every Arabic letter. Losing
      it is the single most common cause of disconnected Arabic on the web.
      Always: pyftsubset --layout-features='*' --unicodes=<script block>

   3. NEVER subset to the characters currently on the page. Student names,
      exam titles and answer keys arrive at runtime, so a page-derived subset
      will be missing glyphs for real user data. Subset by script block only.

   Chinese is the exception to eager loading: the SC face is measured in
   megabytes, so it is requested only on Chinese routes and never appears in
   the global stylesheet. */

/*
@font-face {
  font-family: 'IBM Plex Sans Arabic';
  src: url('/fonts/plex-ar-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  size-adjust: 100%;
  ascent-override: 105%;
  unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
}
@font-face {
  font-family: 'IBM Plex Sans';
  src: url('/fonts/plex-latin-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0100-024F, U+0131, U+2000-206F, U+20A0-20BF;
}
*/


*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--pearl);
  color: var(--graphite);
  font-family: var(--font-ui);
  /* 16px floor: anything smaller triggers an auto zoom on iOS. */
  font-size: var(--t-body);
  line-height: var(--lh-body);
}
:lang(en), :lang(fr), :lang(de), :lang(es), :lang(tr) { --font-ui: var(--font-latin); line-height: var(--lh-tight); }
:lang(hi) { --font-ui: var(--font-hi); }
:lang(zh) { --font-ui: var(--font-zh); }

/* A German compound is one unbreakable token, and Aufgabenanalyse is wider than
   a sidebar row. Hyphenation follows the document's own lang attribute, so one
   rule is right in eight languages, and break-word is the floor beneath it. The
   failure this prevents is invisible to anyone testing in Arabic. */
.stat-label, .stat-note, .nav-item span, .tab, .btn span, td, th, p {
  -webkit-hyphens: auto; hyphens: auto; overflow-wrap: break-word;
}
/* Except a number, an identifier and an answer string, which are never broken
   and never hyphenated whatever the language around them does. */
.num, .marks, .mark, td.num-col, th.num-col { -webkit-hyphens: none; hyphens: none; }

/* Every number, identifier and answer string. Tabular so columns line up under
   a fast scan, and never reordered by the paragraph direction. Never wrapped
   either: a phone column narrow enough to break "9.0 / 12.5" across three lines
   turns one value into what looks like three, and the table already has its own
   sideways scroll for exactly this. Found by screenshot at 390. */
.num, .marks, td.num-col, th.num-col { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.num, td.num-col, th.num-col { white-space: nowrap; }
/* A NUMBER FIELD READS LEFT TO RIGHT WHATEVER THE PAGE DOES. An Arabic page
   renders the value -0.33 as "0.33-", and a teacher reads that as a third of a
   mark GRANTED rather than deducted, which is the sign of the thing reversed.
   The digits still sit on the edge an Arabic reader starts from, so only the
   minus moves. Found in a screenshot of the key editor's penalty field. */
input.num { direction: ltr; text-align: end; }

/* Focus is never removed. Two pixels, offset, on every interactive thing. */
:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: var(--r-1); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.skip {
  position: absolute; inset-block-start: -100%; inset-inline-start: var(--s-4);
  background: var(--surface); color: var(--ink); padding: var(--s-3) var(--s-4);
  border-radius: var(--r-1); z-index: var(--z-modal);
}
.skip:focus { inset-block-start: var(--s-3); }

/* ── Shell ───────────────────────────────────────────────────────────────── */
.shell { display: grid; grid-template-columns: 264px minmax(0, 1fr); min-block-size: 100dvh; }
.side {
  position: relative; background: var(--surface);
  border-inline-end: 1px solid var(--hairline);
  padding: var(--s-5) var(--s-4); display: flex; flex-direction: column; gap: var(--s-5);
}
/* The timing strip: the sidebar's own edge treatment, on the outer edge, and it
   follows the direction with the sidebar. A DEPICTION of a sheet would not,
   since paper has a physical left edge, but this is chrome and not a picture.
   Drawn as a gradient rather than as an element so it fills whatever height the
   column takes: an SVG with a fixed viewBox gets centred instead. */
.side::before {
  content: ''; position: absolute; inset-block: 0; inset-inline-start: 0; inline-size: 9px;
  background: repeating-linear-gradient(
    to bottom,
    var(--surface-sunk) 0 7px,
    transparent 7px 20px
  );
}
/* The lockup: the mark, then the name, with the clear space the logo spec
   fixes at half the mark's height. The mark never mirrors; the row does. */
.brand { display: flex; align-items: center; gap: 10px; margin: 0; font-weight: 600; color: var(--ink); }
.brand .mark { display: block; flex: none; }

.nav { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  display: flex; align-items: center; gap: var(--s-3);
  min-block-size: 36px; padding-inline: var(--s-3); padding-block: var(--s-2);
  border-radius: var(--r-1); color: var(--graphite); text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out);
}
.nav-item:hover { background: var(--surface-sunk); }
.nav-item.is-current { background: var(--ink-wash); color: var(--ink); font-weight: 600; }
/* The corner square of the sheet, marking where you are. */
.fiducial { inline-size: 6px; block-size: 6px; background: transparent; border-radius: 1px; }
.nav-item.is-current .fiducial { background: var(--ink); }
.icon { flex: none; }

.side-foot { margin-block-start: auto; font-size: var(--t-sm); color: var(--graphite-2); }
.sync { display: flex; align-items: center; gap: var(--s-2); margin: 0 0 var(--s-1); }
.sync-detail, .sync-pending { margin: 0; }
.langs { list-style: none; display: flex; flex-wrap: wrap; gap: var(--s-3); padding: 0; margin: var(--s-4) 0 0; }
.langs a { color: var(--graphite-2); text-decoration: none; }
.langs a[aria-current] { color: var(--ink); font-weight: 600; }

.main { padding: var(--s-6); min-inline-size: 0; }

/* ── Screen head ─────────────────────────────────────────────────────────── */
.screen-head { margin-block-end: var(--s-5); }
.eyebrow { margin: 0 0 var(--s-1); color: var(--graphite-2); font-size: var(--t-sm); }
.screen-title { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); flex-wrap: wrap; }
h1 { font-size: var(--t-h1); line-height: var(--lh-tight); margin: 0; display: flex; align-items: center; gap: var(--s-2); }
h2 { font-size: var(--t-h2); line-height: var(--lh-tight); margin: 0; display: flex; align-items: center; gap: var(--s-2); }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); gap: var(--s-3); margin-block-start: var(--s-5); }
.stat { background: var(--surface); border-radius: var(--r-2); padding: var(--s-4); }
.stat-label { margin: 0; font-size: var(--t-sm); color: var(--graphite-2); }
.stat-value { display: block; font-size: var(--t-display); line-height: 1.15; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.stat-missing { display: block; font-size: var(--t-h2); line-height: 1.4; color: var(--review); }
.stat-note { margin: var(--s-1) 0 0; font-size: var(--t-xs); color: var(--graphite-2); }
/* A metric strip that is NOT inside the screen header has to carry the header's
   own bottom spacing, or the panel under it sits flush against the cards and
   the two read as one surface [seen in a screenshot at 390 and at 1280]. */
.stats-loose { margin-block-end: var(--s-5); }

.tabs { display: flex; gap: var(--s-1); margin-block-start: var(--s-5); border-block-end: 1px solid var(--hairline); flex-wrap: wrap; }
.tab {
  min-block-size: 44px; display: inline-flex; align-items: center; padding-inline: var(--s-4);
  color: var(--graphite-2); text-decoration: none; border-block-end: 2px solid transparent;
}
.tab.is-current { color: var(--ink); border-block-end-color: var(--ink); font-weight: 600; }

/* ── Panels, tables ──────────────────────────────────────────────────────── */
.panel { background: var(--surface); border-radius: var(--r-2); padding: var(--s-5); margin-block-end: var(--s-4); min-inline-size: 0; }
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-4); margin-block-end: var(--s-4); flex-wrap: wrap; }
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: var(--s-4); min-inline-size: 0; }

/* The one element allowed to scroll sideways, and only inside itself. */
.table-wrap { overflow-x: auto; max-inline-size: 100%; }
table { inline-size: 100%; border-collapse: collapse; font-size: var(--t-sm); }
caption { text-align: start; }
th { text-align: start; font-weight: 600; }
thead th {
  color: var(--graphite-2); font-weight: 400; font-size: var(--t-xs);
  padding-block: var(--s-2); padding-inline: var(--s-3);
  border-block-end: 1px solid var(--hairline); white-space: nowrap;
}
thead th:first-child, tbody tr > *:first-child { padding-inline-start: 0; }
thead th:last-child, tbody tr > *:last-child { padding-inline-end: 0; }
tbody th, tbody td {
  padding-block: var(--s-2); padding-inline: var(--s-3);
  border-block-end: 1px solid var(--hairline); block-size: 36px;
}
tbody tr:last-child th, tbody tr:last-child td { border-block-end: 0; }
.num-col { text-align: end; }

/* ── Controls ────────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-block-size: 44px; padding-inline: var(--s-4);
  border: 1px solid var(--hairline); border-radius: var(--r-1);
  background: var(--surface); color: var(--graphite);
  font: inherit; font-size: var(--t-sm); cursor: pointer;
  /* This class is worn by BUTTONS AND BY LINKS. A link that looks like a button
     and is underlined like prose reads as neither [seen on the confirmation
     page, where the only control is an anchor]. */
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.btn:hover { background: var(--surface-sunk); }
.btn-primary { background: var(--ink); border-color: var(--ink); color: #fff; }
.btn-primary:hover { background: var(--ink-hover); border-color: var(--ink-hover); }
.btn-danger { color: var(--review); border-color: var(--review); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }

/* A labelled control. The builder and the key editor are made of these. */
.field { display: flex; flex-direction: column; gap: var(--s-2); }
.field-head { display: flex; align-items: center; gap: var(--s-2); }
.field-head label { font-size: var(--t-sm); color: var(--graphite-2); }
/* The way out for a forgotten password, sitting where every product puts it:
   across from the label it rescues. Pushed to the far end by the margin, not
   by a wrapper, so field-head's own layout stays one row. */
.field-link { margin-inline-start: auto; font-size: var(--t-sm); color: var(--ink); }
.field-input {
  min-block-size: 44px; padding-inline: var(--s-3);
  border: 1px solid var(--hairline); border-radius: var(--r-1);
  background: var(--surface); color: var(--graphite);
  font: inherit; font-size: var(--t-sm); inline-size: 100%;
  transition: border-color var(--dur-fast) var(--ease-out);
}
.field-input:hover { border-color: var(--graphite-2); }
/* A file input carries the browser's own button inside it, which needs room
   the other controls do not: the 44px floor is the whole control here, not
   the text inside it. */
.field-file { padding-block: var(--s-2); line-height: 28px; }
/* A consequence of the current value, not a hint about how to fill it in. */
.field-note { margin: 0; font-size: var(--t-sm); color: var(--graphite-2); }
/* The sheet the builder's choices produce, drawn from the real layout.
   It is the only thing on the panel that can show a SHAPE decision: the
   letterhead band, where the white breaks fall and which side the name box
   starts from all move the paper and move no number worth rounding. */
.sheet-figure { margin: var(--s-5) 0 0; display: grid; justify-items: center; gap: var(--s-2); }
.sheet-preview {
  /* Height led, because the sheet is what it is: a portrait page whose aspect
     ratio comes from the paper size, so a width would fight it on A5 and A6. */
  block-size: 340px; max-inline-size: 100%;
  /* --surface, not a literal white: the token IS the product's paper. */
  background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-1);
}
.sheet-figure figcaption { font-size: var(--t-sm); color: var(--graphite-2); text-align: center; }

/* The columns collapse on a phone rather than shrinking a 44px control. */
.field-grid {
  display: grid; gap: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

/* The mandatory explanation marker beside every control and section title. */
.help { position: relative; display: inline-flex; }
.help-dot {
  inline-size: 16px; block-size: 16px; border-radius: 50%;
  border: 1px solid var(--hairline); background: var(--surface-sunk);
  color: var(--graphite-2); font: inherit; font-size: 11px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
}
/* 16px is the drawn size; the touch area is expanded to 44 without moving it. */
.help-dot::after { content: ''; position: absolute; inset: -14px; }
/* The mark itself, from the DOCUMENT'S language rather than from the markup.
   It was written into the HTML as the Arabic question mark, so every control on
   the English dashboard wore an Arabic glyph. Only Arabic among the eight
   languages uses it, so the Latin mark is the default and Arabic overrides,
   which is the same shape as the font rules at the top of this file. */
.help-dot::before { content: '?'; }
:lang(ar) .help-dot::before { content: '\61F'; }
.help-body {
  position: absolute; inset-block-start: calc(100% + 6px); inset-inline-start: 0;
  inline-size: max-content; max-inline-size: 260px;
  background: var(--graphite); color: var(--pearl); font-size: var(--t-xs); line-height: 1.5;
  padding: var(--s-2) var(--s-3); border-radius: var(--r-1);
  z-index: var(--z-popover);
  /* Removed from layout, not merely hidden: a visibility-hidden element still
     takes part in layout, and a 260px popover anchored beside a title near the
     page edge pushed the whole document wider than a phone. Found by
     screenshot, and only on the 390px pass. */
  display: none;
}
.help-dot:focus-visible + .help-body, .help-dot:hover + .help-body { display: block; }

/* ── Result language: the three reserved colours, always with a glyph ────── */
.bubble {
  display: inline-flex; align-items: center; justify-content: center; position: relative;
  inline-size: 28px; block-size: 28px; border-radius: 50%;
  border: 1.5px solid var(--hairline); font-size: var(--t-sm); font-family: var(--font-mono);
}
.bubble-correct { border-color: var(--correct); color: var(--correct); }
.bubble-wrong { border-color: var(--wrong); color: var(--wrong); }
.bubble-review { border-color: var(--review); color: var(--review); }
.bubble-blank { border: 0; color: var(--graphite-2); inline-size: 28px; }
.bubble-blank::before { content: '\2013'; }
.bubble-mark { position: absolute; inset-block-end: -2px; inset-inline-end: -2px; font-size: 10px; background: var(--surface); border-radius: 50%; line-height: 1; }

.marks { display: flex; flex-wrap: wrap; gap: 2px; }
.mark {
  inline-size: 20px; block-size: 20px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-sunk); border-radius: var(--r-1); font-size: var(--t-xs);
}
.mark-c { background: var(--ink-wash); color: var(--ink); }
.mark-u, .mark-d, .mark-e, .mark-x { color: var(--review); box-shadow: inset 0 0 0 1px var(--review); background: var(--surface); }

/* ── Signing in ──────────────────────────────────────────────────────────── */
/* Its own shell, not the dashboard's: the sidebar names four destinations and
   a sync state, and every one of those is a claim about an account that this
   page has no right to make yet. */
.login { min-block-size: 100dvh; display: grid; place-items: center; padding: var(--s-5); }
.login-card {
  background: var(--surface); border-radius: var(--r-2); padding: var(--s-6);
  inline-size: min(380px, 100%); display: flex; flex-direction: column; gap: var(--s-4);
  box-shadow: var(--lift-1);
}
.login-card h1 { font-size: var(--t-h2); margin: 0; }
/* RULE 2, AND IT COST A REWRITE. The first version of this wore the review
   amber, and the design system test refused it: the three reserved colours
   belong to a grading result and a failed sign in is not one. It reads better
   this way anyway. A wrong password is not a catastrophe, it is a correction,
   and the action colour says "look here" without borrowing the vocabulary a
   teacher reads marks in. */
/* The import refusal shares this rule rather than repeating it: a file this
   product will not read is the same kind of message as a password it will not
   accept, and two declarations of one treatment are two things that drift. */
.login-problem,
.report-refused {
  margin: 0; font-size: var(--t-sm); color: var(--ink);
  background: var(--ink-wash); border-radius: var(--r-1); padding: var(--s-3);
}
.login-card .btn { justify-content: center; }
.login-card .langs { margin: 0; }
/* A form living inside a card that is already a flex column: the form drops
   out of layout so its fields join the card's own gap rhythm, instead of
   arriving as one unspaced block. */
.card-form { display: contents; }

/* The two quiet lines on the door screens: the consent sentence over the
   signup button and the "other door" link under either button. Small and
   greyed because both are read once; the links inside stay real links. */
.login-agree, .login-alt { margin: 0; font-size: var(--t-sm); color: var(--graphite-2); }
.login-agree a, .login-alt a { color: var(--ink); }
.login-problem ul { margin: 0; padding-inline-start: var(--s-4); }
/* A select drawn as the text fields are, minus the parts a browser adds. */
select.field-input { appearance: auto; inline-size: 100%; }
.field-help { margin: var(--s-1) 0 0; font-size: var(--t-xs); color: var(--graphite-2); }
/* Signing out is a form so it can be a POST, and a form is a block element the
   button would otherwise stretch across. It is a quiet button because leaving
   is not the thing this screen wants anybody to do. */
.signout { margin: 0; }

/* The two documents a person reads before they have an account. A reading
   page, not a card: measure capped where lines stay scannable, headings that
   separate without shouting, and the body colour softened one step because
   nobody reads a legal page at label weight. */
.doc { max-inline-size: 72ch; margin-inline: auto; padding: var(--s-6) var(--s-4) var(--s-7); }
.doc h1 { display: block; font-size: var(--t-h1); margin: 0 0 var(--s-1); }
.doc-version { margin: 0 0 var(--s-6); color: var(--graphite-2); font-size: var(--t-sm); }
.doc section { margin-block-end: var(--s-5); }
.doc h2 { display: block; font-size: var(--t-h2); margin: 0 0 var(--s-2); }
.doc p { margin: 0 0 var(--s-3); color: var(--graphite-1); line-height: var(--lh-body); }
.doc ul { margin: 0 0 var(--s-3); padding-inline-start: var(--s-5); color: var(--graphite-1); }
.doc li { margin-block-end: var(--s-2); line-height: var(--lh-body); }

/* The first thing a new account sees. One paragraph, at reading width, because
   the whole screen is one instruction and anything else on it would compete. */
.empty-lead { margin: 0; max-inline-size: 60ch; color: var(--graphite-2); }

/* Two ways out of the preview, side by side and wrapping on a narrow screen.
   A link and a button sit in this row, so the link is given the button's own
   shape: what they do differs, what they weigh does not. */
.report-actions { display: flex; flex-wrap: wrap; gap: var(--s-3); margin: var(--s-4) 0 0; }

/* What an import just did. It sits inside the panel that took the file rather
   than above it, so the answer is where the question was asked, and the rows
   the file lost get a heading of their own: a count a teacher cannot act on is
   not a report. The refusal wears the ACTION colour and none of the three
   reserved ones, for the reason written above the sign in problem below: a
   file this product cannot read is a correction, not a grade. */
.report {
  margin-block-start: var(--s-5); padding-block-start: var(--s-5);
  border-block-start: 1px solid var(--hairline);
}
.report .stats { margin-block-start: var(--s-4); }
.report-file { margin: 0; font-size: var(--t-sm); color: var(--graphite-2); }
.report-title { margin: var(--s-5) 0 var(--s-3); font-size: var(--t-sm); }

/* The number under a pupil's name. It sits with the title rather than in the
   metric strip because it identifies them, it does not measure them. */
.student-id { margin: calc(var(--s-5) * -1) 0 0; font-size: var(--t-sm); color: var(--graphite-2); }

/* ── The answer key editor ───────────────────────────────────────────────── */
/* Level one is a row per question and nothing else: a number, its options, and
   what it is worth. PLAN.md 5ب-2 makes that binding, so nothing below adds a
   control to this row. Everything else lives inside the disclosure. */
.key-list { list-style: none; margin: 0; padding: 0; }
.key-q { border-block-end: 1px solid var(--hairline); padding-block: var(--s-1); }
.key-q:last-child { border-block-end: 0; }
/* Three columns and not a flex row with an auto margin: the mark belongs
   BESIDE its question, and pushing it to the far edge of a wide panel put a
   hand span of empty paper between a question and what it is worth. */
.key-head {
  display: grid; grid-template-columns: 3ch max-content minmax(0, 1fr);
  gap: var(--s-3); align-items: center;
}
.key-num { min-inline-size: 3ch; color: var(--graphite-2); font-size: var(--t-sm); }
.key-picks { border: 0; margin: 0; padding: 0; display: flex; gap: var(--s-1); flex-wrap: wrap; min-inline-size: 0; }
/* Rule 13: the circle stays 28px and the touch area around it becomes 44. */
.key-pick { display: inline-flex; align-items: center; justify-content: center; min-inline-size: 44px; min-block-size: 44px; }
.bubble-key { cursor: pointer; color: var(--graphite-2); background: var(--surface); }
.key-pick:hover .bubble-key { border-color: var(--graphite-2); }
/* Shaded the way a pencil shades it on paper, in ink. Rule 2 keeps the three
   result colours out of a key: green means a PUPIL was right, and there is no
   pupil in a key. Weight carries it as well as colour, so the shading survives
   a monochrome print and a colour blind reader alike. */
.key-input:checked + .bubble-key {
  background: var(--graphite); border-color: var(--graphite); color: var(--pearl); font-weight: 600;
}
/* Rule 14: the control is visually hidden, so the ring has to be drawn on the
   circle instead or the keyboard user loses their place entirely. */
.key-input:focus-visible + .bubble-key { outline: 2px solid var(--ink); outline-offset: 2px; }
.key-score { font-size: var(--t-sm); }
/* Rule 17 has ONE carrier in this product and this is it, resized. Giving the
   key its own "not there" class would have been a second thing meaning the
   same thing, and the two would disagree the first time one was restyled. */
.key-score .stat-missing { display: inline; font-size: var(--t-sm); }
/* Drawn small and hit big, the same trade the help marker makes: a question
   row that spent 44 drawn pixels on its SECOND action doubled the height of
   every row in the list, and a fifty question key is scrolled either way. */
.key-more summary {
  cursor: pointer; color: var(--ink); font-size: var(--t-xs);
  inline-size: max-content; padding-inline-start: calc(3ch + var(--s-3));
  position: relative; line-height: 1.6;
}
.key-more summary::after { content: ''; position: absolute; inset-block: -12px; inset-inline: 0; }
.key-editor { padding-block: var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-4); }
.key-awards { display: flex; flex-direction: column; gap: var(--s-2); }
.key-award { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: var(--s-2); align-items: center; }
.key-award-head span { font-size: var(--t-xs); color: var(--graphite-2); }
.key-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }
/* Hidden by the browser bundle when another version's tab is chosen. Without
   it every version is on the page, in order, which is complete and correct. */
.key-panel.is-hidden { display: none; }

/* ── Charts ──────────────────────────────────────────────────────────────── */
.chart { margin: 0; }
.chart figcaption { font-size: var(--t-sm); color: var(--graphite-2); margin-block-end: var(--s-3); }
/* The chart's coordinate space is left to right by construction: the x maths
   above assumes it, and its labels are numbers. Inheriting the page direction
   reorders "100%" and anchors it off the edge of the viewBox, which is the same
   mistake as mirroring an identifier. Verified by screenshot, not by reading. */
.chart-svg { inline-size: 100%; block-size: auto; direction: ltr; }
.c-bar { fill: var(--ink); }
.c-band { fill: var(--ink-wash); }
.c-band-label, .c-tick, .c-note, .c-label { fill: var(--graphite-2); font-size: 11px; font-family: var(--font-mono); }
.c-label { text-anchor: end; }
.c-note { text-anchor: end; }
.c-flag { text-anchor: middle; }
.c-tick { text-anchor: middle; }
.c-flag { fill: var(--ink); font-size: 12px; }
.c-grid { stroke: var(--hairline); stroke-width: 1; }
.chart-empty { color: var(--graphite-2); font-size: var(--t-sm); margin: 0; }

/* ── Small screens ───────────────────────────────────────────────────────── */
@media (max-width: 900px) {
  /* The sidebar becomes a top bar. Same four destinations, same order, same
     single level: a small screen gets a different shape, never a different
     information architecture. */
  /* ROWS, NOT JUST ONE COLUMN. The 100dvh floor on the grid stretches its rows
     to fill the screen, which is invisible while the sidebar is a COLUMN beside
     the content and becomes half an empty screen the moment it stacks above it:
     the top bar took its equal share of the viewport and the first thing on
     every dashboard page was a blank band [seen in a screenshot at 390]. The
     bar is worth its content, and the content takes the rest. */
  .shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr; }
  .side {
    flex-direction: column; gap: var(--s-3);
    border-inline-end: 0; border-block-end: 1px solid var(--hairline);
    padding: var(--s-3) var(--s-4);
  }
  .side::before { inset-inline: 0; inset-block-start: 0; inset-block-end: auto; inline-size: auto; block-size: 8px;
    background: repeating-linear-gradient(to right, var(--surface-sunk) 0 7px, transparent 7px 20px); }
  /* Four destinations, two rows, and none of them hidden. What this replaced
     was a sideways scroll inside the bar, which fitted two and a half of the
     four into 390px: the third was cut mid word and the fourth was entirely off
     the screen, with no scrollbar and no fade to say so [seen in a screenshot at
     390, and measured there: 526px of items in a 358px box]. A scroll region
     nobody can see is a scroll region nobody finds, and the two it hid were the
     sheets screen and the settings. Two rows cost 44px of chrome once. */
  .nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-1); }
  .nav-item { min-block-size: 44px; white-space: nowrap; }
  .side-foot { margin-block-start: 0; display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }
  .side-foot p { margin: 0; font-size: var(--t-xs); }
  .langs { margin: 0; }
  .main { padding: var(--s-4); }
  .screen-title { align-items: flex-start; }
  h1 { font-size: var(--t-h2); }
  /* Two metric cards per row: four in a column is a scroll before any data. */
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-value { font-size: var(--t-h1); }
  .panel { padding: var(--s-4); }
  .tabs { overflow-x: auto; flex-wrap: nowrap; }
  .tab { white-space: nowrap; }
  .marks { flex-wrap: nowrap; }
}

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