/* =====================================================================
   Vibedu Platform — Design System  (Structured Minimalism, Spec §7/§8)
   Plain CSS + tokens. Mobile-first. No build step.
   ===================================================================== */
:root{
  /* Brand — from the Vibedu logo (Spec §7) */
  --blue:#244F94;         /* Primary — CTAs, headers, nav, active states */
  --blue-600:#1d3f78;
  --blue-100:#e8eff8;
  --orange:#E08431;       /* Primary accent — FILLS ONLY (buttons, bars, dots) */
  /* Ink version of the accent, for accent-coloured TEXT on a light ground.
     --orange itself is 2.79:1 on white; it was the colour of .eyebrow, used ~90
     times across the site, so the most-repeated label on every page failed AA by
     a wide margin. --orange-600 existed but at #c6711f was only 3.63:1 — still
     short of the 4.5:1 body-text bar, and 3.12:1 once it sat on the .16 orange
     tint the badges use. #9a5410 clears 4.5:1 on white (5.75), on --bg (5.36)
     and on every orange tint in this file (>=4.94). Text uses this token; fills
     keep --orange. */
  --orange-600:#9a5410;
  --ocean:#002B4C;        /* Dark — footer, premium/finance sections */
  --ice:#C0EBFF;          /* Light card fill / backgrounds */
  --summer:#F59E71;       /* Soft secondary badge (not primary CTA) */
  /* Accent TEXT on a DARK ground — the mirror of --orange-600.
     --orange-600 fixed accent text on white; the dark half of the site was
     missed, and it is where .eyebrow actually appears most: .hero .eyebrow,
     .hero h1 .accent, .visa-stat b and .cd-time all overrode the token back to
     --summer. On the hero's lightest gradient stop (#2f63b8) --summer measures
     2.78:1 — WORSE than the 2.79:1 the audit reported on white, and under the
     3:1 that even the 2.2rem .visa-stat numbers need. .cd-time sat at 3.82:1 on
     the countdown banner, which is on every page.
     #FFE3C9 keeps the warm accent and measures 4.75:1 on #2f63b8, 6.52:1 on
     --blue and 11.78:1 on --ocean, so it clears AA body text on every dark
     ground in this file. --summer stays for fills and borders. */
  --summer-ink:#FFE3C9;

  /* Neutrals (blue-biased) */
  --white:#ffffff;
  --bg:#f4f7fb;
  --surface:#ffffff;
  --surface-2:#f7fafd;
  --text:#16202e;
  /* MEASURED, do not "fix" again: 5.46:1 on --surface and 5.08:1 on --bg, so it
     clears AA body text on both light grounds with headroom. A 2026-08 design
     audit reported this as 3.85:1 and failing; that figure is wrong — recomputed
     from the sRGB luminance formula it is 5.46:1. (The one ground where it DID
     fail is --ocean at 2.65:1, which .foot .muted already overrides below.) */
  --muted:#5b6b7e;
  --border:#e3eaf3;
  /* 2026-09-09, audit: was #cdd9e8, which is 1.37:1 against .field's own fill and 1.43:1 on
     white. This border is the ONLY thing that says "this is a control" — .field's fill against
     the page is 1.03:1 and .btn-ghost's is 1.07:1 — so WCAG 1.4.11's 3:1 applies to it.
     #6f849e measures 3.84 on --surface, 3.57 on --bg, 3.66 on --surface-2 and 3.01-3.09 on all
     four card tints, so a field dropped inside a tinted card still clears the bar.
     Mirrored in the critical block in layouts/app.blade.php — change both or first paint
     draws the old hairline and the async sheet snaps it darker. */
  --border-strong:#6f849e;

  /* Card tints — pale grounds of the four brand fills (blue / orange / teal /
     ocean) already used by the homepage tool cards. Consumed only through
     --card-bg on .card, so nothing paints with them by accident. Measured on
     every one: --text >=12.7:1, --muted-on-tint #46566a >=5.81:1, --blue
     >=6.21:1, --orange-600 >=4.52:1. The ocean value is #DCE7EF and not the
     #D9E4ED it started as because that held --orange-600 at 4.46. */
  --tint-blue:#DCE8F7;
  --tint-peach:#FADFCB;
  --tint-teal:#D2EBE9;
  --tint-ocean:#DCE7EF;

  /* System — each one is used BOTH as text on a pale tint of itself and as a
     solid fill under white text, so both directions have to clear AA. The old
     values did neither: --ok was 3.64:1 on its own .14 tint, --warn 2.98:1,
     --danger 4.08:1. Darkened until the worst pairing in this file passes 4.5:1;
     white-on-fill improves at the same time (e.g. white on --ok 4.33 -> 5.78). */
  --ok:#16744a; --warn:#8a5410; --danger:#b03a2f;
  /* WhatsApp brand green is 1.98:1 under white text — the single worst pairing
     measured. Its darker sibling keeps the colour recognisably WhatsApp while
     reaching 5.43:1 both as a fill and as text on white. */
  --wa-green:#0f7a3a;
  /* An OPAQUE ground for text that has to sit on a colour this stylesheet does
     not choose. The exam tiles are painted with each exam's own brand colour
     from config/logos.php and those span #000000 (GMAT) to #FFAA00 (Duolingo):
     no single ink clears 4.5:1 on both ends, so .exam-card-score's white-at-.9
     measured 1.78:1 on the orange and 2.43:1 on TOEFL's lilac. Text on a tile we
     do not control brings its own ground instead of borrowing the tile's; white
     on this is 14.48:1 no matter what is underneath. */
  --on-brand-chip:#002B4C;
  --radius:16px; --radius-sm:11px; --radius-lg:22px; --radius-pill:999px;
  --shadow-sm:0 1px 2px rgba(16,40,70,.06);
  --shadow:0 2px 6px rgba(16,40,70,.06),0 12px 30px rgba(16,40,70,.08);
  --shadow-lg:0 18px 46px rgba(16,40,70,.16);
  /* Bottom-pinned chrome casts upward. Was written twice, as -6px 22px and
     -6px 20px at the same alpha — one idea, two numbers, 2px apart. */
  --shadow-up:0 -6px 22px rgba(16,40,70,.08);
  --nav-h:64px;

  /* ---------- Type scale ----------
     Four audits running, Typography has scored 46-58 with the same diagnosis:
     two good families and no scale under them. There were no size tokens at all,
     so every rule picked its own number and the result reads as drift rather
     than hierarchy — measured on the homepage, all nine h2s were exactly 32px so
     the FAQ carried the same weight as the destinations grid, while h3 (the most
     repeated text on the site) rendered at 16, 16.96, 18 and 19.2px, five sizes
     inside a 3.2px band that nobody can see as deliberate. At 16px a card title
     was the same size as the paragraph under it.
     This is NOT a 1.25 scale and an earlier version of this comment said it
     was. Only the step going DOWN is 1.25 (16/1.25 = 12.8 = --fs-xs). Upward
     the ratios are 1.094 (12.8->14), 1.143 (14->16), 1.125 (16->18), 1.222
     (18->22): sizes that were already in the file, tokenised — not a scale
     that was designed. Re-spacing is deferred on purpose, and the reason is
     not overflow: h3 at 20px was measured against the narrowest real .uni-card
     column (219px, 2-up at the 560px breakpoint) and overflows nothing at any
     breakpoint. It is deferred because only 43 declarations in this file
     reference these tokens while 410 inline font-size declarations in
     resources/views do not, so moving the tokens widens the drift instead of
     closing it, and because one step cascades into all five. Two measurements
     to keep for whoever does re-space: --fs-h2 is pinned at its 24px clamp
     floor for every viewport under 1000px (2.4vw only reaches 24px at 1000px),
     which leaves it 9% from --fs-h2-sm's flat 22px — on a phone the quieter
     heading is not visibly quieter; and lifting that floor to 26px costs
     +76.1px of vertical on the homepage's six section headings and wraps two
     more. --fs-body is currently referenced zero times anywhere.
     The old 11.52px size was carrying real content (tab-bar labels, every
     .eyebrow, photo credits) and 12.8px is the floor a phone can read. That

     RAMP (changed 2026-09-05): h1:h2 was 1.20 on every viewport under 533px --
     the entire stated audience -- against 1.60 at desktop, so page headline and
     section heading were nearly the same voice on a phone. h1's floor lifts to
     34px, giving 1.42 at 375px and 1.84 at 768px. h2's 24px mobile FLOOR is
     deliberately untouched, because the objection measured above still stands;
     only its vw term moves, so h2 grows in the 706-882px tablet band where
     vertical space is cheap and nothing wraps.
     floor is enforced by a test, not by good intentions.
     Deliberately no token name followed by a colon anywhere in this prose: a
     parser reading these tokens will swallow the definition into the comment
     unless it strips comments first, and the first version of that test did
     exactly that and reported a passing floor it could not actually see. */
  --track-label:.08em;  /* uppercase micro-labels — see .mini-h for the measurement */
  --fs-xs:.8rem;        /* 12.8 — labels, credits, legal. The floor.       */
  --fs-sm:.875rem;      /* 14   — captions, meta rows                      */
  --fs-body:1rem;       /* 16   — body copy                                */
  --fs-h3:1.125rem;     /* 18   — card titles                              */
  --fs-h2-sm:1.375rem;  /* 22   — secondary section headings               */
  --fs-h2:clamp(1.5rem,3.4vw,1.875rem);   /* 24 floor to 706px, ~26 at 768, 30 from 882 */
  --fs-h1:clamp(2.125rem,6.2vw,3rem);     /* 34 at 375, 48 from 775 — see the ramp note below */
  --lh-card:1.28;       /* wrapped card titles (h3) — see below         */

  /* ---------- Spacing ----------
     A 4px grid. Gaps in this file ran 2,3,4,5,6,7,8,9,10,12,14,16,18,26,48px
     with no tokens, which is why nothing lines up between components that were
     written weeks apart. New work uses these; existing values move to them as
     each component is next touched, rather than in one sweeping edit that
     nobody can review. */
  --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px;
  --space-5:24px; --space-6:32px; --space-7:48px; --space-8:64px;

  /* ---------- Section rhythm ----------
     Thirteen homepage sections were all padded identically, so the scroll never
     changed gear and a 651px video rail read as no more important than a 110px
     CTA strip. Three weights: a feature section can breathe, a utility strip
     (marquee, recently-viewed, FAQ) gets out of the way, and everything else is
     standard. */
  --pad-compact:clamp(1.6rem,3.2vw,2.5rem);
  --pad-section:clamp(2.5rem,5.5vw,4rem);
  --pad-feature:clamp(3.5rem,7vw,6rem);
  /* TYPE. The site shipped with no brand face at all: --font was the raw OS
     stack, so headings AND body rendered as whatever the device defaults to
     (Segoe UI on Windows, San Francisco on Mac). A design audit called it the
     single strongest "generic template" signal on the site — every reference
     product in this category ships a considered face, and a pure system stack
     reads as unstyled no matter how good the layout underneath is.
     Inter for body (built for UI, excellent at the 13-16px this site lives in)
     and Plus Jakarta Sans for display (geometric-humanist, more character in
     the 800 weights the headings use). The OS stack stays as the fallback, so
     a font failure degrades to exactly what shipped before, never to serif. */
  --font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
  --font-display:"Plus Jakarta Sans",var(--font);
  --maxw:1180px;
}

*{box-sizing:border-box;}
html{-webkit-text-size-adjust:100%;}
body{
  margin:0;background:var(--bg);color:var(--text);font-family:var(--font);
  line-height:1.55;-webkit-font-smoothing:antialiased;
  /* clear the fixed bottom-nav on mobile */
  padding-bottom:calc(var(--nav-h) + env(safe-area-inset-bottom));
}
@media(min-width:900px){ body{padding-bottom:0;} }
a{color:var(--blue);text-decoration:none;}
img{max-width:100%;display:block;}
/* 2026-09-09, audit: the UA sheet's [hidden]{display:none} is a plain element rule, so ANY
   author display beats it — .btn-block{display:flex} included. That is why the drawer's
   "Install the Vibedu app" and "Get deadline alerts" buttons rendered on every device
   despite carrying `hidden`: on iOS Safari beforeinstallprompt never fires, so the install
   button was permanently visible AND inert (vbInstall() returns immediately with no prompt).
   Those two dead buttons were 116px of a 307px sticky footer that was eating half the menu.
   The JS only ever toggles the attribute (setAttribute/removeAttribute/.hidden), never
   style.display, so !important here cannot strand anything visible. */
[hidden]{display:none!important;}
/* Form controls do NOT inherit font-family from their parent — that is UA
   behaviour, not a bug in the cascade — so an unstyled <button> renders in the
   browser's own default face. This file had been fixing that one component at a
   time (`font:inherit` appears on ~20 selectors below), which is exactly why the
   filter buttons on /universities were still painting in Arial while everything
   around them was Inter. Reset it once, here, so new controls are correct by
   default instead of correct only when someone remembers. */
button,input,select,textarea,optgroup{font:inherit;color:inherit;letter-spacing:inherit;}
/* <b> and <strong> default to `bolder`, which is RELATIVE: inside a parent at
   700 it resolves to 900, and no face in this file goes that heavy. Pin them to
   a real weight so they use a real font file. */
b,strong{font-weight:700;}
/* Reading measure. 820px is right for a column of prose and wrong for a page
   built out of card grids, but it had been pasted as `style="max-width:820px"`
   into 30 places, several of which wrap 12-card layouts — on /university/* that
   rendered 784px of cards inside a 1265px viewport with ~240px dead on either
   side. Prose pages keep the cap and now say so with a class; grid pages use
   the full --maxw and rely on the 72ch cap below to keep paragraphs readable. */
.container.reading{max-width:820px;}
h1,h2,h3{margin:0 0 .4rem;letter-spacing:-.02em;line-height:1.12;font-family:var(--font-display);}
/* Size was the ONLY thing separating the three heading levels: all of h1-h3 sat
   at weight 800 and line-height 1.12, so an h3 beside an h2 read as the same
   voice slightly smaller. Splitting the weights gives the hierarchy a second
   signal that survives a card whose title happens to be short.
   h3 also gets a looser line: every card on the site titles with an h3, those
   titles wrap, and 1.12 set for a one-line hero jams wrapped lines together.
   1.28 is not a new opinion -- .uni-card h3 already measured its way there. */
h1{font-weight:800;}
h2{font-weight:700;}
h3{font-weight:600;line-height:var(--lh-card);}
/* Balanced wrapping on headings. Verified at 375px: widows drop from 11 to 4,
   including the H1 and all four section headings. Mirrored into the critical
   block below -- app.css is preload-swapped, so without the mirror the H1 paints
   unbalanced and REFLOWS when the async sheet lands, which is the above-the-fold
   moment. The tallest heading here is 4 lines, inside Chrome's 6-line balance
   cap, so there is no silent fallback. */
h1,h2,h3{text-wrap:balance;}.brand,.btn,.eyebrow,.stat .n,.job-nums .n,.emi-main .n,.score-val,.gauge-n,.dl-days,.slider-val,.meetup-date .md-d,.greviews-score b,.visa-stat b,.hero-trust .ht-item b,.vb-drawer-label,.post-cat,.marquee-label,.trust-h,.modal-eyebrow,.news-cat,.inv-status{font-family:var(--font-display);}.stat .n,.job-nums .n,.emi-main .n,.score-val,.gauge-n,.dl-days,.slider-val,.meetup-date .md-d,.greviews-score b,.visa-stat b,.uni-rank,td.num,.emi-out b{font-variant-numeric:tabular-nums;}
h1{font-size:var(--fs-h1);}
h2{font-size:var(--fs-h2);}
/* A section that is a utility strip, not a chapter. Every h2 on the homepage
   measured exactly 32px, so the marquee label and the FAQ shouted as loudly as
   the destinations grid; this is the quieter step that lets them stop. */
.section-compact h2,h2.h2-sm{font-size:var(--fs-h2-sm);}
/* Same level-modifier convention as .h2-sm above. The auth flow hardcoded 1.7rem
   and 1.5rem -- neither is in the scale -- because inheriting a hero-tuned
   --fs-h1 above a 440px card wraps to three lines and overpowers the form.
   This puts the whole flow on one responsive on-scale size instead. */
h1.h1-sm{font-size:var(--fs-h2);}
/* 1.06rem was 16.96px against a 16px body — a 6% step the eye does not read as
   a heading at all. Every card on the site titles with an h3, so this one value
   decides whether cards have hierarchy. Now the scale's own step. */
h3{font-size:var(--fs-h3);}
/* <small> had no rule, so it took the UA default of 13.333px — a third small
   size sitting between this file's own 12.8 and 14, present on nearly every
   card. */
small{font-size:var(--fs-sm);}
p{margin:.4rem 0;}

/* Landscape on a notched iPhone puts the island over one edge of the page. env() is 0px
   everywhere else, so max() keeps the usual 18px gutter on every other device. */
.container{max-width:var(--maxw);margin:0 auto;
  padding-left:max(18px,env(safe-area-inset-left));padding-right:max(18px,env(safe-area-inset-right));}
/* VERTICAL RHYTHM.
   This was clamp(1.6rem,4vw,3rem) — 26px on a phone, 48px at any desktop width,
   and it never grew past 48 no matter how wide the screen got. Twelve sections
   stacked at that cadence is what made the site read as a wall of bands rather
   than a composed page; the reference products sit at 96-160px between sections.
   A section owns HALF the gap at each end, so consecutive sections produce
   80px on a phone and 128px on a desktop. */
.section{padding:var(--pad-section) 0;}
/* Two more gears. Applied per section in the views — a page that uses only
   .section still looks exactly as it did. */
.section-feature{padding:var(--pad-feature) 0;}
.section-compact{padding:var(--pad-compact) 0;}

/* MEASURE. Body copy ran to 93 characters per line on a university page and the
   fine print to about 130 — against the 60-75 that is comfortable to read.
   The first version of this rule was `.container p:not([class])`, on the
   assumption that a classed paragraph carried its own sizing. It does not:
   .muted is a COLOUR, not a width, and it sits on 299 of the ~340 classed
   paragraphs in resources/views. Measured at a 1280 viewport before this
   change: /university/university-of-oxford had 16 substantial paragraphs in a
   .container and ZERO matched the cap — 10 ran past 72ch, the worst being
   11.52px fine print across 1090px, a 150-character measure; /loans left its
   rate disclaimer at 784px of 12.8px text (97ch); the homepage left a 12.8px
   note running the full 1144px (142ch). The four paragraphs the old selector
   did catch were 21-27-character labels in 236px flex cells, where the cap
   cannot bind. So the cap now applies to every paragraph in a .container and
   the real exceptions are NAMED below instead of inferred from "has a class".
   THE UNIT WAS WRONG, and the number hid it. `ch` is the advance width of the
   glyph "0" — about 0.63em in Inter — while running lowercase text averages
   nearer 0.51em. So a cap written as 72ch delivers roughly 72 x 0.63 / 0.51 = 89
   characters. Counted for real with Range rects across three /loans paragraphs:
   89, 91, 93, 94, 95, 96 — a 93-character mean against the 66-75 that is
   comfortable, i.e. 29% over, while the rule and its comment both said 72.
   36em is ~585px at 16px and measures ~75 characters. em is glyph-agnostic, so
   it survives a font change; ch does not, and would silently re-scale the
   measure the day the typeface is swapped. */
.container p,.legal p,.prose p{max-width:36em;}
/* Opt-outs. .foot-copy is not prose — it carries the footer's 1px border-top,
   which reads as a full-width rule; capping the paragraph would cut that line
   to 727px inside a 1144px footer. .measure-full is the escape hatch for any
   future paragraph that is really a full-width band. Both are written as
   `.container p.x` (0,2,1) so they beat the cap (0,1,1) regardless of source
   order — a bare `.foot-copy` (0,1,0) would silently lose to it. */
.container p.foot-copy,.container p.measure-full,.legal p.measure-full,.prose p.measure-full{max-width:none;}
/* A capped box is still a left-aligned box, so centre-aligned paragraphs have
   to centre the BOX or the text jumps left: the homepage marquee label sits in
   a 1144px box that the cap takes to 581px (281px of leftward shift) and the
   /loans hub-hero intro in a 784px box taken to 727px (28px). Child selector
   on purpose — every centred paragraph measured is a direct child of .center;
   anything nested deeper should carry `class="center"` itself. */
.center>p,p.center{margin-inline:auto;}
/* .eyebrow and .accent are small accent-coloured TEXT — the ink token, not the
   fill token. See --orange-600. */
/* .72rem was 11.52px — used 27-35 times on the homepage alone, on every section
   label on the site. Below the 12.8px floor a phone can comfortably read, and it
   is uppercase with .13em tracking, which makes it harder again. */
.eyebrow{font-size:var(--fs-xs);font-weight:800;letter-spacing:var(--track-label);text-transform:uppercase;color:var(--orange-600);}
.muted{color:var(--muted);}
.accent{color:var(--orange-600);}
.center{text-align:center;}

/* .mini-h was a hook with nothing behind it: grep found 14 inline copies across
   10 views, every one re-declaring the same five properties, and 9 of them at
   .72rem = 11.52px, under the floor. This definition must land BEFORE those
   inline copies are deleted — with no rule here .mini-h falls back to the 16px
   body and "Tuition & scholarships" measured 17.8px tall against 24.8px.
   Folding the two tracking variants (.06em in guide-body/study-in/visa-guide,
   .08em everywhere else) into one costs nothing: at all three widths that
   actually occur — 310px 1-col at a 390px viewport, 211px 2-col at 560px,
   315px 2-col at 768px — all nine real labels stay on one line with no
   overflow, before and after. .trust-h is defined further down this file and
   still wins where success.blade.php asks for both. */
.mini-h{font-size:var(--fs-xs);font-weight:700;text-transform:uppercase;letter-spacing:var(--track-label);color:var(--muted);}

/* The honesty disclaimers were the smallest text on the site: 22 of them set
   inline at .72rem and .75rem — 11.52px and 12px, both under the floor — while
   the marketing copy above them ran 16px. "Indicative only", "not an official
   score", "we're paid by partner universities, not by you": the sentences the
   trust posture rests on, in the least readable type on the page. At the floor
   they do not reflow — the longest (/tools/work-rights, 154 characters) measured
   3 lines at 390px both before and after, 53.5px to 57.6px tall.
   No margin and no line-height on purpose: the 22 sites use six different
   margins and body already sets line-height:1.55, so only the size moves out of
   the markup and nothing shifts. */
.fineprint{font-size:var(--fs-xs);color:var(--muted);}

/* Four error pages each carried this identical four-property block inline. */
.err-code{font-size:4rem;font-weight:900;color:var(--blue);letter-spacing:-.03em;}

/* ---------- Header (app top bar) ---------- */
/* The 18px-spread shadow under a 100px block of saturated blue made the header
   read as a heavy slab and pushed the "wall of blue" impression the audit
   flagged. A hairline plus a short shadow separates it from the page just as
   well at a fraction of the visual weight. The two-row structure is kept
   deliberately: the second row is the only place a desktop visitor can see the
   site's eight sections, and collapsing it would trade a design complaint for a
   navigation one. */
/* 2026-09-09, reported from an iPhone: "the header options are hiding behind the speakers".
   The document declares viewport-fit=cover (layouts/app.blade.php:6), which is what lets the
   blue run edge to edge under the status bar — but nothing reserved the space, so on a notched
   or Dynamic Island iPhone the logo, search, home, Counselling and menu controls all painted
   UNDERNEATH the clock and battery. The padding puts the bar below the island while the blue
   background still fills the area behind it, which is the point of viewport-fit=cover.
   The rest of the file already assumed this was handled: .vb-conn sits at
   calc(58px + env(safe-area-inset-top)), i.e. below a header that reserves the inset. It did
   not. On anything without an inset env() resolves to 0px, so no other device changes.
   Landscape: on a notched iPhone the island eats one side, so the bar's own gutter takes the
   larger of its usual 16px and whatever the system reserves. */
.app-header{position:sticky;top:0;z-index:40;background:var(--blue);color:#fff;
  padding-top:env(safe-area-inset-top);
  box-shadow:0 1px 0 rgba(255,255,255,.08),0 4px 12px rgba(0,20,50,.10);}
.app-header .bar{display:flex;align-items:center;gap:14px;height:58px;max-width:var(--maxw);margin:0 auto;
  padding-left:max(16px,env(safe-area-inset-left));padding-right:max(16px,env(safe-area-inset-right));}
.brand{display:flex;align-items:center;gap:9px;font-weight:800;font-size:1.25rem;color:#fff;letter-spacing:-.01em;min-height:44px;padding:0 5px;}
/* The wordmark next to the square logomark. Under 380px the header row needs the
   space for the icon buttons and CTA, so the mark carries the brand alone there —
   the anchor's aria-label still names the company for assistive tech. */
.brand-word{font-family:var(--font-display);font-weight:800;font-size:1.25rem;letter-spacing:-.02em;line-height:1;}
/* Hidden below 440px, not 380px. The header row carries logo + wordmark +
   search + home + CTA + burger, and at 390px (iPhone 12/13/14/15 — the single
   most common viewport) the wordmark pushed the row over the edge: the burger
   measured x=367..411 in a 390px viewport, so the primary navigation control was
   clipped 21px off-screen and unreachable, because overflow-x is clipped. 440px
   is where the row already compresses everything else, so the wordmark now
   leaves at the same point rather than 60px later. */
@media(max-width:439px){ .brand-word{display:none;} }
/* With the wordmark gone the mark alone carries the brand, and at 29px it was
   an illegible dot. It gets the room the wordmark released. */
@media(max-width:439px){ .brand-logo{height:34px;} }
.brand .dot{width:11px;height:11px;border-radius:50%;background:var(--orange);display:inline-block;}
.app-header .spacer{flex:1;}
.icon-btn{width:38px;height:38px;border-radius:12px;display:grid;place-items:center;color:#fff;
/* Not a text size: an icon glyph inside a 44px button. */
  background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.16);font-size:1.06rem;cursor:pointer;transition:.15s;}
.icon-btn:hover{background:rgba(255,255,255,.22);}
/* ONE orange per screen. On a phone the header CTA, the sticky bar's primary and
   the contact hub all push "talk to a counsellor", and the first two were the
   same orange fill — so nothing read as THE action and the top of the screen
   competed with the bottom. Below 700px the header CTA becomes an outline: still
   present, still tappable, visibly secondary to whatever the page's own primary
   is. It keeps the solid fill from 700px up, where there is no sticky bar and it
   IS the page's primary call. Min-height 40px keeps it a legal tap target
   (it was 33px). */
.header-cta{display:inline-flex;align-items:center;justify-content:center;gap:.35rem;
  background:transparent;border:1px solid rgba(255,255,255,.55);color:#fff;
  /* 44, not 40. This file already sets 44 as its own floor — .icon-btn is
     44x44, .tabbar a carries min-width:44px, .pcard-cta min-height:44px — and
     the most-tapped control in the header was the one sitting under it. A floor
     that the primary CTA is exempt from is not a floor. */
  font-weight:700;padding:.45rem .8rem;min-height:44px;border-radius:12px;font-size:.8rem;white-space:nowrap;}
@media(min-width:700px){ .header-cta{background:linear-gradient(135deg,#ff9a3d,var(--orange));border-color:transparent;
  color:var(--text);padding:.55rem 1.15rem;font-size:var(--fs-sm);gap:.45rem;} }

/* ---------- Phone app bar (inner pages) ----------
   Below 900px an inner page's header reads like an app's: "‹ back", the page's
   name, search, a round orange Counselling button, menu. The logo gives way to
   the name, and the Home button goes on every phone page because Home is the
   first bottom tab again. Desktop keeps the website header untouched.
   MIRRORED in the critical block in layouts/app.blade.php — the header is the
   first thing painted and this file loads async. Both copies or neither. */
.appbar-back,.appbar-title{display:none;}
@media(max-width:899px){
  .app-header .hdr-home{display:none;}
  .app-header.is-inner .brand,.app-header.is-inner .spacer{display:none;}
  .app-header.is-inner .bar{gap:8px;}
  .app-header.is-inner .appbar-back{display:grid;margin-left:-8px;background:transparent;border-color:transparent;}
  .appbar-back-ic{transform:rotate(180deg);}
  .app-header.is-inner .appbar-title{display:block;flex:1;min-width:0;font-family:var(--font-display);font-weight:800;font-size:1.03rem;
    letter-spacing:-.01em;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  .app-header.is-inner .header-cta{width:44px;min-width:44px;padding:0;border-radius:50%;
    background:linear-gradient(135deg,#ff9a3d,var(--orange));border-color:transparent;color:var(--text);}
  .app-header.is-inner .header-cta .cta-short,.app-header.is-inner .header-cta .cta-full{display:none;}
}
/* App-style screen change. Every page — a full load or a wire:navigate swap,
   which replaces <body> wholesale — fades its content up from .35. Opacity only:
   a transform on <main> would make it the containing block for every
   position:fixed element inside it for the length of the animation. Starting
   above 0 keeps the first frame a real paint for Largest Contentful Paint. */
main{animation:vb-screen-in .22s ease-out;}
@keyframes vb-screen-in{from{opacity:.35;}}
/* The grey flash Android paints on every tap is the most "website" thing a
   phone does; the pressed states in item 10 below already answer the tap. */
html{-webkit-tap-highlight-color:transparent;}
/* The inline search box and the magnifier icon-button are meant to be mutually
   exclusive — box on wide screens, icon on narrow. They were not: the critical
   CSS in layouts/app.blade.php swaps them at 820px, this file revealed the box
   at 700px and never hid the icon at all. So once the async stylesheet landed,
   every viewport from 700 to 819px showed BOTH a search field and a search
   button side by side, which is what the design audit reported as "double
   search". Same breakpoint as the critical block, and the pair is swapped
   together so neither can appear without the other disappearing. */
.nav-search{display:none;}
.nav-search-m{display:grid;}
@media(min-width:820px){ .nav-search{display:flex;} .nav-search-m{display:none;} }
.brand-logo{height:29px;width:auto;max-width:116px;display:block;}
@media(min-width:700px){ .brand-logo{height:34px;max-width:170px;} }

/* ---------- Bottom tab nav (mobile app shell) ---------- */
.tabbar{position:fixed;left:0;right:0;bottom:0;z-index:50;background:var(--surface);
  border-top:1px solid var(--border);display:flex;height:calc(var(--nav-h) + env(safe-area-inset-bottom));
  padding-bottom:env(safe-area-inset-bottom);box-shadow:var(--shadow-up);
  /* The bottom inset was handled; the side ones were not, so in landscape the first and last
     tab sat under the island. */
  padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right);}
.tabbar a{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
  color:var(--muted);font-size:var(--fs-xs);font-weight:600;padding-top:6px;}
.tabbar a .ti{font-size:1.25rem;line-height:1;}
/* WCAG 1.4.1 — the entire "you are here" signal was hue plus a 1px lift. Measured
   on the bar's real ground (--surface #fff): active --blue 8.01:1, inactive --muted
   5.46:1, .tab-ai's --orange-600 5.75:1 at luminance .1325 against inactive's .1425
   — in greyscale the AI tab and an unselected tab were the same, and the AI tab was
   the only one carrying a filled shape, so it outranked whichever tab was actually
   selected. The selected tab now caps its top edge with a 3px bar, the same shape
   .desk-nav-a.is-on uses; the state survives greyscale because it is a shape, not
   a hue.
   The bar is --blue, NOT --orange. desk-nav paints its orange marker on --blue-600
   (#1d3f78) where it reads 3.69:1, but THIS bar's ground is --surface #fff, where
   --orange is 2.79:1 vs white and 2.31:1 vs the --border it overlaps — both under
   the 3:1 that WCAG 1.4.11 requires of a state indicator. Copying the marker across
   the light/dark inversion would have traded a 1.4.1 failure for a 1.4.11 one.
   --blue is 8.01:1 on white and 6.61:1 on the border.
   No cell tint: --blue-100 is 1.16:1 against --surface so it is not perceivable as
   an indicator on its own, it would drop the active label 8.01:1 -> 6.92:1, and
   *{box-sizing:border-box} plus .tabbar's padding-bottom:env(safe-area-inset-bottom)
   means a background on the flex child stops ~34px above the physical screen edge on
   a notched iPhone — a hard-edged rectangle floating above a white strip.
   position:relative is what the cap hangs off — no later .tabbar a rule sets position
   (they set transform/transition/min-height only), and .tabbar has no overflow, so
   top:-1px will not clip. */
.tabbar a{position:relative;}
.tabbar a.is-active{color:var(--blue);}
.tabbar a.is-active::before{content:"";position:absolute;top:-1px;left:22%;right:22%;
  height:3px;border-radius:0 0 3px 3px;background:var(--blue);}
.tabbar a.is-active .ti{transform:translateY(-1px);}
/* The AI tab's colour is set further down, with the icon ground it belongs to.
   A second .tabbar a.tab-ai{color:...}lived here at the same specificity,one place. */

.tabbar a.tab-ai.is-active{color:var(--blue);}
/* 6-item bar — width follows the label, because equal cells never fitted.
   The previous rule bought space with -.05em tracking and no side padding and
   said so; measured at 375 it still did not work. flex:1 gives every tab an
   identical 63px cell, "Scholarship" renders 66px, so the longest label on the
   site's tightest row overflowed its own cell by 2px each side and touched its
   neighbours — which is the "congested" read — while "Loan" sat in 63px it did
   not need. Equal cells were never the constraint: the six labels total 270px
   in 375, so there are 105px spare. flex-basis:auto hands each tab its own
   content and shares the slack equally, so the long label gets room from the
   short ones instead of from its type size. Measured after: 8-9px of clear air
   either side of "Scholarship", narrowest tab 45px (>=44 target), tracking back
   to normal.
   That was written for SIX tabs, where the six labels totalled 270px and the
   shortest slot landed at exactly the 44px floor — no slack left at all. The bar
   is five now (Profile went to the drawer; see the note in layouts/app.blade.php
   for why it was Profile and not Courses), which is what bought the room back:
   five labels total 255px in 375, so each tab takes its own width plus a 24px
   share of the slack instead of 13px. The old floor still earns its place — see
   below — but nothing is running at the limit any more. */
/* min-width is the tap floor, not a guess. With flex-basis:auto the padding
   cancels out of the result — every slot resolves to its own label plus an equal
   share of the slack — so widening the padding cannot buy the shortest tab a
   bigger target. At six tabs "Loan" came out at 43.3px, a pixel under the floor.
   Kept at five because the arithmetic that produced that miss has not changed;
   only the number of labels sharing the slack has. */
.tabbar-5 a{font-size:var(--fs-xs);min-width:44px;white-space:nowrap;flex:1 1 auto;padding-left:5px;padding-right:5px;}
/* One icon slot for every tab, so the AI tab can carry a ground without moving
   its own label. The filled tile this replaces was removed for exactly that: its
   box was 38px against the siblings' 22px, which pushed "AI Tool" 16px below the
   words beside it. Sizing the slot on EVERY tab means the tinted one is the same
   box as the bare ones and every label sits on one baseline. */
/* Not a text size: an ICON glyph box inside the tab bar. */
.tabbar-5 a .ti{font-size:1.06rem;width:30px;height:26px;display:grid;place-items:center;border-radius:8px;}
/* Orange, and legible as orange. --orange-600 #9a5410 clears AA on white (5.75)
   but at 12.8px it reads brown, which is why the one tab meant to stand out did
   not. #B0600F is the most saturated orange that still clears 4.5:1 on white
   (4.64), and the pale ground carries the hue at a size where ink alone cannot:
   #FDEEE2 holds #B0600F at 4.09 — under the 4.5 text bar, so the LABEL keeps its
   ink on white (4.64) and only the ICON sits on the tint, where the bar is 3:1
   for graphics and 4.09 clears it with room.
   Same box as its five siblings, so unlike the old tile this shifts nothing. */
.tabbar a.tab-ai{color:#B0600F;}
.tabbar a.tab-ai .ti{background:#FDEEE2;}
@media(min-width:900px){ .tabbar{display:none;} }

/* ===== Mobile nav drawer (hamburger ☰) ===== */
.vb-burger{margin-left:1px;}
.hdr-home.is-on{background:rgba(255,255,255,.18);}
.vb-drawer{position:fixed;inset:0;z-index:120;visibility:hidden;}
.vb-drawer.is-open{visibility:visible;}
.vb-drawer-scrim{position:absolute;inset:0;background:rgba(6,20,44,.55);opacity:0;transition:opacity .25s ease;}
.vb-drawer.is-open .vb-drawer-scrim{opacity:1;}
.vb-drawer-panel{position:absolute;top:0;right:0;height:100%;width:min(87vw,344px);background:var(--surface);
  box-shadow:-12px 0 46px rgba(6,20,44,.30);display:flex;flex-direction:column;overflow-y:auto;-webkit-overflow-scrolling:touch;
  transform:translateX(102%);transition:transform .3s cubic-bezier(.32,.72,0,1);}
.vb-drawer.is-open .vb-drawer-panel{transform:translateX(0);}
/* The drawer is position:fixed;inset:0, so its own top row sits under the island exactly the
   way the page header did — the Vibedu logo and the close button were the two controls behind
   the clock once the menu was open. It is sticky at top:0 inside a scrolling panel, so the
   inset belongs on its padding rather than on the panel. */
.vb-drawer-top{display:flex;align-items:center;justify-content:space-between;
  padding:calc(13px + env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) 13px 16px;
  border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--surface);z-index:1;}
.vb-drawer-logo{height:30px;width:auto;display:block;}
.vb-drawer-x{width:44px;height:44px;display:grid;place-items:center;border:0;background:var(--surface-2);border-radius:12px;color:var(--muted);
  font-size:1rem;cursor:pointer;display:grid;place-items:center;line-height:1;}
.vb-drawer-nav{display:flex;flex-direction:column;padding:8px 0;}
.vb-drawer-label{font-size:var(--fs-xs);font-weight:800;text-transform:uppercase;letter-spacing:var(--track-label);
  color:var(--muted);padding:12px 18px 5px;}
.vb-drawer-nav a{display:flex;align-items:center;gap:13px;padding:11px 18px;color:var(--text);
  font-weight:600;font-size:1rem;text-decoration:none;}
.vb-drawer-nav a .ic{color:var(--blue);flex:none;}
.vb-drawer-nav a:active{background:var(--surface-2);}
/* The account row. This is the site's only permanent account door now that the
   Profile tab has left the bottom bar, so it opens the drawer rather than
   trailing it — it used to be control 19 of 26, below the drawer's own fold.
   Given a ground and two lines so it reads as a destination rather than as the
   first of nineteen links. */
.vb-drawer-account{background:var(--blue-100);margin:0 12px 6px;border-radius:12px;align-items:flex-start !important;}
.vb-drawer-account b{display:block;font-size:var(--fs-sm);line-height:1.25;}
.vb-drawer-account small{display:block;font-size:var(--fs-xs);color:var(--muted);line-height:1.3;margin-top:1px;}
.vb-drawer-actions{margin-top:auto;padding:14px 16px calc(18px + env(safe-area-inset-bottom));
  border-top:1px solid var(--border);display:flex;flex-direction:column;gap:12px;background:var(--surface);
  position:sticky;bottom:0;}
.vb-drawer-actions .btn-block{width:100%;justify-content:center;}
.vb-drawer-contact{display:flex;gap:8px;}
.vb-drawer-contact a{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:10px;min-height:44px;
  border:1px solid var(--border);border-radius:12px;color:var(--text);font-size:.8rem;font-weight:700;text-decoration:none;}
.vb-drawer-contact a .ic{color:var(--blue);}
.vb-drawer-lang{display:flex;gap:7px;flex-wrap:wrap;}
.vb-drawer-lang a{padding:6px 13px;border:1px solid var(--border);border-radius:999px;font-size:.8rem;
  font-weight:600;color:var(--muted);text-decoration:none;}
.vb-drawer-lang a.on{background:var(--blue);color:#fff;border-color:var(--blue);}
body:has(.vb-drawer.is-open){overflow:hidden;}
@media(prefers-reduced-motion:reduce){ .vb-drawer-panel,.vb-drawer-scrim{transition:none;} }

/* ---------- Buttons ---------- */
/* THIS is where "big, balloon, bulk, ugly" actually came from.
   The FABs only made it visible: every button on the site was one size, fully
   round at --radius-pill (999px), filled with a 135deg gradient and wearing a
   26px coloured glow. A 999px radius on anything wider than it is tall reads as
   a lozenge, the glow reads as inflation, and with no size scale every action
   shouted at the same volume — the cookie banner's "Accept all" carried the
   identical weight as the flagship "Find my matches".
   12px radius, a flat fill, and an inset highlight instead of a drop glow. The
   label stays dark ink: white on this orange measures 2.11-2.79:1, and --text
   is ~7:1 on the same fill, so repainting the brand was never the cheaper fix.
   999px is kept for chips and filter pills, where a lozenge is correct. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;font-weight:700;font-size:1rem;
  min-height:44px;padding:.6rem 1.25rem;border-radius:12px;border:0;cursor:pointer;transition:.16s;text-align:center;}
/* A real size scale, so a page can rank its actions instead of shouting them
   all at once. One primary per viewport is the rule these enable. */
.btn-sm{min-height:44px;  /* was 36 — unused, and would have broken the 44px policy on first use */ padding:.35rem .85rem;font-size:.875rem;border-radius:12px;}
/* Deliberate: .btn-lg is the large CTA size; it pairs with min-height:52px. */
.btn-lg{min-height:52px;padding:.8rem 1.7rem;font-size:1.06rem;border-radius:16px;}
/* The label is a FIXED ink, not var(--text).
   --text is a themeable token: .section-navy re-points it to white for its dark
   subtree, and because this button asked for var(--text) it inherited that — so
   a primary CTA dropped into a navy band rendered white on orange at 2.79:1,
   the precise failure the comment above exists to prevent. The button's ground
   is always --orange regardless of the section it sits in, so its ink must be
   constant too. #16202e is --text's light-mode value, ~7:1 on this orange. */
.btn-primary{background:var(--orange);color:#16202e;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 1px 2px rgba(16,40,70,.16);}
.btn-primary:hover{background:#d17a26;color:#16202e;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 2px 6px rgba(16,40,70,.2);}
.btn-blue{background:var(--blue);color:#fff;}
.btn-blue:hover{background:var(--blue-600);color:#fff;}
.btn-ghost{background:var(--surface);color:var(--blue);border:1.5px solid var(--border-strong);}
.btn-ghost:hover{border-color:var(--blue);}
.btn-block{display:flex;width:100%;}
/* A button with an author background-color is not restyled by the browser when it goes
   disabled, so .btn-primary kept --orange and its inset highlight verbatim while inert.
   37 controls carry wire:loading.attr="disabled"; nine had no other signal, including both
   /get-started CTAs — a student tapped "Continue" and nothing at all changed. */
.btn:disabled,.btn[disabled]{opacity:.55;cursor:not-allowed;box-shadow:none;}

/* ---------- Chips / badges ---------- */
.chip{display:inline-flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:700;
  padding:6px 12px;border-radius:var(--radius-pill);box-shadow:0 0 0 1px rgba(16,40,70,.07);}
.chip-ice{background:var(--blue-100);color:var(--blue);}
.chip-trust{background:#E4F1EB;color:var(--ok);}
/* Only legible on a dark ground. Scoped to it — on the white scholarship cards
   this rendered white text on white at 1:1 contrast, i.e. invisible. */
.hero .chip-line,.uni-hero .chip-line,.foot .chip-line{background:transparent;border:1px solid rgba(255,255,255,.4);color:#fff;}
.chip-line{background:var(--surface-2);border:1px solid var(--border-strong);color:var(--text);}
a.chip{min-height:40px;}

/* ---------- Cards ---------- */
/* --card-bg is an OPTIONAL tint, and the fallback is why this is safe.
   The site was white cards on a near-white ground: --surface #fff against --bg
   #f4f7fb is 1.07:1, so a card was separated from the page by its border alone
   and a grid of them read as flat. The 2026-08 design audit said the same thing
   from the other side — the palette is correct in the tokens but renders
   blue-monotone, with ice/peach/navy barely used as grounds.
   Written as a fallback rather than an override on purpose: a card that sets its
   own background (.tool-c1's solid blue, the glass cards in .section-navy) still
   wins normally, because this rule only supplies a default when nothing else
   painted one. Doing it as `.grid > .card:nth-child(...)` instead would be
   (0,3,0) and would have silently repainted every one of those. */
.card{background:var(--card-bg, var(--surface));border:1px solid var(--tint-border, var(--border));border-radius:var(--radius);box-shadow:var(--shadow);}
/* 18px was under-padded by about half against any current reference product;
   22px on a phone and 26px from 640px gives a card real internal air. */
/* The four-pastel tint cycle that lived here (.grid > *:nth-child(4n+…) painting
   --tint-blue/peach/teal/ocean by POSITION) is gone, on the owner's verdict: the
   coloured blocks read as a website, not an app, and a colour chosen by position
   said nothing about the card it painted. Cards are white again, and colour moved
   to where it carries meaning — the icon tile (see "App card system" below). The
   --tint-* tokens stay defined for the few components that name one directly. */
.card-p{padding:22px;}
@media(min-width:640px){ .card-p{padding:26px;} }

/* 2026-09-12 prune: 54 classes lost their markup when every card grid moved to the app
   card system below — the old tinted university card (.uni-card, .uni-top, .uni-meta), the
   four solid tool-card fills (.tool-c1-4), the scholarship card (.schol*), the bespoke
   exam, invoice, appointment, notification, deadline and score cards, and the pastel
   .job-global figure. Their rules went with them.
   The first pruner walked the file with strpos('}') to find each rule's end, which reads
   the first brace INSIDE an @media block as the end of the block: every rule after it was
   re-emitted a level deeper and silently stopped applying (.trust-badge lost its pill).
   The replacement matches braces and recurses into at-rules, and the nesting depth of a
   known top-level rule is checked afterwards. If this is ever run again, check it. */

/* ================= App card system (2026-09-11) =================
   The owner's brief: no pastel blocks, white cards like a phone app, "more
   colours than blue and orange", more engaging. So colour comes OFF the card and
   goes INTO the icon — the iOS Settings pattern: a white row, a rounded-square
   icon in a saturated hue with a white glyph, a title, a line of meta, a chevron.
   Pick the hue by the KIND of thing (money green, tests violet, visas teal…),
   never by position, so colour means something.

   Every hue is measured with its white glyph at the LIGHT end of the gradient,
   where contrast is worst; icons are graphics, so the bar is 3:1:
   blue #3B82F6 3.65 · violet #8B5CF6 3.54 · green #16A34A 3.30 · teal #0D9488
   3.74 · orange #EA580C 3.55 · rose #F43F5E 3.64 · amber #D97706 3.19 · sky
   #0284C7 3.44 · slate #64748B 4.76. The brighter "brand" versions of several of
   these (#22C55E, #14B8A6, #FB923C, #38BDF8) fall to 2.3-2.8 and were rejected. */
:root{
  --hue-blue:linear-gradient(135deg,#3B82F6,#1D4ED8);
  --hue-violet:linear-gradient(135deg,#8B5CF6,#6D28D9);
  --hue-green:linear-gradient(135deg,#16A34A,#15803D);
  --hue-teal:linear-gradient(135deg,#0D9488,#0F766E);
  --hue-orange:linear-gradient(135deg,#EA580C,#C2410C);
  --hue-rose:linear-gradient(135deg,#F43F5E,#BE123C);
  --hue-amber:linear-gradient(135deg,#D97706,#B45309);
  --hue-sky:linear-gradient(135deg,#0284C7,#0369A1);
  --hue-slate:linear-gradient(135deg,#64748B,#334155);
}
/* Icon tile. Default blue; add one hue class. Sizes: .sm 36, default 44, .lg 52. */
.app-ic{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;flex:none;color:#fff;
  background:var(--hue-blue);box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 4px 10px rgba(16,40,70,.14);}
.app-ic svg{width:50%;height:50%;}
.app-ic.sm{width:36px;height:36px;border-radius:11px;}
.app-ic.lg{width:52px;height:52px;border-radius:15px;}
.app-ic.i-blue{background:var(--hue-blue);}
.app-ic.i-violet{background:var(--hue-violet);}
.app-ic.i-green{background:var(--hue-green);}
.app-ic.i-teal{background:var(--hue-teal);}
.app-ic.i-orange{background:var(--hue-orange);}
.app-ic.i-rose{background:var(--hue-rose);}
.app-ic.i-amber{background:var(--hue-amber);}
.app-ic.i-sky{background:var(--hue-sky);}
.app-ic.i-slate{background:var(--hue-slate);}

/* ---------- Card colour (2026-09-15) ----------
   The owner, on the tools page: "looks plain white cards, make cards coloured".
   White cards on a near-white ground (--surface #fff on --bg #f4f7fb, 1.07:1) are
   separated by their border alone, and a grid of them reads flat.

   This is NOT the four-pastel cycle removed above. That one painted a card by its
   POSITION (nth-child), so the colour said nothing about the card and the grid read
   as a website. Here a card is washed with the colour ITS OWN TILE carries, so the
   ground means exactly what the icon beside it means — loans green wherever a loan
   card appears, AI violet wherever the AI tools do.

   Set through --card-bg rather than background, for the reason .card's own note
   gives: a card that paints its own surface (.tool-c1's solid blue, the glass cards
   in .section-navy) must keep winning, and it does, because this only supplies the
   variable that rule already falls back on. :has() reads the tile without touching a
   hundred templates; a browser without :has() keeps white cards, which is the state
   this file shipped yesterday.

   The classes are .tone-*, not .tint-*: --tint-blue/peach/teal/ocean are the older
   pastel TOKENS that the removed position cycle painted with, and they still exist
   for the components that name one directly. Two different things should not share
   a name in a file this size.

   Grounds are opaque hex — 13% of the hue flattened on #fff, borders 30% — per the
   chip rule further down: a translucent fill renders differently on every ground and
   that bug has been fixed in this file twice. Measured on each of the nine: --text
   #16202e ≥13.84:1, --muted #5b6b7e ≥4.60:1 (AA holds on every one). The pill fill #F1F5F9 measures 1.1:1
   against these grounds — it vanishes — so a toned card hands its pills white through
   --pill-bg (#334155 on #fff, 9.4:1). */
.tone-blue,.card:has(> .app-ic.i-blue){--card-bg:#E6EFFE;--tint-border:#C4DAFC;--pill-bg:#FFFFFF;}
.tone-violet,.card:has(> .app-ic.i-violet){--card-bg:#F0EAFE;--tint-border:#DCCEFC;--pill-bg:#FFFFFF;}
.tone-green,.card:has(> .app-ic.i-green){--card-bg:#E1F3E7;--tint-border:#B9E3C9;--pill-bg:#FFFFFF;}
.tone-teal,.card:has(> .app-ic.i-teal){--card-bg:#E0F1F0;--tint-border:#B6DFDB;--pill-bg:#FFFFFF;}
.tone-orange,.card:has(> .app-ic.i-orange){--card-bg:#FCE9DF;--tint-border:#F9CDB6;--pill-bg:#FFFFFF;}
.tone-rose,.card:has(> .app-ic.i-rose){--card-bg:#FEE6EA;--tint-border:#FCC5CF;--pill-bg:#FFFFFF;}
.tone-amber,.card:has(> .app-ic.i-amber){--card-bg:#FAEDDF;--tint-border:#F4D6B4;--pill-bg:#FFFFFF;}
.tone-sky,.card:has(> .app-ic.i-sky){--card-bg:#DEEFF8;--tint-border:#B3DAEE;--pill-bg:#FFFFFF;}
.tone-slate,.card:has(> .app-ic.i-slate){--card-bg:#EBEDF0;--tint-border:#D1D5DC;--pill-bg:#FFFFFF;}
/* The default tile is blue without a hue class, so an untinted .app-ic would leave a
   blue icon on a white card beside eight coloured neighbours. */
.card:has(> .app-ic):not(:has(> .app-ic[class*="i-"])){--card-bg:#E6EFFE;--tint-border:#C4DAFC;--pill-bg:#FFFFFF;}
/* Same guard, and the same reason, as the tint cycle's: this subtree re-points --text
   to white, so a light tint here would be near-white text on a pale ground. Written at
   (0,4,0) because :has() takes the specificity of its argument and would otherwise
   outrank the (0,2,0) rule that handles the cycle. */
.section-navy .card:has(> .app-ic){--card-bg:var(--surface);--tint-border:rgba(255,255,255,.16);--pill-bg:#F1F5F9;}

/* App row: the default shape for a card you tap to go somewhere.
   <a class="card app-row"> <span class="app-ic i-…">icon</span>
     <span class="app-body"><span class="app-title">…</span><span class="app-sub">…</span>
       <span class="app-pills"><span class="app-pill">…</span></span></span>
     <span class="app-chev">chevron</span></a> */
.app-row{display:flex;align-items:center;gap:14px;padding:16px;min-height:76px;color:var(--text);text-decoration:none;}
.app-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px;}
.app-title{font-family:var(--font-display);font-weight:800;font-size:1rem;line-height:1.25;letter-spacing:-.01em;color:var(--text);margin:0;}
.app-sub{font-size:var(--fs-sm);line-height:1.45;color:var(--muted);margin:0;}
.app-pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px;}
.app-pill{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-xs);font-weight:700;line-height:1.5;
  color:#334155;background:var(--pill-bg,#F1F5F9);border-radius:999px;padding:2px 9px;white-space:nowrap;}
.app-pill .ic{width:12px;height:12px;}
/* Decorative: the whole row is the link, so the chevron carries no meaning of its
   own and needs no contrast ratio — it is an affordance, like iOS's. */
.app-chev{flex:none;display:grid;place-items:center;color:#94A3B8;transition:transform .16s ease,color .16s ease;}
a.app-row:hover .app-chev{transform:translateX(3px);color:var(--blue);}

/* App tile: the vertical shape, for a grid of features or tools where a card holds
   more than a row can — icon top-left, title, a line, and a foot that pins to the
   bottom so a row of tiles lines up whatever the copy length. */
.app-tile{display:flex;flex-direction:column;align-items:flex-start;gap:10px;padding:18px;color:var(--text);text-decoration:none;}
.app-tile .app-title{font-size:1.06rem;}
.app-tile-foot{margin-top:auto;padding-top:6px;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;
  font-size:var(--fs-sm);font-weight:700;color:var(--blue);}

/* App info card: a fact with a coloured label — for "Key facts", stats with words,
   anything read rather than tapped. */
.app-info{display:flex;gap:12px;align-items:flex-start;padding:16px;}
.app-info .app-label{font-size:var(--fs-xs);font-weight:800;text-transform:uppercase;letter-spacing:var(--track-label);color:var(--muted);margin:0;}
.app-info .app-value{font-weight:600;line-height:1.45;margin:2px 0 0;}

/* Pictures, the owner's other ask: "colours, logos, images everywhere". A real image
   beats an icon whenever the thing HAS one — a campus photo, a country, a lender's
   logo, an exam's brand tile — so these are the image counterparts of .app-ic, in
   the same footprints so a row can mix them.
   .app-thumb  a 56px rounded photo (object-fit:cover) in a row's icon slot
   .app-thumb.logo  a brand mark, contained on white with a hairline — logos are
               not photographs and must never be cropped
   .app-flag   a country flag (x-flag) seated in a 44px tile */
.app-thumb{width:56px;height:56px;border-radius:14px;flex:none;object-fit:cover;display:block;background:var(--blue-100);
  box-shadow:0 4px 10px rgba(16,40,70,.14);}
.app-thumb.sm{width:44px;height:44px;border-radius:12px;}
.app-thumb.logo{object-fit:contain;background:#fff;padding:6px;border:1px solid var(--border);box-shadow:0 2px 6px rgba(16,40,70,.08);}
.app-flag{width:44px;height:44px;border-radius:13px;flex:none;display:grid;place-items:center;background:#fff;border:1px solid var(--border);
  box-shadow:0 2px 6px rgba(16,40,70,.08);}
.app-flag svg{width:28px;height:19px;border-radius:3px;box-shadow:0 0 0 1px rgba(16,40,70,.12);}
/* A photo band across the top of an .app-tile — for destinations, guides, hubs.
   <a class="card app-tile has-media"><span class="app-tile-media"><img …></span> … */
.app-tile.has-media{overflow:hidden;padding-top:0;}
.app-tile-media{display:block;position:relative;width:calc(100% + 36px);margin:0 -18px 6px;height:132px;overflow:hidden;background:var(--blue-100);}
.app-tile-media img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .5s ease;}
@media (hover:hover) and (pointer:fine){ a.app-tile.has-media:hover .app-tile-media img{transform:scale(1.04);} }
/* An icon or flag riding on the photo's bottom edge, like the university logo does. */
.app-tile.has-media .app-tile-media + .app-ic,.app-tile.has-media .app-tile-media + .app-flag{margin-top:-34px;position:relative;z-index:1;
  box-shadow:0 6px 16px rgba(16,40,70,.22);}

/* Card header: an icon tile beside the card's heading, for any content card that is
   read rather than tapped (an overview, a form, a checklist, an FAQ group). The rule,
   from the owner: no card on this site is a plain white box.
   <div class="card card-p"><div class="card-head"><span class="app-ic i-…">…</span>
     <div><h2 class="card-head-title">…</h2><p class="card-head-sub">…</p></div></div> … </div> */
.card-head{display:flex;align-items:center;gap:12px;margin:0 0 16px;}
.card-head > div{min-width:0;}
.card-head-title{margin:0;font-family:var(--font-display);font-weight:800;font-size:var(--fs-h3);line-height:1.25;letter-spacing:-.01em;}
h2.card-head-title{font-size:clamp(1.15rem,2.6vw,1.35rem);}
.card-head-sub{margin:2px 0 0;font-size:var(--fs-sm);color:var(--muted);}

/* A tappable card lifts under a real pointer, like every other card family here. */
@media (hover:hover) and (pointer:fine){
  a.app-row:hover,a.app-tile:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg);}
}
a.app-row,a.app-tile{transition:transform .16s ease,box-shadow .16s ease;}
.grid{display:grid;gap:14px;}
/* g2 was two columns at EVERY width. A `1fr` track is really minmax(auto,1fr),
   so any child wider than its share pushes the track out — on /loans the lender
   cards forced 221px columns inside a 339px grid, putting 6 of 12 lenders past
   the right edge of a 375px screen. html/body carry overflow-x:clip, so there
   was no scrollbar to reach them: half the comparison simply did not exist on a
   phone. One column on narrow screens, and minmax(0,1fr) everywhere so a wide
   child can never blow a track out again. */
.g2{grid-template-columns:1fr;}
.g3{grid-template-columns:1fr;}
.g4{grid-template-columns:repeat(2,minmax(0,1fr));}
@media(min-width:560px){ .g2{grid-template-columns:repeat(2,minmax(0,1fr));} }
/* The university results list, scoped away from .g2 on purpose: .g2 is used by
   48 views and stops at two columns with no further breakpoint, which is right
   for a pair of side-by-side panels and wrong for a catalogue. /universities was
   the one page where that cost something real — measured at 1280px it rendered
   two 455px cards in a 924px grid, i.e. the mobile layout stretched onto a
   desktop, and the page had been capped to a 960px container to stop those cards
   growing to ~560px of mostly empty row. That cap treated the symptom.
   Three columns instead, so the cards can keep a card shape AND the page can use
   its full measure: at the 1180px container that is ~368px per card, close to the
   455px the two-column layout gave them, with half again as many on screen.
   Stops at three deliberately — a fourth column inside --maxw computes to 271px,
   which is narrower than the 110px campus photo, logo plate, name, city and rank
   chips sit comfortably in. */
.uni-grid{display:grid;gap:14px;grid-template-columns:1fr;}
@media(min-width:560px){ .uni-grid{grid-template-columns:repeat(2,minmax(0,1fr));} }
@media(min-width:1000px){ .uni-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;} }
@media(min-width:640px){ .g3{grid-template-columns:repeat(3,minmax(0,1fr));} .g4{grid-template-columns:repeat(4,minmax(0,1fr));} }

/* ---------- Hero (AI Course Finder — Spec §3) ---------- */
.hero{background:radial-gradient(130% 120% at 85% 0%,#2f63b8 0%,var(--blue) 52%,var(--ocean) 100%);color:#fff;position:relative;overflow:hidden;}
.hero::after{content:"";position:absolute;right:-90px;top:-90px;width:320px;height:320px;border-radius:50%;
  background:radial-gradient(circle,rgba(224,132,49,.4),transparent 68%);}
.hero .container{position:relative;z-index:2;padding-top:clamp(1.8rem,5vw,3.2rem);padding-bottom:clamp(2rem,6vw,3.6rem);}
.hero .eyebrow{color:var(--summer-ink);}
.hero h1{color:#fff;max-width:16ch;}
.hero h1 .accent{color:var(--summer-ink);}
/* Deliberate: the hero subtitle is the one place a step above body reads as
   intent rather than noise. MIRRORED in the critical block — both copies must
   move together if this ever changes. */
.hero .sub{color:rgba(255,255,255,.86);font-size:1.06rem;max-width:46ch;margin:.6rem 0 1.3rem;}
.hero .hero-cta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;}
.hero .proof{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:1.2rem;color:rgba(255,255,255,.82);font-size:.875rem;font-weight:600;}
.hero .proof b{color:#fff;}

/* Hero photograph.

   This used to be display:none below 900px "to protect mobile LCP", which meant
   the majority-traffic device saw no photograph above the fold on any .hero
   page — the client's headline requirement switched off on the device that
   matters most. The caution was misplaced: the mobile file is 940x427 at 49KB
   (50,074 bytes on disk), which is smaller than several icons on the page. The
   desktop file is 1600x727 at 108KB. These figures are also stated in
   home.blade.php's preload comment — keep the two in step, or re-measure.

   ONE composition at every width: a full-bleed photograph under a directional
   blue scrim. It used to be two — a flat gradient panel on the left and the
   photo confined to the right 46%, feathered with a mask that did not do its
   job, leaving a visible vertical seam around 1030px. It read as two panels
   bolted together rather than one hero, which is exactly what the split
   invites. The phone already worked this way; desktop now matches it.

   The scrim is DIRECTIONAL, not flat: heavy where the copy sits and thinning
   across to the right so the photograph is actually visible. A single even
   overlay would either drown the picture or leave the headline sitting on it —
   the reason the split existed in the first place was to guarantee clean text,
   and that guarantee has to be kept by the gradient instead. */
.hero.has-media{position:relative;
  background-image:linear-gradient(165deg,rgba(4,26,52,.90) 0%,rgba(4,26,52,.74) 45%,rgba(0,43,76,.92) 100%),url('/images/hero/graduation-mobile.webp');
  background-size:cover;background-position:center;}
/* The right-hand panel element is retained in the markup but never painted —
   the photograph is the hero's own background now. */
.hero .hero-media{display:none;}
@media(min-width:900px){
  /* Both layers RESTATED, never merely un-set: .hero declares its gradient via
     the `background` shorthand, so the gradient IS the background-image.
     Writing background-image:none here once deleted the gradient along with the
     mobile photo and left the panel transparent — white headline on a white
     page. Dropping one layer of a shorthand means re-declaring the one kept. */
  .hero.has-media{
    /* The stops are anchored to the COPY COLUMN, not to a share of the viewport.
       The old five-stop ramp ran .95/.90 across the left third and thinned to .46
       by 72%, which got it wrong in both directions at once. Measured against the
       real photo pixels: only 0.07 of the photograph's contrast range survived at
       x=8%, so the copy sat on a navy rectangle and the picture read as a second
       panel bolted on at the right. Meanwhile the 17px .sub runs the full 600px of
       the column, so on a 905px-wide hero its lines END in the thin part of the
       ramp over bright sky and measured 3.27:1 — below AA. Too heavy where nothing
       is written, too light where something is.

       HOW THE STOPS RESOLVE — read this before touching the numbers. A percentage
       in a gradient colour stop is a share of the GRADIENT LINE, not of the element
       width. For 100deg on a W x H hero the line length is
         L = 0.98481*W + 0.17365*H
       and a stop at distance d crosses the hero's VERTICAL MID-HEIGHT at
         x_mid = W/2 + (d - L/2) / sin(100deg).
       (1) On the `50% + 60px` branch L cancels and x_mid = W/2 + 60.9px at any
           height. That is arithmetic luck, not the --maxw/600px cap doing work.
       (2) The `680px` branch is a flat absolute and DOES move with hero height. It
           runs from 900px up to ~1167px viewport (crossover 0.5L + 60 = 680, i.e.
           L = 1240). /about's shorter hero shifts the release ~16px versus home at
           the same 920px width.
       (3) The isochrome is tilted 10deg off vertical: dx/dy = -tan(10deg) = -0.176,
           so over a 560px hero the release point is ~49px further RIGHT at the top
           and ~49px further LEFT at the bottom than x_mid. The lowest and dimmest
           line in the hero is .hero .proof, so IT is the worst-case candidate, not
           the .sub. Any re-measurement must include it and the .eyebrow.
       The 600px copy cap comes from `.hero.has-media .container > *` winning on
       specificity (0,3,0) over `.hero .sub{max-width:46ch}` (0,2,0). If either the
       cap, --maxw, or that specificity changes, re-derive both constants with the
       formula above — do not nudge them.

       Measured worst text pixel / photo retained at x=8%, compositing the real
       image + this gradient + .hero::after, sampled at the BOTTOM of each line box
       across h1/.sub/.proof/.eyebrow:
       920px 3.27:1 -> 5.60:1, 0.07 -> 0.28;  1265px 5.57 -> 5.65, 0.07 -> 0.28;
       1440px 5.60 -> 5.60;  1920px 5.67 -> 5.55;  /about 5.50 -> 5.67.
       Four times more photograph and the AA failure at 920px is gone. */
    background-image:
      linear-gradient(100deg,
        rgba(4,26,52,.72) 0,
        rgba(4,26,52,.72) max(680px, 50% + 60px),
        rgba(8,48,86,.40) max(980px, 50% + 360px),
        rgba(12,66,116,.24) 100%),
      url('/images/hero/graduation.webp');
    background-size:cover;
    background-position:center, 72% 30%;
  }
  .hero.has-media .container{max-width:var(--maxw);}
  /* The copy keeps its own column so it never runs across the subject of the
     photograph, even though the image now spans the full width. */
  .hero.has-media .container > *{max-width:min(100%,600px);}
}

/* ---------- Tool cards ----------
   `align-self:start` used to live here, added to kill ~40px of empty card under
   the last line. It worked on one card and broke every row: only some tools
   carry a live-count line, so unstretched cards size to their own content and
   /tools rendered rows of 160/196/216 — a 56px stagger on all seven rows.
   The empty space was never the real problem, dangling space was. Cards stretch
   again, and `.go` already carries margin-top:auto, so the slack collects
   ABOVE the link and every CTA in a row lands on the same line. */
.tool{display:flex;flex-direction:column;gap:7px;padding:20px;text-align:left;color:var(--text);}

/* ---------- Coloured tool cards ----------
   Four white boxes in a row read as inert — nothing to look at and nothing to
   separate one tool from the next. Each card now takes one of the four brand
   colours, and the INK is chosen per background rather than applied uniformly,
   because white is only legible on two of them: measured, white on --blue is
   8.01:1 and on --ocean 14.48:1, but only 2.79:1 on --orange and 1.27:1 on
   --ice, where the dark ink reads 5.87:1 and 12.94:1 instead. A single text
   colour across the set would have failed on half of it.

   This block MUST stay BELOW the base .tool rules that follow. `.tool-c p` and
   `.tool p` have the SAME specificity (0,1,1), as do `.tool-c .go` and
   `.tool .go` (0,2,0) — so source order alone decides, and sitting above them
   silently lost every colour to var(--muted)/var(--blue). Measured before the
   move: the "Find funding →" link on the blue card was 1.00:1, i.e. painted in
   its own background colour. Do not hoist this block. */
.tool .ti{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;font-size:1.25rem;color:#fff;}
.tool .t-blue{background:linear-gradient(135deg,#2f63b8,var(--blue));}
.tool .t-orange{background:linear-gradient(135deg,#ff9a3d,var(--orange));}
.tool .t-ocean{background:linear-gradient(135deg,#0a4f8c,var(--ocean));}
.tool .t-teal{background:linear-gradient(135deg,#1aa3a3,#0c6e78);}
.tool h3{font-size:var(--fs-h3);margin:.2rem 0 0;}
.tool p{font-size:.8rem;color:var(--muted);margin:0;}
.tool .go{margin-top:auto;color:var(--blue);font-weight:700;font-size:.8rem;}   /* test scores */

/* ---------- Stat tiles ---------- */
/* min-height because the label under the number wraps to two lines on some
   tiles and not others: the nine tiles on /university/* measured 92px in the
   first row and 112px in the second, so two rows of the identical component
   sat 20px apart. Centring the content means the numbers stay on one optical
   line whether the label wraps or not. */
.stat{padding:16px;text-align:center;min-height:112px;display:flex;flex-direction:column;justify-content:center;}
.stat .n{font-size:clamp(1.5rem,4vw,2rem);font-weight:800;color:var(--blue);line-height:1;letter-spacing:-.02em;}
/* Repointing .accent at the ink token did not reach here: this selector is
   three classes deep and won the cascade, so every accented stat number stayed
   at 2.79:1 — under even the 3:1 bar large text gets. */
.stat .n .accent{color:var(--orange-600);}
.stat .l{font-size:.8rem;color:var(--muted);margin-top:6px;font-weight:600;}

/* ---------- Destinations ---------- */
.dest-row{display:flex;gap:10px;overflow-x:auto;padding:4px 2px 8px;scrollbar-width:none;}
.dest-row::-webkit-scrollbar{display:none;}
.dest{flex:0 0 auto;width:120px;border-radius:var(--radius);overflow:hidden;position:relative;color:#fff;
  height:150px;background:var(--blue);display:flex;align-items:flex-end;padding:12px;font-weight:800;font-size:var(--fs-sm);
  box-shadow:var(--shadow);}
.dest::before{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,20,50,.05),rgba(0,20,50,.7));}
.dest span{position:relative;z-index:2;}

/* ---------- Section head ---------- */
.sec-head{display:flex;align-items:end;justify-content:space-between;gap:12px;margin-bottom:14px;}
.sec-head .link{font-size:.875rem;font-weight:700;color:var(--blue);white-space:nowrap;}

/* ---------- Footer ---------- */
.foot{background:var(--ocean);color:rgba(255,255,255,.8);padding:2.4rem 0 2rem;margin-top:1rem;}
/* .muted means "de-emphasised on the CURRENT surface", but its token is tuned
   for light surfaces — so the footer tagline shipped #5b6b7e on --ocean, 2.65:1,
   on every page of the site. Invert it once here rather than at each call site. */
.foot .muted{color:rgba(255,255,255,.82);}
.foot a{color:rgba(255,255,255,.8);}
.foot .brand{color:#fff;margin-bottom:.6rem;}
.foot small{color:rgba(255,255,255,.55);}

/* ---------- No floating contact hub ----------
   The floating contact hub was styled here. It is deleted rather than tuned again.

   A fixed element may only exist if it CANNOT be over a control: it spans the
   full width and the layout reserves its height (.tabbar via body's
   padding-bottom, .sticky-actions via body:has(...) main/footer), or the reader
   opened it (.vb-drawer, .modal-overlay, .chatbot-panel, .skip-link). The
   allowlist and the reason for each entry live in
   tests/Feature/PageStructureTest.php, which fails on any new position:fixed
   selector that is not argued for there.

   The alternative — reserving a 68px right lane so the hub had somewhere legal
   to sit — was measured at 390x844 and rejected: the content column goes 354px
   to 286px with an 18px/86px asymmetry, .card loses 19.2%, .field 21.8%,
   .btn-block 22.1%, and /loans grows from 43,881px to 53,078px of scrolling
   (+21.0%). 68px of the horizontal axis is 17.4% of a 390px screen and the page
   cannot scroll sideways to get it back; the same 68px reserved vertically by
   .sticky-actions costs 8.1% of an 844px screen and the page scrolls. Reserve
   vertically, never horizontally, and if neither is acceptable the element does
   not float. */

/* ---------- AI Course Finder component ---------- */
.acf .lbl{font-size:.8rem;font-weight:700;color:var(--muted);display:block;margin-bottom:5px;}
.acf.is-busy{opacity:.92;}
/* Every filter and search on this site was a silent server round-trip:
   .is-busy was hooked to six surfaces and styled on exactly one of them, so the
   grid simply sat there while Livewire fetched. An absent state is felt as the
   site being broken, not as the site being unpolished.

   :not(.acf) is load-bearing, do not "tidy" it away. The AI Course Finder puts
   wire:loading.class on its ROOT, and its real feedback (.gen-loading, button
   label swaps) lives inside that root — pointer-events:none there would freeze
   the wizard mid-generation and disable the reader's escape route. Its .92 dim
   above is deliberately gentler for the same reason.

   A dim, not a skeleton: swapping the grid for placeholders collapses the page
   height and yanks the scroll on every keystroke of a 300ms-debounced search.
   The reduced-motion catch-all further down neutralises the spinner. */
.is-busy:not(.acf){opacity:.6;pointer-events:none;transition:opacity .12s ease;}
/* Promoted out of an inline <style> in search.blade.php, where it worked but
   only /search could use it. The result-count line on every explorer is an
   aria-live region already, so the spinner rides along with the announcement
   instead of being a second, separate signal. */
/* Track is --border, NOT --border-strong. A spinner ring is not a control boundary — the
   token's only job here is to be the quiet part the moving head is read against, so the
   1.4.11 re-point above is actively wrong for it. Head-vs-track on --blue: --border 6.61:1,
   --border-strong 2.09:1 (it was 5.60:1 at the old #cdd9e8). At 2.09 the rotating segment
   disappears into the ring and the only "search is running" signal on the three explorers
   reads as a static grey circle. Same reason at .acf .spinner and .vb-ptr-spin. */
.busy-spin{width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--blue);
  border-radius:50%;display:inline-block;vertical-align:-3px;animation:busyspin .6s linear infinite;}
@keyframes busyspin{to{transform:rotate(360deg)}}
.acf .rescard{border:1px solid var(--border);border-radius:12px;padding:12px 14px;background:var(--surface-2);}
.acf .notice{background:#fdeee2;color:var(--orange-600);border:1px solid #f6d6b8;border-radius:12px;padding:10px 12px;font-size:.875rem;font-weight:600;}
.acf .doc{white-space:pre-wrap;background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:16px;font-size:var(--fs-sm);line-height:1.7;max-height:440px;overflow:auto;}
.acf textarea.field{resize:vertical;line-height:1.5;}
.acf .doctabs{display:flex;gap:8px;border-bottom:1px solid var(--border);padding-bottom:2px;}
.acf .doctab{flex:1;background:none;border:0;border-bottom:3px solid transparent;padding:10px 6px;font:inherit;
  font-weight:700;font-size:var(--fs-sm);color:var(--muted);cursor:pointer;transition:.15s;}
.acf .doctab.on{color:var(--blue);border-bottom-color:var(--orange);}
.acf .fmt{background:var(--surface-2);border:1.5px solid var(--border-strong);border-radius:var(--radius-pill);
  padding:.4rem .9rem;font:inherit;font-size:.875rem;font-weight:700;color:var(--text);cursor:pointer;}
.acf .fmt.on{background:var(--blue);color:#fff;border-color:var(--blue);}
.acf .gen-loading{display:none;align-items:center;gap:10px;justify-content:center;padding:2.4rem 1rem;color:var(--muted);font-weight:600;}
.acf .spinner{width:20px;height:20px;border:3px solid var(--border);border-top-color:var(--blue);border-radius:50%;animation:spin .7s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
.uni-place{display:flex;align-items:center;gap:5px;font-size:.875rem;color:var(--muted);margin:5px 0 0;}
/* A second `.uni-card h3` used to sit here at font-size:1rem. Same specificity,
   three lines later, so it won — and set the university name to exactly the
   body size, which is why the listing had no visible hierarchy despite the
   note above saying the name is what gets the size. Removed rather than
   re-tuned; there is only one rule for this element now. */
/* Not text: a 44px monogram tile, sized to fill the square, not to read as copy. */
.uni-mono{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;font-weight:800;font-size:1.06rem;
  color:#fff;background:linear-gradient(140deg,#2f63b8,var(--blue));flex:none;}
.uni-mono.lg{width:64px;height:64px;font-size:1.5rem;border-radius:16px;}
/* Real brand logos (universities / exams / marquee) with monogram fallback */
/* LANDSCAPE chip, not a square. A university wordmark is roughly 5:1 — forcing
   it into a 44x44 box capped at 82% meant the image could only ever be 36px
   WIDE, so its height collapsed to whatever the aspect ratio allowed: measured
   on the listing, Imperial rendered 36x4px, ETH Zurich 36x6, Cambridge and
   Stanford 36x8, Oxford 36x11. Nine of eleven logos were under 12px tall inside
   a bordered, shadowed white box five to eleven times taller than its contents,
   which reads as an empty square with a smear in it.

   Every logo was present in the DOM and invisible on screen — the difference
   between shipping a feature and shipping the appearance of one. The chip now
   takes its width from the mark instead of the mark being crushed into the chip. */
/* A FIXED landscape chip. Both earlier attempts failed for the same reason and
   it is worth recording: an auto-width chip sized from the image, while the
   image was sized with max-width:100% of the chip, is circular and resolves to
   zero. Stating the height on the image instead did not help either, because
   several of these marks are SVGs with no intrinsic dimensions, so width:auto
   has no ratio to derive from and collapses to 0 again.
   Fixing BOTH axes on the chip means the image's max-width and max-height each
   resolve against a real number, and object-fit does the rest — which works
   for a 5:1 wordmark and a square crest alike. */
/* 116x36 is sized from the WIDEST real mark, not from a round number: Imperial
   College is 1165x128, a 9.1:1 lockup, and in a 92px chip it could only ever be
   8px tall. At 100px of usable width it clears 11px, which is the size that
   wordmark is drawn to survive. Squarer marks (MIT at 1.8:1) hit the height cap
   first and sit at 24px, so both extremes land legible. */
.uni-logo{width:116px;height:36px;border-radius:10px;
  display:grid;place-items:center;background:#fff;border:1px solid var(--line2);overflow:hidden;flex:none;}
.uni-logo img{max-width:100px;max-height:24px;width:auto;height:auto;object-fit:contain;}
.uni-logo.lg{width:150px;height:52px;border-radius:14px;}
.uni-logo.lg img{max-width:124px;max-height:34px;}
/* Test-prep: full-colour landscape cards in a horizontal slider */
/* The scrollbar is hidden here as it is on every other horizontal scroller in
   this file. A raw grey track under a row of brand-coloured tiles is browser
   chrome sitting in the middle of the page, and it was the one place the site
   showed it. */
.exam-slider{display:flex;gap:12px;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;padding:2px 2px 4px;scrollbar-width:none;}
.exam-slider::-webkit-scrollbar{display:none;}
/* The rail hides ~81% of its own content and suppresses its scrollbar, so with
   cards cut flush at the edge nothing signals there is more. A 24px fade is the
   affordance.
   :focus-WITHIN, not :focus-visible. :focus-visible matches only this element,
   and focus lands here once per rail but on a CHILD (every exam card is a native
   <a href>) every other time. With a child focused the mask stayed on, and
   scroll-into-view parks that card flush at the right padding edge — so its last
   22px, and the right side of the white 3px outline + 6px halo focus ring,
   dissolved from 92% to 8% alpha. :focus-within also fires on mouse focus; the
   fade lifting for a moment on click is much the cheaper trade. Do not
   "simplify" this to :focus-visible. */
.exam-slider:not(:focus-within){
  -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 24px),transparent);
          mask-image:linear-gradient(90deg,#000 calc(100% - 24px),transparent);}
/* ---------- Programme card ----------
   Photo, who is offering it, what it is, the two numbers a family decides on,
   one action. The old card led with two chips and buried the fee in a meta row;
   this leads with the institution, which is what a student recognises.

   Built on .card, so it inherits the --card-bg tint cycle. The inner surfaces
   are OPAQUE white rather than a translucent overlay: see the chip note above —
   a translucent panel composites against whichever of the four tints the card
   drew, and would land on a different colour in every column. White on all four
   tints is the same panel every time, and the tint still reads as the frame. */
.pcard{padding:0;overflow:hidden;display:flex;flex-direction:column;gap:0;text-decoration:none;color:var(--text);}
/* 2026-09-09, audit: this was background-image:var(--pc-img). A background image ignores
   loading="lazy", so /universities fetched all 22 campus photos in one burst at ~490ms —
   3.13 MB — when two cards are above the fold on a phone. Measured on staging: 25 of the 26
   image requests had initiatorType "css". Carrying the photo in a real <img> gets native
   lazy-loading and the browser's own priority heuristics for free. The band keeps its fixed
   132px height, so nothing shifts. */
.pcard-media{display:block;position:relative;overflow:hidden;height:132px;
  background-color:var(--blue-100);}
.pcard-media-img{width:100%;height:100%;object-fit:cover;display:block;}
.pcard-body{display:flex;flex-direction:column;gap:12px;padding:16px;flex:1;}
/* Identity row. 34px badge matches the university explorer's, so the same
   institution is the same size wherever a student meets it. */
.pcard-uni{display:flex;align-items:center;gap:10px;min-width:0;}
.pcard-uni-txt{display:flex;flex-direction:column;min-width:0;}
/* 600, not 700. The institution is context; the programme is the choice. The
   measured failure was a two-line 14.4px/700 block out-massing a one-line
   title, and mass is what the weight step attacks. */
.pcard-uni-name{font-weight:600;font-size:var(--fs-sm);line-height:1.25;
  overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}
.pcard-uni-meta{display:flex;align-items:center;gap:5px;font-size:var(--fs-xs);color:var(--muted);margin-top:2px;}
.pcard-flag{width:17px;height:12px;border-radius:2px;flex:none;box-shadow:0 0 0 1px rgba(16,40,70,.14);}
.pcard-dot{opacity:.55;}
/* The programme name is the thing being chosen, so it outranks the institution
   above it. Two lines then ellipsis — "Master of Engineering in Supply Chain
   Management (Residential)" is a real title in this list and ran to four. */
/* No font-size of its own. This is an <h3>, so it takes --fs-h3 (18px) from
   the heading rule and follows the scale instead of opting out one step below
   it at 1.06rem/16.96px. The card was collapsing five roles into a 4.16px band —
   the programme title ended up only 2.56px larger than the two-line university
   name above it, so the qualifier out-massed the thing it qualifies.
   font-weight is stated because the shared h1,h2,h3 rule now splits by level
   and h3 resolves to 600; a card title wants the heavier cut. */
.pcard-title{font-weight:800;line-height:1.25;margin:0;
  overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}
/* The two numbers, in their own panel. margin-top:auto pins it and the CTA to
   the bottom, so stats and buttons line up across a row whose titles wrap to
   different heights — the ragged bottom edge is what made the old grid look
   unfinished. */
.pcard-stats{margin-top:auto;display:flex;background:var(--surface);border:1px solid var(--border);
  border-radius:12px;overflow:hidden;}
.pcard-stat{flex:1;min-width:0;padding:9px 12px;display:flex;flex-direction:column;gap:1px;}
.pcard-stat + .pcard-stat{border-left:1px solid var(--border);}
/* The VALUE wraps; only the label below it truncates.
   nowrap+ellipsis here truncated university fee ranges to "USD89,692–USD96,8…",
   and a cut-off fee is worse than no fee — it reads as a number the reader can
   act on while hiding half of it. Browsers offer a line-break opportunity after
   the en-dash, so a range breaks between its two figures and both stay whole. */
.pcard-stat b{font-size:var(--fs-sm);font-weight:800;line-height:1.18;}
/* The fee's confidence tier rides in this label, never separated from the
   number it qualifies. */
.pcard-stat small{font-size:var(--fs-xs);color:var(--muted);line-height:1.2;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* Whichever of these two comes FIRST is what pins the block to the bottom.
   margin-top:auto lived only on .pcard-stats, so a card with no stats — no
   sourced fee AND no intakes — had nothing pinned: its content bunched at the
   top and left a void the height of the tallest card in the row. That is the
   "blank card" report. The pair rule hands the auto back to the stats whenever
   they exist, so exactly one element ever absorbs the free space. */
.pcard-cta{margin-top:auto;display:flex;align-items:center;justify-content:center;gap:6px;min-height:44px;
  border-radius:12px;background:var(--blue-100);color:var(--blue);font-weight:700;font-size:var(--fs-sm);
  font-family:var(--font-display);}
.pcard-stats + .pcard-cta{margin-top:10px;}
.pcard:hover .pcard-cta{background:var(--blue);color:#fff;}
/* A card with no campus photo gets a plain band, not a borrowed photograph.
   36 of the 143 published universities have no image in config/campus_images,
   and the explorer has always refused a country-level stand-in — a card reading
   "Pasadena, USA" under a photograph of Manhattan is the plausible-but-wrong
   detail that costs a site its credibility. Omitting the band entirely was the
   previous answer and it was worse in a grid: equal-height rows meant a
   photo-less card kept the row's height and simply showed a void where every
   neighbour had an image, which reads as a broken <img> rather than as an
   honest absence.
   So the band is always there and carries a quiet brand gradient plus the
   institution's own initials — obviously a graphic, impossible to mistake for a
   campus, and the same height as a real photo so the row stays even. */
.pcard:not(.pcard-has-media) .pcard-media{display:grid;place-items:center;
  background:linear-gradient(135deg,var(--blue-100),var(--surface-2));
  border-bottom:1px solid var(--border);}
.pcard-media-mono{font-family:var(--font-display);font-weight:800;font-size:1.6rem;
  letter-spacing:.06em;color:var(--blue);opacity:.42;}

/* University variant. On a programme card the institution is context and the
   programme is the choice; here the institution IS the choice, so the name is
   the heading inside the identity row and takes the size .pcard-title has on the
   other card. Everything below — stats panel, CTA, bottom alignment — is shared,
   which is the point: the same object looks the same in both grids. */
/* Same deletion as .pcard-title, and mandatory rather than optional: the note
   above says this name "takes the size .pcard-title has on the other card", so
   changing one without the other breaks a documented invariant and drifts the
   two grids apart again. */
.ucard-name{line-height:1.22;margin:0;font-weight:800;
  overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}
.ucard .pcard-uni{align-items:flex-start;}
/* The brand mark straddles the photo's bottom edge — half on the image, half on
   the card — at the chip's own size. See the note in <x-uni-card>: squared to
   38px beside the name, every wordmark was cropped to a fragment. */
/* Fixed at the logo chip's 36px, bottom-aligned: the 44px monogram tile overflows
   upward into the photo instead of pushing its name 8px below the neighbours'. */
.ucard-mark{display:flex;align-items:flex-end;height:36px;margin:-34px 0 -4px;position:relative;z-index:1;}
.ucard-mark .uni-logo,.ucard-mark .uni-mono{box-shadow:0 4px 14px rgba(0,20,50,.22);}
.ucard,.ccard{transition:transform .16s ease,box-shadow .16s ease;}
.ucard .pcard-media-img,.ccard .pcard-media-img{transition:transform .5s ease;}
.ucard .pcard-uni-meta{margin-top:3px;}
.ucard-chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
/* Course card. Same bones as .ucard — photo band, identity, stats, action — because the
   owner's instruction was that course details read as cards with images "everywhere",
   and two card grammars on one catalogue is how they drift apart again.
   The stream tile sits on the photo's bottom edge for the same reason .ucard-mark does:
   over a photograph, a coloured tile with a white glyph stays legible where a flat icon
   beside the title would not, and the field's colour is the first thing read. */
.ccard-mark{display:flex;height:44px;margin:-40px 0 -2px;position:relative;z-index:1;}
.ccard-mark .app-ic{box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 6px 16px rgba(0,20,50,.3);}
.ccard .pcard-body{gap:9px;}
.ccard .pcard-uni-meta{margin:0;}
.ccard .app-pills{margin:0;}
/* Shown only when the stat panel is half empty or absent — it is filling a gap,
   so it clamps rather than pushing the card taller than its neighbours. */
.pcard-blurb{font-size:var(--fs-xs);line-height:1.45;color:var(--muted);margin:0;
  overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;}

/* ---------- Destination hubs: universities by tier ----------
   /study-in/* listed its universities as a logo, a name and a city on a tinted
   tile — no photograph, no fee — and the owner's verdict was that the section
   neither attracted nor engaged. The cards are now <x-uni-card>, the explorer's
   photo card, so everything here is only the frame around them: an intro line
   with jump chips (the USA hub runs to 108 universities), one line per tier on
   what the tier means for the student, and the "Show all" button.
   The tier word in each heading takes the tier chip's own ink, so the colour a
   student meets in the jump chips is the colour of the section it jumps to. */
.dest-unis-intro p{margin:0;max-width:62ch;color:var(--muted);}
.tier-jump{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}
.tier-jump .chip{text-decoration:none;}
.tier-jump .chip b{font-weight:800;font-variant-numeric:tabular-nums;}
.tier-block{margin-top:clamp(28px,4vw,44px);}
.tier-head{display:flex;flex-direction:column;gap:4px;}
.tier-head h2{margin:0;text-wrap:balance;}
.tier-head p{margin:0;max-width:62ch;color:var(--muted);font-size:var(--fs-sm);}
.tier-ink-ambitious{color:var(--orange-600);}
.tier-ink-moderate{color:var(--blue);}
.tier-ink-safe{color:var(--ok);}
/* width:fit-content, or a display:flex button is block-level and fills the row. */
.tier-more{display:flex;width:fit-content;margin:18px auto 0;}
.tier-more-caret{transform:rotate(90deg);}

/* Rail arrows. The fade above says "clipped"; these say "operable".
   Added by app-mobile.js only when the rail actually overflows, so a row that
   fits shows nothing, and each button hides at its own end — an arrow that
   cannot move is worse than no arrow.
   Pointer-gated: on touch you swipe, and two 40px circles sitting on the first
   and last card would be two things to mis-tap. hover:hover + pointer:fine is
   the mouse/trackpad test, not a width test, so a small laptop window still
   gets them and a large tablet does not. */
.rail{position:relative;}
.rail-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;
  width:40px;height:40px;border-radius:50%;display:none;place-items:center;
  background:var(--surface);color:var(--blue);border:1px solid var(--border-strong);
  box-shadow:0 4px 14px rgba(16,40,70,.18);cursor:pointer;padding:0;}
.rail-nav:hover{background:var(--blue);color:#fff;border-color:var(--blue);}
.rail-nav[hidden]{display:none;}
.rail-prev{left:-8px;}
.rail-next{right:-8px;}
@media (hover:hover) and (pointer:fine){
  .rail-nav:not([hidden]){display:grid;}
}
.exam-card{flex:none;width:200px;min-height:120px;scroll-snap-align:start;border-radius:16px;display:flex;flex-direction:column;justify-content:center;gap:10px;padding:20px;color:#fff;text-decoration:none;box-shadow:0 6px 18px rgba(16,40,70,.16);transition:transform .15s;}
.exam-card.lg{width:224px;min-height:140px;}
.exam-card:active{transform:scale(.98);}
.exam-card-logo{max-height:48px;max-width:84%;width:auto;object-fit:contain;}
.exam-card.lg .exam-card-logo{max-height:56px;}
/* Auto-scrolling university logo marquee — optically normalised.
   Every mark was sized by bounding box alone (max-height:42px/max-width:165px)
   with computed filter:none, so across the 141 logos painted area ran 1,031px²
   (Unitec 24.6x42) to 6,920px² (Lakehead 165x41.9) = 6.71:1, and hue ran wider
   still — AUT rendered as a solid black rectangle beside Carnegie Mellon's
   165x15 of hairline type.

   These are REAL university marks and they now render in their real colours.
   grayscale(1) at opacity .9 used to sit on .logo-track to flatten the hue half
   of that spread; Suresh asked for the actual logos — "exact logos colour, no
   need grey shade" — and he is right that a wall of desaturated marks reads as
   placeholder art rather than as institutions we work with. Oxford blue, LSE
   red and RWTH's black-and-blue are the recognition this section exists for.

   The SIZE half of the normalisation below is untouched and still doing the
   real work: the 112px width cap and the 96px min-width on the cell are what
   took the painted-area spread from 6.71:1 to 4.56:1. Hue variety is the point
   of a logo wall; area variety was the defect.

   Opacity is 1, not .9. It was .9 to sit under greyscale (measured: at .6, eight
   of the 28 marks rendered here dropped below 3:1 — Southampton 7.50->2.87,
   Arizona State 5.79->2.53, UNSW 3.14->1.88). With the filter gone there is no
   reason to wash a real logo at all, and every mark gains contrast rather than
   losing it. Do not reintroduce a partial opacity here without re-measuring.

   Removing the filter is also strictly cheaper: this track transform-animates
   for 75s without stopping, and the old rule made the compositor apply a filter
   to that layer every frame. Hover still pauses the animation.

   Width cap 165 -> 112px: area spread 6.71:1 -> 4.56:1 over 141, 4.37:1 over
   the 28 rendered. Near the ceiling — sweeping every (H,W) pair bottoms out at
   4.33:1, because aspect ratios span 0.585 to 11.16. min-width:96px on the cell
   is the gutter fix: gap:48px was measured between bounding boxes, so a 25px
   crest floated in 48px of air. Costs 221px of track (54.7 -> 57.7 px/s). */
.marquee-label{font-size:var(--fs-xs);font-weight:800;text-transform:uppercase;letter-spacing:var(--track-label);color:var(--muted);margin-bottom:18px;}
.logo-marquee{overflow:hidden;width:100%;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 7%,#000 93%,transparent);}
.logo-track{display:flex;align-items:center;gap:48px;width:max-content;animation:mqscroll 75s linear infinite;}
.logo-marquee:hover .logo-track{animation-play-state:paused;}
.mq-logo{flex:none;height:46px;min-width:96px;display:grid;place-items:center;}
.mq-logo img{max-height:42px;max-width:112px;width:auto;object-fit:contain;}
@media(prefers-reduced-motion:reduce){.logo-track{transition:none;}}
@keyframes mqscroll{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media(prefers-reduced-motion:reduce){.logo-track{animation:none;flex-wrap:wrap;justify-content:center;width:auto;}}
.tier-ambitious{background:#FAEBDE;color:var(--orange-600);}
.tier-moderate{background:var(--blue-100);color:var(--blue);}
.tier-safe{background:#E0EFE8;color:var(--ok);}
/* The tier chip is the single most decision-relevant word on a university page,
   and its fill used to be a TRANSLUCENT tint tuned to composite on a white card.
   On a dark ground the tint composited against the ground instead: measured over
   the real campus photo plus the .uni-hero-img scrim on /university/*, ambitious
   fell to 1.39-2.67:1 and safe to 1.56-2.93:1, at 12.8px/700 where AA wants 4.5:1.
   Fixed by handing the chip the colour the tint was ALREADY resolving to on white,
   as an OPAQUE fill — #FAEBDE is rgba(224,132,49,.16) flattened on #fff, #E0EFE8
   is rgba(31,138,91,.14) flattened on #fff — so it stops depending on what is
   behind it. 4.93:1 / 4.87:1 on ANY ground.

   That fix has since been applied to all 17 translucent status fills in this file
   (.chip-trust, .status-*, .trust-badge, .rank-badge, .news-*, .cal-dot, ...) for
   the same reason arriving from the other direction: card grounds are no longer
   always white. .card now takes a --card-bg tint that cycles through four brand
   colours, and .chip-trust — built to composite on #fff — measured 4.06:1 on the
   teal and ocean tints on /loans. A chip whose colour depends on its ground will
   break every time the ground changes, and the ground WILL change.

   The pair of .uni-hero/.section-navy overrides that used to live here is gone:
   once the base fills became opaque, those rules were byte-identical duplicates of
   them, which is the equal-specificity trap this file has been bitten by before.

   RULE: a chip fill is opaque. If a new one needs a tint, flatten it on #fff and
   commit the hex, so it renders identically on white, on a card tint and on navy.
   .tier-moderate was already opaque (--blue-100, 6.92:1) and never had the bug. */

/* university detail hero */
/* Interior pages opened on a ~100px coloured strip rather than a hero — there
   was no opening moment on /universities, /loans, /scholarships or any
   university page. Real hero proportions now: 56px on a phone, 104px wide. */
.uni-hero{background:radial-gradient(130% 130% at 85% 0%,#2f63b8,var(--blue) 55%,var(--ocean));color:#fff;padding:clamp(2rem,4.5vw,3.5rem) 0 clamp(2.25rem,5vw,4rem);}
/* Same dark ground as .hero, so the same ink. Without this an .eyebrow dropped
   into a uni-hero inherits --orange-600, which is tuned for white and measures
   1.39:1 here — the light-surface fix turned into a dark-surface bug. */
.uni-hero .eyebrow,.uni-hero .accent{color:var(--summer-ink);}
.uni-back{color:rgba(255,255,255,.85);font-size:.875rem;font-weight:600;display:inline-block;margin-bottom:1rem;}
/* Breadcrumb trail — inherits its container's text colour (white on a hero, dark on light bg) */
.crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:5px 7px;font-size:.8rem;font-weight:600;line-height:1.3;margin-bottom:1rem;}
.crumbs a{color:inherit;opacity:.78;}
.crumbs a:hover{opacity:1;text-decoration:underline;}
.crumbs .sep{opacity:.5;}
.crumbs [aria-current]{opacity:1;font-weight:700;}
.uni-hero-row{display:flex;align-items:center;gap:16px;}

/* pager */
.pager{display:flex;align-items:center;justify-content:center;gap:14px;}
.pager-btn{background:var(--surface);border:1.5px solid var(--border-strong);border-radius:var(--radius-pill);
  padding:.6rem 1.2rem;font:inherit;font-weight:700;font-size:var(--fs-sm);color:var(--blue);cursor:pointer;}
.pager-btn:disabled{opacity:.4;cursor:default;color:var(--muted);}
.pager-info{font-size:.875rem;color:var(--muted);font-weight:600;}

/* ---------- Career outcomes (Adzuna) ---------- */
.job-stat{border-top:1px solid var(--border);padding:14px 0;}
.job-stat:first-of-type{border-top:0;}
.job-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.job-nums{display:flex;gap:24px;margin-top:8px;}
.job-nums .n{font-size:1.5rem;font-weight:800;color:var(--blue);line-height:1;letter-spacing:-.02em;}
.job-nums .l{font-size:var(--fs-xs);color:var(--muted);margin-top:3px;}
.job-cos{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;}
.job-cos a{text-decoration:none;}

/* ---------- Accounts / dashboard ---------- */
.save-btn{display:inline-flex;align-items:center;gap:.4rem;background:var(--surface);border:1.5px solid var(--border-strong);
  border-radius:var(--radius-pill);padding:.5rem 1.1rem;font:inherit;font-weight:700;font-size:var(--fs-sm);color:var(--blue);cursor:pointer;transition:.15s;}
.save-btn:hover{border-color:var(--blue);}
/* Carries the visible word "Saved", so it is body text, not an icon: #d6455c on
   this pink was 3.84:1. #b02a45 stays pink and reaches 5.72:1. */
.save-btn.on{background:#fdeef0;border-color:#f0a9b4;color:#b02a45;}
.or-divider{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:.8rem;margin:14px 0;}
.or-divider::before,.or-divider::after{content:"";flex:1;height:1px;background:var(--border);}
.dash-head{display:flex;align-items:center;gap:14px;}
.dash-avatar{width:52px;height:52px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:1.5rem;
  color:#fff;background:linear-gradient(140deg,#2f63b8,var(--blue));flex:none;}
.status-open{background:#E0EFE8;color:var(--ok);}
.status-soon{background:#FAEBDE;color:var(--orange-600);}
.status-closed{background:var(--surface-2);color:var(--muted);border:1px solid var(--border);}
.status-neutral{background:var(--surface-2);color:var(--muted);border:1px solid var(--border);}
.verified{font-size:.8rem;color:var(--ok);font-weight:600;margin-top:8px;}
.verified a{color:var(--blue);}

/* ---------- Loans ---------- */
.seg{display:inline-flex;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-pill);padding:3px;gap:2px;flex-wrap:wrap;}
.seg-btn{background:none;border:0;border-radius:var(--radius-pill);padding:.5rem 1rem;font:inherit;font-weight:700;font-size:.875rem;color:var(--muted);cursor:pointer;}
.seg-btn.on{background:var(--blue);color:#fff;}
.lender-top{display:flex;align-items:center;justify-content:space-between;gap:8px;}

/* ---------- Test-prep chips (homepage) ---------- */

/* ---------- Mock test engine ---------- */
.mt-bar{display:flex;align-items:center;justify-content:space-between;font-weight:700;font-size:.875rem;}
.mt-progress{color:var(--muted);}
.mt-timer{color:var(--blue);font-variant-numeric:tabular-nums;}
.mt-timer.low{color:var(--danger);}
.mt-track{height:6px;background:var(--border);border-radius:8px;overflow:hidden;margin-top:8px;}
.mt-track span{display:block;height:100%;background:linear-gradient(90deg,var(--blue),var(--orange));border-radius:8px;transition:width .3s ease;}
/* Reading passages. The box was built when every passage was a hundred words: no
   white-space rule, so paragraph breaks collapsed into one block of text, and a
   220px cap that turned a real 700-word IELTS passage into a keyhole. pre-line
   keeps the paragraphing the passage was written with; the cap scales with the
   viewport so the question stays visible underneath it on a phone. */
.mt-passage{background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:var(--fs-sm);line-height:1.65;margin-top:12px;max-height:min(52vh,440px);overflow:auto;white-space:pre-line;}
.mt-opts{display:flex;flex-direction:column;gap:10px;margin-top:14px;}
.mt-opt{display:flex;align-items:center;gap:12px;text-align:left;background:var(--surface-2);border:2px solid var(--border);
  border-radius:12px;padding:13px 14px;font:inherit;font-size:1rem;cursor:pointer;color:var(--text);transition:.14s;}
.mt-opt:hover{border-color:var(--border-strong);}
.mt-opt.sel{border-color:var(--blue);background:var(--blue-100);}
.mt-key{width:28px;height:28px;flex:none;border-radius:8px;display:grid;place-items:center;font-weight:800;font-size:.875rem;
  background:#fff;border:1px solid var(--border-strong);color:var(--blue);}
.mt-opt.sel .mt-key{background:var(--blue);color:#fff;border-color:var(--blue);}
.mt-nav{display:flex;justify-content:space-between;gap:10px;margin-top:18px;}
.mt-nav .btn{flex:1;}
.mt-sec{display:flex;align-items:center;font-size:.875rem;margin:8px 0;}
.mt-sec > span:first-child{min-width:92px;font-weight:600;}

/* ---------- Comparison tool ---------- */
.cmp-results{display:flex;flex-direction:column;gap:6px;}
.cmp-result{display:flex;align-items:center;gap:10px;text-align:left;background:var(--surface-2);border:1px solid var(--border);
  border-radius:12px;padding:9px 12px;font:inherit;cursor:pointer;color:var(--text);width:100%;}
.cmp-result:hover{border-color:var(--blue);}
.cmp-result b{font-size:var(--fs-sm);}
.cmp-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:16px;box-shadow:var(--shadow);background:var(--surface);}
.cmp{border-collapse:collapse;width:100%;min-width:520px;}
.cmp th,.cmp td{padding:12px 14px;border-bottom:1px solid var(--border);text-align:center;vertical-align:middle;font-size:var(--fs-sm);}
.cmp thead th{background:var(--surface-2);position:relative;min-width:140px;}
.cmp .uni-mono{margin:0 auto 6px;}
.cmp-name{font-weight:700;font-size:.875rem;color:var(--text);line-height:1.2;display:block;}
.cmp-attr{text-align:left !important;font-weight:700;color:var(--muted);font-size:.8rem;background:var(--surface-2);min-width:120px;position:sticky;left:0;}
.cmp-x{position:absolute;top:6px;right:6px;width:22px;height:22px;border-radius:50%;border:0;background:var(--border);
  color:var(--muted);cursor:pointer;font-size:var(--fs-xs);line-height:1;}
/* The visual mark stays 22px — it sits in a chip corner and a 44px circle would   swallow it. The TAP TARGET is expanded to 44x44 with a centred pseudo-element   instead, which is what the 44px policy asks for: a thumb-sized region, not a   thumb-sized graphic. */.cmp-x::after{content:"";position:absolute;top:50%;left:50%;width:44px;height:44px;transform:translate(-50%,-50%);}
.cmp-x:hover{background:var(--danger);color:#fff;}
.cmp tbody tr:last-child td{border-bottom:0;}

/* ---------- Tools: sliders ---------- */
.slider-row{margin:14px 0;}
.slider-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;}
.slider-head label{font-weight:700;font-size:var(--fs-sm);}
/* Not body text: a single numeral read against a slider track. */
.slider-val{font-weight:800;color:var(--blue);font-size:1.06rem;}
.slider-scale{display:flex;justify-content:space-between;font-size:var(--fs-xs);color:var(--muted);margin-top:4px;}
input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:6px;border-radius:8px;
  background:linear-gradient(90deg,var(--blue),var(--ice));outline:none;}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:26px;height:26px;border-radius:50%;
  background:var(--orange);border:3px solid #fff;box-shadow:0 3px 10px rgba(224,132,49,.5);cursor:pointer;}
input[type=range]::-moz-range-thumb{width:24px;height:24px;border-radius:50%;background:var(--orange);
  border:3px solid #fff;box-shadow:0 3px 10px rgba(224,132,49,.5);cursor:pointer;}

/* ---------- Tools: EMI result ---------- */
.emi-result{margin-top:18px;border-radius:16px;background:radial-gradient(120% 130% at 85% 0%,#2f63b8,var(--ocean));color:#fff;padding:18px;}
.emi-main .l{font-size:.8rem;opacity:.8;} .emi-main .n{font-size:2.2rem;font-weight:800;letter-spacing:-.02em;line-height:1;}
.emi-split{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:14px;border-top:1px solid rgba(255,255,255,.2);padding-top:12px;}
.emi-split .l{font-size:var(--fs-xs);opacity:.75;} .emi-split .v{font-weight:700;font-size:var(--fs-sm);margin-top:2px;}
.score-val{font-size:1.5rem;font-weight:800;color:var(--blue);letter-spacing:-.02em;margin:2px 0;}

/* ---------- Tools: profile gauge ---------- */
.gauge{position:relative;width:150px;height:150px;border-radius:50%;margin:0 auto 6px;display:grid;place-items:center;
  background:conic-gradient(var(--orange) calc(var(--pct)*1%),var(--border) 0);}
.gauge::before{content:"";position:absolute;width:116px;height:116px;border-radius:50%;background:var(--surface);}
.gauge-n{position:relative;font-size:2.4rem;font-weight:800;color:var(--blue);letter-spacing:-.02em;}
.gauge-n small{font-size:1rem;color:var(--muted);font-weight:700;}

/* ---------- Form fields ---------- */
.field{width:100%;font:inherit;font-size:1rem;padding:.8rem 1rem;border:1.5px solid var(--border-strong);
  border-radius:var(--radius-sm);background:var(--surface-2);color:var(--text);transition:.15s;}
.field:focus{outline:none;border-color:var(--blue);background:#fff;box-shadow:0 0 0 3px var(--blue-100);}
.field::placeholder{color:#66788d;}   /* 4.6:1 on white — WCAG 1.4.3 (was #9aabbd ≈ 2.4:1) */

/* two-column split on desktop */
.split{display:grid;gap:18px;}
@media(min-width:860px){ .split{grid-template-columns:1.15fr .85fr;align-items:center;} }

/* ---------- SVG icons (replace emoji everywhere) ---------- */
.ic{flex:none;vertical-align:-.15em;}
.eyebrow .ic{vertical-align:-2px;}
.tool .ti .ic,.tabbar .ti .ic,.icon-btn .ic,.fab-wa .ic,.cb-badge .ic{vertical-align:middle;}
.go .ic,.link .ic,.schol-src .ic{vertical-align:-.18em;margin-left:2px;}
.tprep-ico{color:var(--blue);line-height:0;}

/* ---------- Destination cards (flag tiles) ---------- */
.dest-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;}
@media(min-width:560px){ .dest-grid{grid-template-columns:repeat(3,1fr);} }
@media(min-width:820px){ .dest-grid{grid-template-columns:repeat(5,1fr);} }

/* ---------- Callback form ---------- */
.callback{padding:20px;}
.cb-head{display:flex;gap:12px;align-items:flex-start;}
.cb-badge{width:46px;height:46px;border-radius:16px;flex:none;display:grid;place-items:center;color:#fff;
  background:linear-gradient(135deg,#ff9a3d,var(--orange));box-shadow:0 8px 18px rgba(224,132,49,.32);}
.cb-badge-ok{background:linear-gradient(135deg,#34c07e,var(--ok));box-shadow:0 8px 18px rgba(31,138,91,.3);
  width:56px;height:56px;border-radius:16px;margin:0 auto;}
.cb-head h3{margin:0;font-size:var(--fs-h3);}
.cb-sub{margin:3px 0 0;font-size:.875rem;color:var(--muted);display:flex;align-items:center;gap:7px;}
.cb-live{width:8px;height:8px;border-radius:50%;background:var(--ok);position:relative;flex:none;}
.cb-live::after{content:"";position:absolute;inset:-4px;border-radius:50%;background:var(--ok);opacity:.4;animation:cbpulse 1.9s ease-out infinite;}
@keyframes cbpulse{0%{transform:scale(.5);opacity:.5}100%{transform:scale(1.9);opacity:0}}
.cb-form{display:flex;flex-direction:column;gap:12px;margin-top:16px;}
.cb-field{display:flex;flex-direction:column;gap:5px;}
.cb-err{color:var(--danger);font-size:.8rem;font-weight:600;}
.cb-safe{display:flex;align-items:center;justify-content:center;gap:6px;color:var(--muted);font-size:.8rem;margin-top:2px;}
.cb-safe .ic{color:var(--ok);}
@media(prefers-reduced-motion:reduce){ .cb-live::after{animation:none;} }

/* ===================================================================
   Phase 5 — Student account modules (dashboard tabs 83–88)
   =================================================================== */
[x-cloak]{display:none !important;}
.lbl{font-size:.8rem;font-weight:700;color:var(--muted);display:block;margin-bottom:5px;}

/* ---- Tab nav ---- */
.dtabs{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px;
  border-bottom:1px solid var(--border);}
.dtabs::-webkit-scrollbar{display:none;}
.dtab{flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;background:none;border:0;
  border-bottom:3px solid transparent;padding:11px 10px;margin-bottom:-1px;font:inherit;font-weight:700;
  font-size:var(--fs-sm);color:var(--muted);cursor:pointer;white-space:nowrap;transition:.14s;}
.dtab:hover{color:var(--text);}
.dtab.on{color:var(--blue);border-bottom-color:var(--orange);}
/* A count is information, not decoration — white on --orange was 2.79:1 at
   .68rem. The ink token as the FILL puts it at 5.75:1 and keeps it orange. */
.dtab-badge{background:var(--orange-600);color:#fff;font-size:var(--fs-xs);font-weight:800;min-width:17px;height:17px;
  border-radius:12px;display:inline-grid;place-items:center;padding:0 4px;}

/* ---- Empty states ---- */
.empty{text-align:center;padding:38px 20px;background:var(--surface-2);border:1px dashed var(--border-strong);border-radius:var(--radius);}
.empty b{display:block;}
.empty p{margin:.2rem auto 0;max-width:34ch;font-size:.875rem;}

/* ---- Application tracker (83) ---- */
.track-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;}
.stepper{display:flex;margin-top:16px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px;}
.stepper::-webkit-scrollbar{display:none;}
.step{flex:1;min-width:64px;position:relative;text-align:center;}
.step::before{content:"";position:absolute;top:13px;left:-50%;width:100%;height:3px;background:var(--border);z-index:0;}
.step:first-child::before{display:none;}
.step-dot{position:relative;z-index:1;width:28px;height:28px;border-radius:50%;display:inline-grid;place-items:center;
  background:var(--surface);border:2px solid var(--border-strong);color:var(--muted);font-weight:800;font-size:.8rem;}
.step-lbl{display:block;font-size:var(--fs-xs);color:var(--muted);margin-top:5px;font-weight:600;}
.step.done .step-dot{background:var(--ok);border-color:var(--ok);color:#fff;}
.step.done::before{background:var(--ok);}
.step.now .step-dot{background:var(--blue);border-color:var(--blue);color:#fff;box-shadow:0 0 0 4px var(--blue-100);}
.step.now .step-lbl{color:var(--blue);font-weight:800;}
.track-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;padding-top:12px;border-top:1px solid var(--border);}

/* ---- Tasks / reminders (84) ---- */
.task-add{display:flex;gap:8px;flex-wrap:wrap;}
.task-add .field{flex:1;min-width:160px;}
.task-add .task-date{flex:0 0 150px;min-width:130px;}
.task-row{display:flex;align-items:center;gap:12px;padding:12px 14px;}
.task-check{flex:none;width:22px;height:22px;border-radius:8px;border:2px solid var(--border-strong);background:var(--surface);
  cursor:pointer;display:grid;place-items:center;color:#fff;transition:.14s;}
.task-check:hover{border-color:var(--blue);}
.task-check.on{background:var(--ok);border-color:var(--ok);}
.task-row b{font-size:var(--fs-sm);font-weight:600;}
.task-due{display:block;font-size:.8rem;color:var(--muted);}
.task-row.is-overdue{border-color:#f1c7c1;}
.task-row.is-overdue .task-due{color:var(--danger);font-weight:700;}
.task-row.is-done b{text-decoration:line-through;color:var(--muted);}

/* ---- Counsellor chat (85) ---- */
.chat{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;background:var(--surface);box-shadow:var(--shadow);}
.chat-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--border);background:var(--surface-2);}
.chat-avatar{width:38px;height:38px;border-radius:50%;flex:none;display:grid;place-items:center;color:#fff;font-weight:800;
  background:linear-gradient(140deg,#2f63b8,var(--blue));}
.chat-body{padding:16px 14px;display:flex;flex-direction:column;gap:10px;max-height:420px;overflow-y:auto;background:var(--bg);}
.bubble{max-width:80%;padding:9px 13px;border-radius:16px;font-size:var(--fs-sm);line-height:1.45;position:relative;}
.bubble.in{align-self:flex-start;background:var(--surface);border:1px solid var(--border);border-bottom-left-radius:4px;}
.bubble.out{align-self:flex-end;background:var(--blue);color:#fff;border-bottom-right-radius:4px;}
.bubble-time{display:block;font-size:var(--fs-xs);opacity:.6;margin-top:4px;}
.chat-input{display:flex;gap:8px;padding:10px;border-top:1px solid var(--border);background:var(--surface);}
.chat-input .field{flex:1;}
.chat-input .btn{padding:.7rem 1rem;}

/* ---- Appointments (86) ---- */
.slot-row{display:flex;gap:8px;flex-wrap:wrap;}
.slot{background:var(--surface-2);border:1.5px solid var(--border-strong);border-radius:var(--radius-pill);
  padding:.5rem 1rem;font:inherit;font-weight:700;font-size:.875rem;color:var(--text);cursor:pointer;transition:.14s;}
.slot:hover{border-color:var(--blue);}
.slot.on{background:var(--blue);color:#fff;border-color:var(--blue);}
.appt{display:flex;align-items:center;gap:14px;}
.appt.is-past{opacity:.62;}
.inv-status{font-size:var(--fs-xs);font-weight:800;text-transform:uppercase;letter-spacing:.04em;padding:2px 8px;border-radius:22px;}
.status-paid{background:#E0EFE8;color:var(--ok);}
.status-due{background:#FAEBDE;color:var(--orange-600);}
.status-refunded{background:var(--surface-2);color:var(--muted);}
.ref-code{font-size:1.5rem;font-weight:800;letter-spacing:.12em;font-family:var(--font);}
.ref-row{display:flex;align-items:center;gap:12px;}
.ref-avatar{width:38px;height:38px;border-radius:50%;flex:none;display:grid;place-items:center;color:#fff;font-weight:800;
  background:linear-gradient(140deg,#ff9a3d,var(--orange));}
.ref-invited{background:var(--surface-2);color:var(--muted);border:1px solid var(--border);}
.ref-joined{background:var(--blue-100);color:var(--blue);}
.ref-rewarded{background:#E0EFE8;color:var(--ok);}

/* ===================================================================
   Section A — Lead capture & conversion
   =================================================================== */
.clean{list-style:none;padding:0;margin:0;}

/* footer */
.foot-grid{display:grid;gap:26px;}
@media(min-width:760px){ .foot-grid{grid-template-columns:1.4fr 1fr 1fr;} }
.foot-brand .brand-logo{height:34px;max-width:170px;}
/* h3, not h4. The markup moved to h3 for the screen-reader outline and this
   rule was left naming h4 — so every footer column heading rendered at .foot's
   dim rgba(255,255,255,.8), the same ink as its own links, on every page.
   Third dead-selector bug of this class in this file; see fix 1's DeadSelectorTest. */
.foot-col h3{color:#fff;margin:0 0 10px;}
.foot-col a{display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.8);font-size:.875rem;padding:4px 0;}
.foot-col a:hover{color:#fff;}
.foot-social{display:flex;gap:10px;margin-top:14px;}
/* 44, the file's floor. Five social links in the footer at 34x34 were the
   last fixed-size controls on every page still under it. */
.foot-social a{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;color:#fff;
  background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.16);transition:.15s;}
.foot-social a:hover{background:rgba(255,255,255,.22);}
.foot-copy{margin-top:22px;padding-top:16px;border-top:1px solid rgba(255,255,255,.12);}

/* exit-intent modal */
.modal-overlay{position:fixed;inset:0;z-index:100;background:rgba(6,20,42,.55);backdrop-filter:blur(3px);
  display:flex;align-items:center;justify-content:center;padding:18px;animation:fadeIn .2s ease;}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.modal{background:var(--surface);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);width:100%;max-width:400px;
  padding:24px;position:relative;animation:popIn .22s cubic-bezier(.2,.9,.3,1.2);}
@keyframes popIn{from{transform:translateY(14px) scale(.96);opacity:0}to{transform:none;opacity:1}}
.modal-x{position:absolute;top:12px;right:12px;width:44px;height:44px;display:grid;place-items:center;border-radius:50%;border:0;background:var(--surface-2);
  color:var(--muted);cursor:pointer;font-size:var(--fs-sm);}
.modal-x:hover{background:var(--border);}
.modal-eyebrow{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);font-weight:800;letter-spacing:var(--track-label);
  text-transform:uppercase;color:var(--orange-600);}

/* lead wizard */
.wiz-progress{display:flex;justify-content:space-between;margin-bottom:16px;position:relative;}
.wiz-dot{flex:1;text-align:center;position:relative;}
.wiz-dot::before{content:"";position:absolute;top:13px;left:-50%;width:100%;height:3px;background:var(--border);}
.wiz-dot:first-child::before{display:none;}
.wiz-dot span{position:relative;z-index:1;width:28px;height:28px;border-radius:50%;display:inline-grid;place-items:center;
  background:var(--surface);border:2px solid var(--border-strong);color:var(--muted);font-weight:800;font-size:.8rem;}
.wiz-dot small{display:block;font-size:var(--fs-xs);color:var(--muted);margin-top:5px;font-weight:600;}
.wiz-dot.now span{background:var(--blue);border-color:var(--blue);color:#fff;box-shadow:0 0 0 4px var(--blue-100);}
.wiz-dot.now small{color:var(--blue);font-weight:800;}
.wiz-dot.done span{background:var(--ok);border-color:var(--ok);color:#fff;}
.wiz-dot.done::before{background:var(--ok);}
.wiz-nav{display:flex;justify-content:space-between;gap:10px;margin-top:18px;}
.wiz-nav .btn{flex:1;}

/* branch cards */
.branch-card{transition:border-color .16s,box-shadow .16s;}
.branch-card.is-near{border-color:var(--ok);box-shadow:0 0 0 3px rgba(31,138,91,.14);}
.branch-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.branch-actions{display:flex;gap:16px;flex-wrap:wrap;margin-top:6px;}
.branch-actions .link{display:inline-flex;align-items:center;gap:5px;font-size:.875rem;}

/* chat availability status */
.chat-status{display:flex;align-items:center;gap:6px;font-size:.8rem;font-weight:600;}
.chat-status .status-dot{width:8px;height:8px;border-radius:50%;flex:none;}
.chat-status.is-online{color:var(--ok);}
.chat-status.is-online .status-dot{background:var(--ok);box-shadow:0 0 0 3px rgba(31,138,91,.2);}
.chat-status.is-away{color:var(--muted);}
.chat-status.is-away .status-dot{background:#b9c4d1;}

/* guide download CTA */
.guide-cta{background:linear-gradient(120deg,var(--blue-100),var(--surface));border-color:var(--border-strong);}

/* ===================================================================
   Section B — University & Course Discovery
   =================================================================== */
/* explorer filters */
.filter-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
@media(min-width:560px){ .filter-grid{grid-template-columns:repeat(3,1fr);} }
@media(min-width:860px){ .filter-grid{grid-template-columns:repeat(5,1fr);} }

/* ranking badges on cards */
/* Ranks are reference, not headline. Three solid saturated pills sat at the same
   visual weight as the tier chip and louder than the university's own name, on a
   card only half a column wide — a large part of what read as clutter. Outlined
   and quiet: still scannable, no longer shouting over the thing being chosen. */
.rank-badges{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px;}
.rank-badge{font-size:var(--fs-xs);font-weight:700;padding:3px 9px;border-radius:22px;letter-spacing:.02em;
  background:transparent;border:1px solid var(--border-strong);color:var(--muted);}
.rank-badge.qs{background:var(--blue-100);border-color:rgba(16,40,70,.09);color:var(--blue-600);}
.rank-badge.the{background:transparent;border-color:var(--border-strong);color:var(--muted);}
.rank-badge.acc{background:#FAEBDE;border-color:rgba(16,40,70,.09);color:var(--orange-600);}
.dl-days{font-size:1.9rem;font-weight:800;letter-spacing:-.02em;}
.dl-lbl{font-size:var(--fs-xs);font-weight:700;text-transform:uppercase;opacity:.9;margin-top:2px;}

/* cost calculator */
.cost-rows{display:flex;flex-direction:column;gap:8px;}
.cost-row{display:flex;align-items:center;justify-content:space-between;padding:9px 12px;background:var(--surface-2);border:1px solid var(--border);border-radius:12px;font-size:var(--fs-sm);}
.cost-row b{font-variant-numeric:tabular-nums;}

/* virtual tour */
.video-embed{position:relative;padding-top:56.25%;border-radius:16px;overflow:hidden;background:#000;}
.video-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0;}

/* intake calendar */
.cal-table th{text-align:center;}
.cal-table td{text-align:center;}
.cal-dot{display:inline-grid;place-items:center;width:26px;height:26px;border-radius:50%;background:#E0EFE8;color:var(--ok);}
/* --border, not --border-strong: this is the one place the token was used as an INK, and
   the 1.4.11 re-point is a border target that has no business landing on a glyph that is
   deliberately ghosted. #6f849e put "no intake this month" at 3.84:1 on white, next to the
   filled .cal-dot cells it is supposed to recede behind. */
.cal-empty{color:var(--border);font-weight:800;}

/* course-by-stream */
.course-tile{transition:transform .16s,box-shadow .16s;}

/* admission checklist */
.checklist{display:flex;flex-direction:column;gap:8px;}
.check-item{display:flex;align-items:flex-start;gap:12px;text-align:left;background:var(--surface-2);border:1px solid var(--border);
  border-radius:12px;padding:12px 14px;font:inherit;cursor:pointer;color:var(--text);transition:.14s;width:100%;}
.check-item:hover{border-color:var(--border-strong);}
.check-item .check-box{flex:none;width:24px;height:24px;border-radius:8px;border:2px solid var(--border-strong);background:#fff;
  display:grid;place-items:center;color:#fff;transition:.14s;}
.check-item b{display:block;font-size:var(--fs-sm);}
.check-item small{display:block;}
.check-item.is-done .check-box{background:var(--ok);border-color:var(--ok);}
.check-item.is-done b{text-decoration:line-through;color:var(--muted);}

/* ===================================================================
   Section C — Trust & Social Proof (scaffolding)
   =================================================================== */
/* University card destination band. A short photo strip at the head of the card,
   with the identity badge sitting on it — enough to make the listing read as
   places rather than rows, without pretending the photo is the campus. The
   gradient keeps the tier chip and the logo legible over any image. */
.uni-card-img{position:relative;padding-top:0;overflow:hidden;}
.uni-card-img::before{content:"";display:block;height:124px;margin:0 -18px;
  background-image:linear-gradient(180deg,rgba(0,43,76,.14),rgba(0,43,76,.58)),var(--uc-img);
  background-size:cover;background-position:center;}

.trust-soon{display:flex;align-items:center;gap:14px;padding:18px;border:2px dashed var(--border-strong);
  border-radius:16px;background:var(--surface-2);flex-wrap:wrap;}
.trust-soon-body{flex:1 1 200px;min-width:0;}
.trust-soon-cta{flex:none;}
/* Below 560px the row cannot hold icon + text + button: the 50px icon and a
   ~185px button plus gaps and padding leave about 37px for the text, which
   renders one word per line and ~900px tall, and on the longest label the button
   overflowed a 375px viewport and overlapped the text underneath it. The button
   takes its own full-width line instead, so the note — which on these pages is
   the honesty disclosure itself — is actually readable. */
@media(max-width:560px){
  .trust-soon{align-items:flex-start;}
  .trust-soon-body{flex:1 1 calc(100% - 64px);}
  .trust-soon-cta{flex:1 0 100%;justify-content:center;}
}
.soon-ic{flex:none;width:50px;height:50px;border-radius:50%;background:var(--blue-100);color:var(--blue);display:grid;place-items:center;}
.trust-soon b{display:block;}
.trust-h{font-size:var(--fs-xs);font-weight:800;text-transform:uppercase;letter-spacing:var(--track-label);color:var(--muted);text-align:center;margin:0 0 10px;}

/* google reviews
   Suresh: the block says "Google reviews" in words but looks like every other
   section, so nothing signals whose reviews these are. These are Google's four
   brand colours, used as an accent device — NOT a redrawn Google logo. The mark
   is a specific piece of artwork and an approximate hand-built copy reads as a
   counterfeit; the palette plus the word "Google" carries the recognition
   honestly. If Suresh obtains the official badge asset from Google's brand
   resources, it drops straight into .g-rule's place. */
:root{--g-blue:#4285F4;--g-red:#EA4335;--g-yellow:#FBBC04;--g-green:#34A853;}
/* Four-segment rule. Decorative only — it carries no information, so the yellow
   segment's low contrast against white is not a 1.4.11 concern; it is read as
   one band, and the block is titled in text either way. */
.g-rule{display:flex;height:4px;border-radius:4px;overflow:hidden;margin:0 0 14px;max-width:180px;}
.g-rule i{flex:1;}
.g-rule i:nth-child(1){background:var(--g-blue);}
.g-rule i:nth-child(2){background:var(--g-red);}
.g-rule i:nth-child(3){background:var(--g-yellow);}
.g-rule i:nth-child(4){background:var(--g-green);}
.greviews-score{text-align:right;line-height:1.1;}
.greviews-score b{font-size:1.7rem;color:var(--blue);font-weight:800;}
/* Google's own review stars are #FBBC04, which is 1.71:1 on white — the previous
   #f5a623 was 2.03 and no better. Neither clears the 3:1 a graphic needs, and
   darkening far enough (#C97F04, 3.22) turns the gold brown and loses the very
   recognition this change is for. So the stars keep the authentic gold and every
   star row carries role="img" with the rating spelled out, which puts the
   information in text — the condition under which 1.4.11 does not apply to the
   graphic. The rating must never be shown as stars alone. */
.gstars{color:var(--g-yellow);letter-spacing:1px;}
.gstars.sm{font-size:.8rem;}
/* The four colours again, cycling across reviewer avatars, so a row of real
   reviews reads as Google's without a single letterform being imitated.
   These are the DARK variants, not the brand primaries: the avatar carries a
   white initial, which is text and needs 4.5:1, and white measures 3.56 / 3.92 /
   3.06 / 1.71 on the four primaries — all four would have failed. Google's own
   darker shades clear it (5.36 / 5.80 / 5.02) and still read as the same
   palette; the gold needed taking to #8A6A00 (5.07). The primaries stay on the
   rule and the stars, which carry no text. */
.review-card:nth-child(4n+1) .review-avatar{background:#1a67d2;}
.review-card:nth-child(4n+2) .review-avatar{background:#C5221F;}
.review-card:nth-child(4n+3) .review-avatar{background:#188038;}
.review-card:nth-child(4n+4) .review-avatar{background:#8A6A00;}
/* The CTA reads as the Google action it is. #1a67d2 is Google blue darkened to
   clear 4.5:1 under white label text (4285F4 itself is 3.56 and would fail). */
.btn-google{background:#1a67d2;color:#fff;border:1px solid #1a67d2;}
.btn-google:hover{background:#14539f;border-color:#14539f;color:#fff;}
.soon-ic.g-ic{background:#e8f0fe;color:#1a67d2;}
.review-row{display:flex;gap:12px;overflow-x:auto;padding:4px 2px 10px;scrollbar-width:none;}
.review-row::-webkit-scrollbar{display:none;}
.review-card{flex:0 0 280px;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:16px;box-shadow:var(--shadow-sm);}
.review-top{display:flex;align-items:center;gap:10px;margin-bottom:8px;}
.review-avatar{width:38px;height:38px;border-radius:50%;flex:none;display:grid;place-items:center;color:#fff;font-weight:800;
  background:linear-gradient(140deg,#2f63b8,var(--blue));}
.review-text{font-size:.875rem;color:var(--text);margin:6px 0 0;line-height:1.5;}

/* video testimonials */
.testi-card{flex:0 0 260px;}
.testi-thumb{position:relative;display:block;border-radius:12px;overflow:hidden;aspect-ratio:16/9;background:#000;margin-bottom:8px;}
.testi-thumb img{width:100%;height:100%;object-fit:cover;opacity:.85;}
.testi-play{position:absolute;inset:0;margin:auto;width:52px;height:52px;border-radius:50%;background:rgba(192,57,43,.92);
  color:#fff;display:grid;place-items:center;box-shadow:0 6px 16px rgba(0,0,0,.35);}

/* success stories */
.story-card .story-proof{width:100%;border-radius:12px;margin-top:10px;border:1px solid var(--border);}

/* counsellor cards */
.counsellor-card{text-align:center;}
.counsellor-photo{width:72px;height:72px;border-radius:50%;margin:0 auto 8px;display:grid;place-items:center;overflow:hidden;
  color:#fff;font-weight:800;font-size:1.7rem;background:linear-gradient(140deg,#2f63b8,var(--blue));}
.counsellor-photo img{width:100%;height:100%;object-fit:cover;}

/* logo strips — partners, trust badges and press marks.
   Full colour, same call as the university marquee above: these are real
   organisations' marks and a desaturated wall of them reads as placeholder art.
   The .8 opacity went with the greyscale — washing a real logo has no purpose
   once it is in its own colours, and every mark gains contrast by losing it. */
.logo-strip{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;align-items:center;}
.logo-cell{width:140px;height:70px;background:var(--surface);border:1px solid var(--border);border-radius:12px;display:grid;place-items:center;padding:10px;}
.logo-cell img{max-width:100%;max-height:100%;object-fit:contain;transition:.15s;}

/* ===================================================================
   Section D — Tools & Calculators
   =================================================================== */
/* inline numeric input inside a cost row */
.cost-input{width:120px;flex:none;text-align:right;padding:.45rem .6rem;font-weight:700;font-size:var(--fs-sm);}
.cost-row{gap:10px;}

/* cost-of-living comparison bars */
.col-list{display:flex;flex-direction:column;gap:14px;}
.col-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;}
.col-country{font-weight:700;color:var(--blue);}
.col-bar{height:12px;background:var(--surface-2);border-radius:8px;overflow:hidden;margin:5px 0 3px;border:1px solid var(--border);}
.col-bar i{display:block;height:100%;border-radius:8px;background:linear-gradient(90deg,var(--blue),var(--orange));}

/* timeline planner */
.timeline{position:relative;padding-left:8px;}
.tl-item{display:grid;grid-template-columns:78px 24px 1fr;align-items:start;gap:8px;padding-bottom:16px;position:relative;}
.tl-item:not(:last-child) .tl-dot::after{content:"";position:absolute;top:16px;left:50%;transform:translateX(-50%);width:2px;height:calc(100% + 0px);background:var(--border);}
.tl-date{font-size:.8rem;font-weight:800;color:var(--blue);text-align:right;padding-top:1px;}
.tl-dot{position:relative;width:16px;height:16px;border-radius:50%;background:#fff;border:3px solid var(--blue);margin:2px auto 0;z-index:1;}
.tl-final .tl-dot{background:var(--orange);border-color:var(--orange);}
.tl-body b{display:block;font-size:var(--fs-sm);}
.tl-body small{display:block;}

/* ===================================================================
   Section E — Content & SEO
   =================================================================== */
@media(min-width:760px){ .foot-grid{grid-template-columns:1.6fr 1fr 1fr 1fr;} }

/* blog cards */
.post-card{overflow:hidden;transition:transform .16s ease,box-shadow .16s ease;}
.post-cover{aspect-ratio:16/9;display:flex;align-items:flex-end;padding:10px;
  background:linear-gradient(135deg,hsl(calc(208 + var(--i)*34) 58% 46%),hsl(calc(208 + var(--i)*34) 64% 30%));}
.post-cover-img{background:var(--blue);background-image:linear-gradient(180deg,rgba(3,20,44,0) 45%,rgba(3,20,44,.4)),var(--cover);
  background-size:cover;background-position:center;background-repeat:no-repeat;}
.post-cat{background:rgba(255,255,255,.92);color:var(--blue);font-size:var(--fs-xs);font-weight:800;padding:3px 9px;
  border-radius:22px;text-transform:uppercase;letter-spacing:.03em;}

/* article prose */
.prose{line-height:1.7;font-size:1rem;}
/* The audit called the font-size below a no-op "because it already equals
   --fs-h2-sm". It is not: .prose is never inside a .section-compact (checked
   all six views that use it), so removing it would inherit h2{--fs-h2} and
   jump every article heading from 22px to 24-30px inside an 820px reading
   column. Kept deliberately. Only the MARGINS changed here. */
.prose h2{font-size:var(--fs-h2-sm);margin:2.25rem 0 .6rem;}
.prose h3{margin:2rem 0 .5rem;}
.prose p{margin:0 0 1.35em;}
.prose ul,.prose ol{margin:0 0 1.35em;padding-left:1.25rem;}
.prose li{margin:.35rem 0;}
.prose a{color:var(--blue);font-weight:600;text-decoration:underline;}
.prose > :last-child{margin-bottom:0;}
/* The first-child reset is what makes the heading margins above safe: .legal
   sits inside a card at about.blade.php:183 whose FIRST element is an h2, and
   .prose does the same at study-in.blade.php:32 and partials/guide-body:26.
   Without it those cards gain 2.25rem of dead space at the top. */
.prose > :first-child,.legal > :first-child{margin-top:0;}
.prose,.legal{line-height:1.7;}
/* The measure cap covered <p> only, so on /privacy an <li> ran ~91 characters
   against the paragraph's ~68 — most of the reading text sat outside the rule
   written to protect it. Capping the LIST, not the li, keeps the right edge. */
.prose ul,.prose ol,.prose blockquote,.legal ul,.legal ol{max-width:36em;}

/* FAQ (schema) */
.faq-list{display:flex;flex-direction:column;gap:8px;}
.faq-item{border:1px solid var(--border);border-radius:12px;overflow:hidden;}
.faq-q{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--surface);
  border:0;padding:14px 16px;font:inherit;font-weight:700;font-size:1rem;text-align:left;cursor:pointer;color:var(--text);}
.faq-item.is-open .faq-q{color:var(--blue);}
.faq-caret{flex:none;color:var(--muted);transition:transform .18s ease;}
.faq-item.is-open .faq-caret{transform:rotate(90deg);color:var(--blue);}
.faq-a{padding:0 16px 14px;}

/* news */
.news-row{display:flex;align-items:center;gap:14px;}
.news-cat{flex:none;font-size:var(--fs-xs);font-weight:800;text-transform:uppercase;letter-spacing:.03em;padding:4px 10px;border-radius:22px;align-self:flex-start;}
.news-visa{background:var(--blue-100);color:var(--blue);}
.news-policy{background:#FAEBDE;color:var(--orange-600);}
.news-deadline{background:#F6E5E4;color:var(--danger);}
.news-update{background:var(--surface-2);color:var(--muted);border:1px solid var(--border);}

/* ---- Section F — loan consent ---- */
.consent-row{display:flex;gap:10px;align-items:flex-start;font-size:.8rem;color:var(--muted);cursor:pointer;padding:2px 0;line-height:1.45;}
.consent-row input{margin-top:2px;flex:none;width:18px;height:18px;accent-color:var(--blue);}

/* ===================================================================
   Section H/I — Communication, consent, chatbot, i18n
   =================================================================== */
/* language switcher */
.lang-switch{position:relative;}
.lang-menu{position:absolute;top:calc(100% + 6px);right:0;background:var(--surface);border:1px solid var(--border);
  border-radius:12px;box-shadow:var(--shadow-lg);padding:6px;min-width:132px;z-index:60;}
.lang-menu a{display:block;padding:8px 12px;border-radius:8px;color:var(--text);font-size:var(--fs-sm);}
.lang-menu a:hover{background:var(--surface-2);}
.lang-menu a.on{color:var(--blue);font-weight:700;background:var(--blue-100);}
.foot-lang{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;}
.foot-lang a{font-size:.8rem;color:rgba(255,255,255,.75);padding:3px 8px;border:1px solid rgba(255,255,255,.16);border-radius:16px;}
.foot-lang a.on{color:#fff;border-color:rgba(255,255,255,.5);}

/* consent banner */
.consent-banner{position:fixed;left:0;right:0;bottom:0;z-index:80;background:var(--ocean);color:#fff;padding:14px 16px;box-shadow:0 -6px 24px rgba(0,0,0,.22);}
/* The rule that hid the two bottom-corner FABs under the consent banner used to
   be here. There are no bottom-corner FABs any more, so there is nothing to hide
   and nothing focusable under the overlay. */
@media(max-width:899px){ .consent-banner{bottom:calc(var(--nav-h) + env(safe-area-inset-bottom));} }
/* Reserve the height, don't just span the width.
   FIXED_ALLOWED in PageStructureTest exempts this banner as "full width; the
   page is gated until a choice is made". Only the first half was true. Nothing
   gates the page — every control outside the banner stays clickable — and
   nothing reserved its height, so it covered whatever was at the bottom of the
   viewport: a .faq-q button at 1280, 153px of the destination grid at 375.
   Full width only stops it landing BESIDE a control.
   --consent-h is written by the banner itself (partials/consent-banner) from its
   own offsetHeight and re-measured on reflow, so it cannot drift from the real
   height the way the hardcoded 78px FAB offset drifted from --sticky-h. It is
   absent until the banner is up, hence the 0px fallback.
   Specificity 0,1,2 clears the plain `body` rules at both breakpoints. */
html.vb-consent-pending body{padding-bottom:calc(var(--consent-h, 0px) + var(--nav-h) + env(safe-area-inset-bottom));}
@media(min-width:900px){ html.vb-consent-pending body{padding-bottom:var(--consent-h, 0px);} }
.consent-inner{max-width:var(--maxw);margin:0 auto;display:flex;align-items:center;gap:16px;flex-wrap:wrap;justify-content:space-between;}
.consent-text{font-size:.875rem;margin:0;flex:1;min-width:220px;color:rgba(255,255,255,.9);}
.consent-text a{color:#fff;text-decoration:underline;}
.consent-actions{display:flex;gap:8px;flex:none;}
.consent-actions .btn{padding:.5rem 1rem;font-size:.875rem;}
.consent-actions .btn-ghost{background:transparent;color:#fff;border-color:rgba(255,255,255,.4);}

/* security badges (footer) */
.sec-badges{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:20px;padding-top:16px;border-top:1px solid rgba(255,255,255,.12);}
.sec-badges span{display:inline-flex;align-items:center;gap:6px;font-size:.8rem;color:rgba(255,255,255,.72);}

/* 24/7 chatbot */
/* The chatbot's floating button is deleted with the contact hub, for the same
   reason and not as a side effect. Measured: display:none at 390px, so it was
   never a mobile affordance at all; display:grid at 1280 and 1440, where its rect
   (x16-68) sat 25px inside the container's left edge — the hub's defect,
   mirrored into the other corner. The panel below opens from the drawer's "Ask a
   question" via the vb-open-chat event, at every width. */
.chatbot-panel{position:fixed;left:16px;bottom:calc(var(--nav-h) + 78px + env(safe-area-inset-bottom));z-index:46;
  width:min(340px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border);border-radius:16px;
  box-shadow:var(--shadow-lg);overflow:hidden;display:flex;flex-direction:column;max-height:70vh;}
@media(min-width:900px){ .chatbot-panel{bottom:84px;} }
.chatbot-head{display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--blue);color:#fff;}
.chatbot-head b{color:#fff;}
.chatbot-head .chat-avatar{background:rgba(255,255,255,.2);}
.chatbot-head .chat-status.is-online{color:#c8f5dd;}
.chatbot-x{margin-left:auto;width:44px;height:44px;display:grid;place-items:center;background:none;border:0;color:#fff;font-size:1rem;cursor:pointer;}
.chatbot-body{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:8px;background:var(--bg);}
.chatbot-body .bubble{max-width:88%;}
.chatbot-link{display:block;margin-top:6px;font-weight:700;color:var(--blue);font-size:.875rem;}
.bubble.out .chatbot-link{color:#fff;text-decoration:underline;}
.chatbot-quick{display:flex;gap:6px;padding:8px 10px;flex-wrap:wrap;border-top:1px solid var(--border);background:var(--surface);}
.chatbot-quick button,.chatbot-quick a{font-size:.8rem;font-weight:700;padding:5px 10px;border-radius:22px;
  background:var(--surface-2);border:1px solid var(--border);color:var(--blue);cursor:pointer;text-decoration:none;}
.chatbot-input{display:flex;gap:6px;padding:10px;border-top:1px solid var(--border);background:var(--surface);}
.chatbot-input input{flex:1;border:1.5px solid var(--border-strong);border-radius:var(--radius-pill);padding:.6rem 1rem;font:inherit;font-size:var(--fs-sm);}
.chatbot-input button{width:40px;height:40px;border-radius:50%;background:var(--blue);color:#fff;border:0;display:grid;place-items:center;cursor:pointer;flex:none;}

/* ---- #1 lead-gen: live SOP counter + review upsell ---- */
.sop-counter{display:inline-flex;align-items:center;gap:8px;background:var(--blue-100);border:1px solid var(--border);
  color:var(--blue);padding:8px 14px;border-radius:var(--radius-pill);font-size:.875rem;font-weight:600;}
.sop-counter b{font-weight:800;}
.sop-counter-dot{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 3px rgba(31,138,91,.22);flex:none;
  animation:cbpulse 2s ease-out infinite;}
/* The other white-on-translucent-white panel on this hero, and it sits a few
   lines under .hero-cta-ghost on the home page. It was left on the construction
   that rule was just rewritten to escape: an rgba(255,255,255,.14) panel lightens
   the ground under its own white ink, so the counter measured the same 4.36:1 on
   the gradient's lightest stop and 3.62:1 under the .hero::after glow — under the
   4.5:1 bar at .86rem/600. Both are also invisible to the sweep in
   ColourContrastTest, which excludes white-on-translucent-white because it cannot
   know the backdrop.
   Same answer, same token family: an opaque --ocean ground carries white at
   14.48:1 anywhere on the hero. The fill matches the gradient's own dark end, so
   at that end it is the border that has to carry the pill's shape — at .24 it was
   2.11:1 against its own fill; .6 measures 6.07:1. */
.hero .sop-counter{background:var(--ocean);border-color:rgba(255,255,255,.6);color:#fff;}

/* ===================================================================
   Section 4 — Mobile-only app-feel (PWA shell + thumb-zone)
   =================================================================== */
/* 6/9 — SPA navigation progress bar */
.vb-navbar{position:fixed;top:0;left:0;height:3px;width:0;z-index:100;background:linear-gradient(90deg,var(--blue),var(--orange));opacity:0;}
.vb-navbar.go{width:85%;opacity:1;transition:width 8s cubic-bezier(.1,.7,.1,1);}
.vb-navbar.done{width:100%;opacity:1;transition:width .2s;}

/* 5 — pull-to-refresh */
.vb-ptr{position:fixed;top:6px;left:50%;margin-left:-18px;width:36px;height:36px;border-radius:50%;background:var(--surface);
  box-shadow:var(--shadow);display:grid;place-items:center;z-index:60;opacity:0;transform:translateY(-44px);pointer-events:none;}
.vb-ptr-spin{width:18px;height:18px;border:2.5px solid var(--border);border-top-color:var(--blue);border-radius:50%;}
.vb-ptr.ready .vb-ptr-spin{animation:spin .7s linear infinite;border-top-color:var(--orange);}

/* 7 — offline status toast */
.vb-conn{position:fixed;top:calc(58px + env(safe-area-inset-top));left:0;right:0;z-index:70;display:none;background:var(--ocean);color:#fff;text-align:center;font-size:.8rem;padding:8px 14px;}
.vb-conn.show{display:block;}

/* The install and push banners were .vb-appbanner: position:fixed, 366px wide
   in a 390px viewport, reserving nothing, revealed on a 25-second timer. Measured
   on /get-started, the push banner overlapped a control at 23 of 27 sampled
   scroll positions. Both are now buttons in the drawer (partials/mobile-menu),
   which is reader-invoked and displaces nothing.
   .vb-x stays: the geo and consent banners still use it. */
.vb-x{background:none;border:0;color:var(--muted);font-size:var(--fs-sm);cursor:pointer;flex:none;padding:4px;}

/* Counselling lives in the header CTA — .header-cta, in the sticky header,
   measured on screen at 289 of 289 sampled scroll positions across five pages at
   390px and 1280px, and reachable in one tap. Call, WhatsApp and the chat live in
   the drawer behind the burger. Nothing contact-related floats. */

/* 11/14 — sticky bottom action bar on tool/result pages */
/* The bar's height is a TOKEN, not a number repeated at each call site. It used
   to be implicit: the buttons were pill-shaped and free to wrap, so a label like
   "Talk to a counsellor — free" broke to three lines in its ~146px half and the
   bar grew to ~102px — while the FAB above it was offset by a hardcoded 78px.
   The two numbers disagreed, so the FAB physically overlapped the button and
   both were tappable in the same place. Now the buttons cannot wrap (so the
   height is fixed and knowable) and everything above the bar clears
   --sticky-h, which means the offset can never drift from the real height. */
:root{--sticky-h:68px;}
.sticky-actions{position:fixed;left:0;right:0;bottom:var(--nav-h);z-index:44;background:var(--surface);border-top:1px solid var(--border);
  box-shadow:var(--shadow-up);padding:10px 12px;padding-bottom:calc(10px + env(safe-area-inset-bottom));}
.sticky-actions-in{max-width:var(--maxw);margin:0 auto;display:flex;gap:10px;align-items:stretch;}
/* Not pills. A 999px radius on a full-width bar button is what produced the
   "balloon" read; 14px matches the cards and inputs around it. One line only —
   a truncated label is recoverable, a 3-line oval is not. */
.sticky-actions .btn{flex:1;min-height:48px;border-radius:16px;font-size:1rem;padding:.6rem 1rem;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:inline-flex;}
/* One primary per screen: the secondary is a quiet outline that takes only the
   width it needs, so the orange CTA is unambiguously THE action. */
.sticky-actions .btn-ghost{flex:0 1 auto;background:var(--surface);border:1px solid var(--border-strong);color:var(--text);}
body:has(.sticky-actions) main{padding-bottom:calc(var(--sticky-h) + 16px);}
/* the bar is fixed over the whole page, and <footer> is main's sibling — without
   this the last ~120px of the footer sat permanently underneath it */
body:has(.sticky-actions) footer{padding-bottom:calc(var(--sticky-h) + 32px);}
/* The FABs used to be lifted above this bar by two more offsets that had to be
   kept in step with --sticky-h. Both FABs are gone, so the bar is now the only
   thing in the bottom stack and the only offset is the one it reserves itself
   (main and footer, just above). One number, one owner. */
@media(min-width:900px){ .sticky-actions{bottom:0;} }

/* 10 — haptic-style visual press feedback */
a:active,button:active,[role="button"]:active{opacity:.72;}
.btn:active,.card:active,.dcard:active,.header-cta:active,.tabbar a:active,.dtab:active,.seg-btn:active{transform:scale(.97);opacity:1;}
.tabbar a,.card{transition:transform .09s ease,box-shadow .16s ease;}

/* 15 — min 44x44px tap targets */
.icon-btn{width:44px;height:44px;}
.tabbar a{min-height:54px;}
.faq-q,.check-item,.slot,.seg-btn,.doctab,.fmt{min-height:44px;}

/* Tap-target stragglers found by sweeping the RENDERED pages rather than
   reading the stylesheet. Each is fixed on its own original rule further down
   this file rather than here — a patch block at this point in the cascade loses
   to those later rules on source order, which is exactly what happened on the
   first attempt: the declarations were present and had no effect.
   Section-header links grow via padding plus a matching negative margin, so the
   hit area reaches 44px without shifting anything on the page. */
/* Every .link, not just the ones in a section head.
   This technique was already here and was doing its job — pad the target out,
   pull the margin back by the same amount so nothing moves — but only for
   .sec-head .link. Meanwhile /loans renders 49 "Official source ↗" links at
   101x15, one per lender card, and every "Explore all →" and "Why Vibedu →" on
   the site was the same 15px-tall target. 15px of padding takes them to 45.
   These are STANDALONE action links, which is why this is safe: of 59 uses,
   none sits mid-sentence — the one inside a <p> ("Back to sign in") is the only
   thing in it. A prose link would get an overlapping hit area and should not
   carry this class. */
.link{padding:15px 0;margin:-15px 0;display:inline-flex;align-items:center;}
/* .btn:disabled does not reach here. The OTP "Resend code" control in the AI course finder
   is a <button class="link">, carries wire:loading.attr="disabled", and had no disabled
   rule anywhere in this file — so the one control a student is most likely to tap twice
   was the one that acknowledged nothing. */
.link:disabled,.link[disabled]{opacity:.55;cursor:not-allowed;}

/* 16 — no hover-dependent behaviour: neutralise hover transforms on touch devices */

/* 19 — bottom-sheet modals on mobile (native pattern) */
@media(max-width:640px){
  .modal-overlay{align-items:flex-end;padding:0;}
  .modal{max-width:100%;width:100%;border-radius:22px 22px 0 0;padding:22px 18px calc(22px + env(safe-area-inset-bottom));animation:sheetUp .28s cubic-bezier(.2,.9,.3,1);}
}
@keyframes sheetUp{from{transform:translateY(100%)}to{transform:none}}

/* utilities */
.mt{margin-top:1rem;} .mt2{margin-top:1.6rem;} .hide-desktop{}
@media(min-width:900px){ .hide-desktop{display:none;} }

/* ===== Section 4 (items 21–40): one-question wizard, swipe deck, share ===== */

/* soft secondary button (share) */
.btn-soft{background:var(--surface-2);color:var(--blue);border:1px solid var(--border);}
.btn-soft:hover{background:#eaf1fb;}
.linkbtn{background:none;border:0;padding:0;font:inherit;color:var(--blue);font-weight:700;cursor:pointer;text-decoration:underline;}
.cb-known{font-size:.8rem;color:var(--muted);text-align:center;margin:.55rem 0 0;}

/* vertical (stacked) segmented control */
/* display:flex, NOT the .seg inline-flex above: an inline-flex column shrink-wraps
   to its widest child, so the width:100% on .seg-col .seg-btn below resolved to the
   text width and two "full-width" buttons rendered at 118px. */
.seg-col{display:flex;flex-direction:column;gap:.5rem;background:none;padding:0;}
.seg-col .seg-btn{width:100%;text-align:left;border:1.5px solid var(--border);border-radius:16px;padding:.85rem 1rem;background:var(--surface);}
.seg-col .seg-btn.on{border-color:var(--blue);background:#eaf1fb;color:var(--blue);}

/* one-question-per-screen wizard (items 28/32/33) */
.wiz-top{display:flex;align-items:center;gap:.6rem;margin-bottom:1.1rem;}
.wiz-back{flex:0 0 auto;width:36px;height:36px;border-radius:50%;border:1px solid var(--border);background:var(--surface);color:var(--ink);display:grid;place-items:center;cursor:pointer;transform:scaleX(-1);}
.wiz-back.is-off{opacity:.35;pointer-events:none;}
.wiz-prog{flex:1;height:7px;border-radius:var(--radius-pill);background:var(--surface-2);overflow:hidden;}
.wiz-prog span{display:block;height:100%;border-radius:var(--radius-pill);background:linear-gradient(90deg,var(--blue),var(--orange));transition:width .3s ease;}
.wiz-count{flex:0 0 auto;font-weight:800;font-size:.8rem;color:var(--muted);font-variant-numeric:tabular-nums;}
.wiz-body{min-height:190px;}
.wiz-swipe-hint{text-align:center;font-size:.8rem;color:var(--muted);margin:.7rem 0 0;}

/* Tinder-style swipe deck (item 37) */
.swipe-stage{position:relative;height:340px;user-select:none;touch-action:pan-y;}
.swipe-card{position:absolute;inset:0;background:var(--surface);border:1px solid var(--border);border-radius:22px;
    box-shadow:0 12px 30px rgba(2,32,71,.12);overflow:hidden;display:flex;flex-direction:column;will-change:transform;cursor:grab;}
.swipe-card:active{cursor:grabbing;}
.swipe-flag{height:44%;overflow:hidden;}
.swipe-flag .flag{width:100%;height:100%;object-fit:cover;display:block;}
.swipe-body{padding:1.1rem 1.2rem 1.3rem;display:flex;flex-direction:column;gap:.25rem;}
.swipe-field{font-weight:800;font-size:1.25rem;line-height:1.15;}
.swipe-country{font-weight:700;color:var(--blue);}
.swipe-hook{color:var(--muted);font-size:var(--fs-sm);margin:.4rem 0 0;line-height:1.4;}
/* Not a text size: a rotated "stamp" overlay, sized optically against its border. */
.swipe-stamp{position:absolute;top:18px;font-weight:800;font-size:1.06rem;letter-spacing:.06em;padding:.35rem .7rem;border-radius:12px;border:3px solid;pointer-events:none;}
.swipe-yes{left:16px;color:#0f9d58;border-color:#0f9d58;transform:rotate(-12deg);}
.swipe-no{right:16px;color:#e0463e;border-color:#e0463e;transform:rotate(12deg);}
.swipe-controls{display:flex;align-items:center;justify-content:center;gap:1.4rem;margin-top:1.1rem;}
.swipe-progress{font-weight:800;color:var(--muted);font-variant-numeric:tabular-nums;min-width:44px;text-align:center;}
.swipe-btn{width:58px;height:58px;border-radius:50%;border:1px solid var(--border);background:var(--surface);display:grid;place-items:center;cursor:pointer;box-shadow:0 6px 16px rgba(2,32,71,.1);}
.swipe-btn-yes{color:#0f9d58;border-color:#bce7cd;}
.swipe-btn-no{color:#e0463e;border-color:#f3c9c6;}
.swipe-btn:active{transform:scale(.92);}

/* ===== Section 5 (items 41–70): trust, personalization & speed UI ===== */

/* 42/49 — header reviews chip */
.hdr-reviews{position:relative;color:#ffd66b;}
.hdr-rating{position:absolute;bottom:-3px;right:-3px;background:#fff;color:var(--blue);font-size:var(--fs-xs);font-weight:800;line-height:1;padding:2px 3px;border-radius:8px;box-shadow:var(--shadow-sm);}

/* 41/42 — above-the-fold hero trust */
.hero-trust{display:flex;flex-wrap:wrap;gap:10px;margin-top:14px;}
.hero-trust .ht-item{display:flex;flex-direction:column;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);
  border-radius:16px;padding:.5rem .8rem;color:#fff;min-width:96px;}
.hero-trust .ht-item b{font-size:1.25rem;line-height:1;font-weight:800;}
.hero-trust .ht-item small{font-size:var(--fs-xs);color:rgba(255,255,255,.82);margin-top:3px;}
.hero-trust .ht-google b .gstars{color:#ffd66b;}
.hero-trust .ht-google{text-decoration:none;}

/* 46 — honest live-activity badge */
.live-activity{display:inline-flex;align-items:center;gap:.5rem;margin-top:12px;background:rgba(255,255,255,.12);
  border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:var(--radius-pill);padding:.4rem .85rem;font-size:.8rem;font-weight:600;}
.la-dot{width:8px;height:8px;border-radius:50%;background:#38d39f;box-shadow:0 0 0 0 rgba(56,211,159,.6);animation:laPulse 1.8s infinite;}
@keyframes laPulse{0%{box-shadow:0 0 0 0 rgba(56,211,159,.6)}70%{box-shadow:0 0 0 8px rgba(56,211,159,0)}100%{box-shadow:0 0 0 0 rgba(56,211,159,0)}}

/* 44 — visa-success banner */
.visa-banner{background:linear-gradient(120deg,var(--ocean),var(--blue));color:#fff;}
.visa-banner-in{display:flex;align-items:center;gap:18px;padding:16px 18px;flex-wrap:wrap;}
.visa-stat{display:flex;flex-direction:column;line-height:1;}
.visa-stat b{font-size:2.2rem;font-weight:800;color:var(--summer-ink);}
.visa-stat span{font-size:.8rem;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,.85);margin-top:4px;}
.visa-copy{flex:1;min-width:200px;}
.visa-copy p{margin:0;color:rgba(255,255,255,.92);}
.visa-link{display:inline-flex;align-items:center;gap:.3rem;color:#fff;font-weight:700;margin-top:4px;}

/* 43 — testimonial reels (Instagram-Reel style, swipeable) */
.reel-row{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;padding:6px 2px 12px;}
.reel-row::-webkit-scrollbar{display:none;}
/* Same affordance and the same :focus-within reasoning as .exam-slider above —
   here the focusable child is each card's .reel-mute button, which sits at
   right:10px and is a control under WCAG 1.4.11 (3:1). At the rail's right edge
   its trailing 14px sat in the fade at alpha .42 -> 1.0, i.e. a decorative
   gradient dissolving a functional control. */
.reel-row:not(:focus-within){
  -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 24px),transparent);
          mask-image:linear-gradient(90deg,#000 calc(100% - 24px),transparent);}
.reel-card{position:relative;flex:0 0 auto;width:200px;aspect-ratio:9/16;border-radius:22px;overflow:hidden;scroll-snap-align:start;
  background:#0b1a2e;box-shadow:var(--shadow);}
.reel-card video{width:100%;height:100%;object-fit:cover;display:block;}
/* 44, not 40 — the file's own floor. Seven of these sit on the homepage, and
   they were the single largest group of under-target controls on it. */
.reel-mute{position:absolute;top:10px;right:10px;width:44px;height:44px;border-radius:50%;border:0;background:rgba(0,0,0,.45);color:#fff;display:grid;place-items:center;cursor:pointer;}
/* The muted state is a white glyph on this fill; --orange left it at 2.79:1,
   under the 3:1 an icon-only control has to make. */
.reel-mute.on{background:var(--orange-600);}
.reel-cap{position:absolute;left:0;right:0;bottom:0;padding:22px 12px 10px;background:linear-gradient(transparent,rgba(0,0,0,.75));color:#fff;}
.reel-cap b{display:block;font-size:var(--fs-sm);}
.reel-cap small{color:rgba(255,255,255,.8);font-size:.8rem;}

/* 45 — counsellor chip above CTA */
.counsellor-chip{display:flex;align-items:center;gap:10px;background:var(--surface-2);border:1px solid var(--border);
  border-radius:16px;padding:8px 12px;margin-bottom:12px;}
.cc-photo{position:relative;width:40px;height:40px;border-radius:50%;background:var(--blue-100);color:var(--blue);display:grid;place-items:center;flex:0 0 auto;overflow:hidden;}
.cc-photo img{width:100%;height:100%;object-fit:cover;border-radius:50%;}
.cc-online{position:absolute;bottom:0;right:0;width:11px;height:11px;border-radius:50%;background:#38d39f;border:2px solid #fff;}
.cc-meta b{display:block;font-size:var(--fs-sm);}
.cc-meta small{color:var(--muted);font-size:.8rem;}

/* 48 — trust badges under submit */
.form-badges{display:flex;flex-wrap:wrap;gap:6px 14px;justify-content:center;margin-top:12px;}
.form-badges span{display:inline-flex;align-items:center;gap:4px;font-size:var(--fs-xs);font-weight:600;color:var(--muted);}
.form-badges span .ic{color:var(--ok);}

/* 50 / 70 — swipeable card rows built on .review-row */
.cs-row,.recent-row{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;padding-bottom:10px;}
.cs-row::-webkit-scrollbar,.recent-row::-webkit-scrollbar{display:none;}
.cs-card{flex:0 0 84%;max-width:340px;scroll-snap-align:start;}
@media(min-width:640px){ .cs-card{flex:0 0 320px;} }
.recent-card{flex:0 0 200px;scroll-snap-align:start;padding:14px;display:flex;flex-direction:column;gap:4px;}
.recent-card b{font-size:var(--fs-sm);line-height:1.2;}

/* 66 — pinned intake countdown */
.vb-countdown{position:sticky;top:58px;z-index:39;background:linear-gradient(90deg,var(--ocean),var(--blue));color:#fff;
  box-shadow:0 2px 10px rgba(0,20,50,.18);}
.vb-cd-in{display:flex;align-items:center;gap:12px;padding:8px 18px;font-size:.875rem;}
.cd-badge{display:inline-flex;align-items:center;gap:5px;font-weight:800;white-space:nowrap;}
.cd-time{font-weight:800;color:var(--summer-ink);font-variant-numeric:tabular-nums;white-space:nowrap;}
.cd-note{flex:1;color:rgba(255,255,255,.85);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* 44 and a 12px radius, matching .header-cta directly above it in the same
   sticky stack — two buttons a few pixels apart were 40px pills against 44px
   rounded rectangles. */
.cd-cta{background:#fff;color:var(--blue);font-weight:800;padding:.3rem .9rem;border-radius:12px;white-space:nowrap;font-size:.8rem;min-height:44px;display:inline-flex;align-items:center;}
@media(max-width:640px){ .cd-note{display:none;} .vb-cd-in{gap:8px;} }

/* 61 — geo nearest-branch banner */
.vb-geo{display:none;align-items:center;gap:10px;background:var(--blue-100);color:var(--blue);padding:8px 16px;font-size:.875rem;border-bottom:1px solid var(--border);}
.vb-geo.show{display:flex;}
.vb-geo span{flex:1;}
.vb-geo-call,.vb-geo-link{font-weight:700;white-space:nowrap;}
.vb-geo-x{margin-left:2px;background:none;border:0;color:var(--muted);font-size:1rem;cursor:pointer;line-height:1;}

/* The WhatsApp preview card was here: position:fixed in the bottom-right corner,
   270px, desktop-only — the deleted contact hub's exact footprint, carrying its
   own private copy of the overlap solver. It offered a WhatsApp conversation the
   drawer and the footer already carry on every page. */

/* 58 — save-data / low-end: kill non-essential motion */
.vb-saver .la-dot,.vb-saver .cb-live{animation:none;}
.vb-saver .reel-card video{display:none;}
.vb-saver *{transition-duration:.01ms !important;}
.job-updated{display:inline-flex;align-items:center;gap:6px;font-size:.8rem;font-weight:600;color:var(--muted);}
.job-cos-lbl{display:block;font-size:var(--fs-xs);color:var(--muted);margin-bottom:4px;width:100%;}

/* ===== §9-D speaking practice mic button ===== */
.mic-btn{width:76px;height:76px;border-radius:50%;border:0;background:var(--blue);color:#fff;display:grid;place-items:center;cursor:pointer;box-shadow:0 10px 26px rgba(36,79,148,.35);transition:.15s;}
.mic-btn:active{transform:scale(.94);}
.mic-btn.rec{background:var(--danger);animation:micPulse 1.2s infinite;}
@keyframes micPulse{0%{box-shadow:0 0 0 0 rgba(192,72,60,.5)}70%{box-shadow:0 0 0 16px rgba(192,72,60,0)}100%{box-shadow:0 0 0 0 rgba(192,72,60,0)}}
.mic-btn[disabled]{opacity:.5;pointer-events:none;}

/* ===== §H listening player ===== */
.listen-bar{display:flex;align-items:center;gap:12px;margin-top:10px;flex-wrap:wrap;}
.listen-status{font-size:.8rem;font-variant-numeric:tabular-nums;}

/* ===== §10E unified trust badge (reused across scholarships/jobs/scores/universities) ===== */
.trust-badge{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);font-weight:700;
  padding:3px 9px;border-radius:var(--radius-pill);line-height:1.2;white-space:nowrap;}
.trust-badge .tb-date{font-weight:600;opacity:.85;}
.trust-badge.tb-ok{background:#E4F1EB;color:var(--ok);}
.trust-badge.tb-live{background:#E4F1EB;color:var(--ok);}
.trust-badge.tb-live .ic{color:#38d39f;animation:laPulse 1.8s infinite;border-radius:50%;}
.trust-badge.tb-ind{background:#F6ECE0;color:var(--warn);}

/* ===== §10 scholarship card trust footer ===== */
.schol-title{color:inherit;text-decoration:none;display:block;}
.schol-title h3:hover{color:var(--blue);}
.schol-foot{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:1px solid var(--border);}
.schol-src{font-size:.8rem;font-weight:700;}
.schol-more{margin-left:auto;font-size:.8rem;font-weight:700;color:var(--blue);text-decoration:none;}
.status-open{background:#E4F1EB;color:var(--ok);}
.status-soon{background:#F6ECE0;color:var(--warn);}
.status-closed{background:var(--surface-2);color:var(--muted);}
.status-neutral{background:var(--surface-2);color:var(--muted);}
.lender-facts li{font-size:.875rem;display:flex;align-items:flex-start;gap:6px;margin:2px 0;}
.lender-facts li .ic{color:var(--blue);flex:0 0 auto;margin-top:2px;}

/* ===== Additional-services: footer legal row + legal content pages ===== */
.foot-legal{display:flex;flex-wrap:wrap;gap:6px 16px;justify-content:center;padding:14px 0 4px;border-top:1px solid rgba(255,255,255,.12);margin-top:16px;}
.foot-legal a{color:rgba(255,255,255,.72);font-size:.8rem;padding:6px 2px;display:inline-block;}
.foot-legal a:hover{color:#fff;}
.legal h2{font-size:var(--fs-h2-sm);margin:2.25rem 0 .6rem;}  /* was a 20px off-scale heading */
.legal ul{padding-left:1.2rem;} .legal li{margin:.35rem 0;color:var(--muted);}
.legal p{color:var(--muted);margin:0 0 1.35em;} .legal > :last-child{margin-bottom:0;}
.legal .notice{padding:10px 12px;border-radius:12px;margin:12px 0;font-size:.875rem;}

/* ============ §E Community hub (items 35–42) ============ */
.section-alt{background:var(--surface);}

/* ---------- Section grounds (palette deployment) ----------
   The six-colour palette was in the tokens but not on the page: a count of the
   stylesheet found --blue used 111 times, --ocean 15, --ice ONCE and --summer
   never. With only white and --bg as grounds, every page was the same blue-on-
   pale stamp and the warmth the palette was chosen for never appeared.
   These are the two extra grounds. Use them to break up a long page — an ice
   band for a soft/explanatory section, a navy band for money, trust and premium
   content — never two of the same ground in a row. */
.section-ice{background:linear-gradient(180deg,var(--ice) 0%,#e4f4ff 100%);}
.section-ice + .section-ice{background:#e4f4ff;}
.section-ice .eyebrow{color:#0b4a63;}

/* Dark band. Everything inside inverts, because a section that only repainted
   its background left --text ink on navy at 1.6:1. Border and surface tokens are
   re-pointed for the subtree so cards dropped into the band inherit correctly
   instead of punching white holes in it. */
.section-navy{
  background:linear-gradient(160deg,var(--ocean) 0%,#063c62 100%);color:#fff;
  --text:#ffffff; --muted:rgba(255,255,255,.78); --border:rgba(255,255,255,.16);
  --surface:rgba(255,255,255,.07); --surface-2:rgba(255,255,255,.11);
}
/* The tint cycle must not reach in here. Its four grounds are light and this
   subtree re-points --text to white, so a tinted card in a navy band would be
   near-white text on a pale blue card. --surface is already the glass fill this
   section wants, and --muted goes back to the translucent white the dark ground
   needs. Same (0,2,0) as the cycle, declared after it, so this wins on order —
   which is only true while it stays below; see the .tool-c note for what happens
   when two equal-specificity rules drift apart in this file. */
.section-navy .grid > *,.section-navy .g2 > *,.section-navy .g3 > *,.section-navy .g4 > *{
  --card-bg:var(--surface); --muted:rgba(255,255,255,.78);}
.section-navy h1,.section-navy h2,.section-navy h3{color:#fff;}
.section-navy .eyebrow{color:var(--summer-ink);}
.section-navy a:not(.btn){color:var(--ice);}
.section-navy .card{backdrop-filter:blur(2px);}
/* The token block above re-points --text, --muted, --border and --surface for
   this dark subtree, but .stat .n and .stat .n .accent name --blue and
   --orange-600 DIRECTLY (both tuned for a light ground), so they ignored it.
   The result: the four headline trust numbers on the homepage — the only
   proof-of-scale above the footer — rendered brand-blue on navy at a measured
   1.48:1, i.e. four apparently empty boxes with captions under them, while the
   captions beside them sat at 7.87:1. Re-pointing the tokens is not enough when
   a selector reaches past them; these two have to be stated for the dark ground.
   --ice on this gradient measures ~9.5:1, --orange ~5.3:1. */
/* ---------- Explore panel (curated entry points) ----------
   A lede column beside a row of image cards on a tinted ground — the shape used
   for a curated selection on any mature catalogue site. The lede carries the
   heading and the escape hatch to the full list; each card carries a real photo,
   a live count and a working filter. */
.explore-panel{display:grid;gap:26px;}
@media(min-width:900px){
  .explore-panel{grid-template-columns:minmax(240px,1fr) 2.1fr;gap:36px;align-items:center;}
}
.explore-lede h2{margin:.2rem 0 .4rem;}
.explore-lede .btn{margin-top:14px;}
.explore-cards{display:grid;gap:14px;grid-template-columns:1fr;}
@media(min-width:560px){ .explore-cards{grid-template-columns:repeat(auto-fit,minmax(190px,1fr));} }

.explore-card{position:relative;overflow:hidden;display:flex;flex-direction:column;color:var(--text);
  transition:box-shadow .16s ease  /* transform dropped: it silently overrode .card's shared .09s press timing for these three cards only */;}
.explore-media{display:block;height:132px;background:var(--blue-100);position:relative;}
.explore-card.has-img .explore-media{
  background-image:linear-gradient(180deg,rgba(0,43,76,.10),rgba(0,43,76,.42)),var(--ex-img);
  background-size:cover;background-position:center;}
.explore-body{display:flex;flex-direction:column;gap:3px;padding:16px 18px 18px;}
.explore-body b{font-family:var(--font-display);font-size:var(--fs-h3);line-height:var(--lh-card);letter-spacing:-.015em;}
.explore-body small{color:var(--muted);font-size:.8rem;}
.explore-n{display:inline-flex;align-items:center;gap:6px;margin-top:10px;font-weight:700;font-size:.875rem;
  color:var(--blue);font-variant-numeric:tabular-nums;}
/* Names whose campus is in the picture. Small and quiet — it is a caption, not
   a claim about the whole band. It sits on the BOTTOM edge of the photo, which
   is where a reader looks for a caption; pinned to the card's top-left it read
   as a tag or a debug label, and the "Pictured:" prefix it needed to explain
   itself was the tell. In the right place it needs no prefix. */
.explore-who{position:absolute;bottom:10px;left:10px;max-width:calc(100% - 20px);
  font-size:var(--fs-xs);font-weight:600;color:#fff;
  background:rgba(2,18,42,.62);padding:4px 9px;border-radius:8px;backdrop-filter:blur(3px);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}

/* ---------- Tool cards ----------
   The index was fourteen identical tiles: same size, same weight, one differing
   word. Nothing looked more useful than anything else, and the flagship the
   whole site is built around sat as one tile among fourteen. Three changes —
   a live figure per card, a wider card for the flagship, and a grid that fills
   its last row instead of leaving three empty slots beside an orphan. */
.tool-stat{display:inline-flex;align-items:center;gap:5px;margin-top:auto;padding-top:10px;
  font-size:.8rem;font-weight:700;color:var(--blue-600);font-variant-numeric:tabular-nums;}
.section-ice .tool-stat{color:#0b4a63;}
.tool .go{padding-top:8px;}

/* auto-fit means the final row stretches to fill rather than stranding a lone
   card beside three empty columns, which is what made the page look unfinished */
@media(min-width:640px){
  .tool-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));}
}
@media(min-width:900px){
  .tool-featured{grid-column:span 2;}
  .tool-featured h3{font-size:var(--fs-h2-sm);}
  .tool-featured p{font-size:1rem;}
}

/* ---------- Sticky in-page section nav ----------
   Pinned under the header once the reader is past the hero, so a page that runs
   several thousand pixels can be navigated instead of only scrolled. Restrained
   on purpose — a rule, an active underline and nothing else — because it sits
   directly under a saturated blue header and a second coloured band there would
   read as chrome piled on chrome. Scrolls horizontally on a phone rather than
   wrapping to two rows and eating the viewport. */
.page-nav{position:sticky;top:58px;z-index:35;background:var(--surface);
  border-bottom:1px solid var(--border);box-shadow:0 4px 12px rgba(16,40,70,.06);}
@media(min-width:900px){ .page-nav{top:103px;} }
.page-nav-in{max-width:var(--maxw);margin:0 auto;padding:0 30px 0 16px;display:flex;gap:2px;
  overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;
  scroll-padding-right:30px;}
.page-nav-in::-webkit-scrollbar{display:none;}
/* The bar overflows and said nothing about it. Measured on /university/
   university-of-oxford at 390px: scrollWidth 523 against clientWidth 390, so
   25.4% of it sat off-screen — and the cut landed on a seam. "Programmes" ended
   at x=388.1 of 390, flush, so it read as the last item, while "Scholarships"
   began at x=390.1 and was never seen at rest. The two rules above switch off
   both scrollbars, which removed the last hint that anything was there.
   A fade over the trailing edge is the affordance: whatever is cut now looks
   cut. Its width matches the 30px of end padding added above, so at full scroll
   the fade lies entirely over that padding — measured 30.3px of clear space
   after the last item — and the item you scrolled to is never dimmed.
   scroll-padding-right must match that 30px or the KEYBOARD case breaks: focus
   scrolls with `nearest` alignment, which is the MINIMUM scroll, not the
   maximum. Measured without it, tabbing to "Scholarships" stopped at scrollLeft
   23 and parked the link 0.3px from the edge — its last 30px under the opaque
   end of the fade, taking the focus ring (outline + halo, painted in the link's
   own layer and therefore UNDER this pseudo-element) with it. With it, focus
   lands at scrollLeft 53 with 30.3px clear.
   It has to be a pseudo-element painted after the links, not a background: a
   background paints BEHIND the text and would fade nothing. Where the bar does
   not overflow it is off-content and therefore invisible — at 1280px the fade
   sits 20px past the centred 1180px inner box, over plain --surface, and a
   2-item bar (any university with no entry_requirements) reports
   scrollWidth === clientWidth === 390. .page-nav is already position:sticky, so
   it is the containing block; top/bottom:0 is the padding box, which leaves its
   1px border intact.
   In forced-colors mode Chrome drops the gradient outright, so this can never
   paint over forced text — the affordance is simply absent there. Do not "fix"
   that with a solid colour. */
.page-nav::after{content:"";position:absolute;top:0;bottom:0;right:0;width:30px;
  background:linear-gradient(90deg,transparent,var(--surface));pointer-events:none;}
.page-nav a{flex:none;padding:14px 14px 11px;font-size:.875rem;font-weight:700;color:var(--muted);
  white-space:nowrap;border-bottom:3px solid transparent;min-height:44px;display:inline-flex;align-items:center;}
.page-nav a:hover{color:var(--text);}
.page-nav a.is-on{color:var(--blue);border-bottom-color:var(--blue);}
/* Deliberately NOT setting scroll-behavior here. A global smooth-scroll on this
   site, combined with scroll-snap, previously produced a scroll loop that never
   settled and reported NO_LCP in the field. Anchor jumps stay instant.

   Anchored sections must clear everything pinned above them, or a jump lands
   with the heading hidden underneath. --page-nav-bottom is measured at runtime
   by the page-nav component (the sticky stack varies: the intake countdown is
   present on some pages and not others), with these as the fallback for pages
   that carry no jump bar at all. */
[id]{scroll-margin-top:var(--page-nav-bottom,118px);}
@media(min-width:900px){ [id]{scroll-margin-top:var(--page-nav-bottom,163px);} }

/* ---------- Hub page photo hero ----------
   The content hubs (/programs, /scholarships, /loans, /test-prep, /tools and the
   parent, community and get-started pages) opened on a centred block of text
   with no photograph anywhere — /loans ran 43,808px without a single image.
   These carry a subject-matched licensed photo behind the intro.

   Inverts through the same token re-pointing as .section-navy, so cards and
   copy dropped inside adapt instead of rendering dark-on-dark. The scrim is
   heavy on purpose: a hero has to stay readable over a photograph whose
   brightness we do not control, and legibility beats a prettier picture. */
.hub-hero{
  position:relative;color:#fff;padding:var(--pad-section) 0;  /* was the literal — see TokenLiteralTest */
  background-image:linear-gradient(160deg,rgba(4,26,52,.86) 0%,rgba(4,26,52,.62) 48%,rgba(0,43,76,.88) 100%),var(--hub-img);
  background-size:cover;background-position:center;
  --text:#ffffff;
  /* --muted was rgba(255,255,255,.82). Composited against the ACTUAL photo pixels
     under each hero's own text (image drawn at its real cover geometry, the 160deg
     scrim above painted over it, sampled at the top, middle and BOTTOM of every
     line box) the lead paragraph came out at 5.23:1 on /tools but 4.51:1 on /loans
     — one hundredth over the 4.5 bar, which is not a margin, it is a rounding
     accident. .94 lifts it clear without touching the photograph.
     The two inks that share this hero were measured at the same time and both pass
     as they are, so they are deliberately left alone: .hub-hero .eyebrow
     (--summer-ink) reads 10.89 on /tools and 5.27 on /loans, and
     .hub-hero a:not(.btn) (--ice) renders on neither page.
     The SCRIM is deliberately not touched. It already retains 0.35-0.38 of each
     photograph's contrast range — the homepage hero retained 0.07 before it was
     fixed — so these heroes read as photographs already, and lightening them buys
     ~0.04 more retention while pushing this same lead back under AA. The copy here
     is centred and spans 20-80% of the width, so there is no dark column to hide
     it in either. Ink, not scrim. */
  --muted:rgba(255,255,255,.94); --border:rgba(255,255,255,.20);
  --surface:rgba(255,255,255,.08); --surface-2:rgba(255,255,255,.12);
}
.hub-hero h1,.hub-hero h2,.hub-hero h3{color:#fff;}
.hub-hero .eyebrow{color:var(--summer-ink);}
.hub-hero a:not(.btn){color:var(--ice);}
/* .btn-ghost names --blue directly for its label, which is a light-ground ink
   and disappears here — the same class of bug as the primary CTA taking a
   themeable --text into a navy band. */
.hub-hero .btn-ghost{color:#fff;border-color:rgba(255,255,255,.45);}
.hub-hero .btn-ghost:hover{border-color:#fff;}

.section-navy .stat .n{color:var(--ice);}
/* --summer-ink, not --orange: this file already defines --summer-ink as the
   accent INK for dark grounds (the mirror of --orange-600 for light ones), and a
   guard test enforces that --orange stays a fill. It measures 11.78:1 on
   --ocean, so it is also the better answer here, not merely the permitted one. */
.section-navy .stat .n .accent{color:var(--summer-ink);}
/* Home trust band: orange tiles, white type — the owner's call. Not the brand
   orange: white on #E08431 is 2.79:1, which is why .btn-primary carries dark ink.
   These are the deep end of the same hue, measured against white: #C84B0F 4.70:1,
   #A93B08 6.33:1 — AA at both stops, so the 12.8px caption holds too. */
.stat.stat-orange{background:linear-gradient(145deg,#C84B0F 0%,#A93B08 100%);border-color:transparent;box-shadow:0 10px 24px rgba(110,36,0,.30);}
.section-navy .stat-orange .n,.stat.stat-orange .n{color:#fff;}
.stat.stat-orange .l{color:#fff;}

/* ---------- AI Course Finder card (home, top of Free Tools) ----------
   The finder was the hero, then its own section; it lives with the free tools
   now. Same brand blue as the first screen, so it reads as the flagship among
   the white tool cards rather than as a fifth one. */
.finder-card{display:flex;flex-direction:column;gap:16px;margin-bottom:18px;padding:clamp(18px,3vw,28px);border-radius:var(--radius);color:#fff;
  background:radial-gradient(60% 90% at 92% 0%,rgba(110,185,255,.35),transparent 70%),linear-gradient(150deg,#0766bd 0%,#005baa 55%,#00488a 100%);
  box-shadow:var(--shadow-lg);}
.finder-card-head{display:flex;gap:14px;align-items:flex-start;}
.finder-card-ic{width:48px;height:48px;border-radius:14px;display:grid;place-items:center;flex:none;background:#fff;color:#005baa;}
.finder-card h3{margin:0;color:#fff;font-weight:800;}
.finder-card p{margin:4px 0 0;color:rgba(255,255,255,.9);max-width:62ch;}
.finder-search{max-width:none;}
.finder-card .sop-counter,.finder-card .live-activity{align-self:flex-start;margin-top:0;}
.split-copy,.split-form{min-width:0;}
@media(min-width:860px){ .split-rev .split-copy{order:2;} .split-rev .split-form{order:1;} }
.tick-list{list-style:none;padding:0;margin:.6rem 0;display:flex;flex-direction:column;gap:.4rem;}
.tick-list li{position:relative;padding-left:1.6rem;color:var(--muted);font-size:var(--fs-sm);}
.tick-list li::before{content:"";position:absolute;left:0;top:.15em;width:1.05rem;height:1.05rem;border-radius:50%;background:var(--blue-100);}
.tick-list li::after{content:"";position:absolute;left:.35rem;top:.4em;width:.34rem;height:.17rem;border-left:2px solid var(--blue);border-bottom:2px solid var(--blue);transform:rotate(-45deg);}
.chip-wrap{display:flex;flex-wrap:wrap;gap:8px;}
.chip-join{cursor:pointer;background:var(--surface);border:1px solid var(--border-strong);color:var(--text);transition:border-color .15s,background .15s,color .15s;}
.chip-join:hover{border-color:var(--wa-green);color:var(--wa-green);background:#eafaf0;}
.chip-join .ic{color:var(--wa-green);}
.cc-grid{display:grid;gap:10px;}
@media(min-width:560px){ .cc-grid{grid-template-columns:1fr 1fr;} .cc-grid > .cb-field:nth-child(3){grid-column:1 / -1;} }
.cc-form .lbl{display:block;font-size:.8rem;font-weight:700;color:var(--muted);margin-bottom:3px;}
.webinar{display:flex;flex-direction:column;}
.webinar .chip{align-self:flex-start;}
.webinar .btn{margin-top:auto;}
.meetup-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--border);margin-top:8px;}
.meetup-date{width:46px;height:46px;border-radius:12px;background:var(--blue-100);color:var(--blue);display:grid;place-items:center;line-height:1.05;flex:none;}
.meetup-date .md-d{font-size:1.25rem;font-weight:800;}
.meetup-date .md-m{font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.05em;}
.refer-teaser{display:flex;align-items:center;gap:14px;flex-wrap:wrap;}
.refer-teaser .btn{margin-left:auto;}
@media(max-width:560px){ .refer-teaser .btn{margin-left:0;width:100%;text-align:center;} }
.lb-row{display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid var(--border);}
.lb-row:first-child{border-top:0;}
.lb-rank{width:26px;height:26px;border-radius:50%;background:var(--surface-2);color:var(--muted);font-weight:800;font-size:.8rem;display:grid;place-items:center;flex:none;}
.lb-row.is-me{background:var(--blue-100);border-radius:12px;padding:9px 10px;border-top-color:transparent;}
.lb-row.top1 .lb-rank{background:#fef0d8;color:var(--orange-600);}
.lb-prog{height:7px;border-radius:5px;background:var(--surface-2);overflow:hidden;margin-top:8px;}
.lb-prog span{display:block;height:100%;background:linear-gradient(90deg,var(--blue),var(--orange));}
/* notification center (item 41) */
.notif-row{display:flex;gap:11px;padding:12px 0;border-top:1px solid var(--border);}
.notif-row:first-child{border-top:0;}
.notif-row .n-body{flex:1;min-width:0;}
.notif-row .n-time{font-size:.8rem;color:var(--muted);white-space:nowrap;}
.dtab-badge.is-live{background:var(--orange-600);color:#fff;}
.chip-warn{background:#fbeee0;color:var(--warn);}

/* ============ Homepage hero search (AI-primary) ============ */
/* 14px, not a 999px pill. This is the site's primary entry point and it was the
   last place still wearing the old lozenge shape, so it read as a leftover from
   a different design next to every other control on the page. The field and its
   button share the radius family the rest of the UI now uses. */
.hero-search{display:flex;align-items:center;gap:8px;background:#fff;border-radius:16px;padding:6px 6px 6px 14px;max-width:640px;box-shadow:var(--shadow-lg);}
.hero-search-ic{color:var(--muted);display:grid;place-items:center;flex:none;}
.hero-search-input{flex:1;min-width:0;border:0;outline:0;background:transparent;font-size:1rem;color:var(--text);padding:14px 4px;font-family:inherit;}
.hero-search-input::placeholder{color:#8595a8;}
.hero-search-btn{flex:none;white-space:nowrap;border-radius:12px;}
/* A translucent WHITE panel lightens the ground under its own white text, and
   how much depends on where it lands. These two buttons sit directly under the
   H1 on the home hero: 4.36:1 on the gradient's lightest stop (#2f63b8), and
   3.62:1 where the .hero::after orange glow washes over them — both under the
   4.5:1 body-text bar, and neither knowable from this rule alone, which is also
   why the stylesheet sweep in ColourContrastTest could not measure it.
   Same answer as .exam-card-score below: on a ground we do not control, bring
   your own. An opaque --blue-600 puts white at 10.32:1 wherever the button
   lands, glow included, and hovering to --blue brightens it while still holding
   8.01:1. The border now carries the button's shape, so it has to clear the 3:1
   non-text bar too — at the old .28 it was 1.49:1 against the lightest stop,
   an outline you could not see; .65 measures 3.04:1 there and 7.54:1 on
   --ocean. */
.hero-cta-ghost{background:var(--blue-600);color:#fff;border:1px solid rgba(255,255,255,.65);}
.hero-cta-ghost:hover{background:var(--blue);border-color:rgba(255,255,255,.85);}
@media(max-width:560px){
  .hero-search{flex-wrap:wrap;border-radius:22px;padding:10px;gap:8px;}
  .hero-search-ic{display:none;}
  .hero-search-input{flex:1 1 100%;padding:13px 8px;border-bottom:1px solid var(--border);}
  .hero-search-btn{flex:1 1 100%;justify-content:center;}
}

/* ============ §4 shared-by banner + §3 celebrate card ============ */
.shared-by{background:linear-gradient(90deg,var(--blue-100),var(--surface));border-bottom:1px solid var(--border);}
.shared-by-inner{display:flex;align-items:center;gap:12px;padding:8px 0;font-size:.875rem;color:var(--text);}
.shared-by-inner .ic{color:var(--orange-600);vertical-align:-2px;}
.shared-by-inner a{font-weight:700;color:var(--blue);}
.shared-by-x{margin-left:auto;background:none;border:0;color:var(--muted);cursor:pointer;font-size:1rem;line-height:1;padding:2px 6px;}
.celebrate{text-align:center;border:2px solid var(--orange);background:linear-gradient(135deg,var(--surface),var(--blue-100));}
.celebrate-burst{width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg,#ff9a3d,var(--orange));color:var(--text);display:grid;place-items:center;margin:0 auto 8px;box-shadow:0 8px 20px rgba(224,132,49,.35);}
.celebrate-h{margin:.2rem 0;}
.celebrate-d{font-size:.875rem;margin:.2rem 0 .8rem;}

/* Lesson prose. The body is our own copy, built from paragraphs and short lists, so it needs
   a measure and a rhythm rather than a full editorial stylesheet — .prose styles database
   content and carries assumptions this does not want. */
.lesson-body p{margin:0 0 14px;line-height:1.7;}
.lesson-body p:last-child{margin-bottom:0;}
.lesson-body ul{margin:0 0 14px;}
.lesson-body b{font-weight:700;}

/* Band trajectory on the dashboard: one bar per attempt, oldest on the left. Bars, not a
   line: with three attempts a line chart draws a trend that three points do not support. */
.traj{display:flex;align-items:flex-end;gap:4px;height:64px;padding:6px 0;}
.traj-bar{flex:1;min-width:6px;max-width:28px;height:var(--h,10%);border-radius:4px 4px 2px 2px;
  background:linear-gradient(180deg,#8B5CF6,#6D28D9);}
.traj-bar:last-child{background:linear-gradient(180deg,#16A34A,#15803D);}

/* Answer review, on the mock result. Every question carries a written explanation and none
   of them had ever been shown to a student — the list only exists after submit, because the
   answer key is deliberately not sent to the client during the test. */
.qrev{border-top:1px solid var(--border);padding:12px 0;}
.qrev:first-child{border-top:0;}
.qrev-q{margin:0 0 6px;font-size:.94rem;line-height:1.45;}
.qrev-a{display:flex;flex-wrap:wrap;gap:6px;margin:0;}
.qrev-x{margin:8px 0 0;font-size:.875rem;line-height:1.5;color:var(--muted);border-left:3px solid var(--border-strong);padding-left:10px;}

/* Scholarship card. Same bones as .ucard and .ccard — band, identity, chips, stat, action —
   so the awards grid reads as part of the same catalogue rather than as a list of rows with
   an icon. Two differences, both forced by what an award is:

   The root is a DIV, not an <a>: the card must carry the official-source link, and an anchor
   inside an anchor is invalid. So there is no whole-card hover lift here — the title and the
   two footer links are the targets.

   The band's fallback takes the CARD's own tone rather than the blue one .pcard uses, because
   a scholarship card is toned by the kind of award: a blue gradient under a rose private-award
   card looked like a card from a different set. */
.scard .pcard-body{gap:9px;}
/* The identity line is a flex ROW on the university card, where it holds a flag, a city and
   a rank chip. Here it is a sentence — provider, destination, level — and a provider long
   enough to wrap ("UK Foreign, Commonwealth & Development Office") pushed the rest of it
   into a second column beside itself. Block flow, with the separators spaced in CSS. */
.scard .pcard-uni-meta{display:block;margin:0;}
.scard .pcard-uni-meta .pcard-dot{margin:0 5px;}
.scard .app-pills{margin:0;}
.scard .pcard-stats{margin-top:0;}
.scard .schol-foot{margin-top:auto;}
.scard .ucard-name{font-size:1rem;}
.scard:not(.pcard-has-media) .pcard-media{background:linear-gradient(135deg,var(--card-bg,var(--blue-100)),var(--surface));}
/* The shared monogram is --blue at 42%, which reads as a stray blue word on a rose or teal
   band. On these it is the page ink, so it belongs to whatever tone the card carries. */
.scard .pcard-media-mono{color:var(--text);opacity:.3;}
.scard .ccard-mark .app-flag{box-shadow:0 6px 16px rgba(0,20,50,.3);}
/* A pill sits on nine different grounds now (white, and the eight tones). The status fills
   are opaque pastels tuned for white — #F6ECE0 "closing soon" on the amber tone measures
   1.05:1 against it and simply disappears. A hairline ring costs nothing on white and keeps
   every pill an object on every ground. */
.app-pill{box-shadow:0 0 0 1px rgba(16,40,70,.07);}

/* ============ Destination landmark cards + country-hero images ============ */
.dcard{position:relative;display:block;height:150px;border:0;border-radius:16px;overflow:hidden;text-decoration:none;
  background-color:var(--blue-100);box-shadow:var(--shadow);}
/* Same reason as .pcard-media: the destination tiles were background images too, another
   669 KB on /universities and the same again on the homepage, all fetched off-screen.
   z-index 0 keeps it under ::after's gradient (1) and .dcard-body (2). */
.dcard-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0;}
.dcard::after{content:"";position:absolute;inset:0;z-index:1;background:linear-gradient(180deg,rgba(2,18,42,0) 34%,rgba(2,18,42,.8));}
.dcard-body{position:absolute;left:12px;right:10px;bottom:9px;z-index:2;padding:0;display:flex;align-items:center;justify-content:space-between;color:#fff;}
.dcard-body b{font-family:var(--font-display);font-size:var(--fs-h3);font-weight:800;line-height:var(--lh-card);letter-spacing:-.02em;color:#fff;text-shadow:0 1px 5px rgba(0,0,0,.4);}
.dcard-go{color:#fff;opacity:.92;}

/* ============ Hover lifts, gated (2026-09-07) ============
   Every lift used to be declared per component and then un-declared for touch
   by an enumerated @media(hover:none) reset 540 lines earlier. Two problems:
   the reset could not reset box-shadow, and it was a list — .explore-card kept
   its lift AND its shadow after a tap because it was added to one place and not
   the other. Gating at the source means a device without a real pointer never
   receives the rule at all, and there is nothing left to reset. */
@media (hover:hover) and (pointer:fine){.ucard:hover,.ccard:hover,.course-tile:hover,.post-card:hover,.explore-card:hover{transform:translateY(-3px);box-shadow:var(--shadow-lg);}
  /* The photo leans in a little under the pointer. The band clips it (overflow:
     hidden), so nothing moves outside the card. */
  .ucard:hover .pcard-media-img,.ccard:hover .pcard-media-img{transform:scale(1.04);}
  .dcard:hover .dcard-go{transform:translateX(2px);}
}
@media(min-width:560px){ .dcard{height:162px;} }
.uni-hero.uni-hero-img{position:relative;background-color:var(--ocean);background-size:cover;background-position:center;background-repeat:no-repeat;padding:2.4rem 0 2.6rem;}
.uni-hero.uni-hero-img::before{content:"";position:absolute;inset:0;z-index:1;background:linear-gradient(115deg,rgba(2,18,42,.86),rgba(2,18,42,.46));}
.uni-hero.uni-hero-img > .container{position:relative;z-index:2;}
/* Attribution for a CC-BY/CC-BY-SA campus photo. Deliberately quiet — it is a
   legal credit, not content — but it must be legible: .62 white on the hero's
   dark scrim measures ~7:1, so it satisfies the licence without competing with
   the university's name. */
.uni-hero-credit{margin:14px 0 0;font-size:var(--fs-xs);color:rgba(255,255,255,.62);line-height:1.4;}
.uni-hero-credit a{color:rgba(255,255,255,.82);text-decoration:underline;}

/* ===================================================================
   Accessibility & interaction polish
   =================================================================== */
/* Skip-to-content — the first tab stop; slides in only when focused */
.skip-link{position:fixed;top:-72px;left:12px;z-index:200;background:var(--blue);color:#fff;
  /* 43px before this: 10px padding either side of a 23px line box. The first
     control a keyboard user ever reaches should not miss the floor by 1px. */
  padding:10px 16px;min-height:44px;display:inline-flex;align-items:center;
  border-radius:12px;font-weight:700;font-size:var(--fs-sm);box-shadow:var(--shadow-lg);
  transition:top .18s ease;}
.skip-link:focus{top:calc(12px + env(safe-area-inset-top));outline:none;}
#main:focus{outline:none;}

/* Screen-reader-only utility */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0,0,0,0);white-space:nowrap;border:0;}

/* Visible keyboard focus ring on every interactive element (keyboard only —
   :focus-visible never fires on mouse click, so pointer users see nothing new).
   Form fields keep their own blue ring from .field:focus above. */
/* TWO-TONE, and it has to stay that way. A single-colour ring is only ever
   correct against the one background someone had in mind when they picked it:
   this was `3px solid var(--blue)`, chosen for ~5.9:1 on white, and --blue is
   the exact background of .app-header — so the first five tab stops on every
   page of the site (brand, search, home, counselling CTA, hamburger) drew a
   #244F94 ring on #244F94 at 1.00:1. Invisible. The same hole existed on
   .section-navy, .uni-hero and the footer.
   White ring + dark halo means one of the two layers always contrasts, whatever
   is behind it: on light ground the halo reads, on navy the ring reads. Do not
   "simplify" this back to one colour.

   NO border-radius here. This rule used to end `border-radius:inherit`, which was
   wrong in a way that only shows on keyboard: `inherit` takes the PARENT's radius,
   not the element's own. Eleven of fourteen interactive elements sampled sit in a
   0px-radius parent, so focusing the "Share" button on /university/* computed
   border-radius:0 against its own 12px rule and it rendered as a hard-cornered
   rectangle beside the still-rounded "Save" pill. Every button, pill and card on
   the site changed shape the moment a keyboard user reached it — shipped by the
   round that was fixing the focus ring. Both `outline` and `box-shadow` already
   follow the element's own border-radius in every current browser, so the
   declaration bought nothing and cost that.

   `input` and `.field` are in the list on purpose. They were missing, so every
   text field on the site fell through to `.field:focus{box-shadow:0 0 0 3px
   var(--blue-100)}` — and --blue-100 is #e8eff8, 1.09:1 on white. The entire
   focus indicator on a form field was a 1px border-colour change, including when
   validation moves focus there for the student. `.field:focus-visible` is (0,2,0),
   the same as `.field:focus`, and sits later in the file, so it wins the tie. */
a:focus-visible,button:focus-visible,select:focus-visible,textarea:focus-visible,
input:focus-visible,.field:focus-visible,
[tabindex]:focus-visible,.btn:focus-visible,.tabbar a:focus-visible,.card:focus-visible,
.icon-btn:focus-visible,
.crumbs a:focus-visible,.dtab:focus-visible,.seg-btn:focus-visible{
  outline:3px solid #fff;outline-offset:2px;
  box-shadow:0 0 0 6px rgba(4,26,52,.85);}

/* Mobile hardening: a stray wide element must never scroll the whole page
   sideways. overflow-x:clip guards without creating a scroll container, so
   sticky headers keep working (unlike overflow-x:hidden). */
html,body{overflow-x:clip;}

/* Desktop primary nav — hidden on phones (the drawer covers those), shown from 900px
   where there is room for a real menu bar. */
.desk-nav{display:none;background:var(--blue-600);border-top:1px solid rgba(255,255,255,.10);}
.desk-nav-in{max-width:var(--maxw);margin:0 auto;padding:0 16px;display:flex;gap:2px;align-items:stretch;}
.desk-nav-a{display:inline-flex;align-items:center;gap:7px;padding:.62rem .75rem;color:rgba(255,255,255,.88);
  font-size:.875rem;font-weight:600;white-space:nowrap;border-bottom:3px solid transparent;}
/* The icon is a scanning aid, not a second focal point: held below the label's
   weight so the bar still reads as words first, shapes second. Full strength
   only on the section you are actually in. */
.desk-nav-ic{opacity:.72;flex:none;}
.desk-nav-a:hover .desk-nav-ic,.desk-nav-a.is-on .desk-nav-ic{opacity:1;}
.desk-nav-a:hover{color:#fff;background:rgba(255,255,255,.08);}
.desk-nav-a.is-on{color:#fff;border-bottom-color:var(--orange);}
@media(min-width:900px){ .desk-nav{display:block;} }

/* The header row (logo + search + home + CTA + menu) needs ~440px to fit at full
   size. Below that the flex row overflows and the browser shrinks whatever can
   shrink — which was the two 44x44 icon buttons, squeezed to 22px wide on a
   375px phone. Half a tap target, and under the 24px WCAG 2.5.8 floor, on the
   most common phone width there is. So: shorten the CTA from 440px down (this
   used to start at 360px, which was far too late), and put a hard min-width on
   the icon buttons so a future overflow can never quietly eat them again. */
.icon-btn{min-width:44px;flex:none;}

.header-cta .cta-short{display:none;}

@media (max-width:440px){
  .app-header .bar{gap:10px;padding:0 12px;}
  .header-cta{padding:.45rem .7rem;font-size:var(--fs-xs);}
  .header-cta .cta-full{display:none;}
  .header-cta .cta-short{display:inline;}
  .brand-logo{max-width:104px;}
}

@media (max-width:360px){
  .app-header .bar{gap:8px;padding:0 10px;}
  .header-cta{padding:.45rem .6rem;font-size:var(--fs-xs);}
  .brand-logo{max-width:92px;}
}

/* Brand text selection + remove the grey mobile tap flash for an app feel */
::selection{background:var(--blue);color:#fff;}
html{-webkit-tap-highlight-color:transparent;}

/* Honour reduced-motion globally — near-instant animations/transitions for
   users who ask for less motion (keeps content, drops the movement). */
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important;scroll-behavior:auto!important;}
}

/* =====================================================================
   Long-form guide navigation (/loans and any page using .guide-long)

   /loans is ~42,000 characters — about fifty phone screens — and shipped
   with one anchor and no contents list. Nothing was cut to fix that; the
   page got a table of contents, an id on every heading, and a way back
   to the contents from wherever the reader stopped.
   ===================================================================== */
/* What is pinned above this page is not a constant, which is how the first
   version of this got the offset wrong: the app header is 58px on a phone but
   103px once the desktop nav row appears, and the intake-deadline banner
   (#vb-countdown, sticky at top:58px) adds another 45px on EVERY page for as
   long as a deadline is still ahead. A hardcoded 74px on desktop put the
   clicked heading 29px behind that banner — you tapped "The moratorium" and
   landed in the middle of its second paragraph with the title out of sight.
   loans.blade.php measures the real stack and writes it into these two
   variables; the fallbacks are what the stack measures today, for the case
   where that script never runs. */
.guide-long{--stack:103px;--anchor-offset:160px;}
.guide-long h2{
  scroll-margin-top:var(--anchor-offset);
}
.guide-long h2 .guide-back{
  font-size:.72em;font-weight:700;color:var(--muted);text-decoration:none;
  padding:0 .25em;opacity:0;transition:opacity .15s;vertical-align:.08em;
}
.guide-long h2:hover .guide-back,.guide-long h2 .guide-back:focus-visible{opacity:1;}
/* No hover on touch, and this is the one-handed escape hatch there, so on
   phones it is always visible rather than always hidden. */
@media(hover:none){ .guide-long h2 .guide-back{opacity:.75;} }

.toc{
  /* Pinned directly BELOW everything else that is pinned, not at a guessed
     offset: at top:64px the contents bar landed inside the countdown banner and,
     one z-index lower, was painted over by it — 39 of its 49 pixels were hidden,
     so the navigation this page was given existed but could not be tapped.
     z-index matches the banner's; the bar is later in the document, so equal
     values leave it on top if the two ever meet again. */
  position:sticky;top:calc(var(--stack) + 6px);z-index:39;margin:0 0 6px;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
  box-shadow:var(--shadow-sm);
}
.toc-d{padding:0;}
.toc-sum{
  display:flex;align-items:center;gap:.5rem;cursor:pointer;list-style:none;
  padding:12px 16px;font-weight:700;font-size:var(--fs-sm);color:var(--blue);
}
.toc-sum::-webkit-details-marker{display:none;}
.toc-sum::after{content:"▾";margin-left:auto;font-size:.8rem;transition:transform .15s;}
.toc-d[open] .toc-sum::after{transform:rotate(180deg);}
.toc-list{
  margin:0;padding:0 16px 14px 34px;max-height:min(52vh,420px);overflow-y:auto;
  font-size:var(--fs-sm);line-height:1.45;
}
.toc-list li{margin:.45rem 0;}
/* 44px targets: this list is thumbed on a phone, not clicked with a mouse.
   `min-height:32px` claimed 44 in the comment and measured 33 in the browser —
   the rows have to actually BE 44, and flex is what centres a one-line entry in
   that box instead of parking it at the top of an empty 44px block. */
.toc-list a{color:var(--text);text-decoration:none;display:flex;align-items:center;padding:7px 0;min-height:44px;}
.toc-list a:hover,.toc-list a:focus-visible{color:var(--blue);text-decoration:underline;}
.toc-note{font-size:.875rem;margin:10px 0 0;}

/* Tablet and up there is room for the list to stay open (the page opens the
   <details> itself at that width — CSS cannot reliably force a closed one open,
   and a closed-by-default control is the safe fallback if that script never
   runs). Sticky is dropped there: on a wide screen the contents card is already
   in view beside the reading column. */
@media(min-width:820px){
  .toc{position:static;}
  .toc-list{max-height:none;}
  /* No sticky contents bar here, so only the header + banner have to be
     cleared. Still measured — the desktop header is a different height again. */
  .guide-long{--anchor-offset:111px;}
}

/* A language switcher offering a single language is a control with nothing to
   control. config/locales decides what is genuinely translated; when that
   leaves one entry, the switcher takes itself off the page rather than sitting
   there as a dead button. Add a second real translation and it returns. */
.foot-lang:not(:has(a)),.foot-lang:has(a:only-of-type),
.vb-drawer-lang:not(:has(a)),.vb-drawer-lang:has(a:only-of-type){display:none;}

/* ===================== Scroll containment (2026-09-05) =====================
   overscroll-behavior appeared ZERO times in this build, which is why the
   custom pull-to-refresh stacked on top of the browser's native one, and why
   reaching the end of any of the eight horizontal rails scrolled the page
   behind it. Containment on the root stops the chain; containment on each
   overlay and rail stops a gesture inside them reaching the document at all. */
html,body{overscroll-behavior-y:contain;}
.vb-drawer-panel,.chatbot-body,.modal,
.dest-row,.acf,.exam-slider,.mt-passage,.cmp-wrap,.dtabs,.stepper,.review-row,.reel-row,.cs-row{overscroll-behavior:contain;}

/* ── Legacy FAQ and guide pages carried over from the old site ──────────────
   Long-form prose that arrives as harvested HTML, so these rules style raw
   descendants (.lp-body h2, .lp-body p) rather than authored classes — there
   is no markup to add classes to. --border-strong is the audited 3.84:1
   border token; the earlier #cdd9e8 failed WCAG 1.4.11 at 1.37:1. */
.lp-wrap{max-width:820px;padding-top:clamp(1.5rem,3vw,2.5rem);padding-bottom:clamp(2rem,5vw,4rem);}
.lp-meta{color:rgba(255,255,255,.82);font-size:.875rem;font-weight:600;margin-top:.75rem;}
.lp-body{color:var(--text);line-height:1.7;}
.lp-body h2{font-size:clamp(1.25rem,2.4vw,1.6rem);line-height:1.25;margin:2rem 0 .75rem;text-wrap:balance;}
.lp-body h3{font-size:clamp(1.05rem,1.9vw,1.25rem);line-height:1.3;margin:1.5rem 0 .5rem;text-wrap:balance;}
.lp-body p{margin:0 0 1rem;}
.lp-body ul,.lp-body ol{margin:0 0 1.25rem;padding-left:1.35rem;}
.lp-body li{margin:.4rem 0;}
.lp-body strong{font-weight:700;}
.lp-body a{color:var(--blue);text-decoration:underline;text-underline-offset:2px;}
.lp-body dl{margin:0 0 1.25rem;}
.lp-body dt{font-weight:700;margin-top:.75rem;}
.lp-body dd{margin:.25rem 0 0;padding-left:0;}

.lp-faq{margin-top:2.5rem;}
.lp-faq>h2{font-size:clamp(1.25rem,2.4vw,1.6rem);margin:0 0 1rem;}
.lp-faq-list{display:flex;flex-direction:column;gap:.6rem;}
.lp-q{border:1px solid var(--border-strong);border-radius:10px;background:var(--surface,#fff);overflow:hidden;}
.lp-q summary{cursor:pointer;padding:.85rem 1rem;font-weight:650;line-height:1.4;list-style:none;}
.lp-q summary::-webkit-details-marker{display:none;}
.lp-q summary::after{content:'+';float:right;margin-left:1rem;font-weight:700;color:var(--blue);}
.lp-q[open] summary::after{content:'–';}
.lp-q summary:focus-visible{outline:2px solid var(--blue);outline-offset:-2px;}
.lp-a{padding:0 1rem 1rem;line-height:1.7;}
.lp-a p{margin:0;}

.lp-cta{margin-top:2.5rem;padding:1.5rem;border:1px solid var(--border-strong);border-radius:14px;
  background:var(--surface-2,#f4f7fb);text-align:center;}
.lp-cta h2{margin:0 0 .35rem;font-size:1.2rem;}
.lp-cta p{margin:0 0 1rem;color:var(--muted);}

.lp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;}
.lp-empty{color:var(--muted);}
