/* =========================================================
   Albalagh International Premium College — London Campus
   Brand System — Royal Blue / Gold / Oxblood Red
   ========================================================= */

:root {
  /* =====================================================================
     THE PALETTE
     ---------------------------------------------------------------------
     Every token below has one job. The rule this file is built on is that
     a colour earns its place by carrying a meaning — a ground, a state, a
     kind of information — not by adding variety. Where two tokens would
     do the same job, there is one token.

     The original three-colour set (--royal / --gold / --red) is kept
     verbatim at the bottom of each group as an alias, because 87 other
     pages and eight stylesheets reference those names. Nothing that
     renders today changes because this block grew.
     ===================================================================== */

  /* --- Blues · the institutional spine -------------------------------
     Ordered deepest to lightest. Each step is a different altitude of
     ground, not a different hue: the page should read as one blue seen
     under more or less light, which is what makes a navy palette feel
     like an institution rather than a colour scheme. */
  --oxford:       #0A1428;   /* deepest ground — footer, CTA band, page floor */
  --midnight:     #0E1B33;   /* hero base, the darkest content ground */
  --navy:         #14264A;   /* header, standard dark section ground */
  --royal-blue:   #1F3D7A;   /* primary interactive blue — links, active nav */
  --sapphire:     #27508F;   /* lifted section grounds, gradient partners */
  --steel:        #4A6491;   /* rules and borders on dark ground */
  --cerulean:     #6E93C4;   /* readable light-blue text and links on dark */

  /* --- Golds · distinction, never decoration -------------------------
     Five gold tones because gold behaves differently by size: a hairline
     rule, a 12px small-cap, and a 64px display figure each need a
     different value to read as the same metal. Large type gets champagne
     (light enough to hold its own on navy); hairlines get royal; pressed
     states get antique. */
  --gold-royal:     #C7A24A; /* rules, borders, hairlines — the default gold */
  --gold-rich:      #D4AF37; /* seals, crest, the CTA — the loudest gold used */
  --gold-antique:   #A8863C; /* pressed and active states */
  --gold-soft:      #E7C97A; /* headings and mid-size type on dark ground */
  --gold-champagne: #F2E3C0; /* large display type and figures on dark */
  /* Bronze is the substitute gold for light grounds, so it is the one
     token whose value is fixed by measurement rather than by eye. At
     the first-chosen #8C6A3F it computed to 4.23:1 on --pearl and
     4.50:1 on --ivory — i.e. it failed AA on exactly the chapters it
     exists to serve. This value clears 4.5:1 on every light ground in
     the palette, --stone included. */
  --bronze:         #795A32; /* small-caps metadata and icon strokes on light ground */

  /* --- Neutrals · the paper ------------------------------------------
     Warm throughout. A cool grey next to this navy reads as a dashboard;
     a warm one reads as paper, which is the entire difference between
     "software" and "prospectus". */
  --warm-white:   #FDFBF6;   /* the lightest ground — chapter 01 */
  --cream:        #FAF6EC;   /* secondary light ground */
  --ivory:        #F7F4EC;   /* standard light ground (existing) */
  --pearl:        #F1EDE3;   /* recessed light ground, table stripes */
  --stone:        #E4DED0;   /* rules and dividers on light ground */
  --grey-soft:    #C9C6BE;   /* disabled, placeholder */
  --slate:        #4B5768;   /* secondary body text on light */
  --charcoal:     #2A2F38;   /* strong body text where ink is too blue */
  --ink:          #16202E;   /* primary body text on light */

  /* --- Supporting · used only where they carry meaning ---------------
     These are not accents to sprinkle. Each is bound to one kind of
     information, and using one outside that meaning is a bug: a green
     that sometimes means "achievement" and sometimes means "here is a
     nice green" stops meaning anything. */
  --emerald:      #1E6A4F;   /* achievement, completion, a passed level */
  --burgundy:     #6E1F2E;   /* ceremony, academic distinction, awards */
  --oxblood:      #A32638;   /* emphasis on light ground — rare */
  --purple:       #3E2A56;   /* honours, the Senate, governance */
  --teal:         #1D5C63;   /* the platform, technology, innovation */
  --amber:        #C98A16;   /* attention — never used for error */
  --silver:       #B9BCC2;   /* secondary and inactive elements */

  /* --- Aliases · every pre-existing name, unchanged in value ---------
     Kept so nothing outside this file needs editing. New work should
     use the semantic names above; these stay for the 87 pages that
     already reference them. */
  --royal:        var(--navy);
  --royal-deep:   #0A1730;   /* footer ground (existing) */
  --royal-bright: #274B85;
  --gold:         var(--gold-royal);
  --gold-bright:  var(--gold-soft);
  --paper:        #FCFAF4;   /* card surface on ivory */
  --red:          var(--oxblood);
  --red-bright:   #C23A4E;
  --ink-soft:     var(--slate);

  --line:         rgba(199,162,74,.35);
  --line-dark:    rgba(231,201,122,.22);
  --line-stone:   #E4DED0;   /* the neutral rule, for where gold would shout */

  /* =====================================================================
     THE RADIUS SCALE
     ---------------------------------------------------------------------
     Radius is not a taste setting; it is a function of the element's
     size. A 4px corner on a 600px plate reads as a mistake — the eye
     sees a square that failed to be square. A 24px corner on a 40px
     badge reads as a bubble. The perceived softness of a corner is the
     radius as a PROPORTION of the shortest edge, so a system that wants
     one material across every scale needs the radius to climb with the
     component, holding that proportion roughly constant at ~6–10%.

     That is the whole scale. Each step names a size of thing, not a
     number of pixels, and choosing the step is choosing what kind of
     object you are drawing.

     WHAT STAYS SQUARE, DELIBERATELY. Not everything should soften —
     an institution needs orthogonal structure to lean on, and a page
     where every edge is rounded reads as a consumer app. Square, by
     decision: table cells and their rules, hairline dividers, chapter
     rules, the ledger, column keylines, the grain and guilloché
     grounds. Those are the page's ARCHITECTURE. Radius belongs to the
     OBJECTS placed on it. */
  --r-xs:   6px;    /* inline marks: CEFR badges, tags, code, strands */
  --r-sm:   10px;   /* controls: buttons, inputs, chips, small icon wells */
  --r-md:   16px;   /* cards, tiles, list wells, callouts */
  --r-lg:   24px;   /* plates, panels, feature blocks, mega-panels */
  --r-xl:   34px;   /* section-scale containers, hero furniture */
  --r-2xl:  48px;   /* full-bleed inset bands */
  --r-pill: 999px;  /* anything whose height is its own radius */

  /* Elevation scale — a resting, a hover, and a floating (dropdown/
     modal/dialog) tier, all sharing the same warm, low-chroma shadow
     colour so depth reads as "considered," not as a generic drop
     shadow lifted from a UI kit. --shadow kept as the existing name
     (now aliased to the floating tier) so every pre-existing caller
     is untouched.

     EACH TIER IS TWO SHADOWS, and that is the upgrade. A single blurred
     offset is what a UI kit ships and it always reads as a sticker on a
     page. Real objects cast two: a tight, dark CONTACT shadow where the
     object meets the surface, which is what tells the eye the two are
     touching, and a wide, soft AMBIENT shadow from the room's fill
     light, which is what gives the object mass. Removing either one is
     immediately visible — without contact the object floats; without
     ambient it is a decal. The pairing is why these read as objects
     rather than as blur. */
  --shadow-sm:    0 1px 2px -1px rgba(10,23,48,.22), 0 3px 12px -6px rgba(10,23,48,.18);
  --shadow-md:    0 2px 4px -2px rgba(10,23,48,.24), 0 16px 36px -16px rgba(10,23,48,.30);
  --shadow-lg:    0 4px 8px -3px rgba(10,23,48,.26), 0 28px 68px -22px rgba(10,23,48,.38);
  --shadow:       var(--shadow-lg);
  --shadow-gold:  0 2px 6px -2px rgba(122,92,28,.34), 0 14px 34px -12px rgba(199,162,74,.48);

  /* =====================================================================
     THE RELIEF MODEL
     ---------------------------------------------------------------------
     Gold, on this site, is a metal — and a metal surface is defined by
     its EDGES, not by its fill. Light comes from above and slightly
     forward (the same direction js/atelier.js's ambient-hour system
     assumes), so every raised surface carries a bright inset hairline
     along its top edge and a dark inset hairline along its bottom, and
     every recessed one carries exactly the reverse. Two inset shadows,
     one declaration, and a flat rectangle becomes a struck plate.

     These are the tokens; the components below simply compose them
     with a --shadow-* tier. Anything that wants to look worked rather
     than filled uses one. */
  --relief-raised: inset 0 1px 0 rgba(255,252,240,.30), inset 0 -1px 0 rgba(8,16,34,.42);
  --relief-inset:  inset 0 1px 2px rgba(8,16,34,.45), inset 0 -1px 0 rgba(255,252,240,.14);
  --relief-metal:  inset 0 1px 2px rgba(255,255,255,.58), inset 0 -2px 3px rgba(90,62,20,.42);
  --relief-glass:  inset 0 1px 0 rgba(255,252,240,.22), inset 0 0 0 1px rgba(231,201,122,.10),
                   inset 0 -20px 40px -28px rgba(8,16,34,.7);

  /* Motion tokens — one considered easing curve (a gentle ease-out,
     decisive but never bouncy) and a small duration scale, used
     everywhere instead of ad hoc `.15s ease` / `.2s ease` per rule, so
     every interaction across the platform decelerates the same way. */
  --ease-premium: cubic-bezier(.16,.84,.44,1);
  --dur-fast:     .18s;
  --dur-med:      .34s;
  --dur-slow:     .6s;

  --gold-gradient: linear-gradient(135deg, var(--gold) 0%, var(--gold-bright) 100%);

  /* =====================================================================
     THE FACES
     ---------------------------------------------------------------------
     --font-display  Bodoni Moda. A Didone: vertical stress, unbracketed
                     serifs, and a stroke contrast steep enough that at
                     display size the letterforms read as engraved rather
                     than written. It replaced Playfair Display, which is
                     a fine face and the single most-used "elegant" font
                     on the web — a masthead that a reader has already
                     seen on ten thousand other sites cannot carry an
                     institution's authority no matter how well it is set.
                     Requested VARIABLE on `opsz`; see the note in
                     scripts/build.js for why that axis is the reason
                     this face can serve both a 4.6rem headline and a
                     1.05rem sub-heading.

     --font-ceremonial  Cinzel. Inscriptional Roman capitals, the letters
                     cut into Trajan's column and inherited by every
                     British institution that ever carved its name over a
                     door. RATIONED, absolutely: the crest lockup,
                     chapter numerals, CEFR marks, seals and awards. It
                     has no lowercase worth the name and it is not a
                     reading face. Used for roughly forty characters per
                     page, it says "this place is old". Used for a
                     paragraph, it says "this place is a theme".

     --font-body     Inter. Unchanged, and deliberately so: it sets the
                     body of eighty-eight pages, every form, every table
                     and the whole student platform. The prestige comes
                     from the display face, the rhythm and the space
                     around it — not from replacing a workhorse that is
                     doing its job.

     Fallbacks name real Didones (Didot, Bodoni MT) before Georgia, so a
     machine without webfonts still gets the closest thing it owns
     rather than jumping to a slab. 'Amiri' stays in every stack as the
     Arabic backstop — see css/arabic.css, which is what actually holds
     the line for Arabic. */
  --font-display:    'Bodoni Moda', 'Didot', 'Bodoni MT', Georgia, 'Amiri', serif;
  --font-ceremonial: 'Cinzel', 'Trajan Pro', 'Bodoni Moda', Georgia, 'Amiri', serif;
  --font-body:       'Inter', -apple-system, 'Cairo', sans-serif;

  --wrap: 1180px;
}

* { box-sizing: border-box; }
/* The [hidden] attribute is meant to be an unconditional "not
   displayed" signal, but it's a user-agent-stylesheet rule — any
   author rule setting `display` on the same element (e.g. `.field {
   display: flex }`) silently wins over it regardless of specificity,
   since author styles always beat UA styles. Restoring that guarantee
   here once, rather than special-casing every component that might
   combine `hidden` with a `display`-setting class. */
[hidden] { display: none !important; }

/* Visually hidden but still announced by screen readers — for an
   accessible name/label that's redundant to show twice on screen
   (e.g. a <legend> repeating a visible <h2> right above it). */
.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;
}

/* Replaces ~20 repeated identical `style="justify-content:center"`
   inline declarations across pages/*.html (on .btn-row, .tag-row,
   .eyebrow — all flex containers) with one shared class. */
.u-center { justify-content: center; }

html { scroll-behavior: smooth; }
/* The header is sticky, so an anchor jump lands with the target
   heading underneath it. Every in-page target on the site is either a
   section or an element carrying an id, and both need clearance equal
   to the condensed header plus a little breathing room. Set here once
   rather than per-anchor, because the failure is silent: the page does
   scroll, it just scrolls to the wrong place, and nothing reports it. */
:target, section[id], [data-chapter] { scroll-margin-top: 96px; }
body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--ivory);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* Let the browser interpolate the optical-size axis. Without this the
     variable display face is pinned to its default cut and the entire
     reason it was chosen is switched off — silently, since the page
     still renders in the right family. */
  font-optical-sizing: auto;
  /* Never let the browser fake a WEIGHT. A synthesised bold on a Didone
     smears the hairlines along with the stems and destroys the stroke
     contrast that IS the face — and with a variable weight axis loaded
     there is never a reason to synthesise one.
     Style synthesis is deliberately left ON: Inter is served without an
     italic file, so every <em> on the site depends on it. */
  font-synthesis-weight: none;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
/* js/site.js toggles this the instant the full-screen mobile drawer
   opens, so the page beneath it stops scrolling — see the note at
   .nav-mobile in this file. */
body.nav-open { overflow: hidden; }
h1, h2, h3, h4 {
  font-family: var(--font-display); margin: 0 0 .5em;
  line-height: 1.15; font-weight: 600;
  /* Break the line where the RAGGED EDGE is best, not where the line
     happens to fill. This is the single largest perceived-quality
     difference between a headline a designer set and one a browser set:
     no two-word last line, no orphan hanging under a full measure. It
     costs nothing and is ignored by browsers that do not have it. */
  text-wrap: balance;
}
/* Small headings are where a Didone earns or loses its keep. `opsz`
   already thickens the hairlines as the size drops; these two go the
   rest of the way — a touch more weight so the stems hold at 16px, and
   POSITIVE tracking, because tight tracking is a large-size correction
   and applying it at reading size closes the counters. Display sizes
   set their own tracking in css/atelier.css and are unaffected. */
h3, h4 { font-weight: 650; letter-spacing: .004em; }
/* Widow control on running text — the paragraph equivalent of the
   balanced headline above. `pretty` only adjusts the last few lines,
   so long copy keeps its even colour. */
p, .lede, li { text-wrap: pretty; }
p { margin: 0 0 1em; }

/* =========================================================
   THE ENGLISH TYPE SYSTEM
   One fluid scale, applied by default, so every English page
   inherits it rather than only the homepage. Scoped to
   [dir="ltr"] throughout rather than the bare tag: css/arabic.css
   already !important-overrides tracking and leading on Arabic,
   but nothing in it corrects font-size or measure for h2-h4/p/
   body, because brand.css never set them for anyone before this.
   Scoping every new rule here — rather than trusting that chain
   to happen to cover properties it was never written to cover —
   is what keeps Arabic, which this pass does not touch, genuinely
   untouched rather than untouched-by-accident.
   ========================================================= */
[dir="ltr"] h1 { font-size: clamp(2.05rem, 1.35rem + 3.1vw, 3.5rem); line-height: 1.06; letter-spacing: -.019em; font-weight: 700; }
[dir="ltr"] h2 { font-size: clamp(1.7rem, 1.2rem + 2.1vw, 2.6rem); line-height: 1.1; letter-spacing: -.016em; font-weight: 700; }
[dir="ltr"] h3 { font-size: clamp(1.3rem, 1.1rem + 1vw, 1.75rem); line-height: 1.2; letter-spacing: -.01em; font-weight: 600; }
/* H4 moves to Inter rather than a fourth shade of Playfair: a
   display-optical serif reads thin and mushy below ~20px, so the
   smallest heading level changing family is a legibility fix, not
   decoration. */
[dir="ltr"] h4 { font-family: var(--font-body); font-size: clamp(1.05rem, 1rem + .3vw, 1.25rem); line-height: 1.3; letter-spacing: -.006em; font-weight: 700; }

/* The reading measure this site never had: bare <p> sits directly
   in .container (1180px) or .callout (unconstrained), so on a wide
   desktop a paragraph could run past 100 characters a line — the
   single strongest "hard to read" cause on a text-heavy site like
   this one (about.html alone carries ~1,470 words of prose,
   governance.html ~2,600). A max-width only ever constrains from
   above, so it cannot break short UI copy or anything already
   narrower than this — .lede (62ch) and .pull-quote (68ch) keep
   their own tighter, more-specific measures untouched. */
[dir="ltr"] p { max-width: 70ch; }

/* 16px -> 18px fluid, up from the implicit flat 16px this never
   set explicitly before. Safe in isolation: nav, buttons and UI
   chrome all carry their own explicit rem sizes already, and
   headings now have explicit sizes above instead of cascading from
   body's em-relative default. */
[dir="ltr"] body { font-size: clamp(1rem, .97rem + .16vw, 1.125rem); line-height: 1.65; }

.container { max-width: var(--wrap); margin: 0 auto; padding: 0 24px; }
section { position: relative; }

/* -------------------- Utility type -------------------- */
.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: .6em;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .74rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--gold);
}
.eyebrow::before {
  content: '';
  width: 28px;
  height: 1px;
  background: var(--gold);
}
.section--light .eyebrow { color: var(--red); }
.section--light .eyebrow::before { background: var(--red); }

.module-marker {
  display: inline-block;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: .72rem;
  letter-spacing: .12em;
  color: var(--gold);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: .3em .9em;
  margin-bottom: 1em;
}
.section--light .module-marker { color: var(--red); border-color: rgba(163,38,56,.3); }
/* CONTRAST, MEASURED RATHER THAN EYEBALLED.
   .module-marker sets 11px small caps and defaulted to --gold, which is
   #C7A24A. On the pale grounds these markers actually sit on that is
   2.31:1 — WCAG AA wants 4.5:1 for text this size, so the eyebrow above
   almost every section on the site was failing.

   The fix is not a new colour. --bronze is defined in the palette as
   "small-caps metadata and icon strokes on light ground", which is this
   component exactly; it was simply never used here. It measures 6.06:1
   on the cream ground and keeps the same metallic family, so nothing
   about the page's character changes — only its legibility. */
.section--cream .module-marker,
.section--paper .module-marker,
.section--pearl .module-marker,
.section--warm .module-marker { color: var(--bronze); }

.lede { font-size: 1.15rem; color: var(--ink-soft); max-width: 62ch; }
.section--dark .lede { color: rgba(247,244,236,.78); }

/* -------------------- Sections -------------------- */
.section--dark { background: var(--royal); color: var(--ivory); }
.section--deep { background: var(--royal-deep); color: var(--ivory); }
.section--light { background: var(--ivory); color: var(--ink); }
.section--paper { background: var(--paper); color: var(--ink); }
.section--dark h1, .section--dark h2, .section--dark h3 { color: #fff; }

/* The additional grounds the chapter palette needs. Each is a real
   surface, not a tint of the last one — a reader scrolling the page
   should feel they have turned a page, not that the same page got
   slightly darker. The four below join the four above; every one of
   them inherits the existing --dark/--light typography rules via the
   grouped selectors that follow. */
.section--oxford   { background: var(--oxford);   color: var(--ivory); }
.section--midnight { background: var(--midnight); color: var(--ivory); }
.section--warm     { background: var(--warm-white); color: var(--ink); }
.section--cream    { background: var(--cream);    color: var(--ink); }
.section--pearl    { background: var(--pearl);    color: var(--ink); }
.section--oxford h1, .section--oxford h2, .section--oxford h3,
.section--midnight h1, .section--midnight h2, .section--midnight h3 { color: #fff; }
.section--oxford .lede, .section--midnight .lede { color: rgba(247,244,236,.78); }

/* The four new dark grounds need every rule that currently keys off
   `.section--dark` / `.section--deep`. Rather than appending two more
   selectors to each of the ~20 rules that do so — which is how a
   stylesheet quietly acquires a maintenance tax — the two new dark
   grounds simply also carry `.section--dark` in the markup, and these
   background overrides win on source order. Same for the light ones. */
.section--dark.section--oxford   { background: var(--oxford); }
.section--dark.section--midnight { background: var(--midnight); }
.section--light.section--warm    { background: var(--warm-white); }
.section--light.section--cream   { background: var(--cream); }
.section--light.section--pearl   { background: var(--pearl); }

/* -------------------- Icons -------------------- */
/* Every icon on the site is a <use> reference into the sprite injected
   after <body> by scripts/build.js (partials/icons.html). They inherit
   `currentColor` deliberately: an icon should take the colour of the
   text it sits with, so a single sprite works on navy, on paper, in a
   gold rule and inside a burgundy heading without a per-context copy. */
.icon {
  width: 1.25em; height: 1.25em; flex-shrink: 0;
  fill: none; stroke: currentColor; stroke-width: 1.25;
  stroke-linecap: square; stroke-linejoin: miter;
  vertical-align: -.2em;
}
.icon--lg { width: 2.5rem; height: 2.5rem; stroke-width: 1; }
.icon--xl { width: 3.5rem; height: 3.5rem; stroke-width: .85; }
.icon--gold { color: var(--gold-royal); }
.section--light .icon--gold, .section--paper .icon--gold,
.section--warm .icon--gold, .section--cream .icon--gold, .section--pearl .icon--gold {
  /* Gold on paper is ~2.3:1. An icon is not text, but a 1px stroke at
     that contrast is genuinely hard to see, so light grounds get bronze
     — the same substitution .card__num already makes for the same
     reason further down this file. */
  color: var(--bronze);
}
.section-pad { padding: 96px 0; }
@media (max-width: 720px) { .section-pad { padding: 64px 0; } }

.section-head { max-width: 720px; margin-bottom: 48px; }
.section-head.center { margin-left: auto; margin-right: auto; text-align: center; }

/* -------------------- Buttons -------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: .5em;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .88rem;
  letter-spacing: .04em;
  padding: .95em 1.9em;
  /* 3px, previously — the radius of a shape that was meant to be square
     and was not committed to it. A control this size reads as either a
     cut rectangle (0) or a formed one (~10px at a 44px height); the
     values between the two are the ones that look accidental. */
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  cursor: pointer;
  position: relative;
  isolation: isolate; /* keeps ::before's shine sweep clipped to this button, not painting over siblings */
  overflow: hidden;
  transition: transform var(--dur-fast) var(--ease-premium), box-shadow var(--dur-fast) var(--ease-premium),
              background var(--dur-med) var(--ease-premium), border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
/* A restrained diagonal shine that sweeps across on hover — the one
   "premium micro-interaction" flourish on the platform's single most-
   clicked element family, deliberately not repeated on every card so
   it stays a signature rather than noise. Purely decorative (a ::before
   layer), so it never affects the button's hit area or focus ring. */
.btn::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.35) 48%, transparent 66%);
  transform: translateX(-120%); transition: transform var(--dur-slow) var(--ease-premium);
}
.btn:hover::before { transform: translateX(120%); }
/* Bevelled rather than flat: an inset highlight along the top edge and
   an inset shadow along the bottom, the same worked-metal recipe as
   .glance__icon and the world clock's face — a struck medallion, not
   a painted rectangle. The outer shadow still carries --shadow-sm/
   --shadow-gold so hover/active keep their existing lift. */
/* THE STRUCK MEDALLION.
   Three layers of light, and each is doing separate work:
   · the fill is a three-stop gradient with the lightest band ABOVE
     centre, which is where a convex surface catches an overhead source
     — a two-stop gradient reads as a ramp, not as a curve;
   · --relief-metal is the bevel: a bright hairline cut into the top
     edge and a shaded one along the bottom;
   · the shadow pair beneath sets it on the page rather than in it. */
.btn--gold {
  background:
    linear-gradient(178deg, rgba(255,252,240,.5) 0%, rgba(255,252,240,0) 46%),
    linear-gradient(135deg, var(--gold-antique) 0%, var(--gold-rich) 34%,
                    var(--gold-soft) 62%, var(--gold-royal) 100%);
  color: var(--royal-deep);
  box-shadow: var(--relief-metal), var(--shadow-sm);
}
.btn--gold:hover {
  box-shadow: var(--relief-metal), var(--shadow-gold);
  transform: translateY(-2px);
}
/* Pressed metal loses its cast shadow and gains an inner one: the
   object has gone INTO the surface, which is the only honest reading of
   a press. Lifting the highlight to the bottom edge at the same time is
   what sells it — the light now enters from below because the face has
   tilted away from the source. */
.btn--gold:active {
  transform: translateY(1px);
  box-shadow: inset 0 2px 4px rgba(90,62,20,.55), inset 0 -1px 0 rgba(255,252,240,.4);
}
.btn--outline { border-color: var(--gold); color: inherit; }
.btn--outline:hover { background: rgba(199,162,74,.1); transform: translateY(-2px); }
.btn--outline:active { transform: translateY(0); }
.btn--red { background: var(--red); color: #fff; box-shadow: var(--shadow-sm); }
.btn--red:hover { background: var(--red-bright); transform: translateY(-2px); box-shadow: var(--shadow-md); }
.btn--red:active { transform: translateY(0); }
.btn--ghost { color: var(--gold-bright); border-color: var(--line-dark); }
.btn--ghost:hover { border-color: var(--gold); background: rgba(231,201,122,.06); }
/* Every other focusable control (.field inputs, .toggle) gets an
   explicit gold focus ring — .btn was relying entirely on the browser
   UA default, which is inconsistent with that and, behind
   .btn--ghost's dark/translucent grounds especially, can be genuinely
   hard to see for a keyboard user tabbing through CTAs. */
.btn:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: 2px; }
/* THE RATION, WRITTEN DOWN. Where a gold button also carries `.aurum`
   (see css/atelier.css) the orbit is deliberately slowed to thirteen
   seconds, which is roughly four times the interval at which motion
   reads as animation rather than as an occurrence. At that pace a
   reader looking at the button sees a point of light cross its edge
   once and then leaves; a reader reading the page beside it never
   notices anything moving at all. That is the difference between a
   signature and a distraction, and it is a number, not a judgement.

   ALLOWED on the single primary call to action in a view. NOT on
   secondary buttons, not on a row of two golds, not in the header. */
.btn.aurum { --aurum-dur: 13s; --aurum-width: 1.5px; }
.btn-row { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 28px; }

/* -------------------- Skip link -------------------- */
.skip-link {
  position: absolute; top: -100px; left: 12px; z-index: 100;
  background: var(--gold); color: var(--royal-deep); font-weight: 700;
  font-size: .88rem; padding: .8em 1.4em; border-radius: 4px;
  transition: top .15s ease;
}
.skip-link:focus { top: 12px; }
[dir="rtl"] .skip-link { left: auto; right: 12px; }

/* -------------------- Topbar -------------------- */
.topbar { background: var(--royal-deep); color: rgba(247,244,236,.82); font-size: .8rem; }
/* NOWRAP UNCONDITIONALLY, and the previous version is instructive: the
   rule below used to say `flex-wrap: wrap`, with nowrap applied only
   under 720px. The comment further down already asserted that a utility
   bar "should cost one row at every width" — but the rule only enforced
   it on phones, so between 721 and 833 the bar quietly grew a second
   row and doubled from 60px to 120px. A band bounded on both sides by
   correct behaviour is the hardest kind of fault to notice by eye and
   the easiest to measure, which is roughly the story of this whole
   header.

   Wrapping is not a graceful degradation here. It is the failure. The
   bar sheds items instead — see the drop rules below. */
.topbar__inner {
  max-width: var(--wrap); margin: 0 auto; padding: 8px 24px;
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: nowrap;
}
.topbar__contacts { display: flex; gap: 22px; flex-wrap: nowrap; }
.topbar__item:hover { color: var(--gold-bright); }
.topbar__item--muted { color: rgba(247,244,236,.55); }
.topbar__right { display: flex; align-items: center; gap: 18px; }
.topbar__lang {
  font-weight: 600; letter-spacing: .04em;
  padding-inline: 12px; border-inline: 1px solid rgba(247,244,236,.16);
}
.topbar__lang:hover { color: var(--gold-bright); }
.topbar__social { display: flex; gap: 8px; }
.topbar__item, .topbar__lang {
  display: inline-flex; align-items: center; gap: .55em; min-height: 44px;
}
.topbar__item .icon, .topbar__lang .icon {
  width: 13px; height: 13px; opacity: .62; color: var(--gold-royal);
  transition: opacity var(--dur-fast) ease;
}
.topbar__item:hover .icon, .topbar__lang:hover .icon { opacity: 1; }
/* The visible ring is 30px; the tap target must be 44px.

   The ANCHOR is the 44px box and the ring is drawn by a pseudo-element
   inside it — not the other way round. Sizing the anchor to the ring
   and hanging a 44px pseudo-element off it looks identical and is
   wrong: tests/browser/route-audit.mjs measures the interactive
   element's own box, as does every accessibility tool and every
   thumb, and none of them can see a decorative overlay. Putting the
   size on the pseudo-element regressed all three social links to 30px
   on all 88 pages while looking, on screen, like nothing had changed. */
.topbar__social a {
  position: relative; flex: 0 0 auto;
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  color: rgba(247,244,236,.82);
  transition: color var(--dur-fast) ease;
}
.topbar__social a::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 30px; height: 30px; transform: translate(-50%, -50%);
  border: 1px solid rgba(247,244,236,.2); border-radius: 50%;
  transition: border-color var(--dur-fast) ease, background var(--dur-fast) var(--ease-premium);
}
.topbar__social .icon { width: 14px; height: 14px; position: relative; }
.topbar__social a:hover { color: var(--gold-bright); }
.topbar__social a:hover::before { border-color: var(--gold); background: rgba(199,162,74,.12); }
/* THE BAR SHEDS ITEMS; IT DOES NOT SHRINK THEM AND IT DOES NOT WRAP.
   On a phone this was wrapping to three rows and taking 240px before
   the reader reached the College's name.

   Every control here is 44px because that is the smallest reliable
   thumb target, so buying a shorter bar by shrinking them would trade a
   cosmetic problem for an accessibility one. Things go instead, in
   order of how little they are needed: the campus strapline first (the
   hero repeats it verbatim), then the social ring. The language switch
   is the one control a Gulf reader may genuinely need on a phone, so it
   never goes.

   940, not 720, for the strapline. Measured with the bar forced to one
   row: its natural content needed 785px in English and 726px in Arabic
   against the viewport less 48px of padding, so English ran out at
   833 and the threshold sat at 860. The Contact link the architecture
   moved into this bar adds ~90px to the right cluster, which pushes
   the English break past 920 — so the strapline now goes at 940.
   720 was the width at which the bar had ALREADY been wrapping for
   a hundred pixels. */
@media (max-width: 940px) {
  .topbar__contacts .topbar__item--muted { display: none; }
}
@media (max-width: 720px) {
  .topbar { font-size: .76rem; }
  .topbar__inner { gap: 12px; padding-inline: 16px; }
}
@media (max-width: 620px) {
  .topbar__social { display: none; }
  .topbar__lang { padding-inline: 0; border: 0; }
  /* The address can ellipsis rather than force the bar to wrap. */
  .topbar__item { min-width: 0; }
  .topbar__contacts { flex: 1 1 auto; min-width: 0; }
  .topbar__contacts .topbar__item { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* -------------------- Header / Nav -------------------- */
/* `.site-header.lattice`, and the doubled class is load-bearing.
   partials/header.html carries `class="site-header lattice"`, and
   css/atelier.css declares `.lattice { position: relative }`. Both
   selectors score 0,1,0 and atelier.css loads second, so the header has
   been `position: relative` on every page of this site since the
   lattice was introduced — it scrolled away, and the entire condense
   behaviour below it (the crest scaling down, the campus line
   withdrawing, the translucent ground) was unreachable code, firing at
   90px of scroll on an element that was no longer on screen.
   Nothing reported it: the header was correct at rest, which is the
   state every screenshot and every audit had ever measured it in.
   Naming both classes wins on specificity and states the conflict where
   somebody will find it. Sticky still establishes the containing block
   .lattice__bed needs, so the lattice is unaffected. */
/* THE METAL EDGE.
   A gold-leaf book has its pages edged, not just its cover — a thin
   band of real metal at the outermost boundary of the object. This is
   that band: one at the very top of the header, one at the very
   bottom of the footer, so every page on the site is bound between
   the same two strips of gold regardless of how long the content
   between them runs. The gradient is the same five-stop banding
   .foil already uses for foil-stamped text, applied here to a solid
   strip instead of a glyph — one "this is metal, not a flat colour"
   recipe for both text and trim rather than two. Static, not
   animated: a shimmering edge on every scroll would be exactly the
   noise the rest of this file is written to avoid. */
.trim-metal {
  height: 3px;
  background: linear-gradient(90deg,
    var(--gold-antique) 0%, var(--gold-champagne) 20%, var(--gold-rich) 40%,
    var(--gold-soft) 60%, var(--gold-champagne) 80%, var(--gold-antique) 100%);
}

.site-header, .site-header.lattice {
  background: var(--royal);
  position: sticky; top: 0; z-index: 50;
  border-bottom: 1px solid var(--line-dark);
  /* The doubled rule the footer closes a page with (.site-footer::before),
     echoed here to open one — a second, fainter line a few pixels below
     the first, the way a printed cover carries two rules rather than
     one. A box-shadow rather than a second element: nothing to position
     against a header that is already sticky and already condenses. */
  box-shadow: 0 5px 0 -4px var(--line-dark);
  transition: background var(--dur-med) var(--ease-premium),
              box-shadow var(--dur-med) var(--ease-premium),
              border-color var(--dur-med) ease;
}
/* THE CONDENSED HEADER.
   js/motion.js adds .is-condensed once the page has scrolled past the
   hero's first screen. The header then loses height, the crest scales
   down, and the ground becomes translucent midnight over whatever
   chapter is behind it. The gold hairline is the tell: at rest the
   header is part of the hero, and once it detaches it announces itself
   as a fixed rule across the page.

   backdrop-filter is progressive: where it is unsupported the header
   simply stays opaque midnight, which is the same design one step less
   refined, not a broken one — hence the solid colour underneath. */
.site-header.is-condensed {
  background: rgba(14,27,51,.88);
  border-bottom-color: var(--gold-royal);
  box-shadow: 0 10px 34px -22px rgba(10,20,40,.9);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .site-header.is-condensed {
    background: rgba(14,27,51,.72);
    -webkit-backdrop-filter: saturate(140%) blur(14px);
    backdrop-filter: saturate(140%) blur(14px);
  }
}
/* ONE RAIL, ONE AXIS.
   Identity at the start, navigation in the middle, actions at the end,
   all on a single baseline. The old design put the wordmark in its own
   centred band above this one, so the eye crossed three different
   alignments on the way down the header; everything now hangs off the
   same grid as the page beneath it.

   18px of padding at rest rather than 14: with the identity in the rail
   the band carries more, and a taller line stops it reading as a
   toolbar. It still comes to roughly a third of what the three stacked
   bands cost. */
.site-header__inner {
  max-width: var(--wrap); margin: 0 auto; padding: 18px 24px;
  display: flex; align-items: center; gap: clamp(16px, 2.4vw, 40px);
  transition: padding var(--dur-med) var(--ease-premium);
}
/* The navigation takes the slack, so the actions sit hard against the
   end margin and the brand hard against the start one — rather than
   three groups drifting apart with space-between.

   The auto margin is restated on .header__actions inside the collapse
   query below, and that is not redundancy. An `auto` margin on a
   `display: none` element pushes nothing: the moment the nav was hidden
   the whole trailing group sprang back to sit against the wordmark,
   leaving the menu button marooned beside the brand with the entire
   right half of the rail empty. Whichever element leads the trailing
   group has to own the slack, and which element that is changes with
   the viewport. */
.site-header__inner > .nav { margin-inline-start: auto; }
.site-header__inner > .header__actions { margin-inline-start: 0; }
.site-header.is-condensed .site-header__inner { padding-top: 9px; padding-bottom: 9px; }
.brand { display: flex; align-items: center; gap: 12px; color: #fff; min-height: 44px; }
.brand__crest {
  width: 34px; height: 40px; flex-shrink: 0;
  transition: width var(--dur-med) var(--ease-premium), height var(--dur-med) var(--ease-premium);
}
.site-header.is-condensed .brand__crest { width: 27px; height: 32px; }
/* The campus line is the first thing to go when space tightens — it is
   the least load-bearing part of the lockup, and dropping it keeps the
   institution's name at full size rather than shrinking everything. */
.site-header.is-condensed .brand__campus {
  opacity: 0; max-height: 0; margin-top: 0; overflow: hidden;
}
.brand__campus { transition: opacity var(--dur-fast) ease, max-height var(--dur-med) var(--ease-premium); max-height: 2em; }
.crest__shield { fill: rgba(199,162,74,.07); stroke: var(--gold-royal); stroke-width: 1.4; }
.crest__inner { fill: none; stroke: var(--gold-soft); stroke-width: .7; opacity: .55; }
.crest__crown { fill: none; stroke: var(--gold-rich); stroke-width: 1.2; stroke-linejoin: miter; }
.crest__book { fill: none; stroke: var(--gold-soft); stroke-width: .85; opacity: .8; }
.crest__mono { font-family: var(--font-display); font-weight: 700; font-size: 8.4px; letter-spacing: .5px; fill: var(--gold-champagne); }
.brand__word { display: flex; flex-direction: column; line-height: 1.15; }
/* NOWRAP, EVERYWHERE IN THE RAIL, and it is not a detail.
   With the identity moved into the rail there is one line to fit four
   groups on, and anything permitted to wrap takes the whole band with
   it: the first build of this put "WorldWide English / College" on two
   lines, wrapped "The College" and "Press & Library", broke "Apply Now"
   in half, and grew the header back to the height it had just lost. A
   rail either fits or collapses to the menu — it never wraps. */
.brand__name {
  font-family: var(--font-display); font-weight: 600;
  font-size: .93rem; letter-spacing: .01em; white-space: nowrap;
}
.brand__campus {
  font-size: .62rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--gold-bright); margin-top: 3px; white-space: nowrap;
}

/* The gap flexes with the viewport so the rail gives up space
   gracefully between 1100 and 1440 rather than failing at one width. */
.nav { display: flex; align-items: center; gap: clamp(14px, 1.85vw, 30px); }
.nav__item > a, .header__actions .btn { white-space: nowrap; }
.nav__item { position: relative; }
.nav__item > a {
  position: relative;
  color: rgba(247,244,236,.86); font-size: .855rem; font-weight: 600; letter-spacing: .015em;
  padding: 8px 0; display: inline-block;
}
.nav__item > a:hover { color: var(--gold-bright); }
/* The underline draws in from the leading edge rather than fading, the
   same grammar .rule-draw already uses for chapter dividers — one
   "gold arrives from the start" gesture across the platform instead of
   a second unrelated hover treatment invented for the nav specifically. */
.nav__item > a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: 0; height: 1px;
  background: var(--gold-bright); pointer-events: none;
  transition: right var(--dur-med) var(--ease-premium);
}
.nav__item > a:hover::after, .nav__item > a:focus-visible::after { right: 0; }
[dir="rtl"] .nav__item > a::after { left: 100%; right: 0; }
[dir="rtl"] .nav__item > a:hover::after, [dir="rtl"] .nav__item > a:focus-visible::after { left: 0; }
.nav__menu {
  position: absolute; top: 100%; left: 0; min-width: 240px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 6px;
  box-shadow: var(--shadow); padding: 10px; opacity: 0; visibility: hidden;
  transform: translateY(6px) scale(.98); transform-origin: top left;
  transition: opacity var(--dur-fast) var(--ease-premium), transform var(--dur-fast) var(--ease-premium), visibility var(--dur-fast);
  z-index: 10;
}
/* A NAVIGATION PANEL IS A READING SURFACE, NOT AN AMBIENT ONE.
   css/atelier.css gives .crystal a 42%-opacity navy ground with a
   backdrop blur, and on the dark hero it was always seen against, that
   is a beautiful piece of glass. The mega menus carry that class and
   set their links in ivory to suit it.

   Making the header sticky changed what is behind them. A menu opened
   half way down a page now sits over cream body copy, and at 42% the
   ground averages out light while the text stays ivory — the panel
   became a smear with a page heading legible straight through it. The
   fault was always in the value; only the fix to the header made it
   possible to see.

   So the mega panel gets a ground opaque enough to be read against
   anything. The blur, the edge and the shadow stay, because the glass
   was never the problem. Both classes are named so this beats
   `.crystal` on specificity regardless of stylesheet order.

   Solid, not merely raised. At 93% a page heading was still legible as
   a ghost through the panel, and a navigation surface with the previous
   page showing through it does not read as glass — it reads as a
   rendering fault. The blur, the edge, the inner highlight and the
   shadow all survive, and they are what made the treatment; the
   transparency was never the part doing the work. */
.nav__menu.crystal { background: #0E1B33; }
[dir="rtl"] .nav__menu { left: auto; right: 0; transform-origin: top right; }
.nav__item--has-menu:hover .nav__menu,
.nav__item--has-menu:focus-within .nav__menu { opacity: 1; visibility: visible; transform: translateY(0) scale(1); }
.nav__menu a { display: block; padding: 10px 12px; border-radius: 4px; color: var(--ink); font-size: .86rem; font-weight: 600; transition: background var(--dur-fast) ease, color var(--dur-fast) ease, padding-left var(--dur-fast) var(--ease-premium); }
.nav__menu a:hover { background: var(--ivory); color: var(--red); padding-left: 16px; }
[dir="rtl"] .nav__menu a:hover { padding-left: 12px; padding-right: 16px; }

.header__actions { display: flex; align-items: center; gap: 16px; }
.nav__toggle {
  display: none; flex-direction: column; gap: 5px; background: none;
  border: none; cursor: pointer; padding: 6px;
  /* The single most important control on a phone: it is how the whole
     site is navigated, and it was 22px tall. */
  min-height: 44px; min-width: 44px; align-items: center; justify-content: center;
}
.nav__toggle span { width: 22px; height: 2px; background: var(--gold); display: block; transition: transform var(--dur-fast) ease, opacity var(--dur-fast) ease; }
/* The burger becomes the close control rather than gaining a second
   one: the drawer sits BELOW the header (see .nav-mobile, next), so
   this button is never covered by it and stays exactly where a reader
   last touched it. 7px = the 2px bar plus the 5px gap between them —
   the middle bar's own position — so the two outer bars rotate onto
   the line the middle one already occupied rather than to an
   eyeballed offset. */
.site-header.is-open .nav__toggle span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.site-header.is-open .nav__toggle span:nth-child(2) { opacity: 0; }
.site-header.is-open .nav__toggle span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* THE DRAWER WAS A FLAT LIST, THEN A GROUPED ONE, AND BOTH STILL READ
   AS A DROPDOWN WEDGED UNDER A HEADER — a utility, not an experience.
   A flagship institution's mobile navigation should feel like turning
   to a bound prospectus's contents page: one full-height room with its
   own ground, the six pillars named once in the same display type the
   hero uses, and nothing else visible until a reader chooses one to
   open. Six short names is spacious. Six names each already unrolled
   into five or six links at once — which is what "grouped" still
   meant — is the jam-packed wall this replaces.

   So each pillar is now an ACCORDION: closed by default, one open at
   a time. Not a new component — .accordion__item/__q/__a is the exact
   markup and JS the FAQ pages already use (js/site.js wires
   aria-expanded/aria-controls and single-open-sibling-closing once,
   generically, for every instance of these classes on the page), only
   restyled here for a dark ceremonial ground instead of reimplemented.

   POSITIONING. The drawer sits from the header's true bottom edge to
   the viewport's, via var(--header-h) — js/motion.js measures the
   header's real height on load, scroll and resize (it used to measure
   this only when a page also had a contents rail, which is roughly
   forty pages short of "every page"; now unconditional). Starting
   below the header rather than covering it means the toggle button
   above is never hidden behind the very panel it opened, with no
   z-index contest and no separate close control to keep in sync. */
/* =========================================================
   THE HEADER CLOCK
   One compact line above the quicknav strip: London time,
   the reader's own local time, and today's date. Mobile only
   and deliberately textual rather than an analogue face — the
   first version put a full clock panel and an eight-tile grid
   in the homepage body, which then duplicated the quicknav
   strip's own six pillars. This is the entire replacement:
   one line of chrome, sitewide, costing almost nothing in
   height, in the one place — above the strip — a reader
   actually glances for it.
   ========================================================= */
/* Polished ivory, not navy — the strip and the clock line above it are
   their own panel, set apart from the header bar it sits under (which
   stays navy) the way a printed programme insets a cream card into a
   dark cover: the material changes, the institution's palette doesn't.
   Every colour below is chosen to hold >= 4.5:1 on this ground. */
.headerclock { display: none; }
@media (max-width: 860px) {
  .headerclock {
    display: flex; align-items: center; gap: 8px;
    position: relative; overflow: hidden;
    background: linear-gradient(180deg, var(--warm-white) 0%, var(--pearl) 100%);
    border-top: 1px solid rgba(199,162,74,.4);
    padding: 8px 20px;
    font-family: var(--font-body); font-size: .72rem; font-weight: 600;
    color: var(--ink);
  }
  .headerclock .icon { width: 14px; height: 14px; color: var(--gold-antique); flex-shrink: 0; }
  .headerclock__times { display: flex; align-items: baseline; gap: 5px; }
  .headerclock__tz {
    font-size: .58rem; font-weight: 700; letter-spacing: .06em;
    color: var(--bronze); margin-inline-start: 1px;
  }
  .headerclock__sep { color: var(--bronze); }
  .headerclock__date {
    margin-inline-start: auto; color: var(--slate);
    font-size: .68rem; letter-spacing: .01em; flex-shrink: 0;
  }
}

/* =========================================================
   THE VESSEL
   The shared worked-metal tile used everywhere a shape needs
   to be clicked: the quicknav strip, and the footer's own
   secondary-pages grid below. Both live on the same polished
   ivory ground now, so the vessel no longer needs to contrast
   itself against navy — it earns its presence by elevation
   instead, the way a blind-embossed card reads as a raised
   object on paper the same colour as itself. Two shapes, two
   materials — never one:
   1. .vessel — the setting. A rounded square in warm pearl,
      lifted off the cream panel behind it by shadow and a gold
      hairline rather than by colour contrast alone. Two
      travelling lights circle its edge — a warm gold comet
      (::before) and a cooler, quicker diamond spark (::after),
      counter-rotating at different speeds so the two never lap
      each other in sync.
   2. .vessel__badge — dark dome, gilded glyph: radial sapphire-
      to-oxford, a genuine bevel (inset highlight along the top,
      inset shadow along the bottom). The glyph itself is drawn
      in an actual foil gradient (stroke: url(#i-foil-gold),
      partials/icons.html) rather than a flat gold tint, so it
      catches graduated light the way a struck foil stamp does,
      not a single embossed colour.
   Deliberately not scoped to any breakpoint or section here —
   it is the callers (.quicknav, .footergrid) that decide where
   and when a vessel appears; the material itself is constant.
   ========================================================= */
/* The bezel the badge is seated in, cut on the same logic: a chamfer
   with a hard stop rather than a smooth radial, so the plate and the
   badge belong to the same object instead of being a disc resting on a
   pillow. The angle matches the badge's 152deg, because two bevels lit
   from different directions is what makes a composite read as assembled
   rather than struck. */
.vessel {
  position: relative; width: 80px; height: 80px; border-radius: 22px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background:
    linear-gradient(152deg,
      rgba(255,255,255,.95) 0%,
      rgba(255,255,255,.34) 31.5%,
      rgba(255,255,255,0) 32.1%,
      rgba(120,100,60,.10) 32.7%,
      rgba(120,100,60,.26) 100%),
    radial-gradient(circle at 32% 26%, var(--warm-white) 0%, var(--pearl) 55%, var(--stone) 100%);
  box-shadow:
    inset 0 1.5px 0 rgba(255,255,255,.98),
    inset 0 -1.5px 0 rgba(120,100,60,.42),
    inset 0 6px 10px -4px rgba(255,255,255,.85),
    inset 0 -8px 12px -5px rgba(120,100,60,.30),
    0 14px 26px -12px rgba(20,30,50,.42),
    0 0 0 1.5px rgba(199,162,74,.55);
}
/* THE HALOES WERE PAINTING WEDGES, NOT RINGS.
   A conic gradient fills its whole box from the centre outward, so a
   conic with one bright arc is a PIE SLICE — and that is precisely what
   these were: a hard triangular beam of white sweeping across the face
   of every badge in the footer grid and the quicknav strip, on every
   page. It reads as a rendering fault, which is the worst thing a
   decorative effect can read as, and it survived because the intent in
   the comment above ("two travelling lights circle its edge") describes
   what everyone assumed the code did.

   The fix is the mask-composite ring from css/atelier.css: paint the
   conic across the box, then subtract the content box from the border
   box so only the frame survives. The light now travels the PERIMETER,
   which is what a comet round an edge means. Radii are concentric with
   the vessel — 24px inset by 3px is 27px, and any other number puts the
   ring at a varying distance from the corner it is orbiting. */
.vessel::before, .vessel::after {
  content: ''; position: absolute; inset: -3px; border-radius: 27px;
  padding: 2.5px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
.vessel::before {
  background: conic-gradient(from 0deg,
    transparent 0deg, transparent 290deg,
    rgba(212,175,55,.55) 316deg, var(--gold-rich) 340deg, #fff6da 350deg, var(--gold-rich) 358deg,
    transparent 360deg);
  filter: blur(1.2px);
  animation: glance-halo-spin 4.5s linear infinite;
  animation-delay: var(--halo-delay, 0s);
}
.vessel::after {
  background: conic-gradient(from 180deg,
    transparent 0deg, transparent 336deg,
    rgba(255,255,255,.7) 349deg, #ffffff 355deg, rgba(255,255,255,.75) 359deg,
    transparent 360deg);
  filter: blur(.8px);
  animation: glance-halo-spin 6.5s linear infinite reverse;
  animation-delay: calc(var(--halo-delay, 0s) + 1.1s);
}
@keyframes glance-halo-spin { to { transform: rotate(360deg); } }
/* THE BADGE IS STRUCK, NOT MOULDED.
   It used to be a single radial gradient, which is how you draw a
   sphere: light falls off smoothly in every direction and the eye reads
   soft plastic. A struck medal does not do that. It has FACETS — a
   chamfer cut around the rim at an angle to the face, so the surface
   changes direction along a hard line and the two planes take light
   differently.

   That hard line is what the layered gradients below produce. The first
   is the chamfer: a linear gradient with a near-instant stop (34% to
   34.6%) so it terminates on an EDGE rather than a fade, angled at 152deg
   so the cut runs across the upper-left face the way a bevel would under
   a light set high and left. The conic beneath it varies the rim's
   brightness around the circumference, which is what stops the chamfer
   reading as a flat wedge pasted on.

   The box-shadow does the same job at the boundary: a 0-blur inset at the
   top and bottom draws the crisp lit and shaded edges of the bevel, and
   the blurred insets behind them give the face its depth. Blur alone
   reads as a dome; a hard edge in front of blur reads as metal. */
.vessel__badge {
  position: relative; overflow: hidden; width: 58px; height: 58px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background:
    linear-gradient(152deg,
      rgba(255,255,255,.34) 0%,
      rgba(255,255,255,.13) 33.4%,
      rgba(255,255,255,.02) 34%,
      rgba(0,0,0,.10) 34.6%,
      rgba(0,0,0,.30) 100%),
    conic-gradient(from 196deg at 50% 50%,
      rgba(255,255,255,.16) 0deg,
      rgba(0,0,0,.20) 84deg,
      rgba(0,0,0,.26) 190deg,
      rgba(255,255,255,.20) 300deg,
      rgba(255,255,255,.16) 360deg),
    radial-gradient(circle at 34% 26%, var(--sapphire) 0%, var(--navy) 54%, var(--oxford) 100%);
  box-shadow:
    inset 0 1.5px 0 rgba(255,255,255,.50),
    inset 0 -1.5px 0 rgba(0,0,0,.62),
    inset 0 7px 11px -4px rgba(255,255,255,.16),
    inset 0 -9px 13px -5px rgba(0,0,0,.60),
    0 8px 16px -8px rgba(0,0,0,.66),
    0 0 0 1.5px rgba(199,162,74,.52);
}
/* The badge's own surface is foiled, not just the glyph on it: a fixed
   specular sweep — bright along the upper-left curve, dark along the
   lower-right — the way struck metal catches light on one face and
   holds shadow on the other. mix-blend-mode:overlay so it reads as a
   property of the surface rather than a sticker sitting on top of it. */
.vessel__badge::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  background: linear-gradient(128deg,
    rgba(255,255,255,.55) 0%, rgba(255,255,255,.14) 22%,
    transparent 46%, transparent 60%, rgba(0,0,0,.22) 100%);
  mix-blend-mode: overlay;
}
.vessel__badge .icon {
  position: relative; width: 25px; height: 25px;
  stroke: url(#i-foil-gold); color: var(--gold-soft);
  filter: drop-shadow(0 1px 1px rgba(0,0,0,.45));
}
/* The gold variant — the quicknav strip's Full Menu tile, or any
   future "this one is the escape hatch" case — inverts the badge
   only, so the frame around it stays the same pearl every vessel
   uses; one element changes, not two. The foil stroke would vanish
   against a gold badge, so this is the one place the icon reverts
   to a flat, dark ink line instead. */
.vessel__badge--gold {
  background: radial-gradient(circle at 32% 24%, var(--gold-champagne) 0%, var(--gold-royal) 55%, var(--gold-antique) 100%);
  box-shadow:
    inset 0 3px 4px rgba(255,255,255,.6),
    inset 0 -4px 6px rgba(90,62,20,.5),
    0 6px 12px -7px rgba(0,0,0,.5);
}
.vessel__badge--gold .icon { stroke: var(--royal-deep); color: var(--royal-deep); filter: none; }
/* A smaller cut of the same two materials — the footer's categorised
   link grids sit at this scale rather than the full 80px showcase
   size, so six-to-a-section reads as a dense index, not another hero
   grid competing with Explore Further above it. */
.vessel--sm { width: 50px; height: 50px; border-radius: 15px; }
.vessel--sm::before, .vessel--sm::after { inset: -4px; border-radius: 18px; }
.vessel--sm .vessel__badge { width: 37px; height: 37px; }
.vessel--sm .vessel__badge .icon { width: 16px; height: 16px; }
/* The badge alone, no squircle frame and no travelling light — for a
   menu row rather than a showcase grid. A list of seven does not need
   seven lights in motion; the foiled dome and its bevel already say
   "worked metal" on their own, restraint the rest of the way. */
.vessel__badge--sm { width: 34px; height: 34px; }
.vessel__badge--sm .icon { width: 15px; height: 15px; }
@media (prefers-reduced-motion: reduce) {
  .vessel::before, .vessel::after { animation: none; opacity: .8; }
}

/* =========================================================
   THE QUICKNAV STRIP
   A horizontally-swiping row of the six pillars, sitting
   inside the header itself rather than in page content —
   mobile only, since the six pillars already occupy the
   visible nav row from 861px up, and repeating them there
   would be chrome doing nothing.

   Native scroll-snap carries the entire "swipe" interaction:
   seven flex children in an overflow-x row with
   scroll-snap-align on each. No carousel library, no JS
   beyond the one click handler that makes the last tile open
   the drawer (js/site.js).

   --header-h is measured from the header's real rendered
   height (js/motion.js), so adding this permanent row to the
   header does not require touching the drawer's top offset —
   it already starts wherever the header actually ends.
   ========================================================= */
.quicknav { display: none; }
@media (max-width: 860px) {
  .quicknav {
    display: block;
    position: relative; overflow: hidden;
    background: linear-gradient(180deg, var(--pearl) 0%, var(--warm-white) 100%);
    border-top: 1px solid rgba(199,162,74,.4);
    padding: 14px 0;
  }
  .quicknav__track {
    display: flex; gap: 16px;
    overflow-x: auto; scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    padding: 10px 20px;
  }
  .quicknav__track::-webkit-scrollbar { display: none; }
  .quicknav__tile {
    scroll-snap-align: start; flex: 0 0 auto; width: 90px;
    display: flex; flex-direction: column; align-items: center; gap: 9px;
    text-align: center; background: none; border: 0; padding: 0; font: inherit; cursor: pointer;
    min-height: 44px;
  }
  .quicknav__label {
    font-family: var(--font-body); font-weight: 700; font-size: .66rem;
    letter-spacing: .01em; color: var(--ink);
  }
  .quicknav__tile--full .quicknav__label { color: var(--bronze); }
}

/* .nav-mobile is a direct child of .site-header, which is a .lattice —
   and `.lattice > *:not(.lattice__bed) { position: relative }`
   (css/atelier.css) outranks the plain `.nav-mobile` selector below on
   specificity (two classes beats one), silently flipping this drawer
   from fixed to relative. Relative position then reads its own `top`
   as an offset from its normal in-flow slot — already below the
   header's own content — instead of from the viewport, stacking a
   second header's worth of empty space above the drawer before any
   menu content appears. The three-class selector here outranks the
   lattice rule back. */
.site-header.lattice > .nav-mobile,
.nav-mobile {
  display: none; flex-direction: column;
  position: fixed; inset-inline: 0; top: var(--header-h, 90px); bottom: 0; z-index: 60;
  padding: 8px 28px 40px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: linear-gradient(175deg, var(--midnight) 0%, var(--oxford) 100%);
}
/* The engraved medallion this site uses for every ceremonial ground —
   the hero, a page masthead — reused at drawer scale: a bespoke
   pseudo-element rather than the shared .guilloche class, because that
   class sets overflow:hidden, which is exactly what a scrolling drawer
   cannot have. */
.nav-mobile::before {
  content: ''; position: absolute; inset: -8% -14% auto -14%; height: 62%; z-index: 0; pointer-events: none;
  background: url('/assets/art/guilloche.svg') no-repeat center 22% / clamp(460px, 96vw, 640px) auto;
  opacity: .15;
  -webkit-mask-image: radial-gradient(ellipse 72% 72% at 50% 30%, #000 6%, transparent 74%);
  mask-image: radial-gradient(ellipse 72% 72% at 50% 30%, #000 6%, transparent 74%);
}
.nav-mobile > * { position: relative; z-index: 1; }

.nav-mobile .accordion { border-top: 1px solid rgba(199,162,74,.2); margin-top: 10px; }
.nav-mobile .accordion__item { border-bottom: 1px solid rgba(199,162,74,.2); }
.nav-mobile .accordion__q {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 2px; background: none; border: none; text-align: start; cursor: pointer;
  font-family: var(--font-display); font-weight: 600; font-size: 1.32rem; line-height: 1.2;
  color: var(--gold-champagne); min-height: 44px;
}
.nav-mobile .accordion__q-label { display: flex; align-items: center; gap: 16px; }
.nav-mobile .accordion__q .plus { color: var(--gold-royal); font-weight: 400; font-size: 1.5rem; }
.nav-mobile .accordion__a-inner { padding: 2px 2px 20px; max-width: none; }
.nav-mobile .accordion__a-inner a {
  display: flex; align-items: center; min-height: 44px; padding: 5px 2px 5px 4px;
  color: rgba(247,244,236,.78); font-weight: 500; font-size: .95rem;
}
[dir="rtl"] .nav-mobile .accordion__a-inner a { padding: 5px 4px 5px 2px; }
.nav-mobile .accordion__a-inner a:hover { color: var(--gold-bright); }

/* Accessibility and Privacy are obligations, not destinations —
   present, but visibly quieter, and not worth an accordion of their
   own for two lines. */
.nav-mobile__group--utility { display: flex; flex-wrap: wrap; column-gap: 22px; row-gap: 4px; padding: 20px 2px; border-bottom: 1px solid rgba(199,162,74,.2); }
.nav-mobile__group--utility a { min-height: 36px; display: flex; align-items: center; color: rgba(247,244,236,.55); font-weight: 500; font-size: .82rem; }
.nav-mobile__lang { display: flex; align-items: center; justify-content: space-between; padding: 20px 2px; }
.nav-mobile .btn { margin-top: 8px; padding-block: 16px; justify-content: center; }

/* 1180, not 980, and the number is measured rather than chosen.
   With the identity in the rail there are four groups on one line —
   crest and wordmark, eight navigation items, two buttons — and at
   1024 they did not fit: Portal disappeared and "Apply Now" was
   clipped by the viewport edge. The old 980 breakpoint was set when
   the wordmark lived in a band of its own and the rail carried only
   the nav.

   Nothing on this site checked the widths in between. The route audit
   measures overflow at 1440 and at 390, and 1024 is neither, so a
   header that broke on every laptop passed every check. It now
   measures 1280, 1180 and 1024 as well. */
/* EIGHT PILLARS NEED MORE RAIL THAN SIX, and the rail had no slack.
   Adding Study and Standards took the navigation from 611.8px to 772.8px
   while the space available to it at 1440 is 648.8px — measured in
   Chromium, header overflowing at 1440 and 1280 and the page itself
   scrolling sideways at 1280.

   The design's own rule is that a rail either fits or collapses to the
   menu, never wraps. Collapsing from 1500px down would hand the hamburger
   to every 1280 and 1366 laptop, which is most laptops, so the rail
   tightens instead: gap to its clamp floor and type down one step, which
   measures 640px against a ~649px budget.

   This is deliberately NOT inside a max-width query. The first attempt
   tightened only below 1500px on the assumption that a wider viewport
   leaves the rail more room. It does not: .site-header__inner is capped
   at var(--wrap), so the budget is constant above that cap and the rail
   overflowed by 158px at 1920 and 152px at 1600 while passing every
   width below. Widening the window made it worse, not better, which is
   the opposite of what an untested guess predicts. Below 1180px the rail
   is display:none, so applying this unconditionally costs nothing.

   The gap is 13px rather than 14 for one measured reason: the header's
   own gap is clamp(16px, 2.4vw, 40px), which reaches its 40px maximum
   at 1920 and so eats a few pixels more there than at 1600. At 14px the
   rail cleared every width except 1920, by two pixels. 13px returns 7px
   across the seven gaps, which clears it with margin to spare. */
.nav { gap: 13px; }
.nav__item > a { font-size: .79rem; }

@media (max-width: 1180px) {
  .nav { display: none; }
  /* See the note at .site-header__inner: the slack has to move to
     whatever now leads the trailing group. */
  .site-header__inner > .header__actions { margin-inline-start: auto; }
  .btn--quiet { display: none; }
  .nav__toggle { display: flex; }
  .site-header.is-open .nav-mobile { display: flex; }
}

/* APPLY NOW SURVIVES THE COLLAPSE, AND THIS IS THE POINT OF THE RAIL.
   The first version dropped the whole actions group at 1180, so every
   laptop between 1024 and 1180 — a very large share of the visitors this
   College is written for — lost the primary call to action to a
   hamburger. Portal is a returning student's door and can live in the
   menu; an application cannot.

   560 is measured, not chosen. Brand plus button plus menu button needs
   446px in English and 411px in Arabic, against a viewport less 48px of
   padding, so the true floor is 494px. 560 keeps roughly 66px in hand
   for a longer wordmark or a visitor whose browser text size is larger
   than the default, rather than sitting on the number. Below it the
   drawer carries Apply Now as its last item. */
@media (max-width: 560px) {
  .header__actions .btn { display: none; }
}

/* 380 IS MEASURED, NOT CHOSEN. At 320 the wordmark row had no slack at
   all: brand 226px + burger 44px + padding filled the viewport to
   within two pixels in one Chromium build and overflowed it by four in
   another — the deploy pipeline's browser, where it failed the chrome
   sweep this rule now satisfies. A row that fits only in the renderer
   it was measured in does not fit. .86rem returns roughly 14px of
   hand, which covers every font-metric spread observed and then some. */
@media (max-width: 380px) {
  .brand { gap: 10px; }
  .brand__name { font-size: .86rem; }
}

/* -------------------- Hero -------------------- */
.hero {
  background:
    radial-gradient(circle at 85% -10%, rgba(199,162,74,.18), transparent 55%),
    linear-gradient(180deg, var(--royal) 0%, var(--royal-deep) 100%);
  color: #fff; padding: 120px 0 100px; overflow: hidden; position: relative;
}
/* The institution's crest seal (assets/images/crest-seal.jpg), used
   as a large ambient watermark rather than a literal logo placement.
   mix-blend-mode: screen drops the seal's own dark-navy background
   out entirely against the hero's dark gradient — only the gold
   crown/shield/lettering read, with no visible image-rectangle edge —
   so the full artwork can be used here even though it isn't a
   transparent PNG. */
.hero__crest-watermark {
  position: absolute; right: -80px; top: -60px; width: 560px; height: 560px;
  background-image: url('/assets/images/crest-seal.jpg');
  background-size: cover; background-position: center;
  opacity: .16; pointer-events: none; mix-blend-mode: screen;
  animation: hero-watermark-drift 40s var(--ease-premium) infinite alternate;
}
/* An extremely slow, subtle drift — "parallax" in spirit (the
   watermark shifts independently of scroll-fixed content) without
   requiring a scroll listener: a 40s alternate animation reads as
   ambient depth, not motion a learner is meant to consciously track. */
@keyframes hero-watermark-drift {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(-16px, 12px) scale(1.03); }
}
.hero__inner { max-width: 760px; }
.hero h1 {
  font-size: clamp(2.2rem, 4.6vw, 3.6rem); font-weight: 600;
  animation: hero-rise var(--dur-slow) var(--ease-premium) both;
}
.hero .lede { font-size: 1.2rem; margin-bottom: 8px; animation: hero-rise var(--dur-slow) var(--ease-premium) .08s both; }
.hero .btn-row { animation: hero-rise var(--dur-slow) var(--ease-premium) .16s both; }
.hero__stats {
  display: flex; gap: 40px; margin-top: 48px; flex-wrap: wrap;
  border-top: 1px solid var(--line-dark); padding-top: 28px;
  animation: hero-rise var(--dur-slow) var(--ease-premium) .24s both;
}
.hero__stat strong { display: block; font-family: var(--font-display); font-size: 1.9rem; color: var(--gold-bright); }
.hero__stat span { font-size: .76rem; letter-spacing: .1em; text-transform: uppercase; color: rgba(247,244,236,.65); }
/* Onboarding-grade entrance for the single most important content on
   the site — a page's first paint. Staggered by 80ms per element
   above so the headline settles a beat before the supporting copy,
   lede, CTA, and stat row follow — deliberate, not simultaneous. */
@keyframes hero-rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* -------------------- Cards / grid -------------------- */
.grid { display: grid; gap: 28px; }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid--3, .grid--4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; } }

.card {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 32px; position: relative; overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-med) var(--ease-premium), box-shadow var(--dur-med) var(--ease-premium), border-color var(--dur-med) ease;
}
/* A hairline gold rule that draws in from the left on hover — the
   card equivalent of the button's shine sweep, at a scale appropriate
   to a resting surface rather than a call to action. */
.card::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: var(--gold-gradient); transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-med) var(--ease-premium);
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-md); border-color: rgba(199,162,74,.55); }
.card:hover::before { transform: scaleX(1); }
.card--dark { background: rgba(255,255,255,.04); border-color: var(--line-dark); }
.card--dark:hover { border-color: var(--line-dark); }
.card h3 { font-size: 1.1rem; }
.card__num { font-family: var(--font-display); font-size: .82rem; color: var(--gold); font-weight: 700; margin-bottom: 10px; display: block; }
/* Gold-on-paper computes to ~2.3:1, well under WCAG AA's 4.5:1 for
   text this size — switching to --red on light grounds matches the
   same fix already applied to .ledger th just below, which has the
   identical contrast problem in the identical context. */
.section--light .card__num, .section--paper .card__num { color: var(--red); }

/* -------------------- The badge dome --------------------
   A struck disc, not a floating glyph — bevel above, shadow below,
   a gold hairline rim. Global so any card, on any page, that wants
   an icon has a surface to give it rather than empty air above a
   heading. --dark is the same recipe recut for a card--dark ground. */
.badge-dome {
  width: 52px; height: 52px; border-radius: var(--r-md);
  display: grid; place-items: center; margin-bottom: 14px;
  background: radial-gradient(circle at 32% 24%, #fff 0%, var(--paper) 55%, var(--stone) 100%);
  box-shadow:
    inset 0 2px 3px rgba(255,255,255,.9),
    inset 0 -3px 5px rgba(20,20,10,.12),
    0 6px 14px -9px rgba(20,20,10,.4),
    0 0 0 1px rgba(199,162,74,.38);
  transition: transform var(--dur-med) var(--ease-premium), box-shadow var(--dur-med) var(--ease-premium);
}
.badge-dome .icon { width: 24px; height: 24px; filter: drop-shadow(0 1px 1px rgba(20,20,10,.14)); }
.badge-dome--sm { width: 44px; height: 44px; border-radius: var(--r-sm); margin-bottom: 12px; }
.badge-dome--sm .icon { width: 20px; height: 20px; }
.card:hover .badge-dome, .audience:hover .badge-dome, .region:hover .badge-dome {
  transform: translateY(-1px);
  box-shadow:
    inset 0 2px 3px rgba(255,255,255,.95),
    inset 0 -3px 5px rgba(20,20,10,.14),
    0 9px 18px -9px rgba(20,20,10,.46),
    0 0 0 1px rgba(199,162,74,.55);
}

/* --lg: the dome as the card's primary visual anchor rather than a
   marginal mark — Academics and the six study levels, where a page
   of otherwise-plain prose cards needs the icon to carry real
   weight. Gold-toned rather than ink, and the glyph breathes on the
   same 6.5s cycle .case__medallion uses, so a reader who has met one
   recognises the other as the same material, just lit differently. */
.badge-dome--lg {
  width: 84px; height: 84px; border-radius: 28px; margin-bottom: 20px;
  box-shadow:
    inset 0 3px 4px rgba(255,255,255,.92),
    inset 0 -4px 6px rgba(20,20,10,.14),
    0 12px 26px -12px rgba(20,20,10,.42),
    0 0 0 1.5px rgba(199,162,74,.42);
}
.badge-dome--lg .icon {
  width: 36px; height: 36px; stroke-width: 1.1;
  color: var(--gold-royal); filter: drop-shadow(0 1px 1px rgba(20,20,10,.18));
  animation: badge-dome-breathe 6.5s var(--ease-premium) infinite alternate;
}
.card:nth-child(2) .badge-dome--lg .icon { animation-delay: .8s; }
.card:nth-child(3) .badge-dome--lg .icon { animation-delay: 1.6s; }
.card:nth-child(4) .badge-dome--lg .icon { animation-delay: 2.4s; }
@keyframes badge-dome-breathe { from { transform: scale(1); } to { transform: scale(1.06); } }
.card:hover .badge-dome--lg {
  transform: translateY(-1px);
  box-shadow:
    inset 0 3px 4px rgba(255,255,255,.98),
    inset 0 -4px 6px rgba(20,20,10,.18),
    0 16px 30px -12px rgba(20,20,10,.5),
    0 0 0 1.5px rgba(199,162,74,.62);
}

.badge-dome--dark {
  background: radial-gradient(circle at 32% 24%, var(--sapphire) 0%, var(--navy) 55%, var(--oxford) 100%);
  box-shadow:
    inset 0 3px 4px rgba(255,255,255,.22),
    inset 0 -4px 6px rgba(0,0,0,.55),
    0 10px 22px -10px rgba(0,0,0,.7),
    0 0 0 1.5px rgba(199,162,74,.45);
}
.badge-dome--dark .icon {
  color: var(--gold-soft); stroke: url(#i-foil-gold);
  filter: drop-shadow(0 1px 1px rgba(0,0,0,.45));
}
.card--dark:hover .badge-dome--dark {
  background: radial-gradient(circle at 26% 18%, #35619f 0%, var(--sapphire) 45%, var(--navy) 100%);
  box-shadow:
    inset 0 3px 4px rgba(255,255,255,.28),
    inset 0 -4px 6px rgba(0,0,0,.6),
    0 14px 26px -10px rgba(0,0,0,.75),
    0 0 0 1.5px rgba(199,162,74,.62);
}
@media (prefers-reduced-motion: reduce) {
  .badge-dome, .card:hover .badge-dome, .audience:hover .badge-dome, .region:hover .badge-dome,
  .badge-dome--lg, .card:hover .badge-dome--lg { transform: none; transition: none; }
  .badge-dome--lg .icon { animation: none; }
}

/* -------------------- The click light --------------------
   A one-shot ring of light struck from the point a reader presses,
   positioned and removed by js/motion.js. No CSS-only fallback —
   a ring with nothing to trigger it is a decoration for nobody. */
.click-ring {
  position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; pointer-events: none; z-index: 6;
  background: radial-gradient(circle, rgba(255,246,218,.95) 0%, rgba(212,175,55,.6) 42%, transparent 74%);
  animation: click-ring-expand .65s var(--ease-premium) forwards;
}
@keyframes click-ring-expand {
  from { transform: scale(0); opacity: .95; }
  to   { transform: scale(16); opacity: 0; }
}

/* -------------------- Faculty roster -------------------- */
/* The credentials line sits between the name and the specialism, so it
   needs to read as metadata rather than as body copy — smaller, softer,
   and separated by a rule so a long degree list does not run into the
   sentence below it. --ink-soft on paper is the same pairing .lede
   already uses, so the contrast is known-good rather than guessed. */
.faculty__creds {
  font-size: .86rem; color: var(--ink-soft); margin: 0 0 10px;
  padding-bottom: 10px; border-bottom: 1px solid var(--line);
}
/* Same class of fault as .module-marker above, in the other direction:
   --ink-soft resolves to --slate, which the palette defines as secondary
   body text ON LIGHT. On the dark faculty card it measures 2.04:1, so a
   member's qualifications — the line a reader is most likely to squint
   at — were the least legible text on the card. --cerulean is the
   palette's readable-on-dark token and measures 4.72:1. */
.section--dark .faculty__creds,
.section--deep .faculty__creds,
.section--midnight .faculty__creds,
.section--oxford .faculty__creds,
.card--dark .faculty__creds { color: var(--cerulean); }

/* -------------------- Ledger table -------------------- */
.ledger { width: 100%; border-collapse: collapse; margin-top: 8px; }
.ledger th {
  text-align: left; font-size: .72rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--gold); font-weight: 700; padding: 12px 14px; border-bottom: 1px solid var(--line);
}
.section--light .ledger th, .section--paper .ledger th { color: var(--red); border-color: rgba(163,38,56,.25); }
.ledger td { padding: 16px 14px; border-bottom: 1px solid var(--line-dark); vertical-align: top; font-size: .94rem; transition: background var(--dur-fast) ease; }
.section--light .ledger td, .section--paper .ledger td { border-color: var(--line); }
.ledger tr:last-child td { border-bottom: none; }
.ledger tr:hover td { background: rgba(199,162,74,.05); }
.section--dark .ledger tr:hover td, .section--deep .ledger tr:hover td { background: rgba(231,201,122,.05); }
.ledger td strong { font-family: var(--font-display); font-weight: 600; font-size: 1rem; }
[dir="rtl"] .ledger th, [dir="rtl"] .ledger td { text-align: right; }
.table-scroll { overflow-x: auto; }

/* -------------------- Stat row -------------------- */
.stat-row { display: flex; flex-wrap: wrap; gap: 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.stat-row__item { flex: 1 1 160px; padding: 26px 20px; text-align: center; border-right: 1px solid var(--line); }
[dir="rtl"] .stat-row__item { border-right: none; border-left: 1px solid var(--line); }
.stat-row__item:last-child { border: none; }
/* strong AND b, and the omission was expensive.
   This component was only ever styled for <strong>. Of the 138 stat
   figures the site publishes, 105 are marked up with <b> — every one of
   them on a generated page, because the generators were written after
   this rule and nobody compared. Those 105 got no block display, no
   display face, no size and no colour: "B1CEFR LEVEL", "10MODULES",
   "$3,167TUITION", value and label jammed into one line of body text on
   all six level pages in both languages, the BASCE and Senate figures,
   and the competency count.
   Fixed here rather than in 105 places. The two elements mean different
   things in HTML — <strong> is importance, <b> is typographic
   prominence without it — and a headline figure in a statistics band is
   arguably the second. Both are legitimate here, so both are styled. */
.stat-row__item strong, .stat-row__item b {
  display: block; font-family: var(--font-display); font-size: 1.7rem; color: var(--red);
}
.section--dark .stat-row__item strong, .section--deep .stat-row__item strong,
.section--dark .stat-row__item b, .section--deep .stat-row__item b { color: var(--gold-bright); }
/* THE DIRECT CHILD, not any descendant.
   This targets the LABEL — the small-caps caption under the figure —
   but written as a descendant selector it also caught any <span> nested
   INSIDE the figure. The Arabic level pages wrap their numerals in
   <span dir="ltr"> so a browser does not reorder "$3,167" inside a
   right-to-left line, and that wrapper inherited the label's .74rem:
   the <b> computed to 38.4px and rendered at 11.84px, on every stat
   figure on all six Arabic pages, while the English ones were correct.
   A rule that means "the label" should say so. */
.stat-row__item > span { font-size: .74rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-soft); }
.section--dark .stat-row__item > span, .section--deep .stat-row__item > span { color: rgba(247,244,236,.65); }

/* -------------------- Dot-leader list -------------------- */
.dot-list { list-style: none; margin: 0; padding: 0; }
.dot-list li {
  display: flex; align-items: baseline; gap: 8px; padding: 14px 0;
  border-bottom: 1px solid var(--line-dark); font-size: .96rem;
}
.section--light .dot-list li, .section--paper .dot-list li { border-color: var(--line); }
.dot-list .num { font-family: var(--font-display); color: var(--gold); font-weight: 700; min-width: 2ch; }
.section--light .dot-list .num, .section--paper .dot-list .num { color: var(--red); }
.dot-list .leader { flex: 1; border-bottom: 1px dotted var(--line); height: 1px; margin: 0 8px; align-self: center; }

/* -------------------- CTA band -------------------- */
.cta-band { padding: 72px 0; text-align: center; }
.cta-band h2 { font-size: clamp(1.6rem, 3vw, 2.2rem); }
.cta-band .lede { margin: 0 auto 8px; }

/* -------------------- Placeholder / status callout -------------------- */
.callout {
  border: 1px dashed var(--line); border-radius: 8px; padding: 24px 28px;
  background: rgba(199,162,74,.06); margin: 28px 0;
}
.callout__label {
  font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; font-weight: 800;
  color: var(--red); display: block; margin-bottom: 8px;
}
.section--dark .callout, .section--deep .callout { background: rgba(231,201,122,.06); border-color: var(--line-dark); }
.section--dark .callout__label, .section--deep .callout__label { color: var(--gold-bright); }
.callout p:last-child { margin-bottom: 0; }

/* -------------------- Pull quote -------------------- */
.pull-quote {
  border-left: 3px solid var(--red); padding-left: 28px; font-family: var(--font-display);
  font-style: italic; font-size: 1.3rem; color: var(--ink); max-width: 68ch;
}
[dir="rtl"] .pull-quote { border-left: none; border-right: 3px solid var(--red); padding-left: 0; padding-right: 28px; }
.pull-quote cite { display: block; font-family: var(--font-body); font-style: normal; font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); margin-top: 14px; }

/* -------------------- Accordion (FAQ) -------------------- */
.accordion__item { border-bottom: 1px solid var(--line); }
.accordion__q {
  width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  display: flex; justify-content: space-between; align-items: center; gap: 20px;
  padding: 22px 0; font-family: var(--font-display); font-size: 1.05rem; font-weight: 600; color: inherit;
}
[dir="rtl"] .accordion__q { text-align: right; }
.accordion__q .plus { color: var(--gold); font-size: 1.3rem; transition: transform var(--dur-med) var(--ease-premium); flex-shrink: 0; }
.section--light .accordion__q .plus, .section--paper .accordion__q .plus { color: var(--red); }
.accordion__item.is-open .plus { transform: rotate(135deg); }
.accordion__q { transition: color var(--dur-fast) ease; }
.accordion__q:hover { color: var(--gold); }
.section--light .accordion__q:hover, .section--paper .accordion__q:hover { color: var(--red); }
.accordion__a { max-height: 0; overflow: hidden; transition: max-height var(--dur-med) var(--ease-premium); }
.accordion__a-inner { padding-bottom: 22px; color: var(--ink-soft); max-width: 68ch; }
.section--dark .accordion__a-inner { color: rgba(247,244,236,.75); }

/* -------------------- Forms -------------------- */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; gap: 6px; }
.field--full { grid-column: 1 / -1; }
.field label { font-size: .76rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 700; color: var(--ink-soft); }
.section--dark .field label, .section--deep .field label { color: rgba(247,244,236,.7); }
.field input, .field select, .field textarea {
  font-family: var(--font-body); font-size: .95rem; padding: 12px 14px;
  /* A comfortable thumb target. 12px padding on a .95rem line came out
     at 41px rendered — close enough to look right on a desktop mock and
     under the mark on the device where nearly every admissions form is
     actually filled in. Measured, not estimated: the route audit counts
     rendered geometry at 390px. */
  min-height: 44px;
  border: 1px solid var(--line); border-radius: 4px; background: var(--paper); color: var(--ink);
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) var(--ease-premium);
}
/* A soft glow alongside the existing outline (below) rather than
   replacing border-color on focus — .field.is-invalid's red border
   and red focus outline (below) must still win on an invalid field;
   this only adds depth, it never changes which colour "means error." */
.field input:focus, .field select:focus, .field textarea:focus { box-shadow: 0 0 0 3px rgba(199,162,74,.15); }
.field.is-invalid input:focus, .field.is-invalid select:focus, .field.is-invalid textarea:focus { box-shadow: 0 0 0 3px rgba(163,38,56,.15); }
.section--dark .field input, .section--dark .field select, .section--dark .field textarea,
.section--deep .field input, .section--deep .field select, .section--deep .field textarea {
  background: rgba(255,255,255,.06); border-color: var(--line-dark); color: #fff;
}
.field input:focus, .field select:focus, .field textarea:focus { outline: 2px solid var(--gold); outline-offset: 1px; }
.field.is-invalid input, .field.is-invalid select, .field.is-invalid textarea { border-color: var(--red); }
.field.is-invalid input:focus, .field.is-invalid textarea:focus { outline-color: var(--red); }
.field__error {
  display: none; color: var(--red); font-size: .78rem; font-weight: 600; margin-top: 5px;
}
.field.is-invalid .field__error { display: block; }
.section--dark .field__error, .section--deep .field__error { color: #E1899A; }
.form-status {
  display: none; align-items: center; gap: 10px; padding: 12px 16px; border-radius: 4px;
  font-size: .88rem; font-weight: 600; margin-bottom: 18px;
}
.form-status.is-visible { display: flex; }
.form-status--success { background: rgba(63,107,82,.12); color: #3F6B52; border: 1px solid rgba(63,107,82,.3); }
.section--dark .form-status--success, .section--deep .form-status--success { background: rgba(143,203,168,.14); color: #8FCBA8; border-color: rgba(143,203,168,.3); }
.form-status--error { background: rgba(163,38,56,.08); color: var(--red); border: 1px solid rgba(163,38,56,.28); }
.section--dark .form-status--error, .section--deep .form-status--error { background: rgba(225,137,154,.12); color: #E1899A; border-color: rgba(225,137,154,.3); }
/* Distinct from success/error: "we handed this off another way" —
   true, but not yet confirmed the way an API success is. */
.form-status--info { background: rgba(199,162,74,.1); color: #8A6A20; border: 1px solid rgba(199,162,74,.35); }
.section--dark .form-status--info, .section--deep .form-status--info { background: rgba(231,201,122,.12); color: var(--gold-bright); border-color: rgba(231,201,122,.3); }

/* Loading state on a submit button — spinner replaces nothing in
   layout (button width stays stable), label text still communicates
   state for screen readers. */
.btn--loading { position: relative; color: transparent !important; pointer-events: none; }
.btn--loading::after {
  content: ''; position: absolute; top: 50%; left: 50%; width: 16px; height: 16px;
  margin: -8px 0 0 -8px; border: 2px solid rgba(255,255,255,.4); border-top-color: currentColor;
  border-radius: 50%; animation: btn-spin .7s linear infinite; color: inherit;
}
.btn--gold.btn--loading::after { border-top-color: var(--royal-deep); border-color: rgba(10,23,48,.25); border-top-color: var(--royal-deep); }
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .btn--loading::after { animation-duration: 1.4s; } }
.form-note { font-size: .82rem; color: var(--ink-soft); margin-top: 16px; }
.section--dark .form-note, .section--deep .form-note { color: rgba(247,244,236,.6); }

/* -------------------- Level self-assessment -------------------- */
.level-quiz { max-width: 720px; }
.level-quiz fieldset { border: 0; margin: 0; padding: 0; min-width: 0; } /* reset UA fieldset chrome — grouping is semantic only here */
.level-quiz__options { display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px; }
.level-quiz__option {
  display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px;
  border: 1px solid var(--line-dark); border-radius: 8px; cursor: pointer;
  background: rgba(255,255,255,.03); transition: border-color .15s ease, background .15s ease;
}
/* THE RADIO IS A 44px CONTROL, drawn as a 20px one.
   These six sat at the browser default of 13×13 — the smallest tap
   targets on the site, on the one interaction an applicant must
   complete six times to get a level recommendation. The label around
   each was already comfortably large, but a thumb aiming at the circle
   still hit 13px, and every accessibility tool measured 13px because
   that is what the interactive element's own box was.
   Same technique as the declaration checkboxes in css/pages.css: a
   44px box with the visible mark drawn as a background image. Not
   `transform: scale()` — that changes the measured rect too, so it
   would report the small size and fail for the same reason. */
.level-quiz__option input[type="radio"] {
  -webkit-appearance: none; appearance: none;
  width: 44px; height: 44px; flex: 0 0 auto;
  margin: -12px -12px -12px -12px; cursor: pointer;
  border: 0; background: transparent center / 20px 20px no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='9' fill='rgba(8,16,34,.45)' stroke='%23C7A24A' stroke-width='1.5'/%3E%3C/svg%3E");
  transition: background-image var(--dur-fast) ease;
}
.level-quiz__option input[type="radio"]:checked {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='9' fill='rgba(8,16,34,.45)' stroke='%23E7C97A' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='5' fill='%23D4AF37'/%3E%3C/svg%3E");
}
.level-quiz__option input[type="radio"]:focus-visible {
  outline: 2px solid var(--gold-bright); outline-offset: -10px; border-radius: 50%;
}
.level-quiz__option:hover { border-color: var(--gold); }
.level-quiz__option:has(input:checked) { border-color: var(--gold); background: rgba(199,162,74,.1); }
.level-quiz__option input { margin-top: 3px; accent-color: var(--gold); flex-shrink: 0; }
.level-quiz__option span { font-size: .92rem; line-height: 1.5; color: rgba(247,244,236,.88); }
.level-quiz__result {
  margin-top: 22px; padding: 22px 24px; border: 1px solid var(--line-dark);
  border-left: 3px solid var(--gold); border-radius: 6px; background: rgba(255,255,255,.04);
}
[dir="rtl"] .level-quiz__result { border-left: none; border-right: 3px solid var(--gold); }
.level-quiz__result p { font-family: var(--font-display); font-size: 1.15rem; color: #fff; margin: 8px 0 18px; }

/* -------------------- Portal login shell -------------------- */
.portal-shell {
  max-width: 420px; margin: 0 auto; background: var(--paper); border: 1px solid var(--line);
  border-radius: 10px; padding: 40px; box-shadow: var(--shadow);
}
/* A LIGHT CARD INSIDE A DARK SECTION INHERITS THE WRONG COLOURS.
   .portal-shell paints itself --paper and then sits inside
   .section--dark, so everything in it kept the dark section's palette:
   the heading rendered #fff on cream at 1.04:1 — a headline nobody could
   read — with the eyebrow at 2.31 and the ghost button at 1.54. The card
   looked broken and no earlier check caught it, because measuring
   against the nearest SOLID ancestor found the dark section rather than
   the cream card actually behind the glyphs.

   The card therefore restates the light-ground palette for its own
   contents. Any light card dropped into a dark section needs this; it is
   not specific to the portal. */
.portal-shell { color: var(--ink); }
.portal-shell h2 { font-size: 1.4rem; text-align: center; color: var(--royal-deep); }
.portal-shell .lede, .portal-shell p { color: var(--ink-soft); }
.portal-shell .module-marker { color: var(--bronze); }
.portal-shell .btn--ghost { color: var(--bronze); border-color: rgba(121,90,50,.42); }
.portal-shell .btn--ghost:hover { color: var(--royal-deep); border-color: var(--bronze); }
.portal-shell .lede { text-align: center; margin: 0 auto 24px; font-size: .95rem; }

/* -------------------- Footer -------------------- */
/* Footer navigation is the largest single source of small tap targets
   on the marketing site — fifteen per page, on every page, because they
   are inline links at their line height. Made block-level with vertical
   padding so the target matches what the link looks like it is.
   Deliberately scoped to the footer's navigation columns: inline links
   inside body copy stay inline, because turning those into blocks would
   break the sentences they sit in. */
.footer__col a {
  display: block;
  min-height: 44px;
  display: flex;
  align-items: center;
}
.site-footer {
  background:
    radial-gradient(ellipse 70% 100% at 15% 0%, rgba(31,61,122,.36), transparent 62%),
    var(--oxford);
  color: rgba(247,244,236,.75);
  border-top: 1px solid rgba(199,162,74,.34);
  position: relative;
}
/* A second hairline just inside the first — the doubled rule that a
   printed colophon uses to close a page. Two lines at different
   weights read as deliberate; one reads as a border. */
.site-footer::before {
  content: ''; position: absolute; top: 4px; left: 0; right: 0; height: 1px;
  background: rgba(199,162,74,.14);
}
/* FIVE columns, not four. The footer has five children — the brand
   colophon plus four link columns — and the grid declared four, so
   "Get in Touch" silently wrapped onto a second row underneath the
   crest and sat there in a metre of empty space. It rendered without
   error and looked, at a glance, like a design decision.

   THE GRID IS NOW VISIBLE, not merely structural: every cell carries
   its own full hairline border rather than relying on `gap` for
   separation. A collapsed-border table trick (each cell borderless
   except a shared 1px between neighbours) was tried and dropped — it
   requires knowing which cell starts a row to avoid a stray line at a
   wrap point, and this grid re-wraps three times between 1440 and
   320px. A complete border on every cell needs no such knowledge: at
   five columns, three, two or one, each cell always draws its whole
   frame and the result is correct at every width by construction,
   which a grid built for a printed programme's specification table
   should be. */
/* Once five cells (brand + four link columns) sharing one specification
   table; now one — the brand block alone. Every other column moved into
   its own stacked .footergrid section below (see THE FOOTER GRID), so
   this is a single bordered strip rather than a grid needing three
   re-wrap breakpoints. */
.site-footer__inner { max-width: var(--wrap); margin: 44px auto 0; padding: 0 24px; }
/* The strip takes the same material as every other object on the site:
   a corner, a floor lit from within, and a rim. --r-md rather than
   --r-lg because this is a panel INSIDE the footer, not the footer
   itself, and a nested radius has to sit one step below the thing that
   contains it or the nesting reads inverted.

   (This rule is the resolution of a merge: it once carried a five-column
   grid with the same treatment, written before the columns moved out
   into .footergrid. The grid is gone; the material stays, applied to
   what is actually left.) */
.footer__brand {
  border: 1px solid rgba(199,162,74,.22); padding: 36px 30px;
  border-radius: var(--r-md);
  background: linear-gradient(168deg, rgba(255,252,240,.045), rgba(3,8,20,.14));
  box-shadow: inset 0 1px 0 rgba(242,227,192,.14);
}
.brand__crest-wrap { display: inline-block; }
.footer__brand .brand__crest--footer { width: 40px; height: 46px; margin-bottom: 14px; }
.footer__name { font-family: var(--font-display); color: #fff; font-size: 1.05rem; font-weight: 600; }
.footer__name span { font-size: .7rem; letter-spacing: .18em; text-transform: uppercase; color: var(--gold-bright); }
.footer__motto { font-style: italic; font-size: .86rem; margin-top: 12px; color: rgba(247,244,236,.6); max-width: 26ch; }
/* Same worked-vessel recipe as .glance__icon and .quicknav__badge,
   scaled to a column heading: a small dark dome with a gilded glyph,
   rather than a bare icon floating in the heading text. */
.footer__col-badge {
  width: 26px; height: 26px; border-radius: 50%; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 30% 24%, var(--sapphire) 0%, var(--navy) 55%, var(--oxford) 100%);
  box-shadow:
    inset 0 2px 2px rgba(255,255,255,.22),
    inset 0 -2px 3px rgba(0,0,0,.55),
    0 0 0 1px rgba(199,162,74,.4);
}
.footer__col-badge .icon {
  width: 13px; height: 13px; color: var(--gold-soft);
  filter: drop-shadow(0 1px 1px rgba(0,0,0,.4));
}
/* -------------------- QUICK LINKS (the swipeable strip) -------------------- */
/* Not every footer link is a destination worth browsing — a few are
   things a returning visitor wants fast, with a line of context a
   plain tile can't carry ("start here," not just "here"). Rather than
   force that context into a grid cell, it gets its own still-swipeable
   strip, on the footer's own navy rather than another ivory panel, so
   it reads as a distinct register: browse the grids above it, or grab
   one of these six directly. Same scroll-snap mechanism as the
   header's quicknav — no carousel library here either. */
.footerlinks { max-width: var(--wrap); margin: 0 auto 44px; padding: 0 24px; }
.footerlinks__heading {
  display: flex; align-items: center; gap: .7em;
  font-family: var(--font-body); font-weight: 700;
  font-size: .68rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--gold-royal); padding-bottom: 18px;
}
.footerlinks__track {
  display: flex; gap: 16px;
  overflow-x: auto; scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
  padding-bottom: 8px;
}
.footerlinks__track::-webkit-scrollbar { display: none; }
.footerlinks__card {
  scroll-snap-align: start; flex: 0 0 auto; width: 240px; min-height: 44px;
  display: flex; flex-direction: column; gap: 12px;
  padding: 22px 20px; border-radius: 10px;
  background: linear-gradient(160deg, rgba(31,61,122,.4) 0%, rgba(10,20,40,.3) 100%);
  border: 1px solid rgba(199,162,74,.32);
  transition: border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-premium);
}
.footerlinks__card:hover { border-color: rgba(199,162,74,.6); transform: translateY(-3px); }
.footerlinks__card-title {
  font-family: var(--font-display); font-weight: 600; font-size: 1.05rem;
  color: var(--gold-champagne);
}
.footerlinks__card-desc { font-size: .82rem; line-height: 1.5; color: rgba(247,244,236,.62); }
[dir="ltr"] .footerlinks__card-desc { font-size: .9rem; }
@media (prefers-reduced-motion: reduce) {
  .footerlinks__card:hover { transform: none; }
  .footerlinks__card { transition: none; }
}

.site-footer__bottom {
  border-top: 1px solid var(--line-dark); padding: 22px 24px; max-width: var(--wrap); margin: 0 auto;
  display: flex; justify-content: space-between; align-items: center; gap: 20px; flex-wrap: wrap; font-size: .78rem; color: rgba(247,244,236,.5);
}
.footer__trust { display: flex; align-items: flex-start; gap: 10px; max-width: 60ch; }
.footer__trust .icon { flex-shrink: 0; margin-top: .15em; color: var(--gold-royal); }
.footer__legal { max-width: 60ch; margin: 0; }
.footer__legal a { color: rgba(247,244,236,.7); text-decoration: underline; }
/* The language switch and social row, reused a third time (topbar,
   mobile drawer, and here) rather than restyled — a colophon is where
   a printed piece names the languages it comes in, and it is the
   other place on the page, besides the topbar, that a reader expects
   to find it. */
.footer__utility { display: flex; align-items: center; gap: 18px; margin-inline-start: auto; }

/* -------------------- THE FOOTER GRID -------------------- */
/* The footer's second register: real, published pages that don't belong
   to any one of the four link columns — fifteen of them, not a token
   handful, because a register that only samples the site isn't one. A
   still grid rather than the quicknav strip's swipeable one — everything
   visible at once, the way a prospectus lists its appendices — built
   from the same .vessel / .vessel__badge material so the two read as
   one system, not two. Polished ivory, like the header's own panel: an
   inset card of cream on the footer's navy, not another dark rectangle
   stacked on the ones above it. */
.footergrid {
  max-width: var(--wrap); margin: 0 auto 44px; padding: 0;
  position: relative; overflow: hidden; border-radius: var(--r-xl);
  background: linear-gradient(165deg, var(--warm-white) 0%, var(--pearl) 60%, var(--stone) 100%);
  border: 1px solid rgba(199,162,74,.45);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.9), 0 30px 70px -34px rgba(3,8,20,.9);
}
.footergrid__heading {
  display: flex; align-items: center; gap: .7em;
  font-family: var(--font-body); font-weight: 700;
  font-size: .68rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--bronze); padding: 30px 30px 22px;
}
/* Get in Touch carries a real address line and an email above its tile
   row — neither belongs in a tile, an email is read, not clicked as an
   icon, so it stays plain text, in the same ink as the tile labels. */
.footergrid__intro { margin: -10px 30px 20px; color: var(--slate); font-size: .87rem; }
/* inline-flex rather than block: the email keeps reading as prose next
   to its note, but still claims a full 44px of tappable height, the
   same tap-target discipline the rest of the footer's links carry. */
.footergrid__intro a { display: inline-flex; align-items: center; min-height: 44px; color: var(--bronze); }
.footergrid__intro + .footergrid__row { padding-top: 4px; }
/* Desktop scales to the content — as many columns as fit a tile that
   still reads as a considered object, five or six on the full wrap
   width, fewer as the viewport narrows — rather than a number picked
   in advance. Phone is deliberately NOT left to that same math: it
   locks to a fixed four (the vessel shrinks to make room, see below),
   falling back to three only on the narrowest handsets where even a
   shrunk vessel can't fit four without crowding. */
/* THE LAST ROW USED TO BE AN ORPHAN.
   auto-FILL with 1fr tracks stretches every column to fill the row, so a
   group of fifteen at six across left three tiles hard against the left
   edge with a third of the band empty beside them — the arrangement read
   as unfinished rather than composed.

   auto-FIT with a fixed maximum track and justify-content:center fixes
   both halves of that: tracks no longer stretch to swallow the leftover
   space, and a short final row sits centred under the rows above it. A
   group of any size now ends deliberately.

   FLEX, NOT GRID, and that distinction is the whole fix. Grid's
   justify-content centres the TRACKS within the container; it does not
   centre the items of a short final row, because those items still sit
   in the first tracks of a row whose track count the container fixed.
   auto-fit plus justify-content:center therefore left the orphan exactly
   where it had been — measured, not assumed. Wrapping flex has no track
   grid to sit in, so a short last line centres itself. */
.footergrid__row {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 14px 4px;
  padding: 0 26px 36px;
}
.footergrid__row > .footergrid__tile { flex: 0 1 168px; max-width: 168px; }
/* Phone keeps four across by sizing the tile rather than by counting
   columns, because the row is flex now and a column count would be a
   rule with nothing to apply to. The basis is what four-up costs at this
   width; the vessel shrinks below to make room for it. */
@media (max-width: 780px) {
  .footergrid__row { padding-inline: 18px; gap: 12px 2px; }
  .footergrid__row > .footergrid__tile { flex-basis: calc(25% - 2px); max-width: calc(25% - 2px); }
}
@media (max-width: 340px) {
  .footergrid__row > .footergrid__tile { flex-basis: calc(33.333% - 2px); max-width: calc(33.333% - 2px); }
}
/* The full-size vessel (Explore Further) is comfortably wide enough at
   its usual 80px that four of them plus gaps overflow a phone-width
   row; it shrinks here so the row keeps its preferred four columns
   instead of dropping to three. .vessel--sm is already small enough
   that it needs no equivalent adjustment. */
@media (max-width: 780px) {
  .footergrid .vessel:not(.vessel--sm) {
    width: 62px; height: 62px; border-radius: 18px;
  }
  .footergrid .vessel:not(.vessel--sm)::before,
  .footergrid .vessel:not(.vessel--sm)::after { inset: -4px; border-radius: 21px; }
  .footergrid .vessel:not(.vessel--sm) .vessel__badge { width: 44px; height: 44px; }
  .footergrid .vessel:not(.vessel--sm) .vessel__badge .icon { width: 19px; height: 19px; }
}
.footergrid__tile {
  display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px;
  min-height: 44px; padding: 16px 6px;
  transition: transform var(--dur-fast) var(--ease-premium);
}
.footergrid__tile:hover { transform: translateY(-3px); }
.footergrid__tile:focus-visible { outline: 2px solid var(--bronze); outline-offset: 3px; border-radius: 4px; }
.footergrid__label {
  font-family: var(--font-body); font-weight: 700; font-size: .68rem;
  letter-spacing: .01em; color: var(--ink);
}
.footergrid__tile:hover .footergrid__label { color: var(--bronze); }
@media (prefers-reduced-motion: reduce) {
  .footergrid__tile:hover { transform: none; }
  .footergrid__tile { transition: none; }
}

/* -------------------- Misc -------------------- */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: start; }
@media (max-width: 860px) { .two-col { grid-template-columns: 1fr; gap: 32px; } }
ul.check-list { list-style: none; margin: 0; padding: 0; }
ul.check-list li { padding: 8px 0 8px 28px; position: relative; }
[dir="rtl"] ul.check-list li { padding: 8px 28px 8px 0; }
ul.check-list li::before {
  content: '✓'; position: absolute; left: 0; color: var(--gold); font-weight: 700;
}
[dir="rtl"] ul.check-list li::before { left: auto; right: 0; }
.section--light ul.check-list li::before, .section--paper ul.check-list li::before { color: var(--red); }

.tag-row { display: flex; flex-wrap: wrap; gap: 10px; }
.tag { border: 1px solid var(--line); border-radius: 999px; padding: .5em 1.1em; font-size: .82rem; font-weight: 600; }

.reveal {
  opacity: 0; transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-premium), transform var(--dur-slow) var(--ease-premium);
  transition-delay: var(--reveal-delay, 0s);
}
.reveal.is-visible { opacity: 1; transform: none; }

/* =====================================================================
   THE SHARED FLAGSHIP LAYER
   ---------------------------------------------------------------------
   Everything below is used by the homepage first and is deliberately
   written to be page-agnostic, because the remaining pages are being
   rebuilt onto the same system. Nothing here is homepage-specific;
   homepage-only composition lives in css/home.css.
   ===================================================================== */

/* -------------------- Display typography -------------------- */
/* SETTING A DIDONE.
   ---------------------------------------------------------------------
   Bodoni Moda is not Playfair Display with better manners, and the
   settings below are not the old ones nudged. A Didone has vertical
   stress, unbracketed serifs and extreme stroke contrast, and each of
   those changes how it must be set:

   1. WEIGHT. The hairlines are genuinely hair-thin, so at 600 on a dark
      ground the face begins to shimmer and the stems bloom into them.
      Display sizes take 500 — lighter than Playfair wanted, and the
      right answer, because the drama in a Didone is the CONTRAST
      between stem and hairline, not mass. Weight climbs back as the
      size falls, which the `opsz` axis is doing in parallel.

   2. TRACKING. Playfair took -.021em at the top of the scale. That is
      much too tight here: a Didone's serifs are flat, thin and
      horizontal, so closing the letters up makes adjacent serifs touch
      and the line reads as though it were underscored. Display-1 sits
      at -.008em — closed only enough to stop a 5rem line drifting
      apart, never enough to weld it.

   3. LEADING. Higher contrast makes each line read as a heavier band,
      so the lines need more air or the block looks striped. 1.04
      becomes 1.08 at the top of the scale.

   Everything stays clamp()ed, so there is no breakpoint at which a
   headline is the wrong size, and every value moves on one continuous
   ramp between a phone and a 1440px desktop. The top of the ramp is
   also raised (4.6rem → 5rem): the previous ceiling was set for a face
   with a larger x-height, and the same measured cap-height needs more
   point size in this one. */
.display-1 {
  font-family: var(--font-display); font-weight: 500;
  font-size: clamp(2.5rem, 1.35rem + 4.9vw, 5rem);
  line-height: 1.08; letter-spacing: -.008em; margin: 0 0 .34em;
  font-optical-sizing: auto;
}
.display-2 {
  font-family: var(--font-display); font-weight: 550;
  font-size: clamp(1.9rem, 1.2rem + 2.8vw, 3.2rem);
  line-height: 1.12; letter-spacing: -.006em; margin: 0 0 .4em;
  font-optical-sizing: auto;
}
.display-3 {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(1.32rem, 1.05rem + 1.2vw, 1.8rem);
  line-height: 1.22; letter-spacing: 0;
  font-optical-sizing: auto;
}
/* REVERSE TYPE IS OPTICALLY LIGHTER, and a high-contrast face is where
   that stops being a nicety. Light glyphs on a dark ground irradiate:
   the light spreads into the surrounding dark and eats the thin
   strokes, so the same weight that is elegant on cream looks
   under-inked on navy — while the STEMS, being wide, hold. A Didone
   set identically on both grounds therefore reads as two different
   faces. Every reversed display size takes about half a weight step
   back, and the tracking opens a hair for the same reason: irradiation
   closes the space between letters as well as inside them.

   This is the correction letterpress printers made by hand for reverse
   panels, and it is why the two grounds on this page look like one
   publication rather than like two. */
.section--dark .display-1, .section--deep .display-1,
.section--oxford .display-1, .section--midnight .display-1,
.hero--flagship .display-1, .cta--flagship .display-1 { font-weight: 560; letter-spacing: -.004em; }
.section--dark .display-2, .section--deep .display-2,
.section--oxford .display-2, .section--midnight .display-2,
.hero--flagship .display-2, .cta--flagship .display-2 { font-weight: 600; letter-spacing: -.002em; }
.section--dark .display-3, .section--deep .display-3,
.section--oxford .display-3, .section--midnight .display-3 { font-weight: 640; }
.section--dark h3, .section--dark h4, .section--deep h3, .section--deep h4,
.section--oxford h3, .section--oxford h4,
.section--midnight h3, .section--midnight h4 { font-weight: 690; }
/* Small caps used as institutional labelling — chapter numbers, plate
   captions, table headers.
   TRACKING IS A FUNCTION OF SIZE, and treating it as a constant is the
   commonest small-caps error on the web: .2em is right at 11px and
   visibly loose at 15px, because the gap the eye actually reads is the
   tracking multiplied by the size. The three steps below are the same
   optical amount of space at three sizes, which is why they read as one
   treatment rather than as three separate decisions. */
.smallcaps {
  font-family: var(--font-body); font-weight: 700;
  font-size: .7rem; letter-spacing: .2em; text-transform: uppercase;
}
.smallcaps--sm { font-size: .62rem; letter-spacing: .24em; }
.smallcaps--lg { font-size: .82rem; letter-spacing: .15em; }

/* -------------------- Ceremonial capitals -------------------- */
/* Cinzel, and the whole of its permitted territory is this one block:
   numerals that number a chapter, marks that mark a level, and the
   lockups carrying the College's own name. Tracking is wide because
   inscriptional capitals were cut wide — a Roman letter-cutter spaced
   by eye against the stone, and a Trajan face set solid stops looking
   like an inscription and starts looking like a font.

   NOT ALLOWED, named so the ration is enforceable: body copy,
   sub-headings, button labels, navigation, or any run longer than about
   four words. */
.roman-caps {
  font-family: var(--font-ceremonial); font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
}

/* -------------------- Figures -------------------- */
/* Numbers that sit in a COLUMN are lined up by the font, not by the
   author. Proportional figures make a table of hours and credits look
   like it was typed; tabular figures make it look like it was set.
   Applied to every table cell and to any statistic that has a
   neighbour it should align with. */
.ledger td, .ledger th, .stat-figure, .tabular {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* -------------------- Gold foil & paper grain -------------------- */
/* A foil sweep: a gradient that runs across large gold type so it
   catches light like stamped foil instead of sitting flat. Applied
   with background-clip, so it degrades to plain --gold-champagne on
   anything that cannot clip a background to text. */
.foil {
  background: linear-gradient(100deg,
    var(--gold-antique) 0%, var(--gold-champagne) 26%, var(--gold-rich) 44%,
    var(--gold-champagne) 62%, var(--gold-antique) 100%);
  background-size: 220% 100%;
  color: var(--gold-champagne);
}
@supports (-webkit-background-clip: text) {
  .foil {
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
  }
}
/* The sweep only travels once, on entry, and then rests. A foil
   headline that shimmers continuously is a crypto website; one that
   catches the light as it arrives is a printed cover. */
.foil.is-visible { animation: foil-sweep 2.6s var(--ease-premium) .2s both; }
@keyframes foil-sweep {
  from { background-position: 120% 0; }
  to   { background-position: 0% 0; }
}

/* Paper grain — a single tiled SVG turbulence, at an opacity low
   enough that it is felt rather than seen. It is what stops a large
   flat navy area from looking like a #14264A rectangle. */
.grain::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.4'/%3E%3C/svg%3E");
  opacity: .05; mix-blend-mode: overlay;
}
.grain > * { position: relative; z-index: 1; }

/* -------------------- Engraved guilloché ground -------------------- */
/* The banknote rosette. Tiled, very low opacity, and masked to fade
   out towards the content so it never competes with type. */
.guilloche { position: relative; overflow: hidden; }
.guilloche::before {
  content: ''; position: absolute; inset: -10% -5%; pointer-events: none; z-index: 0;
  background-image: url('/assets/art/guilloche.svg');
  /* ONE medallion, not a tiled field. Left to repeat, the rosette
     lines up into a grid of identical discs across the width of the
     section, and a repeating pattern of circles reads as wallpaper —
     the precise opposite of the engraved-plate effect this is for. A
     single large impression, centred and masked, reads as the
     watermark in a sheet of laid paper. */
  background-repeat: no-repeat;
  background-size: clamp(680px, 78vw, 1080px) auto;
  background-position: center 42%;
  opacity: .26;
  -webkit-mask-image: radial-gradient(ellipse 70% 68% at 50% 44%, #000 4%, transparent 74%);
  mask-image: radial-gradient(ellipse 70% 68% at 50% 44%, #000 4%, transparent 74%);
}
.guilloche > * { position: relative; z-index: 1; }

/* -------------------- Drawn rules -------------------- */
/* A gold hairline that draws itself in from the leading edge when its
   section arrives. Used as a chapter divider. */
.rule-draw {
  height: 1px; border: 0; margin: 0; background: var(--gold-royal);
  transform: scaleX(0); transform-origin: left;
  transition: transform 1.1s var(--ease-premium);
}
[dir="rtl"] .rule-draw { transform-origin: right; }
.rule-draw.is-visible { transform: scaleX(1); }
/* A rule with the crest lozenge set into its centre — the divider used
   between major chapters, where a plain line would be too plain. */
.rule-crest {
  display: flex; align-items: center; gap: 18px; margin: 0;
  border: 0; color: var(--gold-royal);
}
.rule-crest::before, .rule-crest::after {
  content: ''; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, currentColor 35%, currentColor 65%, transparent);
}
.rule-crest .icon { color: var(--gold-rich); }

/* -------------------- Split-text rise -------------------- */
/* Each word sits in an overflow-hidden mask and rises into it, offset
   by index. Set by js/motion.js, which writes --i per word. The mask
   is what separates this from a fade: the word arrives from behind the
   line above it, the way letterpress type is set. */
.split-line { display: block; overflow: hidden; padding-bottom: .06em; }
.split-word {
  display: inline-block; will-change: transform;
  transform: translateY(105%);
  transition: transform .95s var(--ease-premium);
  transition-delay: calc(var(--i, 0) * 55ms);
}
.split-line.is-visible .split-word { transform: none; }

/* -------------------- Typewriter -------------------- */
/* Exactly one line on the page does this. The caret is a real element
   so it can be gold and can stop blinking when typing finishes. */
.typeline { display: inline-flex; align-items: baseline; min-height: 1.2em; }
.typeline__out { white-space: pre; }
.typeline__caret {
  display: inline-block; width: 2px; height: 1em; margin-left: .12em;
  background: var(--gold-rich); animation: caret-blink 1.05s steps(1) infinite;
}
[dir="rtl"] .typeline__caret { margin-left: 0; margin-right: .12em; }
@keyframes caret-blink { 0%,49% { opacity: 1 } 50%,100% { opacity: 0 } }

/* -------------------- Framed plate (the image slot) -------------------- */
/* This is the component a photograph drops into. It is a real figure
   with a fixed aspect ratio, a gold hairline frame, drawn corner
   ornaments and a small-caps caption — so the page's composition is
   already final, and supplying artwork later changes the contents of
   the frame rather than the layout around it.

   docs/photography-brief.md lists, per slot, exactly what image goes
   here. Until then each frame holds an authored SVG plate. */
.plate { margin: 0; position: relative; }
/* The frame is deep navy on EVERY ground, including the light ones.
   Two reasons, and the second is the important one:

   1. The plates are drawn in gold line at roughly 1px. On pearl or
      cream that computes to about 1.6:1 and the artwork simply
      disappears — it was legible in the design only because it was
      being judged against a dark mock.
   2. A dark plate inset into a cream page is what a tipped-in
      intaglio plate looks like in a printed book. Matching the frame
      to the page would have been the more obvious choice and the less
      considered one. */
.plate__frame {
  position: relative; overflow: hidden;
  aspect-ratio: var(--plate-ratio, 4 / 3);
  border: 1px solid var(--line);
  /* THE PLATE IS THE LARGEST OBJECT ON THE PAGE, so it takes the
     largest radius on the scale. This is the corner the eye judges the
     whole composition by: a 600px plate with a 4px corner reads as a
     screenshot, and the same plate at --r-lg reads as something that
     was cut to a size and mounted. */
  border-radius: var(--r-lg);
  background:
    radial-gradient(ellipse 80% 70% at 50% 40%, rgba(31,61,122,.5), transparent 70%),
    linear-gradient(160deg, var(--midnight), var(--oxford));
  /* The inner vignette that was here already, plus a lit top rim and a
     turned bottom one — the frame now has a thickness, and light
     falling on it from above. */
  box-shadow: inset 0 0 60px -20px rgba(0,0,0,.7),
              inset 0 1px 0 rgba(242,227,192,.34),
              inset 0 -1px 0 rgba(6,13,28,.5);
}
.section--light .plate__frame, .section--paper .plate__frame,
.section--warm .plate__frame, .section--cream .plate__frame, .section--pearl .plate__frame {
  /* A plate sitting on paper casts a shadow onto it; one sitting on a
     dark ground does not. This is the only thing that differs. */
  border-color: rgba(199,162,74,.5);
  box-shadow: inset 0 0 60px -20px rgba(0,0,0,.7),
              inset 0 1px 0 rgba(242,227,192,.34),
              inset 0 -1px 0 rgba(6,13,28,.5),
              var(--shadow-lg);
}
.plate__frame > img, .plate__frame > svg {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* Drawn corner ornaments — two L-shaped gold rules, inset. A frame
   with corners reads as engraved; a frame without them reads as a
   border-radius someone forgot to set.

   CONCENTRIC, now that the frame itself is curved. The rule is simple
   and it is the one nearly every rounded interface gets wrong: an inner
   radius equals the outer radius MINUS the gap between them. The frame
   is --r-lg (24px) and the ornament is inset 12px, so its own corner is
   12px — any other value and the two arcs run at different distances
   from each other around the turn, which the eye reads as a wobble long
   before it can name what is wrong. */
.plate__frame::before, .plate__frame::after {
  content: ''; position: absolute; width: 26px; height: 26px; pointer-events: none;
  border: 1px solid var(--gold-royal); z-index: 2;
}
.plate__frame::before {
  top: 12px; left: 12px; border-right: 0; border-bottom: 0;
  border-top-left-radius: calc(var(--r-lg) - 12px);
}
.plate__frame::after  {
  bottom: 12px; right: 12px; border-left: 0; border-top: 0;
  border-bottom-right-radius: calc(var(--r-lg) - 12px);
}
.plate__caption {
  display: flex; align-items: center; gap: .7em;
  margin-top: 14px; color: var(--bronze);
  font-family: var(--font-body); font-weight: 700;
  font-size: .68rem; letter-spacing: .18em; text-transform: uppercase;
}
.section--dark .plate__caption, .section--deep .plate__caption,
.section--oxford .plate__caption, .section--midnight .plate__caption { color: var(--gold-soft); }
/* Ken-burns: an extremely slow drift across the plate, so a static
   illustration still has life in it. 44s, alternating, no easing
   surprises — motion a reader feels only if they stop to look. */
.plate--drift > .plate__frame > img, .plate--drift > .plate__frame > svg {
  animation: plate-drift 44s var(--ease-premium) infinite alternate;
  transform-origin: 55% 45%;
}
@keyframes plate-drift {
  from { transform: scale(1.015) translate3d(0, 0, 0); }
  to   { transform: scale(1.075) translate3d(-1.2%, .9%, 0); }
}

/* -------------------- Chapter rail -------------------- */
/* A fixed hairline down the leading edge of the viewport, marked with
   one dot per chapter, filling gold as the reader descends. It gives a
   long editorial page the thing a printed prospectus has and a web
   page usually does not: a sense of how much is left. Hidden below
   1180px, where there is no margin to put it in. */
.chapter-rail {
  position: fixed; top: 50%; left: 26px; transform: translateY(-50%);
  z-index: 40; display: none; flex-direction: column; gap: 14px;
  padding: 0; margin: 0; list-style: none;
}
[dir="rtl"] .chapter-rail { left: auto; right: 26px; }
@media (min-width: 1180px) { .chapter-rail { display: flex; } }
.chapter-rail__link {
  display: flex; align-items: center; gap: 10px;
  color: rgba(247,244,236,.45); text-decoration: none;
}
.chapter-rail__dot {
  width: 7px; height: 7px; border: 1px solid currentColor; border-radius: 50%;
  transition: background var(--dur-med) var(--ease-premium),
              transform var(--dur-med) var(--ease-premium),
              border-color var(--dur-med) ease;
}
.chapter-rail__label {
  font-size: .62rem; letter-spacing: .16em; text-transform: uppercase; font-weight: 700;
  opacity: 0; transform: translateX(-6px);
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) var(--ease-premium);
  white-space: nowrap;
}
[dir="rtl"] .chapter-rail__label { transform: translateX(6px); }
.chapter-rail__link:hover .chapter-rail__label,
.chapter-rail__link:focus-visible .chapter-rail__label { opacity: 1; transform: none; }
.chapter-rail__link.is-current { color: var(--gold-rich); }
.chapter-rail__link.is-current .chapter-rail__dot { background: var(--gold-rich); transform: scale(1.35); }
/* On a light chapter the rail must invert, or it disappears. motion.js
   sets this from the ground colour of the section under the rail. */
.chapter-rail.is-on-light .chapter-rail__link { color: rgba(22,32,46,.4); }
.chapter-rail.is-on-light .chapter-rail__link.is-current { color: var(--bronze); }
.chapter-rail.is-on-light .chapter-rail__link.is-current .chapter-rail__dot { background: var(--bronze); }

/* -------------------- Counter -------------------- */
/* The figure is rendered by JS from data-count-to. It is set in
   tabular figures so the number does not jitter horizontally while it
   counts — the single detail that separates a considered counter from
   a distracting one. */
[data-count-to] { font-variant-numeric: tabular-nums; }

/* -------------------- Wax seal -------------------- */
/* Presses in once, when the CTA arrives. Overshoots very slightly on
   the scale so it reads as pressure applied and released. */
.wax-seal { display: inline-block; transform: scale(.82); opacity: 0; }
.wax-seal.is-visible { animation: wax-press .9s var(--ease-premium) both; }
@keyframes wax-press {
  0%   { transform: scale(.82) rotate(-7deg); opacity: 0; }
  60%  { transform: scale(1.045) rotate(1.5deg); opacity: 1; }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}

/* One rule, covering every motion added in this pass — new
   animations/transforms added later should stay listed here rather
   than each shipping its own reduced-motion carve-out, so this stays
   the single source of truth for "what we consider non-essential
   motion" on this platform. */
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  html { scroll-behavior: auto; }
  .hero__crest-watermark { animation: none; }
  .hero h1, .hero .lede, .hero .btn-row, .hero__stats { animation: none; opacity: 1; transform: none; }
  .btn, .btn::before, .card, .card::before, .nav__menu, .nav__menu a,
  .nav__toggle span, .nav-mobile .accordion__a, .nav-mobile .accordion__q .plus,
  .nav__item > a::after {
    transition-duration: .01ms !important;
  }
  .btn--gold:hover, .btn--outline:hover, .btn--red:hover, .card:hover {
    transform: none;
  }

  /* --- The flagship layer, same contract as everything above ---------
     Each of these resolves to its FINISHED state, never to a hidden or
     half-drawn one. The failure mode to avoid is an entrance animation
     that is merely disabled, leaving the element stuck at opacity:0 —
     for a reader with vestibular sensitivity that is not a calmer
     page, it is a blank one. Every rule below therefore sets the end
     state explicitly rather than only setting `animation: none`. */
  .split-word { transform: none; transition: none; }
  .rule-draw { transform: scaleX(1); transition: none; }
  .foil, .foil.is-visible { animation: none; background-position: 0 0; }
  .plate--drift > .plate__frame > img,
  .plate--drift > .plate__frame > svg { animation: none; transform: none; }
  .wax-seal, .wax-seal.is-visible { animation: none; opacity: 1; transform: none; }
  .typeline__caret { animation: none; opacity: 1; }
  .site-header, .site-header__inner, .brand__crest, .brand__campus,
  .chapter-rail__dot, .chapter-rail__label { transition: none; }
  /* js/motion.js reads this same query and, when it matches, writes the
     typewriter's final string in one go instead of typing it — the
     motion is in the JS, so the carve-out has to be there too. */
}

/* -------------------- RTL global -------------------- */
[dir="rtl"] body { font-family: var(--font-body); }
[dir="rtl"] .eyebrow::before { order: 2; }
[dir="rtl"] .card__num, [dir="rtl"] .module-marker { direction: ltr; display: inline-block; }
[dir="rtl"] .hero__crest-watermark { right: auto; left: -80px; }

/* ---------------------------------------------------------------------
   THE SIGNED-IN LEARNER'S ROUTE INTO THE PORTAL
   ---------------------------------------------------------------------
   Rendered only when a real session exists (js/portal-entry.js). In the
   shipped no-key state this selector matches nothing, so the Student
   Portal page stays exactly as authored. */
.portal-entry {
  background: linear-gradient(160deg, #101F3C 0%, var(--royal) 55%, #1B3054 100%);
  color: #fff;
  padding: clamp(2rem, 1.2rem + 3vw, 3rem) 0;
}
.portal-entry[hidden] { display: none; }
.portal-entry > * { width: min(var(--wrap), calc(100% - 2.5rem)); margin-inline: auto; }
.portal-entry__welcome {
  margin: 0 0 .3rem;
  font: 600 clamp(1.5rem, 1.1rem + 2vw, 2.2rem)/1.15 var(--font-display);
}
.portal-entry__lede { margin: 0 0 1.3rem; opacity: .85; }
.portal-entry .btn-row { display: flex; gap: .8rem; flex-wrap: wrap; }

/* -------------------- The sound control -------------------- */
/* Injected by js/sonics.js into .topbar__right, and only when the
   browser can actually produce sound. Sized to the 44px the rest of
   the top bar keeps, with the visible chrome smaller than the target —
   the same anchor-is-the-target rule the social links follow. */
.sonics-toggle {
  display: inline-flex; align-items: center; gap: .5em;
  min-height: 44px; padding: 0 .7em;
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-body); font-weight: 600; font-size: .74rem;
  letter-spacing: .06em; color: rgba(247,244,236,.62);
  transition: color var(--dur-fast) ease;
}
.sonics-toggle:hover { color: var(--gold-bright); }
.sonics-toggle:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: 2px; border-radius: var(--r-xs); }
/* Three bars that stand up when sound is on and lie flat when it is
   off — the state is legible without reading the label, which matters
   because the label is the first thing a narrow viewport drops. */
.sonics-toggle__wave { display: inline-flex; align-items: center; gap: 2px; height: 14px; }
.sonics-toggle__wave i {
  display: block; width: 2px; height: 3px; border-radius: 1px;
  background: currentColor; opacity: .55;
  transition: height var(--dur-med) var(--ease-premium), opacity var(--dur-med) ease;
}
.sonics-toggle.is-on { color: var(--gold-soft); }
.sonics-toggle.is-on .sonics-toggle__wave i { opacity: 1; animation: sonics-bar 1.4s ease-in-out infinite; }
.sonics-toggle.is-on .sonics-toggle__wave i:nth-child(1) { height: 7px;  animation-delay: 0s; }
.sonics-toggle.is-on .sonics-toggle__wave i:nth-child(2) { height: 13px; animation-delay: .18s; }
.sonics-toggle.is-on .sonics-toggle__wave i:nth-child(3) { height: 9px;  animation-delay: .36s; }
@keyframes sonics-bar { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.55); } }
/* The bar shed rules drop the label first, then the whole control, in
   the same order and for the same reason as everything else in the bar. */
@media (max-width: 900px) { .sonics-toggle__label { display: none; } }
@media (max-width: 620px) { .sonics-toggle { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .sonics-toggle.is-on .sonics-toggle__wave i { animation: none; }
}

/* -------------------- THE MATERIAL, EXTENDED -------------------- */
/* The rim and the travelling light were introduced on the homepage and
   proved there. These rules take them into the shared components, so
   the other 87 pages gain the same material without a single one of
   them being edited — which is the difference between a design system
   and a page that looks better than its neighbours.

   Applied in component CSS rather than as utility classes in markup for
   one reason: most of pages/ is GENERATED (scripts/build-*.js), and a
   class typed into a generated file is stripped out the next time its
   generator runs. A rule here cannot be lost that way. */

/* THE CARD gains the lit rim. ::before is already the gold rule that
   draws across the top on hover, so the rim takes ::after — the same
   division of labour .edge-lit and .aurum keep in css/atelier.css. */
.card::after {
  content: ''; position: absolute; inset: 0; z-index: 2;
  border-radius: inherit; pointer-events: none; padding: 1px;
  background: linear-gradient(148deg,
    rgba(255,255,255,.9) 0%, rgba(199,162,74,.3) 24%,
    rgba(121,90,50,.1) 52%, rgba(52,42,30,.28) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}
/* On dark ground the light is champagne and the turned edge is
   blue-black, because a navy surface in this room does not have warm
   grey shadows. Same geometry, different material. */
.card--dark::after, .section--dark .card::after,
.section--oxford .card::after, .section--midnight .card::after {
  background: linear-gradient(148deg,
    rgba(242,227,192,.6) 0%, rgba(199,162,74,.24) 22%,
    rgba(199,162,74,.06) 46%, rgba(6,13,28,.55) 100%);
}
[dir="rtl"] .card::after { transform: scaleX(-1); }

/* THE LEDGER ROW lights as the eye crosses it — on every ledger on the
   site, not only the homepage's. Six rows of near-identical figures is
   exactly where a reader loses their line, and the fix is the oldest
   one in typesetting: mark the line being read. */
.ledger tbody tr { transition: background var(--dur-fast) ease; }
.ledger tbody tr:hover { background: rgba(199,162,74,.055); }
.section--light .ledger tbody tr:hover, .section--paper .ledger tbody tr:hover,
.section--warm .ledger tbody tr:hover, .section--cream .ledger tbody tr:hover,
.section--pearl .ledger tbody tr:hover { background: rgba(121,90,50,.05); }

/* THE TAG AND THE STRAND get the struck edge rather than a flat rule,
   so a row of them reads as a set of small objects rather than as
   outlined text. */
.tag {
  box-shadow: var(--relief-raised);
  background: linear-gradient(180deg, rgba(255,255,255,.5), rgba(255,255,255,0));
  transition: transform var(--dur-fast) var(--ease-premium),
              box-shadow var(--dur-fast) var(--ease-premium),
              border-color var(--dur-fast) ease;
}
.tag:hover { transform: translateY(-2px); border-color: var(--gold); }
.section--dark .tag, .section--oxford .tag, .section--midnight .tag {
  background: linear-gradient(180deg, rgba(242,227,192,.1), rgba(3,8,20,.2));
}

/* THE PLATE, under the pointer, is lit from its own edge. The figure's
   pseudo-elements are free — the corner ornaments live on .plate__frame
   inside it — so the ring traces the mounted artwork without disturbing
   the concentric corners that are the frame's signature. */
@media (hover: hover) and (pointer: fine) {
  .plate { position: relative; }
  .plate > .plate__frame::after { transition: box-shadow var(--dur-med) var(--ease-premium); }
  .plate:hover > .plate__frame {
    box-shadow: inset 0 0 60px -20px rgba(0,0,0,.7),
                inset 0 1px 0 rgba(242,227,192,.5),
                inset 0 -1px 0 rgba(6,13,28,.5),
                0 0 0 1px rgba(231,201,122,.42),
                var(--shadow-lg);
  }
}
@media (prefers-reduced-motion: reduce) {
  .tag, .tag:hover, .ledger tbody tr { transition: none; transform: none; }
}

/* ── THE ACTION BAND ──────────────────────────────────────────────────
   A second band beneath the rail carrying the three things a reader
   does, rather than the places a reader goes. It has to read as part of
   the header rather than as a strip of buttons parked under it, so it
   shares the header's ground and is separated from the rail by a single
   gold hairline rather than by a gap.

   The item badges are cut on the same chamfer as .vessel__badge — a hard
   stop at 152deg — so every struck surface on this site takes light from
   the same direction. That consistency is most of what separates a
   designed page from an assembled one. */
.actionband {
  position: relative;
  background:
    linear-gradient(180deg, rgba(255,255,255,.05) 0%, rgba(0,0,0,.16) 100%),
    linear-gradient(90deg, var(--oxford) 0%, var(--navy) 50%, var(--oxford) 100%);
  border-top: 1px solid rgba(199,162,74,.34);
  border-bottom: 1px solid rgba(199,162,74,.20);
}
.actionband__inner {
  max-width: var(--wrap); margin: 0 auto; padding: 0 24px;
  display: grid; grid-template-columns: repeat(3, 1fr);
}
.actionband__item {
  position: relative; display: flex; align-items: center; gap: 13px;
  padding: 13px 20px; min-height: 44px; color: rgba(247,244,236,.9);
  transition: background var(--dur-med) var(--ease-premium),
              color var(--dur-med) var(--ease-premium);
}
/* Hairline separators drawn between items only — a border on each would
   put one against the container edge, which reads as a cage. */
.actionband__item + .actionband__item::before {
  content: ''; position: absolute; inset-inline-start: 0; top: 22%; bottom: 22%;
  width: 1px; background: linear-gradient(180deg,
    transparent, rgba(199,162,74,.34) 22%, rgba(199,162,74,.34) 78%, transparent);
}
.actionband__badge {
  flex-shrink: 0; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background:
    linear-gradient(152deg,
      rgba(255,255,255,.30) 0%, rgba(255,255,255,.11) 33.4%,
      rgba(255,255,255,.02) 34%, rgba(0,0,0,.12) 34.6%, rgba(0,0,0,.30) 100%),
    radial-gradient(circle at 34% 26%, var(--sapphire) 0%, var(--navy) 56%, var(--oxford) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.44),
    inset 0 -1px 0 rgba(0,0,0,.58),
    0 4px 9px -5px rgba(0,0,0,.7),
    0 0 0 1px rgba(199,162,74,.46);
  transition: transform var(--dur-med) var(--ease-premium),
              box-shadow var(--dur-med) var(--ease-premium);
}
.actionband__badge .icon { width: 16px; height: 16px; color: var(--gold-champagne); }
.actionband__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.actionband__title {
  font-family: var(--font-body); font-weight: 700; font-size: .8rem;
  letter-spacing: .012em; color: var(--warm-white); white-space: nowrap;
}
.actionband__note {
  font-family: var(--font-body); font-size: .69rem; letter-spacing: .006em;
  color: rgba(247,244,236,.56);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.actionband__go {
  width: 15px; height: 15px; margin-inline-start: auto; flex-shrink: 0;
  color: rgba(199,162,74,.72);
  transition: transform var(--dur-med) var(--ease-premium), color var(--dur-med) var(--ease-premium);
}
/* The arrow travels along the reading direction, so it must flip in RTL
   rather than pointing back at the text it is leading away from. */
[dir="rtl"] .actionband__go { transform: scaleX(-1); }

.actionband__item:hover { background: rgba(255,255,255,.045); }
.actionband__item:hover .actionband__badge {
  transform: translateY(-1px);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.55),
    inset 0 -1px 0 rgba(0,0,0,.6),
    0 6px 13px -6px rgba(0,0,0,.75),
    0 0 0 1px rgba(199,162,74,.7);
}
.actionband__item:hover .actionband__go { color: var(--gold-champagne); transform: translateX(3px); }
[dir="rtl"] .actionband__item:hover .actionband__go { transform: scaleX(-1) translateX(3px); }
.actionband__item:focus-visible { outline: 2px solid var(--bronze); outline-offset: -3px; }

/* The lead route is the one the College actually wants taken, and it is
   marked by a gold rule under it rather than by a filled button: a third
   filled control this close to "Apply Now" in the rail above would read
   as two competing primaries. */
.actionband__item--lead::after {
  content: ''; position: absolute; inset-inline: 14px; bottom: 0; height: 2px;
  background: linear-gradient(90deg, transparent, var(--gold-champagne), transparent);
  opacity: .75;
}
.actionband__item--lead .actionband__title { color: var(--gold-champagne); }

/* Below the rail's own breakpoint the quicknav strip takes over. */
@media (max-width: 1180px) { .actionband { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .actionband__badge, .actionband__go { transition: none; }
  .actionband__item:hover .actionband__badge { transform: none; }
  .actionband__item:hover .actionband__go { transform: none; }
  [dir="rtl"] .actionband__item:hover .actionband__go { transform: scaleX(-1); }
}

/* ROMAN NUMERALS ARE LATIN SCRIPT IN AN RTL SENTENCE.
   I, V and X are Latin letters to the Unicode bidi algorithm, so a level
   numeral sitting beside Arabic can be reordered — "المستوى VIII" is safe
   alone, but the moment a numeral neighbours digits or punctuation the
   run can flip. The Arabic audit found III, VII and VIII unisolated on
   the home page. Isolating the elements that exist to hold them is one
   rule; wrapping every occurrence in markup would be dozens of edits and
   would be missed by the next person who adds a level. */
[dir="rtl"] .roman,
[dir="rtl"] .roman-caps,
[dir="rtl"] .ledger__roman { direction: ltr; unicode-bidi: isolate; display: inline-block; }
