/* ==========================================================================
   graduate.pk - design system
   ==========================================================================
   DIRECTION: "almanac".

   This is a reference work about Pakistani higher education, not a product
   landing page. So it borrows from print reference design rather than from
   web-app UI: warm paper, hairline rules instead of boxed cards, ruthless
   type hierarchy, and numerals treated as the hero material - because rank,
   fee and count ARE the content here.

   Rules the design holds to:
    - numbers are typographic events, not table cells
    - rules and space separate things; boxes are a last resort
    - labels are small, tracked and quiet; values are large and confident
    - mobile gets the full-strength design, not a squeezed desktop
    - nothing decorative costs a request

   Deliberately absent, because they are the house style of generic templates:
   gradients, glassmorphism, soft drop-shadowed cards on every surface,
   centred hero + two buttons, uniform 3-up card grids everywhere.

   Structure: 1 tokens · 2 base · 3 layout · 4 type · 5 header · 6 components
              7 utilities · 8 print
   ========================================================================== */

/* 0. TYPEFACE ============================================================
   Newsreader, self-hosted. Two static instances, subsetted here from the
   variable originals with fontTools: optical size pinned to 32 for display
   use, weight pinned to 400, and the character set cut to Latin-1 plus the
   punctuation the templates actually emit. 128.9 KB -> 19.0 KB roman,
   61.8 KB -> 20.5 KB italic.

   The italic is declared but costs nothing on most pages: a browser fetches a
   face only when a glyph actually needs it, and only the home hero and article
   pull quotes set italic serif.

   Licence: SIL Open Font License 1.1 - see /fonts/OFL.txt, which is served.
   ========================================================================= */

@font-face {
  font-family: "Jakarta";
  src: url("/fonts/jakarta.woff2") format("woff2");
  font-weight: 400 800;   /* one variable file covers body through display */
  font-style: normal;
  font-display: swap;
}
/* Metric-matched fallback. Jakarta's x-height is 0.536 em against Segoe UI's
   0.50, so without the adjustment the swap visibly reflows every heading. */
@font-face {
  font-family: "Jakarta Fallback";
  src: local("Segoe UI"), local("Helvetica Neue"), local("Arial");
  size-adjust: 104%;
  ascent-override: 96%;
  descent-override: 24%;
  line-gap-override: 0%;
}

/* 1. TOKENS ============================================================== */

:root {
  /* Type. No webfont: zero requests, zero FOUT, zero CLS. Character comes from
     scale contrast and tracking instead. A display face can be introduced later
     by changing --font-display alone. */
  /* One family, weight 400-800, doing both jobs. The serif experiment is gone:
     a print face was the wrong register for a product people use on a phone. */
  --font-sans: "Jakarta", "Jakarta Fallback", system-ui, -apple-system,
               "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: var(--font-sans);
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Type scale. Tightened from the first pass: the top two steps were sized for
     a poster, which is what made every page read as three headlines and a
     paragraph. Headlines are still the largest thing on the page, but they no
     longer push the content that matters below the fold. */
  --step--2: clamp(0.78rem, 0.755rem + 0.11vw, 0.85rem);
  --step--1: clamp(0.90rem, 0.870rem + 0.13vw, 0.98rem);
  --step-0:  clamp(1.06rem, 1.020rem + 0.17vw, 1.16rem);
  --step-1:  clamp(1.22rem, 1.150rem + 0.30vw, 1.40rem);
  --step-2:  clamp(1.50rem, 1.340rem + 0.70vw, 1.95rem);
  --step-3:  clamp(1.85rem, 1.550rem + 1.30vw, 2.65rem);
  --step-4:  clamp(2.15rem, 1.720rem + 1.90vw, 3.40rem);
  --step-5:  clamp(2.50rem, 1.950rem + 2.70vw, 4.30rem);

  /* In `em`, not `ch`. Jakarta's zero is 0.73em wide, so 68ch resolved to
     920px and articles ran at 113 characters a line - comfortable reading is
     60-75. `em` tracks the element's own (fluid) font size, so the line length
     stays constant as the type scales. 32em lands around 72 characters. */
  --measure: 32em;
  --lh-tight: 1.04;
  --lh-snug: 1.22;
  --lh-body: 1.62;

  /* Spacing. The whole scale above --sp-s is pulled in by roughly a third; the
     previous rhythm left a phone screen carrying two rows of content. */
  --sp-3xs: 0.25rem;
  --sp-2xs: 0.5rem;
  --sp-xs:  0.7rem;
  --sp-s:   0.95rem;
  --sp-m:   1.3rem;
  --sp-l:   1.85rem;
  --sp-xl:  2.6rem;
  --sp-2xl: 3.75rem;

  /* Colour.
     The ground is plain white - no tinted paper, no colour bands. Teal appears
     ONLY as accent: links, buttons, hover, focus, the rank badge and the mark
     itself. Hierarchy is carried by type, space and hairline rules instead.

     Two teals on purpose. --brand is the logo colour sampled straight out of
     assets/logo.png (#13988a, 3.57:1 on white) - correct for the mark and for
     fills, but short of AA for body-size text. --brand-ink is the same hue
     darkened to 80% (5.21:1) and is what link and button text uses. */
  /* The greys are NEUTRAL, very slightly warm - never tinted toward the brand.
     Every one of these used to sit at hue 163–200°, i.e. teal: --surface-2 at
     12.5% saturation, --rule at 8.3%, and all three ink levels around 5%.
     Individually invisible; together the page read as faintly green, because
     the text, the rules, the footer and every panel ground were all tinted at
     once. Teal now appears ONLY in the --brand-* tokens. */
  color-scheme: light;

  /* Light. The canvas is a hair off white so that white cards lift off it -
     that separation is what reads as "modern" and no amount of hairline rules
     substitutes for it. The greys are neutral: measured, not eyeballed, after
     an earlier palette turned out to be tinted teal across every single one. */
  --paper:      #f5f6f7;   /* the canvas the cards sit on */
  --surface:    #ffffff;   /* cards, header, anything raised */
  --surface-2:  #eef0f2;   /* row hover, chips, quiet fills */
  --rail:       #eef0f2;
  --ink:        #14161a;
  --ink-2:      #545862;   /* 7.15:1 on --surface */
  --ink-3:      #696e79;   /* 5.28:1 on --surface, 4.79:1 on --surface-2 */
  --rule:       #e3e6e9;
  --rule-2:     #cfd3d9;
  --brand:      #13988a;   /* EXACT logo teal, sampled from assets/logo.png */
  --brand-ink:  #0f7a6e;   /* same hue at 80% - 5.21:1 on white, clears AA */
  --brand-deep: #0c635a;   /* 65% - hover/pressed, 7.12:1 (AAA) */
  --brand-soft: #e7f4f2;   /* 6% tint, for chips and hover fills */
  /* Any solid brand-coloured surface carrying text uses this pair. --brand
     itself is only 3.57:1 against white, so filling a button or a badge with it
     and setting white text failed AA - invisible in the default render because
     it only showed on hover, in the quiz score and on the skip link. In dark
     mode the fill is light, so the text on it flips to near-black. */
  --brand-fill: #0f7a6e;
  --on-brand:   #ffffff;
  /* The wordmark. On white the mark is the teal logo and ".pk" is teal;
     on a dark ground both go white, which is what assets/logo-light.png is
     for. Swapping via a custom property rather than two <img> tags means
     only the image actually in use is ever downloaded, and it follows the
     data-theme toggle — a <picture> media query would only follow the OS. */
  --logo: url("/logo.png");
  --word: var(--ink);
  --tld:  var(--brand);
  --accent:     #b1541f;
  --accent-soft:#fdf0e6;
  --focus:      #0c7367;

  /* Layered rather than single-blur shadows - the difference between "a box
     with a shadow" and something that reads as lifted. Tuned against the
     off-white canvas, not against pure white. */
  --shadow-1: 0 1px 2px rgb(16 22 34 / .05), 0 1px 3px -1px rgb(16 22 34 / .06);
  --shadow-2: 0 2px 4px -2px rgb(16 22 34 / .07), 0 8px 18px -6px rgb(16 22 34 / .12);
  --shadow-3: 0 6px 12px -6px rgb(16 22 34 / .12), 0 22px 44px -18px rgb(16 22 34 / .26);

  --radius-s: 8px;
  --radius:   14px;
  --radius-l: 20px;
  --header-h: 3.75rem;

  --ease: cubic-bezier(.22, .61, .36, 1);
}

/*
 * Dark theme.
 *
 * Three states, not two. An explicit choice stamps data-theme on <html>;
 * the default "system" setting stamps nothing, so only the media query
 * separates light from dark there. The media block is guarded with
 * :not([data-theme="light"]) so choosing light beats a dark OS, and the
 * [data-theme="dark"] block repeats the values so choosing dark beats a
 * light OS. The two blocks must stay in sync - they are deliberately
 * adjacent so a change to one is obviously a change to both.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --paper:      #0f1114;
    --surface:    #171a1e;
    --surface-2:  #1f2329;
    --rail:       #1f2329;
    --ink:        #eef0f3;
    --ink-2:      #adb2bc;
    --ink-3:      #868c97;
    --rule:       #262b32;
    --rule-2:     #3a404a;
    --brand:      #2fd0b8;
    --brand-ink:  #4bdcc6;
    --brand-deep: #74e8d5;
    --brand-soft: #10302c;
    --brand-fill: #2fd0b8;
    --on-brand:   #052220;
    --logo: url("/logo-light.png");
    --word: var(--brand);
    --tld:  var(--ink);
    --accent:     #f0955c;
    --accent-soft:#2b1c12;
    --focus:      #4bdcc6;

    --shadow-1: 0 1px 2px rgb(0 0 0 / .5);
    --shadow-2: 0 4px 16px -4px rgb(0 0 0 / .6);
    --shadow-3: 0 12px 36px -10px rgb(0 0 0 / .7);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --paper:      #0f1114;
  --surface:    #171a1e;
  --surface-2:  #1f2329;
  --rail:       #1f2329;
  --ink:        #eef0f3;
  --ink-2:      #adb2bc;
  --ink-3:      #868c97;
  --rule:       #262b32;
  --rule-2:     #3a404a;
  --brand:      #2fd0b8;
  --brand-ink:  #4bdcc6;
  --brand-deep: #74e8d5;
  --brand-soft: #10302c;
  --brand-fill: #2fd0b8;
  --on-brand:   #052220;
  --logo: url("/logo-light.png");
  --word: var(--brand);
  --tld:  var(--ink);
  --accent:     #f0955c;
  --accent-soft:#2b1c12;
  --focus:      #4bdcc6;

  --shadow-1: 0 1px 2px rgb(0 0 0 / .5);
  --shadow-2: 0 4px 16px -4px rgb(0 0 0 / .6);
  --shadow-3: 0 12px 36px -10px rgb(0 0 0 / .7);
}

/* 2. BASE ================================================================ */

*, *::before, *::after { box-sizing: border-box; }

html {
  background: var(--paper);   /* not body - see .crescent-layer */
  -webkit-text-size-adjust: 100%;
  /* Reserve the vertical scrollbar's width so short and long pages do not
     shift horizontally as you navigate between them. */
  scrollbar-gutter: stable;
}
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--step-0);
  line-height: var(--lh-body);
  color: var(--ink);
  background: transparent;   /* canvas is on html, so the crescent layer shows */
  -webkit-font-smoothing: antialiased;
  font-feature-settings: "kern" 1;
  /* Backstop against a horizontal scrollbar. `clip` rather than `hidden`
     deliberately: `hidden` would turn body into a scroll container and break
     the sticky header. Every known overflow source is fixed at source (the nav
     no longer uses a negative margin, the city shelf and the section tabs wrap
     instead of scrolling); this only guarantees a stray future element cannot
     reintroduce one. */
  overflow-x: clip;
}

/* Long unbroken strings - URLs in prose, an institute name in a narrow table
   cell - are the other way a page starts scrolling sideways. */
p, li, dd, dt, td, th, h1, h2, h3, h4, .index-title, .shelf-name { overflow-wrap: break-word; }

/* Weight and tight tracking carry the hierarchy now. A variable face means
   800 costs nothing extra, so headings are genuinely heavy rather than a
   half-step above body text. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  margin: 0 0 var(--sp-2xs);
  line-height: var(--lh-snug);
  font-weight: 700;
  letter-spacing: -0.024em;
  text-wrap: balance;
}
h1 { font-size: var(--step-4); line-height: var(--lh-tight); font-weight: 800; letter-spacing: -0.038em; }
h2 { font-size: var(--step-2); font-weight: 750; letter-spacing: -0.03em; }
h3 { font-size: var(--step-1); letter-spacing: -0.022em; }
h4 { font-size: var(--step-0); letter-spacing: -0.015em; }
/* An accent word inside a headline - no italic now, the colour does the work. */
h1 em, h1 i, h2 em, h2 i { font-style: normal; color: var(--brand-ink); }

p, ul, ol, dl, table, figure, blockquote { margin: 0 0 var(--sp-s); }
p { text-wrap: pretty; }

a { color: var(--brand-ink); text-decoration-thickness: 1px; text-underline-offset: .18em; }
a:hover { color: var(--brand-deep); }

:focus-visible { outline: 2.5px solid var(--focus); outline-offset: 2px; border-radius: 2px; }

img, svg, video { max-width: 100%; height: auto; }
hr { border: 0; border-top: 1px solid var(--rule); margin: var(--sp-l) 0; }
::selection { background: var(--brand-soft); color: var(--ink); }

/* Every number on this site is comparison material. */
.num, td.num, th.num, time, .rank-num, .stat-value, .data-row dd {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* 3. LAYOUT ============================================================== */

.wrap {
  width: 100%;
  max-width: 75rem;
  margin-inline: auto;
  /* Gutters respect notched displays without ever exceeding the viewport. */
  padding-inline: max(var(--sp-s), env(safe-area-inset-left));
}
@media (min-width: 40rem) { .wrap { padding-inline: var(--sp-l); } }
@media (min-width: 64rem) { .wrap { padding-inline: var(--sp-xl); } }

main { display: block; padding-bottom: var(--sp-2xl); }

/* Every top-level block inside a wrap is separated, not just <section> pairs:
   the home page stacks .duo wrappers, which `section + section` never matched,
   so two card grids sat flush against one another. */
section + section,
.duo + .duo,
.duo + section,
section + .duo { margin-top: var(--sp-2xl); }

/* --- duo: two columns on desktop, stacked on mobile.
   The single-column-forever layout is what made every page feel sparse - on a
   1440px screen it left two thirds of the width empty and pushed everything
   into a long vertical ribbon. */
.duo { display: grid; gap: var(--sp-2xl); }
@media (min-width: 58rem) {
  .duo { grid-template-columns: 1fr 1fr; gap: var(--sp-xl) var(--sp-2xl); }
  .duo-wide { grid-template-columns: 1.45fr 1fr; }
}
.duo > section + section,
.duo > * + * { margin-top: 0; }

/* --- band: a full-bleed section, separated by a rule rather than a fill.
   Colour fields were tried and removed: the brief is a neutral ground with teal
   used only as accent, so bands now mark structure with hairlines and space. */
.band { width: 100%; }
.band > .wrap { padding-block: var(--sp-xl); }
/* Bands are full-bleed and carry their own vertical padding, so adjacent ones
   must not ALSO take the sibling margin below - that stacked 60px of margin on
   top of 60px+60px of padding and left a canyon between sections. The padding
   alone gives ~84px between one band's content and the next. */
main > .band + .band { margin-top: 0; }
.band-hairline { border-block: 1px solid var(--rule); background: var(--surface); }

/* --- panel: content lifted off the paper */
.panel {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-l);
  padding: var(--sp-m);
  box-shadow: var(--shadow-1);
}
@media (min-width: 48rem) { .panel { padding: var(--sp-m) var(--sp-l); } }

.skip-link {
  position: absolute; left: -9999px; z-index: 100;
  background: var(--brand-fill); color: var(--on-brand);
  padding: var(--sp-2xs) var(--sp-s); font-weight: 600;
}
.skip-link:focus { left: 0; top: 0; }

/* 4. TYPE ================================================================ */

.page-header { padding: var(--sp-m) 0 var(--sp-s); }

.lede {
  font-size: var(--step-1);
  line-height: 1.45;
  color: var(--ink-2);
  max-width: 62ch;
  margin-bottom: var(--sp-s);
  text-wrap: pretty;
}

/* Small tracked label - the connective tissue of the whole design. */
.label, .data-row dt, .stat-label, .footer-col h2, thead th, .eyebrow {
  font-size: var(--step--2);
  text-transform: uppercase;
  letter-spacing: .1em;
  font-weight: 620;
  color: var(--ink-3);
  line-height: 1.3;
}
.eyebrow { display: block; margin-bottom: var(--sp-2xs); color: var(--brand-ink); }

.byline {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2xs) var(--sp-s);
  margin-top: var(--sp-s); padding-top: var(--sp-2xs);
  border-top: 1px solid var(--rule);
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .1em;
  font-weight: 620; color: var(--ink-3);
}
.byline time { text-transform: none; letter-spacing: .04em; }
.byline > * + *::before { content: "·"; margin-right: var(--sp-s); color: var(--rule-2); }
.muted { color: var(--ink-3); font-size: var(--step--1); }

/* --- prose.
   Tuned to what this content actually is. Across every article and question
   there are 430 list items, 92 h2s and 41 h3s, but only 7 blockquotes and 3
   images: these are heavily-sectioned practical guides, not essays. So the
   effort goes into heading rhythm and list texture, and no drop cap - a
   versal on "Read fee structure for M.Sc..." would be decoration fighting
   the content type. */
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--sp-s); }
.prose > :first-child { margin-top: 0; }

/* Sections need to be findable when there are six or seven per page. The rule
   is short and sits above the heading, so it reads as a section mark rather
   than as a box lid. */
.prose h2 {
  margin-top: var(--sp-xl); padding-top: var(--sp-s);
  font-size: var(--step-2); position: relative;
}
.prose h2::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 2rem; height: 2px; background: var(--brand);
}
.prose h3 { margin-top: var(--sp-l); font-size: var(--step-1); color: var(--ink); }
.prose h2 + *, .prose h3 + * { margin-top: var(--sp-2xs); }

/* Lists carry most of this content. Markers hang outside the text column so
   the left edge of the prose stays straight, and ordered lists get the same
   lining serif numerals the rankings use. */
.prose ul, .prose ol { padding-left: 1.35em; }
.prose li { padding-left: .15em; }
.prose li + li { margin-top: var(--sp-2xs); }
.prose li > ul, .prose li > ol { margin-top: var(--sp-2xs); }
.prose ul > li::marker { color: var(--brand); content: "\2013\00a0"; }   /* en dash */
.prose ol > li::marker { font-variant-numeric: tabular-nums; color: var(--brand-ink); font-weight: 700; }

.prose img { border-radius: var(--radius); display: block; }
.prose figure { margin-inline: 0; }
.prose figcaption { margin-top: var(--sp-2xs); font-size: var(--step--1); color: var(--ink-3); }

.prose a { text-decoration-thickness: 1px; text-underline-offset: .16em; }
.prose strong { font-weight: 650; }

/* The pull quote - the one place on the site that uses the italic face, and
   the reason it is declared at all. */
.prose blockquote {
  margin: var(--sp-l) 0; padding: var(--sp-s) var(--sp-m);
  border-left: 3px solid var(--brand);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: var(--surface-2);
  font-size: var(--step-1); font-weight: 500; line-height: 1.45; color: var(--ink-2);
}
.prose blockquote p { margin: 0; }
.prose blockquote > * + * { margin-top: var(--sp-2xs); }

.prose code {
  font-family: var(--font-mono); font-size: .88em;
  background: var(--surface-2); padding: .1em .35em; border-radius: var(--radius-s);
}
/* Tables inside CMS prose stay real tables and simply fit their column: no
   scroll container at all. The earlier `width: max-content` + `nowrap` headers
   guaranteed a sideways scroll on any table wider than the measure, which is
   exactly what the no-scrollbar rule forbids. Every prose table here has 2 to
   4 columns of text, so wrapping is the honest answer; below 46rem they stack
   into labelled cards instead. */
.prose table {
  display: table; width: 100%; table-layout: auto;
  font-size: var(--step--1);
}
/* `white-space: normal` is load-bearing. The global `thead th` rule sets
   nowrap - correct for the site's own short column heads, wrong for a CMS
   heading like "BSCS Starting Salary in Semi Government Companies", which
   could not wrap and pushed a table to 1073px inside a 753px viewport. A
   table cannot shrink below its min-content width, so `width: 100%` alone
   does not save it. */
.prose th { text-align: left; white-space: normal; }
.prose thead th { border-bottom: 1.5px solid var(--ink); padding: 0 var(--sp-s) var(--sp-2xs) 0; }
.prose td { padding: var(--sp-2xs) var(--sp-s) var(--sp-2xs) 0; border-bottom: 1px solid var(--rule); }

/* 5. HEADER ============================================================== */

/* The bar is exactly one row at every width: brand left, toggle (or the full
   menu, from 60rem) right. It never wraps, so it never grows taller than
   --header-h and never pushes past the viewport edge. */
.site-header {
  position: sticky; top: 0; z-index: 60;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  transition: box-shadow .25s var(--ease);
}
/* Depth appears only once the page is scrolled under it - at rest the header
   is flush with the paper. */
.site-header.is-stuck { box-shadow: 0 1px 0 var(--rule), 0 10px 26px -18px rgb(20 24 26 / .30); }

/* With no JavaScript the nav cannot collapse, so a sticky header would eat the
   screen. It scrolls away instead. */
html:not(.js) .site-header { position: static; }

.header-bar {
  display: flex; align-items: center; flex-wrap: nowrap;
  gap: var(--sp-s); min-height: var(--header-h);
}

.brand {
  display: inline-flex; align-items: center; gap: .5rem;
  margin-right: auto; min-width: 0;
  font-family: var(--font-display);
  font-weight: 750; font-size: var(--step-1);
  letter-spacing: -.04em; color: var(--word);
  text-decoration: none; white-space: nowrap;
}
.brand-mark {
  flex: 0 0 auto; width: 32px; height: 32px;
  background: var(--logo) center / contain no-repeat;
  transition: transform .25s var(--ease);
}
.brand:hover { color: var(--word); }
.brand:hover .brand-mark { transform: scale(1.06) rotate(-4deg); }
/* The ".pk" flips once on load, then rests.
   The cycle is 7s but the rotation only occupies the first 16% of it, so it
   reads as a periodic wink rather than something spinning in the corner of
   every page. A header element in constant motion is genuinely hard to read
   past, and this sits next to the site's own name.
   `perspective` on the parent is what makes it a coin flip rather than a flat
   squash. Reduced-motion is handled globally in section 2 and explicitly
   below, so the mark simply sits still for anyone who asks for that. */
.brand { perspective: 420px; }
.brand-tld {
  color: var(--tld);
  display: inline-block;
  transform-origin: 50% 50%;
  animation: tld-spin 7s cubic-bezier(.55, .06, .22, 1) infinite;
}
@keyframes tld-spin {
  0%   { transform: rotateY(0deg); }
  16%  { transform: rotateY(360deg); }
  100% { transform: rotateY(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .brand-tld { animation: none; transform: none; }
}

/* --- theme toggle. Sun and moon are stacked in the same box and cross-faded,
   so the control never changes size and nothing shifts around it. */
.theme-toggle {
  position: relative; flex: 0 0 auto;
  display: grid; place-items: center;
  width: 2.35rem; height: 2.35rem; padding: 0;
  color: var(--ink-2); background: var(--surface-2);
  border: 1px solid transparent; border-radius: 999px;
  cursor: pointer;
  transition: color .16s var(--ease), background-color .16s var(--ease), border-color .16s var(--ease);
}
.theme-toggle[hidden] { display: none; }
.theme-toggle:hover { color: var(--brand-ink); border-color: var(--brand); background: var(--brand-soft); }
.theme-toggle svg { grid-area: 1 / 1; transition: opacity .2s var(--ease), transform .3s var(--ease); }
.icon-moon { opacity: 0; transform: rotate(-45deg) scale(.7); }
.theme-toggle[aria-pressed="true"] .icon-sun  { opacity: 0; transform: rotate(45deg) scale(.7); }
.theme-toggle[aria-pressed="true"] .icon-moon { opacity: 1; transform: none; }

/* --- toggle (mobile only, and only when scripted) */
.nav-toggle { display: none; }
.js .nav-toggle {
  display: inline-flex; align-items: center; gap: .5rem; flex: 0 0 auto;
  padding: .4rem .75rem .4rem .65rem;
  font: inherit; font-size: var(--step--1); font-weight: 620;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--rule-2); border-radius: 999px;
  cursor: pointer;
  transition: border-color .15s var(--ease), color .15s var(--ease), background-color .15s var(--ease);
}
.js .nav-toggle:hover { border-color: var(--brand); color: var(--brand-ink); }
[data-open] .nav-toggle { border-color: var(--brand); color: var(--brand-ink); background: var(--brand-soft); }
.nav-toggle-box { position: relative; width: 15px; height: 11px; flex: 0 0 auto; }
.nav-toggle-bar,
.nav-toggle-bar::before,
.nav-toggle-bar::after {
  position: absolute; left: 0; width: 100%; height: 1.75px;
  background: currentColor; border-radius: 2px;
  transition: transform .22s var(--ease), background-color .22s var(--ease);
}
.nav-toggle-bar { top: 4.6px; }
.nav-toggle-bar::before { content: ""; top: -4.6px; }
.nav-toggle-bar::after  { content: ""; top: 4.6px; }
[data-open] .nav-toggle-bar { background: transparent; }
[data-open] .nav-toggle-bar::before { transform: translateY(4.6px) rotate(45deg); }
[data-open] .nav-toggle-bar::after  { transform: translateY(-4.6px) rotate(-45deg); }

.site-nav { min-width: 0; }
.nav-list { display: flex; list-style: none; margin: 0; padding: 0; }
.nav-item-wrap { position: static; }

.nav-item > summary {
  display: flex; align-items: center;
  cursor: pointer; list-style: none;
  font-weight: 570; color: var(--ink-2);
}
.nav-item > summary::-webkit-details-marker { display: none; }
.nav-item > summary::after {
  content: ""; flex: 0 0 auto;
  width: .38em; height: .38em;
  border-right: 1.6px solid currentColor; border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translateY(-.12em); opacity: .5;
  transition: transform .2s var(--ease);
}
.nav-item > summary:hover { color: var(--ink); }
.nav-item[open] > summary { color: var(--brand-ink); }
.nav-item[open] > summary::after { transform: rotate(-135deg) translateY(-.12em); opacity: 1; }

.nav-panel ul { list-style: none; margin: 0; padding: 0; }
.nav-panel a {
  display: block; color: var(--ink-2); text-decoration: none;
  transition: color .14s var(--ease);
}
.nav-panel a:hover { color: var(--brand-ink); }

.nav-search {
  display: flex; align-items: center; gap: .45rem;
  color: var(--ink-2); text-decoration: none; font-weight: 570;
}
.nav-search:hover { color: var(--brand-ink); }
.nav-search-icon { flex: 0 0 auto; }

/* --- mobile: one panel, its own background, dropped below the bar --------- */
@media (max-width: 59.99rem) {
  .js .site-nav {
    position: absolute; left: 0; right: 0; top: 100%;
    display: none;
    background: var(--surface);
    border-bottom: 1px solid var(--rule);
    box-shadow: var(--shadow-3);
    /* dvh, not vh: mobile browser chrome must not clip the last item. */
    max-height: calc(100dvh - var(--header-h));
    overflow-y: auto; overscroll-behavior: contain;
    padding-bottom: var(--sp-2xs);
  }
  .js .site-header[data-open] .site-nav { display: block; }

  .nav-list { flex-direction: column; align-items: stretch; }
  .nav-item-wrap + .nav-item-wrap { border-top: 1px solid var(--rule); }

  .nav-item > summary,
  .nav-search {
    justify-content: space-between;
    padding: var(--sp-xs) max(var(--sp-s), env(safe-area-inset-left));
    font-size: var(--step-0);
    min-height: 2.9rem;   /* comfortable tap target */
  }
  @media (min-width: 40rem) {
    .nav-item > summary, .nav-search { padding-inline: var(--sp-l); }
  }
  .nav-search { justify-content: flex-start; }

  /* The nested list gets a tinted ground so an open group is unmistakable. */
  .nav-panel {
    background: var(--surface-2);
    border-top: 1px solid var(--rule);
    padding: var(--sp-3xs) 0;
  }
  .nav-panel a {
    padding: var(--sp-2xs) max(var(--sp-s), env(safe-area-inset-left));
    padding-left: calc(max(var(--sp-s), env(safe-area-inset-left)) + var(--sp-s));
    font-size: var(--step--1); min-height: 2.75rem;   /* 44px touch target */
    display: flex; align-items: center;
  }
  @media (min-width: 40rem) {
    .nav-panel a { padding-left: calc(var(--sp-l) + var(--sp-s)); }
  }
  .nav-panel a:hover { background: var(--brand-soft); }
}

/* --- desktop: inline items with dropdowns -------------------------------- */
@media (min-width: 60rem) {
  /* The full menu is on show, so the toggle has nothing to reveal. */
  .js .nav-toggle { display: none; }

  .nav-list { align-items: center; gap: var(--sp-3xs); }
  .nav-item-wrap { position: relative; flex: 0 0 auto; }

  .nav-item > summary,
  .nav-search {
    gap: var(--sp-3xs);
    padding: var(--sp-2xs) var(--sp-xs);
    font-size: var(--step--1);
    white-space: nowrap;
    border-radius: var(--radius-s);
    transition: background-color .14s var(--ease), color .14s var(--ease);
  }
  .nav-item > summary:hover, .nav-search:hover { background: var(--surface-2); }
  .nav-item[open] > summary { background: var(--brand-soft); }

  .nav-panel {
    position: absolute; right: 0; top: calc(100% + .55rem);
    min-width: 14rem; width: max-content; max-width: 20rem;
    background: var(--surface);
    border: 1px solid var(--rule); border-radius: var(--radius);
    box-shadow: var(--shadow-2);
    padding: var(--sp-3xs);
  }
  /* Bridges the gap between summary and panel so the pointer can cross it. */
  .nav-panel::before { content: ""; position: absolute; left: 0; right: 0; top: -.55rem; height: .55rem; }
  .nav-panel a {
    padding: var(--sp-2xs) var(--sp-xs);
    font-size: var(--step--1); border-radius: var(--radius-s);
  }
  .nav-panel a:hover { background: var(--surface-2); }
}

/* 6. COMPONENTS ========================================================== */

/* --- breadcrumbs: quiet, not a design feature */
.breadcrumbs { padding-top: var(--sp-s); }
.breadcrumbs ol {
  display: flex; flex-wrap: wrap; gap: var(--sp-3xs) var(--sp-2xs);
  list-style: none; margin: 0; padding: 0;
  font-size: var(--step--2); color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .07em; font-weight: 600;
}
.breadcrumbs li { display: flex; align-items: center; gap: var(--sp-2xs); }
.breadcrumbs li + li::before { content: "/"; color: var(--rule-2); }
.breadcrumbs a { color: var(--ink-3); text-decoration: none; }
.breadcrumbs a:hover { color: var(--brand); }
.breadcrumbs [aria-current] { color: var(--ink-2); }

/* --- masthead: the institute/degree identity block */
.masthead { padding: var(--sp-m) 0 var(--sp-s); }
.masthead h1 { margin-bottom: var(--sp-2xs); }

/* --- editorial masthead, for the two templates that are actually read rather
   than consulted. The block is held to roughly the prose measure so the
   headline breaks on a sensible line instead of running the full 75rem. */
.article > .masthead,
.question > .masthead,
.exam > .masthead,
.degree > .masthead {
  max-width: 46rem;
  padding-block: var(--sp-l) var(--sp-m);
}
.article > .masthead h1,
.question > .masthead h1,
.exam > .masthead h1,
.degree > .masthead h1 {
  font-size: var(--step-4); line-height: 1.06; letter-spacing: -.026em;
  margin-bottom: var(--sp-s);
}
/* The lede is the direct answer, which is what answer engines lift, so it is
   given the display face and real size rather than treated as caption text. */
.article > .masthead .lede,
.question > .masthead .lede,
.exam > .masthead .lede,
.degree > .masthead .lede,
.answer .lede {
  font-size: var(--step-1); font-weight: 450; line-height: 1.5;
  color: var(--ink-2); max-width: 54ch; margin-bottom: 0;
}

/* On a question page the lede IS the answer, so it is labelled and marked. */
.answer {
  max-width: 46rem;
  margin: var(--sp-s) 0 var(--sp-m);
  padding-left: var(--sp-m);
  border-left: 2px solid var(--brand);
}
.answer .lede { margin-bottom: 0; }
.answer-label {
  display: block; margin-bottom: var(--sp-3xs);
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .14em;
  font-weight: 700; color: var(--brand-ink);
}

/* Institute masthead: logo and rank flanking the name. */
.institute-masthead { border-bottom: 1px solid var(--rule); }
.institute-masthead > .wrap { padding-block: var(--sp-s) var(--sp-m); }
.institute-masthead h1 { margin: 0; }
.masthead-top {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--sp-s) var(--sp-m);
}
.masthead-headings { min-width: 0; }
.masthead-logo {
  width: 3.25rem; height: 3.25rem;
  object-fit: contain; background: #fff;
  border: 1px solid var(--rule);
  padding: 5px; border-radius: var(--radius-s);
}
.rank-badge { grid-column: 2; display: flex; align-items: baseline; gap: .35em; }
@media (min-width: 48rem) {
  .masthead-top { grid-template-columns: auto 1fr auto; }
  .masthead-logo { width: 4.75rem; height: 4.75rem; }
  .rank-badge { grid-column: 3; justify-self: end; text-align: right; }
}

/* The signature element: rank set as a large tabular numeral. */
.rank-num {
  font-size: var(--step-3); font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.045em; line-height: 1; color: var(--brand-ink);
}
.rank-num::before { content: "#"; font-size: .55em; vertical-align: .35em; opacity: .5; margin-right: .04em; }
.rank-caption { font-size: var(--step--2); text-transform: uppercase; letter-spacing: .08em; color: var(--ink-3); max-width: 5.5em; text-align: left; line-height: 1.2; }

/* --- data rows: rules, not a boxed panel */
.data-rows { margin: 0 0 var(--sp-l); }
.data-rows.panel .data-row:last-child { border-bottom: 0; padding-bottom: 0; }
.data-rows.panel .data-row:first-child { padding-top: 0; }
.data-row {
  display: grid;
  grid-template-columns: 8rem 1fr;
  gap: var(--sp-s);
  align-items: baseline;
  padding: var(--sp-2xs) 0;
  border-bottom: 1px solid var(--rule);
  margin: 0;
}
.data-row dt { margin: 0; padding-top: .2em; }
/* The value takes the display face. On a spec sheet the fact is the content,
   and setting it in the same sans as the label made every row read as flat
   running text. Works for a word ("Public") as well as a figure ("4 Years"). */
.data-row dd {
  margin: 0;
  font-weight: 650;
  font-size: var(--step-1); letter-spacing: -.015em; line-height: 1.3;
  font-variant-numeric: tabular-nums;
}
/* Opt-out for values that are not facts to be scanned but strings to be read
   or copied - an address, an email, a phone number, a link to a site. */
.data-row dd.plain { font-weight: 500; letter-spacing: 0; }

/* On a wide screen the spec sheet is two columns of rows, not one long list. */
@media (min-width: 58rem) {
  .data-rows { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--sp-xl); }
  .data-rows.panel .data-row:nth-child(2) { padding-top: 0; }
}
.data-row dd a { text-decoration: none; }
.data-row dd a:hover { text-decoration: underline; }
@media (max-width: 26rem) { .data-row { grid-template-columns: 1fr; gap: 0; } }

/* --- stat strip: a few numbers, given room */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
  gap: var(--sp-s) var(--sp-m);
  padding: var(--sp-s) 0 0;
  margin: var(--sp-m) 0 0;
  border-top: 1px solid var(--rule);
}
.stat-value {
  display: block;
  font-size: var(--step-2); font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.04em; line-height: 1.05; color: var(--ink);
}
.stat-label { display: block; margin-top: var(--sp-3xs); }

/* --- index list: the primary way of showing many institutes/degrees.
   Rows sit on their own surface with a logo avatar. The 213 institute logos on
   Cloudinary were going unused; they are the site's only real imagery and they
   double as recognition cues, so they earn their place. */
.index-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2xs); }
.index-item {
  position: relative;
  display: grid;
  grid-template-columns: var(--rank-col, 3rem) 1fr auto;
  gap: 0 var(--sp-s);
  align-items: center;
  padding: var(--sp-xs) var(--sp-s);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease), transform .18s var(--ease);
}
.index-item:hover {
  border-color: var(--brand);
  box-shadow: var(--shadow-2);
  transform: translateY(-2px);
}
.index-item:hover .index-title a { color: var(--brand-ink); }

/* Two columns of rows once there is width for them - a 10-item ranking is
   otherwise a very tall, very narrow strip on a desktop screen. */
@media (min-width: 58rem) {
  .index-list.two-col { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--sp-xl); }
}

/* Institute logo avatar */
/* Institute lists that lead with a logo instead of a rank. */
.index-list.logo-list { --rank-col: 2.9rem; }
@media (min-width: 40rem) { .index-list.logo-list { --rank-col: 3.4rem; } }

.avatar {
  /* `1 / -1` does NOT work here: -1 counts back from the end of the EXPLICIT
     grid, and these rows are all implicit (no grid-template-rows), so it
     silently resolved to row 1 only and the logo sat top-aligned with 67px of
     dead space beneath it. Spanning a fixed count is the reliable form; empty
     implicit rows cost nothing because the row gap is 0. */
  grid-row: 1 / span 4;
  align-self: center;
  width: 100%; max-width: 2.9rem; aspect-ratio: 1;
  height: auto;
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: var(--radius-s);
  padding: 3px;
}
@media (min-width: 40rem) { .avatar { max-width: 3.4rem; } }
.avatar-fallback {
  display: grid; place-items: center;
  font-family: var(--font-display);
  font-size: var(--step-0); font-weight: 700;
  /* --brand on --brand-soft measured 3.16:1 - these initials stand in for a
     missing logo and carry real information, so they take the deep tone
     (6.53:1) rather than the mark colour. */
  color: var(--brand-deep); background: var(--brand-soft);
  border-color: transparent;
  letter-spacing: -.02em;
}
/* The rank as a filled badge: on a card it needs a shape to sit in. */
.index-rank {
  grid-row: 1 / span 4;
  align-self: center; justify-self: center;
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem;
  font-size: var(--step-0); font-weight: 750;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.03em; line-height: 1;
  color: var(--brand-ink); background: var(--brand-soft);
  border-radius: var(--radius-s);
  transition: background-color .18s var(--ease), color .18s var(--ease);
}
.index-item:hover .index-rank { background: var(--brand-fill); color: var(--on-brand); }

/*
 * Medal colours for the top three, on genuinely ranked lists only.
 *
 * Scoped to `ol.index-list` deliberately: an <ol> is what the ranked lists use
 * (home ranking, /top-universities), while institute listings are name-sorted
 * <ul>s. Colouring "first" on a list sorted alphabetically would be a lie told
 * in gold.
 *
 * Both themes are set from the same hue with different lightness, and each pair
 * was checked rather than eyeballed - the bronze on white is the tight one.
 */
ol.index-list > .index-item:nth-child(1) .index-rank { background: hsl(45 85% 90%); color: hsl(38 90% 26%); }
ol.index-list > .index-item:nth-child(2) .index-rank { background: hsl(210 16% 91%); color: hsl(212 20% 32%); }
ol.index-list > .index-item:nth-child(3) .index-rank { background: hsl(25 60% 91%); color: hsl(22 75% 30%); }
ol.index-list > .index-item:nth-child(-n+3):hover .index-rank { background: var(--brand-fill); color: var(--on-brand); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) ol.index-list > .index-item:nth-child(1) .index-rank { background: hsl(45 40% 16%); color: hsl(45 85% 68%); }
  :root:not([data-theme="light"]) ol.index-list > .index-item:nth-child(2) .index-rank { background: hsl(210 14% 20%); color: hsl(210 20% 76%); }
  :root:not([data-theme="light"]) ol.index-list > .index-item:nth-child(3) .index-rank { background: hsl(25 35% 17%); color: hsl(25 70% 66%); }
}
:root[data-theme="dark"] ol.index-list > .index-item:nth-child(1) .index-rank { background: hsl(45 40% 16%); color: hsl(45 85% 68%); }
:root[data-theme="dark"] ol.index-list > .index-item:nth-child(2) .index-rank { background: hsl(210 14% 20%); color: hsl(210 20% 76%); }
:root[data-theme="dark"] ol.index-list > .index-item:nth-child(3) .index-rank { background: hsl(25 35% 17%); color: hsl(25 70% 66%); }

/* --- school stage chips: one hue per stage, so the four bands of school life
   are distinguishable at a glance rather than being thirteen identical pills. */
.level-chips a[data-stage] { border-color: hsl(var(--sh) 40% 84%); background: hsl(var(--sh) 60% 97%); color: hsl(var(--sh) 55% 28%); }
.level-chips a[data-stage]:hover { background: hsl(var(--sh) 65% 93%); border-color: hsl(var(--sh) 50% 62%); color: hsl(var(--sh) 60% 22%); }
.level-chips a[data-stage="Early years"] { --sh: 315; }
.level-chips a[data-stage="Primary"] { --sh: 152; }
.level-chips a[data-stage="Middle"] { --sh: 25; }
.level-chips a[data-stage="Secondary (Matric)"] { --sh: 222; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .level-chips a[data-stage] { background: hsl(var(--sh) 26% 13%); border-color: hsl(var(--sh) 22% 24%); color: hsl(var(--sh) 55% 72%); }
  :root:not([data-theme="light"]) .level-chips a[data-stage]:hover { background: hsl(var(--sh) 30% 17%); border-color: hsl(var(--sh) 45% 44%); color: hsl(var(--sh) 65% 80%); }
}
:root[data-theme="dark"] .level-chips a[data-stage] { background: hsl(var(--sh) 26% 13%); border-color: hsl(var(--sh) 22% 24%); color: hsl(var(--sh) 55% 72%); }
:root[data-theme="dark"] .level-chips a[data-stage]:hover { background: hsl(var(--sh) 30% 17%); border-color: hsl(var(--sh) 45% 44%); color: hsl(var(--sh) 65% 80%); }

.index-title {
  grid-column: 2;
  font-weight: 650; font-size: var(--step-1);
  letter-spacing: -.018em; line-height: 1.3;
  margin: 0; align-self: end;
}
.index-title a { color: var(--ink); text-decoration: none; }
.index-title a::after { content: ""; position: absolute; inset: 0; }
.index-meta { grid-column: 2; font-size: var(--step--1); color: var(--ink-3); margin: var(--sp-3xs) 0 0; align-self: start; }
.index-desc { grid-column: 2; margin: var(--sp-3xs) 0 0; font-size: var(--step--1); color: var(--ink-2); max-width: 62ch; }
/* Sits in the third column, so it stays out of the title's way. */
.index-aside {
  grid-column: 3; grid-row: 1;
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap;
}
@media (max-width: 34rem) { .index-aside { display: none; } }

/* Inline rank inside a meta line, for logo-led lists that are NOT in rank
   order - used but never styled until now, so it rendered as plain grey text. */
.rank-chip {
  font-weight: 680; color: var(--brand-ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}

/* --- card fact row.
   The values a student compares, on the card rather than one click away. Set
   as label-under-value pairs so the numbers align across a column of cards
   even when the names above them wrap to different heights. */
.card-facts {
  grid-column: 2; margin: var(--sp-2xs) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-3xs) var(--sp-s);
  list-style: none;
  font-size: var(--step--2); color: var(--ink-3); line-height: 1.35;
}
.card-facts b {
  font-weight: 750; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
  font-size: var(--step--1);
}
.index-item:hover .card-facts b { color: var(--brand-ink); }

/* Sector reads as a status, so it takes a pill rather than another number.
   Public and private are the one distinction students filter on first. */
.fact-tag {
  padding: .12rem .5rem; border-radius: 999px;
  background: var(--surface-2); color: var(--ink-2);
  font-weight: 650; font-size: var(--step--2); letter-spacing: .01em;
}
.fact-tag.is-public { background: var(--brand-soft); color: var(--brand-deep); }
@media (max-width: 30rem) { .card-facts { gap: var(--sp-3xs) var(--sp-xs); } }

.index-item.no-rank { grid-template-columns: 1fr auto; }
/* `background: none` used to live here to cancel the hanging-rank rail
   gradient. That gradient is long gone, and the rule survived only to
   out-specify `.tinted` (two classes against one) and flatten every category
   colour on the listings back to a single fallback. */
.index-item.no-rank:not(.tinted):hover { background: var(--surface-2); }
/* Rank-less cards have no first column, so everything sits in column 1.
   Without this the title landed in the `auto` track and shrank to a ragged
   right-aligned block while the date wrapped down the narrow left column. */
.index-item.no-rank .index-title,
.index-item.no-rank .index-meta,
.index-item.no-rank .index-desc,
.index-item.no-rank .card-facts { grid-column: 1; }
.index-item.no-rank .index-title { align-self: auto; }

/* --- shelf: a wrapping tile grid.
   This was a horizontal scroll-snap row. It worked, but it hid most of its
   contents behind a gesture and put a scrollbar across the page - so it now
   wraps. auto-fill packs three tiles across a 360px phone and six across a
   desktop, showing every city at once instead of four. */
.shelf {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: var(--sp-2xs);
  margin: 0 0 var(--sp-s);
  padding: 0;
  list-style: none;
}
@media (min-width: 40rem) { .shelf { grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); } }
.shelf a {
  display: flex; flex-direction: column; justify-content: space-between;
  height: 100%; min-height: 4.4rem;
  padding: var(--sp-xs) var(--sp-s);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  text-decoration: none; color: var(--ink);
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease), transform .16s var(--ease);
}
.shelf a { box-shadow: var(--shadow-1); }
.shelf a:hover { border-color: var(--brand); box-shadow: var(--shadow-2); transform: translateY(-2px); }
/* Wider tiles for the handful of tool cards, which carry a sentence not a count. */
.shelf-lg { grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); }
@media (min-width: 40rem) { .shelf-lg { grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); } }
.shelf-lg a { min-height: 5.25rem; }
.shelf-lg .shelf-count { text-transform: none; letter-spacing: 0; }

.shelf .shelf-name { font-weight: 620; font-size: var(--step--1); letter-spacing: -.015em; line-height: 1.25; }
.shelf .shelf-count { font-size: var(--step--2); color: var(--ink-3); font-variant-numeric: tabular-nums; margin-top: var(--sp-3xs); }
.shelf a:hover .shelf-count { color: var(--brand-ink); }

/* --- section headers */
.section-head {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-s); flex-wrap: wrap;
  margin-bottom: var(--sp-m);
}
.section-head::after { content: none; }
.section-head h2 { margin: 0; font-size: var(--step-2); }
.section-head a {
  font-size: var(--step--1); font-weight: 650; text-decoration: none; white-space: nowrap;
  padding: .3rem .8rem; border-radius: 999px; background: var(--surface-2);
  transition: background-color .15s var(--ease), color .15s var(--ease);
}
.section-head a:hover { background: var(--brand-soft); color: var(--brand-deep); }

/* --- tabs.
   These used to scroll sideways with the scrollbar hidden, which meant a fourth
   tab could be entirely invisible on a phone with no affordance at all. They
   wrap now: at most four items, so a second row is the worst case. */
.tabs { border-bottom: 1px solid var(--rule); margin-bottom: var(--sp-m); }
.tabs ul { display: flex; flex-wrap: wrap; gap: 0 var(--sp-m); list-style: none; margin: 0; padding: 0; }
.tabs a, .tabs span {
  display: block; padding: var(--sp-2xs) 0 var(--sp-xs);
  font-size: var(--step--1); font-weight: 600;
  text-decoration: none; color: var(--ink-3);
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: color .14s var(--ease), border-color .14s var(--ease);
}
.tabs a:hover { color: var(--ink); border-bottom-color: var(--rule-2); }
.tabs [aria-current] { color: var(--ink); border-bottom-color: var(--brand); }

/* --- tables */
.table-scroll { overflow-x: auto; overscroll-behavior-x: contain; margin-bottom: var(--sp-s); }
table { border-collapse: collapse; width: 100%; font-size: var(--step--1); }
.table-scroll table { min-width: 30rem; }
thead th {
  text-align: left; padding: 0 var(--sp-s) var(--sp-2xs) 0;
  border-bottom: 1.5px solid var(--ink); white-space: nowrap;
}
td { padding: var(--sp-xs) var(--sp-s) var(--sp-xs) 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
tbody tr { transition: background-color .12s var(--ease); }
tbody tr:hover { background: var(--surface-2); }
td.num, th.num { text-align: right; padding-right: 0; }
td a { text-decoration: none; font-weight: 550; }
td a:hover { text-decoration: underline; }

/* Stacked mode for data tables on narrow screens.
   A five-column fee table cannot fit a 360px phone, and the previous answer was
   a sideways scroll inside the page - easy to miss and easy to trap a swipe in.
   Each row becomes a small record card instead, labelled from the column head
   via data-label, so nothing scrolls and nothing is hidden. */
@media (max-width: 46rem) {
  .table-scroll:has(table.stack) { overflow-x: visible; }
  table.stack { min-width: 0; }
  table.stack, table.stack tbody, table.stack tr, table.stack td { display: block; width: 100%; }
  /* display:none, not visually-hidden. Setting display:block on the table,
     tbody, tr and td above already strips the table roles, so an off-screen
     thead no longer provides any header association to recover - it only
     contributed its full-width table-cells to the table's scrollWidth, which
     put the scrollbar back. Each cell carries its heading in data-label. */
  table.stack thead { display: none; }
  table.stack tr {
    padding: var(--sp-xs) var(--sp-s);
    margin-bottom: var(--sp-2xs);
    background: var(--surface);
    border: 1px solid var(--rule);
    border-radius: var(--radius);
  }
  table.stack tr:hover { background: var(--surface); border-color: var(--rule-2); }
  table.stack td {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--sp-s);
    padding: .2rem 0; border: 0; text-align: left;
  }
  table.stack td[data-label]::before {
    content: attr(data-label);
    /* Shrinkable, not `0 0 auto`. The site's own labels are short ("Tuition"),
       but CMS column headings are arbitrary - "Salaries for Experienced
       Programmers" at 36 characters could not shrink, so the flex row's
       min-content exceeded the page and put a scrollbar back. */
    flex: 0 1 auto; min-width: 0; overflow-wrap: break-word;
    font-size: var(--step--2); text-transform: uppercase; letter-spacing: .09em;
    font-weight: 620; color: var(--ink-3);
  }
  /* Prose tables come from the CMS, so the heading can be a full phrase. It
     goes above the value rather than fighting it for the same line. */
  .prose table.stack td[data-label] { display: block; }
  .prose table.stack td[data-label]::before { display: block; margin-bottom: .1rem; }
  /* The first cell is the record's name, so it acts as the card's heading. */
  table.stack td:first-child {
    display: block;
    font-size: var(--step-0); font-weight: 640; letter-spacing: -.015em;
    padding-bottom: var(--sp-2xs); margin-bottom: var(--sp-3xs);
    border-bottom: 1px solid var(--rule);
  }
  table.stack td:first-child::before { content: none; }
  table.stack td.num { text-align: right; font-variant-numeric: tabular-nums; }
}

/* --- ledger: the fee treatment.
   The figure a student actually decides on is the cost, and the spread in this
   data is dramatic - one BSc Computer Science runs 135,000 to 1,890,000 PKR.
   So the spread leads as a display figure, and every amount carries a bar
   proportional to the most expensive programme on the page. The bars encode
   the same number the cell already states, so they are aria-hidden. */
.ledger-lead {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: var(--sp-2xs) var(--sp-s);
  padding: var(--sp-m) 0 var(--sp-s);
  border-bottom: 1.5px solid var(--ink);
  margin-bottom: var(--sp-s);
}
.ledger-figure {
  font-weight: 800;
  font-size: clamp(2.3rem, 1.5rem + 3.9vw, 4.1rem);
  line-height: .92; letter-spacing: -.05em;
  font-variant-numeric: tabular-nums;
  color: var(--brand-ink);
}
.ledger-caption {
  font-size: var(--step--1); color: var(--ink-2);
  max-width: 26ch; line-height: 1.35;
}
.ledger-caption b { display: block; font-weight: 650; color: var(--ink); font-variant-numeric: tabular-nums; }

/* The bar itself, used inside a table cell so the table keeps its semantics. */
td.track { width: 6.5rem; padding-right: 0; vertical-align: middle; }
.bar {
  display: block; height: 5px; width: 100%;
  background: var(--brand-soft); border-radius: 999px; overflow: hidden;
}
/* The floor matters: MBBS totals are 10-500x a general-programme fee, and they
   are real, so a linear scale pushes most rows to a fraction of a percent. The
   number in the adjacent cell is authoritative; this only guarantees the bar
   never vanishes entirely and read as missing data. */
.bar > i {
  display: block; height: 100%;
  width: max(4px, var(--w, 0%));
  background: var(--brand); border-radius: 999px;
}
.bar.is-min > i { background: var(--brand-deep); }
@media (max-width: 46rem) {
  /* In stacked mode the bar spans the card, under the labelled figures. */
  td.track { width: auto; padding-top: var(--sp-2xs); }
  table.stack td.track { display: block; }
  table.stack td.track::before { content: none; }
}

/* --- note band: a quiet editorial aside.
   Reads as a line of house copy rather than an ad unit - no card, no button,
   no colour field. The brand rule on the left is the only marker that it is a
   different kind of statement from the section above it. */
.note-band {
  margin-top: var(--sp-l);
  padding: var(--sp-s) 0 var(--sp-s) var(--sp-m);
  border-left: 3px solid var(--brand);
}
.note-band p {
  margin: 0; max-width: 68ch;
  font-size: var(--step--1); color: var(--ink-2); line-height: 1.55;
}
.note-band strong { color: var(--ink); font-weight: 700; }

/* --- callout */
.callout {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  padding: var(--sp-s) var(--sp-m);
  margin-bottom: var(--sp-s);
}
.callout h3 { margin-top: 0; font-size: var(--step-0); }
.callout > :last-child { margin-bottom: 0; }

/* --- pagination */
.pagination {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-s);
  flex-wrap: wrap;
  margin-top: var(--sp-l); padding-top: var(--sp-s);
  border-top: 1.5px solid var(--ink); font-size: var(--step--1);
}
.pagination a { font-weight: 620; text-decoration: none; }
.pagination a:hover { text-decoration: underline; }

/* --- related */
.related { margin-top: var(--sp-xl); padding-top: var(--sp-s); border-top: 1.5px solid var(--ink); }
.related h2 { font-size: var(--step-1); }
.related ul { list-style: none; padding: 0; }
.related li { border-bottom: 1px solid var(--rule); }
.related li a { display: block; padding: var(--sp-xs) 0; text-decoration: none; font-weight: 550; }
.related li a:hover { color: var(--brand-ink); }

/* --- link columns (dense indexes) */
.link-list { list-style: none; margin: 0; padding: 0; columns: 1; column-gap: var(--sp-l); }
@media (min-width: 30rem) { .link-list { columns: 2; } }
@media (min-width: 52rem) { .link-list { columns: 3; } }
@media (min-width: 72rem) { .link-list { columns: 4; } }
/* Inside a .duo cell there is half the width, so hold at two columns. */
.duo .link-list { columns: 2; }
.link-list li { break-inside: avoid; padding: var(--sp-3xs) 0; font-size: var(--step--1); line-height: 1.4; }
.link-list a { text-decoration: none; }
.link-list a:hover { text-decoration: underline; }
.link-list .muted { white-space: nowrap; }   /* keep "4 Years" off its own line */

/* --- buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 2.7rem; padding: var(--sp-2xs) var(--sp-l);
  background: var(--brand-fill); color: var(--on-brand); border: 1px solid var(--brand-fill);
  border-radius: 999px; font-weight: 700; font-size: var(--step--1);
  letter-spacing: -.005em;
  text-decoration: none; cursor: pointer;
  box-shadow: var(--shadow-1);
  transition: background-color .15s var(--ease), border-color .15s var(--ease),
              box-shadow .15s var(--ease), transform .15s var(--ease);
}
.btn:hover { background: var(--brand-deep); border-color: var(--brand-deep); color: var(--on-brand); box-shadow: var(--shadow-2); transform: translateY(-1px); }
.btn:active { transform: translateY(0); box-shadow: var(--shadow-1); }
.btn-secondary { background: var(--surface); color: var(--brand-ink); border-color: var(--rule-2); }
.btn-secondary:hover { background: var(--brand-soft); border-color: var(--brand-ink); color: var(--brand-deep); }

label { display: block; font-weight: 620; font-size: var(--step--1); margin-bottom: var(--sp-3xs); }
input[type="search"], input[type="text"], input[type="email"], textarea {
  width: 100%; max-width: 26rem; padding: var(--sp-2xs) var(--sp-xs);
  font: inherit; color: var(--ink); background: var(--surface);
  border: 1px solid var(--rule-2); border-radius: var(--radius-s);
}

/* --- home hero: asymmetric and type-led, but no longer a full screen of it.
   The headline used to run to 5.25rem with 3.5rem of padding above it, which
   pushed the first real content - the ranking - entirely below the fold. */
.hero-band { border-bottom: 1px solid var(--rule); background: var(--surface); }
.hero-band > .wrap { padding-block: var(--sp-l) var(--sp-m); }
.hero h1 {
  font-size: var(--step-5); max-width: 14ch; margin-bottom: var(--sp-s);
  line-height: 1.0; letter-spacing: -.032em;
}
.hero .lede { font-size: var(--step-1); max-width: 46ch; margin-bottom: 0; }


/* --- the crescent and star that follows the cursor.

   Behind the page: `z-index: -1` on a layer whose only ancestor is <html>, so
   it paints above the canvas and below every block background and line of text.

   THE OPACITIES BELOW ARE A MEASURED CEILING, NOT A TASTE DECISION.

   Painting underneath is not the free pass it looks like. Text inside a
   transparent container - and `.band` sets no background - has the page canvas
   as its backdrop, so the crescent lands between that text and its background
   and does reduce contrast. On the pages checked, 43 to 60 text nodes per page
   are unshielded like this; the rest sit on an opaque card and are genuinely
   unaffected.

   Compositing the layer into the backdrop of every text node on five
   representative pages and solving for the point where the first one drops
   below AA gives 4.5% in light and 12.7% in dark. These sit just under that.
   Raising them past it will silently fail the accessibility pass.

   An earlier version put this layer ON TOP so it would show over cards. That
   costs 0.8 of a contrast ratio and produced 121 AA failures on one page; the
   most it could carry without failing was 2.4%, which is invisible. Hence: it
   is behind, and it does not appear over cards. That is a real limit, not an
   oversight.

   Only transform is animated, on elements that are already their own layer. */
.crescent-layer {
  position: fixed; inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity .7s var(--ease);
}
.crescent-layer.is-on { opacity: 1; }

.crescent-bit {
  position: absolute; top: 0; left: 0;
  will-change: transform;
  color: var(--brand);
  opacity: .026;                    /* ceiling is 4.5%; sitting well under it */
  filter: blur(1.2px);
}
.crescent-bit.crescent-lead { opacity: .033; filter: blur(1.6px); }
.crescent-bit svg { display: block; fill: currentColor; }

/* Dark has far more headroom - its text sits at 10:1 and up - so the same
   shape can be three times stronger and still clear the same bar. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .crescent-bit { opacity: .090; }
  :root:not([data-theme="light"]) .crescent-bit.crescent-lead { opacity: .110; }
}
:root[data-theme="dark"] .crescent-bit { opacity: .090; }
:root[data-theme="dark"] .crescent-bit.crescent-lead { opacity: .110; }

@media (prefers-reduced-motion: reduce) {
  .crescent-layer { display: none; }
}

/* --- the cycling verb in the headline, typed out: study -> teach -> work.

   Two elements sit in one grid cell. The sizer is invisible and always holds
   the widest of the three words, so the box is that wide from the first frame;
   the live word types inside it, left-aligned. Without the sizer the heading
   would rewrap on almost every keystroke, which at this type size means the
   whole hero jumping several times a second.

   Both words are drawn with `content: attr(data-word)` rather than as text
   nodes. That is what keeps the rendered h1 reading "Where should you study in
   Pakistan?" - generated content is not part of textContent, so a crawler
   running our JS mid-keystroke still extracts the authored sentence and never
   a half-typed word.

   Only `is-ready` carries any of this: until hero.js has built the pair the
   element is a bare <em> and must look exactly like the word it replaces. */
.word-cycle.is-ready { display: inline-grid; vertical-align: baseline; }
.word-cycle-track { display: inline-grid; }
.word-cycle-sizer,
.word-cycle-live { grid-area: 1 / 1; }
/* `visibility: hidden` and not `opacity: 0` - the sizer must still take up its
   width, and must never be read out or hit-tested. */
.word-cycle-sizer { visibility: hidden; padding-right: .16em; }
.word-cycle-live {
  justify-self: start;
  color: hsl(var(--h, 152) 62% 28%);
  transition: color .18s var(--ease);
}
.word-cycle-sizer::before,
.word-cycle-live::before { content: attr(data-word); }

/* The caret. An empty pseudo-element rather than a border on the word itself,
   so it keeps its full height when the word is briefly the empty string. */
.word-cycle-live::after {
  content: '';
  display: inline-block;
  width: .055em; height: .78em;
  margin-left: .05em;
  vertical-align: -.06em;
  background: currentColor;
  animation: caret-blink 1.06s step-end infinite;
}
/* A real caret does not blink while you are typing - it only starts once the
   typing stops. Blinking mid-word reads as a rendering fault. */
.word-cycle.is-typing .word-cycle-live::after { animation: none; }
@keyframes caret-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .word-cycle-live { color: hsl(var(--h, 152) 62% 66%); }
}
:root[data-theme="dark"] .word-cycle-live { color: hsl(var(--h, 152) 62% 66%); }

/* hero.js returns before building anything under reduced motion, so this only
   covers the gap between the stylesheet applying and the script running. */
@media (prefers-reduced-motion: reduce) {
  .word-cycle-live { transition: none; }
  .word-cycle-live::after { animation: none; }
}

/* The dataset stated up front, above the headline - the eyebrow is the site's
   scale, which is the reason to trust the ranking that follows. */
.hero-eyebrow {
  display: block; margin-bottom: var(--sp-s);
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .18em;
  font-weight: 700; color: var(--brand-ink); font-variant-numeric: tabular-nums;
}
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2xs); margin-top: var(--sp-s); }

/* --- hero search: the primary control on the site.
   A single bordered field with the icon inside it and the button flush to the
   right, so it reads as one object rather than three controls in a row. */
.hero-search {
  position: relative;
  display: flex; align-items: center; gap: var(--sp-2xs);
  margin-top: var(--sp-m);
  padding: .3rem .3rem .3rem var(--sp-s);
  background: var(--surface);
  border: 1.5px solid var(--rule-2);
  border-radius: 999px;
  max-width: 30rem;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.hero-search:focus-within {
  border-color: var(--brand);
  box-shadow: 0 0 0 4px var(--brand-soft);
}
/* The icon is decoration. Without this a click on it hit the <svg> and focused
   nothing; now it falls through to the label stretched underneath. */
.hero-search-icon { flex: 0 0 auto; color: var(--ink-3); pointer-events: none; }

/* The full-pill hit area. The label carries no visible text of its own - the
   accessible name sits in a visually-hidden span inside it - so it can cover
   the whole control invisibly and turn every dead pixel into "focus the
   field". Only the input and the button are lifted above it, so a direct
   click on either still reaches them. */
.hero-search-label {
  position: absolute; inset: 0;
  border-radius: inherit;
  cursor: text;
}
.hero-search input[type="search"],
.hero-search .btn { position: relative; z-index: 1; }
.hero-search input[type="search"] {
  flex: 1 1 auto; min-width: 0; max-width: none;
  padding: .45rem 0; border: 0; background: none;
  font: inherit; font-size: var(--step-0); color: var(--ink);
}
.hero-search input[type="search"]:focus { outline: none; }
.hero-search input::placeholder { color: var(--ink-3); }
/* Chrome draws its own clear affordance; it collides with the pill edge. */
.hero-search input[type="search"]::-webkit-search-cancel-button { display: none; }
.hero-search .btn { flex: 0 0 auto; border-radius: 999px; min-height: 2.35rem; padding-inline: var(--sp-m); }
/* Narrow screens: two rows, icon still attached to the field.
   The previous version wrapped with flex and pushed the input to its own row
   with `order: -1`, which stranded the magnifier beside the button looking
   like a stray glyph. A grid keeps the icon where it belongs and lets the
   button span the full width, which is also a better tap target. */
@media (max-width: 30rem) {
  .hero-search {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: var(--sp-2xs) var(--sp-2xs);
    border-radius: var(--radius-l);
    padding: var(--sp-xs);
  }
  .hero-search-icon { grid-area: 1 / 1; }
  .hero-search input[type="search"] { grid-area: 1 / 2; padding-block: .35rem; }
  .hero-search .btn { grid-area: 2 / 1 / 3 / -1; width: 100%; }
}

.hero-quick {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2xs) var(--sp-s);
  margin: var(--sp-s) 0 0; font-size: var(--step--1);
}
.hero-quick > span {
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .12em;
  font-weight: 650; color: var(--ink-3);
}
.hero-quick a { text-decoration: none; border-bottom: 1px solid var(--rule-2); }
.hero-quick a:hover { border-bottom-color: var(--brand); }

/* --- field tiles: pick a subject.
   Icon, name, count. Bordered rather than filled, so the grid stays on the
   paper - a nine-tile block of colour would be the loudest thing on the page
   and this is a way in, not the destination. */
.field-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: var(--sp-2xs);
  list-style: none; margin: 0 0 var(--sp-m); padding: 0;
}
@media (min-width: 40rem) { .field-grid { grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); } }
/* ==========================================================================
   TINTED CARDS - the one colour mechanism, used everywhere.

   A card opts in with class="tinted" and a single `--h` set inline from
   _data/hues.js. Every shade below is derived from that number, in both
   themes, so a card cannot look right on white and muddy on near-black.

   The label always stays --ink. Legibility never depends on the hue; the
   accent (icon, count, marker) carries the colour instead.

   Used ONLY where the tint encodes a real category - field, province, exam
   type, article topic. Rankings, fee tables and search results stay neutral so
   the numbers stay readable.
   ========================================================================== */

/* Every selector here is prefixed with `:root` purely to buy a class of
   specificity. Without it `.tinted` (0,1,0) loses to component rules like
   `.shelf a` (0,1,1) and `.tint-accent` loses to `.shelf .shelf-count`, so the
   tint applied in dark - where the rules already carry a `:root[data-theme]`
   prefix - and silently flattened to one colour in light. Matching the two
   sides keeps the themes from drifting apart again. */
:root .tinted {
  background: hsl(var(--h, 200) 68% 97%);
  border-color: hsl(var(--h, 200) 42% 88%);
}
:root .tinted:hover {
  background: hsl(var(--h, 200) 72% 94%);
  border-color: hsl(var(--h, 200) 55% 70%);
}
/* 28%, not the 34% this started at: green (152), lime (95) and orange (42)
   are the high-luminance hues, and at 34% they landed at 3.81:1 on the hover
   tint - below AA. 28% clears every hue in the registry at 5.25:1 worst case,
   measured against BOTH the resting and the hover ground. */
:root .tint-accent { color: hsl(var(--h, 200) 62% 28%); }
:root .tinted:hover .tint-title { color: hsl(var(--h, 200) 62% 28%); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tinted {
    background: hsl(var(--h, 200) 26% 12%);
    border-color: hsl(var(--h, 200) 22% 22%);
  }
  :root:not([data-theme="light"]) .tinted:hover {
    background: hsl(var(--h, 200) 30% 15%);
    border-color: hsl(var(--h, 200) 45% 42%);
  }
  :root:not([data-theme="light"]) .tint-accent { color: hsl(var(--h, 200) 62% 66%); } /* 62% put violet (268) at 4.27:1 */
  :root:not([data-theme="light"]) .tinted:hover .tint-title { color: hsl(var(--h, 200) 65% 72%); }
}
:root[data-theme="dark"] .tinted {
  background: hsl(var(--h, 200) 26% 12%);
  border-color: hsl(var(--h, 200) 22% 22%);
}
:root[data-theme="dark"] .tinted:hover {
  background: hsl(var(--h, 200) 30% 15%);
  border-color: hsl(var(--h, 200) 45% 42%);
}
:root[data-theme="dark"] .tint-accent { color: hsl(var(--h, 200) 62% 66%); } /* 62% put violet (268) at 4.27:1 */
:root[data-theme="dark"] .tinted:hover .tint-title { color: hsl(var(--h, 200) 65% 72%); }

/* A category label, tinted to match its card. */
.tint-tag {
  display: inline-block;
  padding: .1rem .5rem; border-radius: 999px;
  background: hsl(var(--h, 200) 55% 92%);
  color: hsl(var(--h, 200) 60% 26%);
  font-size: var(--step--2); font-weight: 650; letter-spacing: .01em;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tint-tag { background: hsl(var(--h, 200) 30% 18%); color: hsl(var(--h, 200) 60% 72%); }
}
:root[data-theme="dark"] .tint-tag { background: hsl(var(--h, 200) 30% 18%); color: hsl(var(--h, 200) 60% 72%); }

/* --- field tiles now just use the shared layer. */
.field-grid a {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-3xs);
  height: 100%; padding: var(--sp-m) var(--sp-s);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-1);
  color: var(--ink); text-decoration: none;
  transition: border-color .16s var(--ease), box-shadow .16s var(--ease),
              transform .16s var(--ease), background-color .16s var(--ease);
}
.field-grid a:hover { box-shadow: var(--shadow-2); transform: translateY(-2px); }
.field-icon { margin-bottom: var(--sp-3xs); transition: transform .2s var(--ease); }
.field-grid a:hover .field-icon { transform: scale(1.08); }
.field-name {
  font-weight: 700;
  font-size: var(--step-1); letter-spacing: -.022em; line-height: 1.2;
}
.field-count {
  font-size: var(--step--2); color: var(--ink-2);
  font-variant-numeric: tabular-nums; line-height: 1.3;
}

/* --- level chips: the second axis, kept deliberately quieter than the tiles */
.level-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2xs) var(--sp-s); }
.level-chips { display: flex; flex-wrap: wrap; gap: var(--sp-3xs); list-style: none; margin: 0; padding: 0; }
.level-chips a {
  display: inline-flex; align-items: baseline; gap: .4rem;
  padding: .3rem .7rem;
  border: 1px solid var(--rule); border-radius: 999px;
  font-size: var(--step--1); font-weight: 550;
  color: var(--ink-2); text-decoration: none;
  transition: border-color .14s var(--ease), color .14s var(--ease), background-color .14s var(--ease);
}
.level-chips a:hover { border-color: var(--brand); color: var(--brand-ink); background: var(--brand-soft); }
.level-chips span { font-size: var(--step--2); color: var(--ink-3); font-variant-numeric: tabular-nums; }
.level-chips a:hover span { color: var(--brand-ink); }
@media (min-width: 56rem) {
  .hero { display: grid; grid-template-columns: 1.25fr 1fr; gap: var(--sp-xl); align-items: end; }
  .hero h1 { margin-bottom: 0; }
}


/* --- article hero image */
img.hero-img {
  display: block; width: 100%; max-height: 24rem; object-fit: cover;
  border-radius: var(--radius); margin-bottom: var(--sp-l);
}

/* --- footer */
.site-footer {
  border-top: 1px solid var(--rule);
  background: var(--surface);
  margin-top: var(--sp-xl);
}

/* --- top band: brand and social on the left, link columns on the right.
   The brand column is deliberately wider; five equal columns of plain link
   lists is what made the old footer read as a sitemap block. */
.footer-top {
  display: grid;
  gap: var(--sp-l);
  padding: var(--sp-xl) 0 var(--sp-l);
}
@media (min-width: 34rem) { .footer-top { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 56rem) { .footer-top { grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: var(--sp-xl); } }

.footer-brand { display: flex; flex-direction: column; align-items: flex-start; gap: var(--sp-2xs); }
@media (min-width: 34rem) { .footer-brand { grid-column: 1 / -1; } }
@media (min-width: 56rem) { .footer-brand { grid-column: auto; } }
.footer-logo { font-size: var(--step-1); }
.footer-brand p { margin: 0; max-width: 34ch; font-size: var(--step--1); color: var(--ink-2); }

.footer-social { display: flex; gap: var(--sp-2xs); list-style: none; margin: var(--sp-2xs) 0 0; padding: 0; }
.footer-social a {
  display: grid; place-items: center;
  width: 2.4rem; height: 2.4rem;
  color: var(--ink-2); background: var(--surface-2);
  border: 1px solid transparent; border-radius: 999px;
  transition: color .15s var(--ease), background-color .15s var(--ease), border-color .15s var(--ease);
}
.footer-social a:hover { color: var(--brand-ink); background: var(--brand-soft); border-color: var(--brand); }

.footer-col h2 {
  margin: 0 0 var(--sp-2xs);
  font-size: var(--step--2); text-transform: uppercase; letter-spacing: .12em;
  font-weight: 700; color: var(--ink-3);
}
.footer-col ul { list-style: none; padding: 0; margin: 0; }
.footer-col li { padding: var(--sp-3xs) 0; }
.footer-col a {
  color: var(--ink-2); text-decoration: none; font-size: var(--step--1);
  transition: color .14s var(--ease);
}
.footer-col a:hover { color: var(--brand-ink); }

/* --- bottom band: everything legal, on one quiet line. */
.footer-bottom {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2xs) var(--sp-m);
  padding: var(--sp-s) 0 var(--sp-l);
  border-top: 1px solid var(--rule);
  font-size: var(--step--2); color: var(--ink-3);
}
.footer-copy { margin: 0; font-variant-numeric: tabular-nums; }
.footer-legal { display: flex; flex-wrap: wrap; gap: var(--sp-2xs) var(--sp-s); list-style: none; margin: 0; padding: 0; }
.footer-legal a { color: var(--ink-3); text-decoration: none; }
.footer-legal a:hover { color: var(--brand-ink); text-decoration: underline; }
.footer-note { margin: 0; flex: 1 1 22rem; max-width: 62ch; }
@media (min-width: 56rem) { .footer-note { text-align: right; } }

/* 7. UTILITIES =========================================================== */

.visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.no-wrap { white-space: nowrap; }

/* --- back to top.
   Ships `hidden` and is revealed by nav.js past 700px of scroll, so it is out
   of the tab order until it does something. `max()` against the safe-area
   inset keeps it clear of a home indicator without ever leaving the viewport
  - a plain right offset is a classic source of horizontal overflow. */
.to-top {
  position: fixed;
  right: max(var(--sp-s), env(safe-area-inset-right));
  bottom: max(var(--sp-s), env(safe-area-inset-bottom));
  z-index: 45;
  display: grid; place-items: center;
  width: 2.85rem; height: 2.85rem;
  padding: 0;
  color: var(--brand-ink); background: var(--surface);
  border: 1px solid var(--rule-2);
  border-radius: 999px;
  box-shadow: var(--shadow-2);
  cursor: pointer;
  opacity: 0; transform: translateY(.6rem) scale(.92);
  transition: opacity .22s var(--ease), transform .22s var(--ease),
              background-color .15s var(--ease), border-color .15s var(--ease), color .15s var(--ease);
}
.to-top[hidden] { display: none; }
.to-top.is-visible { opacity: 1; transform: none; }
.to-top:hover {
  color: var(--on-brand); background: var(--brand-fill); border-color: var(--brand-fill);
  box-shadow: var(--shadow-3);
}
.to-top:active { transform: translateY(1px); }
@media (min-width: 48rem) {
  .to-top { width: 3rem; height: 3rem; right: var(--sp-m); bottom: var(--sp-m); }
}

/* 8. PRINT =============================================================== */

@media print {
  .site-header, .site-footer, .breadcrumbs, .tabs, .pagination, .related,
  .skip-link, .shelf, .to-top, .nav-toggle { display: none; }
  body { color: #000; background: #fff; font-size: 10.5pt; }
  .masthead, .section-head { border-bottom: 1px solid #000; }
  a[href^="/"]::after { content: " (graduate.pk" attr(href) ")"; font-size: 8pt; color: #555; }
}

/* 9. INTERACTIVE (Phase 7) ================================================
   Search, comparison and quiz. Each script loads only on its own page, so
   these styles cost nothing on the 1,100+ content pages. */

/* --- search */
.search-form { margin-bottom: var(--sp-m); }
.search-row { display: flex; gap: var(--sp-2xs); flex-wrap: wrap; }
.search-row input[type="search"] { flex: 1 1 18rem; max-width: none; font-size: var(--step-0); }

/* --- comparison picker */
.compare-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--sp-s);
  margin-bottom: var(--sp-m);
}
select {
  width: 100%;
  padding: var(--sp-2xs) var(--sp-xs);
  font: inherit;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule-2);
  border-radius: var(--radius-s);
}
select:disabled { color: var(--ink-3); }

/* --- quiz */
.quiz-questions { padding-left: 1.4em; }
.quiz-q { margin-bottom: var(--sp-m); }
.quiz-q .question { font-weight: 600; margin-bottom: var(--sp-2xs); }
.quiz-q .options { list-style: none; padding: 0; display: grid; gap: var(--sp-3xs); }
.quiz-choice {
  display: block; width: 100%; text-align: left;
  padding: var(--sp-2xs) var(--sp-s);
  font: inherit; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius-s);
  cursor: pointer;
}
.quiz-choice:hover:not(:disabled) { border-color: var(--brand); background: var(--brand-soft); }
.quiz-choice:disabled { cursor: default; opacity: .85; }
.quiz-choice.is-correct { border-color: var(--brand); background: var(--brand-soft); font-weight: 600; }
.quiz-choice.is-wrong { border-color: var(--accent); background: var(--accent-soft); }
.quiz-score {
  position: sticky; top: calc(var(--header-h) + var(--sp-2xs)); z-index: 5;
  display: inline-block;
  padding: var(--sp-2xs) var(--sp-s);
  background: var(--brand-fill); color: var(--on-brand);
  border-radius: 999px; font-weight: 620; font-size: var(--step--1);
}
/* Without JavaScript the answer is simply marked, so the page still teaches. */
.quiz-option.is-answer { font-weight: 600; }
.quiz-option.is-answer::after { content: " ✓"; color: var(--brand); }
#quiz .quiz-option.is-answer::after { content: none; }   /* JS marks it instead */

/* --- contact form */
.contact-form { display: grid; gap: var(--sp-s); max-width: 32rem; }
.contact-form textarea { max-width: none; }
/* Questions with no recorded answer are reference-only, not interactive. */
.quiz-q-unscored .options { padding-left: 1.1em; list-style: disc; }
.quiz-q-unscored .quiz-option { color: var(--ink-2); }
