/* 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; }
/* A figure that is withheld rather than absent: the quiet colour, because the
   amber of a missing value would read as a fault on every row of a column that
   is off on purpose [seen in a screenshot of the question table]. */
.stat-held { color: var(--graphite-2); }
.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. */
/* The builder's two halves, side by side from the width where both fit: the
   controls decide, the sheet answers, and a teacher sees the answer without
   scrolling past the question. The sheet column sticks while the controls
   scroll, because it is the thing being watched. */
.builder-grid { display: grid; gap: var(--s-4); align-items: start; min-inline-size: 0; }
@media (min-width: 60rem) {
  .builder-grid { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .builder-side { position: sticky; inset-block-start: var(--s-4); }
}
/* Two actions that belong together, in one group rather than spread apart. */
.btn-row { display: flex; gap: var(--s-2); flex-wrap: wrap; }
/* A form's own actions, at its foot: where a person looks once the fields are
   filled, with a margin that separates them from the last field rather than
   letting them read as another one. */
.form-actions { margin-block-start: var(--s-4); }

/* THE CAMERA SCREEN. The preview is the one place in this product where a
   picture is the interface, so it gets the width and the paper ratio a sheet
   actually has; everything around it stays the same quiet card as every other
   screen. No result colour anywhere: a scanned paper is a line in a list, and
   the review flag is a word (design system rule 2). */
.scan-stage { display: grid; gap: var(--s-3); }
.scan-frame { position: relative; inline-size: 100%; max-inline-size: 34rem; }
/* CONTAIN, NOT COVER, and it is a correctness rule rather than taste: the
   engine reads the WHOLE frame, and a cover-cropped preview shows the teacher
   less than the engine sees, so they aim at a lie. What the preview shows must
   be exactly what is read [the owner found real papers hard to aim; every
   viewfinder is WYSIWYG for this reason]. */
.scan-video {
  display: block; inline-size: 100%; aspect-ratio: 3 / 4;
  background: var(--graphite); border-radius: var(--r-1);
  object-fit: contain;
}
/* The four aiming corners, echoing the sheet's own corner squares: the teacher
   lines the paper's corners up with these. Decoration to a screen reader
   (empty spans), guidance to a hand. */
.scan-corner { position: absolute; inline-size: 22px; block-size: 22px; border: 3px solid var(--surface); opacity: 0.85; }
.scan-corner.c-tl { inset-block-start: 6%; inset-inline-start: 6%; border-block-end: 0; border-inline-end: 0; }
.scan-corner.c-tr { inset-block-start: 6%; inset-inline-end: 6%; border-block-end: 0; border-inline-start: 0; }
.scan-corner.c-bl { inset-block-end: 6%; inset-inline-start: 6%; border-block-start: 0; border-inline-end: 0; }
.scan-corner.c-br { inset-block-end: 6%; inset-inline-end: 6%; border-block-start: 0; border-inline-start: 0; }
/* The status ON the viewfinder, where the eyes are while aiming. */
.scan-status {
  position: absolute; inset-block-end: 0; inset-inline: 0; margin: 0;
  padding: var(--s-2) var(--s-3); text-align: center;
  color: var(--surface); background: color-mix(in srgb, var(--graphite) 62%, transparent);
  border-end-start-radius: var(--r-1); border-end-end-radius: var(--r-1);
  font-size: var(--t-sm); min-block-size: 1.4em;
}
/* The result card over the camera, held while the gate is locked: the score a
   teacher reads without looking away from the paper in their hand. */
.scan-flash {
  /* Centered with auto margins, NOT a 50% logical offset: inline-start is the
     RIGHT in RTL, so a 50% start plus a translate pushed the card off the
     frame's left edge [seen in a screenshot]. Auto margins in a full-bleed box
     need no direction at all. */
  position: absolute; inset: 0; margin: auto;
  inline-size: fit-content; block-size: fit-content;
  display: grid; justify-items: center; gap: var(--s-1);
  background: var(--surface); border-radius: var(--r-2);
  padding: var(--s-4) var(--s-5); box-shadow: var(--lift-2);
  text-align: center; max-inline-size: 86%;
}
.scan-flash-score { font-size: var(--t-display); line-height: 1.1; color: var(--ink); }
.scan-flash-who { font-weight: 600; overflow-wrap: anywhere; }
.scan-flash-review { display: inline-flex; align-items: center; gap: var(--s-1); font-size: var(--t-sm); }
/* The written name box on the card: the handwriting IS the identity of a
   paper the grid could not name. */
.scan-flash-snip { max-inline-size: 220px; max-block-size: 64px; border: 1px solid var(--hairline); border-radius: var(--r-1); background: var(--surface); }
/* The paper page's pictures: the whole sheet scrolls inside its own card, and
   the name snip stays handwriting-sized. */
.paper-shot { display: block; inline-size: 100%; max-inline-size: 34rem; border: 1px solid var(--hairline); border-radius: var(--r-1); }
.paper-name-shot { display: block; max-inline-size: 22rem; border: 1px solid var(--hairline); border-radius: var(--r-1); }
.scan-list { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s-2); }
.scan-line {
  display: flex; align-items: baseline; gap: var(--s-2); flex-wrap: wrap;
  padding-block: var(--s-2); border-block-end: 1px solid var(--hairline);
}
.scan-mark { margin-inline-start: auto; }
/* The review flag is a WORD beside the review bubble, not a colour of its own:
   design system rule 2 reserves the amber for a grading outcome carried by a
   glyph, and the line already carries that glyph. */
.scan-flag { color: var(--graphite-2); font-size: var(--t-sm); }
.is-hidden { display: none; }

/* ── The student sitting pages (docs/ONLINE-EXAMS.md section 8) ──────────────
   Mobile first, because the default reader is a child on a phone: one column
   that starts at 375px and merely gains air on a laptop. NOTHING SCROLLS
   SIDEWAYS AND NO ANSWER IS EVER CUT (owner rule 8-7): a bubble row that runs
   out of width WRAPS. The bubbles here are true answer bubbles, which is what
   the design system reserves circles for. */
/* A timestamp is longer than any measurement the stats row was sized for, and
   at the stat's own size it clipped its leading digit [seen in a screenshot of
   the board]. It is a fact to read, not a figure to compare, so it takes the
   body size and fits. */
.sitting-when .stat-value { font-size: var(--t-body); }
/* The address a teacher writes on the board: readable across a classroom. */
.sitting-url { font-size: var(--t-h2); }

.sit-shell { max-inline-size: 640px; margin-inline: auto; padding: var(--s-4); position: relative; }
.sit-card {
  background: var(--surface); border-radius: var(--r-3); box-shadow: var(--lift-1);
  padding: var(--s-5); position: relative;
}
.sit-head { padding-block: var(--s-4); }
.sit-head h1 { font-size: var(--t-h1); }
.sit-eyebrow { color: var(--graphite-2); font-size: var(--t-sm); }
.sit-help { color: var(--graphite-2); font-size: var(--t-sm); margin-block-start: var(--s-2); }
/* A fault the student can act on. The COLOUR comes from stat-missing in the
   markup (rule 17's fault word), never from here: rule 2's test holds the
   reserved tokens to their allow list and this selector is not on it. */
.sit-problem { font-size: var(--t-sm); margin-block-start: var(--s-2); }
.sit-facts { display: flex; flex-wrap: wrap; gap: var(--s-4); margin-block-start: var(--s-2); }
.sit-enter { display: grid; gap: var(--s-3); margin-block-start: var(--s-4); }
.sit-enter input {
  font-size: var(--t-h2); padding: var(--s-3); text-align: center;
  border: 1px solid var(--hairline); border-radius: var(--r-2); min-block-size: 48px;
}
.sit-questions { list-style: none; margin-block-start: var(--s-2); }
.sit-question {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3);
  padding-block: var(--s-3); border-block-start: 1px solid var(--hairline);
}
.sit-question:first-child { border-block-start: 0; }
.sit-qn { min-inline-size: 4.5em; }
.sit-bubbles { display: flex; flex-wrap: wrap; gap: var(--s-2); }
/* The bubble is the touch target, and it does not go under 48px: it is
   pressed by a child's thumb (rule 8-7, and the 44pt floor everywhere). */
.sit-bubble { position: relative; display: inline-grid; }
.sit-bubble input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.sit-bubble span {
  inline-size: 48px; block-size: 48px; border-radius: 50%;
  border: 2px solid var(--graphite-2); display: grid; place-items: center;
  font-size: var(--t-body); font-variant-numeric: tabular-nums; background: var(--surface);
}
.sit-bubble input:checked + span { background: var(--ink); border-color: var(--ink); color: #fff; }
.sit-bubble input:focus-visible + span { outline: 2px solid var(--ink); outline-offset: 2px; }
.sit-clear {
  margin-inline-start: auto; border: 0; background: none; color: var(--graphite-2);
  font-size: var(--t-xs); cursor: pointer; padding: var(--s-2); min-block-size: 44px;
}
.sit-gate { margin-block-start: var(--s-3); }
.sit-gate.is-hidden { display: none; }
.sit-submit { inline-size: 100%; margin-block-start: var(--s-4); min-block-size: 48px; }
.sit-submit:disabled { opacity: 0.5; cursor: not-allowed; }
.sit-done { text-align: center; padding-block: var(--s-6); }
.sit-score-label { color: var(--graphite-2); margin-block-start: var(--s-4); }
.sit-score { font-size: var(--t-display); font-weight: 600; font-variant-numeric: tabular-nums; }
/* The deterrence watermark (section 10): the seat's own tag, tiled faintly
   OVER the whole page, the way a video wears one. Over, not under: a
   screenshot of the card must carry the tag, or the tag deters nothing.
   Quiet enough to never fight the questions, and untouchable to the finger. */
.sit-mark {
  position: fixed; inset: 0; pointer-events: none; z-index: 2; overflow: hidden;
  display: grid; grid-template-columns: repeat(3, 1fr); align-content: space-evenly;
  justify-items: center; opacity: 0.09; color: var(--graphite);
}
.sit-mark span { transform: rotate(-25deg); font-size: var(--t-sm); white-space: nowrap; }
/* An author display declaration beats the browser's own rule for the hidden
   attribute, because author declarations outrank user agent ones. Every
   element in this file that is hidden by script and given a display needs
   this line, and the builder is where it was found: a refused sheet kept the
   last paper that worked on screen while the words above it said it does not
   fit [seen in a screenshot; no assertion could see it, because the markup
   was already right]. */
[hidden] { display: none !important; }
.sheet-figure { margin: var(--s-5) 0 0; display: grid; justify-items: center; gap: var(--s-2); }
.sheet-art { display: grid; justify-items: center; inline-size: 100%; }
.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.
     Taller than the schematic it replaced: this is the real sheet now, and a
     teacher is meant to READ it before they print it. */
  block-size: min(62vh, 520px); 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));
}

/* Controls in named groups rather than one long grid: the questions, then the
   header a student fills, then the paper. A fieldset, because that is the
   element a screen reader announces the group name from. The zero minimum
   inline size is not cosmetic: a fieldset defaults to min-content width and
   would refuse to let its grid collapse on a phone. */
.field-groups { display: grid; gap: var(--s-5); }
.field-group {
  margin: 0; min-inline-size: 0;
  border: 1px solid var(--hairline); border-radius: var(--r-1);
  padding: var(--s-4); padding-block-start: var(--s-2);
}
.field-group > legend {
  padding-inline: var(--s-2); font-weight: 600; color: var(--ink); font-size: var(--t-sm);
}
.field-group-help {
  margin: 0 0 var(--s-4); color: var(--graphite-2); font-size: var(--t-sm);
}

/* 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); }
/* The ready sheets, each shown as itself: the drawing, the name, the facts
   read from the spec, and the download. The drawing sits on the sunk surface
   the way a paper sits on a desk, and its height is fixed so three different
   paper sizes make one even row of cards. */
.sheet-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: var(--s-4); }
.sheet-card { background: var(--surface); border-radius: var(--r-2); padding: var(--s-4); box-shadow: var(--lift-1); display: flex; flex-direction: column; gap: var(--s-3); }
.sheet-fig { margin: 0; display: flex; justify-content: center; align-items: center; block-size: 180px; background: var(--surface-sunk, var(--pearl)); border-radius: var(--r-1); padding: var(--s-3); }
.sheet-fig svg { max-block-size: 100%; max-inline-size: 100%; height: 100%; width: auto; box-shadow: var(--lift-1); }
.sheet-card h3 { margin: 0; }
.sheet-blurb { margin: 0; color: var(--graphite-2); font-size: var(--t-sm); }
.sheet-facts { margin: 0; display: grid; gap: var(--s-1); }
.sheet-fact { display: flex; justify-content: space-between; gap: var(--s-3); font-size: var(--t-sm); }
.sheet-fact dt { color: var(--graphite-2); }
.sheet-fact dd { margin: 0; font-variant-numeric: tabular-nums; }
.sheet-get { margin: auto 0 0; }
.sheet-get .btn { inline-size: 100%; justify-content: center; }
.sheet-custom { justify-content: center; }
.sheet-open { margin: 0; }
.sheet-open .btn { inline-size: 100%; justify-content: center; }

/* 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; }
}
