/* Edu360 public site tokens — code SSoT: lib/app/app_theme.dart */
:root{
  --edu-ink:#0C0C20;
  --edu-bg:#FFFDFE;
  --edu-surface:#FFFDFE;
  --edu-primary:#DC0C83;
  --edu-primary-fg:#FFFEFF;
  --edu-accent:#00C8B3;
  --edu-muted:#455064;
  --edu-soft:#F6F2F4;
  --edu-line:#E7E3E5;
  --edu-secondary:#EBE6E8;
  --edu-radius-btn:6px;
  --edu-radius-card:10px;
  --edu-radius-xl:20px;
  --edu-space-1:4px;
  --edu-space-2:8px;
  --edu-space-3:12px;
  --edu-space-4:16px;
  --edu-space-5:20px;
  --edu-space-6:24px;
  --edu-space-8:40px;
  --edu-max:1120px;
  --edu-shadow:0 10px 30px rgba(13,10,44,.08);
  --edu-touch:44px;
}
/* Deliberately light-only.
   The landing is a fixed light design with a dark hero panel — its palette is
   written into its own stylesheet and does not follow the system. The generated
   pages did follow it, so on a machine set to dark the site rendered as two
   different products: a light landing wearing a dark-mode header whose near-white
   link text sat on a near-white bar and could not be read, next to a fully dark
   guide. One theme, chosen, beats two themes disagreeing. */
:root{color-scheme:light}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
  margin:0;
  background:var(--edu-bg);
  color:var(--edu-ink);
  /* Noto Sans Myanmar ships on Android and most Linux, Padauk on almost
     nothing by default — so a Windows or Mac reader fell past both and got
     whatever the generic fallback picked, which renders Burmese stacked
     consonants badly. "Myanmar Text" (Windows) and "Myanmar Sangam MN"
     (macOS/iOS) are the platform fonts that actually exist. No webfont: a
     Burmese face is 200-400 KB, and this audience is on mobile data. */
  font:16px/1.65 Lato,"Segoe UI",Roboto,"Noto Sans Myanmar","Padauk",
       "Myanmar Text","Myanmar Sangam MN",system-ui,sans-serif;
  -webkit-font-smoothing:antialiased;
}
a{color:var(--edu-primary);text-decoration:none}
a:hover{text-decoration:underline}
a:focus-visible,.btn:focus-visible,.site-links a:focus-visible,.lang-switch button:focus-visible,.langs button:focus-visible{
  outline:2px solid var(--edu-primary);outline-offset:2px;
}
code{font-family:ui-monospace,Consolas,monospace;font-size:.88em;color:var(--edu-muted)}
.site-nav{
  position:sticky;top:0;z-index:40;
  display:flex;flex-wrap:wrap;gap:var(--edu-space-3);align-items:center;justify-content:space-between;
  padding:var(--edu-space-3) var(--edu-space-5);
  background:color-mix(in srgb,var(--edu-surface) 92%,transparent);
  border-bottom:1px solid var(--edu-line);backdrop-filter:blur(12px);
}
.site-brand{
  font-weight:800;letter-spacing:.2px;color:var(--edu-ink);text-decoration:none;
  font-size:1.05rem;line-height:1;min-height:var(--edu-touch);
  display:inline-flex;align-items:center;
}
.site-brand:hover{text-decoration:none}
.site-brand span{color:var(--edu-primary)}
.site-links{display:flex;flex-wrap:wrap;gap:var(--edu-space-1);align-items:center}
.site-links a{
  color:var(--edu-ink);text-decoration:none;font-weight:600;font-size:14px;
  /* On-scale spacing. 10/14 was off the --edu-space-* scale on both axes and
     was the single offender that pushed `/` (231 vs 230) and `/install/`
     (80 vs 70) over the spacing ratchet in design_pattern_audit.mjs.
     8/12, NOT 12/16: the larger step was measured first and wrapped the
     sticky header to 130px at 1024px in Burmese, whose nav labels are the
     longest of the three languages — ux_audit.mjs caught it on four pages.
     The touch target is guaranteed by min-height, not by this padding. */
  min-height:var(--edu-touch);padding:var(--edu-space-2) var(--edu-space-3);
  border-radius:999px;border:1px solid transparent;
  display:inline-flex;align-items:center;
}
.site-header .site-links a{color:var(--edu-ink)}
.site-links a:hover{background:var(--edu-soft);text-decoration:none}
.site-links a[aria-current="page"]{
  background:var(--edu-primary);color:var(--edu-primary-fg);border-color:var(--edu-primary);
}
.site-wrap{max-width:var(--edu-max);margin:0 auto;padding:0 var(--edu-space-5) 72px}

/* ---------- liquid glass ----------
   Apple's material as far as the web can honestly take it: a tinted, blurred
   panel, a rim whose light changes around the perimeter so the panel reads as
   having thickness, and a specular highlight that follows the pointer. A flat
   blur with a hairline border is glassmorphism, and it reads as frosted
   plastic; the rim and the moving highlight are what make it read as a lens.

   Glass is for chrome that floats above content — the header, the section
   strip, the chips on the hero. Never for the content itself: body text on
   glass stops being readable the moment the backdrop gets busy.

   ponytail: no refraction. Genuinely bending the backdrop needs an SVG
   feDisplacementMap or a shader, and an element with backdrop-filter becomes a
   backdrop root for its own children, so the rim cannot re-sample the page
   through it — a second pass would only blur the panel's own tint. Straight
   lines behind the panel therefore stay straight. Reach for the SVG filter
   only if that edge distortion is ever worth the paint cost. */
:root{
  --glass-blur:18px;
  /* Two levers hold the ink readable over the darkest thing this site can
     scroll under the header — the hero — without going nearly opaque and
     losing the material entirely: a moderate tint, and a brightness lift
     applied to the backdrop *before* the tint. Lifting the backdrop keeps the
     panel see-through (you still watch the hero move behind it) while the ink
     stays above 7:1; tint alone would have to reach ~.85 to do the same job,
     and at .85 this is white plastic. Re-derive the numbers with
     `node site/audit/measure-contrast.mjs`, which samples composited pixels with
     the hero scrolled behind the chrome — 18.1:1 nav, 8.4:1 strip, 11.8:1 hero
     headline on 2026-08-11. */
  --glass-tint:linear-gradient(140deg,rgba(255,255,255,.64),rgba(255,255,255,.46));
  --glass-lift-backdrop:brightness(1.35);
  --glass-edge:rgba(255,255,255,.62);
  --glass-lift:0 14px 34px -16px rgba(12,12,32,.42);
}
.glass{
  position:relative;
  background:var(--glass-tint);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(180%) var(--glass-lift-backdrop,brightness(1));
  backdrop-filter:blur(var(--glass-blur)) saturate(180%) var(--glass-lift-backdrop,brightness(1));
  border:1px solid var(--glass-edge);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.85),inset 0 -1px 0 rgba(255,255,255,.28),var(--glass-lift);
}
/* The rim. One ring, masked out of its own middle, lit unevenly around the
   perimeter — an edge that is uniformly bright is a border, not a bevel. */
.glass::before{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  padding:1.5px;
  background:conic-gradient(from 210deg,
    rgba(255,255,255,.92),rgba(255,255,255,.10) 22%,
    rgba(255,255,255,.72) 48%,rgba(255,255,255,.08) 74%,rgba(255,255,255,.92));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude;
}
/* Specular highlight, driven by --gx/--gy from the pointer (SHARED_JS). Still
   glass is a screenshot; the highlight moving is the part that reads as real. */
.glass::after{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:radial-gradient(190px 140px at var(--gx,50%) var(--gy,-30%),
    rgba(255,255,255,.55),rgba(255,255,255,0) 72%);
  opacity:var(--sheen,0);transition:opacity .3s ease;
}
/* Over the dark hero the material inverts: almost no tint, a brighter rim. */
.glass-deep{
  --glass-tint:linear-gradient(140deg,rgba(255,255,255,.15),rgba(255,255,255,.05));
  --glass-edge:rgba(255,255,255,.24);
  --glass-lift:0 12px 30px -14px rgba(0,0,0,.75);
  --glass-blur:16px;
  /* No backdrop lift here: this variant sits on the dark hero and carries light
     text, so brightening what is behind it would erase the contrast the light
     tint is buying. */
  --glass-lift-backdrop:brightness(1);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.30),var(--glass-lift);
}
.glass-deep::before{
  background:conic-gradient(from 210deg,
    rgba(255,255,255,.55),rgba(255,255,255,.06) 24%,
    rgba(255,255,255,.42) 50%,rgba(255,255,255,.05) 76%,rgba(255,255,255,.55));
}
/* Reduce Transparency is a first-class case, not an exception: the material
   falls back to an opaque surface rather than to a thinner blur. */
@media (prefers-reduced-transparency:reduce){
  .glass{background:var(--edu-surface);-webkit-backdrop-filter:none;backdrop-filter:none}
  .glass-deep{background:rgba(12,12,32,.94)}
  .glass::before,.glass::after{display:none}
  /* The body surfaces have their own block, below the tokens they override —
     custom properties resolve by source order, so an override written up here
     would simply be replaced by the definition further down. */
}
@media (prefers-reduced-motion:reduce){.glass::after{display:none}}

/* Shared chrome — the same markup renders on the landing (its own stylesheet
   carries matching rules) and on every generated page. */
.site-header{
  position:sticky;top:0;z-index:40;
  border-bottom:1px solid var(--edu-line);
  border-radius:0;
}
/* Document pages have no dark hero. The landing lift (1.35) would bleach
   the wash to a white slab. Less lift lets the brand blobs show through. */
body.public-doc .site-header.glass{
  --glass-lift-backdrop:brightness(1);
  --glass-tint:linear-gradient(140deg,rgba(255,255,255,.40),rgba(255,255,255,.22));
}
/* Not sticky on a phone. Two rows of chrome pinned to the top of a 900px-tall
   screen is a tenth of the reading area gone for the whole scroll, and the
   thing worth pinning down there is the section strip, which is 49px and
   actually navigates the page you are on. Scroll back up and the header is
   where you left it. Keep it positioned so the shared header's z-index stays
   effective above decorative content while the header scrolls with the page. */
@media (max-width:899px){
  .site-header{position:relative}
}
/* The header spans the viewport, so its rim would draw a line down each edge of
   the screen; only the bottom edge is a real edge here. */
.site-header::before{display:none}
/* Phone first, because that is what nearly everyone here reads this on.
   Two rows, not five wrapped ones: brand and the actions on the first, the
   site links on the second as a strip that scrolls sideways. Letting the links
   wrap grew the header to 230px — a quarter of the screen, sticky, with the
   content sliding under it. */
.header-inner{
  max-width:var(--edu-max);margin:0 auto;
  display:flex;flex-wrap:wrap;gap:var(--edu-space-2) var(--edu-space-3);align-items:center;
  padding:var(--edu-space-2) var(--edu-space-4);
}
.site-links{
  order:3;flex:1 0 100%;flex-wrap:nowrap;overflow-x:auto;
  scrollbar-width:none;-ms-overflow-style:none;
  /* Bleed to the screen edge so the strip reads as scrollable rather than as
     a row that happens to be clipped. */
  margin:0 calc(var(--edu-space-4) * -1);padding:0 var(--edu-space-4);
}
.site-links::-webkit-scrollbar{display:none}
.site-links a{flex:0 0 auto}
/* Two rows still measure 168px, and sticky that is a permanent 26% of a 360x640
   phone — the size of a paragraph, taken from the reader on every screen. Below
   700px the header scrolls away with the page instead. Getting back to it costs
   nothing now that every page carries the back-to-top control, which returns
   the reader to the top and moves focus into this header. */
@media (max-width:700px){
  .site-header{position:relative}
}
/* Sign in is the point of the page; hiding it on the smallest screens, which
   is what the landing used to do below 640px, hid the one action that matters
   from most of the people who come here. */
.header-end .btn{display:inline-flex}
@media (min-width:900px){
  .header-inner{gap:var(--edu-space-3);padding:var(--edu-space-3) var(--edu-space-5)}
  .site-links{order:0;flex:0 1 auto;flex-wrap:wrap;overflow:visible;margin:0;padding:0}
}
.site-brand img{border-radius:7px;margin-right:8px}
.brand-360{color:var(--edu-primary)}
.header-end{margin-left:auto;display:flex;gap:var(--edu-space-3);align-items:center}
/* The two-row header above is a 3px-wide bet on a phone, and it was losing it.
   Measured on a 390px screen with a scrollbar (343px of content): brand 98 +
   gap 12 + actions 236 = 346, so the actions dropped to a row of their own and
   the header went 115px -> 167px. A Pixel's 412px hid it.

   Two gaps at 12px -> 8px is 8 of those pixels; the other 4 come from scoping
   the landing's roomier `.btn` padding away from the shared header, in that
   page's own sheet. Together they hold down to a 375px content width — below
   that (a 360px phone) it wraps again, and three rows there is the honest
   outcome rather than pills too small to hit.

   A third rule lived here, tightening the language pills to `padding:6px 8px`.
   It never applied: `.site-header .lang-switch button` appears again further
   down with the same specificity, so the later declaration won and the pills
   kept their 6px 10px. Measured before and after, the pills were 60/39/34px
   both times. Removed rather than out-specified — the 8px above is enough,
   and a rule that has to fight its own stylesheet to shave 6px is not. */
@media (max-width:899px){
  .header-inner{column-gap:var(--edu-space-2)}
  .header-end{gap:var(--edu-space-2)}
}
/* Keep the Myanmar actions on the first row without reducing touch targets.
   The clipped wordmark still supplies the logo link's accessible name. */
@media (max-width:430px){
  .header-inner{padding-inline:var(--edu-space-3);column-gap:var(--edu-space-2)}
  .site-links{margin-inline:calc(var(--edu-space-3) * -1);padding-inline:var(--edu-space-3)}
  .site-header .site-brand{min-width:var(--edu-touch);justify-content:center}
  .site-header .site-brand img{margin-right:0}
  .site-header .site-brand > span{
    position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
    clip:rect(0,0,0,0);white-space:nowrap;border:0;
  }
}
.lang-switch{display:flex;gap:4px}
.lang-switch button{
  border:1px solid var(--edu-line);background:transparent;color:var(--edu-muted);
  /* Same box as the Sign in button beside it — one touch target for the whole
     header, so the cluster sits on one line rather than at two heights. */
  min-height:var(--edu-touch);padding:6px 14px;border-radius:12px;cursor:pointer;
  font-weight:700;font-size:13px;white-space:nowrap;
  /* Burmese hangs marks above and below the baseline; 1.6 gives the line box
     room for both, and a 12px radius keeps the upper marks clear of the corner
     in a way a 999px pill does not. */
  line-height:1.6;
  font-family:"Noto Sans Myanmar","Padauk","Myanmar Text","Myanmar Sangam MN",
              Lato,system-ui,sans-serif}
/* Burmese needs a larger optical size than Latin to stay legible: at the 13px
   that suits EN and VI, မြန်မာ turns into a smudge. Sized up rather than
   sizing the whole group up, which would leave EN and VI looking shouty. */
.lang-switch button[lang="my"]{font-size:15px}
/* The descendant selector is not a specificity guard — the landing's own button
   rules (.langs, .rail-btns) do not match this markup, and its header is
   replaced wholesale at build time anyway. It scopes the sizing to the header
   copy of the control, so the pills centre and share a floor width there while
   the rule above stays the plain, unscoped appearance. */
.site-header .lang-switch button{
  color:var(--edu-muted);background:transparent;border-radius:12px;
  /* 44px square, not 39x44 and 35x44. The height was already right; the width
     came from the padding and left two of the three pills under the floor. */
  min-width:var(--edu-touch);
  padding:6px 10px;display:inline-flex;align-items:center;justify-content:center}
/* The 54px floor squares the pills up on a desktop header. On a phone it cost
   30px across three pills, which was the difference between the brand, the
   language and Sign in sharing one row and wrapping onto two. */
@media (min-width:900px){
  .site-header .lang-switch button{min-width:54px;padding:6px 14px}
}
.site-header .lang-switch button[aria-pressed="true"]{
  color:var(--edu-primary-fg);background:var(--edu-primary)}
.lang-switch button:hover{color:var(--edu-ink);border-color:var(--edu-primary)}
.lang-switch button[aria-pressed="true"]{
  background:var(--edu-primary);border-color:var(--edu-primary);color:var(--edu-primary-fg)}
/* Translucent rather than the solid --edu-soft it used to be, so the page's
   wash carries through the last band instead of stopping dead at a grey slab.
   No blur: this is a full-width block at the end of a long scroll, and there
   is nothing behind it that a blur would reveal. */
.site-foot{
  border-top:1px solid var(--edu-line);margin-top:var(--edu-space-8);
  background:color-mix(in srgb,var(--edu-soft) 60%,transparent)}
@media (prefers-reduced-transparency:reduce),print{
  .site-foot{background:var(--edu-soft)}
}
.foot-inner{
  max-width:var(--edu-max);margin:0 auto;padding:var(--edu-space-8) var(--edu-space-5) var(--edu-space-6);
  display:grid;gap:var(--edu-space-6);
}
@media (min-width:760px){.foot-inner{grid-template-columns:1.3fr 1.2fr .75fr .75fr}}
.foot-col h2{margin:0 0 var(--edu-space-3);font-size:14px;letter-spacing:.02em}
.foot-col ul{list-style:none;margin:0;padding:0;display:grid;gap:var(--edu-space-2)}
.foot-col a{color:var(--edu-ink);font-size:14px}
.foot-col a:hover{color:var(--edu-primary)}
.foot-about p{margin:var(--edu-space-3) 0 0;color:var(--edu-muted);font-size:14px;line-height:1.6;max-width:44ch}
.foot-marks{display:flex;gap:var(--edu-space-3);align-items:center;margin-top:var(--edu-space-4)}
.foot-marks img{
  width:34px;height:34px;object-fit:contain;background:#fff;
  border:1px solid var(--edu-line);border-radius:8px;padding:4px;
}
.store-badges{display:flex;flex-wrap:wrap;gap:var(--edu-space-2)}
/* The second selector beats `.site-foot a` further down this same file, which
   sets display:inline-flex on every footer link. Without it the badge's kicker
   and store name run together on one line ("Get it onGoogle Play"). */
.store-badge,.site-foot a.store-badge{
  display:grid;gap:1px;padding:8px 14px;min-height:var(--edu-touch);
  border:1px solid var(--edu-line);border-radius:var(--edu-radius-btn);
  background:var(--edu-surface);color:var(--edu-ink);text-decoration:none;
}
.store-badge:hover{border-color:var(--edu-primary);text-decoration:none}
/* 11px was decoration, not text. 12px is the floor this site holds itself to. */
.store-badge-kicker{font-size:12px;color:var(--edu-muted);line-height:1.2}
.store-badge-name{font-size:14px;font-weight:800;line-height:1.2}
/* An unpublished listing is shown as an empty slot, not as a dead link. */
.store-badge.is-pending{opacity:.55;cursor:default}
.store-note{margin:var(--edu-space-3) 0 var(--edu-space-2);color:var(--edu-muted);font-size:13px}
.store-qr{background:#fff;border:1px solid var(--edu-line);border-radius:10px;padding:6px}
/* Two equal store columns; each one is badge → QR → note, left-aligned so the
   QR sits under its own badge. On narrow screens they stack. */
.store-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--edu-space-4);align-items:start}
@media (max-width:420px){.store-options{grid-template-columns:1fr}}
.store-option{display:grid;gap:var(--edu-space-3);justify-items:start}
.store-option .store-badge{width:100%}
.store-qr-fig{margin:0;display:grid;gap:var(--edu-space-2);justify-items:start;max-width:132px}
.store-qr-fig .store-note{margin:0;font-size:12px;line-height:1.35}
/* Phone visitors can open the store directly; installation-page QRs remain. */
@media (max-width:600px){.site-foot .store-qr-fig{display:none}}
.foot-bar{
  max-width:var(--edu-max);margin:0 auto;
  padding:0 var(--edu-space-5) var(--edu-space-8);
  display:flex;flex-wrap:wrap;gap:var(--edu-space-3);
  color:var(--edu-muted);font-size:13px;align-items:center;
}
.chip{
  border:1px solid var(--edu-line);border-radius:999px;padding:2px 10px;font-size:12px;
}
.chip.warn{border-color:color-mix(in srgb,#b45309 45%,transparent);color:#b45309}
.hero{
  padding:clamp(28px,5vw,56px) 0 var(--edu-space-6);
  display:grid;gap:var(--edu-space-5);
}
/* A grid item defaults to `min-width:auto`, which means it refuses to shrink
   below its own min-content. The hero column held a 507px min-content on a
   390px phone and the overflow was simply clipped — the Burmese eyebrow lost
   its tail. Same fix, and same reason, as `.layout > main` further down. */
.hero > *{min-width:0}
/* The landing's hero is built by its own prototype stylesheet, which wins on
   source order, and the column that overflowed is `.core` inside it. Naming the
   body class outranks that sheet instead of racing it. */
body.public-landing .hero > *,
body.public-landing .core,
body.public-landing .core > *{min-width:0}
@media (min-width:900px){
  .hero.landing{grid-template-columns:1.15fr .85fr;align-items:center;gap:var(--edu-space-8)}
}
.eyebrow{
  display:inline-block;background:color-mix(in srgb,var(--edu-primary) 12%,transparent);
  color:var(--edu-primary);font-weight:700;padding:var(--edu-space-1) var(--edu-space-3);
  border-radius:999px;font-size:13px;letter-spacing:.02em;
  /* Every hero that carries one is a grid, and a grid item stretches to its
     column no matter what `display` says — which turned this pill into a
     1080px pink bar across the install page that read as a tab, not a label.
     justify-self is the one that grid actually honours. */
  justify-self:start;
}
h1{
  font-family:Inter,Lato,system-ui,sans-serif;font-size:clamp(28px,4.5vw,42px);
  line-height:1.12;margin:var(--edu-space-3) 0 var(--edu-space-2);letter-spacing:-.02em;font-weight:800;
}
h2{font-size:clamp(1.15rem,2.2vw,1.35rem);line-height:1.25;font-weight:800;letter-spacing:-.01em}
h3{font-size:1.05rem;line-height:1.3;font-weight:700}
/* Owner, 2026-10-13: page intros fill the column and are justified; a
   58ch cap left the right half of every desktop hero empty. */
.lead{font-size:1.125rem;line-height:1.55;color:var(--edu-muted);margin:0}
.meta{font-size:13px;line-height:1.45;color:var(--edu-muted);margin:0}
.cta-row{display:flex;flex-wrap:wrap;gap:var(--edu-space-3);margin-top:var(--edu-space-4)}
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--edu-space-2);
  min-height:var(--edu-touch);padding:10px 18px;border-radius:var(--edu-radius-btn);
  font-weight:700;font-size:14px;border:1px solid transparent;cursor:pointer;text-decoration:none;
}
.btn:hover{text-decoration:none;filter:brightness(.97)}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--edu-primary);color:var(--edu-primary-fg)}
.btn-secondary{background:transparent;color:var(--edu-ink);border-color:var(--edu-line)}
.btn-secondary:hover{border-color:var(--edu-primary);color:var(--edu-primary)}
/* ---------- body surfaces ----------
   The chrome is glass and the pages under it were flat white boxes on flat
   white, which is a two-material page pretending to be one. The blur was
   never the missing part: a backdrop-filter over a blank page samples blank
   page. What was missing is a ground.

   So one fixed brand wash sits behind every page, and every panel, card and
   contact box is a light surface floating over it. Paler than the landing's
   on purpose — these are documents people read for minutes, not a hero they
   glance at, so the wash stays under the text rather than competing with it.

   Not the .glass class: its pointer-sheen loop measures every .glass element
   on every frame, and a legal page has a few dozen surfaces. No backdrop
   brightness lift either — the ground is near-white already and lifting it
   clips the wash back to the blank page this exists to fix. */
:root{
  --edu-surface-tint:linear-gradient(140deg,rgba(255,255,255,.80),rgba(255,255,255,.62));
  --edu-surface-blur:blur(14px) saturate(160%);
  --edu-surface-edge:rgba(255,255,255,.74);
  /* The white rim vanishes where the wash is palest, so a hairline ink ring
     carries the edge there. Both, or cards lose their outline half the time. */
  --edu-surface-shadow:inset 0 1px 0 rgba(255,255,255,.92),
    inset 0 -1px 0 rgba(255,255,255,.34),0 0 0 1px rgba(12,12,32,.05),
    0 16px 34px -24px rgba(12,12,32,.45);
}
body::before{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:
    radial-gradient(100% 160px at 50% 0%,color-mix(in srgb,var(--edu-primary) 22%,transparent),transparent 72%),
    radial-gradient(52% 38% at 8% 12%,color-mix(in srgb,var(--edu-primary) 14%,transparent),transparent 72%),
    radial-gradient(48% 36% at 94% 28%,color-mix(in srgb,var(--edu-accent) 16%,transparent),transparent 72%),
    radial-gradient(60% 42% at 66% 92%,color-mix(in srgb,var(--edu-primary) 10%,transparent),transparent 74%);
}
.panel{
  position:relative;
  background:var(--edu-surface-tint);
  -webkit-backdrop-filter:var(--edu-surface-blur);backdrop-filter:var(--edu-surface-blur);
  border:1px solid var(--edu-surface-edge);
  border-radius:var(--edu-radius-xl);padding:var(--edu-space-5) var(--edu-space-6);
  box-shadow:var(--edu-surface-shadow);
}
/* The bevel: a ring masked out of its own middle, lit hardest at the two
   corners the page's light comes from. An evenly lit edge is just a border. */
.panel::before,.card::before,.contact-box::before{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;padding:1px;
  background:linear-gradient(140deg,rgba(255,255,255,.95),rgba(255,255,255,.12) 42%,
    rgba(255,255,255,.10) 58%,rgba(255,255,255,.78));
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude;
}
.panel h2,.panel h3{margin:0 0 var(--edu-space-3)}
.panel > :last-child{margin-bottom:0}
.grid-3{display:grid;gap:var(--edu-space-4);margin-top:var(--edu-space-2)}
@media (min-width:800px){.grid-3{grid-template-columns:repeat(3,1fr)}}
.card{
  position:relative;
  background:var(--edu-surface-tint);
  -webkit-backdrop-filter:var(--edu-surface-blur);backdrop-filter:var(--edu-surface-blur);
  border:1px solid var(--edu-surface-edge);box-shadow:var(--edu-surface-shadow);
  border-radius:var(--edu-radius-xl);padding:var(--edu-space-5);
  display:flex;flex-direction:column;gap:var(--edu-space-2);
}
.card h2,.card h3{margin:0;font-size:1.05rem}
.card p{margin:0;color:var(--edu-muted);font-size:14px;line-height:1.5}
.card .btn{margin-top:auto;align-self:flex-start}
.feature-icon{
  width:40px;height:40px;border-radius:var(--edu-radius-card);
  display:grid;place-items:center;background:var(--edu-soft);color:var(--edu-primary);
  font-weight:800;margin:0 0 var(--edu-space-2);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--edu-accent) 22%,transparent);
}
.legal-head{display:flex;gap:var(--edu-space-4);align-items:flex-start;margin-bottom:0}
.legal-ico{
  width:48px;height:48px;border-radius:var(--edu-radius-card);
  display:grid;place-items:center;background:var(--edu-soft);color:var(--edu-primary);flex:0 0 auto;
  font-weight:800;
}
.legal-section{margin:var(--edu-space-4) 0}
.legal-section h3{margin:0;font-size:1rem}
/* Owner, 2026-10-13: legal text fills the card and is justified on both
   edges. The earlier 76ch cap left the right half of a desktop card empty. */
.legal-section p{margin:var(--edu-space-2) 0 0;color:var(--edu-muted);line-height:1.65}

/* Each clause is a <details>. The whole summary row is the hit target, so it
   gets the pointer and the focus ring rather than the heading inside it. */
.legal-section > summary{
  display:flex;align-items:center;justify-content:space-between;gap:var(--edu-space-3);
  cursor:pointer;list-style:none;font-weight:700;color:var(--edu-ink);
  /* The row measured 1030x21 — 25px in Burmese — because the padding that makes
     it look comfortable sits on the parent <details>, where a click toggles
     nothing. Padding here with a matching negative margin grows the hit target
     to 45px without moving a single pixel of the layout. */
  padding:12px 0;margin:-12px 0;
}
.legal-section > summary::-webkit-details-marker{display:none}
/* A rotating chevron drawn in CSS — no icon font, no SVG request. */
.legal-section > summary::after{
  content:"";width:9px;height:9px;flex:0 0 auto;
  border-right:2px solid var(--edu-muted);border-bottom:2px solid var(--edu-muted);
  transform:rotate(45deg);transform-origin:60% 60%;transition:transform .18s ease;
}
.legal-section[open] > summary::after{transform:rotate(-135deg)}
.legal-section > summary:hover{color:var(--edu-primary)}
.legal-section > summary:focus-visible{outline:2px solid var(--edu-primary);outline-offset:2px}
@media (prefers-reduced-motion:reduce){
  .legal-section > summary::after{transition:none}
}
@media print{
  .legal-section > summary::after{display:none}
}

/* Burmese line-height. A Myanmar syllable stacks a medial below the base glyph
   and a vowel sign and tone mark above it, so a leading tuned for Latin lets
   the marks of one line collide with the line under it. At five short
   paragraphs nobody noticed; the legal documents now run to 26 clauses, and
   the collision is the first thing a Burmese reader sees.

   Scoped to `.langblock[lang="my"]`, not a bare `[lang="my"]`: these pages are
   served as `<html lang="my">`, so the bare selector matches the root element
   and inherits the Burmese leading into the English and Vietnamese blocks too.
   Same reasoning as `.lang-switch button[lang="my"]` above for the size bump. */
.langblock[lang="my"]{line-height:2}
.langblock[lang="my"] p,
.langblock[lang="my"] li,
.langblock[lang="my"] .legal-section p{line-height:2}
.langblock[lang="my"] .lead{line-height:1.95}
.langblock[lang="my"] h1,
.langblock[lang="my"] h2,
.langblock[lang="my"] h3,
.langblock[lang="my"] summary{line-height:1.55}
.langblock[lang="my"] td,
.langblock[lang="my"] th{line-height:1.8}

/* The landing has no `.langblock` — it swaps text in place through `data-i18n`
   and SHARED_JS rewrites `document.documentElement.lang` on every switch, so
   the root attribute is the selector there and it stays correct after a switch.
   Measured 2026-08-25 before this rule: hero lead 1.7, section prose 1.55,
   footer 1.6 — the same Latin leading, on the page most readers see first, in
   the language most of them read. Prose only; the display headings are large
   enough that their stacks clear, and re-leading them would move the hero. */
html[lang="my"] body.public-landing p,
html[lang="my"] body.public-landing li,
html[lang="my"] body.public-landing dd,
html[lang="my"] body.public-landing .lead,
html[lang="my"] body.public-landing .foot-about p{line-height:1.95}
html[lang="my"] body.public-landing .card p,
html[lang="my"] body.public-landing .orbit-card p{line-height:1.9}
/* Card headings wrap at 14-20px, which is where Myanmar stacks actually
   collide. `.news-lead h3` used to carry `line-height:1.28` as an inline style
   on the element the news script injects — nothing in a stylesheet can beat an
   inline style, so it is declared in the landing's sheet now instead. */
html[lang="my"] body.public-landing .news-lead h3,
html[lang="my"] body.public-landing .news-row h4,
html[lang="my"] body.public-landing #notices li span{line-height:1.75}
/* The one surface that keeps a brand tint rather than a neutral one — it is
   the "how to reach a human" box, and it should read as the warm exception on
   a page of clauses. */
.contact-box{
  position:relative;
  margin-top:var(--edu-space-5);padding:var(--edu-space-5);border-radius:var(--edu-radius-xl);
  background:linear-gradient(140deg,
    color-mix(in srgb,var(--edu-primary) 9%,rgba(255,255,255,.82)),
    color-mix(in srgb,var(--edu-primary) 5%,rgba(255,255,255,.62)));
  -webkit-backdrop-filter:var(--edu-surface-blur);backdrop-filter:var(--edu-surface-blur);
  border:1px solid color-mix(in srgb,var(--edu-primary) 22%,var(--edu-surface-edge));
  box-shadow:var(--edu-surface-shadow);
}
/* Reduce Transparency gets an opaque page, the same way the chrome does. This
   sits below the token block on purpose — custom properties resolve by source
   order, so overriding them any earlier would achieve nothing. */
@media (prefers-reduced-transparency:reduce){
  :root{
    --edu-surface-tint:var(--edu-surface);
    --edu-surface-blur:none;
    --edu-surface-edge:var(--edu-line);
    --edu-surface-shadow:var(--edu-shadow);
  }
  body::before{display:none}
  .panel::before,.card::before,.contact-box::before{display:none}
  .contact-box{background:color-mix(in srgb,var(--edu-primary) 6%,var(--edu-surface))}
}
/* Same reason as the guide's print block: paper has no backdrop to filter, so
   the tint prints as a grey wash across a terms page people sign against. */
@media print{
  :root{
    --edu-surface-tint:#fff;
    --edu-surface-blur:none;
    --edu-surface-edge:#ddd;
    --edu-surface-shadow:none;
  }
  body::before{display:none}
  .panel::before,.card::before,.contact-box::before{display:none}
  .contact-box{background:#fff}
}
.contact-box h3{margin:0 0 var(--edu-space-2)}
.contact-box p{margin:0 0 var(--edu-space-2);line-height:1.5}
.contact-box p:last-child{margin-bottom:0}
.site-foot a{
  color:var(--edu-muted);min-height:var(--edu-touch);display:inline-flex;align-items:center;
  font-weight:600;
}
.site-foot a:hover{color:var(--edu-primary);text-decoration:none}
.site-foot p{margin:0 0 var(--edu-space-2)}
.site-foot p:last-child{margin-bottom:0}
/* There is exactly one .lang-switch on any page — the header's — and it is
   styled once, up with the rest of the chrome. A second copy of these rules
   used to live here for a switcher the legal pages rendered in their own body:
   same class, different job, declared later, so it won a padding-top of 16px
   onto the header pills and dropped them out of line with the Sign in button.
   The body switcher is gone; do not reintroduce a rule for it. */
/* Per-language pages (one URL per language): the switcher is a row of links to
   the counterpart page, drawn exactly like the buttons the legacy single-page
   copy (web/user-guide.html) still uses. */
.lang-switch a{
  border:1px solid var(--edu-line);background:transparent;color:var(--edu-muted);
  min-height:var(--edu-touch);min-width:var(--edu-touch);padding:6px 10px;border-radius:12px;
  font-weight:700;font-size:13px;white-space:nowrap;line-height:1.6;text-decoration:none;
  display:inline-flex;align-items:center;justify-content:center;
  font-family:"Noto Sans Myanmar","Padauk","Myanmar Text","Myanmar Sangam MN",
              Lato,system-ui,sans-serif}
.lang-switch a[lang="my"]{font-size:15px}
@media (min-width:900px){.site-header .lang-switch a{min-width:54px;padding:6px 14px}}
.lang-switch a:hover{color:var(--edu-ink);border-color:var(--edu-primary);text-decoration:none}
.lang-switch a[aria-current="true"]{
  background:var(--edu-primary);border-color:var(--edu-primary);color:var(--edu-primary-fg)}
/* /news/ */
.news-item{margin-top:var(--edu-space-4)}
.news-item .tag{font-weight:700}
.news-item .news-image{display:block;max-width:100%;height:auto;border-radius:12px;margin-bottom:var(--edu-space-3)}
.news-announcements{margin-top:var(--edu-space-5)}
.news-announcements ul{margin:0;padding-left:1.15em;display:grid;gap:var(--edu-space-2)}
.langblock{display:none}
/* .active wins over [hidden] here on purpose — both are toggled together, and
   the class is what the guide's search code keys off. */
.langblock.active{display:block}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .btn:active{transform:none}
}
/* Motion (2026-08-29). Two tokens and two behaviours, no library.

   This stylesheet had no duration or easing scale — every transition wrote its
   own `.18s ease` — so these are the first ones, not a second scale competing
   with an existing one. `--edu-ease-out` is the curve skill
   `web-quality-baseline` calls `--ease-out-expo`, renamed to this file's own
   `--edu-` prefix so the naming stays consistent. */
:root{
  --edu-dur-fast:150ms;
  --edu-dur-base:240ms;
  --edu-ease-out:cubic-bezier(0.16,1,0.3,1);
}
/* Back to top.
   The guide is one 1.2 MB document and the legal pages run to thousands of
   words; on a phone that is a minute of swiping back to the header. Occasional
   surface, so standard motion rather than none — and it only appears after 1.5
   viewports, because a control that is already there when you land is a control
   with nothing to do. */
.to-top{
  position:fixed;right:max(var(--edu-space-4),env(safe-area-inset-right));
  bottom:max(var(--edu-space-4),env(safe-area-inset-bottom));
  width:var(--edu-touch);height:var(--edu-touch);z-index:39;
  display:inline-flex;align-items:center;justify-content:center;
  border-radius:999px;border:1px solid var(--edu-line);
  background:var(--edu-surface);color:var(--edu-primary);
  box-shadow:var(--edu-shadow);cursor:pointer;
  /* Named properties, never `all`: this element also carries a shadow and a
     border colour that must not animate. */
  transition:opacity var(--edu-dur-base) var(--edu-ease-out),
             transform var(--edu-dur-base) var(--edu-ease-out);
  opacity:0;transform:translateY(12px) scale(.96);pointer-events:none;
}
.to-top.is-on{opacity:1;transform:none;pointer-events:auto}
.to-top svg{width:20px;height:20px;display:block}
.to-top:focus-visible{outline:2px solid var(--edu-primary);outline-offset:2px}
@media (hover:hover) and (pointer:fine){.to-top:hover{filter:brightness(.98)}}
@media print{.to-top{display:none}}
/* Section reveal.
   Content the reader scrolls past once fades up the first time it enters the
   viewport and then stays put. Never re-runs: a block that re-animates on every
   pass is a page that will not sit still while you read it. */
.reveal{opacity:0;transform:translateY(14px)}
.reveal.is-in{
  opacity:1;transform:none;
  transition:opacity var(--edu-dur-base) var(--edu-ease-out),
             transform var(--edu-dur-base) var(--edu-ease-out);
  transition-delay:var(--reveal-delay,0ms);
}
/* Reduced motion: gentler, not gone. The fade still says something arrived; the
   travel and the stagger — the parts that move the page under a reader who
   asked it not to — are dropped. */
@media (prefers-reduced-motion:reduce){
  .reveal{transform:none}
  .reveal.is-in{transition:opacity var(--edu-dur-fast) ease;transition-delay:0ms}
  .to-top,.to-top.is-on{transform:none;transition:opacity var(--edu-dur-fast) ease}
}
/* Page-to-page loading. Every public page is a full document load, and on a
   slow link the old page used to sit there unchanged after a tap with nothing
   saying the tap was taken — readers tapped again or gave up. SHARED_JS adds
   .is-on to this bar when a same-site link is followed; it trickles towards
   85% and never claims 100%, because the browser, not the bar, knows when the
   next page arrives (it replaces this document). The 150ms delay keeps a fast
   navigation from flashing it. Where the browser supports cross-document view
   transitions the two pages also cross-fade instead of cutting. */
.nav-progress{
  position:fixed;top:0;left:0;z-index:9999;height:3px;width:0;
  pointer-events:none;opacity:0;
  background:linear-gradient(90deg,var(--edu-primary),var(--edu-accent));
  box-shadow:0 0 8px rgba(220,12,131,.45);
}
.nav-progress.is-on{
  opacity:1;transition:opacity .2s ease .15s;
  animation:nav-trickle 10s cubic-bezier(.08,.7,.2,1) .15s forwards;
}
@keyframes nav-trickle{from{width:0}to{width:85%}}
html[aria-busy="true"]{cursor:progress}
@media (prefers-reduced-motion:no-preference){
  @view-transition{navigation:auto}
}
@media (prefers-reduced-motion:reduce){
  .nav-progress.is-on{animation:none;width:100%}
}
/* Body copy at 14px is a desktop size. On a 360px handset - the cheap Android
   this audience actually carries - it is squinting, and this site gets read on
   the bus while checking a child's attendance. One step up on the surfaces that
   carry running prose; the chrome keeps its tighter scale.

   Placed at the end of the stylesheet on purpose. The first attempt sat beside
   the header rules and did nothing: `.card p` is declared further down at the
   same specificity, so source order handed the 14px straight back. */
/* Owner, 2026-10-13: prose fills its card and is justified, like the legal
   clauses. Tables and code-like rows are not prose and keep their own flow.
   Not on a phone, and not in the landing hero: in a column that narrow,
   justifying opens gaps between the words. */
@media (min-width:700px){
  .legal-section p, .recipe p, .sec-body > p, p.when, p.result, .pitfalls,
  .news-item > p:not(.meta), body:not(.public-landing) .lead{
    text-align:justify;hyphens:auto}
}

@media (max-width:480px){
  .card p,
  .foot-col a,
  .foot-about p,
  .pitfalls,
  .checklist,
  .sec-body li,
  .sec-body p,
  /* The landing keeps its own <style> block from the prototype, and it is
     emitted after this stylesheet — at equal specificity it wins. Prefixing
     with the body class outranks it instead of fighting over source order. */
  body.public-landing .auth-card p,
  body.public-landing .news-lead .body p,
  .auth-card p,
  .news-lead .body p,
  p.meta{font-size:15px}
}

