/* ═══════════════════════════════════════════════════════════════════════════
   TOKENS — copied VERBATIM from apps/edge-agent/frontend/core.css.
   This file cannot link that stylesheet (the whole UI is one Python string),
   so the :root and dark blocks are duplicated here value-for-value. Do not
   retune them here: change core.css and copy again, or the box and the cloud
   drift apart.

   The rule the system enforces: NEUTRALS plus ONE tint. If something is
   coloured it is interactive (--tint) or it is a status (--ok/--warn/--bad).
   Everything else is grey, and hierarchy comes from weight, size, spacing and
   separators. There are no gradients and no filled status chips anywhere.
   ═══════════════════════════════════════════════════════════════════════════ */
:root{
  color-scheme: light;

  --bg-1: #FFFFFF;
  --bg-2: #F6F5F3;
  --bg-3: #EFEDE9;
  --surface: #FFFFFF;

  --label-1: #1A1815;
  --label-2: #6B655C;
  --label-3: #8C857B;
  --label-4: #BEB8AF;

  --separator: rgba(26, 24, 21, .10);
  --separator-opaque: #E4E1DB;

  --tint: #8A5A2B;
  --tint-pressed: #6B441F;
  --tint-wash: rgba(138, 90, 43, .08);
  --on-tint: #FFFFFF;

  --ok:   #1E7B4F;
  --warn: #A45E00;
  --bad:  #B3261E;

  --material: rgba(255, 255, 255, .72);
  --material-blur: blur(20px) saturate(180%);

  /* Inter, matching mergen-ai.tech. Self-hosted, never a CDN — see
     scripts/fetch_inter.py. --mono stays JetBrains Mono: every figure on
     the parent page is tabular and must not reflow between readings. */
  /* SF, without shipping SF. Apple's licence for SF Pro / SF Compact / SF Mono
     / New York covers designing and building apps for Apple platforms; it does
     not cover serving the files as webfonts. It does not have to. On an iPhone
     -- which is where a parent actually reads this -- -apple-system resolves to
     the real SF Pro, already on the device, at zero bytes and with the optical
     sizing Apple ships. ui-monospace resolves to the real SF Mono the same way.
     Everywhere else this falls to Inter, which is self-hosted here and was
     drawn to sit in the same place. */
  --font: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display',
          'Inter', 'Segoe UI Variable', 'Segoe UI', system-ui, sans-serif;
  --mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo,
          'JetBrains Mono', 'Cascadia Mono', Consolas, monospace;

  --t-display: 2.125rem;  --t-display-lh: 1.08;  --t-display-tr: -.022em;
  --t-title:   1.5rem;    --t-title-lh:   1.15;  --t-title-tr:   -.018em;
  --t-head:    1.125rem;  --t-head-lh:    1.25;  --t-head-tr:    -.011em;
  --t-body:    .9375rem;  --t-body-lh:    1.55;  --t-body-tr:     0;
  --t-sub:     .8125rem;  --t-sub-lh:     1.45;  --t-sub-tr:      .002em;
  --t-cap:     .75rem;    --t-cap-lh:     1.35;  --t-cap-tr:      .01em;

  --s1: .25rem;  --s2: .5rem;   --s3: .75rem;  --s4: 1rem;
  --s5: 1.5rem;  --s6: 2rem;    --s7: 3rem;    --s8: 4rem;

  --r-sm: 6px;
  --r:    10px;
  --r-lg: 14px;
  --r-pill: 999px;

  --shadow-1: 0 1px 2px rgba(26, 24, 21, .05);
  --shadow-2: 0 4px 16px rgba(26, 24, 21, .08), 0 1px 3px rgba(26, 24, 21, .04);

  --ease: cubic-bezier(.32, .72, 0, 1);
  --spring: cubic-bezier(.34, 1.26, .64, 1);
  --fast: 120ms;
  --med: 260ms;
  --slow: 400ms;

  --sidebar-w: 232px;
  --toolbar-h: 52px;
  --measure: 68ch;
}

:root[data-theme="dark"]{
  color-scheme: dark;

  --bg-1: #121110;
  --bg-2: #1A1817;
  --bg-3: #221F1D;
  --surface: #1C1A19;

  --label-1: #F3F0EB;
  --label-2: #A8A198;
  --label-3: #7B756D;
  --label-4: #544F49;

  --separator: rgba(255, 255, 255, .12);
  --separator-opaque: #2E2A26;

  --tint: #C99A55;
  --tint-pressed: #DCAE68;
  --tint-wash: rgba(201, 154, 85, .12);
  --on-tint: #17130F;

  --ok:   #4ADE80;
  --warn: #FBBF24;
  --bad:  #FF6B6B;

  --material: rgba(28, 26, 25, .72);

  --shadow-1: 0 1px 2px rgba(0, 0, 0, .4);
  --shadow-2: 0 4px 16px rgba(0, 0, 0, .5), 0 1px 3px rgba(0, 0, 0, .35);
}

/* ═══ Base (core.css) ════════════════════════════════════════════════════ */
*, *::before, *::after{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{
  font-family:var(--font);
  font-size:var(--t-body);
  line-height:var(--t-body-lh);
  font-weight:500;
  color:var(--label-1);
  background:var(--bg-2);
  /* 100vh on a phone is the height with the address bar *hidden*, so it is
     taller than what is actually on screen and pushes content under the
     browser chrome. dvh is the live value; the vh line stays underneath it
     for anything that does not know dvh. */
  min-height:100vh;
  min-height:100dvh;
  text-rendering:optimizeLegibility;
  font-variant-numeric:tabular-nums;
}
h1,h2,h3,h4{font-weight:600;color:var(--label-1)}
h1{font-size:var(--t-display);line-height:var(--t-display-lh);letter-spacing:var(--t-display-tr)}
h2{font-size:var(--t-title);line-height:var(--t-title-lh);letter-spacing:var(--t-title-tr)}
h3{font-size:var(--t-head);line-height:var(--t-head-lh);letter-spacing:var(--t-head-tr)}
h4{font-size:var(--t-body);line-height:var(--t-body-lh);letter-spacing:var(--t-body-tr)}
a{color:var(--tint);text-decoration:none;cursor:pointer}
a:hover{text-decoration:underline;text-underline-offset:3px}
::selection{background:var(--tint-wash)}
:focus-visible{outline:2px solid var(--tint);outline-offset:2px;border-radius:var(--r-sm)}

/* ═══ Text utilities (core.css) ══════════════════════════════════════════ */
.t-display{font-size:var(--t-display);line-height:var(--t-display-lh);letter-spacing:var(--t-display-tr);font-weight:600}
.t-title{font-size:var(--t-title);line-height:var(--t-title-lh);letter-spacing:var(--t-title-tr);font-weight:600}
.t-head{font-size:var(--t-head);line-height:var(--t-head-lh);letter-spacing:var(--t-head-tr);font-weight:600}
.t-body{font-size:var(--t-body);line-height:var(--t-body-lh)}
.t-sub{font-size:var(--t-sub);line-height:var(--t-sub-lh);letter-spacing:var(--t-sub-tr);color:var(--label-2)}
.t-cap{font-size:var(--t-cap);line-height:var(--t-cap-lh);letter-spacing:var(--t-cap-tr);color:var(--label-3)}
.dim{color:var(--label-2)}
.dim2{color:var(--label-3)}
.eyebrow{font-size:var(--t-cap);font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--label-3)}
.num{font-family:var(--mono);font-variant-numeric:tabular-nums;letter-spacing:-.01em}

/* ═══ Materials — translucent chrome, content scrolls under (§12) ════════ */
.material{
  background:var(--material);
  backdrop-filter:var(--material-blur);
  -webkit-backdrop-filter:var(--material-blur);
}
.scroll-edge::after{
  content:'';position:absolute;left:0;right:0;bottom:-16px;height:16px;
  background:linear-gradient(var(--bg-2),transparent);pointer-events:none;
}

/* ═══ Surfaces ═══════════════════════════════════════════════════════════ */
.card{background:var(--surface);border:1px solid var(--separator);border-radius:var(--r-lg);padding:var(--s5)}
.card-link{transition:border-color var(--fast) var(--ease),background-color var(--fast) var(--ease)}
.card-link:hover{border-color:var(--label-4)}
.card-link:active{background:var(--bg-3)}
.divide > * + *{border-top:1px solid var(--separator)}

/* ═══ Controls ═══════════════════════════════════════════════════════════ */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--s2);
  min-height:36px;padding:0 var(--s4);
  border:1px solid var(--separator);border-radius:var(--r);
  background:var(--surface);color:var(--label-1);
  font:inherit;font-weight:600;font-size:var(--t-sub);cursor:pointer;
  transition:transform var(--fast) var(--ease),
             background-color var(--fast) var(--ease),
             border-color var(--fast) var(--ease);
}
.btn:hover{border-color:var(--label-4)}
.btn:active{transform:scale(.97);background:var(--bg-3)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-primary{background:var(--tint);border-color:var(--tint);color:var(--on-tint)}
.btn-primary:hover{background:var(--tint-pressed);border-color:var(--tint-pressed)}
.btn-primary:active{background:var(--tint-pressed)}
.btn-quiet{background:transparent;border-color:transparent;color:var(--label-2)}
.btn-quiet:hover{background:var(--bg-3);border-color:transparent;color:var(--label-1)}
.btn-danger{color:var(--bad);border-color:var(--separator);background:var(--surface)}
.btn-danger:hover{border-color:var(--bad)}

input,select,textarea{
  font:inherit;font-size:var(--t-body);color:var(--label-1);
  background:var(--bg-1);border:1px solid var(--separator);border-radius:var(--r);
  padding:var(--s2) var(--s3);min-height:36px;width:100%;
  transition:border-color var(--fast) var(--ease);
}
input:hover,select:hover,textarea:hover{border-color:var(--label-4)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--tint)}
input::placeholder,textarea::placeholder{color:var(--label-3)}
label{display:block;font-size:var(--t-sub);font-weight:600;color:var(--label-2);margin-bottom:var(--s2)}

/* ═══ Status — a dot and a word. No filled chips. ════════════════════════ */
.status{display:inline-flex;align-items:center;gap:var(--s2);font-size:var(--t-sub);font-weight:600;white-space:nowrap}
.status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
/* Darker than the raw signal colours, and only where they are worn as text.
   --ok, --warn and --bad are also used for dots and meter fills, where a 3:1
   non-text ratio is the bar; as words on a translucent card over a photograph
   they were measured at 3.37:1 (Ирсэн), 4.00 (Өвчтэй) and 3.47 (Чөлөөтэй) —
   the four words that carry the whole answer a parent came for, and the only
   text on the page still under AA. The hue is unchanged; only the value moves,
   so green still reads as green. */
.status-ok{color:#146138}
.status-warn{color:#8A4E00}
.status-bad{color:#8C1D18}
.status-off{color:#59544C}
/* "Happening right now" is the system's own live marker, not a verdict — the
   one place tint is allowed to label something. */
.status-now{color:var(--tint)}

/* ═══ Tables — rules, not stripes ════════════════════════════════════════ */
table{width:100%;border-collapse:collapse;font-size:var(--t-sub)}
th{
  text-align:left;font-size:var(--t-cap);font-weight:600;letter-spacing:.06em;
  text-transform:uppercase;color:var(--label-3);
  padding:var(--s2) var(--s3);border-bottom:1px solid var(--separator);white-space:nowrap;
}
td{padding:var(--s3);border-bottom:1px solid var(--separator);color:var(--label-1)}
tbody tr{transition:background-color var(--fast) var(--ease)}
tbody tr:hover{background:var(--bg-2)}
tr:last-child td{border-bottom:none}
td.num,th.num{text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums}

/* ═══ Empty state — say so plainly, never fake a chart ═══════════════════ */
.empty{
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--s2);padding:var(--s7) var(--s4);
  color:var(--label-3);font-size:var(--t-sub);text-align:center;
}
.empty-tight{padding:var(--s5) var(--s4)}

/* ═══ Motion ═════════════════════════════════════════════════════════════ */
/* The entrance moves; it does not fade.
   It used to animate opacity from 0, with fill-mode 'both' so the element held
   that from-state through its stagger delay. That makes the animation
   responsible for the content being visible at all — and when the animation
   does not run, which it reliably does not when the class lands in the same
   frame that its container stops being display:none, the element holds opacity
   0 forever. The symptom was a dashboard that loaded correctly, hid the login
   screen, and then showed an empty background: eleven blocks at opacity 0, no
   error in the console, nothing to report and nothing to click.
   Animating only transform removes the whole class of failure. If the animation
   never starts the content sits four pixels low, which nobody will ever notice,
   instead of not being there. */
@keyframes rise{from{transform:translateY(5px)}to{transform:none}}
.rise{animation:rise var(--med) var(--ease) both}
.rise-1{animation-delay:30ms} .rise-2{animation-delay:60ms}
.rise-3{animation-delay:90ms} .rise-4{animation-delay:120ms}
.rise-5{animation-delay:150ms} .rise-6{animation-delay:180ms}

@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms!important;animation-iteration-count:1!important;
    transition-duration:.01ms!important;scroll-behavior:auto!important;
  }
  .rise{animation:none;opacity:1;transform:none}
}
@media (prefers-reduced-transparency: reduce){
  .material{background:var(--bg-1);backdrop-filter:none;-webkit-backdrop-filter:none}
}
@media (prefers-contrast: more){
  :root{--separator:rgba(26,24,21,.28);--label-2:#4A453E;--label-3:#6B655C}
  :root[data-theme="dark"]{--separator:rgba(255,255,255,.32);--label-2:#C8C2B8;--label-3:#9A938A}
  .material{backdrop-filter:none;-webkit-backdrop-filter:none;background:var(--bg-1)}
}

/* ═══════════════════════════════════════════════════════════════════════════
   THIS PAGE, BUILT ON THOSE ROLES
   Nothing below invents a colour, a font-size or a radius.
   ═══════════════════════════════════════════════════════════════════════════ */
.hidden{display:none!important}

/* ── Chrome (§12): a floating translucent bar, content passes underneath.
      Not an opaque strip with a border — a border says "the page stops here",
      which is untrue while the page is scrolling behind it. ─────────────── */
.chrome{
  position:fixed;top:0;left:0;right:0;z-index:30;
  min-height:var(--toolbar-h);
  display:flex;align-items:center;gap:var(--s3);
  padding:var(--s2) var(--s5);
}
.chrome-brand{display:flex;align-items:center;gap:var(--s2);font-weight:600;font-size:var(--t-sub);
  letter-spacing:var(--t-sub-tr);color:var(--label-1);flex:none}
/* An image, not an inline SVG. The mark is a low-poly render with gradients
   across every facet; redrawing that as vector paths would either lose the
   shading or cost more bytes than the 28KB PNG it replaces. It also means the
   mark can no longer be recoloured by currentColor, which is why it ships
   already lightened rather than being tinted per surface. */
.brand-mark{width:22px;height:22px;flex:none;object-fit:contain;display:block}
/* The overseer follows the OS theme, so its chrome can be dark. content: on an
   img swaps the rendered image without touching the markup, which keeps the
   template free of a theme it does not otherwise know about. */
:root[data-theme="dark"] .brand-mark{ content:url('/static/img/logo-on-dark.png') }
.chrome-school{
  font-size:var(--t-sub);color:var(--label-2);font-weight:600;
  padding-left:var(--s3);border-left:1px solid var(--separator);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:34vw;
}
.chrome-spacer{flex:1 1 auto}
.chrome .btn{min-height:40px}

.page{max-width:1000px;margin:0 auto;padding:calc(var(--toolbar-h) + var(--s5)) var(--s5) var(--s8)}

/* ── Page head: ONE identity line. Where am I, then what is here. ──────── */
.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--s4);flex-wrap:wrap;
  margin-bottom:var(--s6)}
.page-head h1{overflow-wrap:anywhere}
.head-actions{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap}
.date-in{width:auto;min-width:9.5rem;min-height:40px}

/* ── Sections: an eyebrow, a hairline, content. No nested panels. ──────── */
.section{margin-top:var(--s7)}
.section-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:var(--s3);flex-wrap:wrap;
  padding-bottom:var(--s2);border-bottom:1px solid var(--separator);margin-bottom:var(--s2);
}
.section-note{font-size:var(--t-cap);color:var(--label-3);letter-spacing:var(--t-cap-tr)}

/* ── The row. One shape for every list on every screen. ───────────────── */
.rows{list-style:none}
.row{
  display:flex;align-items:center;gap:var(--s3) var(--s4);flex-wrap:wrap;
  padding:var(--s3) 0;min-height:52px;
}
.row-main{flex:1 1 11rem;min-width:0}
.row-title{display:block;font-size:var(--t-body);font-weight:600;color:var(--label-1);
  overflow-wrap:anywhere}
.row-sub{display:block;font-size:var(--t-cap);line-height:var(--t-cap-lh);color:var(--label-3)}
.row-aside{flex:none;margin-left:auto;display:flex;align-items:center;gap:var(--s3)}
.row-att{flex:none}

/* Whole-row target (overseer school list). Hover moves the separator and the
   recess, never the elevation. */
.rowbtn{
  width:100%;text-align:left;background:transparent;border:none;font:inherit;color:inherit;
  cursor:pointer;padding-left:var(--s3);padding-right:var(--s3);border-radius:var(--r);
  transition:background-color var(--fast) var(--ease),transform var(--fast) var(--ease);
}
.rowbtn:hover{background:var(--bg-3)}
.rowbtn:active{transform:scale(.995);background:var(--bg-3)}

/* ── Stat run. Four bordered tiles became one line of figures separated by
      hairlines; the number that matters is simply the biggest. ─────────── */
.stats{display:flex;flex-wrap:wrap;align-items:stretch}
.stat{flex:1 1 8rem;padding:0 var(--s5)}
.stat:first-child{padding-left:0}
.stat + .stat{border-left:1px solid var(--separator)}
.stat-val{font-size:var(--t-title);line-height:var(--t-title-lh);letter-spacing:var(--t-title-tr);
  font-weight:600;font-family:var(--mono);font-variant-numeric:tabular-nums;color:var(--label-1)}
.stat.lead .stat-val{font-size:var(--t-display);line-height:var(--t-display-lh);letter-spacing:var(--t-display-tr)}
.stat-lbl{margin-top:var(--s1);font-size:var(--t-cap);font-weight:600;letter-spacing:.06em;
  text-transform:uppercase;color:var(--label-3)}
.stat-note{margin-top:var(--s1)}

/* ── Measurement meter. Neutral track, tint fill — this is data, and tint is
      the one colour data is allowed. Never green/amber/red by threshold. ── */
.meter{display:inline-flex;align-items:center;gap:var(--s2);min-width:5.5rem}
.meter-track{flex:1;height:4px;border-radius:2px;background:var(--bg-3);overflow:hidden;min-width:2.5rem}
.meter-track i{display:block;height:100%;background:var(--tint);
  transition:width var(--med) var(--ease)}

/* ── The day strip. Kept, because a calendar of yes/no days IS a strip and
      not a line chart. Rebuilt as a neutral cell plus a 6px status dot. ─── */
.strip{display:grid;grid-template-columns:repeat(7,1fr);gap:var(--s1)}
.strip-flow{display:flex;flex-wrap:wrap;gap:var(--s1)}
.cell{
  min-width:2rem;height:44px;border-radius:var(--r-sm);background:var(--bg-3);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
  font-family:var(--mono);font-size:var(--t-cap);color:var(--label-2);
}
.strip-flow .cell{flex:1 1 2rem}
.cell-dot{width:6px;height:6px;border-radius:50%;background:var(--label-4);flex:none}
.cell.s-present .cell-dot{background:var(--ok)}
.cell.s-sick .cell-dot{background:var(--warn)}
.cell.s-skipped .cell-dot{background:var(--warn)}
.cell.s-excused .cell-dot{background:var(--label-3)}
.cell.s-unknown .cell-dot{background:var(--label-4)}
.cell.s-off{background:transparent;color:var(--label-4)}
.cell.s-off .cell-dot{display:none}
/* Today is a ring, not a fill: it marks position, it does not restate status. */
.cell.s-today{box-shadow:inset 0 0 0 2px var(--tint)}
.cell-meter{width:60%;height:3px;border-radius:2px;background:var(--separator-opaque);overflow:hidden}
.cell-meter i{display:block;height:100%;background:var(--tint)}
.legend{display:flex;flex-wrap:wrap;gap:var(--s2) var(--s4);margin-top:var(--s3)}

/* ── Reason bars. The label is the legend; a colour swatch would only repeat
      the word next to it. ────────────────────────────────────────────────── */
.bars{display:flex;flex-direction:column;gap:var(--s2);margin-top:var(--s3)}
.bar-row{display:grid;grid-template-columns:6rem 1fr 2.5rem;align-items:center;gap:var(--s3)}
.bar-label{font-size:var(--t-sub);color:var(--label-2)}
.bar-track{height:6px;border-radius:3px;background:var(--bg-3);overflow:hidden}
.bar-fill{height:100%;border-radius:3px;background:var(--tint);transition:width var(--med) var(--ease)}
.bar-val{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:var(--t-sub);
  text-align:right;color:var(--label-1)}

/* ── Charts. Chart.js needs an explicitly sized parent. ─────────────────── */
.chart-wrap{position:relative;height:180px;margin-top:var(--s3)}
.chart-wrap-sm{height:132px}
@media(max-width:640px){.chart-wrap{height:152px}}

/* ── Attendance correction. The primary teacher workflow, so it stays two
      explicit buttons at a 44px target — never a toggle to decode. ─────── */
.att{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap}
.seg{display:inline-flex;gap:var(--s1)}
.seg button{
  min-height:44px;min-width:5rem;padding:0 var(--s3);
  border:1px solid var(--separator);border-radius:var(--r);
  background:var(--surface);color:var(--label-2);
  font:inherit;font-size:var(--t-sub);font-weight:600;cursor:pointer;
  transition:transform var(--fast) var(--ease),background-color var(--fast) var(--ease),
             border-color var(--fast) var(--ease),color var(--fast) var(--ease);
}
.seg button:hover:not(:disabled){border-color:var(--label-4);color:var(--label-1)}
.seg button:active:not(:disabled){transform:scale(.97)}
.seg button[aria-pressed="true"]{background:var(--tint);border-color:var(--tint);color:var(--on-tint)}
.seg button:disabled{opacity:.5;cursor:not-allowed}
.seg-busy{opacity:.55;pointer-events:none}
.reason-sel{width:auto;min-width:9rem;min-height:44px;font-size:var(--t-sub)}
.reason-sel.unset{color:var(--label-3)}
.btn-undo{min-height:44px;min-width:44px;padding:0 var(--s3)}
.att-meta{display:flex;align-items:center;gap:var(--s3)}
.src{font-size:var(--t-cap);letter-spacing:var(--t-cap-tr);color:var(--label-3);white-space:nowrap}

/* ── Login ─────────────────────────────────────────────────────────────── */
.login-wrap{min-height:100vh;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:var(--s5)}
.login-card{width:100%;max-width:23rem}
.login-brand{display:flex;align-items:center;gap:var(--s2);margin-bottom:var(--s5)}
.field{margin-bottom:var(--s4)}
.form-err{color:var(--bad);font-size:var(--t-sub);margin-top:var(--s3);min-height:1.2em}
.login-alt{margin-top:var(--s5);padding-top:var(--s4);border-top:1px solid var(--separator);text-align:center}
.btn-block{width:100%}

/* ── Sheet (password change). Blurred backdrop, one floating card. ─────── */
.sheet{position:fixed;inset:0;z-index:500;display:flex;align-items:center;justify-content:center;padding:var(--s5)}
.sheet-card{width:100%;max-width:23rem;background:var(--surface);border:1px solid var(--separator);
  border-radius:var(--r-lg);box-shadow:var(--shadow-2);padding:var(--s5)}
.sheet-actions{display:flex;gap:var(--s2);justify-content:flex-end;margin-top:var(--s4)}

/* ── Toast. Chrome, not status: a plain surface, with the failure stated in
      --bad TEXT rather than a red panel. ─────────────────────────────────── */
#toast{
  position:fixed;left:50%;bottom:var(--s5);transform:translateX(-50%) translateY(8px);
  z-index:1000;max-width:calc(100vw - 2rem);padding:var(--s3) var(--s4);
  border-radius:var(--r);background:var(--surface);border:1px solid var(--separator);
  box-shadow:var(--shadow-2);color:var(--label-1);
  font-size:var(--t-sub);font-weight:600;
  opacity:0;pointer-events:none;
  transition:opacity var(--med) var(--ease),transform var(--med) var(--ease);
}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast.err{color:var(--bad)}

.tbl-wrap{overflow-x:auto}
.figs{display:flex;gap:var(--s5);flex:none}
.fig{text-align:right}
.fig b{display:block;font-family:var(--mono);font-variant-numeric:tabular-nums;
  font-size:var(--t-head);line-height:var(--t-head-lh);font-weight:600}
.fig i{display:block;font-style:normal;margin-top:var(--s1)}

/* ═══════════════════════════════════════════════════════════════════════════
   PARENT VIEW
   A parent opens this on a phone to answer one question: is my child OK, were
   they at school today. So the ANSWER is the largest thing on the screen, in
   --label-1 with a single status dot; which lessons and what time sit quiet
   below it; history and alerts further down still.

   Calm, not alarming. A missed lesson is --warn TEXT. --bad is reserved for a
   safety incident a teacher has actually confirmed. There is no tinted answer
   card, no coloured left border, and no red panel anywhere on this screen.
   ═══════════════════════════════════════════════════════════════════════════ */
.pv-main{max-width:34rem;margin:0 auto;padding:calc(var(--toolbar-h) + var(--s5)) var(--s5) var(--s8)}
.pv-answer{margin:var(--s2) 0 var(--s6)}
.answer-line{display:flex;align-items:center;gap:var(--s3)}
.answer-dot{width:10px;height:10px;border-radius:50%;flex:none;background:var(--label-4)}
.pv-answer.is-in .answer-dot{background:var(--ok)}
.pv-answer.is-sick .answer-dot{background:var(--warn)}
.pv-answer.is-out .answer-dot{background:var(--warn)}
.pv-answer.is-excused .answer-dot{background:var(--label-3)}
.pv-answer.is-off .answer-dot{background:var(--label-4)}
#pvAnswerTitle{font-size:var(--t-display);line-height:var(--t-display-lh);
  letter-spacing:var(--t-display-tr);font-weight:600;color:var(--label-1);overflow-wrap:anywhere}
.pv-answer-sub{display:block;margin-top:var(--s2);font-size:var(--t-body);color:var(--label-2)}
.pv-answer-src{display:block;margin-top:var(--s2);font-size:var(--t-cap);
  letter-spacing:var(--t-cap-tr);color:var(--label-3)}
.pv-eyebrow-school{color:var(--label-3);font-weight:600}
.pv-daynav{display:flex;align-items:center;justify-content:space-between;gap:var(--s3);
  margin:var(--s5) 0 var(--s2)}
.pv-daynav .btn{min-height:44px;min-width:44px;padding:0 var(--s3)}
.pv-daynav-label{font-size:var(--t-sub);font-weight:600;color:var(--label-1)}
.bell-no{flex:none;width:1.5rem;text-align:center;font-family:var(--mono);
  font-size:var(--t-sub);color:var(--label-3)}
.pv-foot{margin-top:var(--s7);padding-top:var(--s4);border-top:1px solid var(--separator);
  font-size:var(--t-cap);line-height:var(--t-cap-lh);color:var(--label-3)}

@media (min-width:560px){
  .pv-daynav{justify-content:flex-start;gap:var(--s4)}
}
@media (max-width:559px){
  .chrome-school{display:none}
}
@media (max-width:640px){
  .chrome{padding:var(--s2) var(--s4)}
  .page{padding-left:var(--s4);padding-right:var(--s4)}
  .pv-main{padding-left:var(--s4);padding-right:var(--s4)}
  .stat{flex:1 1 45%;padding:var(--s3) var(--s4)}
  .stat:first-child{padding-left:0}
  .stat + .stat{border-left:none}
  .stats{gap:var(--s3) 0}
  /* The correction control gets its own full-width line rather than being
     squeezed until "Ирээгүй" clips mid-word. */
  .row-aside{width:100%;margin-left:0}
  .att{width:100%}
  .seg{flex:1 1 auto}
  .seg button{flex:1;min-width:0}
  .reason-sel{flex:1 1 8rem;min-width:0}
  .figs{gap:var(--s4)}
}

/* ═══════════════════════════════════════════════════════════════════════════
   DEPTH, TEXTURE, MOTION — the polish layer.

   Three additions, each of which has to earn its place against the system's
   own rule (neutrals plus ONE tint, no gradients as decoration):

     1. A DOT GRID on the --bg-2 plane. The empty areas read as a void rather
        than a surface; a 24px grid of --label-1 mixed down to a whisper gives
        them a floor without adding a colour.
     2. A CURSOR-TRACKED TINT WASH on the few blocks that are actually
        interactive or actually the answer. This is a light effect — the one
        gradient in the system besides the scroll edge — and it is --tint-wash
        and nothing else.
     3. AN ENTRANCE STAGGER plus an instrument SETTLE on figures that change,
        so navigation and live updates read as motion instead of a repaint.

   Everything here degrades to nothing under prefers-reduced-motion, and the
   JS handlers check matchMedia for the same reason.
   ═══════════════════════════════════════════════════════════════════════════ */

/* A 1px dot inside a 24px cell covers roughly a third of one percent of that
   cell, so a literal 3% per dot renders as nothing at all — the alpha has to
   come up to land at the intended weight. 5% was picked against a 2x phone
   screen, where the dots are drawn at device resolution: 7% read as graph
   paper there, and anything under 4% vanished. Same colour, same role, one
   value, mixed down.

   Painted once, on the body, so every screen — login included — sits on the
   same floor and no two layers can stack the same dots twice. */
body{
  background-image:radial-gradient(color-mix(in srgb, var(--label-1) 5%, transparent) 1px, transparent 1px);
  background-size:24px 24px;
  background-position:-1px -1px;
}

/* ── Spotlight. --mx/--my are written by one delegated pointermove handler.
      Painted behind the content (isolation + a negative z-index) so it never
      sits over text, and only on hover so a still page stays still. ─────── */
.spot{position:relative;isolation:isolate}
.spot::after{
  content:'';position:absolute;inset:0;z-index:-1;border-radius:inherit;pointer-events:none;
  background:radial-gradient(240px at var(--mx,50%) var(--my,50%), var(--tint-wash), transparent 70%);
  opacity:0;transition:opacity var(--med) var(--ease);
}
.spot:hover::after,.spot:focus-within::after{opacity:1}

/* ── Interactive surfaces lift. NON-interactive ones still do not: a card that
      rises under a cursor that cannot click it is a lie about affordance. ── */
.card-int{
  transition:border-color var(--med) var(--ease),
             box-shadow var(--med) var(--ease),
             background-color var(--med) var(--ease);
}
.card-int:hover{border-color:var(--label-4);box-shadow:var(--shadow-2)}

/* ── Panels. A bordered container so a list reads as one object instead of
      scattered rows floating on the page. ───────────────────────────────── */
.panel{
  background:var(--surface);border:1px solid var(--separator);
  border-radius:var(--r-lg);padding:var(--s5);
}
.list-boxed{
  background:var(--surface);border:1px solid var(--separator);
  border-radius:var(--r-lg);padding:0 var(--s4);
}
.list-boxed .row{padding:var(--s3) 0}
.rows-roomy .row{padding:var(--s4) 0;min-height:60px}

/* ── Controls: hover shifts the ground under the label, press scales. ───── */
.btn{
  transition:transform var(--fast) var(--ease),
             background-color var(--med) var(--ease),
             border-color var(--med) var(--ease),
             box-shadow var(--med) var(--ease);
}
/* Scoped away from the variants that own their own hover treatment, so this
   adds a background shift to the plain button without overriding them. */
.btn:not(.btn-primary):not(.btn-quiet):not(.btn-danger):hover{background:var(--bg-3);border-color:var(--label-4)}
.btn:active{transform:scale(.97)}
.rowbtn:active{transform:scale(.99)}

/* Focus is a ring in the tint wash plus a tint border — both at --fast, so a
   keyboard user gets the same immediacy a pointer user gets. */
input,select,textarea{
  transition:border-color var(--fast) var(--ease),box-shadow var(--fast) var(--ease);
}
input:focus,select:focus,textarea:focus{box-shadow:0 0 0 3px var(--tint-wash)}
.seg button:focus-visible{box-shadow:0 0 0 3px var(--tint-wash)}

/* ── Stagger + settle ───────────────────────────────────────────────────── */
.rise-7{animation-delay:210ms} .rise-8{animation-delay:240ms}

/* Instrument settle: seven discrete steps over ~half a second. A figure that
   changed should tick into place the way a gauge does — not spring, and not
   count up digit by digit, which invents intermediate values that were never
   true. */
@keyframes settle{
  from{opacity:.25;transform:translateY(-3px)}
  to{opacity:1;transform:none}
}
.settle{animation:settle 490ms steps(7,end) both}

@media (prefers-reduced-motion: reduce){
  .settle{animation:none}
  .spot::after{display:none}
  .card-int:hover{box-shadow:none}
}

/* ═══════════════════════════════════════════════════════════════════════════
   TYPOGRAPHIC DRAMA
   The scale existed and was being used timidly. The rule applied below: the
   one figure that answers the screen's question is far bigger than anything
   near it, and the words that support it are smaller and quieter than they
   were. Every size here is derived from a scale token — none is invented.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Parent hero: name (quiet) → ANSWER (huge) → summary (quiet). ───────── */
.pv-hero{padding:var(--s2) 0 var(--s3)}
.pv-who{
  display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--s2);
  font-size:var(--t-cap);line-height:var(--t-cap-lh);letter-spacing:var(--t-cap-tr);
}
#pvName{font-size:var(--t-cap);font-weight:600;color:var(--label-2);letter-spacing:.04em;
  text-transform:uppercase}
#pvClass{font-size:var(--t-cap);font-weight:500;color:var(--label-3);
  letter-spacing:var(--t-cap-tr);text-transform:none}
.pv-answer{margin:var(--s4) 0 0}
.answer-line{align-items:center;gap:var(--s3)}
.answer-dot{width:12px;height:12px}
#pvAnswerTitle{
  font-size:calc(var(--t-display) * 1.35);
  line-height:1.02;letter-spacing:-.028em;font-weight:600;
}
.pv-answer-sub{margin-top:var(--s3);font-size:var(--t-body);color:var(--label-2)}
.pv-answer-src{margin-top:var(--s2);font-size:var(--t-cap);color:var(--label-3)}

/* ── Day nav: one segmented control, not three loose buttons. ──────────── */
.pv-daynav{
  display:flex;align-items:center;gap:var(--s1);
  margin:var(--s6) 0 0;padding:var(--s1);
  background:var(--bg-3);border-radius:var(--r);
}
.pv-daynav .btn{min-height:40px;min-width:40px;padding:0 var(--s3);
  background:transparent;border-color:transparent;color:var(--label-2)}
.pv-daynav .btn:hover:not(:disabled){background:var(--surface);border-color:var(--separator);color:var(--label-1)}
.pv-daynav .btn:disabled{opacity:.35}
.pv-daynav-label{flex:1 1 auto;text-align:center;white-space:nowrap;
  font-size:var(--t-sub);font-weight:600;color:var(--label-1);padding:0 var(--s2)}
.pv-today-btn{flex:none}

/* ── Teacher KPIs: the lead figure is the screen's answer, so it is bigger
      than a heading, and the labels under it are smaller and quieter. ───── */
.kpi-panel{margin-top:var(--s2)}
.stat.lead .stat-val{font-size:calc(var(--t-display) * 1.2);line-height:1.02;letter-spacing:-.026em}
.stat-lbl{margin-top:var(--s2);color:var(--label-3)}
.stat-note{margin-top:var(--s2)}

/* Charts say in words what they are counting. A chart with no caption makes
   the reader guess the unit. */
.chart-cap{margin-top:var(--s2);font-size:var(--t-cap);line-height:var(--t-cap-lh);
  letter-spacing:var(--t-cap-tr);color:var(--label-3)}

/* ── Login: centred, present, on the textured plane. ───────────────────── */
.login-wrap{flex-direction:column}
.login-card{
  max-width:24rem;
  background:var(--surface);border:1px solid var(--separator);
  border-radius:var(--r-lg);box-shadow:var(--shadow-2);
  padding:var(--s6);
}
.login-brand{margin-bottom:var(--s6);gap:var(--s3)}
.login-brand .brand-mark{width:30px;height:30px}

/* Section rhythm: generous between, tight within. */
.section{margin-top:var(--s7)}
.section + .section{margin-top:var(--s7)}
.section-head{margin-bottom:var(--s3)}

/* A table is a list too — same bordered container, so the overseer screen has
   the same three objects the other screens have instead of loose rules. */
.tbl-wrap{
  background:var(--surface);border:1px solid var(--separator);
  border-radius:var(--r-lg);
}
.tbl-wrap th:first-child,.tbl-wrap td:first-child{padding-left:var(--s4)}
.tbl-wrap th:last-child,.tbl-wrap td:last-child{padding-right:var(--s4)}
.tbl-wrap tr:last-child td{border-bottom:none}

/* The day strip reacts to a pointer the way any other cell does. */
.cell{transition:background-color var(--fast) var(--ease),box-shadow var(--fast) var(--ease)}
.strip .cell:hover,.strip-flow .cell:hover{background:var(--bg-2)}
.cell.s-off:hover{background:transparent}

/* The meter fill and the reason bars already animate their width; the day
   strip's cell meter should too, so a live update reads as a change. */
.cell-meter i{transition:width var(--med) var(--ease)}

/* The sheet arrives rather than appears. */
.sheet-card{animation:rise var(--med) var(--ease) both}
#toast{transition:opacity var(--med) var(--ease),transform var(--med) var(--spring)}

@media(max-width:640px){
  #pvAnswerTitle{font-size:calc(var(--t-display) * 1.15)}
  .stat.lead .stat-val{font-size:calc(var(--t-display) * 1.05)}
  .login-card{padding:var(--s5)}
  /* The narrow-screen rule that drops .row-aside onto its own line exists for
     the teacher's two-button correction control, which genuinely needs the
     width. A read-only status word does not, and dropping it made every
     parent list twice as tall with the answer floating under the subject
     instead of beside it. */
  .row-aside-fixed{width:auto;margin-left:auto}
}

/* ── Patterns: the four questions one day cannot answer ────────────────────
   "Was she there today" is answered by the hero. "Which lesson does she
   actually miss", "is he always late on Mondays" only exist across weeks, and
   a parent has no way to compute them by scrolling back day by day.
   Each panel is one question with one answer, in the card frame the rest of
   the page already uses — no second container style. */
.pat{display:grid;gap:var(--s4);margin-top:var(--s3);
  grid-template-columns:repeat(auto-fit,minmax(17rem,1fr))}
.pat-card{border:1px solid var(--separator);border-radius:var(--r-lg);
  background:var(--surface);padding:var(--s5)}
.pat-card h3{font-size:var(--t-cap);line-height:var(--t-cap-lh);
  letter-spacing:var(--t-cap-tr);text-transform:uppercase;
  color:var(--label-3);font-weight:600;margin-bottom:var(--s4)}
.pat-card .figs{justify-content:flex-start;gap:var(--s6)}
.pat-card .fig{text-align:left}
.pat-card .rows{margin-top:var(--s1)}
.pat-note{margin-top:var(--s4);font-size:var(--t-cap);line-height:var(--t-cap-lh);
  color:var(--label-3)}
.pat-empty{font-size:var(--t-cap);color:var(--label-3)}

/* Weekday columns. Height carries the number so five days compare at a
   glance; the figure is still written out, because a bar alone is a shape
   and a parent is being asked to act on a fact. */
.pat-week{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--s2)}
.pat-day{text-align:center;min-width:0}
.pat-day .bar{height:56px;border-radius:var(--r-sm);background:var(--bg-3);
  display:flex;align-items:flex-end;overflow:hidden;margin-bottom:var(--s2)}
.pat-day .bar i{display:block;width:100%;background:var(--tint);
  border-radius:inherit;transition:height .5s cubic-bezier(.2,.7,.3,1)}
/* The worst day is the one worth looking at, so it is the one that is not
   the same colour as the other four. */
.pat-day.worst .bar i{background:var(--warn)}
.pat-day .pat-day-label{display:block;font-size:var(--t-cap);color:var(--label-3)}
.pat-day .pat-day-val{display:block;font-family:var(--mono);
  font-variant-numeric:tabular-nums;font-size:var(--t-cap);color:var(--label-2)}

/* Attention over four weeks. A row of days, not one score: a single "73%"
   invites a punishment, a line invites a conversation. */
.pat-spark{display:flex;align-items:flex-end;gap:2px;height:44px;margin-top:var(--s3)}
.pat-spark i{flex:1 1 0;min-width:2px;background:var(--tint);border-radius:1px;
  opacity:.85}
.pat-spark i.gap{background:var(--separator-opaque);opacity:1}

@media (prefers-reduced-motion: reduce){
  .pat-day .bar i{transition:none}
}

/* The day's own recorded facts, under the hero answer — evidence for the
   headline, never competing with it. */
.pv-facts{margin-top:var(--s4);justify-content:flex-start;gap:var(--s6)}
.pv-facts .fig{text-align:left}
@keyframes bandSpin{ from{ transform:rotate(0deg) } to{ transform:rotate(360deg) } }

/* ═══ HERO VARIANT 2 — light glass, all the way down ═══════════════════════
   Variant 1 put one glass card on a dark band. This is the opposite bet: keep
   the page light, and make every surface on it glass.

   Which needs an ambient first. Glass is translucency over something worth
   seeing, and a page whose every card is #FFFFFF over #F6F5F3 has nothing
   behind it to see — frosting those cards would only make them grey. So a warm
   field goes down first, coloured by the brand mark, and then the surfaces
   stop being opaque.

   The transformation is one token swap, not a list of selectors: every card in
   this stylesheet is already background:var(--surface) + 1px var(--separator).
   Redefining those two under turns all of them at once, and
   any card added later joins in without being told to. */

:root{
  /* .52 is not a taste decision, it is the measured floor plus a margin.
     Swept against the real page over the real photograph, scrolling a viewport
     at a time: .64 → 4.89:1, .56 → 4.72, .48 → 4.51, .40 → 4.34 and below AA.
     The floor is .48 and it clears the line by 0.01, which is no margin at all
     for a wording change, so .52 ships.
     A card over a controlled backdrop can go far more transparent than this.
     This one sits over navy uniforms and a shadowed wooden floor and cannot,
     and no amount of edge lighting substitutes for the text being readable. */
  --surface: rgba(255,255,255,.24);
  --bg-1: rgba(255,255,255,.42);
  --bg-3: rgba(255,255,255,.40);
  /* The whole secondary ramp steps down one notch. Measured on the painted
     pixels, --label-3 on a warm glass card came out at 3.12:1 — below the 4.5
     floor, on the line that tells a parent when the lesson was. Every label
     moves rather than just the failing one, so the hierarchy between them
     survives the correction. */
  --label-2: #59544C;
  --label-3: #6B655C;
  --label-4: #9A938A;
  --separator: rgba(74,45,20,.10);
  --separator-opaque: rgba(74,45,20,.14);
  --material: rgba(255,255,255,.58);
  /* Shadows warm up too. A neutral grey shadow under a warm-tinted panel is
     the giveaway that the colour was painted on rather than lit. */
  --shadow-1: 0 1px 2px rgba(74,45,20,.06);
  --shadow-2: 0 10px 30px rgba(74,45,20,.10), 0 1px 3px rgba(74,45,20,.05);
}

/* The ambient, fixed rather than scrolling: the glass should feel like it is
   moving over a room, not carrying a picture along with it.

   It is painted by a fixed pseudo-element rather than by background-attachment,
   because background-attachment:fixed does not work on a phone. iOS Safari
   ignores it outright, and both it and Chrome on Android size a fixed
   background against the *document* rather than the viewport — so on a page
   that scrolls for several screens, cover scales the photograph to the whole
   scroll height and what reaches the glass is a smeared, wildly zoomed strip
   that then scrolls with the content. Desktop Chrome implements it correctly,
   which is why every measurement here passed while the actual phone did not.

   position:fixed on a pseudo-element is honoured everywhere and gives exactly
   the intended behaviour: the room stays put, the panes travel over it. */
body::before{
  content:'';
  position:fixed;
  inset:0;
  z-index:-1;
  pointer-events:none;
  /* A real classroom, sitting behind the glass.
     Colour fields alone gave a calm but flat result — a backdrop-filter has
     nothing to bend in a smooth gradient. A photograph has desks, window
     frames and light falling across a floor: edges everywhere, which is what
     the cards are for. It is heavily softened by the glass on top, so what
     reaches the reader is atmosphere rather than a picture they study.
     The wash on top is not decoration. Measured raw, the middle of this
     image sits at 0.32 relative luminance, which is far too dark to put dark
     text over; the gradient lifts the centre where the reading column runs
     and leaves the edges alone, so the room still reads at the margins. */
  /* One flat wash, not a vertical ramp with a hotspot. The ramp existed to
     lift the middle band where a naked heading used to sit; every heading is
     inside a pane now, so the backdrop only has to be light enough for the
     panes to sit on, and an even one keeps the room reading the same top to
     bottom. .50 with panes at .74 is the measured floor — see the panes block
     at the end of this file. */
  /* Plain cover, the whole room in frame. The crop was pushed to 210% and
     260% for a while to keep the reading column off the old photograph's black
     lower half — that was steering around a bad image, not a design. The
     replacement is lit evenly enough that there is nothing to steer around:
     measured at plain cover, nothing below 4.5:1 on any viewport. */
  background:
    url('/static/img/bg-ambient.webp') center 50% / cover no-repeat,
    linear-gradient(#FCF7F1, #EFE3D4);
}

/* Section headings are the one piece of text with no card under it, so they
   meet the ambient at full strength wherever it happens to be brightest. They
   get the darker label rather than a scrim: a heading that has to carry its own
   backing plate is a heading in the wrong place. */
.eyebrow,
.section-note{ color:#4E4941 }

/* Every pane catches light on its top edge. One lit edge is the difference
   between a translucent box and a piece of glass. */
.card,
.list-boxed,
.pat-card,
.chart-wrap,
.chart-wrap-sm,
.sheet-card,
.login-card,
.pv-hero,
#acctInsightCard{
  border-top-color:rgba(255,255,255,.96);
  border-left-color:rgba(255,255,255,.72);
  position:relative;
  /* Four lights, not one. A pane of glass is read from its edges: the top
     catches the brightest reflection, the left picks up a weaker one, and the
     bottom returns a trace of what is under it. A single flat border gives a
     translucent rectangle; these give thickness.
     The left and bottom edges are inset shadows rather than an ::after, because
     .spot::after is already spoken for by the hover glow on the hero and login
     cards — reusing it would silently break their hover state. */
  box-shadow:
    var(--shadow-2),
    inset 0  1px 0 rgba(255,255,255,.60),
    inset  1px 0 0 rgba(255,255,255,.38),
    inset 0 -1px 0 rgba(255,255,255,.14);
}

/* The specular sweep: a hairline across the top that is brightest in the middle
   and fades before it reaches either corner. Held off the corners on purpose —
   a highlight that runs into a 16px radius reads as a drawn border, and the
   whole point is that it should read as light landing on an edge. */
.card::before,
.list-boxed::before,
.pat-card::before,
.chart-wrap::before,
.sheet-card::before,
.login-card::before,
.pv-hero::before,
#acctInsightCard::before{
  content:'';
  position:absolute; top:0; left:14%; right:14%; height:1px;
  background:linear-gradient(90deg, transparent,
                             rgba(255,255,255,.85) 50%, transparent);
  pointer-events:none;
  z-index:1;
}

/* The hero has no surface of its own in the base sheet — it is a bare header.
   Here it becomes the first and brightest pane. */
.pv-hero{
  margin:var(--s5) 0 0;
  padding:var(--s6) var(--s5);
  border:1px solid var(--separator);
  border-top-color:rgba(255,255,255,.92);
  border-radius:var(--r-lg);
  background:rgba(255,255,255,.54);
}

/* Every pane is frosted here, which is a real cost and a deliberate one: this
   variant's whole proposition is that the interface is made of glass, and one
   frosted card among ten opaque ones just looks like a mistake. The radius is
   held at 22px — enough to dissolve the mark's facets, cheap enough that a
   mid-range phone still scrolls at 60fps. */
.card,
.list-boxed,
.pat-card,
.chart-wrap,
.chart-wrap-sm,
.sheet-card,
.login-card,
.pv-daynav,
.pv-hero,
#acctInsightCard,
#parentApp .chrome{
  backdrop-filter:blur(22px) saturate(1.7);
  -webkit-backdrop-filter:blur(22px) saturate(1.7);
}

/* The day nav is a recessed track, so it darkens against the ambient instead
   of lifting off it — the one control here that should read as carved in. */
.pv-daynav{
  background:rgba(74,45,20,.06);
  border:1px solid rgba(255,255,255,.55);
}

/* ── Two surfaces the token swap quietly broke ────────────────────────────
   Both inherit var(--surface), which is fine for a card that sits in the flow
   and got a backdrop-filter along with everything else. Neither of these is
   that.

   The toast is fixed to the bottom of the viewport, so what is behind it is
   whatever the reader happened to scroll to — light desks or a row of children
   in navy. At 52% white with no blur it was legible in the screenshot only
   because the scroll position was lucky. A message confirming that a change
   saved has to be readable every time, so it gets its own near-solid surface
   and does not participate in the glass. */
#toast{
  background:rgba(255,253,250,.94);
  backdrop-filter:blur(16px) saturate(1.4);
  -webkit-backdrop-filter:blur(16px) saturate(1.4);
  border-color:rgba(74,45,20,.14);
}

/* The sheet is the full-screen layer behind a modal, and in this variant it was
   inheriting the light --material. A pale scrim over a pale page says nothing:
   the card floated at 52% white on a backdrop at 58% white, two nearly equal
   sheets of nothing. A modal has to read as being on top of the page, which
   means the page has to visibly go behind it. */
#passwordModal.sheet{
  background:rgba(52,34,18,.34);
  backdrop-filter:blur(10px) saturate(1.1);
  -webkit-backdrop-filter:blur(10px) saturate(1.1);
}
/* And with the room dimmed behind it, the card stops needing to fight for
   legibility and can be the brightest thing on screen. */
.sheet-card{
  background:rgba(255,253,250,.92);
  border-color:rgba(255,255,255,.7);
}

/* Without backdrop-filter the marks would show through the cards unblurred and
   sit behind the text. Everything goes opaque rather than nearly-legible. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  :root{ --surface: rgba(255,255,255,.94); --bg-1: rgba(255,255,255,.90) }
  .pv-hero{ background:rgba(255,255,255,.94) }
  #parentApp .chrome{ background:rgba(255,255,255,.96) }
}
/* Nothing in this variant moves: the ambient is fixed and the rings are static,
   so there is no motion for a reduced-motion preference to switch off. */

/* ═══ LOGIN ════════════════════════════════════════════════════════════════
   Centred, pill-shaped, icon in the field. A parent meets this screen once a
   term on a phone they are holding in one hand, so everything is full width and
   nothing needs aiming at.

   Structure lives here, unscoped, so all three hero variants get a coherent
   login. Only the colour treatment is per-variant. */
.sr-only{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
.login-card{ text-align:center }
.login-card .login-brand{ justify-content:center }
.login-card .t-title{ margin-bottom:var(--s6) }

.login-field{ position:relative; margin-bottom:var(--s3); text-align:left }
.login-field input,
.login-field select{
  width:100%;
  /* A pill, and the right padding is not symmetric on purpose: the icon lives
     in that gap and text must never run under it. */
  padding:var(--s4) 3rem var(--s4) var(--s5);
  border-radius:var(--r-pill);
  font-size:var(--t-body);
}
.login-field select{ padding-right:3rem }
.login-field:has(.login-field-btn) input{ padding-right:3.4rem }
.login-field-icon{
  position:absolute; right:1.1rem; top:50%; transform:translateY(-50%);
  width:18px; height:18px; pointer-events:none; opacity:.6;
}
/* The placeholder is decoration for a sighted reader; the label carries the
   name. Both say the same words, so nothing is lost either way. */
.login-field input::placeholder{ color:var(--label-3); opacity:1 }

.login-submit{
  margin-top:var(--s4);
  border-radius:var(--r-pill);
  padding-block:var(--s4);
  font-size:var(--t-body);
}
.login-foot{
  margin-top:var(--s5);
  font-size:var(--t-cap); line-height:var(--t-cap-lh);
  color:var(--label-3);
}

/* No wash on this screen, and the photograph fills it.
   The wash on <body> keeps dark text alive over the picture, but every word on
   the login screen sits inside the card, so here it buys nothing and costs the
   room. background-size:cover crops whichever axis has spare — padding the
   image to avoid that was tried and abandoned, because the padded edge is soft
   where the photograph is sharp and reads as a blurred border around it. */
body:has(#loginScreen:not(.hidden))::before{
  background:
    url('/static/img/bg-ambient.webp') center 55% / cover no-repeat,
    linear-gradient(#FCF7F1, #EFE3D4);
}
.login-wrap{ background:none }

/* ── The login card ───────────────────────────────────────────────────────
   Light glass, like every other surface in the product. It was a smoked dark
   pane for a while, which let the classroom stay bright behind it — but it also
   made the login the one screen that did not look like the rest of the app, and
   the first thing a parent sees should not be the odd one out.

   The cost is named: dark text on a translucent card over a photograph with
   navy uniforms in it needs the card opaque enough to win, so less of the room
   shows through than a dark card would allow. The page wash stays off on this
   screen, so the room around the card is at full strength and carries the
   atmosphere the transparency used to. */
.login-card{
  /* .20, and the number is measured rather than chosen. The reference recipe
     this follows uses .05; swept against this photograph at 5px blur it lands
     at 4.48:1 on a phone — 0.02 under the AA floor, which is no margin at all
     for one extra word of copy. .20 costs almost nothing visually and clears at
     5.02:1. On a desktop even .05 passes; the phone is the constraint, because
     the card sits over rows of navy uniforms rather than a lit wall. */
  background:rgba(255,255,255,.20);
  backdrop-filter:blur(5px) saturate(1.3);
  -webkit-backdrop-filter:blur(5px) saturate(1.3);
  border:1px solid rgba(255,255,255,.35);
  /* At this transparency the card has almost no surface of its own, so the
     edges have to carry it: a lit top, a weaker lit left, and a trace returned
     along the bottom. */
  box-shadow:
    0 8px 32px rgba(40,26,12,.18),
    inset 0  1px 0 rgba(255,255,255,.55),
    inset  1px 0 0 rgba(255,255,255,.32),
    inset 0 -1px 0 rgba(255,255,255,.14);
  padding:var(--s7) var(--s6);
}
.login-card::before{ left:20%; right:20% }

/* The field has to read as somewhere to type. On a light translucent card an
   unfilled input at the same lightness dissolves into it, so it goes slightly
   darker than its surroundings rather than lighter. */
.login-field input,
.login-field select{
  background:rgba(255,255,255,.55);
  border:1px solid rgba(74,45,20,.16);
  color:var(--label-1);
}
.login-field input::placeholder{ color:var(--label-3) }
/* The icons take the darker label now that the card is mostly transparent.
   At .72 opacity the pill was its own pale surface and --label-3 read fine
   against it; at .20 the photograph comes through, and on a wide screen the
   icon landed at 2.58:1 against a lit window. Non-text needs 3:1. */
.login-field-icon{ color:#4E4941; opacity:1 }
.login-field-icon svg{ width:100%; height:100%; display:block }

/* The reveal control. It replaces a decorative icon, so it keeps that icon's
   position but stops being 18px of dead space: the tap target is 40px, which
   is the smallest thing a thumb should be asked to find, while the eye itself
   stays the same size as the icon in the field above it. */
.login-field-btn{
  width:40px; height:40px; right:.55rem;
  padding:11px; margin:0; border:0; background:none; border-radius:50%;
  pointer-events:auto; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  color:#4E4941;
  transition:background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.login-field-btn:hover{ background:rgba(74,45,20,.10); color:var(--label-1) }
.login-field-btn:focus-visible{
  outline:2px solid var(--tint); outline-offset:2px; background:rgba(74,45,20,.10);
}
.login-field-btn[aria-pressed="true"]{ color:var(--tint) }

/* The one thing to press is the one solid, saturated object on the card. */
.login-submit{
  background:var(--tint); color:var(--on-tint); border-color:transparent;
  font-weight:700;
}
.login-submit:hover{ background:var(--tint-pressed) }

/* Full ink. This line is the only instruction on the screen — where a parent's
   username actually comes from — and at small-print grey it was being read as
   decoration and skipped. It sits at the bottom of the card, which on a phone
   is where the photograph behind is darkest, so it had the least contrast of
   anything here while carrying the most meaning. */
.login-foot{ color:var(--label-1); font-weight:500 }

@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .login-card{ background:rgba(255,255,255,.95) }
}

/* ── 2. Autofill ──────────────────────────────────────────────────────────
   Chrome paints a remembered field with its own opaque background and ignores
   `background`. On a glass card that is the whole illusion gone: the moment a
   parent's password manager fills the password, the field turns into a solid
   pale blue rectangle sitting on a translucent pane. The only property that
   reaches it is an inset shadow large enough to cover the box, and the text
   colour has to be set through -webkit-text-fill-color because `color` is
   overridden too. The long transition-delay is the standard trick for the
   brief moment Chrome repaints before the shadow applies. */
.login-field input:-webkit-autofill,
.login-field input:-webkit-autofill:hover,
.login-field input:-webkit-autofill:focus{
  -webkit-box-shadow:0 0 0 40rem rgba(255,255,255,.62) inset;
  box-shadow:0 0 0 40rem rgba(255,255,255,.62) inset;
  -webkit-text-fill-color:var(--label-1);
  caret-color:var(--label-1);
  transition:background-color 100000s ease 0s;
}

/* ── B. Focus ─────────────────────────────────────────────────────────────
   A border colour change is almost invisible on glass — the border is already
   a light hairline against a light card. A ring reads at a glance and, more to
   the point, is visible to someone moving through the form with a keyboard,
   who has no pointer to tell them where they are. */
.login-field input:focus,
.login-field select:focus{
  background:rgba(255,255,255,.78);
  border-color:var(--tint);
  outline:none;
  box-shadow:0 0 0 3px var(--tint-wash), 0 0 0 1px var(--tint);
}

/* ── E. The button while it is working ────────────────────────────────────
   The label stays in place and keeps the button its size; the ring turns
   beside it. Replacing the word with a spinner makes the button jump, and a
   button that changes width under a thumb is a button that gets mis-tapped. */
.login-submit{ position:relative }
.login-spinner{
  width:16px; height:16px; margin-left:var(--s2);
  border-radius:50%;
  border:2px solid rgba(255,255,255,.35);
  border-top-color:var(--on-tint);
  display:none;
}
.login-submit.is-busy .login-spinner{ display:inline-block; animation:spin .7s linear infinite }
.login-submit.is-busy{ cursor:progress }
.login-submit:disabled{ opacity:1 }          /* busy is not the same as unavailable */
@keyframes spin{ to{ transform:rotate(360deg) } }
@media (prefers-reduced-motion: reduce){
  .login-submit.is-busy .login-spinner{ animation:none; border-top-color:rgba(255,255,255,.85) }
}

/* ── The error, now above the button ──────────────────────────────────────
   It sits between the fields and the action, which is where the correction
   has to happen. Empty it takes no space at all, so nothing shifts until
   there is something to say. */
/* The error carries its own surface. Every other word on this card can be
   measured once against the photograph behind it, because it is always in the
   same place; this line is the one that appears when something has gone wrong,
   and it has to be readable whatever the glass happens to be sitting over.
   Measured as bare red text it reached 4.96:1 on a phone and 3.78:1 on a wide
   window — the same message, readable or not depending on the size of someone's
   browser. An opaque backing makes it 8.9:1 everywhere, and the tint doubles as
   the signal that this is a failure rather than a note. */
.login-card .form-err{
  margin:var(--s3) 0 0;
  padding:var(--s2) var(--s3);
  border-radius:var(--r-sm);
  text-align:left;
  font-size:var(--t-cap); line-height:var(--t-cap-lh); font-weight:500;
  color:#8C1D18;
  background:rgba(255,236,233,.94);
  border:1px solid rgba(140,29,24,.22);
}
.login-card .form-err:empty{ display:none }

/* ── D. Sit a little below centre ─────────────────────────────────────────
   Perfect centring puts the fields high on a tall phone, and this is a form
   filled one-handed. The shift is small and only applies where there is room
   for it — on a short screen the card must stay fully visible, which matters
   more than reach. */
@media (min-height: 700px){
  /* padding-top, not bottom. With align-items:center the padding is taken off
     the box before the card is centred in what remains, so padding at the
     bottom lifts it and padding at the top lowers it — the first attempt here
     moved the card 15px the wrong way. */
  .login-wrap{ align-items:center; padding-top:10vh }
}

/* ═══ ONE PANE PER SECTION ══════════════════════════════════════════════════
   The dashboard used to put its section headings on the photograph and its
   content in a box underneath, which meant a quarter of the page's text had no
   glass over it at all. That is the structural difference from the login, where
   every word sits inside the one card — and it is why the login's material
   could not simply be copied here: naked headings over a classroom photograph
   measured as low as 1.10:1 no matter how the cards were tuned.

   So the section becomes the pane, heading included, and the boxes inside it
   stop being boxes. Nothing on this page is left sitting directly on the
   picture, which is what makes the transparency affordable. */

#parentApp .section{
  position:relative;
  padding:var(--s5);
  border-radius:var(--r-lg);
  background:var(--surface);
  border:1px solid rgba(255,255,255,.35);
  border-top-color:rgba(255,255,255,.55);
  border-left-color:rgba(255,255,255,.42);
  /* 16px, not 5. Measured over 176 strings on the real page: at 5px the
     classroom's edges survive into the pane at roughly the spatial frequency
     of an 11px Cyrillic stroke, so the eye separates letterform from room on
     every word. Luminance deviation in the band under each string -- ground
     with no letters in it -- ran 0.090. At 16px it is 0.055, a 39% drop, and
     the minimum contrast barely moves: 7.02 to 6.49, against a floor of 4.5.
     Raising the pane's opacity instead buys 9% for a third of the
     transparency this design exists for, and heavier text shadow buys none
     of it at all -- contrast was never what was wrong. The room still shows
     through; it is simply further away.
     The login card keeps 5px: one card over a chosen part of the photograph
     is a different problem from a dozen panes scrolling over all of it. */
  backdrop-filter:blur(16px) saturate(1.32);
  -webkit-backdrop-filter:blur(16px) saturate(1.32);
  box-shadow:
    0 8px 32px rgba(40,26,12,.18),
    inset 0  1px 0 rgba(255,255,255,.55),
    inset  1px 0 0 rgba(255,255,255,.32),
    inset 0 -1px 0 rgba(255,255,255,.14);
}
/* The specular sweep the other panes carry, held off the rounded corners. */
#parentApp .section::before{
  content:'';
  position:absolute; top:0; left:14%; right:14%; height:1px;
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.85) 50%, transparent);
  pointer-events:none; z-index:1;
}
/* Sections sit closer together now that each one is a visible object; the old
   spacing was separating a floating heading from the box below it. */
#parentApp .section + .section{ margin-top:var(--s5) }
#parentApp .section-head{ margin-bottom:var(--s4) }

/* ── The boxes inside stop being boxes ───────────────────────────────────
   A pane inside a pane reads as a mistake, and stacking two backdrop-filters
   costs twice as much to paint for a border nobody wanted. */
#parentApp .section .list-boxed,
#parentApp .section .chart-wrap,
#parentApp .section .chart-wrap-sm{
  background:none; border:0; border-radius:0; box-shadow:none;
  backdrop-filter:none; -webkit-backdrop-filter:none;
  padding:0;
}
#parentApp .section .list-boxed::before,
#parentApp .section .chart-wrap::before{ display:none }

/* The pattern cards keep their grid but lose their surface: a hairline is
   enough to separate two figures that already sit in separate columns. */
#parentApp .section .pat-card{
  background:none; box-shadow:none; backdrop-filter:none;
  -webkit-backdrop-filter:none;
  border:0; border-top:1px solid var(--separator);
  border-radius:0;
  padding:var(--s5) 0 0;
}
#parentApp .section .pat-card::before{ display:none }
#parentApp .section .pat{ gap:0 var(--s5) }

/* #acctInsightCard is itself a section, so it must not also be a card. */
#parentApp .section#acctInsightCard{ padding:var(--s5) }


/* ═══ INK FOR A TRANSPARENT PANE ════════════════════════════════════════════
   The panes now carry the login's material — .20 white over 5px of blur, no
   wash on the photograph. That is what was asked for and it is what ships.

   It costs legibility, and the cost was measured rather than guessed: 144
   strings sampled against their own painted backdrop, one viewport at a time,
   the whole page scrolled. At the previous setting none of them fell below the
   4.5:1 the guideline asks for. At this one, 44 do, the worst at 1.00:1 —
   which is text the same colour as what is behind it.

   Everything below claws back what can be clawed back without touching the
   glass. Pushing the secondary ramp to near-black takes the failures from 44
   to 26 and the floor from 1.00:1 to 1.43:1. It changes no transparency: the
   room shows through exactly as much as it did a moment ago.

   That leaves ~25 strings — the small coloured figures, mostly, over the
   darkest part of the photograph. There is no text colour that fixes those;
   they need something opaque behind them, which is the thing we took away.

   To go back, one line: --surface .20 → .74 and restore the .50 wash on body. */
#parentApp{
  --label-2: #241C10;
  --label-3: #241C10;
  --label-4: #3A3229;
}
#parentApp .eyebrow,
#parentApp .row-sub,
#parentApp .section-note,
#parentApp .pat-note,
#parentApp .pv-answer-sub,
#parentApp .pat-day-val,
#parentApp .fig i{ color:#241C10 }

/* The four status words a parent opens the page for, taken as dark as they can
   go while still reading as four different colours. */
#parentApp .status-ok  { color:#08301A }
#parentApp .status-warn{ color:#402400 }
#parentApp .status-bad { color:#450E0B }
#parentApp .status-off { color:#2E2A24 }
