/* ===========================================================================
   ILO FLOW — polish layer
   ===========================================================================
   Loaded LAST, so it refines what the earlier sheets establish rather than
   competing with them. It deliberately does not restate the design language:
   the ledger palette, the Owe/Entrust spine and the Fraunces/Inter/Plex
   pairing in tokens.css are the identity, and this file exists to make that
   identity render correctly rather than to replace it.

   Everything here is either a domain-specific correctness fix (figures that
   must align in columns, statements that must survive being printed) or a
   refinement that earlier sheets had no single place to put.
   =========================================================================== */


/* ---------------------------------------------------------------------------
   1. TABULAR FIGURES — the most consequential fix in this file
   ---------------------------------------------------------------------------
   Inter and Fraunces both ship PROPORTIONAL numerals by default, meaning a '1'
   is visibly narrower than a '8'. In ordinary prose that is correct and looks
   better. In a column of money it is a defect: the digits stop lining up
   vertically, so 1,111.11 and 8,888.88 render at different widths and the
   decimal points drift out of alignment down the column.

   For an accounting product that is not cosmetic. Scanning a trial balance
   for a transposition error, or checking that a column foots, depends on the
   eye being able to run straight down the digits. Misaligned figures make the
   exact skill the app teaches harder to perform inside the app itself.

   `tabular-nums` was already applied — but only to two cash-flow selectors,
   which is how this reads as an oversight rather than a decision. Extended
   here to every element that carries a figure.

   `slashed-zero` disambiguates 0 from O, which matters in account codes. */
.journal-balance,
.ledger-strip,
.record-total,
.checkout-total,
.statement-row,
.statement-total,
.statement-check,
.taccount-balance,
.trial-balance-total,
.cashflow-activity-total,
.jp-amount,
.amount,
input[type="number"],
[data-amount],
[data-figure] {
  font-variant-numeric: tabular-nums slashed-zero;
  font-feature-settings: "tnum" 1, "zero" 1;
}

/* The mono face is already fixed-width, but IBM Plex Mono's default zero is
   unslashed; turning it on keeps account codes unambiguous. */
.mono,
code,
kbd,
samp,
[class*="-code"] {
  font-feature-settings: "zero" 1;
}

/* Money should never wrap mid-figure, and a minus sign should never be
   orphaned onto its own line. */
.statement-total,
.trial-balance-total,
.record-total,
.checkout-total,
.taccount-balance {
  white-space: nowrap;
}


/* ---------------------------------------------------------------------------
   2. TYPOGRAPHIC RENDERING
   ---------------------------------------------------------------------------
   Fraunces is loaded from Google Fonts with its optical-size axis exposed
   (`opsz,wght@9..144`) — see the font <link> in index.html — but nothing in
   the CSS ever drove that axis, so every heading rendered at the default
   optical size regardless of how large it was set. Enabling automatic optical
   sizing is what the axis is for: the face subtly reduces stroke contrast and
   opens spacing at small sizes, and tightens both at display sizes.

   This is the difference between a serif that looks bought and one that looks
   chosen, and it costs one declaration because the font was already being
   downloaded with the axis included. */
h1, h2, h3, .wordmark b, .spine-side h3 {
  font-optical-sizing: auto;
}

html {
  /* Greyscale antialiasing on dark backgrounds. Subpixel rendering fringes
     light-on-dark text with colour and makes the parchment tone look muddy;
     this is the standard correction for a dark-first interface. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* Headings set tight benefit from kerning and ligatures being explicit. */
h1, h2, h3 {
  font-kerning: normal;
  font-variant-ligatures: common-ligatures;
}

/* Long-form explanation copy reads better with a slightly looser measure and
   hanging punctuation where supported. */
.lesson-body p,
.spine-side p,
.card p {
  text-wrap: pretty;          /* avoids typographic orphans; ignored elsewhere */
  hanging-punctuation: first;
}

/* Headings should not break awkwardly across lines. */
h1, h2, h3 {
  text-wrap: balance;
}


/* ---------------------------------------------------------------------------
   3. SELECTION
   ---------------------------------------------------------------------------
   The browser default is a blue that belongs to no part of this palette and
   is the single most jarring moment in an otherwise controlled dark theme —
   and users select text constantly in a learning product, copying figures and
   account names. Entrust gold, at a weight that keeps text readable rather
   than inverting it. */
::selection {
  background: rgba(217, 164, 65, .28);
  color: var(--parchment);
  text-shadow: none;
}
::-moz-selection {
  background: rgba(217, 164, 65, .28);
  color: var(--parchment);
  text-shadow: none;
}


/* ---------------------------------------------------------------------------
   4. INTERACTION FEEDBACK
   ---------------------------------------------------------------------------
   Hover states already exist. Press states largely did not — so on touch,
   where there is no hover at all, a tap produced no acknowledgement until the
   next screen rendered. On a slow connection that reads as an unresponsive
   button and provokes a second tap. */
.btn:active,
button:not(:disabled):active,
[role="button"]:active {
  transform: translateY(1px) scale(.995);
  transition-duration: .06s;
}

/* Interactive cards lift slightly. Kept small — this is a ledger, not a
   dashboard, and the restraint is the point. */
.card:hover,
[data-nav]:hover {
  transition: transform .18s var(--ease), border-color .18s var(--ease);
}

/* Disabled controls must look unavailable, not merely faded, or learners keep
   clicking them. */
.btn:disabled,
button:disabled {
  opacity: .45;
  cursor: not-allowed;
  filter: saturate(.4);
}
.btn:disabled:active {
  transform: none;
}

/* Touch targets. WCAG 2.2 (2.5.8) sets a 24px minimum; 44px is the Apple HIG
   figure and the one that actually feels right under a thumb. Applied only on
   coarse pointers so the desktop layout is untouched. */
@media (pointer: coarse) {
  .btn,
  button,
  [role="button"],
  .home-jumpnav a {
    min-height: 44px;
  }
  .btn-sm {
    min-height: 40px;
  }
}


/* ---------------------------------------------------------------------------
   5. FOCUS
   ---------------------------------------------------------------------------
   base.css already sets a gold :focus-visible ring, which is good. What it
   lacks is a contrasting inner edge — on a panel that is itself gold-tinted
   (the Entrust side of the spine, or a hovered jump-nav pill) a gold ring on
   gold is nearly invisible, so keyboard users lose their place exactly where
   the interface is most decorated. The dark inner outline guarantees an edge
   against any background. */
:focus-visible {
  outline: 2px solid var(--entrust);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--ink);
}

/* Skip link — present for keyboard and screen-reader users, visually absent
   until focused. The app is a long scrolling syllabus behind a persistent
   topbar, which is precisely the case where tabbing through the nav on every
   screen becomes punishing. */
.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: -100px;
  z-index: 100;
  padding: 12px 20px;
  background: var(--entrust);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 14px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: top .18s var(--ease);
}
.skip-link:focus {
  top: var(--sp-4);
}


/* ---------------------------------------------------------------------------
   6. LOADING STATES
   ---------------------------------------------------------------------------
   The app fetches profile, access and progress on boot, and payment options
   on the premium screen. There was no skeleton treatment anywhere, so those
   regions were simply empty until data arrived — which is indistinguishable
   from "broken" on a slow connection. */
.skeleton {
  position: relative;
  overflow: hidden;
  background: var(--ledger-2);
  border-radius: var(--radius-sm);
  color: transparent !important;
  user-select: none;
  pointer-events: none;
}
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    rgba(237, 231, 216, .06),
    transparent
  );
  animation: ilo-shimmer 1.4s infinite;
}
@keyframes ilo-shimmer {
  100% { transform: translateX(100%); }
}
.skeleton-line {
  height: 1em;
  margin: .35em 0;
}
.skeleton-line.is-short { width: 40%; }
.skeleton-line.is-mid   { width: 65%; }

/* A button mid-request should say so rather than looking idle. */
.btn.is-busy {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
.btn.is-busy::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 15px;
  height: 15px;
  margin: -7.5px 0 0 -7.5px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--parchment);
  animation: ilo-spin .6s linear infinite;
}
@keyframes ilo-spin {
  to { transform: rotate(360deg); }
}


/* ---------------------------------------------------------------------------
   7. BALANCED / UNBALANCED — the app's most important binary
   ---------------------------------------------------------------------------
   Whether an entry balances is the single most important piece of feedback
   the product gives, and the existing treatment carried it largely through
   colour. Roughly 1 in 12 men has a red-green colour vision deficiency, and
   the app's danger red against its owe teal is close to the worst-case pair
   for deuteranopia.

   Adding a glyph and a weight change means the state survives being seen in
   greyscale, printed, or by a colour-blind learner — without removing the
   colour, which is doing useful work for everyone else. */
/* NOT on a table row.
   
   A ::before on a <tr> does not sit inside the first cell — the browser wraps
   it in an ANONYMOUS TABLE CELL, which shifts every real cell one column to
   the right. On the trial balance that put the Debit total under the Credit
   heading and pushed the Credit total off the edge of the table entirely,
   with a stray "=" floating at the left of the row.
   
   Every other use of these classes is on a <span>, <em> or <div>, where the
   glyph does exactly what it was written for. The trial balance loses nothing:
   its card header already says "✓ BALANCED" in words.

   NOT on .journal-balance either, for the same underlying reason as the <tr>
   case. .journal-balance is a CSS GRID container (Journal Practice's Total
   Debit / Total Credit / Difference / Balanced bar) and also carries this
   same class, purely to drive its own background/animation — it has no text
   of its own. A generated ::before on a grid container becomes a real, first
   grid item, which silently shifted every one of the bar's four children over
   by one cell: Total Debit rendered in the Total Credit slot, Difference
   rendered where the Balanced pill should be, and a stray unlabelled "=" sat
   alone in the first cell. The individual value (<strong class="is-balanced">)
   and the pill (<em class="is-balanced">) inside that same bar are inline
   text elements, not containers, so they still get the glyph as intended. */
.is-balanced:not(tr):not(.journal-balance)::before,
.is-unbalanced:not(tr):not(.journal-balance)::before {
  font-family: var(--font-mono);
  font-weight: 600;
  margin-right: .5ch;
}
.is-balanced:not(tr):not(.journal-balance)::before {
  content: "=";
  color: var(--owe);
}
.is-unbalanced:not(tr):not(.journal-balance)::before {
  content: "\2260";        /* ≠ */
  color: var(--danger);
}
.is-unbalanced {
  font-weight: 600;
}


/* ---------------------------------------------------------------------------
   8. HIGH CONTRAST
   ---------------------------------------------------------------------------
   The palette is deliberately low-contrast and warm — correct for the ledger
   metaphor, and comfortable for long study sessions. But `parchment-dim`
   (#9B9789) on `ledger` (#14171F) sits around 5.9:1, which passes AA for body
   text and fails AAA, and users who have asked their OS for more contrast
   have asked for a reason. Honour it rather than overriding their choice. */
@media (prefers-contrast: more) {
  :root {
    --parchment-dim: #C8C3B4;
    --rule: #4A5262;
    --rule-soft: #3A4150;
  }
  .btn {
    border-width: 2px;
  }
  :focus-visible {
    outline-width: 3px;
  }
}

/* Forced-colors (Windows high contrast) strips author colours entirely.
   Without this, the mask-image nav icons vanish, because their visible pixels
   come from a background-color that the mode discards. */
@media (forced-colors: active) {
  .nav-icon {
    background: CanvasText;
  }
  .btn {
    border: 1px solid ButtonText;
  }
  .spine::before {
    background: CanvasText;
  }
}


/* ---------------------------------------------------------------------------
   9. MOTION
   ---------------------------------------------------------------------------
   base.css already collapses animation and transition durations under
   prefers-reduced-motion. It does not stop the two INFINITE animations
   introduced above, where a near-zero duration means "spin extremely fast
   forever" rather than "stop" — worse for a vestibular user than the original
   animation. Both are neutralised explicitly. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  .skeleton::after {
    animation: none;
    opacity: .5;
  }
  .btn.is-busy::after {
    animation: none;
    border-top-color: currentColor;
    opacity: .6;
  }
  .btn:active,
  button:not(:disabled):active {
    transform: none;
  }
}


/* ---------------------------------------------------------------------------
   10. PRINT
   ---------------------------------------------------------------------------
   There were no print styles at all, which for THIS product is a genuine gap
   rather than a nicety. Financial statements, trial balances and journal
   entries are artefacts people print, hand in, and mark. Printing the app as
   it stood meant a near-black background across every page: unreadable on
   most printers, and a serious waste of toner for a learner in a school
   computer lab — a plausible deployment for a Nairobi-built accounting
   teaching tool.

   Inverted to ink-on-paper, chrome removed, and the ledger structure kept. */
@media print {
  @page {
    margin: 18mm;
  }

  html, body {
    background: #fff !important;
    color: #111 !important;
  }

  /* Interface chrome carries no meaning on paper. */
  .topbar,
  .topbar-actions,
  .home-jumpnav,
  .skip-link,
  .btn,
  button,
  nav,
  [data-nav],
  .learning-topbar-track {
    display: none !important;
  }

  .app {
    max-width: none;
    padding: 0;
  }

  /* Panels become ruled boxes rather than filled ones. */
  .card,
  .spine,
  .statement,
  .taccount,
  [class*="panel"] {
    background: #fff !important;
    border: 1px solid #999 !important;
    box-shadow: none !important;
    color: #111 !important;
  }

  h1, h2, h3, p, span, td, th, div, b, strong {
    color: #111 !important;
  }

  /* Debit and credit must stay distinguishable in black and white, so they
     are re-encoded as weight rather than hue. */
  .spine-side.is-owe .spine-tag,
  .spine-side.is-entrust .spine-tag {
    color: #111 !important;
    font-weight: 700;
  }
  .spine-side.is-owe,
  .spine-side.is-entrust {
    background: none !important;
  }

  /* A statement split across a page break is unmarkable. */
  .statement,
  .taccount,
  .journal-entry,
  .trial-balance {
    break-inside: avoid;
    page-break-inside: avoid;
  }

  h1, h2, h3 {
    break-after: avoid;
    page-break-after: avoid;
  }

  /* Printed links are useless without their target. */
  a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 10pt;
    color: #555;
  }

  .skeleton::after,
  .btn.is-busy::after {
    display: none !important;
  }
}


/* ---------------------------------------------------------------------------
   11. SCROLL & OVERSCROLL
   --------------------------------------------------------------------------- */

/* Stops a scroll gesture inside a modal or drawer from chaining to the page
   behind it once the inner element hits its end — the "scrolling the wrong
   thing" feeling, and on iOS the cause of accidental pull-to-refresh. */
.modal,
.drawer,
.sheet,
[role="dialog"] {
  overscroll-behavior: contain;
}

/* Anchored sections sit below the sticky jump-nav rather than under it.
   base.css sets this for four specific ids; generalised so a new section does
   not silently scroll to the wrong place. */
[id^="home-"],
section[id] {
  scroll-margin-top: 72px;
}


/* ---------------------------------------------------------------------------
   12. SMALL CORRECTIONS
   --------------------------------------------------------------------------- */

/* Images should never blow out a narrow layout. */
img, svg, video, canvas {
  max-width: 100%;
  height: auto;
}

/* Screen-reader-only utility, for text that must be announced but not shown
   (see the live region added in index.html). */
.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* iOS zooms the viewport when a focused input's font-size is below 16px.
   Several inputs in the journal practice grid are set smaller for density,
   which made every tap on an amount field jerk the page. */
@media (pointer: coarse) {
  input, select, textarea {
    font-size: max(16px, 1em);
  }
}

/* A consistent caret colour; the browser default is stark white against the
   warm palette. */
input, textarea {
  caret-color: var(--entrust);
}


/* ---------------------------------------------------------------------------
   13. SEMANTIC HEADING PROMOTION — visual metrics preserved
   ---------------------------------------------------------------------------
   Eight of ten screens had no <h1>, and three began at <h3>, so a screen-reader
   user navigating by heading got no document anchor. Fixing that meant changing
   tag levels — but h1 is set at clamp(42px, 7vw, 76px) against h2 at
   clamp(21px, 5.4vw, 32px), so a bare tag swap would have visibly redesigned
   the product.

   These two classes carry the metrics of the level each element previously
   was, so the semantic level changes and the rendering does not.

   .screen-title  an <h1> that must LOOK like the <h2> it replaced
   .hero-title    an <h2> that must LOOK like the <h1> it replaced

   Values are copied from the base h1/h2 rules rather than re-derived, so the
   two stay in step if the type scale is ever retuned. */
h1.screen-title {
  margin: 9px 0 9px;
  font-family: var(--font-display);
  font-size: clamp(21px, 5.4vw, 32px);
  line-height: 1.18;
  letter-spacing: -.01em;
}

h2.hero-title {
  margin: 15px 0 5px;
  font-family: var(--font-display);
  font-size: clamp(1.8rem, 3vw, 2.65rem);
  line-height: 1.08;
  letter-spacing: -.015em;
}

/* An inline style="font-size:30px" on the promoted onboarding title already
   wins over both, so that screen needs nothing further. */

/* ---------------------------------------------------------------------------
   13. RESPONSIVE HARDENING
   ---------------------------------------------------------------------------

   Everything below is a fix for a failure that was MEASURED in a real browser
   across the 320-1920px matrix, not a preference. Each rule names the
   measurement that produced it. Nothing here changes the visual identity, the
   information architecture or any screen's layout at a width where that
   layout already fitted — these are the widths where it did not.

   This block lives in polish.css because polish.css is loaded last (asserted
   by tools/qa-ui-dom.js) and several of the rules being corrected are
   re-declared UNCONDITIONALLY by later sections of screens.css, which beats an
   earlier @media block on cascade order no matter how narrow the viewport is.
   That is the actual root cause of the Error Hunter failure below, so a fix
   placed anywhere earlier would have lost the same argument.
   --------------------------------------------------------------------------- */


/* --- 13.1 The top navigation must never leave a control off-screen ---------

   MEASURED: .topbar-actions is a non-wrapping flex row. At 390px it laid out
   571px wide inside a 358px container, putting "Academy" at 335-463 and
   "Restart" at 471-587 — both entirely outside the viewport. Because
   html,body carry overflow-x:hidden (base.css, "UNIVERSAL DEVICE RESILIENCE")
   the page did not scroll to reveal them: they were silently clipped, and
   document.elementFromPoint at their own centres returned nothing. Two
   navigation controls simply did not exist on any phone.

   The same row overflowed on learning screens at 768-1180px, where the
   learning stage track claims a 320-420px minimum beside it: at 1024px
   "Academy" and "Restart" were off-screen on a laptop.

   Wrapping is the whole fix. The buttons keep their size, their pill styling
   and their order; they flow onto a second row instead of off the edge. */
.topbar-actions{
  flex-wrap: wrap;
  justify-content: flex-end;
  row-gap: var(--sp-2);
  min-width: 0;
  /* max-width is what actually makes the wrap happen. The row is a flex ITEM
     of .topbar with flex:0 0 auto (and flex-shrink:0 on learning screens), so
     its used width is its max-content width - 410px inside a 296px topbar at
     320px - and children that never run out of room never wrap. Capping it to
     the container is the one line that turns "wrap allowed" into "wrap". */
  max-width: 100%;
}

/* A flex item's default min-width:auto let these buttons shrink below their
   own label and then clip it — measured at 320px as a 100px label inside a
   90px button ("Journal" rendered as "Journ"). With the row wrapping they no
   longer need to shrink, and this makes that explicit. */
.topbar-actions .btn{
  flex: 0 0 auto;
  min-width: 0;
}

/* The learning stage track carries min-width:420px (320px below 980px), which
   is more than the row can spare anywhere under 1280px. base.css already
   drops it onto its own line below 760px; this is that same rule, applied at
   the width where the overflow actually begins. Above 1280px nothing changes. */
@media (max-width: 1279px){
  .topbar{ flex-wrap: wrap; }
  .topbar.is-learning .learning-topbar-track{
    order: 3;
    flex-basis: 100%;
    min-width: 0;
    max-width: none;
    padding: 2px 0 0;
  }
  .topbar.is-learning .topbar-actions{ margin-left: auto; }
}


/* --- 13.2 Error Hunter: restore the small-screen collapse ------------------

   MEASURED: at 390px .forensics-layout computed to `250px 82px`. The case
   archive kept its full desktop column and the main case panel — the brief,
   the evidence board and every answer button — was squeezed into 82px, one
   word per line, with the verdict panel overlapping it. The answer buttons
   themselves laid out at 286-572px against a 390px viewport and were clipped
   out of reach. The 200-case module was unusable on a phone.

   CAUSE, precisely: screens.css DOES carry @media(max-width:900px) rules that
   collapse this grid to one column (line ~1556 and ~1628). Two later sections
   of the same file — "Journal Practice fit alignment" and "FINAL DENSITY
   POLISH" — then re-declare .forensics-layout unconditionally. Equal
   specificity, later wins, media query irrelevant: the mobile collapse was
   dead from the moment those blocks were appended.

   These rules restate the collapse the file already intended, after those
   blocks, with the same values. No new layout is introduced. */
@media (max-width: 1180px){
  .forensics-layout{ grid-template-columns: minmax(0, 250px) minmax(0, 1fr); }
  .forensics-right{ grid-column: 1 / -1; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* MEASURED at 1024px: the identity bar's fixed `270px 430px 260px` totals
     960px plus gaps inside a 958px shell and was clipped by the shell's own
     overflow:hidden. */
  .forensics-identity{ grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr) auto; }
}
@media (max-width: 900px){
  .forensics-layout{ grid-template-columns: minmax(0, 1fr); }
  .forensics-left,
  .forensics-right,
  /* MEASURED: .forensics-main is itself a grid whose single `1fr` track
     resolved to 300px (a 1fr track's automatic minimum is min-content, which
     does not shrink) inside an 82px column. minmax(0,1fr) is the standard
     correction and changes nothing where the column is already wide enough. */
  .forensics-main{ grid-template-columns: minmax(0, 1fr); }
  .forensics-right{ grid-column: auto; }
  .forensics-identity{ grid-template-columns: minmax(0, 1fr) auto; }
  .case-heading{ grid-template-columns: minmax(0, 1fr); }
  .forensic-answer-grid{ grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 620px){
  .forensics-left,
  .forensics-right{ grid-template-columns: minmax(0, 1fr); }
  .evidence-grid{ grid-template-columns: minmax(0, 1fr); }
  .forensics-actions,
  .forensics-result-actions{ flex-direction: column; }
  .forensics-actions .btn{ width: 100%; }
}


/* --- 13.3 Mentor hero: a fixed 300px portrait in a 254px column ------------

   MEASURED at 320px: .teacher-hero-art .teacher-avatar is width:300px, so the
   hero grid's single 1fr track resolved to 300px inside a 254px card and the
   copy beside it was clipped by the card's overflow:hidden. img{max-width:100%}
   in section 12 could not help — the box around the image is what was fixed.

   The portrait keeps its aspect ratio and its place in the composition; it
   simply stops being wider than the card it sits in. */
@media (max-width: 760px){
  .teacher-hero{ grid-template-columns: minmax(0, 1fr); }
  .teacher-hero-art .teacher-avatar,
  .teacher-hero-art .teacher-avatar-ben{
    width: min(100%, 300px);
    max-width: 100%;
  }
}
@media (max-width: 420px){
  .teacher-hero-art .teacher-avatar,
  .teacher-hero-art .teacher-avatar-ben{
    width: min(100%, 200px);
    height: auto;
    aspect-ratio: 300 / 360;
  }
  .teacher-hero-art{ min-height: 0; }
}


/* --- 13.4 Home tab strip: the last tab was unreachable ---------------------

   MEASURED at 320px and 360px: the "How ILO FLOW works" tab laid out at
   271-414 and 275-418 against viewports of 320 and 360. home-redesign.css
   makes the strip scrollable at <=640px, but the tabs still shrink below
   their labels first and the last one lands past the edge before the scroll
   is usable. Giving each tab a non-shrinking basis makes the strip scroll
   properly instead of overflowing silently. */
@media (max-width: 640px){
  .home-tabs{
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .home-tabs::-webkit-scrollbar{ display: none; }
  .home-tab{
    flex: 0 0 auto;
    scroll-snap-align: start;
    white-space: nowrap;
  }
}


/* --- 13.5 Touch targets -----------------------------------------------------

   MEASURED: the mentor-voice checkbox rendered 13x13 CSS px on every
   coarse-pointer viewport — roughly a quarter of the 24x24 minimum. Only the
   hit area grows; the control's drawn size is the browser's own. */
@media (pointer: coarse){
  input[type="checkbox"],
  input[type="radio"]{
    min-width: 24px;
    min-height: 24px;
  }
}


/* --- 13.6 Long unbroken strings ---------------------------------------------

   MEASURED on /contact.html at 320px: the support address laid out at
   124-327 against a 320px viewport. A single 28-character token with no break
   opportunity cannot wrap without being told it may. */
.site-footer a,
.fin-contact-item a,
a[href^="mailto:"]{
  overflow-wrap: anywhere;
  word-break: break-word;
}


/* --- 13.7 A link is not a target at 21px -----------------------------------

   MEASURED: .video-watch-link renders display:block, full width and 21.5px
   tall on every phone viewport - a full-width block link is not an inline
   link in a sentence, so the 24px minimum applies to it. Padding only; the
   type, colour and underline are unchanged. */
.video-watch-link,
.fin-contact-item a{
  padding-block: 3px;
  min-height: 24px;
}


/* --- 14.1 Error Hunter stats strip: label+value overflowing their own card -

   MEASURED: below 900px, .forensics-stats>div is 140px wide (min-width at
   900px, flex-basis at 620px), but its two real children — the label <p>
   and the value <b> — sit side by side on one row via grid-template-
   columns:auto 1fr auto, sized to their own content ("auto" grid tracks
   don't shrink below their content's natural width, and nothing on this
   element suppresses that). For the widest card, "FORENSIC RATING" (label)
   plus "RISING STAR" (value) need more width than a 140px card minus
   padding has to give — the grid overflows its own box sideways, so the
   text spills into the neighbour card and its border-right cuts straight
   through a word, matching the reference screenshot ("ACC|URACY").

   The first card (Investigator Level, with the shield badge) has its own
   fix in 14.2 below, since it carries a third element (the XP progress
   meter) that this simple label/value stack doesn't account for. For the
   plain label/value cards, stacking the value under the label instead of
   beside it means each line only ever needs to fit the card's own width,
   never the neighbour's: no shrinking, no clipping, nothing hidden. */
@media (max-width: 900px){
  .forensics-stats > div:not(:first-child){
    grid-template-columns: 1fr;
    row-gap: 2px;
  }
  .forensics-stats > div:not(:first-child) b{
    grid-column: 1;
    justify-self: start;
  }
}


/* --- 14.2 Error Hunter stats strip: Investigator Level squeezed into card 2 -

   MEASURED: the same 140px card width applies to card 1 (Investigator
   Level), but card 1 carries a THIRD element the others don't — the XP
   progress meter — on top of a longer label ("INVESTIGATOR LEVEL 01") than
   any other card. Its grid is still `auto 1fr auto` with the label column
   sized to `1fr`, which (like `auto`) does not shrink below the label's own
   content width unless told to. At 140px that content width is wider than
   the card, so the same class of overflow as 14.1 happens here too — except
   this card's overflow bleeds sideways into card 2 (Case Streak), which is
   why the streak label and the level bar/XP text were reading as tangled
   together in the reference screenshot.

   Rebuilt as two columns instead of three (badge, then label/meter/XP
   stacked in a single column with `minmax(0,1fr)` so it is genuinely
   allowed to shrink to the card's own width) and given a little more room
   than the plain cards, since it is carrying more content than they are. */
@media (max-width: 900px){
  .forensics-stats > div:first-child{
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    min-width: 172px;
  }
  .forensics-stats > div:first-child .stat-shield{
    grid-row: 1 / -1;
  }
  .forensics-stats > div:first-child p{
    grid-column: 2;
    grid-row: 1;
  }
  .forensics-stats > div:first-child .forensics-meter{
    grid-column: 2;
    grid-row: 2;
  }
  .forensics-stats > div:first-child b{
    grid-column: 2;
    grid-row: 3;
    justify-self: start;
  }
}
@media (max-width: 620px){
  .forensics-stats > div:first-child{
    flex: 0 0 172px;
  }
}


/* --- 14.3 Error Hunter stats strip: the edge fade paints over live text ----

   MEASURED: screens.css's `.forensics-stats::after` (added to signal "more
   to scroll") is a 28px-wide gradient from transparent to a fully OPAQUE
   `#091019` at the right edge, pinned to the scroll container's own box —
   not to the scrolled content. Whichever card's text happens to land under
   that 28px strip at a given scroll position gets its trailing characters
   painted over completely, not just dimmed, which is what "Accuracy" fading
   into nothing was: the fade sat on top of legible, already-visible text
   instead of only hinting at content still off-screen.

   Narrower and genuinely translucent (fading toward the background colour
   at partial opacity, never fully opaque) keeps the same "there's more →"
   cue without erasing whatever text happens to sit underneath it. */
@media (max-width: 900px){
  .forensics-stats::after{
    width: 16px;
    background: linear-gradient(90deg, transparent, rgba(9, 16, 25, .55));
  }
}


/* --- 14.4 Error Hunter stats strip: Accuracy reads dim next to its siblings -

   MEASURED: "0/200" is plain parchment (bright, near-white), "RISING STAR"
   is .gold (--f-gold, #d9a441 — warm and saturated), but "100%" only gets
   .positive (--f-green, #82c88d) with no other adjustment. #82c88d is a
   pale, desaturated green — against the #091019 card background it has
   noticeably less contrast than its neighbours, so even once the edge fade
   in 14.3 stopped painting over it, it still reads as the "quiet" value in
   the row next to a bright white figure and a warm gold one.

   Scoped to this one strip only (the shared --f-green token is left alone,
   since it's used correctly elsewhere — e.g. the intelligence panel and the
   current-case marker — where it isn't sitting directly beside gold and
   white siblings). A brighter, more saturated green closes that gap without
   turning it into a different colour. */
.forensics-stats .positive{
  color: #6fe08e;
}
