/* =============================================================================
   brand.css — RAKMHSU brand OVERRIDE / accessibility layer
   Load LAST, after bootstrap.min.css and theme.css, with ?v=<filemtime>.
   This file is thin & ADDITIVE: it must NOT duplicate or re-skin theme.css.
   It adds (1) a11y safety nets, (2) the breadcrumb photo-hero scrim,
   (3) the per-college accent system driven by body.accent-* classes.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   0. ANCHOR-LANDING OFFSET
   The header is position:sticky (~120px tall), so an in-page anchor (e.g. the
   About mega-menu → about.php#overview) would otherwise land with its heading
   hidden behind the header. Give any hash-targeted element room to clear it.
   -------------------------------------------------------------------------- */
:target { scroll-margin-top: 120px; }

/* -----------------------------------------------------------------------------
   1. SKIP LINK  (reinforces theme.css .skip-link; guarantees on-top + legible)
   -------------------------------------------------------------------------- */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 3000;
  background: var(--maroon);
  color: var(--paper) !important;
  padding: 0.65rem 1.1rem;
  font-weight: 600;
  border-radius: 0 0 8px 0;
  text-decoration: none;
}
.skip-link:focus,
.skip-link:focus-visible {
  left: 0;
  outline: 3px solid var(--gold-bright);
  outline-offset: 2px;
}

/* -----------------------------------------------------------------------------
   2. VISIBLE FOCUS  (keyboard users — WCAG 2.4.7). Additive to theme ring.
   -------------------------------------------------------------------------- */
:focus-visible {
  outline: 2px solid var(--accent, var(--color-primary));
  outline-offset: 2px;
  border-radius: 4px;
}
/* Never strip focus from interactive elements that some resets zero out. */
a:focus-visible,
button:focus-visible,
[role="button"]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--accent, var(--color-primary));
  outline-offset: 2px;
}
/* Tab panels are focusable (tabindex=0) so deep links like #pane-admission work,
   but the browser's fragment-focus must NOT draw a ring around the whole panel on
   open. Keep the tab BUTTONS' focus visible; suppress only the panel's own ring. */
.tab-pane:focus,
.tab-pane:focus-visible { outline: none; }

/* -----------------------------------------------------------------------------
   3. SCREEN-READER-ONLY UTILITY  (visually-hidden, but focusable when needed)
   -------------------------------------------------------------------------- */
.visually-hidden,
.sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.visually-hidden-focusable:not(:focus):not(:focus-within) {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* -----------------------------------------------------------------------------
   4. BREADCRUMB PHOTO-HERO SCRIM
   Page contract drives this with $bcBg (background image). The scrim keeps
   white breadcrumb text legible over any photo. Usage:
     <section class="bc-hero" style="background-image:url(<?= $bcBg ?>)"> ... </section>
   -------------------------------------------------------------------------- */
.bc-hero {
  position: relative;
  background-color: var(--breadcrumb-hero-bg);
  background-image: var(--bc-img);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  color: var(--paper);
  isolation: isolate;

  /* ---- ONE band height, on every page, at every width ---------------------
     The band is deliberately NOT sized from each photograph's own aspect ratio.
     It was, briefly, and it worked — every picture was shown whole — but it
     made the band a different height in every section (289px where the banner
     is 1905x287, 458px where it is 1920x458), and a University site wants one
     banner, not seven. So the height is a property of the VIEWPORT alone and
     the photographs are cropped to it, which is what `cover` is for.

     --bc-band-ratio is 287/1905 — the proportions of the Admissions / Colleges
     banner, which is the height every page is matched to.

     --bc-band-floor is the measured floor at each breakpoint (set in the media
     queries below): the tallest that the crumb row + H1 themselves come to at
     that width, across all 19 pages sampled, so the floor — not the content —
     decides the height and every page lands on the same number. `min-height`
     rather than `height`, always: a heading longer than anything measured
     grows its band instead of being clipped.
       >=992px  288px   (content is 283, and exactly 288 from 1280px up)
       768-991  280px   (content 229; 276 on the longest title, `about`)
       576-767  270px   (content 224; 266 on the longest)
       <576     260px   (content 220-257; also the exact height at which the
                         3:2 phone crop is shown whole on a 390px screen)

     Above ~1912px the ratio takes over from the floor, so the band keeps
     growing on a wide monitor instead of stranding the type in a thin strip —
     capped at --bc-band-max so a breadcrumb never becomes a hero. */
  --bc-band-ratio: 0.1506;
  --bc-band-floor: 288px;
  --bc-band-max: 460px;
  min-height: max(var(--bc-band-floor), min(calc(100vw * var(--bc-band-ratio)), var(--bc-band-max)));

  /* The copy is anchored to the BOTTOM of whatever height that works out to,
     and this is legibility, not taste. The scrim above is a floor: it darkens
     from the bottom edge and is fully clear by --bc-floor-top (78%). While the
     band was content-sized the type had nowhere else to be, so nothing said so
     — the moment the band can be taller than its content, top-aligned copy
     drifts up out of the very layer that was measured to carry it. Same
     mechanism `.gal-hero` (§23.2) already uses for the same reason. */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;

  /* ---- tunables. Change the scrim HERE, not in the gradients below. --------
     --bc-wash-*  : the brand wash, left -> right. Same three scrim stops as the
                    original treatment; only the percentages are reduced.
     --bc-floor-* : a neutral (ink, NOT brand) floor that darkens only the lower
                    band where the crumb row and H1 actually sit. It buys text
                    contrast WITHOUT adding brand colour, which is what lets the
                    wash come down as far as it does.
     Raise --bc-wash-l for more brand presence; raise --bc-floor-mid for more
     text contrast on a bright photograph. */
  --bc-wash-l:     0.60;   /* was 0.92 — left edge      (-35%) */
  --bc-wash-m:     0.40;   /* was 0.78 — 45% across     (-49%) */
  --bc-wash-r:     0.12;   /* was 0.42 — right edge     (-71%) */
  --bc-floor-btm:  0.55;
  --bc-floor-mid:  0.34;
  --bc-floor-top:  78%;    /* floor is fully clear above this — sky stays sky */
}

/* WHY THESE NUMBERS (measured, not taste — see docs/CHANGELOG.md 2026-07-30):
   The 47 photographs actually used by $bcBg span a crumb-band relative luminance
   of 0.029 (alumni-hero) to blown white (chwd-hero / chwd-about-hero, 16 pages,
   whose worst 14x22px patch under the crumb row averages #FFFFFF). A TRANSLUCENT
   wash over a bright crop does not darken it — it tints it and RAISES luminance,
   which is why the old 0.92/0.78/0.42 wash read as a coloured haze while still
   flattening the photograph (it destroyed 84.5% of the image's luminance overall
   and 91.9% under the crumb strip), leaving the top-right of every crop
   essentially untouched at the new strength (12% wash, 0% floor).

   THE SIX VALUES ABOVE WERE TUNED UNDER THE RETIRED MAROON PRIMARY and are kept
   here unchanged — they are the deliverable of the branch this block came from,
   set to an explicit instruction to reduce only the percentages. The three stops
   are now the indigo scrim (--maroon-950 / --maroon-black / --maroon-rich); when
   composited at these alphas their luminance over a bright crop is 0-12% LOWER
   than the retired values, so the tokenisation darkens the band very slightly
   rather than lightening it, and no ratio measured on the old wash got worse.
   A re-sweep of 25 in-use photographs across 10 viewport/wrap cases nevertheless
   puts the crumb link at 4.18:1 at 992px and 3.16:1 in the 768-991px band, under
   the 4.5:1 AA minimum for small text. That is deliberately NOT silently
   "corrected" here: raising the floor is the visible trade-off the reduction was
   asked for in the first place, so it is on the record as a matter awaiting a
   decision in the management report. Every value is tunable in one place, above.

   `isolation: isolate` is LOAD-BEARING: .bc-hero has z-index:auto, so without it
   the z-index:-1 pseudo escapes to the root stacking context and paints BEHIND
   this section's own background-image, and the scrim disappears site-wide. */
.bc-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(90deg,
      rgba(var(--maroon-950-rgb), var(--bc-wash-l)) 0%,
      rgba(var(--maroon-black-rgb), var(--bc-wash-m)) 45%,
      rgba(var(--maroon-rich-rgb), var(--bc-wash-r)) 100%),
    linear-gradient(0deg,
      rgba(var(--ink-rgb), var(--bc-floor-btm)) 0%,
      rgba(var(--ink-rgb), var(--bc-floor-mid)) 45%,
      rgba(var(--ink-rgb), 0) var(--bc-floor-top));
}

/* Phones: a left-to-right falloff does nothing on a 390px viewport — the whole
   band is "left". Go vertical so the reduction still happens where it can be
   seen (top of the photo clear, floor under the type). The zero stop is 92%
   rather than var(--bc-floor-top): the phone band is shorter, so the floor has
   to reach higher to cover the same type. Changing --bc-floor-top does NOT
   move this one. */
/* THE BAND FLOOR, per breakpoint. See the table in .bc-hero above — these are
   measured content heights, not round numbers picked to look tidy. */
@media (max-width: 991.98px) { .bc-hero { --bc-band-floor: 280px; } }
@media (max-width: 767.98px) { .bc-hero { --bc-band-floor: 270px; } }
@media (max-width: 575.98px) { .bc-hero { --bc-band-floor: 260px; } }

/* SMALL PHONES — buy the floor enough headroom to actually win.
   The markup carries `py-5` on the section and `py-4` on the container: 144px
   of vertical padding, on a band that is 260px tall. That left ~116px for the
   crumb row and the H1, so any page whose heading took a third line — or whose
   crumb trail wrapped, which is what happens to "Sustainability & Excellence
   Hub > About & Governance > Director's Welcome" at 320px — pushed its own band
   past the floor and ended up taller than every other page. Measured: six
   different band heights across 21 pages at 320px.

   Cutting the padding to 24px + 12px leaves ~188px, which clears the tallest
   heading and the longest trail the site has at 320px, so the floor decides and
   every page matches. The type does not move up — the band is bottom-anchored,
   so this is purely the gap below it, 36px instead of 72px, on a band where the
   scrim is darkest at the bottom edge anyway.

   `!important` IS required here and is not laziness: Bootstrap's spacing
   utilities are themselves declared `!important` (`.py-5{padding:3rem!important}`),
   so loading brand.css last and out-specifying the selector both lose. Measured
   the wrong way round first — without it the rule is dead and all six heights
   survive. */
@media (max-width: 575.98px) {
  .bc-hero { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
  .bc-hero > .container { padding-top: .75rem !important; padding-bottom: .75rem !important; }
}

/* DESKTOP — the same argument, for the same reason.
   `py-lg-5` puts 96px of container padding inside the band from 992px up. With
   a 288px floor that leaves ~192px, and a heading that takes a SECOND line
   needs ~195px — so a page whose title wraps grew to 335px while every other
   page sat at 288px. It is not a rare shape: measured across a 58-page sample
   at 992px, 56 were 288px and 2 (`qaie`, `sustainability/roles-responsibilities`)
   were 335px, because Bootstrap's container is 960px wide from 992-1199 and
   only 1140px from 1200, which is where those titles start fitting on one line.

   Halving it to 48px leaves ~240px, which fits two lines with room to spare, so
   those pages join the floor instead of overriding it. A third line still grows
   the band — that is `min-height` doing its job, and no fixed height can serve a
   heading that long without clipping it. */
@media (min-width: 992px) {
  .bc-hero > .container { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
}

/* PHONES — the art-directed crop.
   A uniform band is a ~1.5:1 box on a phone, and no amount of
   `background-position` rescues a 4.19:1 picture in a 1.5:1 box: the centred
   crop keeps a 36%-wide slice, which on the dentistry banner is the blurred
   back of the room with the dentist cut off. The only honest fix is a
   different crop of the same photograph. When one exists on disk as
   `<base>-sm.<ext>`, includes/breadcrumb.php passes it as --bc-img-sm and it
   is used here; with no sibling this falls back to the wide image, so a banner
   without a phone crop renders exactly as it did before.

   Every phone crop is 3:2, which is why the <576px floor above is 260px: at a
   390px screen that is the exact height at which the crop is shown whole. */
@media (max-width: 767.98px) {
  .bc-hero {
    background-image: var(--bc-img-sm, var(--bc-img));
  }

  .bc-hero::before {
    background:
      linear-gradient(180deg,
        rgba(var(--maroon-rich-rgb), var(--bc-wash-r)) 0%,
        rgba(var(--maroon-black-rgb), var(--bc-wash-m)) 55%,
        rgba(var(--maroon-950-rgb), var(--bc-wash-l)) 100%),
      linear-gradient(0deg,
        rgba(var(--ink-rgb), var(--bc-floor-btm)) 0%,
        rgba(var(--ink-rgb), var(--bc-floor-mid)) 55%,
        rgba(var(--ink-rgb), 0) 92%);
  }
}

/* PER-PHOTOGRAPH FRAMING — keyed on the FILE, not on the page.
   Framing is a property of the photograph, and 49 pages point $bcBg at this one
   file, so a modifier class would have to be copy-pasted onto all 49 (hard rule
   3 forbids exactly that). breadcrumb.php prints the path inside the inline
   `style` attribute, which makes an attribute selector the single point of
   control — the same mechanism the note below explains cannot detect a MISSING
   image, used here for the case it can do: matching a known filename.

   WHY the Admissions banner needs it: it is authored 1905x287 (6.6:1), and the
   band is now sized to exactly that ratio, so from ~1200px up the photograph is
   whole and this rule changes nothing. Below that the H1 and crumb row are
   taller than the ratio allows, the band grows past it, and `cover` goes back to
   cropping the WIDTH — the centred crop removes ~313px from each side at 1280px,
   which is precisely where the student is. Anchoring to the photograph's right
   edge spends that crop on the empty wall at the left instead, and that wall is
   where the crumb row and H1 sit anyway.
   Phones are excluded because they no longer need it: below 768px they are
   served `breadcrumb-admissions-sm`, a 3:2 crop cut around the student. */
@media (min-width: 768px) {
  .bc-hero[style*="breadcrumb-admissions"] { background-position: right center; }
}

/* NOTE on the missing-photograph case: there is deliberately no CSS fallback for
   it. includes/breadcrumb.php emits `style="background-image:url(...)"`
   unconditionally — asset() returns the path even when the file is absent, it
   just drops the ?v — so an attribute-based selector such as
   `.bc-hero:not([style*="background-image"])` can never match, and CSS cannot see
   a 404 on the image itself. All 47 photographs currently in use were verified
   present on disk (0 missing), so the flat --breadcrumb-hero-bg background-color
   is the only fallback and it needs no extra rule. If a photograph is ever
   removed, the band degrades to the flat brand surface under the wash; fix it by
   restoring the file, not here.

   If the user has asked the OS to reduce transparency, a translucent photo scrim
   is the wrong answer — go opaque. Same value as the .bc-hero background-color
   above, via the same role, so the two can never drift apart. */
@media (prefers-reduced-transparency: reduce) {
  .bc-hero { background-image: none !important; }
  .bc-hero::before { background: var(--breadcrumb-hero-bg); }
}

.bc-hero a { color: var(--text-on-dark-bright); }
.bc-hero a:hover { color: var(--paper); }
.bc-hero .current,
.bc-hero h1 { color: var(--paper); }
.bc-hero .sep { color: var(--gold-on-media); }

/* The H1 carried no shadow. A tight one costs nothing on the dark crops and is
   the cheapest insurance on the bright ones — it is legibility, not decoration,
   so it stays even though body.home deliberately bans it (§15.2: the home hero
   is one hand-checked dark photograph; these are 47 crops). */
.bc-hero h1 { text-shadow: 0 1px 3px rgba(var(--maroon-975-rgb), .38); }

/* -----------------------------------------------------------------------------
   Breadcrumb — refined, on-brand styling (applies site-wide via the shared
   includes/breadcrumb.php photo hero). Override the vendor theme's ivory bar so
   the crumbs sit cleanly ON the photo hero: transparent, no border, mono caps,
   gold chevron separators, gold current crumb, legible over any photo.
   -------------------------------------------------------------------------- */
/* Remove the vendor .breadcrumb-bar { background: var(--ivory); border-bottom } */
.breadcrumb-bar {
  background: transparent !important;
  border-bottom: 0 !important;
}
.bc-hero .breadcrumb-bar {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .76rem;                          /* was .72rem — 11.5px was the floor
                                                 of legible for letterspaced caps */
  letter-spacing: .09em;
  text-transform: uppercase;
  margin-bottom: 1.1rem;
  text-shadow: 0 1px 3px rgba(var(--maroon-975-rgb), .55); /* legibility over bright photos */
}
/* --text-on-dark-bright (#F7F2EA), not the darker --peach-200 (#E9D9C9) this
   layer shipped before. The lift is what pays for the lighter wash: the darker
   cream was measured at 4.23:1 against the brightest crop in use — an AA fail —
   where the bright warm-white cleared it. Both of those figures were taken
   against the retired maroon wash, so they are quoted as the reason for the
   change, not as the current measurement; the indigo stops composite marginally
   darker, so the direction holds. Current numbers: docs/CHANGELOG.md 2026-07-30. */
.bc-hero .breadcrumb-bar a {
  color: var(--text-on-dark-bright);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color .15s ease, border-color .15s ease;
}
/* Hover/focus underline moved off --gold-bright for the same reason as the
   chevron below: it is the focus indicator as well as the hover affordance, and
   on a bright crop under the reduced wash --gold-bright sits at ~2.2-2.9:1,
   below the 3:1 bar for a non-text indicator. */
.bc-hero .breadcrumb-bar a:hover,
.bc-hero .breadcrumb-bar a:focus-visible {
  color: var(--paper);
  border-bottom-color: var(--gold-on-media);
}

/* ---- separator ----------------------------------------------------------
   BUG FIXED HERE. `[class*="accent-"] .breadcrumb-bar .sep { color: var(--accent) }`
   in §5 has the SAME specificity (0,3,0) as the plain `.bc-hero .breadcrumb-bar
   .sep` rule but comes later, so it wins — and it paints the chevron in the
   brand accent directly on top of the brand scrim. Under the current indigo
   identity --accent is #36298B, which composites to 2.23:1 mid-band and 1.52:1
   at the left edge: the defect got WORSE at the identity change, not better.
   (Under the retired maroon the same rule measured 1.79-2.38:1 on the 94
   `accent-medicine|dentistry|pharmacy|nursing` pages, which is how it was
   found.) The `[class*="accent-"]` prefix below raises this rule to (0,4,0) so
   the photo-hero separator is decided HERE, once, for all 404 pages regardless
   of source order. §5's rule is left intact for any future light breadcrumb bar
   outside .bc-hero.
   --gold-on-media (#F0D6A8) rather than --gold-bright (#D9BC73): the chevron is
   aria-hidden decoration, so SC 1.4.3 does not apply, but it should still clear
   the 3:1 non-text bar — and --gold-bright does not on a bright crop. */
.bc-hero .breadcrumb-bar .sep,
[class*="accent-"] .bc-hero .breadcrumb-bar .sep {
  color: var(--gold-on-media);
  margin: 0 .25rem;
  font-size: 1em;
  opacity: 1;                                 /* was .9 — compounded the failure */
}

/* Current page = the brightest item in the trail, which is also the correct
   hierarchy. Gold stays in the bar as the underline — a graphical object held to
   the 3:1 bar — rather than as small text, which would be held to 4.5:1 and
   fail. NOTE: the two declarations below the colour are branch-only content that
   git left OUTSIDE the conflict markers; they are kept deliberately, not by
   accident, and must not be dropped when this rule is next edited. */
.bc-hero .breadcrumb-bar .current,
[class*="accent-"] .bc-hero .breadcrumb-bar .current {
  color: var(--paper);
  font-weight: 500;
  padding-bottom: 2px;
  border-bottom: 2px solid var(--gold-on-media);
}

/* Forced-colors mode hands every colour back to the user's OS high-contrast
   theme, so a translucent photo scrim is the wrong answer — go opaque/system.
   THIS BLOCK MUST STAY LAST IN §4. A media query carries no specificity of its
   own, so each selector here has to out-rank the `[class*="accent-"] .bc-hero
   .breadcrumb-bar …` rules above (0,4,0); placed any earlier, or written at the
   bare `.bc-hero a` specificity, the whole block is dead code that only looks
   like forced-colors support. Canvas / CanvasText are CSS system colours and are
   deliberately NOT tokenised — substituting a brand token here would defeat the
   mode. */
@media (forced-colors: active) {
  .bc-hero { background-image: none !important; }
  .bc-hero::before { background: Canvas; }
  [class*="accent-"] .bc-hero .breadcrumb-bar a,
  [class*="accent-"] .bc-hero .breadcrumb-bar a:hover,
  [class*="accent-"] .bc-hero .breadcrumb-bar a:focus-visible,
  [class*="accent-"] .bc-hero .breadcrumb-bar .sep,
  [class*="accent-"] .bc-hero .breadcrumb-bar .current,
  .bc-hero .breadcrumb-bar a,
  .bc-hero .breadcrumb-bar a:hover,
  .bc-hero .breadcrumb-bar a:focus-visible,
  .bc-hero .breadcrumb-bar .sep,
  .bc-hero .breadcrumb-bar .current,
  .bc-hero a,
  .bc-hero .sep,
  .bc-hero .current,
  .bc-hero h1 {
    color: CanvasText;
    border-bottom-color: CanvasText;
  }
  /* text-shadow is not adjusted by forced-color-adjust, so it has to go by hand. */
  .bc-hero h1 { text-shadow: none; }
}

/* -----------------------------------------------------------------------------
   5. COLLEGE ACCENT SYSTEM
   Add ONE body class to a college page to re-tint accent elements:
     <body class="accent-medicine">  ·  <body class="accent-dentistry">
     <body class="accent-pharmacy">  ·  <body class="accent-nursing">
   ALL FOUR colleges resolve to the SINGLE university accent.
   The per-college body classes are kept in the markup for structure and for
   one-line reversibility, but they no longer shift hue.

   THE ACCENT UNITS ARE NO LONGER DECLARED HERE (decision D9). The four
   `.accent-*` triplets (--accent / --accent-deep / --accent-soft) are now OWNED
   by assets/css/tokens.css §5, which is GENERATED from $BRAND_ACCENTS in
   includes/brand-colors.php by tools/build-color-tokens.php. To change an accent,
   edit includes/brand-colors.php and re-run the generator — never re-add a
   literal declaration here.
   WHY IT MATTERS THAT THEY ARE GONE: brand.css is the LAST-loaded layer, so a
   second copy of the triplets here would out-rank tokens.css unconditionally and
   silently pin every college page to whatever hex was typed in this file, making
   the generated source of truth a no-op. That is precisely how the retired
   primary survived the first re-tone. Never reintroduce a per-college hue, and
   never reintroduce a literal accent value in this layer.
   NOTE: --gold / --gold-bright and --green are GLOBAL brand tokens, unrelated to
   this block — never fold them in here.
   Everything below keys off the --accent custom property and is unchanged.
   -------------------------------------------------------------------------- */

/* Re-tint the brand accent elements when an accent body class is present. */
[class*="accent-"] .eyebrow { color: var(--accent); }
[class*="accent-"] .eyebrow::before { background: var(--accent); }

[class*="accent-"] .text-maroon { color: var(--accent) !important; }
[class*="accent-"] a.link-arrow,
[class*="accent-"] .link-arrow { color: var(--accent); }
[class*="accent-"] .link-arrow:hover { color: var(--accent-deep); }

/* link-arrow modifier: keep the link styling but drop the trailing "→" glyph. */
.link-arrow.no-arrow::after { content: none; }

[class*="accent-"] .btn-maroon {
  background: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--accent) 28%, transparent);
}
[class*="accent-"] .btn-maroon:hover {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
}
/* Outline maroon button — used widely but never defined in theme.css (it only
   ships .btn-maroon / .btn-outline-ink). Define the bordered/ghost variant here
   so every `.btn-outline-maroon` renders as a proper button, not plain text. */
.btn-outline-maroon {
  background: transparent;
  color: var(--maroon);
  border-color: var(--maroon);
}
.btn-outline-maroon:hover,
.btn-outline-maroon:focus-visible {
  background: var(--maroon);
  color: var(--paper);
  border-color: var(--maroon);
  transform: translateY(-1px);
}
/* Follow the college accent inside an accent-* context (matches .btn-maroon). */
[class*="accent-"] .btn-outline-maroon {
  color: var(--accent);
  border-color: var(--accent);
}
[class*="accent-"] .btn-outline-maroon:hover,
[class*="accent-"] .btn-outline-maroon:focus-visible {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}

[class*="accent-"] .badge-maroon,
[class*="accent-"] .badge-soft { background: var(--accent) !important; color: var(--paper); }

[class*="accent-"] .deg-tag,
[class*="accent-"] .alert-rak { border-left-color: var(--accent); }

/* Active nav underline + breadcrumb separators follow the accent. */
[class*="accent-"] .main-nav .nav-link::after { background: var(--accent); }
[class*="accent-"] .main-nav .nav-item.active .nav-link,
[class*="accent-"] .main-nav .nav-link:hover { color: var(--accent); }
[class*="accent-"] .breadcrumb-bar .sep { color: var(--accent); }

/* The card top-bar + duration tag on accented college cards. */
[class*="accent-"] .card-college .bar { background: var(--accent); }
[class*="accent-"] .card-college .dur-tag { color: var(--paper); background: var(--accent-deep); }

/* -----------------------------------------------------------------------------
   GLOBAL SECTION RHYTHM — tighter vertical spacing between sections than the
   template default (theme.css ships .section 6rem / .section-sm 4rem). Override
   here (brand.css loads last) so the whole site shares one tighter rhythm.
   -------------------------------------------------------------------------- */
.section { padding: 3.25rem 0; }
.section-sm { padding: 2.25rem 0; }
@media (max-width: 768px) {
  .section { padding: 2.25rem 0; }
  .section-sm { padding: 1.6rem 0; }
}

/* -----------------------------------------------------------------------------
   6. COLOUR-CONTRAST GUARD  (WCAG 1.4.3 — 4.5:1 for normal text)
   --gold-bright FAILS as text on light surfaces (1.84:1 on white, measured with
   tools/color-contrast.php). It is fine on dark (mega menu, footer). Provide a
   darker on-light gold for any gold text/links that sit on white/ivory, and stop
   the bright gold from being used as body text on light backgrounds.
   RE-MEASURED after the palette re-tone: --gold itself now reads 5.12:1 on white
   (it used to be 2.65:1 and fail), so the guard is no longer needed for --gold —
   but it is KEPT AS IS, because .text-gold is applied site-wide without knowing
   which of the two golds a given surface pairs with, and --gold-bright still
   fails. Nothing about the behaviour changed; only these figures did.
   -------------------------------------------------------------------------- */
:root { --gold-text: var(--gold-700); }   /* 4.88:1 on white — gold text on light */

/* On light surfaces, force .text-gold to the accessible darker gold.
   (Dark contexts opt back in with .on-dark / .bc-hero / .mega / .site-footer.) */
.text-gold { color: var(--gold-text); }
.bg-charcoal .text-gold,
.bg-maroon .text-gold,
.bg-maroon-rich .text-gold,
.on-dark .text-gold,
.mega .text-gold,
.bc-hero .text-gold,
.site-footer .text-gold { color: var(--gold-bright); }

/* The featured card in each header dropdown sits on the dark brand surface: its
   kicker and its call-to-action stay light gold on every page, including the
   college pages whose accent colour re-tints ordinary links. */
.mega-feature .text-gold { color: var(--gold-bright) !important; }
.mega-feature .link-arrow.on-dark,
[class*="accent-"] .mega-feature .link-arrow.on-dark { color: var(--gold-bright); }
.mega-feature .link-arrow.on-dark:hover,
.mega-feature .link-arrow.on-dark:focus-visible,
[class*="accent-"] .mega-feature .link-arrow.on-dark:hover,
[class*="accent-"] .mega-feature .link-arrow.on-dark:focus-visible { color: var(--gold-on-media); }

/* -----------------------------------------------------------------------------
   7. REDUCED MOTION  (WCAG 2.3.3) — honour the OS preference globally.
   Content reveal animations already force visible state elsewhere; this is a
   blanket safety net so no transition/animation/auto-scroll runs.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  html { scroll-behavior: auto !important; }
  .skip-link { transition: none; }
}

/* -----------------------------------------------------------------------------
   8. DESIGN TOKENS — radius / shadow / spacing scale.
   Named tokens so new components share one source of truth instead of magic
   numbers. Additive: existing components keep their values until migrated.
   -------------------------------------------------------------------------- */
:root {
  --radius-sm: 7px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-pill: 999px;

  --shadow-1: 0 1px 2px rgba(var(--charcoal-rgb), .06), 0 2px 8px rgba(var(--charcoal-rgb), .05);
  --shadow-2: 0 6px 18px rgba(var(--charcoal-rgb), .10), 0 12px 32px rgba(var(--charcoal-rgb), .08);

  --space-1: .25rem;
  --space-2: .5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
}

/* -----------------------------------------------------------------------------
   9. .table-rak — branded data table.
   Opt-in: add class="table table-rak" (Bootstrap base + this skin) to a <table>.
   Uses the brand palette + design tokens; numeric cells get the mono face.
   -------------------------------------------------------------------------- */
.table-rak {
  --bs-table-bg: transparent;
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  overflow: hidden;
  font-size: .95rem;
}
.table-rak > thead > tr > th {
  background: var(--ivory);
  color: var(--ink);
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-weight: 600;
  font-size: .78rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line);
  padding: .8rem 1rem;
  text-align: left;
  vertical-align: bottom;
}
/* Body cells. A row can label itself with <th scope="row"> for screen readers;
   that cell is styled with the data cells so the row reads as one band rather
   than a bold column sitting proud of the rest. */
.table-rak > tbody > tr > td,
.table-rak > tbody > tr > th {
  padding: .75rem 1rem;
  border-top: 1px solid var(--line-soft);
  /* One rule between rows: the row above draws it. Bootstrap's base table also
     puts a border under every cell, which would double the line to 2px. */
  border-bottom: 0;
  vertical-align: top;
  text-align: left;
}
/* The row's own label: carried by weight, not by a different alignment. */
.table-rak > tbody > tr > th {
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-weight: 600;
  color: var(--ink);
}
.table-rak > tbody > tr:first-child > td,
.table-rak > tbody > tr:first-child > th { border-top: 0; }
.table-rak > tbody > tr:hover > td,
.table-rak > tbody > tr:hover > th { background: rgba(var(--maroon-rgb), .035); }
/* Numeric / data cells: opt-in mono via .num (matches the rest of the theme). */
.table-rak .num,
.table-rak td[data-num] {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Caption + zebra option. */
.table-rak > caption { color: var(--muted); padding: .6rem 1rem; caption-side: bottom; font-size: .85rem; }
.table-rak.table-rak-zebra > tbody > tr:nth-child(even) > td,
.table-rak.table-rak-zebra > tbody > tr:nth-child(even) > th { background: rgba(var(--ivory-rgb), .6); }
@media (max-width: 575.98px) {
  .table-rak { font-size: .88rem; }
  .table-rak > thead > tr > th,
  .table-rak > tbody > tr > td { padding: .6rem .7rem; }
}

/* -----------------------------------------------------------------------------
   10. .pubs — the faculty publication lists (the five college publication pages,
   rendered by includes/faculty-pubs-by-year.php). A search box, academic-year
   chips, a hairline list with one numbered row per paper, a per-page control
   and pagination. Colour through roles only.
   -------------------------------------------------------------------------- */
.pubs { margin-top: var(--space-4); }

/* search box */
.pubs-search { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: stretch; }
.pubs-search .pubs-field { position: relative; flex: 1 1 16rem; }
.pubs-search .pubs-field svg {
  position: absolute; left: .95rem; top: 50%; width: 1.1rem; height: 1.1rem;
  transform: translateY(-50%); color: var(--text-muted); pointer-events: none;
}
.pubs-search .form-control {
  height: 100%; min-height: 3rem; padding: .65rem 1rem .65rem 2.8rem;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); font-size: 1rem;
}
.pubs-search .form-control:focus {
  border-color: var(--primary); box-shadow: 0 0 0 .2rem var(--primary-soft);
}
.pubs-search .btn { border-radius: var(--radius-sm); padding-inline: 1.25rem; }
/* the browser's own clear control is hidden; the page draws one */
.pubs-search input[type="search"]::-webkit-search-cancel-button,
.pubs-search input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; display: none; }
.pubs-search input[type="search"]::-ms-clear { display: none; width: 0; height: 0; }
.pubs-search .pubs-clear {
  position: absolute; right: .35rem; top: 50%; transform: translateY(-50%);
  width: 2.1rem; height: 2.1rem; border: 0; border-radius: var(--radius-pill);
  background: transparent; color: var(--text-muted); font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.pubs-search .pubs-clear:hover { background: var(--surface-tint); color: var(--text); }
.pubs.js .pubs-submit { display: none; }
.pubs.js .pubs-search .form-control { padding-right: 2.75rem; }
.pubs-hint {
  margin: var(--space-2) 0 var(--space-4);
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .72rem; letter-spacing: .06em; color: var(--text-muted);
}

/* academic-year chips */
.pubs-years { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }
.pubs-chip {
  display: inline-flex; align-items: baseline; gap: .35rem;
  padding: .45rem .95rem; border-radius: var(--radius-pill);
  border: 1px solid var(--border); background: var(--surface-tint); color: var(--neutral-800);
  font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: .8rem; font-weight: 600;
  letter-spacing: .02em; line-height: 1.2; cursor: pointer;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.pubs-chip:hover { background: var(--primary-soft); color: var(--primary); border-color: var(--primary-soft); }
.pubs-chip.is-active { background: var(--primary); color: var(--paper); border-color: var(--primary); }

/* the line above the list: what is shown, and the per-page control */
.pubs-status {
  display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center;
  gap: var(--space-2) var(--space-3); padding: .75rem 0; margin-bottom: var(--space-1);
  border-bottom: 2px solid var(--text);
}
.pubs-heading {
  margin: 0; font-family: "Fraunces", Georgia, serif; font-weight: 500;
  font-size: 1.2rem; letter-spacing: -.01em; line-height: 1.3;
}
.pubs-tools { display: flex; align-items: center; gap: var(--space-3); }
.pubs-meta {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted);
}
.pubs-perpage {
  display: inline-flex; align-items: center; gap: .5rem; margin: 0;
  font-size: .85rem; color: var(--text-muted);
}
.pubs-perpage .form-select { width: auto; border-radius: var(--radius-sm); border-color: var(--border); }

/* the list */
.pubs-list { list-style: none; margin: 0; padding: 0; }
.pubs-row {
  display: grid; grid-template-columns: 2.5rem minmax(0, 1fr) auto; column-gap: var(--space-3);
  align-items: start; padding: .95rem .5rem;
  border-bottom: 1px solid var(--border-soft);
  transition: background .15s ease;
}
.pubs-row:hover { background: var(--surface-tint); }
.pubs-num {
  padding-top: .2rem; font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .78rem; font-variant-numeric: tabular-nums; color: var(--text-muted);
}
.pubs-title { margin: 0; font-size: 1.02rem; font-weight: 500; line-height: 1.5; font-family: inherit; }
.pubs-title a { color: var(--text); text-decoration: none; }
.pubs-title a:hover, .pubs-title a:focus-visible {
  color: var(--primary); text-decoration: underline; text-underline-offset: .2em;
}
.pubs-ext { margin-left: .4rem; vertical-align: -.1em; color: var(--text-muted); }
.pubs-title a:hover .pubs-ext { color: var(--primary); }
.pubs-year {
  display: none; padding-top: .3rem; white-space: nowrap;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .72rem; letter-spacing: .08em; color: var(--text-muted);
}
.pubs.is-mixed .pubs-year { display: block; }
.pubs.is-single .pubs-year { display: none; }
.pubs-empty { padding: var(--space-4); margin-top: var(--space-3); }

/* pagination */
.pubs-nav { margin-top: var(--space-3); }
.pubs-pagination { gap: .35rem; }
.pubs-pagination .page-link {
  color: var(--primary); border-color: var(--border); border-radius: var(--radius-sm);
  min-width: 2.6rem; padding: .5rem .85rem; text-align: center; font-weight: 600;
}
.pubs-pagination .page-link:hover { background: var(--primary-soft); border-color: var(--primary-soft); color: var(--primary); }
.pubs-pagination .active .page-link { background: var(--primary); border-color: var(--primary); color: var(--paper); }
.pubs-pagination .disabled .page-link { opacity: .5; }

@media (max-width: 575.98px) {
  .pubs-row { grid-template-columns: 2rem minmax(0, 1fr); padding-inline: .25rem; }
  .pubs-year { grid-column: 2; padding-top: .15rem; }
  .pubs-search .btn { flex: 1 1 auto; }
  .pubs-status { border-bottom-width: 1px; }
}

/* ---------------------------------------------------------------------------
   Official RAKMHSU logo lockup — the full colour logo (seal + Arabic + English
   name, one image) used in the header main bar, the footer, and the mobile menu.
   The name is part of the artwork, so no separate text labels are rendered.
--------------------------------------------------------------------------- */
.brand-lockup .brand-logo {
  display: block;
  height: 60px;
  width: auto;
  max-width: 100%;
}
/* Footer & any dark surface: the seal is detailed artwork, so a flat white
   knockout would blob it — instead present the full-colour logo on a light chip. */
.brand-logo--chip {
  background: var(--paper);
  padding: 9px 14px;
  border-radius: 10px;
  box-shadow: 0 6px 18px rgba(var(--black-rgb), .22);
  height: 68px;  /* 60px art + vertical padding */
}
/* Mobile menu header logo — a touch smaller to fit the offcanvas bar. */
.offcanvas-nav .brand-lockup .brand-logo { height: 49px; }
@media (max-width: 575.98px) {
  /* Compact header on phones so the logo clears the search + hamburger. */
  .site-header .brand-lockup .brand-logo { height: 46px; }
}

/* ===========================================================================
   §12 — HOMEPAGE RESPONSIVE LAYER
   Loads LAST; overrides the inline fixed heights/offsets set in index.php.
   Every rule is scoped to body.home so nothing leaks to inner pages.
   Requires index.php to set $bodyClass = 'home' and to carry the
   .sl-mosaic / .sl-tile / .why-badge / .why-feat-grid / .why-feat / .feat-ico
   hooks (see spec §1; the last three are styled in §17).
   NOTE: the hero / recognition / prestige rules that used to live here moved to
   §15 (HOMEPAGE ONE-VIEWPORT STAGE). What remains below is student-life,
   why-badge, alumni-stat and the ultrawide hero scrim ONLY. Do not re-add
   .page-hero min-height rules here — §15 owns hero height.
   Order: global guard -> phones -> tablets -> laptops -> ultrawide.
   =========================================================================== */

/* Global overflow safety net. clip (not hidden) so the sticky header keeps
   working; the real overflow sources are removed in the blocks below. */
body.home { overflow-x: clip; }

/* ============================ PHONES (<=575.98px) ========================== */
@media (max-width: 575.98px) {

  /* [Student life] The fixed 500px cage turned the col-6 tiles into portrait
     slivers. Release the cage, stack all three tiles full width, and size each
     image box from a sane aspect ratio. */
  body.home .sl-mosaic { height: auto !important; }
  body.home .sl-mosaic > [class*="col-"] { flex: 0 0 100%; max-width: 100%; }
  body.home .sl-mosaic .sl-tile {
    height: auto !important;
    flex: none !important;
    aspect-ratio: 16 / 10;
  }

  /* …and then KEEP THE HEADS. Releasing the cage above fixes the layout but
     inverts the crop: the tiles are authored portrait (the mosaic-tall slot is
     640x1000, 0.64:1) and this box is 16/10, so `cover` scales to width and
     shows only the middle ~40% of the picture's height. On the football tile
     that band is torsos and legs — both players were decapitated, measured at
     366x229 on a 390px screen.
     Biasing to 30% moves the kept band to roughly 10-50% of the frame, which is
     where faces sit in a standing shot. Same reasoning, and the same fix, as the
     gallery album banner's `background-position: center 38%` in §23.2.1 — heads
     are above the middle, so a centred crop is the wrong default for people. */
  body.home .sl-mosaic .sl-tile img { object-position: center 30%; }

  /* PER-PHOTOGRAPH, keyed on the FILE — the same mechanism §4 uses for the
     Admissions banner, and for the same reason: framing is a property of the
     picture, not of the page. 30% is the right default for a standing shot, but
     the student-life tile is a wide hall photograph in which the tug-of-war team
     sits at 46-72% of the frame with ceiling above and empty court below; at 30%
     the phone band lands on the roof trusses. 60% centres it on the players.
     One tile, one selector — no modifier class to copy-paste (hard rule 3). */
  body.home .sl-mosaic .sl-tile img[src*="life-students"] { object-position: center 60%; }

  /* [Why RAKMHSU] Re-anchor the floating stat badge inside its frame so the
     inline right:-10px / bottom:-26px can never breach the viewport edge. */
  body.home .why-badge {
    right: 8px !important;
    bottom: 10px !important;
    max-width: calc(100% - 24px);
    padding: 1.1rem 1.2rem !important;
  }

  /* [Alumni] Stats stay 3-up; shrink padding + number so long values
     ("3,400+") clear the column divider instead of overflowing. Scoped to
     .bg-maroon-rich so the prestige-strip stats are unaffected. */
  body.home .bg-maroon-rich .stat { padding: 0.4rem 0.6rem; }
  body.home .bg-maroon-rich .stat .num { font-size: 1.6rem; }
}

/* ================= LARGE DESKTOPS, 1440p, 4K & ULTRAWIDE (>=1600px) ======== */
/* [Hero scrim] An ultrawide re-aim of the homepage hero's maroon gradient used
   to live here. It is gone with the gradient itself — §15.2 now shows the
   banner photograph clean at every width, so there is nothing left to re-aim. */

/* ===========================================================================
   §13 — SHARED HEADER: SUPPRESS DESKTOP MEGA MENUS ON MOBILE  (site-wide)
   The desktop mega-menu dropdowns (`.mega`, header.php) are hover-triggered off
   `.main-nav`, which is `d-none d-lg-flex` (hidden below 992px). But the `.mega`
   panels are direct children of <header> — NOT inside `.main-nav` — so the
   `<=991px { .main-nav { display:none } }` rule never removes them. Left as
   `position:absolute; visibility:hidden`, each panel is still laid out at the
   full container width with an inner `.row.g-2.gx-5` (-1.5rem gutters) that
   spills ~12px past the right edge, forcing the whole page 12px wider than the
   viewport (the phantom empty gap by the scrollbar on phones/tablets).
   They are non-functional below the nav breakpoint, so drop them from layout
   entirely. Applies to every page, not just the homepage.
   =========================================================================== */
@media (max-width: 991.98px) {
  .site-header .mega { display: none !important; }
}

/* ===========================================================================
   §13a — SHARED HEADER: THE UTILITY STRIP ON PHONES AND TABLETS  (site-wide)
   ---------------------------------------------------------------------------
   The strip carries Alumni, Library and Gallery on the left and Contact Us on
   the right, and is shown at every screen size. The template's own
   `util-desktop` class is NOT used on it (theme.css:450 hides that below
   992px); the markup carries `util-links`, so this is layout, not a fight with
   the vendor rule — hard rule 4 holds.
   The row takes the full width at every size so Contact Us sits against the
   right edge. Below 992px the three left labels tighten their spacing and the
   type steps down so the longest label ("Contact Us") cannot wrap: the four
   labels measure about 190px at .72rem, which still clears a 320px screen.
   Each link fills the 38px band so the whole height is tappable rather than
   just the 11px of text. `--hdr-h` stays 119px — the strip is present at every
   width, so the JS-off fallback does not change with the breakpoint.
   =========================================================================== */
.site-header .utility-bar .util-links a {
  display: inline-flex;
  align-items: center;
  height: 38px;
  white-space: nowrap;
}

@media (max-width: 991.98px) {
  .site-header .utility-bar .util-links-main { gap: .75rem; }
  .site-header .utility-bar .util-links a { font-size: .72rem; }
}

/* The narrowest phones still in use (320px). One more step down, and the label
   spacing tightens rather than the row wrapping to a second line — a wrapped
   strip would double the header's height on the smallest screen there is. */
@media (max-width: 359.98px) {
  .site-header .utility-bar .util-links-main { gap: .4rem; }
  .site-header .utility-bar .util-links a { font-size: .68rem; }
}

/* ===========================================================================
   §14 — SITE-WIDE MOBILE HORIZONTAL-OVERFLOW FIX  (all pages)
   ROOT CAUSE: the `.section` utility applies `padding: <v> 0`, whose shorthand
   ZEROES the horizontal padding Bootstrap's `.container` relies on to absorb a
   `.row`'s negative gutters. On the fluid (mobile) container this makes every
   inner `.row` (g-4/g-5 especially) spill ~12-24px past the right edge — the
   empty gap by the scrollbar reported on the homepage AND present on every
   inner page that uses `<div class="container section">`.
   FIX: restore the container's horizontal padding and clamp the oversized g-5
   gutter so the negative row margins cancel exactly.
   SCOPE: <=991.98px. The padding restore used to stop at 575.98px, which left
   the same bug live in the sm/md band: with a classic (non-overlay) scrollbar a
   768px-wide window still satisfies `min-width:768px`, so Bootstrap switches
   .container to max-width:720px while only ~753px is usable — 720 + 48px of
   negative g-5 gutters = 768 > 753, i.e. 8px of the "Why RAKMHSU" row hangs off
   the right (clipped on the homepage by body.home's overflow-x:clip, a real
   sideways scroll on every inner page using `container section` + `row g-5`).
   Restoring the 12px padding gives 696 + 48 = 744 <= 753. Deliberately NOT
   extended to >=992px: there the container is wide and centred, and adding
   padding would narrow every section on every page by 24px for no bug.
   =========================================================================== */
@media (max-width: 991.98px) {
  .container.section,
  .container.section-sm {
    padding-left: calc(var(--bs-gutter-x, 1.5rem) * .5);
    padding-right: calc(var(--bs-gutter-x, 1.5rem) * .5);
  }
}
@media (max-width: 575.98px) {
  /* g-5 gutter (3rem -> -1.5rem margins) exceeds the .75rem container padding;
     clamp it so it cancels instead of overflowing. */
  .row.g-5 { --bs-gutter-x: 1.5rem; }
}

/* Belt-and-suspenders at EVERY width, not just phones. One case survives the
   padding restore above and cannot be fixed by padding at all: at a window
   width of exactly 992-1007px WITH a classic scrollbar, the media query sees
   >=992 and Bootstrap sets .container to 960px while only ~977px is usable, so
   a `.row.g-5`'s -24px margins put its box edge at 993px (measured). What spills
   is gutter, never column content (the column's own 24px padding keeps text at
   969px), so clipping it is lossless — and `clip`, unlike `hidden`, does not
   create a scroll container, so the sticky header keeps working. */
body { overflow-x: clip; }

/* ===========================================================================
   §15 — HOMEPAGE ONE-VIEWPORT STAGE
   ---------------------------------------------------------------------------
   ROOT CAUSE: the homepage opened with three independently-sized sections — a
   hard `min-height:660px` hero, a py-3 recognition band and a `padding:3rem 0
   2rem` stats strip — stacked under a 118px sticky header. Their SUM
   (660 + ~78 + ~150 + 118 = ~1006px) exceeded every laptop viewport, so the
   six prestige stats — the university's strongest proof points — always landed
   below the fold, and on phones with browser chrome the recognition band did
   too. Fixed viewport units made it worse: `100vh` on mobile is the LARGE
   viewport (chrome retracted), so anything sized to it is pushed off-screen by
   the URL bar; `100dvh` resizes mid-scroll and makes the band jitter.
   ---------------------------------------------------------------------------
   FIX: wrap all three in `.home-stage`, a flex column whose min-height is
   `100svh - <measured header> - <peek>`. `svh` is the SMALL viewport (chrome
   shown) — the only unit that is both stable and never optimistic. The hero is
   the sole flexible row (`flex:1 1 auto` + a legibility floor); the bar and the
   stats take their natural height (`flex:0 0 auto`). The stage is therefore
   self-balancing: if #last-viewed reveals itself, or a label wraps, the hero
   simply gives up that many pixels. `--hdr-h` is measured at runtime from
   #siteHeader (see the IIFE in index.php) because theme.css:546 shrinks the
   main bar 80px -> 68px while `.scrolled`; a 118px CSS fallback keeps the
   layout correct with JS off. Because it is min-height (never height), a
   viewport too short for the content overflows and scrolls — it can never clip
   — and §15.6 releases the floor outright below 560px tall.
   Order: tokens -> stage -> hero -> recognition bar -> stats -> breakpoints
   (phones -> tablets -> laptops -> ultrawide) -> escape hatches.
   Every rule is scoped to body.home and carries no !important, with ONE
   documented exception: §15.0's `.site-header.is-measuring *` guard, which is
   deliberately unscoped (it must reach the shared header) and deliberately
   !important (it exists to defeat theme.css:545's transition for one frame).
   §12's conflicting hero / badge-pill / prestige rules were REMOVED, not
   overridden.
   =========================================================================== */

/* --------------------------------------------------------------------------
   §15.0 — Runtime header height + measurement guard
   119px = the 38px utility strip + the 80px main bar + the header's 1px bottom
   rule (includes/header.php). This is only the JS-off fallback; index.php
   measures #siteHeader at runtime, so the value follows the strip if it is ever
   hidden or restored. The home page's opening screen is measured against this
   figure, so the two move together. Below 992px the strip's links are hidden
   (theme.css) and the measured header shrinks to match.
   -------------------------------------------------------------------------- */
:root { --hdr-h: 119px; }

/* JS adds .is-measuring for one frame so theme.css:545's
   `transition: min-height .3s ease` cannot hand back an animating value. */
.site-header.is-measuring,
.site-header.is-measuring * { transition: none !important; }

/* --------------------------------------------------------------------------
   §15.1 — The stage
   -------------------------------------------------------------------------- */
body.home {
  --stage-peek: 0px;    /* the stage ends flush with the screen edge; nothing of #colleges shows in the first view */
  --hero-floor: 320px;  /* hero never collapses below this */
}

body.home .home-stage {
  display: flex;
  flex-direction: column;
  /* 1st declaration = fallback for engines without svh (they ignore the 2nd). */
  min-height: calc(100vh  - var(--hdr-h) - var(--stage-peek));
  min-height: calc(100svh - var(--hdr-h) - var(--stage-peek));
}

/* Hero = the ONLY flexible row. It absorbs and donates every spare pixel.
   No `min-height:0` here: the hero has no scrollable descendant that needs the
   flex min-size override, and we deliberately WANT a floor instead. */
body.home .home-stage__hero { flex: 1 1 auto; min-height: var(--hero-floor); }

/* Bar + stats = natural height, never squeezed. */
body.home .recognition-bar,
body.home .home-stage__stats { flex: 0 0 auto; }

/* Fold cue: the next section starts just under the stage. */
body.home #colleges { padding-top: clamp(2.75rem, 5vh, 6rem); }

/* --------------------------------------------------------------------------
   §15.2 — Hero
   The inline min-height:660px and the inline container padding are GONE from
   index.php; all hero metrics live here. Type is clamped on BOTH vw and vh so
   a wide-but-short window shrinks the headline instead of eating the stats.

   CLEAN BANNER (President's office, 28 Jul 2026): the homepage photograph shows
   with NO treatment over it — no brand scrim, no gold vignette, no text
   shadow. Three separate layers had to go, and all three are killed HERE rather
   than in theme.css, because every other page's breadcrumb hero still needs
   them (small type over busier crops):
     1. `.overlay-grad`      — the 90deg brand wash. The <div> is simply not in
                               index.php any more; the rule below is the belt to
                               that braces, so a future re-add cannot re-tint it.
     2. `.page-hero::after`  — theme.css:185's gold radial in the top-right 40%.
     3. text-shadow          — none is set today, but `text-shadow: none` is
                               declared so the ban is explicit and survives any
                               later "legibility over bright photos" rule of the
                               kind §2 already applies to breadcrumb heroes.
   TRADE-OFF, stated plainly: white type now sits directly on the photograph, so
   its legibility is a property of the ART, not of the CSS. hero-campus.* is a
   dark, evenly-lit interior and clears WCAG AA comfortably; ANY future swap to
   a bright or high-detail crop must be re-checked by eye before it ships.

   FOURTH LAYER, added with the palette re-tone: `.page-hero`'s own BACKGROUND.
   theme.css:183 fills it with var(--maroon-rich) — the brand colour. The
   photograph is `position:absolute; inset:0; object-fit:cover`, so that fill is
   only ever seen in the load state (and if the image 404s), but in that moment
   the "clean banner" instruction is broken: the visitor gets a flat panel of
   brand colour where a photograph belongs. Re-pointed to var(--ink), a neutral
   near-black, so the load state reads as an unlit photo well instead of a brand
   plate. It carries the same LUMINANCE the scrim used to guarantee, so hero text
   stays legible for the frame before the image paints.
   This supersedes the earlier "neutral scrim" approach (which kept a
   luminance-only rgba(var(--ink-rgb), …) gradient over the photo): the
   President's office instruction removed the scrim outright, which is strictly
   stronger, so only the neutral FILL is carried over. Do not re-add a scrim here
   without a new instruction — see §12's note, where the ultrawide `.overlay-grad`
   re-aim was likewise deleted rather than re-toned, so no media query further
   down this file can hand a tinted scrim back at any width.
   -------------------------------------------------------------------------- */
body.home .page-hero { background: var(--ink); }
body.home .home-stage__hero .overlay-grad { display: none; }
body.home .home-stage__hero::after { content: none; }
body.home .home-stage__hero .hero-copy,
body.home .home-stage__hero .hero-copy * { text-shadow: none; }

body.home .home-stage__hero > .container.hero-copy {
  padding-top: clamp(1.25rem, 3vh, 3rem);
  padding-bottom: clamp(1.5rem, 4.5vh, 3.5rem);
}
body.home .hero-copy > div { max-width: 62rem; }

/* First letter of every word capitalised, by request (2026-07-30) — matching the
   site-wide breadcrumb H1 rule. Done in CSS, NOT by editing the sentence: the
   headline wording is approved by the President's office and carries a
   "do not reword" note in index.php, so the source string stays byte-identical
   and the <title>, OG card and JSON-LD keep their own sentence case. */
body.home .hero-h1 {
  font-size: clamp(1.75rem, min(4.2vw, 5.4vh), 3.2rem);
  line-height: 1.08;
  letter-spacing: -.012em;
  text-transform: capitalize;
  margin: 0;
}
/* Gap lives here, not on a Bootstrap `.gap-3` utility in the markup — otherwise
   the phone override below would need !important to beat the utility. */
body.home .hero-cta {
  gap: 1rem;
  margin-top: clamp(.9rem, 2.4vh, 1.75rem);
}
body.home .hero-cta .btn-lg {
  padding: .7rem 1.5rem;
  font-size: clamp(.92rem, .86rem + .2vw, 1.05rem);
}

/* --------------------------------------------------------------------------
   §15.3 — Recognition bar (rebuilt from the legacy top bar,
   C:/xampp8.2/htdocs/website/header_section.php:19-64 + its css:43-191).
   Charcoal -> maroon-rich sweep with gold hairlines.

   ⛔ REBUILT AS A GRID, 5 Aug 2026, at the University's instruction: "full
   container width … and on the mobile screen make it multiple lines, not
   scrollable." Everything below follows from those two sentences.

   WAS: a single `flex`/`nowrap` rail with `overflow-x: auto`, a fading edge
   mask, `scroll-snap`, `tabindex="0"` on the <ul> so a keyboard could scroll it,
   and auto margins centring the group. That shape existed because SIX badges
   could not fit one row — it overflowed at every width below ~1400px and simply
   scrolled. It also meant the badges sat in a centred clump with dead space
   either side of it, and on a phone three of the four were off-screen until the
   visitor guessed the bar swipes.

   NOW: `display: grid` with a fixed column count per breakpoint, so the badges
   are DISTRIBUTED across the full container instead of clustered, and a width
   too narrow for N columns drops to fewer columns and MORE ROWS rather than
   scrolling. 4 columns >=992px · 2 columns 576-991px · 1 column <=575px.
   Consequences, all deliberate:
     · No `overflow-x`, no mask, no scroll-snap — there is nothing to scroll.
     · `tabindex="0"` is GONE from the <ul> in index.php. It existed only to make
       a scrolling region keyboard-reachable; a non-scrollable, non-interactive
       list that takes focus is a WCAG 2.4.3 nuisance (a tab stop that does
       nothing). `aria-label` stays — it names the list.
     · The per-breakpoint `max-width` clamps on the subline (11rem / 7rem /
       5.6rem) are gone with the overflow they were fighting: a grid cell is a
       hard boundary, so the subline simply wraps inside its own column. It is
       still never `display:none` — it carries the awarding body and the year.
   `.rb-inner` no longer needs `flex-wrap`: #last-viewed left this bar on
   28 Jul 2026 (it is a fixed popup now, §15.5) and the rail is the only child.
   -------------------------------------------------------------------------- */
body.home .recognition-bar {
  background: linear-gradient(90deg, var(--charcoal) 0%, var(--neutral-850) 52%, var(--maroon-rich) 100%);
  border-top: 1px solid rgba(var(--gold-rgb), .38);
  border-bottom: 1px solid rgba(var(--gold-rgb), .22);
  color: var(--peach-100);
}
body.home .rb-inner {
  display: flex;
  align-items: center;
  padding-top: .5rem;
  padding-bottom: .5rem;
  min-height: 60px;
}

/* Gold focus ring for the bar. The site-wide brand-primary ring (§2) measures
   1.26-1.43:1 against this gradient — a WCAG 2.2 SC 1.4.11 failure; --gold-bright
   measures 8.15:1 over --maroon-rich, 7.78:1 over the --neutral-850 mid stop and
   8.84:1 over --charcoal. Re-measured with tools/color-contrast.php after the
   palette re-tone: the primary ring went from 1.97-2.38:1 to 1.26-1.43:1, i.e.
   it fails HARDER now, so this gold override is more necessary than before, not
   less. The gold figures moved only where the underlying stop moved.
   The badges themselves are no longer links (28 Jul 2026, at the University's
   instruction — the bar states recognition, it does not navigate) and the grid
   rebuild removed the rail's own `tabindex="0"`, so as of 5 Aug 2026 there is
   nothing focusable in this bar at all. The rule is KEPT, for two reasons: the
   `.rb-badges:focus-visible` half still fires if a future edit re-adds the
   tabindex, and the `a:focus-visible` half is what stops the site-wide maroon
   ring coming back the day a badge is linked again. */
body.home .recognition-bar a:focus-visible,
body.home .rb-badges:focus-visible {
  outline: 2px solid var(--gold-bright);
  outline-offset: 2px;
}

/* ---- patronage lockup ----
   REMOVED 28 Jul 2026 with the markup it styled (index.php): the bar now
   carries rankings, ratings and accreditation only. `.rb-patronage`,
   `.rb-patronage__ico`, `.rb-patronage__txt`, `.rb-eyebrow`, `.rb-name`,
   `.rb-name__role` and `.rb-ext` appear in no template any more — verified by
   grep over *.php / *.html / *.js — so their rules are deleted rather than left
   as dead weight. The patronage CONTENT is untouched and still runs in
   includes/footer.php, about.php, why-rakmhsu.php and
   supreme-council-ruler-message.php. */

/* ---- badge grid ----
   `flex: 1 1 100%` makes the <ul> take the WHOLE container row (it is the only
   child of the flex `.rb-inner`), and `repeat(4, minmax(0, 1fr))` divides that
   width into four equal columns. `minmax(0, 1fr)` rather than plain `1fr`: the
   default `1fr` is `minmax(auto, 1fr)`, whose auto floor is the item's
   min-content width — and `.rb-badge__t` is `white-space: nowrap`, so a plain
   `1fr` would let a long title push its column wider than its share and blow
   the grid past the container. With a 0 floor the columns stay equal and the
   content wraps inside them.
   The four columns mirror the stats band directly beneath this bar (an even
   N-up row of items divided by gold hairlines), which is what makes the two
   read as one unit rather than as two unrelated strips.
   Column count per breakpoint: 4 (>=992px) · 2 (576-991px) · 1 (<=575px).
   NO `overflow`, NO mask, NO scroll-snap: this grid re-flows, it never scrolls. */
body.home .rb-badges {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: center;
  gap: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 100%;
  min-width: 0;
}
/* `justify-content: flex-start`, NOT `center` — measured both. Centring each
   badge inside its column leaves the four marks on ragged left edges (81 / 384 /
   700 / 920 at a 1280px viewport) because the four badges are very different
   widths; starting them puts the marks on the column rhythm exactly (81 / 361 /
   640 / 919, i.e. one 279px step each), which is how every other equal-column
   block on this site behaves — the footer link columns, the college cards.
   `min-width: 0` lets the badge shrink inside its column (without it a flex item
   refuses to go below min-content and overflows). */
body.home .rb-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  min-width: 0;
  padding: .1rem clamp(.5rem, 1.1vw, 1.15rem);
  border-left: 1px solid rgba(var(--gold-rgb), .30);   /* hairline gold separators */
}
/* Seam rule for the 4-up grid: one row, so only the first cell loses its rule.
   The 2-up and 1-up grids re-cut these seams in §15.5/§15.6 — a `border-left`
   on the first cell of a ROW is an orphan hairline hanging off the left edge.
   `padding-left: 0` with it, so the first mark starts on the container's own
   left edge — flush with the hero headline above and the section eyebrows below,
   which is the alignment the rest of the page is built on. */
body.home .rb-item:first-child { border-left: 0; padding-left: 0; }

/* Selector is `.rb-badge`, NOT `a.rb-badge`: the badges are <span> now (28 Jul
   2026 — the bar states recognition, it does not navigate). The hover tint went
   with the links; a background that lights up under the cursor on something
   that cannot be clicked is a false affordance. `text-decoration`/`color:
   inherit` are kept because they cost nothing and make the rule correct again
   the day a badge is re-linked. */
body.home .rb-badge {
  display: flex;
  align-items: center;
  gap: .5rem;
  text-decoration: none;
  color: inherit;
  border-radius: 8px;
  padding: .2rem .25rem;
  /* Both needed for the grid: `max-width` stops a badge painting past its own
     column, `min-width: 0` lets its text child actually shrink to fit one. */
  max-width: 100%;
  min-width: 0;
}
body.home .rb-badge__ico { width: 18px; height: 18px; flex: 0 0 auto; color: var(--gold-bright); }
body.home .rb-badge__ico--trail { width: 15px; height: 15px; opacity: .8; }
/* `width: max-content` (5 Aug 2026): without it the column takes every pixel its
   `max-width` allows, even when the sub-line wraps to two short lines — so the
   MOHESR badge's trailing shield sat ~60px clear of the text it belongs to, and
   the centring of the whole rail was thrown off by that phantom width.
   max-content shrinks the box to its longest actual line.
   `max-width: 100%` (not the old fixed 15rem) is the grid rebuild: the GRID
   COLUMN is the boundary now, so the text block may use its whole share of the
   container at wide widths and wraps inside it at narrow ones — no breakpoint
   needs to guess a rem value. */
body.home .rb-badge__txt {
  display: flex;
  flex-direction: column;
  min-width: 0;
  width: max-content;
  max-width: 100%;
}
body.home .rb-badge__t {
  display: flex;
  align-items: center;
  gap: .35rem;
  font-family: "Hanken Grotesk", sans-serif;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--gold-bright);
  line-height: 1.1;
  white-space: nowrap;
}
/* NO `max-width` (5 Aug 2026): the grid column is the only boundary the subline
   needs. Every fixed cap this rule has carried — 15rem here, 11rem/7rem/5.6rem in
   the breakpoints — existed to stop a six-badge flex line overflowing, and each
   one forced the wording to wrap early and made the bar taller than it had to be:
   a 22ch measure tried here cost the bar 16px of height at 1904px wide (85px
   against 69px) for wrapping that the 324px column does not require. Now the
   awarding body and year run on ONE line wherever the column can hold them and
   wrap only where it cannot. */
body.home .rb-badge__s {
  font-size: .63rem;
  line-height: 1.25;
  color: var(--peach-500);
  margin-top: 2px;
}
body.home .rb-stars { display: inline-flex; gap: 1px; }
body.home .rb-stars svg { width: 10px; height: 10px; color: var(--gold-bright); }

/* ---- ranking FIGURES (.rb-fig) — added 5 Aug 2026 with the badge merge ----
   The bar used to carry four separate ranking badges, two of which stated a bare
   "Ranked 13" / "Ranked 5" with no awarding body. The University attributed both
   (see the note in index.php), so each rank was folded into the badge for the
   body that awarded it — and the Arab Ranking badge now has to state TWO figures
   (56th Arab region, 5th UAE) on one line without a second text row.
   That is what these rules do, and the constraint they respect is HEIGHT: this
   bar shares the first viewport with the hero (§15.1), so the redesign gives the
   numerals presence through SIZE and FAMILY, never through an extra line. Both
   figures still sit inside .rb-badge__t, which is already a nowrap flex row.
     .rb-fig__n  display serif, ~1.5x the eyebrow — reads as a rank, not a word.
     .rb-fig__l  the scope, in the eyebrow's own size/tracking so the pair reads
                 as one unit; baseline-aligned to the numeral.
     separator   a gold hairline between figures of the SAME badge — a dot would
                 be mistaken for the uppercase middot used elsewhere in the bar.
   Screen readers get the word "Ranked" from a .visually-hidden span in the
   markup, because the visual design conveys it typographically. */
body.home .rb-badge__t:has(.rb-fig) { align-items: baseline; gap: .5rem; }
body.home .rb-fig {
  display: inline-flex;
  align-items: baseline;
  gap: .3rem;
  white-space: nowrap;
}
body.home .rb-fig + .rb-fig {
  padding-left: .5rem;
  border-left: 1px solid rgba(var(--gold-rgb), .38);
}
body.home .rb-fig__n {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.12rem;
  font-weight: 700;
  letter-spacing: -.01em;
  text-transform: none;
  line-height: 1;
  color: var(--gold-bright);
}
body.home .rb-fig__l {
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--peach-100);
}

/* "THE" tile — Times Higher Education ships no logo file anywhere in either
   local tree, and hard rule 1 bars fetching one, so the badge wears a lettered
   tile instead of a faked logo. Ported from the legacy top bar
   (website/css/header_section.css:122-131) and retoned from legacy crimson to
   the rebuild's tokens. Same call includes/accreditation-badges.php:22 makes. */
body.home .rb-the {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  min-width: 34px;
  height: 26px;
  padding: 0 .3rem;
  border-radius: 4px;
  background: var(--gold-bright);
  color: var(--maroon-rich);
  font-family: "Hanken Grotesk", sans-serif;
  font-weight: 800;
  font-size: .78rem;
  letter-spacing: .04em;
  line-height: 1;
}

/* White chip — the MOHESR raster has white baked in, so the chip and the art
   must merge seamlessly (same radius, a few px of inline padding). */
body.home .rb-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--paper);
  border-radius: 7px;
  padding: 3px 6px;
  flex: 0 0 auto;
  box-shadow: 0 1px 6px rgba(var(--black-rgb), .28);
}
body.home .rb-chip img { display: block; height: 28px; width: auto; }

/* --plain: no white plate. The Arab Ranking emblem is a true cut-out and the QS
   square carries its own solid orange field, so both read cleanly straight on
   the bar; a white plate behind either would just add a floating rectangle. */
body.home .rb-chip--plain {
  background: none;
  box-shadow: none;
  padding: 0;
  border-radius: 4px;
  overflow: hidden;
}

/* The seal item is gone with the patronage lockup: the file that shipped as
   "rakmhsu-quality-seal.png" is in fact the Arab Ranking for Universities
   emblem, and it now sits on the Arab Ranking badge where it belongs (see the
   note in index.php), so `.rb-seal` / `.rb-item--seal` style nothing any more
   and are deleted rather than left as dead weight. */

/* --------------------------------------------------------------------------
   §15.4 — Stats strip
   theme.css:217-221 sizes .stat at 2.6rem/1.4rem padding — far too tall for a
   shared viewport. Stepped down to a clamp bounded on vh as well as vw so the
   band shrinks on short screens instead of pushing itself off the fold.
   REVEAL: theme.css:494 hides .stat until site.js:384 adds `.in`. site.js's
   scan() runs synchronously at ready() and these stats are above the fold, so
   they normally reveal instantly — but the observer uses a 0.92*vh threshold,
   and the stage puts the band's top at ~0.88-0.93*vh, i.e. right on the
   boundary. Rather than gamble, exempt the STAGE stats from the hide rule
   (specificity 0,5,2 vs theme's 0,3,1 — wins cleanly, no !important). Every
   other .stat on the page (alumni band etc.) keeps its scroll reveal, and
   site.js still harmlessly adds `.in` to these.
   -------------------------------------------------------------------------- */
html.reveal-ready:not(.reveal-all) body.home .home-stage__stats .stat {
  opacity: 1;
  transform: none;
  transition: none;
  will-change: auto;
}

body.home .home-stage__stats > .container {
  padding-top: clamp(.7rem, 1.7vh, 1.2rem);
  padding-bottom: clamp(.7rem, 1.7vh, 1.2rem);
}
body.home .home-stage__stats .row.g-0 { row-gap: clamp(.5rem, 1.4vh, .9rem); }
body.home .home-stage__stats .stat {
  padding: .1rem clamp(.35rem, .9vw, 1.15rem);
  text-align: center;
}
body.home .home-stage__stats .stat .num {
  font-size: clamp(1.3rem, min(2.5vw, 3.4vh), 2.1rem);
  line-height: 1;
}
body.home .home-stage__stats .stat .lbl {
  font-size: clamp(.6rem, .53rem + .2vw, .8rem);
  line-height: 1.25;
  margin-top: .3rem;
  color: var(--peach-400);
}

/* --------------------------------------------------------------------------
   §15.5 — PHONES (<=575.98px)
   -------------------------------------------------------------------------- */
@media (max-width: 575.98px) {
  body.home { --stage-peek: 0px; --hero-floor: 250px; }

  /* The clamp floor of 1.75rem wrapped the headline to 5 lines (151px) at
     320-390px wide, which alone blew the phone height budget. 1.55rem/1.05
     brings it back to 3 lines (~86px) and keeps it comfortably above the 16px
     body minimum. */
  body.home .hero-h1 { font-size: clamp(1.5rem, 7.2vw, 1.85rem); line-height: 1.05; }
  body.home .hero-cta { gap: .6rem; }
  body.home .hero-cta .btn-lg { padding: .6rem 1.15rem; font-size: .92rem; }

  /* Phone height budget: trim the bar's own padding rather than any of its
     content. Nothing here is hidden. */
  body.home .rb-inner { padding-top: .4rem; padding-bottom: .4rem; min-height: 0; }
  body.home .rb-badge__t { font-size: .67rem; }
  body.home .rb-badge__s { font-size: .58rem; }
  body.home .rb-chip img { height: 22px; }
  body.home .rb-the { min-width: 30px; height: 22px; font-size: .7rem; }

  /* ⛔ PHONES GET CREDENTIAL TILES, NOT A LIST (5 Aug 2026, third pass — the
     University asked for centred alignment on small screens and then for a proper
     design rather than the row-per-badge list the first two passes produced).
     Each badge becomes a self-contained 2 x 2 tile: mark centred on top, the
     ranking figure beneath it, the awarding body and year last, everything
     centre-aligned. Why this and not the list:
       · CENTRING FINALLY WORKS. Centring a mark-then-text ROW leaves the four marks
         on four different left edges (58 / 62 / 47 / 52px measured at 500px) because
         the badges are different widths. Stack them and every tile is the SAME
         width, so centred content is symmetrical by construction — the marks sit on
         one axis down each column and the text centres under each.
       · IT READS AS A CREDENTIAL, NOT A TABLE ROW. Four thin full-width rows
         separated by hairlines is a data table; a tile with a surface, a border and
         its figure given room is what a badge is. Same content, no scrolling, still
         "multiple lines".
       · IT IS SHORTER. Two rows of tiles instead of four rows of text.
     The surface is `rgba(var(--paper-rgb), .05)` over the bar's own gradient with a
     `rgba(var(--gold-rgb), .22)` hairline — the same treatment as the tiles in the
     footer and the section cards, so it is the site's card, not a new one. Tokens
     only; no raw colour. */
  /* Height is the one cost of tiles over the old four-row list (223px against
     198px at 500px wide) and this bar shares the first screen with the hero, so
     every value below is trimmed to the tightest that still reads as a card: the
     alternative — mark INLINE with the figure instead of above it — was built and
     measured at 165px, 58px cheaper, and rejected on looks: the Arab Ranking tile's
     two figures stack off-centre beside the mark and each tile ends up visibly
     unbalanced. Tiles at 205px is the better trade. */
  body.home .rb-badges {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .4rem;
  }
  /* `.rb-badges > .rb-item` (not `.rb-item`) so the full `border` shorthand beats
     the base `:first-child { border-left: 0 }` — equal specificity, later in the
     file. Without it tile 1 would be missing its left edge. */
  body.home .rb-badges > .rb-item {
    flex-direction: column;
    justify-content: center;
    align-items: center;
    text-align: center;
    gap: .26rem;
    padding: .5rem .4rem;
    border: 1px solid rgba(var(--gold-rgb), .22);
    border-radius: 12px;
    background: rgba(var(--paper-rgb), .05);
  }
  /* The badge and its text block turn into centred columns too. `.rb-badge__txt`
     must drop `width: max-content` — inside a centred column that leaves the text
     block narrower than the tile and the wrapping goes ragged; 100% lets the
     subline use the tile's full measure and centre on it. */
  body.home .rb-badge {
    flex-direction: column;
    align-items: center;
    gap: .24rem;
    width: 100%;
  }
  body.home .rb-badge__txt { align-items: center; width: 100%; }
  body.home .rb-badge__s { line-height: 1.18; margin-top: 1px; }
  /* The mark sizing from §15.6 is a horizontal `flex: 0 0 40px`; in a COLUMN
     direction that same declaration would set the mark's HEIGHT to 40px and stretch
     the artwork. Reset it, then give all four marks ONE footprint: the artworks are
     natively very different shapes — the ministry crest is tall and narrow (96x120),
     the Arab Ranking emblem is wide (188x172), the QS square is square, the "THE"
     tile is lettering — so at their natural widths the four tiles carry four
     differently-sized objects and the grid looks uneven. A `min-width` + fixed
     height box makes each mark occupy the same space with the art centred inside it,
     which is what makes four tiles read as one set. Nothing is scaled or cropped:
     the box is larger than the widest art at this size (24px inside 34px of content
     box), so this only adds symmetrical air. */
  body.home .rb-badge > .rb-the,
  body.home .rb-badge > .rb-chip {
    flex: 0 0 auto;
    min-width: 44px;
    height: 28px;
  }
  body.home .rb-badge > .rb-the { font-size: .72rem; }
  /* The figure row must be allowed to WRAP here: it is `nowrap` everywhere else, and
     the Arab Ranking badge's two figures measure ~168px against a ~150px tile, so
     nowrap would push them straight through the tile's border. Wrapped, the two
     figures stack and centre. The hairline that separates two figures side by side
     is dropped with it — once they can wrap it would hang off the start of the
     second line; the row gap separates them instead. `.rb-fig` keeps its own
     `nowrap`, so a figure is never split from its label. */
  body.home .rb-badge__t {
    flex-wrap: wrap;
    justify-content: center;
    white-space: normal;
    row-gap: .05rem;
    column-gap: .4rem;
  }
  body.home .rb-fig + .rb-fig { border-left: 0; padding-left: 0; }
  /* The MOHESR tick trails the text on one line; in a centred tile it would sit
     alone under the wording as an unexplained mark. The badge it decorates already
     says "MOHESR Accredited". */
  body.home .rb-badge__ico--trail { display: none; }

  body.home .home-stage__stats .stat { padding: .1rem .3rem; }
  body.home .home-stage__stats .stat .lbl { font-size: .6rem; }
}

/* --------------------------------------------------------------------------
   §15.5b — SMALLEST PHONES (<=359.98px): the credential tiles, one step tighter.
   At 320px a tile is only ~140px wide, so the awarding-body lines wrap to three or
   four and the bar measured 258px — a fifth of the screen more than at 390px. One
   step down in type and padding brings it back without dropping a word, without
   going back to a scroller, and without a third column count to reason about.
   Placed AFTER §15.5 so it wins the ties (identical selectors, same specificity).
   -------------------------------------------------------------------------- */
@media (max-width: 359.98px) {
  body.home .rb-badges { gap: .32rem; }
  body.home .rb-badges > .rb-item { padding: .42rem .3rem; border-radius: 10px; }
  body.home .rb-badge__t { font-size: .63rem; }
  body.home .rb-badge__s { font-size: .55rem; }
  body.home .rb-fig__n   { font-size: .95rem; }
  body.home .rb-fig__l   { font-size: .58rem; letter-spacing: .04em; }
  body.home .rb-badge > .rb-the,
  body.home .rb-badge > .rb-chip { min-width: 40px; height: 26px; }
  body.home .rb-chip img { height: 20px; }
}

/* --------------------------------------------------------------------------
   §15.6 — BELOW THE NAV BREAKPOINT (<=991.98px): TWO COLUMNS, TWO ROWS
   ⛔ 5 Aug 2026, at the University's instruction ("on the mobile screen make it
   multiple lines, not scrollable"). WAS a snap-scrolling one-line rail with edge
   fades and `tabindex="0"`; three of the four badges sat off-screen on a phone
   until the visitor guessed the bar swipes, and a horizontal scroller inside a
   vertically scrolling page is the interaction people miss most often.
   NOW 2 x 2 on tablets, 1-up on phones (§15.5). Nothing is hidden at any width —
   that was true of the scroller and stays true here; the difference is that all
   four badges are now VISIBLE without a gesture.

   ⚠ THE MEDIA QUERY IS `576-991.98px`, NOT `<=991.98px`, AND THAT IS LOAD-BEARING.
   §15.5 (phones) sits EARLIER in this file, so on a 390px screen both blocks
   would match and the later one would win every tie at equal specificity — the
   2-column `grid-template-columns` here would silently beat the 1-column rule
   §15.5 sets, and the phone would get a 2-up grid the University did not ask for.
   Bounding this block from below makes the two ranges disjoint, so neither can
   overwrite the other. The stats-band seam rule genuinely applies to phones too
   and therefore keeps its own `<=991.98px` block below.
   The whole scrolling apparatus is deleted rather than left dormant: no
   `overflow-x`, no `mask-image`, no `scroll-snap-type`, no `scroll-snap-align`,
   no `-webkit-overflow-scrolling`. `.rb-inner` also drops back to a plain
   centred flex row: it was `flex-direction: column` only to stack the rail under
   a sibling that no longer exists (#last-viewed left this bar on 28 Jul 2026),
   and the `flex-wrap: nowrap` reset that came with it was guarding against a
   column-direction overflow that the grid cannot produce.
   -------------------------------------------------------------------------- */
@media (min-width: 576px) and (max-width: 991.98px) {
  body.home .rb-inner {
    padding-top: .55rem;
    padding-bottom: .55rem;
  }
  body.home .rb-badges {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: .1rem;
  }
  /* Re-cut the seams for a 2-column grid: the vertical hairline belongs BETWEEN
     the two columns, so it comes off every odd cell (1 and 3) — on cell 3 it
     would otherwise hang off the left edge as an orphan — and a horizontal
     hairline separates the two rows, drawn on the cells of row 2 only. */
  body.home .rb-item:nth-child(odd) { border-left: 0; padding-left: 0; }
  body.home .rb-item:nth-child(n+3) { border-top: 1px solid rgba(var(--gold-rgb), .18); }
  body.home .rb-item { padding-top: .4rem; padding-bottom: .4rem; }

  /* ONE MARK COLUMN for the 2-up mark-beside-text shape. The four marks are
     naturally different widths — the gold "THE" tile, the Arab Ranking emblem, the
     QS square, the ministry crest — so without this the wording beside them starts
     at four different x positions (58 / 62 / 47 / 52 measured at 500px) and the two
     rows read ragged. A fixed 40px basis gives every badge the same mark column, so
     the titles in both rows start on one line. Each mark is already
     `justify-content: center` inside itself, so nothing is stretched or cropped —
     the widest art at these sizes is 24px inside a 28px content box.
     Scoped to 576-991px ON PURPOSE, twice over: desktop lays the badges out in four
     hairline-separated columns where a padded plate column would just add air, and
     PHONES stack the mark ON TOP of the text (§15.5), where `flex: 0 0 40px` would
     be read in the column direction and set the mark's HEIGHT to 40px. This rule
     used to sit in a `<=991.98px` block placed after §15.5, which — same selector,
     same specificity, later in the file — beat §15.5's reset and stretched the
     artwork on phones. */
  body.home .rb-badge > .rb-the,
  body.home .rb-badge > .rb-chip { flex: 0 0 40px; }
}

/* 576-767px — large phones in landscape and the smallest tablets: the 2-up grid's
   half-container is only ~260-350px, the tightest a badge ever gets. MEASURED at
   600px before this block: the Arab Ranking badge's two figures are inside a
   `white-space: nowrap` row, so they could not wrap and pushed 2px past their own
   column (no visible break — the bar does not scroll and the page does not
   overflow — but 2px of a figure sitting on a hairline is still a defect). The
   same one-step type reduction the narrow-desktop range uses fixes it and takes
   16px off the bar's height here (145px -> 129px), which the hero gets back. */
@media (min-width: 576px) and (max-width: 767.98px) {
  body.home .rb-badge__t  { font-size: .66rem; letter-spacing: .05em; }
  body.home .rb-badge__s  { font-size: .58rem; line-height: 1.2; }
  body.home .rb-fig__n    { font-size: 1.02rem; }
  body.home .rb-fig__l    { font-size: .62rem; letter-spacing: .05em; }
  body.home .rb-badge    { gap: .38rem; }
  body.home .rb-chip img  { height: 24px; }
  body.home .rb-the       { min-width: 30px; height: 23px; font-size: .72rem; }
}

/* 3-up stats grid (phones AND tablets): kill the orphan divider on items 1 and 4.
   Its own block, because it is the one rule in §15.6 that must also reach phones
   — see the media-query warning above. */
@media (max-width: 991.98px) {
  body.home .home-stage__stats [class*="col-"]:nth-child(3n+1) .stat { border-left: 0; }
}

/* --------------------------------------------------------------------------
   §15.7 — LAPTOPS / DESKTOPS (>=992px): single row of six, one orphan seam
   -------------------------------------------------------------------------- */
@media (min-width: 992px) {
  body.home .home-stage__stats [class*="col-"]:first-child .stat { border-left: 0; }
}

/* Desktop 4-up grid (>=992px). The `max-width` clamps that used to live here
   (11rem, then 7rem, then 5.6rem) are GONE with the grid rebuild of 5 Aug 2026:
   they existed to keep six badges on one un-scrolled flex line, and a grid column
   is a hard boundary that does that job by construction. What remains is
   type SCALE — a 4-up grid at 992px gives each badge ~230px, so the marks and
   the wording still have to step down to sit comfortably inside one.
   (The subline is never hidden at any width: it carries the awarding body and the
   year, and `display:none` would drop that from the accessibility tree as well.) */
@media (min-width: 992px) {
  body.home .rb-item { padding-left: .55rem; padding-right: .55rem; }
}

/* 992-1399px — the type steps down so nothing has to overflow its column. */
@media (min-width: 992px) and (max-width: 1399.98px) {
  body.home .rb-badge__s  { font-size: .58rem; line-height: 1.2; }
  body.home .rb-badge__t  { font-size: .66rem; letter-spacing: .05em; }
  body.home .rb-fig__n    { font-size: 1.02rem; }
  body.home .rb-fig__l    { font-size: .62rem; letter-spacing: .05em; }
  body.home .rb-item      { padding-left: .38rem; padding-right: .38rem; }
  body.home .rb-badge    { gap: .38rem; padding: .2rem .15rem; }
  body.home .rb-chip img  { height: 24px; }
  body.home .rb-chip      { padding: 2px 4px; }
  body.home .rb-the       { min-width: 30px; height: 23px; font-size: .72rem; }
  body.home .rb-badge__ico { width: 16px; height: 16px; }
  body.home .rb-stars svg { width: 9px; height: 9px; }
}

/* Narrow laptops (992-1199px): the tightest width the 4-up badge grid and the
   6-up stats band ever share — each badge column is ~230px here. Historical note
   worth keeping: this block once RE-WIDENED `.rb-badge__s` to 9rem, at the same
   specificity as the block above but later in the file, so it silently undid that
   block's clamp and left 86px of rail overflow with MOHESR off the right edge.
   Both clamps are now gone (the grid replaced them) and the ONLY thing this block
   does is tighten the scale further, so the trap cannot recur. */
@media (min-width: 992px) and (max-width: 1199.98px) {
  body.home .rb-badge__s   { font-size: .55rem; line-height: 1.18; }
  body.home .rb-badge__t   { font-size: .62rem; letter-spacing: .03em; }
  body.home .rb-fig__n     { font-size: .95rem; }
  body.home .rb-fig__l     { font-size: .58rem; letter-spacing: .04em; }
  body.home .rb-item       { padding-left: .28rem; padding-right: .28rem; }
  body.home .rb-badge     { gap: .3rem; padding: .2rem .1rem; }
  body.home .rb-chip img   { height: 21px; }
  body.home .rb-the        { min-width: 27px; height: 21px; font-size: .66rem; }
  body.home .rb-badge__ico { width: 14px; height: 14px; }
  body.home .rb-badge__ico--trail { display: none; }  /* decorative tick only */
  body.home .home-stage__stats .stat { padding: .1rem .55rem; }
}

/* --------------------------------------------------------------------------
   §15.8 — ULTRAWIDE / TALL PANELS (>=1600px wide, or >=1100px tall)
   -------------------------------------------------------------------------- */
@media (min-width: 1600px) {
  body.home .rb-inner { min-height: 66px; }
  body.home .rb-chip img { height: 32px; }
  body.home .rb-the { min-width: 38px; height: 29px; font-size: .84rem; }
}
/* On very tall panels a full-viewport hero becomes a wall of image. Cap the
   stage (still never taller than the viewport) so the stats stay comfortable. */
@media (min-height: 1100px) {
  body.home .home-stage {
    min-height: min(calc(100vh  - var(--hdr-h) - var(--stage-peek)), 1040px);
    min-height: min(calc(100svh - var(--hdr-h) - var(--stage-peek)), 1040px);
  }
}

/* --------------------------------------------------------------------------
   §15.9 — ESCAPE HATCHES (mandatory)
   Because the stage uses min-height (never height) and every child sizes from
   content, an under-tall viewport already overflows and scrolls rather than
   clipping. These rules make that graceful instead of merely survivable.
   -------------------------------------------------------------------------- */

/* (a) Genuinely short viewports — landscape phones, tiny windows, split-screen.
       Release the one-viewport contract entirely and just let the page scroll. */
@media (max-height: 560px) {
  body.home .home-stage { min-height: 0; }
  body.home .home-stage__hero { min-height: 215px; }
  body.home .home-stage__hero > .container.hero-copy {
    padding-top: 1rem;
    padding-bottom: 1.1rem;
  }
  body.home .hero-h1 { font-size: clamp(1.5rem, 3.2vw, 2.1rem); }
  body.home .rb-inner { min-height: 0; padding-top: .4rem; padding-bottom: .4rem; }
  /* Trim the band's own padding (never a stat, never a label) so a 553px-tall
     phone still lands all six numbers above the fold on a first visit. */
  body.home .home-stage__stats > .container { padding-top: .45rem; padding-bottom: .45rem; }
}

/* (b) Short AND narrow (e.g. 320x568 with chrome): drop the floor further so
       the CTAs are never pushed under the bar. The stage KEEPS its min-height
       here (it used to be released): with the trims below, 320x568 measures
       hero 220 + bar 59 + stats 116 = 395 against 443 available, so the hero can
       absorb the slack instead of ending the stage 54px above the fold. */
@media (max-width: 575.98px) and (max-height: 640px) {
  body.home { --hero-floor: 210px; }
  /* Padding-only trim (never a stat, never a label): at 320x568 the stats'
     labels wrap to two lines, so the band is 116px and the 18px this layout
     used to run over the fold has to come out of whitespace, not content. */
  body.home .home-stage__hero > .container.hero-copy {
    padding-top: 1rem;
    padding-bottom: 1.1rem;
  }
  body.home .home-stage__stats > .container { padding-top: .45rem; padding-bottom: .45rem; }
}

/* (c) Reduced motion: theme.css:527 already neutralises transforms; make sure
       nothing in the stage animates height. */
@media (prefers-reduced-motion: reduce) {
  body.home .home-stage,
  body.home .home-stage * { transition: none; }
}

/* (d) Print: never force a viewport-tall block onto paper. */
@media print {
  body.home .home-stage { min-height: 0; display: block; }
  /* The overflow/mask/flex-wrap resets this rule used to carry are unnecessary
     since the 5 Aug 2026 grid rebuild — there is no scroller and no mask to undo.
     Print gets two columns so four badges fit a page column without hairline-thin
     text. */
  body.home .rb-badges { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ===========================================================================
   §16 — HEADER ICONS (site-wide)
   ---------------------------------------------------------------------------
   The legacy header carried two glyphs the rebuild had dropped: a magnifier on
   the search field (website/header_section.php:84) and a graduation cap on the
   Apply Now button (:93, and :318 in the mobile menu). The rebuild rendered the
   magnifier as a bare CSS circle — a hollow ring that reads as a decorative dot,
   not as "search" — and the Apply buttons as text alone.
   They are back as INLINE SVG, not Font Awesome: the legacy site pulled a whole
   6.x icon font off cdnjs for two glyphs, which is both a third-party asset
   (hard rule 1) and a render-blocking request for ~30KB we would use twice.
   Both icons are aria-hidden — the search button has its own aria-label and the
   Apply buttons have visible text, so announcing the art would only add noise.
   The magnifier now sits on the main bar beside Apply Now, where it opens the
   search panel; §40 styles that button and the panel it opens.
   =========================================================================== */

.btn-apply-pin .apply-ico,
.btn-maroon .apply-ico {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  margin-right: .45rem;
  vertical-align: -.16em;
}
/* The pin carries Bootstrap's `.d-none .d-sm-inline-block`, whose utilities are
   themselves !important — so flipping it to a flex row to seat the icon needs
   !important to win. Scoped to the exact utility pair so it can only ever apply
   where that markup is, and it never touches the <576px hidden state. This is
   the one !important in §16 and it exists solely to beat a vendor utility. */
@media (min-width: 576px) {
  .btn-apply-pin.d-sm-inline-block { display: inline-flex !important; align-items: center; }
}
.offcanvas-nav .btn-maroon { display: inline-flex; align-items: center; justify-content: center; }

/* ---------------------------------------------------------------------------
   §16a — ONE "APPLY NOW" HOVER, EVERYWHERE
   ---------------------------------------------------------------------------
   The Apply Now button in the homepage banner is a .btn-maroon, so it inherits
   theme.css's full hover: the indigo deepens to --maroon-deep, the button lifts
   1px, and a light sheen sweeps left to right across it. The header pin is NOT
   a .btn-maroon — it is its own component — so it only changed colour and sat
   still. Every other Apply Now on the site is a .btn-maroon or a .btn-gold and
   already behaves correctly; this block brings the pin in line, using the same
   timings and the same sheen gradient so the two are indistinguishable.

   No border is declared here on purpose: .btn-maroon reserves 1.5px for one,
   and adding it to the pin would make the header 3px taller on every page.
   ------------------------------------------------------------------------ */
.btn-apply-pin {
  position: relative;
  overflow: hidden;
  transition: transform .18s ease, box-shadow .18s ease, background .18s ease, color .18s ease;
}
.btn-apply-pin::after {
  content: "";
  position: absolute;
  top: 0;
  left: -120%;
  width: 60%;
  height: 100%;
  background: linear-gradient(100deg, transparent, rgba(var(--paper-rgb), .38), transparent);
  transform: skewX(-18deg);
  transition: left .6s ease;
  pointer-events: none;
}
.btn-apply-pin:hover,
.btn-apply-pin:focus-visible {
  background: var(--maroon-deep);
  color: var(--paper);
  transform: translateY(-1px);
}
.btn-apply-pin:hover::after,
.btn-apply-pin:focus-visible::after { left: 130%; }

/* A visitor who has asked the operating system to reduce motion keeps the
   colour change and loses the lift and the sweep. */
@media (prefers-reduced-motion: reduce) {
  .btn-apply-pin { transition: background .18s ease, color .18s ease; }
  .btn-apply-pin::after { display: none; }
  .btn-apply-pin:hover,
  .btn-apply-pin:focus-visible { transform: none; }
}

/* ---------------------------------------------------------------------------
   §16b — MOBILE DRAWER SPLIT ROWS  (includes/header.php · #mobileNav)
   ---------------------------------------------------------------------------
   Each section row is a LINK to the section landing page + a separate chevron
   button that opens the child list. Before this, the whole row was one collapse
   button and /admissions, /student-life and /excellence/ could not be reached
   from a phone at all — the desktop nav-link went there, the drawer did not.
   Both halves are >=48px tall so each is its own comfortable tap target, and the
   1px divider between them is the visual cue that they do different things.
   theme.css §.offcanvas-nav .accordion-button is left untouched (hard rule 4);
   these are new hooks that exist only here.
   ------------------------------------------------------------------------ */
.offcanvas-nav .oc-row { display: flex; align-items: stretch; gap: 0; }
.offcanvas-nav .oc-row-link {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: .7rem .25rem .7rem 0;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.offcanvas-nav .oc-row-link:hover,
.offcanvas-nav .oc-row-link:focus-visible { color: var(--maroon); }
.offcanvas-nav .oc-row-link.active { color: var(--maroon); }
.offcanvas-nav .oc-row-toggle {
  flex: 0 0 auto;
  width: 48px;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: 0;
  border-left: 1px solid var(--line);
  color: var(--muted);
  cursor: pointer;
}
.offcanvas-nav .oc-row-toggle:hover,
.offcanvas-nav .oc-row-toggle:focus-visible { color: var(--maroon); }
.offcanvas-nav .oc-row-toggle[aria-expanded="true"] { color: var(--maroon); }
.offcanvas-nav .oc-chev {
  width: .5rem;
  height: .5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  margin-top: -3px;
}
.offcanvas-nav .oc-row-toggle[aria-expanded="true"] .oc-chev { transform: rotate(-135deg); margin-top: 3px; }
@media (prefers-reduced-motion: no-preference) {
  .offcanvas-nav .oc-chev { transition: transform .2s ease; }
}

/* ---------------------------------------------------------------------------
   §16c — MOBILE DRAWER QUICK LINKS  (includes/header.php · #mobileNav .oc-quick)
   ---------------------------------------------------------------------------
   Contact Us, Alumni, Gallery and Library, repeated from the utility strip at
   the top of the page. The strip keeps them one tap away at every screen size,
   but on a phone it is 38px of small type at the very top, and it scrolls away;
   the drawer is where a thumb goes looking for navigation, so they are here as
   well, below the sections and above nothing — the last thing in the drawer, so
   the sections a visitor came for are not pushed down the screen.
   Two columns of 48px targets: four full-width rows would add ~200px to a
   drawer that is already a scrolling list on a small phone.
   ------------------------------------------------------------------------ */
.offcanvas-nav .oc-quick { margin-top: 1.25rem; }
.offcanvas-nav .oc-quick-label {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .66rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .5rem;
}
.offcanvas-nav .oc-quick-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .5rem;
}
.offcanvas-nav .oc-quick-grid a {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: .6rem .85rem;
  border: 1px solid var(--line);
  border-radius: .5rem;
  font-size: .9rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.offcanvas-nav .oc-quick-grid a:hover,
.offcanvas-nav .oc-quick-grid a:focus-visible {
  color: var(--maroon);
  border-color: var(--maroon);
}

/* ===========================================================================
   §17 — HOMEPAGE "WHY RAKMHSU" FEATURE GRID  (body.home #why — homepage only)
   ---------------------------------------------------------------------------
   The block grew from 6 generic maroon squares (theme.css `.feature-icon .dot`)
   to 9 items, each carrying its own inline SVG — same convention as §16 and the
   header icons: stroke="currentColor", no icon font, aria-hidden.
   These three hooks (.why-feat-grid / .why-feat / .feat-ico) exist ONLY here:
   index.php has dropped the per-item inline `border-bottom` and the `py-3`
   utility, and moved the row wrapper's inline `border-top` into this layer, so
   nothing below is optional — without it the ledger loses every rule, loses its
   row rhythm, and (worst) each bare <svg> renders at the 300x150 CSS default
   for a replaced element and bursts the 38px .feature-icon chip.
   Every selector is double-scoped to `body.home #why`: .feature-icon is shared
   site-wide (admissions, contact, fees, the four college pages, opac-library,
   student-well-being-center) and nothing here may reach those instances.
   theme.css is NOT touched — the chip (38px, white, 1px border, centred) is
   reused exactly as shipped and `.feature-icon .dot` is simply left unused on
   this page. The 18px glyph is a deliberate homepage-only divergence from the
   shipped 12px dot: it keeps roughly the dot's optical weight inside the chip
   while the line art stays legible, pending a site-wide icon-set rollout.
   Items run 9 x `.col-12 .col-md-6` — `.col-12` matters, because below 768px
   Bootstrap declares nothing for `.col-md-6` and each item would size to
   max-content, so the new hairlines would render at a different width on every
   row instead of the one continuous rule this section is built to produce.
   =========================================================================== */
body.home #why .why-feat-grid { border-top: 1px solid var(--line); }

/* Row rhythm. The padding lives here rather than in a .py-* utility because
   Bootstrap spacing utilities are !important and could not then be tuned. */
body.home #why .why-feat {
  height: 100%;                 /* pairs the two items of a row so their bottom
                                   hairlines align into one continuous rule */
  padding: .85rem 0;
  border-bottom: 1px solid var(--line);
}

/* Close the ledger with no rule under its final row. The grid holds an EVEN 10
   items, so at md+ that final row is the LAST TWO columns — suppressing only
   :last-child would leave a half-length hairline hanging under the left cell.
   Below 768px the items are 1-up, so only the true last item is the final row.
   Matched on the column, not a bare :last-child, so stray text/comment nodes or
   a future non-column sibling can never claim it. */
body.home #why .why-feat-grid > [class*="col-"]:last-child .why-feat { border-bottom: 0; }

@media (min-width: 768px) {
  body.home #why .why-feat-grid > [class*="col-"]:nth-last-child(-n+2) .why-feat { border-bottom: 0; }
}

/* The icon. theme.css sizes `.feature-icon .dot` at 12px; an <svg> child needs
   its own box or it takes the 300x150 default intrinsic size of a replaced
   element and bursts the 38px chip. */
body.home #why .feature-icon .feat-ico {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  display: block;               /* removes the inline-baseline gap */
  color: var(--maroon);         /* the SVGs stroke with currentColor */
}

/* Long single words (partner/program names) must wrap rather than push the text
   block past the flex row and break the hairline alignment. */
body.home #why .why-feat > div:last-child { min-width: 0; }

/* Laptops and up: let the photo track the ledger instead of being pinned.
   At 992-1199px the nine rows plus the heading run ~630px against the inline
   height:500px on the media box, and `align-items-center` then leaves a band of
   dead ivory above and below the photo at exactly the width where the section
   is narrowest. index.php now carries `align-items-lg-stretch` on the outer row
   so the column stretches; the media box itself is matched structurally rather
   than by a new class because that column's markup is preserved verbatim, and
   its inline `height:500px` needs !important the same way §12 overrides the
   other inline heights index.php sets. `min-height` is the safety net: if the
   percentage ever fails to resolve, the box falls back to today's 500px instead
   of collapsing (its <img> is absolutely positioned, so it carries no height of
   its own). The img is already object-fit:cover, so it crops cleanly at any
   height, and .why-badge stays anchored to the bottom of the frame. */
@media (min-width: 992px) {
  body.home #why .col-lg-5 > .position-relative { height: 100%; }
  body.home #why .col-lg-5 > .position-relative > div:first-child {
    height: 100% !important;
    min-height: 500px;
  }
}

/* ===========================================================================
   §18 — INSTAGRAM FEED EMBED  (includes/instagram-feed.php — homepage today)
   ---------------------------------------------------------------------------
   The feed is an Elfsight embed: platform.js builds its own DOM and injects its
   own <style>. NOTHING here touches those internals — no .eapps-* selector, no
   vendor override (hard rule 4). This layer owns only the box the widget drops
   into and the first-party card that occupies it beforehand.

   Hooked on CLASSES (.ig-embed / .ig-fallback), not on #home-instagram, because
   the markup lives in a shared include and a second feed (alumni, news) must
   inherit this styling without a copy — the id stays a landmark only.

   THREE STATES, all driven by classes the include's loader sets. No :has(), no
   wall-clock timer, no vendor class-name guess:
     (default)     first-party card, in normal flow, sized by its own content.
                   Third-party JS has NOT been requested — this is what an
                   undecided/declined visitor sees (consent gate), and what
                   everyone sees if platform.js is blocked, CSP-refused or down.
     .is-reserved  we have committed to loading: the box takes the widget's
                   height UP FRONT so the feed paints into space that already
                   exists. Rendered server-side when consent is already granted
                   (so the reservation is in the FIRST paint — zero CLS), or
                   added by the loader on the click / consent grant that starts
                   the load, i.e. within 500ms of a discrete input, which is the
                   case CLS excludes. There is deliberately NO "release the
                   reservation once the widget appears" rule: collapsing the box
                   under a rendered widget would guarantee a shift in both
                   directions and throw away the only shift-free outcome.
                   NOTE: 480/560/700 are sized to the container, not measured.
                   Re-measure the rendered widget at 360/768/1280 with the live
                   app id and correct them here — over-reserving costs a band of
                   charcoal, under-reserving costs a measured CLS hit.
     .is-loaded    the vendor painted (the app div gained a child) — the card is
                   removed and the widget owns the box.
   `.is-loading` swaps the card's copy for a polite status line while the bundle
   is in flight.

   The card carries NO theme component class (.card-soft/.card-dark) on purpose:
   those sit in theme.css's reveal net (opacity:0 until the observer adds .in),
   and an element that may never be observed must not depend on it. It is a
   white panel on the ivory section so it reads as a deliberate card rather than
   an empty band, and it needs no new colour values — --ivory/--line/--muted
   (theme.css:18-21), --radius-md and --space-3/4 (brand.css §8) already exist.

   WHY THE SECTION IS LIGHT: the vendor renders white post cards with black body
   text and ships its own <style>. We do not re-skin that (hard rule 4), so the
   band around it has to be light or the cards read as pasted-on rectangles.
   Ivory, not plain white, because the section below (Student life) is plain
   white; the hairline below keeps this band distinct from the ivory #why
   section above, which is the only other ivory neighbour.
   =========================================================================== */
.ig-section { border-top: 1px solid var(--line); }

/* Heading column: keep the intro to a readable measure and let the follow
   button sit on its own line on narrow screens rather than crushing the text. */
.ig-intro { max-width: 620px; }
.ig-follow { display: inline-flex; align-items: center; gap: .55rem; white-space: nowrap; }
.ig-follow .ig-glyph { width: 18px; height: 18px; flex: 0 0 auto; }

.ig-embed { position: relative; }               /* containing block for the card */

/* Height reservation — applied only once a load is committed (see above).
   MEASURED, not estimated: the rendered widget was read off the live page with
   the real app id at five widths — 413px @500, 411px @768, 417px @992,
   485px @1280, 530px @1440. It is a single-row carousel, so height tracks card
   width, which is why it steps only at the wide breakpoints. Values below carry
   a ~10px margin over each measurement; `min-height` cannot clip, so a post with
   an unusually long caption simply grows the box.
   The earlier 480/560/700 were unmeasured guesses and over-reserved badly — at
   1440px that left ~170px of empty ivory under the feed, which read as a broken
   gap rather than as breathing room. Re-measure here if the widget's layout,
   post count or caption setting is changed in the Elfsight dashboard. */
.ig-embed.is-reserved { min-height: 420px; }
@media (min-width: 1200px) { .ig-embed.is-reserved { min-height: 490px; } }
@media (min-width: 1400px) { .ig-embed.is-reserved { min-height: 540px; } }

/* The first-party card. In normal flow by default (so it can never be clipped
   by a reserve smaller than its content); it fills the reserved box once one
   exists, so the widget replaces it with ZERO reflow. */
.ig-fallback {
  display: grid;
  place-content: center;
  justify-items: center;        /* stops the .btn stretching to the text width */
  gap: var(--space-3);
  padding: var(--space-4);
  min-height: 220px;
  text-align: center;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: var(--muted);
}
.ig-embed.is-reserved .ig-fallback { position: absolute; inset: 0; }
.ig-fallback p { margin: 0; max-width: 46ch; }  /* gap owns the rhythm */
.ig-fallback__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
}

/* Copy -> status swap while the vendor bundle loads. The status line is a live
   region, so it stays display:none until it actually applies. */
.ig-fallback__status { display: none; }
.ig-embed.is-loading .ig-fallback__msg { display: none; }
.ig-embed.is-loading .ig-fallback__status { display: block; }

/* Vendor painted — the card has done its job. */
.ig-embed.is-loaded .ig-fallback { display: none; }

/* Print: an interactive third-party feed prints as nothing. Drop the
   reservation AND the card so neither pushes a blank band into the page. */
@media print {
  .ig-embed.is-reserved { min-height: 0; }
  .ig-fallback { display: none; }
}
/* ===========================================================================
   §19 — NEW BRAND IDENTITY: two semantic pointers theme.css never picked up
   ---------------------------------------------------------------------------
   The approved identity (docs/reports/brand-new-identity-recommended.md §4)
   repurposed `maroon-ink` as the DANGER text colour — tokens.css:293 wires
   `--danger-text: var(--maroon-ink)`, and the value moved #5A2A2C -> #7A1D0C,
   a warm brown-red. It created two replacement roles for the non-danger uses
   and pointed both at `maroon-rich` (#231B60):
       --text-on-primary-soft     (tokens.css:206)
       --surface-placeholder-dark (tokens.css:227)
   Those two roles were added but their two consumers in theme.css were never
   repointed, so both still read the raw `--maroon-ink`. The result after the
   flip is warm brown-red sitting on cool indigo:

     theme.css:356  .alert-rak       background var(--maroon-100) #E0E2F6 (indigo)
                                     + color   var(--maroon-ink)  #7A1D0C (brown)
     theme.css:265  .img-ph.on-dark  background var(--maroon-ink)  #7A1D0C (brown)

   `.alert-rak` is the BRAND callout, not a danger alert — its `.success`,
   `.gold` and `.info` variants each already override the colour, so only the
   base (brand) case is wrong. Corrected here rather than in theme.css because
   brand.css is the last-loaded override layer (hard rule 4).

   Not a contrast fix — both states pass. #7A1D0C on #E0E2F6 is 8.154:1; the
   correction below is 11.723:1. This is about the identity being one palette.
   =========================================================================== */

.alert-rak { color: var(--text-on-primary-soft); }
.img-ph.on-dark { background-color: var(--surface-placeholder-dark); }

/* ===========================================================================
   §20 — QUICK ACCESS  (includes/quick-access.php — every section landing:
   the Sustainability & Excellence Hub, About, Research, Sustainability and
   the four college landings)
   ---------------------------------------------------------------------------
   ONE QUICK ACCESS, THE SAME ON EVERY PAGE — AND NO EMPTY SPACE
   The directory is generated by ONE include (quick_access() from the section
   rail data, quick_access_cards() from a page's own list), so this section is
   the only place its look is decided. The layout is CSS multi-column
   (`columns: 4 250px`), the pattern the Excellence Hub front page established:
   a card is exactly as tall as its own list, the next card starts directly
   beneath it, and a group of 2 links sits beside a group of 14 without a grid
   row stretching every card to the tallest — which is what the earlier
   Bootstrap `.row` of `h-100` cards did (a two-link card floating above a large
   empty panel, and four cards of unequal length leaving a ragged, half-empty
   row). Column count follows the container width — four on a desktop, two on
   a tablet, one on a phone — with no media query to keep in step.

   NO NEW COLOURS, NO NEW COMPONENT LANGUAGE. `.card-soft` (theme.css:227) and
   `.link-arrow` (theme.css:123) are reused exactly as shipped and only
   re-spaced; every colour is a token (--accent / --ink / --line / --shadow-1 /
   --text-muted / --print-text). All four college accents resolve to the single
   university accent --primary — never introduce another hue. theme.css is not
   touched (hard rule 4).

   REVEAL SAFETY: every card keeps `.card-soft`, so it sits in the reveal net
   (theme.css:467-481 hides `.card-soft` until site.js adds `.in`, with the
   `.reveal-all` failsafe in head.php). Do not remove `.card-soft` from the card.

   OVERFLOW SAFETY: `columns` never makes a column narrower than the container;
   below 250px + gap it simply drops to one column, so nothing here can push
   past `body { overflow-x: clip }` (§14).
   =========================================================================== */

/* --- §20.1 Section head ---------------------------------------------------
   The same clamp() every landing's h2 carried inline; it belongs to the
   component, so it lives here and the markup stays attribute-clean (CSP-safe). */
.quick-access .qa-title {
  font-size: clamp(1.7rem, 3vw, 2.3rem);
  margin: 0 0 var(--space-4);
}

/* --- §20.2 Shortcut chips ------------------------------------------------
   A heading-less group of <= QA_CHIP_MAX links from the rail is lifted out of
   the card flow and drawn here, under the section heading, as the theme's
   `.chip` pill (theme.css:209) — the same object student-life.php's own Quick
   Access bar uses. Only the two things Bootstrap/theme leave undone for a chip
   that is a LINK are added: the underline is removed and an interactive state
   is given. 44px minimum height, so a chip is a comfortable touch target on
   every screen (WCAG 2.2 AA 2.5.8 asks 24px). */
.quick-access .qa-chips { margin: 0 0 var(--space-4); }
.quick-access .qa-chip {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  min-height: 2.75rem;
  text-decoration: none;
  transition: color .2s ease, border-color .2s ease, background-color .2s ease;
}
.quick-access a.qa-chip:hover,
.quick-access a.qa-chip:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.quick-access a.qa-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* An item with no destination yet: the pill still carries the wording — it must
   remain readable — but is plainly not a control. No `aria-disabled`: a <span> is
   role `generic`, which supports no ARIA states. */
.quick-access .qa-chip--muted {
  color: var(--text-muted);
  border-style: dashed;
  cursor: default;
}

/* --- §20.3 The column flow -----------------------------------------------
   `columns: 4 250px` = at most four columns, none narrower than 250px; the
   browser balances the cards across them. `column-gap` is the horizontal gap
   and each card's bottom margin the vertical one — both 1rem, so the directory
   reads as one even mosaic rather than a grid with holes. */
.quick-access .qa-grid {
  columns: 4 250px;
  column-gap: 1rem;
}

/* --- §20.4 Card surface --------------------------------------------------
   Compact: 1rem padding (the theme's `.p-3`) and a 1rem bottom margin.
   `break-inside: avoid` keeps a card whole, so a list never splits across two
   columns. The card stays a plain BLOCK — measured on the Research landing:
   as `display: inline-block` Chromium's column balancing stacked four cards
   into two columns and left the other two empty; as a block it uses all four.
   The theme lifts `.card-soft` 4px on hover. This card is a LIST of links, not
   a link, so a whole-card lift is a false affordance — emphasise with depth
   only. Scoped to this component; every other card on the site keeps its lift. */
.quick-access .qa-card {
  display: block;
  break-inside: avoid;
  padding: 1rem;
  margin: 0 0 1rem;
  height: auto;
}
.quick-access .qa-card:hover {
  transform: none;
  box-shadow: var(--shadow-1);
}

/* --- §20.5 Group heading -------------------------------------------------
   A real <h3> in the display serif at 1.1rem — a card title, not a label — so
   the section reads h1 > h2 > h3 and a screen-reader user can jump between
   groups. The font family comes from the `.serif` class in the markup. A card
   that IS a link (the colleges' "Faculty & Staff") carries `.link-arrow` inside
   the heading and inherits this size; its one-line description sits beneath. */
.quick-access .qa-h {
  font-size: 1.1rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  margin: 0 0 .5rem;
}
.quick-access .qa-h .qa-card-link { font-size: inherit; }
.quick-access .qa-desc {
  font-size: .86rem;
  line-height: 1.5;
}

/* --- §20.6 The link rows -------------------------------------------------
   `.link-arrow` (theme.css:123) supplies the brand colour, the weight and the
   sliding "→"; only the size and rhythm are set here: .86rem rows a quarter-rem
   apart, the density of the Excellence Hub directory. `align-items: baseline`
   + `text-align: left` keep the arrow next to the FIRST line of a label that
   wraps, instead of vertically centred against a two-line block. */
.quick-access .qa-list { gap: .25rem; }
.quick-access .qa-list .qa-link {
  align-items: baseline;
  font-size: .86rem;
  line-height: 1.45;
  text-align: left;
}
.quick-access .qa-list .qa-link::after { flex: 0 0 auto; }

/* External affordance. The PHP adds `.no-arrow` (see `.link-arrow.no-arrow::after`
   in §5) to external rows, so the "↗" replaces the "→" and a row never shows two
   glyphs. The glyph is aria-hidden and paired with a visually-hidden
   "(opens in a new tab)". */
.quick-access .qa-ext { flex: 0 0 auto; font-size: .85em; color: var(--accent); }

/* An item with no destination yet: same rhythm as a link row, plainly not
   clickable. Never display:none — the wording must still be read. No
   `aria-disabled` in the markup either: ARIA states are not allowed on a generic
   role, and the muted ink with no arrow is the whole signal. */
.quick-access .qa-list .qa-muted {
  display: block;
  font-size: .86rem;
  line-height: 1.45;
  cursor: default;
}

/* Phones/tablets: every row is a 36px touch target — WCAG 2.2 AA (2.5.8) asks
   for 24px — while a 14-link card still fits one screen. 44px rows made the
   phone directory twice the height of the same list on a desktop. */
@media (max-width: 767.98px) {
  .quick-access .qa-list .qa-link,
  .quick-access .qa-list .qa-muted { min-height: 2.25rem; padding: .25rem 0; }
}

/* --- §20.7 Focus ---------------------------------------------------------
   Additive to §2 / theme.css:583 (`a:focus-visible, .btn:focus-visible, …`); only
   shapes the ring to the row it surrounds. */
.quick-access .qa-list a:focus-visible,
.quick-access .qa-h a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

/* --- §20.8 Motion / print ------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .quick-access .qa-card { transition: none; }
}
@media print {
  .quick-access .qa-grid { columns: 2; }
  .quick-access .qa-card { box-shadow: none; }
  .quick-access .qa-list .qa-link,
  .quick-access .qa-list .qa-muted { min-height: 0; padding: .1rem 0; color: var(--print-text); }
  .quick-access .qa-list .qa-link::after { content: none; }
}

/* ===========================================================================
   §21 — VERIFY CERTIFICATE  (body.page-verify — certificate-verify.php)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   certificate-verify.php was rebuilt full-width on 2026-07-30 (the shared
   "About RAKMHSU" section rail was removed) and promoted to the top utility
   bar. The old two-column markup carried its type scale and every prose colour
   as inline style="" attributes — the only rule-4 smell on that page. Those
   declarations now live here, token-only and scoped to the page's body class,
   so the page body ships with ZERO inline styles.
   This section restyles NO shared component: .card-soft (theme.css:227),
   .eyebrow (51), .btn-maroon (117), .bg-ivory (75), .border-line (447) and
   .maxw-720 / .lh-relaxed (443/446) are used exactly as they ship.
   Colour comes only from semantic roles: --text-secondary and --text-body are
   tokens.css:183-184, i.e. the exact values (--neutral-700 / --neutral-750)
   the page previously hard-coded inline.
   =========================================================================== */
body.page-verify .cv-h2     { font-size: clamp(1.7rem, 3.2vw, 2.3rem); }
body.page-verify .cv-lede   { font-size: 1.1rem; color: var(--text-secondary); }
body.page-verify .cv-prose  { color: var(--text-body); }
body.page-verify .cv-card-h { font-size: 1.4rem; }

/* --- §21.1 Registrar contact rows -----------------------------------------
   Email / phone read as two data rows rather than bullets — the hairline idiom
   already used by .list-rak (theme.css:384) and the contact.php fact rows. They
   stay a real <ul>/<li> pair, so the content count is unchanged and the list
   relationship survives in the accessibility tree. */
body.page-verify .cv-contact                 { list-style: none; padding-left: 0; margin-bottom: 0; }
body.page-verify .cv-contact > li            { padding: .7rem 0; border-bottom: 1px solid var(--line-soft); }
body.page-verify .cv-contact > li:last-child { border-bottom: 0; }

/* --- §21.2 Print ----------------------------------------------------------
   A printed verification page must still say where to go, so the two external
   portal URLs are exposed next to their button labels. */
@media print {
  body.page-verify .cv-portal[href^="http"]::after { content: " (" attr(href) ")"; font-size: .85em; font-weight: 400; }
}

/* ===========================================================================
   §22 — SUSTAINABILITY & EXCELLENCE HUB: the "page under construction" state
   (includes/sustainability-hub-render.php — 51 pages under /excellence/)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The placeholder carries the badge and the "being built" line and NOTHING else
   (2026-07-30, on instruction). Rebuilt the same day as a proper empty state
   with a DISPLAY-SIZE header, because the first attempt — a small bordered card
   holding one small heading — read as an error box rather than as a page that is
   coming. An empty state has to look intentional, so this one is built like a
   real section of the site instead: a full-width ivory band with hairlines top
   and bottom (the house quiet-band idiom, as in §21), generous vertical rhythm,
   and one centred column carrying mark -> badge -> big serif statement -> the
   single way out. Nothing here competes for attention with a real page.

   FRIENDLY, IN PRACTICE
     - the statement is the biggest thing on the screen, so the answer to "why is
       this page empty?" is legible at a glance and from across a room;
     - a warm ivory ground and soft radial lift replace a cold outlined box;
     - the mark is a page whose LAST RULE IS DASHED — partly written, which is the
       truth, rather than a padlock or a warning triangle, which are not;
     - there is exactly one action, at .btn-lg size, so the visitor is never left
       wondering where to go next;
     - nothing moves, blinks or counts down: no invented completion date and no
       fake progress bar, both of which would be a promise the site cannot keep.

   These rules previously lived in a page-local <style id="sus-leaf-styles"> block
   inside the renderer, which is exactly what hard rule 4 exists to prevent; they
   are moved here and that block is deleted. Token-only — the ground, the mark and
   the badge take their colour from --ivory / --maroon-rgb / --gold-rgb, so the
   whole state follows the identity automatically.
   Restyles no shared component: .serif, .btn-maroon, .btn-lg and .container are
   used exactly as they ship.
   =========================================================================== */
.suc-band {
  position: relative;
  overflow: hidden;
  padding: clamp(3.5rem, 9vw, 7rem) 0;
  background: var(--ivory);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
/* A single soft lift behind the composition so the band has a centre of gravity
   without a second border or a drop shadow. Kept under 6% alpha — at this size
   anything stronger becomes a visible disc. */
.suc-band::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(60% 55% at 50% 32%, rgba(var(--maroon-rgb), .055), transparent 70%);
  pointer-events: none;
}

.suc-empty {
  position: relative;   /* above ::before */
  max-width: 44rem;
  margin-inline: auto;
  text-align: center;
}

/* The mark: a hairline disc on the paper colour with a wide, very faint ring, so
   it reads as an inset plate on the ivory rather than as a coloured chip. */
.suc-empty .suc-mark {
  width: 104px;
  height: 104px;
  margin: 0 auto clamp(1.5rem, 4vw, 2.25rem);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--paper);
  border: 1px solid var(--line-soft);
  box-shadow: 0 0 0 10px rgba(var(--paper-rgb), .55);
  color: var(--maroon);
}
.suc-empty .suc-mark svg { width: 42px; height: 42px; }

/* THE BIG HEADER, and now the panel's ONLY words: "Page Under Construction".
   The mono pill badge that used to sit above it (`.suc-badge`) said the same
   three words and was removed with the heading's second sentence on 2026-07-31.
   Display scale, tight leading and the small negative tracking Fraunces wants
   above ~2rem — the same treatment the home hero gets (§15.2). The top margin
   is 0 because the mark's own bottom margin already sets that gap. */
.suc-empty .suc-h {
  margin: 0;
  font-size: clamp(2rem, 5.6vw, 3.4rem);
  line-height: 1.06;
  letter-spacing: -.012em;
  text-wrap: balance;   /* progressive: ignored where unsupported */
}

/* Short gold hairline between the statement and the only action on the page —
   the divider idiom already used by .eyebrow. It hangs off the heading, NOT off
   the button: .btn-maroon carries `overflow: hidden` for its sheen
   (theme.css:539), so a pseudo-element outside the button would be clipped. */
.suc-empty .suc-h::after {
  content: "";
  display: block;
  width: 56px;
  height: 1px;
  margin: clamp(1.6rem, 4vw, 2.4rem) auto 0;
  background: rgba(var(--gold-rgb), .55);
}
.suc-empty .suc-back { margin-top: clamp(1.6rem, 4vw, 2.4rem); }

@media (max-width: 575.98px) {
  .suc-empty .suc-mark { width: 84px; height: 84px; box-shadow: 0 0 0 7px rgba(var(--paper-rgb), .55); }
  .suc-empty .suc-mark svg { width: 34px; height: 34px; }
  /* Full-size .btn padding already clears the 44px target (§20); .btn-lg on a
     narrow screen only adds width, so let it use the column. */
  .suc-empty .suc-back { width: 100%; max-width: 20rem; }
}

/* ===========================================================================
   §23 — GALLERY  (photo hubs · album pages · lightbox)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The gallery system (docs/superpowers/specs/2026-07-30-gallery-system-design.md)
   rebuilds 124 albums / 3,273 photographs onto six owner hubs, 124 album pages
   and a shared photo viewer. Its design comes from the gallery template pack,
   whose gallery rules ship INSIDE that pack's own theme.css. Copying them there
   would re-skin vendor CSS (hard rule 4), so the whole component layer lives
   here instead and the project's theme.css is left untouched.

   WHAT CHANGED AGAINST THE PACK — and why
     - COLOUR. The pack carries literal hexes and literal rgba triplets — a white
       card ground, a warm-grey chip label, a maroon hairline, a gold bloom. Every
       one of them is a semantic role here, so the gallery follows the identity
       automatically and survives a brand flip. The
       translucent values use the house `rgba(var(--token-rgb), a)` idiom already
       used in §22 — token-driven, never a literal triplet.
     - ALBUM DETAIL IS A PAGE, NOT AN OVERLAY. The pack renders every album into
       the hub as a hidden `position:fixed` `:target` overlay; at real scale that
       would ship 1,818 photographs in one document (spec §2.1). The visual
       treatment — dark `.adhero`, the `.album-facts` strip, the masonry
       `.gal-grid` — is kept verbatim and restyled as ordinary in-page sections.
       The overlay-only rules (`position:fixed`, `inset:0`, `:target` toggling)
       are dropped.
     - MARKUP. Cards are `<article class="album-card"><a class="album-card__link">`
       and photographs are `<figure class="gal-item"><button class="gal-item__btn">`
       (spec §6.1/§6.2) — a real navigable link and a real button, replacing the
       pack's `<a href="#…">` + `<div role="button" tabindex="0">`. The button is
       reset to nothing so it renders exactly like the pack's plain div.
     - COVERS. The pack's fixed 210px cover becomes `aspect-ratio: 16 / 9` on a
       real `<img>` with width/height, which is what gives zero CLS (spec §6.4).
     - MOBILE-FIRST. Built up from 320px per the spec §6.6 matrix, not down from
       the pack's desktop-first `max-width` queries. Nothing may scroll the body
       horizontally at 320px and no tap target is under 44 × 44px.
     - MOTION. Every transform/transition sits inside
       `@media (prefers-reduced-motion: no-preference)`, and every lift also
       inside `@media (hover: hover)` so touch devices never get a sticky hover.

   RTL is out of scope for this work (spec §11). No `[dir="rtl"]` rule is written;
   logical properties are used only where they cost nothing.

   Restyles no shared component: `.container`, `.btn-maroon`, `.eyebrow`,
   `.bc-hero` (§4) and `.serif` are used exactly as they ship.
   =========================================================================== */

/* --- §23.1 Component tunables ---------------------------------------------
   Sizes only — never colour. Change the rhythm HERE, not in the rules below.
   They sit on :root because the gallery's surfaces are siblings, not one tree:
   the sub-nav, the filter bar, the grids and the viewer are separate roots, and
   a custom property declared on one of them cannot reach the others.
   --gal-bar-top matches the sticky site header (≈120px, the same figure §0
   uses for :target scroll-margin), so the filter bar parks under it and never
   over it.
   Two gaps, not one: the masonry photo grid is tighter (--gal-gap) than the
   album-card grid (--gal-gap-cards), because a card carries a body of text and
   needs more air between columns than a bare photograph does. Every grid gap and
   tile/card radius that carries the page's RHYTHM reads one of these. Four kinds
   of literal are deliberately left alone, because none of them is rhythm: the
   print block (line ~3300) tightens both for paper on purpose; `border-radius:50%`
   makes the viewer buttons round; `row-gap:0` in the wide `.album-facts` layout is
   a reset, not a measure; and the rem gaps inside flex rows are the space between
   two adjacent elements. Changing a tunable here should not silently move any of
   those, which is precisely why they are not wired to it. */
:root {
  --gal-bar-top: 120px;
  --gal-gap: 20px;          /* masonry photo grid */
  --gal-gap-cards: 24px;    /* album / directory card grids */
  --gal-tap: 44px;          /* WCAG 2.5.8 minimum target */
  --gal-radius-card: var(--radius-lg);
  --gal-radius-tile: 14px;
}

/* --- §23.2 Hero -----------------------------------------------------------
   A modifier on the shared page/breadcrumb photo-hero (§4) rather than a second
   hero component. Spec §6.3.1 makes `class="page-hero gal-hero"` the contract
   the PHP renderers emit, so `.page-hero.gal-hero` is listed alongside the bare
   `.gal-hero`: at (0,2,0) it outranks every `.page-hero` rule in theme.css and
   in §12/§15 here, none of which declares a min-height, so the §6.6 scale wins
   whichever way the class is applied. It only sets that height scale and
   bottom-aligns the copy the way the pack's `d-flex align-items-end` did.

   The album page's dark band (`.album-detail .adhero`, §23.10) is deliberately
   NOT in this group. It is a META BAND, not a hero: the album's cover photo and
   its H1 are already carried by the shared `.page-hero` directly above it, so
   the band holds nothing but an eyebrow and a four-cell facts strip — about
   150px of copy. Handing it this image-hero scale padded it out to 400px and
   left ~250px of dead indigo between the album title and the first
   photograph. It is sized by its own content instead. */
.gal-hero,
.page-hero.gal-hero {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 260px;
}

/* --- §23.2.1 The album banner (`bc-hero gal-hero gal-hero--album`) ---------
   An album page opens on its OWN cover photograph. With the meta band below it
   reduced to a strip, that photograph is now the page's whole first impression
   — and it is the only thing on screen that shows the event before the grid
   decodes — so the album banner is taller than a section hero AND the brand
   wash over it is lighter.

   This is NOT a second scrim. §4 exposes the breadcrumb hero's treatment as six
   custom properties precisely so a context can re-tune it in one place, and
   that is all this does. Only the horizontal brand WASH comes down: it is the
   layer that tints and flattens a photograph without buying any contrast under
   the type. The neutral ink FLOOR — the layer that actually carries the H1 and
   the crumb row — goes UP to compensate, so the composite behind the text is
   darker than the site default while the upper two-thirds of the cover reads
   as a photograph instead of a coloured haze. The phone override in §4 is
   written against the same six properties, so it re-tunes with this for free. */
.bc-hero.gal-hero--album {
  --bc-wash-l:    0.34;   /* site default .60 — let the cover through */
  --bc-wash-m:    0.16;   /* site default .40 */
  --bc-wash-r:    0.05;   /* site default .12 */
  --bc-floor-btm: 0.70;   /* site default .55 — pay the contrast back here */
  --bc-floor-mid: 0.44;   /* site default .34 */

  /* ONE VIEWPORT: the banner and the meta band below it are sized to land on a
     single screen together, on every screen type.

     --album-band is the band's own reserved height, and it is the ONLY thing
     that changes across breakpoints (§23.13/§23.14 below) because the band's
     height is driven entirely by how `.album-facts` wraps: four cells in one
     row from 768px, two rows of two below it, and below 576px the eyebrow and
     the longer fact values can take a second line each. The back strip is a
     no-wrap flex row, so it holds ~72px at every width and is already inside
     each figure. Measured on the widest facts row the data produces.

     It is an ALLOWANCE, not a measurement, and it is only ever subtracted from
     a `min-height` — so an album whose band runs taller than the allowance
     simply scrolls a little. Nothing can ever clip.

     `svh` (small viewport, mobile chrome shown) is the only unit that is both
     stable and never optimistic — the same reasoning as §15.1, which is why
     the `vh` line above it is the fallback for engines without `svh`. The
     clamp keeps the banner a banner on a short laptop and stops it running
     away on a tall desktop; §15.6's floor-release precedent applies here too,
     via the clamp minimum.

     --album-peek is the homepage's `--stage-peek` idiom (§15.1): the fold must
     not land exactly on the bottom of the meta band, or the page reads as if it
     ENDS there. 76px = the grid section's own 36px top padding plus ~40px of
     the first row of photographs, so what shows below the fold is an actual
     photo edge — an unambiguous "there is more" — and not just a strip of
     ivory that could be mistaken for the end of the page. */
  --album-band: 340px;
  --album-peek: 76px;

  min-height: clamp(240px, calc(100vh  - var(--hdr-h) - var(--album-band) - var(--album-peek)), 720px);
  min-height: clamp(240px, calc(100svh - var(--hdr-h) - var(--album-band) - var(--album-peek)), 720px);
}

/* THE CROP. The banner keeps `.bc-hero`'s site-wide `background-size: cover`
   (§4) — the photograph fills the band edge to edge, which is what makes it
   read as the page's opening image, and the top and bottom of the frame are
   cut. That trade-off is a decision, not an oversight: a band this wide and
   this shallow cannot also show a 3:2 frame whole at any useful size (at 316px
   of height a full 3:2 photograph is only ~474px wide, i.e. a strip floating in
   a third of the band), and the album's details sharing the first screen is
   worth more than the edges of the frame. Recorded in docs/CHANGELOG.md.

   What IS tuned is where the cut falls. `center` removes equal slices top and
   bottom; these are event photographs, overwhelmingly groups of people, and in
   a group shot the faces sit above the middle. Biasing to 38% moves the kept
   band up so the cut takes more floor and less head. */
.bc-hero.gal-hero--album { background-position: center 38%; }

/* --- §23.3 Gallery sub-navigation -----------------------------------------
   Owner pills (University · Medicine · Dentistry · Pharmacy · Nursing · Student
   Affairs · SIDC). Below 576px it is a snap-scrolling rail — seven pills will not
   wrap into anything readable on a 320px screen, and wrapping them would be the
   one thing on the page most likely to push the body sideways. */
.gal-subnav {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: nowrap;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  max-width: 100%;
  /* room for the focus ring inside the scroll container, which would otherwise
     clip it at the top and bottom edges */
  padding-block: 3px;
  -webkit-overflow-scrolling: touch;
}
.gal-subnav a {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: inline-flex;
  align-items: center;
  min-height: var(--gal-tap);
  border: 1px solid var(--border);
  background: var(--surface-card);
  border-radius: var(--radius-pill);
  padding: .45rem 1rem;
  font-size: .86rem;
  font-weight: 600;
  color: var(--text-strong);
  text-decoration: none;
  white-space: nowrap;
}
.gal-subnav a.here,
.gal-subnav a[aria-current="page"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.gal-subnav a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- §23.4 Filter bar (tier label + chips) --------------------------------
   Three tiers on a hub: owner/collection, category, year. Sticky ONLY from
   992px up — on a phone a sticky bar eats a third of the viewport, and the
   spec (§6.6) makes it static there. */
.gal-bar {
  background: rgba(var(--paper-rgb), .92);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
  border-top: 1px solid var(--border-soft);
  border-bottom: 1px solid var(--border);
}
.gal-tier {
  display: flex;
  flex-direction: column;   /* label stacks above the chips — mobile default */
  align-items: flex-start;
  gap: .5rem;
  padding: .85rem 0;
  min-width: 0;
}
.gal-tier + .gal-tier { border-top: 1px dashed var(--border); }
.gal-tier-lbl {
  flex: none;
  font-family: "IBM Plex Mono", monospace;
  font-size: .64rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 500;
}

/* The chip rail. Below 992px it scrolls horizontally with a soft edge fade so a
   long year list (20 academic years) never wraps into a wall or widens the page.
   The mask consumes only the ALPHA channel of its stops — var(--paper) is used
   as the opaque stop purely because it is an opaque token, not for its colour. */
.gal-filters {
  display: flex;
  flex-wrap: nowrap;
  gap: .5rem;
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  padding-block: 3px;
  -webkit-overflow-scrolling: touch;
  -webkit-mask-image: linear-gradient(to right, transparent 0, var(--paper) 14px, var(--paper) calc(100% - 22px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, var(--paper) 14px, var(--paper) calc(100% - 22px), transparent 100%);
}
.gal-filters .f {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--gal-tap);
  border: 1px solid var(--border);
  background: var(--surface-card);
  color: var(--text-strong);
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-weight: 600;
  font-size: .9rem;
  line-height: 1.2;
  padding: .55rem 1.15rem;
  border-radius: var(--radius-pill);
  cursor: pointer;
  white-space: nowrap;
}
/* Every spelling of "on": `.active` is the pack's, `aria-pressed="true"` was the
   accessibility correction (spec §6.4) for the old <button> chips, and
   `aria-current="true"` is its equivalent now that a chip is an <a> pointing at
   the filtered URL (2026-08-01). Styling all three means the visual state can
   never drift from the announced one, whichever the markup uses. */
.gal-filters .f.active,
.gal-filters .f[aria-pressed="true"],
.gal-filters .f[aria-current="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.gal-filters .f.off,
.gal-filters .f[hidden] { display: none; }
/* A facet with no results left. The JS sets the real `disabled` attribute
   (spec §6.3.1), which takes the chip out of the tab order as well as out of
   the mouse's reach; the pack's `pointer-events:none` did only the latter and
   stranded a keyboard user on a chip they could still focus and press, so it is
   gone. The JS does still add a `.empty` class alongside the attribute, but this
   section deliberately gives it no rule — all disabled styling hangs off
   `:disabled`, so the attribute and the appearance can never drift apart.
   Dimmed with explicit tokens rather than `opacity`, because opacity on the
   chip would multiply with the `.n` opacity above and dim the count twice.
   --text-muted on --surface-media is 5.085:1, and the count inside it (that
   colour again at .8) is 3.418:1 — a disabled control still has to be
   perceivable, and both clear the 3:1 floor. */
.gal-filters .f:disabled {
  background: var(--surface-media);
  border-color: var(--border-soft);
  color: var(--text-muted);
  cursor: not-allowed;
}
.gal-filters .f:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* The chips became <a> elements on 2026-08-01, when filtering moved from the
   browser to the query string. Two consequences, both handled here rather than
   by touching the shared rules above:
     1. Bootstrap underlines every <a>. A chip is a control, not prose.
     2. `:disabled` cannot match an anchor, and a spent facet is now rendered as
        a <span> instead — so the dead-chip appearance hangs off `.is-empty`.
        NOT off `aria-disabled`: a bare <span> is role `generic`, which supports
        no ARIA states, so that attribute is dropped by the accessibility API and
        only trips axe-core's `aria-allowed-attr`. "Not a link" is already exposed
        structurally, because every live chip IS a link. Identical treatment to
        `.f:disabled` above, and the same contrast reasoning: both the chip and
        its text clear the 3:1 floor a disabled control still owes. */
.gal-filters a.f { text-decoration: none; }
.gal-filters .f.is-empty {
  background: var(--surface-media);
  border-color: var(--border-soft);
  color: var(--text-muted);
  cursor: not-allowed;
}

/* Tier 1 — owner/collection chips: the largest of the three. */
.f-col { padding: .6rem 1.25rem; font-size: .95rem; }
.f-col.active,
.f-col[aria-pressed="true"],
.f-col[aria-current="true"] { box-shadow: 0 6px 16px rgba(var(--maroon-rgb), .26); }

/* Tier 2 — category chips (General · Academic & Scientific · Cultural · Sports).
   Squared and carried on Academic Green, the GLOBAL secondary token, so the three
   tiers are told apart by shape and colour rather than by position alone. This is
   not a per-college hue: no college accent is involved anywhere in §23. */
.f-cat { padding: .42rem .95rem; font-size: .84rem; border-radius: var(--radius-sm); }
.f-cat.active,
.f-cat[aria-pressed="true"],
.f-cat[aria-current="true"] {
  background: var(--secondary);
  border-color: var(--secondary);
  color: var(--on-secondary);
}

/* Tier 3 — the year rail, in the data face. */
.f-year {
  font-family: "IBM Plex Mono", monospace;
  font-size: .85rem;
  letter-spacing: .04em;
  padding: .45rem 1rem;
  border-radius: var(--radius-sm);
}
.f-year.active,
.f-year[aria-pressed="true"],
.f-year[aria-current="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--on-scrim);
}

/* --- §23.5 Summary row: live count · title search · clear · load more -----
   The count is the `aria-live="polite"` region (spec §6.4), so a filter change is
   announced instead of happening silently. */
.gal-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .9rem 0;
}
.gal-summary .count {
  font-family: "IBM Plex Mono", monospace;
  font-size: .82rem;
  color: var(--text-muted);
}
.gal-summary .count b { color: var(--accent); font-weight: 600; }
/* Was a <button> and is now BOTH a submit button (Search) and an anchor (Clear
   filters), because filtering became query-string navigation on 2026-08-01. An
   <a> is display:inline by default, and `min-height` does not apply to a
   non-replaced inline box — the WCAG 2.5.8 target that `--gal-tap` exists to
   guarantee would have vanished silently, with no visual cue and no gate
   failure. inline-flex restores it for both element types. */
.gal-summary .clear {
  display: inline-flex;
  align-items: center;
  min-height: var(--gal-tap);
  padding: .4rem 0;
  font-size: .84rem;
  font-weight: 600;
  color: var(--accent);
  background: none;
  border: 0;
  cursor: pointer;
}
.gal-summary .clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Title search — necessary at 63 albums (spec §6.5). Full width on a phone, a
   fixed field beside the count from 576px up. */
.gal-search {
  flex: 1 1 14rem;
  min-width: 0;
  min-height: var(--gal-tap);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--text);
  font-size: .9rem;
  padding: .5rem .8rem;
}
.gal-search::placeholder { color: var(--text-placeholder); }
.gal-search:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- §23.5b Pager — 24 albums a page, as URL state ------------------------
   Replaces the old "Load more" button (2026-08-01). Filtering and paging are
   now query-string navigation handled in includes/gallery-hub.php, so this is
   a rail of ordinary links and every page of every filtered view has its own
   address. `.gal-more` is gone with the script that drove it; nothing in the
   repo references it. */
.gal-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  margin-top: 2.5rem;
}
.gal-pager .pg-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .4rem;
}
.gal-pager .pg {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  min-height: var(--gal-tap);
  min-width: var(--gal-tap);
  padding: .5rem .9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--text-strong);
  font-family: "IBM Plex Mono", monospace;
  font-size: .84rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
.gal-pager .pg.active,
.gal-pager .pg[aria-current="page"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
/* Previous/Next at the ends of the range. An <a> cannot be `disabled`, so the
   spent step is a plain <span>, dimmed with explicit tokens (not `opacity`)
   exactly as the spent filter chip is — and for the same reason it carries a
   class rather than `aria-disabled`, which role `generic` does not support. */
.gal-pager .pg.is-empty {
  background: var(--surface-media);
  border-color: var(--border-soft);
  color: var(--text-muted);
  cursor: not-allowed;
}
.gal-pager .pg-gap {
  border: 0;
  background: none;
  min-width: 0;
  padding: .5rem .2rem;
  color: var(--text-muted);
}
.gal-pager .pg:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- §23.6 Empty state ----------------------------------------------------
   Hidden by default and revealed with `.show` — the CSS ↔ JS contract in spec
   §6.3.1. It was written the other way round (visible by default, hidden with
   `[hidden]`), which pinned the empty state permanently on screen underneath a
   full grid and made the JS `.show` toggle a no-op. `[hidden]` is kept as the
   last rule so markup that closes it that way still wins. */
.gal-empty {
  display: none;
  text-align: center;
  padding: 4rem 1rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
  background: var(--ivory);
}
/* Order matters: both selectors weigh (0,2,0), so the LAST one wins. `.show` is
   the §6.3.1 contract and must therefore come last — with `[hidden]` last instead,
   markup that ships `<div id="galEmpty" hidden>` (the obvious thing for a renderer
   to write) could never be revealed by the JS, and the empty state would be dead
   again in a new way. The JS also REMOVES the `hidden` attribute when it shows the
   panel, because `hidden` is exposed to the accessibility tree independently of
   `display` — winning the cascade alone would leave it visible but unannounced. */
.gal-empty[hidden] { display: none; }
.gal-empty.show    { display: block; }

/* --- §23.7 Year divider inside a grid ------------------------------------- */
.gal-year-head {
  break-inside: avoid;
  margin: 0 0 var(--gal-gap);
  display: flex;
  align-items: center;
  gap: .8rem;
}
.gal-year-head .y {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.6rem;
  font-weight: 600;
  color: var(--accent);
}
.gal-year-head .r { flex: 1; height: 1px; background: var(--gold); }

/* --- §23.8 Album grid + album card ----------------------------------------
   One column at 320px, and the grid tracks are `minmax(0, 1fr)` rather than the
   pack's `1fr`: a long unbroken album title in a `1fr` track sets the track's
   minimum to its own width and pushes the page sideways. */
.album-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gal-gap-cards);
}
/* Zero results: the JS hides the whole grid with `.hide` (spec §6.3.1). `.hide`
   is otherwise only defined compounded onto `.album-card` / `.gal-item`, so on
   the grid itself it did nothing. */
#albumGrid.hide { display: none; }

.album-card {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-width: 0;
  background: var(--surface-card);
  border: 1px solid var(--border);
  border-radius: var(--gal-radius-card);
  overflow: hidden;
  box-shadow: var(--shadow-1);
}
.album-card.hide,
.album-card[hidden] { display: none; }

/* The whole card is one link (spec §6.1) — middle-click and "open in new tab"
   work, which they did not in the pack. The ring is drawn INSIDE the card
   because the card clips its own corners. */
.album-card__link {
  display: flex;
  flex-direction: column;
  flex: 1;
  color: inherit;
  text-decoration: none;
}
.album-card__link:hover { color: inherit; }
.album-card__link:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
  border-radius: var(--gal-radius-card);
}

.album-card .cover {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  background: var(--surface-media);
  overflow: hidden;
}
.album-card .cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.album-card .yr {
  position: absolute;
  top: 12px;
  left: 12px;
  background: rgba(var(--paper-rgb), .94);
  color: var(--maroon-deep);
  border: 1px solid rgba(var(--maroon-rgb), .16);
  font-family: "IBM Plex Mono", monospace;
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .06em;
  padding: .3rem .65rem;
  border-radius: var(--radius-pill);
}
.album-card .body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1.3rem 1.3rem 1.5rem;
}
.album-card .scope {
  font-family: "IBM Plex Mono", monospace;
  font-size: .64rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: .5rem;
}
.album-card h3 {
  font-size: clamp(1.1rem, 1.4vw, 1.22rem);
  line-height: 1.25;
  margin: 0 0 .5rem;
  /* a 40-character album title must wrap, not widen its column */
  overflow-wrap: anywhere;
}
.album-card .when {
  display: block;
  font-size: .86rem;
  color: var(--text-muted);
}
.album-card .open {
  margin-top: auto;
  padding-top: 1rem;
  font-weight: 600;
  font-size: .9rem;
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: .5rem;
}
.album-card .open::after { content: "\2192"; }
.album-card .bar { display: block; height: 4px; background: var(--gold); }

/* --- §23.9 Directory cards (owner navigation) -----------------------------
   `.dir-grid` is optional: the pack's Bootstrap `row g-3 > .col-sm-6.col-lg-3`
   produces the identical 1 / 2 / 4 / 4 rhythm and is equally acceptable. */
.dir-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1rem;
}
.dir-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1rem;
  min-width: 0;
  height: 100%;
  padding: 1.4rem;
  background: var(--surface-card);
  border: 1px solid var(--border);
  border-radius: var(--gal-radius-tile);
  color: inherit;
  text-decoration: none;
}
.dir-card .n {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.2rem;
  line-height: 1.25;
  color: var(--ink);
}
.dir-card .c {
  font-family: "IBM Plex Mono", monospace;
  font-size: .7rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.dir-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* --- §23.10 Album page: dark hero, facts strip, back strip ----------------
   The pack's `.album-detail` was a full-screen `position:fixed` `:target`
   overlay. Here it is the album PAGE body (spec §2.1), so it is an ordinary
   flow section carrying the ivory ground the masonry sits on. */
.album-detail { background: var(--ivory); }

/* The back link, NOT sticky: an album page has the shared breadcrumb above it
   already, and a second sticky bar would fight the site header.

   It lives INSIDE `.adhero` (2026-07-31) rather than in its own `.adx` band —
   both carried `--surface-primary-dark`, so as two elements they read as one
   slab with an arbitrary seam through it — and it is not a row of its own
   either: `.adtop` puts it on the band's first line opposite the owner eyebrow,
   which is a line the band was already spending. `.adx` no longer exists.
   `flex-wrap` because "RAK Medical & Health Sciences University" and "Back to
   Student Affairs gallery" do not share a line on a phone. */
.album-detail .adhero .adtop {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .35rem 1.25rem;
  margin-bottom: .5rem;
}
/* Zeroed here only: the standalone rule below gives the eyebrow its gap to the
   facts, which on this line is the flex `gap` and `.adtop`'s own margin. */
.album-detail .adhero .adtop .eyebrow { margin-bottom: 0; }
.album-detail .adhero .back {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: var(--gal-tap);
  background: none;
  border: 0;
  color: var(--text-gold-on-dark);
  font-weight: 600;
  font-size: .92rem;
  cursor: pointer;
  text-decoration: none;
}
.album-detail .adhero .back:hover { color: var(--text-on-primary); }
.album-detail .adhero .back:focus-visible {
  outline: 2px solid var(--focus-ring-on-dark);
  outline-offset: 3px;
}

/* Sized by its own content — see the note in §23.2 on why it is not in the
   image-hero height group. The padding is the band's whole vertical rhythm. */
.album-detail .adhero {
  position: relative;
  overflow: hidden;
  background: var(--surface-primary-dark);
  color: var(--text-on-primary);
  padding: 1.35rem 0 1.6rem;
}
/* one warm gold bloom in the top-right corner — the pack's signature on every
   dark brand surface */
.album-detail .adhero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 85% 20%, rgba(var(--gold-bright-rgb), .22), transparent 60%);
  pointer-events: none;
}
.album-detail .adhero > * { position: relative; z-index: 1; }
/* Kept for a record that ever renders a heading inside the band. The album's
   OWN title is not one: the shared `.page-hero` above states it as the
   document's H1, and the band repeating it put the same words on screen twice
   within ~100px. The band carries the eyebrow and the facts only. */
.album-detail .adhero h1,
.album-detail .adhero h2 {
  color: var(--text-on-primary);
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  margin: 0 0 .5rem;
  text-wrap: balance;
}
.album-detail .adhero p {
  color: var(--text-on-primary-muted);
  max-width: 620px;
  line-height: 1.7;
  margin: 0;
}
/* The eyebrow is a <p>, so the reset above zeroes its margin; it needs its own
   gap to the facts strip (or to a description, when a record carries one). */
.album-detail .adhero .eyebrow { margin-bottom: .5rem; }

.album-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: 1.1rem;
  margin-top: 1rem;
}
.album-facts .fct {
  min-width: 0;
  padding: 0 1.2rem;
  border-left: 1px solid rgba(var(--gold-bright-rgb), .35);
}
.album-facts .fct .k {
  font-family: "IBM Plex Mono", monospace;
  font-size: .62rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-on-dark-label-gold);
}
.album-facts .fct .v {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.15rem;
  color: var(--text-on-primary);
  margin-top: .35rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/* Previous / next album, at the foot of an album page (spec §6.5). */
.album-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.5rem 0;
  border-top: 1px solid var(--border);
}
.album-nav a {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  flex: 1 1 14rem;
  min-width: 0;
  min-height: var(--gal-tap);
  padding: .6rem 0;
  color: var(--text);
  text-decoration: none;
}
.album-nav a .k {
  font-family: "IBM Plex Mono", monospace;
  font-size: .64rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.album-nav a .t {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.05rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.album-nav a.next { text-align: right; align-items: flex-end; }
.album-nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* --- §23.11 Masonry photo grid -------------------------------------------- */
.gal-grid {
  column-count: 1;
  column-gap: var(--gal-gap);
}

.gal-item {
  position: relative;
  display: block;
  break-inside: avoid;
  margin: 0 0 var(--gal-gap);
  border: 1px solid var(--border);
  border-radius: var(--gal-radius-tile);
  background: var(--surface-card);
  overflow: hidden;
  box-shadow: var(--shadow-1);
}
.gal-item.hide,
.gal-item[hidden] { display: none; }

/* The pack styled a bare div; ours is a real <button> (spec §6.4). Strip every
   default the UA gives a button so the tile renders identically. */
.gal-item__btn {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.gal-item__btn img {
  display: block;
  width: 100%;
  height: auto;
}
.gal-item__btn:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
  border-radius: var(--gal-radius-tile);
}

/* The caption is a real <figcaption> laid over the tile. `pointer-events: none`
   keeps the whole tile clickable through it. */
.gal-item .cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1.4rem 1.1rem .95rem;
  /* WCAG 1.4.3 — the caption sits over an ARBITRARY photograph, so the text
     needs a backdrop that does not depend on the image. The old single
     `to top` gradient reached only alpha .34 at the eyebrow's top edge, i.e.
     1.17:1 for `.cap .m` over a light photograph.

     The scrim is therefore split at the caption's own top padding edge, using
     two layers whose sizes add up to the box:
       layer 1  the fade — occupies EXACTLY the 1.4rem top padding, 0 -> .90;
       layer 2  a flat >= .90 scrim over the remaining height, which is the
                content box, i.e. every line of caption text.
     Because the boundary is a length equal to `padding-top` and not a
     percentage, it stays exact however many lines the title wraps to. Worst
     case (a pure white photograph) the composite behind the text is
     rgb(50,44,93) and --gold-bright clears 4.5:1 with room to spare. */
  background-color: transparent;
  background-image:
    linear-gradient(to bottom,
                    rgba(var(--maroon-black-rgb), 0),
                    rgba(var(--maroon-black-rgb), .90)),
    linear-gradient(to top,
                    rgba(var(--maroon-black-rgb), .97),
                    rgba(var(--maroon-black-rgb), .90));
  background-repeat: no-repeat, no-repeat;
  background-position: left top, left bottom;
  background-size: 100% 1.4rem, 100% calc(100% - 1.4rem);
  color: var(--on-scrim);
  pointer-events: none;
}
.gal-item .cap .m {
  display: block;
  font-family: "IBM Plex Mono", monospace;
  font-size: .66rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--gold-bright);
  margin-bottom: .3rem;
}
.gal-item .cap .t {
  display: block;
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.05rem;
  line-height: 1.25;
}
.gal-item .stamp {
  position: absolute;
  top: 12px;
  left: 12px;
  background: rgba(var(--paper-rgb), .94);
  color: var(--maroon-deep);
  border: 1px solid rgba(var(--maroon-rgb), .16);
  font-family: "IBM Plex Mono", monospace;
  font-size: .66rem;
  font-weight: 500;
  letter-spacing: .06em;
  padding: .3rem .6rem;
  border-radius: var(--radius-pill);
}
.gal-item .tagdot {
  position: absolute;
  top: 12px;
  right: 12px;
  background: rgba(var(--maroon-rich-rgb), .9);
  color: var(--gold-bright);
  font-family: "IBM Plex Mono", monospace;
  font-size: .62rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .32rem .65rem;
  border-radius: var(--radius-pill);
}

/* --- §23.12 Lightbox (#galLightbox — spec §6.3) ---------------------------
   `100dvh`, never `100vh`: on a phone `100vh` is the viewport WITHOUT browser
   chrome, so the caption row sits under the address bar and the bottom control
   is unreachable. Padding clears the notch on every side via env(). */
.gal-lightbox {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  height: 100dvh;
  /* On a phone the arrows sit OVER the image (they carry their own scrim), so
     the inline padding stays at 1rem and the photograph keeps the width; from
     992px up (§23.15) the padding opens out and the arrows move outside it. */
  padding:
    calc(env(safe-area-inset-top, 0px) + 4rem)
    calc(env(safe-area-inset-right, 0px) + 1rem)
    calc(env(safe-area-inset-bottom, 0px) + 1rem)
    calc(env(safe-area-inset-left, 0px) + 1rem);
  background: rgba(var(--black-warm-rgb), .94);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  overscroll-behavior: contain;
}
/* [hidden] is the ONLY closed state — it beats the display:flex above on
   specificity, so the dialog can never be left visible-but-inert. */
.gal-lightbox[hidden] { display: none; }

.gal-lightbox .stage {
  flex: 1 1 auto;
  min-height: 0;
  width: min(1100px, 100%);
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(var(--gold-bright-rgb), .35);
  border-radius: var(--gal-radius-tile);
  overflow: hidden;
  background: rgba(var(--black-rgb), .35);
  /* horizontal swipes belong to the viewer's JS (next/previous photo); vertical
     panning and pinch-zoom stay native so the image can still be examined */
  touch-action: pan-y pinch-zoom;
}
/* ⛔ THE WHOLE PHOTOGRAPH, ALWAYS.
   `max-height: 100%` on the <img> alone did NOT hold it: the stage's height
   comes from `flex: 1 1 auto`, and a percentage max-height against a
   flex-derived height is not resolved, so a tall frame rendered at its natural
   height and the top and bottom were clipped away with no way to reach them.
   Measured on /news?id=429: a 797x1125 newsletter page rendered 1125px tall in
   an 813px stage. Giving the <picture> a definite box — absolutely positioned
   against the stage — is what makes the percentage resolve. */
.gal-lightbox .stage { position: relative; }
.gal-lightbox .stage picture {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gal-lightbox .stage img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* ZOOMED. The picture is grown past the stage and the stage scrolls, rather
   than the picture being scaled inside a clipped box — a scroll bar is a real
   affordance a keyboard can drive, and nothing ends up outside the frame with
   no way to reach it. Dragging pans as well, for a mouse. */
.gal-lightbox .stage.is-zoomed {
  overflow: auto;
  align-items: flex-start;
  justify-content: flex-start;
  cursor: grab;
  touch-action: auto;
  scrollbar-width: thin;
}
/* Zoomed, the picture leaves its fitted box and the stage scrolls to it. */
.gal-lightbox .stage.is-zoomed picture {
  position: static;
  display: block;
  margin: auto;
}
.gal-lightbox .stage.is-zoomed img { margin: auto; }
.gal-lightbox .stage.is-panning { cursor: grabbing; }

/* The zoom controls live in the caption row, so they never cover the picture. */
.gal-zoom {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  flex: 0 0 auto;
}
.gal-zoom__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 34px;
  padding: 0 .45rem;
  border: 1px solid rgba(var(--paper-rgb), .28);
  border-radius: 8px;
  background: rgba(var(--paper-rgb), .08);
  color: var(--on-scrim);
  font: inherit;
  font-size: .85rem;
  cursor: pointer;
  transition: background .18s ease, border-color .18s ease, opacity .18s ease;
}
.gal-zoom__btn:hover:not(:disabled) { background: rgba(var(--paper-rgb), .2); }
.gal-zoom__btn:disabled { opacity: .38; cursor: default; }
.gal-zoom__btn:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: 2px; }
.gal-zoom__btn--text { font-weight: 600; letter-spacing: .02em; }
.gal-zoom__level {
  min-width: 3.6em;
  text-align: center;
  font-family: "IBM Plex Mono", monospace;
  font-size: .78rem;
  color: var(--text-muted-on-dark);
}

@media (prefers-reduced-motion: reduce) {
  .gal-zoom__btn { transition: none; }
}
.gal-lightbox .meta {
  flex: 0 0 auto;
  width: min(1100px, 100%);
  margin: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  color: var(--text-on-dark);
  font-size: .95rem;
  /* Three things share this row now — caption, zoom, counter — so it wraps
     rather than squeezing the caption to nothing. */
  flex-wrap: wrap;
}
.gal-lightbox .meta > span:first-child {
  flex: 1 1 12rem;
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.15rem;
  line-height: 1.3;
  color: var(--on-scrim);
  min-width: 0;
  overflow-wrap: anywhere;
}
.gal-lightbox .meta [data-lb-count] {
  flex: 0 0 auto;
  font-family: "IBM Plex Mono", monospace;
  font-size: .8rem;
  color: var(--text-muted-on-dark);
  white-space: nowrap;
}

.gal-lightbox .close,
.gal-lightbox .prev,
.gal-lightbox .next {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--gal-tap);
  height: var(--gal-tap);
  min-width: var(--gal-tap);
  min-height: var(--gal-tap);
  padding: 0;
  border: 1px solid rgba(var(--paper-rgb), .3);
  border-radius: 50%;
  background: rgba(var(--black-rgb), .35);
  color: var(--on-media);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.gal-lightbox .prev,
.gal-lightbox .next { top: 50%; transform: translateY(-50%); }
.gal-lightbox .prev { left: calc(env(safe-area-inset-left, 0px) + .5rem); }
.gal-lightbox .next { right: calc(env(safe-area-inset-right, 0px) + .5rem); }
.gal-lightbox .prev,
.gal-lightbox .next { z-index: 1; }
.gal-lightbox .close {
  top: calc(env(safe-area-inset-top, 0px) + .75rem);
  right: calc(env(safe-area-inset-right, 0px) + .75rem);
}
.gal-lightbox .close:focus-visible,
.gal-lightbox .prev:focus-visible,
.gal-lightbox .next:focus-visible {
  outline: 2px solid var(--focus-ring-on-dark);
  outline-offset: 3px;
}
/* The page behind an open viewer must not scroll under it. `gal-lb-open` is the
   class the JS adds (spec §6.3.1). Removing the scrollbar shifts the whole page
   sideways, so the width the JS measured into --gal-sbw on <html> is given back
   as trailing padding; on a phone, and on any overlay-scrollbar platform, that
   measurement is 0 and the fallback keeps it at 0. */
body.gal-lb-open {
  overflow: hidden;
  padding-inline-end: var(--gal-sbw, 0);
}

/* --- §23.13 Responsive · 576px and up ------------------------------------- */
@media (min-width: 576px) {
  .gal-hero,
  .page-hero.gal-hero { min-height: 320px; }

  /* Eyebrow and fact values stop wrapping; the facts are still two rows of two
     (the four-cell row arrives at 768px, below). */
  .bc-hero.gal-hero--album { --album-band: 300px; }

  .album-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .gal-grid   { column-count: 2; }
  .dir-grid   { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .gal-subnav { flex-wrap: wrap; overflow-x: visible; }
  .gal-search { flex: 0 1 18rem; }
}

/* --- §23.14 Responsive · 768px and up ------------------------------------- */
@media (min-width: 768px) {
  /* the tier label sits beside its chips again */
  .gal-tier { flex-direction: row; align-items: flex-start; gap: 1rem; }
  .gal-tier-lbl { flex: 0 0 108px; padding-top: .7rem; }

  .album-facts { grid-template-columns: repeat(4, minmax(0, 1fr)); row-gap: 0; }

  /* The facts collapse to ONE row here, so the band loses ~80px and the banner
     takes every pixel of it back (§23.2.1). */
  .bc-hero.gal-hero--album { --album-band: 220px; }
}

/* --- §23.15 Responsive · 992px and up ------------------------------------- */
@media (min-width: 992px) {
  .gal-hero,
  .page-hero.gal-hero { min-height: 400px; }

  .album-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .gal-grid   { column-count: 3; }
  .dir-grid   { grid-template-columns: repeat(4, minmax(0, 1fr)); }

  /* Sticky only from here up — see §23.4. */
  .gal-bar {
    position: sticky;
    top: var(--gal-bar-top);
    z-index: 900;
  }
  /* There is room to wrap now, so the rail and its edge fade are retired. */
  .gal-filters {
    flex-wrap: wrap;
    overflow-x: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }
  /* room for the arrows to clear the photograph entirely */
  .gal-lightbox { padding-inline: 4.5rem; }
  .gal-lightbox .prev { left: 1.5rem; }
  .gal-lightbox .next { right: 1.5rem; }
}

/* --- §23.16 Responsive · 1400px and up ------------------------------------ */
@media (min-width: 1400px) {
  .album-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .gal-grid   { column-count: 4; }
}

/* --- §23.17 Landscape phone ----------------------------------------------
   844 × 390 leaves ~390px of height: the caption block has to give the image
   back its height, so it collapses to a single truncated line. */
@media (max-height: 500px) and (orientation: landscape) {
  .gal-lightbox {
    gap: .5rem;
    padding-top: calc(env(safe-area-inset-top, 0px) + 3.25rem);
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + .5rem);
  }
  /* A short landscape screen is WIDE — the caption, the zoom controls and the
     counter still fit on one row, and every row down here is height taken off
     the photograph. Keep it to one. */
  .gal-lightbox .meta {
    flex-wrap: nowrap;
    align-items: baseline;
    gap: .75rem;
    font-size: .85rem;
  }
  .gal-lightbox .meta > span:first-child {
    font-size: .95rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .gal-zoom__btn { min-width: 30px; height: 30px; }
}

/* --- §23.17b Photo viewer on a phone --------------------------------------
   Two rows, not three: the caption takes the first on its own, then the zoom
   controls and the counter share the second. Left to wrap on its own the three
   stacked into three rows at 360px and took 126px off the photograph.
   -------------------------------------------------------------------------- */
@media (max-width: 575.98px) {
  .gal-lightbox .meta {
    align-items: center;
    gap: .4rem .75rem;
    font-size: .85rem;
  }
  .gal-lightbox .meta > span:first-child {
    flex: 1 1 100%;
    font-size: .95rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* NOT `margin-right: auto` on the zoom group: in a wrapping flex container an
     auto margin fills the whole line, which pushed the counter onto a third row.
     `justify-content: space-between` on .meta already sends the two ends apart. */
  /* WCAG 2.5.8: a 24px minimum, and a photo viewer is driven by thumb. */
  .gal-zoom__btn { min-width: 42px; height: 42px; }
  .gal-lightbox .meta [data-lb-count] { align-self: center; }
}

/* --- §23.18 Touch / coarse pointer ---------------------------------------
   With no hover there is no way to reveal a hover-only caption, so on a touch
   device the tile caption is simply always on. */
@media (hover: none) {
  .gal-item .cap { opacity: 1; transform: none; }
  .gal-item .tagdot { opacity: 1; }
}

/* --- §23.19 Hover (fine pointers only) ------------------------------------
   Colour, border and shadow changes. Everything that MOVES is in §23.20 so a
   reduced-motion visitor still gets the full hover feedback, just without the
   travel. */
@media (hover: hover) {
  .gal-filters .f:hover { border-color: var(--accent); color: var(--accent); }
  .gal-filters .f.active:hover,
  .gal-filters .f[aria-pressed="true"]:hover,
  .gal-filters .f[aria-current="true"]:hover { color: var(--on-accent); }
  .f-cat:hover { border-color: var(--secondary); color: var(--secondary); }
  .f-cat.active:hover,
  .f-cat[aria-pressed="true"]:hover { color: var(--on-secondary); }
  .f-year:hover { border-color: var(--ink); color: var(--ink); }
  .f-year.active:hover,
  .f-year[aria-pressed="true"]:hover { color: var(--on-scrim); }
  /* A disabled chip is still hoverable in CSS — `:hover` matches even where the
     UA suppresses the events — so without this it would light up as if it were
     live now that `pointer-events:none` is gone (§23.4). (0,4,0) outranks every
     chip hover rule above it. */
  .gal-filters .f:disabled:hover {
    border-color: var(--border-soft);
    color: var(--text-muted);
  }

  .gal-subnav a:hover { border-color: var(--accent); color: var(--accent); }
  .gal-subnav a.here:hover,
  .gal-subnav a[aria-current="page"]:hover { color: var(--on-accent); }

  .gal-summary .clear:hover { color: var(--primary-hover); text-decoration: underline; }
  .gal-pager .pg:hover { border-color: var(--accent); color: var(--accent); }
  .gal-pager .pg.active:hover,
  .gal-pager .pg[aria-current="page"]:hover { color: var(--on-accent); }
  .gal-pager .pg.is-empty:hover { border-color: var(--border-soft); color: var(--text-muted); }

  .album-card:hover { box-shadow: var(--shadow-2); border-color: var(--border-strong); }
  .dir-card:hover { border-color: var(--accent); box-shadow: var(--shadow-2); }
  .dir-card:hover .n { color: var(--accent); }
  .gal-item:hover { box-shadow: var(--shadow-2); }
  .album-nav a:hover .t { color: var(--accent); text-decoration: underline; }

  .gal-lightbox .close:hover { border-color: var(--accent); background: var(--accent); }
  .gal-lightbox .prev:hover,
  .gal-lightbox .next:hover { border-color: var(--accent); background: var(--accent); }
}

/* --- §23.20 Motion --------------------------------------------------------
   Nothing in the gallery moves unless the visitor has NOT asked for less motion
   (spec §6.4). The lifts are additionally gated on a fine pointer, because on a
   touch screen a :hover transform sticks after the tap and reads as a bug. */
@media (prefers-reduced-motion: no-preference) {
  .gal-filters .f,
  .gal-subnav a,
  .gal-pager .pg {
    transition: background .2s ease, color .2s ease, border-color .2s ease, transform .2s ease;
  }
  .album-card,
  .gal-item {
    transition: transform .28s cubic-bezier(.16, .84, .44, 1), box-shadow .28s ease, border-color .28s ease;
  }
  .dir-card {
    transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
  }
  .album-card .open::after { transition: transform .2s ease; }
  .gal-lightbox .close,
  .gal-lightbox .prev,
  .gal-lightbox .next { transition: background .2s ease, border-color .2s ease; }
}

/* The caption reveal. Hidden at rest ONLY where it can actually be revealed —
   i.e. on a fine pointer or by keyboard focus; §23.18 keeps it on for touch. */
@media (prefers-reduced-motion: no-preference) and (hover: hover) {
  .gal-item .cap {
    opacity: 0;
    transform: translateY(10px);
    transition: opacity .3s ease, transform .3s ease;
  }
  .gal-item .tagdot { opacity: 0; transition: opacity .3s ease; }
  .gal-item:hover .cap,
  .gal-item:focus-within .cap { opacity: 1; transform: none; }
  .gal-item:hover .tagdot,
  .gal-item:focus-within .tagdot { opacity: 1; }
}
/* Reduced motion, fine pointer: the same reveal, without the travel. */
@media (prefers-reduced-motion: reduce) and (hover: hover) {
  .gal-item .cap { opacity: 0; }
  .gal-item .tagdot { opacity: 0; }
  .gal-item:hover .cap,
  .gal-item:focus-within .cap,
  .gal-item:hover .tagdot,
  .gal-item:focus-within .tagdot { opacity: 1; }
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .gal-filters .f:hover { transform: translateY(-1px); }
  .gal-filters .f:disabled:hover { transform: none; }
  .album-card:hover { transform: translateY(-6px); }
  .album-card:hover .open::after { transform: translateX(4px); }
  .dir-card:hover { transform: translateY(-4px); }
  .gal-item:hover { transform: translateY(-5px); }
}

/* --- §23.21 Print ---------------------------------------------------------
   Paper is ~816px, so the >=992px column rules are still live and would split an
   album into four columns of thumbnails. Two columns, captions on, chrome off. */
@media print {
  .gal-bar,
  .gal-subnav,
  .gal-summary .clear,
  .gal-search,
  .gal-pager,
  .gal-lightbox { display: none !important; }

  .album-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .gal-grid { column-count: 2; column-gap: 12px; }
  .album-card,
  .gal-item,
  .dir-card { box-shadow: none; break-inside: avoid; }
  .album-detail,
  .gal-empty { background: var(--print-bg); }
  .gal-item .cap {
    position: static;
    opacity: 1;
    transform: none;
    background: none;
    color: var(--print-text);
    padding: .5rem .6rem .7rem;
  }
  .gal-item .cap .m { color: var(--print-text); }
  .gal-item .stamp,
  .gal-item .tagdot { display: none; }
}

/* ===========================================================================
   §24 — PARTNERSHIPS / MoU REGISTER  (body.page-mous — rakmhsu-mous.php)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The page used to be one 55-row, five-column table inside the About rail —
   accurate, and unreadable on a phone. It is now a card per agreement: logo,
   partner, country, expiry, status, website, plus a "Recorded as" line carrying
   the register's own verbatim wording and which side of the agreement the
   partner is. That last line is what lets the table go (removed 2026-07-31 on
   instruction) without the page losing anything the table held (hard rule 2).

   The card is NOT a link. Roughly a fifth of the partners have no website we
   could verify, so a card-wide anchor would be a link on some cards and dead
   space on others; the "Visit website" row is the only interactive element and
   it is present only when there is somewhere to go.

   Everything here is token-driven. The status pills reuse the same
   --success-* / --warning-* pairs the site's alerts use, so "Active" and
   "In process" cannot drift from the rest of the system.
   ========================================================================= */

/* --- §24.1 Stat strip ---------------------------------------------------- */
.mou-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
  margin-bottom: 2rem;
}
.mou-stats .s {
  background: var(--surface-card);
  padding: 1.05rem 1.15rem;
}
.mou-stats .s .n {
  display: block;
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.75rem;
  line-height: 1.1;
  color: var(--maroon);
}
.mou-stats .s .k {
  display: block;
  margin-top: .3rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .64rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* --- §24.2 Toolbar: search + two chip rows ------------------------------- */
.mou-toolbar {
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface-card);
  padding: 1.1rem 1.15rem;
  margin-bottom: 1.4rem;
}
.mou-search {
  position: relative;
  margin-bottom: 1rem;
}
.mou-search input {
  width: 100%;
  min-height: 46px;
  padding: .6rem .9rem .6rem 2.4rem;
  border: 1px solid var(--field-border);
  border-radius: var(--radius-pill);
  background: var(--ivory);
  color: var(--text-strong);
  font-size: .95rem;
}
.mou-search input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-color: var(--maroon);
}
.mou-search svg {
  position: absolute;
  left: .85rem;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  color: var(--text-muted);
  pointer-events: none;
}
.mou-tier + .mou-tier { margin-top: .75rem; }
.mou-tier-lbl {
  display: block;
  margin-bottom: .45rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .62rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.mou-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
}
.mou-chip {
  display: inline-flex;
  align-items: center;
  min-height: 38px;
  padding: .4rem .95rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-strong);
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-weight: 600;
  font-size: .85rem;
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
}
.mou-chip[aria-pressed="true"] {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--text-on-primary);
}
.mou-chip:disabled {
  opacity: .4;
  cursor: not-allowed;
}
.mou-chip:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* --- §24.3 The announced result line ------------------------------------- */
.mou-count {
  margin: 0 0 1.15rem;
  font-size: .92rem;
  color: var(--text-muted);
}
.mou-count b { color: var(--text-strong); }

/* --- §24.4 The grid and the card ----------------------------------------- */
.mou-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1.15rem;
}
.mou-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-card);
  overflow: hidden;
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.mou-card:hover {
  border-color: rgba(var(--maroon-rgb), .35);
  box-shadow: 0 12px 30px rgba(var(--charcoal-rgb), .08);
  transform: translateY(-2px);
}
.mou-card.is-hidden { display: none; }

/* The logo plate. Always white and always `contain`, because what lands here
   ranges from a wide wordmark to a square app icon and the partners' own marks
   are not ours to crop or recolour. */
.mou-card .plate {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 96px;
  padding: 1rem 1.15rem;
  background: var(--paper);
  border-bottom: 1px solid var(--line-soft);
}
.mou-card .plate img {
  max-width: 100%;
  max-height: 64px;
  width: auto;
  height: auto;
  object-fit: contain;
}
/* One partner's only official artwork is a white-knockout mark, which would be
   invisible on the white plate. Recolouring another organisation's logo is not
   ours to do, so that plate takes the dark brand surface the mark was drawn for.
   Driven by `logoOnDark` in includes/data/mou-partners.php, never by a slug. */
.mou-card .plate.on-dark {
  background: var(--surface-primary-dark);
  border-bottom-color: var(--surface-primary-dark);
}
/* Fallback when we hold no logo for a partner: their initials, set in the
   display serif on the brand tint. Deliberately NOT a generic building icon —
   an initial is a fact about the name, an icon would be decoration. */
.mou-card .plate .mono {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: 14px;
  background: var(--surface-primary-tint);
  color: var(--maroon);
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.35rem;
  letter-spacing: .02em;
}
.mou-card .body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1.05rem 1.15rem 1.15rem;
}
.mou-tag {
  align-self: flex-start;
  font-family: "IBM Plex Mono", monospace;
  font-size: .6rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--maroon);
  margin-bottom: .5rem;
}
.mou-name {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.02rem;
  line-height: 1.3;
  color: var(--text-strong);
  margin: 0 0 .7rem;
}
.mou-meta {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  font-size: .82rem;
  color: var(--text-muted);
  margin-bottom: .85rem;
}
.mou-meta .k {
  font-family: "IBM Plex Mono", monospace;
  font-size: .6rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-right: .35rem;
}
.mou-meta .v { color: var(--text-strong); }
/* "Recorded as" is the register's own wording, so it is often a long string with
   a city and a country in it. It gets the full width of the card and wraps;
   `overflow-wrap` because a few of them are single unbroken tokens. */
.mou-meta .mou-rec {
  padding-top: .45rem;
  border-top: 1px dashed var(--line);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.mou-meta .mou-rec .v { font-weight: 500; }
.mou-meta .mou-rec .side {
  color: var(--text-muted);
  white-space: nowrap;
}

/* --- §24.5 Status pill --------------------------------------------------- */
.mou-status {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .28rem .7rem;
  border-radius: var(--radius-pill);
  font-size: .74rem;
  font-weight: 600;
  margin-top: auto;
}
.mou-status::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}
.mou-status.is-active {
  background: var(--success-bg);
  color: var(--success-text);
}
.mou-status.is-process {
  background: var(--warning-bg);
  color: var(--warning-text);
}
.mou-visit {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin-top: .85rem;
  font-size: .84rem;
  font-weight: 600;
  color: var(--maroon);
  text-decoration: none;
}
.mou-visit:hover { text-decoration: underline; }
.mou-visit svg { width: 13px; height: 13px; }
.mou-nolink {
  margin-top: .85rem;
  font-size: .78rem;
  color: var(--text-muted);
}

/* --- §24.6 Empty state + the full register ------------------------------- */
.mou-empty {
  display: none;
  border: 1px dashed var(--line-strong);
  border-radius: 14px;
  padding: 2rem 1.25rem;
  text-align: center;
  color: var(--text-muted);
}
.mou-empty.show { display: block; }
/* The `.mou-register` <details> that used to hold the legacy five-column table
   below the grid was removed with the table itself on 2026-07-31, on
   instruction — the cards carry the register now. */

@media (min-width: 576px) {
  .mou-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 575.98px) {
  .mou-grid { grid-template-columns: 1fr; }
  .mou-chips { flex-wrap: nowrap; overflow-x: auto; padding-bottom: .3rem; }
  .mou-chip { flex: 0 0 auto; }
}

/* ===========================================================================
   §25 — 360° VIRTUAL CAMPUS TOUR  (body.page-campus-tour — rakmhsu-campus-tour.php)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The page carries two media blocks — the 360° tour and the campus video — and
   on 2026-08-06 both were laid out in the SAME pattern as the homepage's
   #home-tour section: copy in a col-lg-5 (eyebrow, display heading, lead, and
   for the tour one solid brand button), media in a col-lg-7, vertically
   centred. So the tour is introduced identically wherever a visitor meets it,
   and the two blocks read as siblings instead of an accidental stack.

   What that replaced: a full-width card whose tinted header strip held the
   title, the intent and a small outline button pinned to its right — the call
   to action sat as far as possible from the sentence that motivates it, and the
   heading got whatever width was left over. The strip is gone; its `#5A514C`
   inline paragraph colour went with it (--neutral-700 now, no page-level hex).

   The tour's media is now the shared launch tile (includes/tour-launch.php,
   styled in §33), the same one the homepage uses. The Kuula <iframe> that used
   to sit here — and every rule that sized it (`.tour360`, `.tour360-frame`, its
   4:3 / 3:4 aspects and min-height floors, which existed because Kuula lays its
   title strip and `thumbs=1` rail INSIDE the frame and a 16:9 box clipped them)
   — was removed with it. What remains here is only what the tile does NOT do:
   this page's block rhythm and the two headings.

   Token-driven throughout — no hex here (docs/BRAND-COLOR-STANDARD.md).
   ========================================================================= */

/* --- §25.1 Block rhythm and copy ------------------------------------------ */
/* One rule for the gap between the blocks rather than a utility repeated on
   each — a third block would otherwise have to remember to carry it. */
.tour-block + .tour-block { margin-top: 3.25rem; }

/* Shared by BOTH blocks' headings and leads, so the tour and the video are
   titled and introduced identically (hard rule 3). */
.tour360-title {
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(1.45rem, 2.6vw, 1.85rem);
  line-height: 1.2;
  margin: 0;
}
.tour360-lead {
  max-width: 46ch;
  font-size: 1.04rem;
  line-height: 1.65;
  color: var(--neutral-700);
}

@media (max-width: 575.98px) {
  .tour-block + .tour-block { margin-top: 2.5rem; }
  .tour-block .btn { width: 100%; justify-content: center; }
}

/* ===========================================================================
   §26 — ACCREDITATION STATUS  (body.page-accreditation — accreditation-status.php)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The page used to be three bordered tables — 26 rows across four to five
   columns, with rowspans, nested <ul>s and a repeated header row halfway down
   the second table. On a phone that is a horizontal scroller, and even on a
   desktop it makes the reader hold "which column was the renewal date again?"
   in their head for 26 rows.

   The redesign (2026-07-31) says the same thing in two card layers:
     1. WHO ACCREDITS US — one card per accrediting authority, carrying the
        thing the tables never showed: the body's own mark, who they are, what
        they cover here, and a link to their site and to their public register
        so a reader can check the claim at the source.
     2. THE REGISTER — one card per accreditation, so each row's programme,
        dates and status arrive together instead of spread across a table row.

   Both grids reuse §24's vocabulary (plate / body / status pill / chips)
   deliberately: the MoU register two clicks away looks the same, and a reader
   who has learnt one has learnt both. The toolbar, the result line and the
   empty state are not re-declared at all — the page reuses §24's classes, so
   the two registers cannot drift apart.

   Token-driven throughout — no hex here (docs/BRAND-COLOR-STANDARD.md).
   ========================================================================= */

/* --- §26.1 Figures strip -------------------------------------------------- */
.acc-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .75rem;
  margin-bottom: 1.5rem;
}
.acc-stats .s {
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface-card);
  padding: 1.05rem 1.15rem;
}
.acc-stats .s .n {
  display: block;
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.75rem;
  line-height: 1.1;
  color: var(--maroon);
}
.acc-stats .s .k {
  display: block;
  margin-top: .3rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .64rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* --- §26.2 Section headings inside the page ------------------------------ */
.acc-head {
  margin: 2.6rem 0 .5rem;
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.5rem;
  line-height: 1.25;
  color: var(--text-strong);
}
.acc-head-note {
  margin: 0 0 1.15rem;
  max-width: 70ch;
  font-size: .93rem;
  line-height: 1.6;
  color: var(--text-muted);
}

/* --- §26.3 Authority cards ------------------------------------------------
   Wider minimum than the register grid (280 vs 272): an authority card carries
   a sentence of prose and two links, and at 240 the longer bodies
   ("Accreditation Commission for Education in Nursing") set to four lines. */
.acc-auth-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1.15rem;
  margin-bottom: .5rem;
}
.acc-auth {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-card);
  overflow: hidden;
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.acc-auth:hover {
  border-color: rgba(var(--maroon-rgb), .35);
  box-shadow: 0 12px 30px rgba(var(--charcoal-rgb), .08);
  transform: translateY(-2px);
}

/* The logo plate. Always the light paper surface and always `contain` — what
   lands here ranges from a wide bilingual ministry wordmark to a square gold
   seal, and another body's mark is not ours to crop, pad or recolour. */
.acc-auth .plate {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 128px, not 110: the ministry's mark is a 4:3 crest-over-bilingual-wordmark
     and is height-constrained, so at a 74px cap it set about half the size of
     the wordmarks beside it and read as the least important body on the page —
     which is the opposite of true. The taller plate lets every mark reach 88px
     and evens them out. */
  height: 128px;
  padding: 1rem 1.15rem;
  background: var(--paper);
  border-bottom: 1px solid var(--line-soft);
}
.acc-auth .plate img {
  max-width: 100%;
  max-height: 88px;
  width: auto;
  height: auto;
  object-fit: contain;
}
/* --- The live-mark plate (ADCI Smart Logo) --------------------------------
   ADCI's badge is supplied at a fixed 181x200 and their installation guide says
   organisations "are not authorised to resize the logo without permission". So
   this card's plate cannot be the 128px box the other five use: it releases the
   fixed height and lets the badge stand at its authorised size. The row grows
   with it — CSS grid stretches the siblings, which gain trailing space in their
   body rather than a cropped mark, and that is the correct trade: a compliance
   mark we may not resize outranks an even plate line.

   Nothing here styles the badge itself. Everything below the `.plate` selector
   positions the BOX; ADCI's own inline styles size what is inside it, verbatim
   (includes/adci-smart-logo.php). Do not add rules that reach into the iframe.

   No fallback artwork sits behind it, deliberately — the badge's frame is
   transparent, so anything behind would show through the live mark. On a domain
   ADCI has not registered (localhost, staging) this box is expected to be blank;
   the card's name, country, role and links carry the meaning either way. */
.acc-auth .plate.is-livemark {
  height: auto;
  min-height: 128px;
  flex-direction: column;
  gap: .55rem;
  padding: 2.1rem 1.15rem 1.15rem;   /* top clears the pinned .acc-scope badge */
}
.acc-livemark-note {
  max-width: 26ch;
  color: var(--neutral-700);
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .6rem;
  line-height: 1.5;
  letter-spacing: .04em;
  text-align: center;
  text-wrap: balance;
}

/* Fallback when we hold no mark for a body: its acronym, set in the display
   serif on the brand tint. Deliberately NOT a generic certificate icon — an
   acronym is a fact about the body, an icon would be decoration standing in for
   artwork we do not have. Unused today; all six bodies have their own mark. */
.acc-nomark {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 64px;
  height: 64px;
  padding: 0 .9rem;
  border-radius: 14px;
  background: var(--surface-primary-tint);
  color: var(--maroon);
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.2rem;
  letter-spacing: .02em;
  text-align: center;
}
.acc-by .mark .acc-nomark {
  min-width: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border-radius: 7px;
  font-size: .6rem;
}

/* Scope badge, pinned to the plate so the eye picks up National vs
   International before it reads the body's name. */
.acc-scope {
  position: absolute;
  top: .6rem;
  left: .7rem;
  padding: .2rem .55rem;
  border-radius: var(--radius-pill);
  background: var(--surface-primary-tint);
  color: var(--maroon);
  font-family: "IBM Plex Mono", monospace;
  font-size: .58rem;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.acc-auth .body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1.05rem 1.15rem 1.15rem;
}
.acc-auth-name {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.02rem;
  line-height: 1.3;
  color: var(--text-strong);
  margin: 0 0 .15rem;
}
/* The acronym and the country on one mono line — the two facts a reader scans
   for when checking "is this the body I think it is?". */
.acc-auth-sub {
  display: block;
  margin-bottom: .6rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .62rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.acc-auth-role {
  margin: 0 0 .85rem;
  font-size: .85rem;
  line-height: 1.5;
  color: var(--text-muted);
}
/* Count badge — how much of the register this body is responsible for. It was a
   button that filtered the grid below until 2026-07-31, when the "Accrediting
   body" chip row it drove was removed; with no chip left to un-press, filtering
   from here would have been a one-way door, so it is now a plain statement of
   fact. Deliberately NOT styled as pressable: no cursor, no hover, no focus
   ring — nothing that promises an interaction it no longer has. */
.acc-auth-count {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin: 0 0 .9rem;
  padding: .3rem .75rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-strong);
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-size: .78rem;
  font-weight: 600;
  line-height: 1.2;
}
.acc-auth-count svg { width: 12px; height: 12px; flex: 0 0 auto; color: var(--maroon); }

/* Link row sits at the bottom of every card regardless of how long the role
   sentence ran, so the links line up across the grid. */
.acc-auth-links {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  margin-top: auto;
  padding-top: .35rem;
  border-top: 1px dashed var(--line);
}

/* --- §26.4 The register grid and its card -------------------------------- */
.acc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(272px, 1fr));
  gap: 1.15rem;
}
.acc-card {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-card);
  padding: 1.15rem;
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.acc-card:hover {
  border-color: rgba(var(--maroon-rgb), .35);
  box-shadow: 0 12px 30px rgba(var(--charcoal-rgb), .08);
  transform: translateY(-2px);
}
.acc-card.is-hidden { display: none; }

/* The issuing body, at the TOP of the card — the question a reader arrives
   with is "who says so?", so the mark answers it before the programme name.
   A fixed 42px plate keeps the names left-aligned down the column whatever the
   aspect ratio of the mark inside it. */
.acc-by {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding-bottom: .8rem;
  margin-bottom: .8rem;
  border-bottom: 1px solid var(--line-soft);
}
.acc-by .mark {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  padding: 3px;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
  background: var(--paper);
}
.acc-by .mark img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}
.acc-by .who {
  min-width: 0;
  font-family: "IBM Plex Mono", monospace;
  font-size: .64rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--maroon);
  line-height: 1.35;
}
.acc-by .who .full {
  display: block;
  margin-top: .1rem;
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-size: .72rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.acc-tags {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
  margin-bottom: .5rem;
}
.acc-tag {
  padding: .18rem .5rem;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-family: "IBM Plex Mono", monospace;
  font-size: .57rem;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.acc-name {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.02rem;
  line-height: 1.3;
  color: var(--text-strong);
  margin: 0 0 .5rem;
}
.acc-name .fam {
  display: block;
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: .15rem;
}

/* Concentrations. The legacy table nested a <ul> inside a table cell; here the
   list gets its own labelled box so it is not mistaken for the programme title
   running on. */
.acc-conc {
  margin: 0 0 .7rem;
  padding: .5rem .7rem;
  border-radius: 10px;
  background: var(--surface-sunken);
}
.acc-conc .k {
  display: block;
  margin-bottom: .25rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .57rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.acc-conc ul {
  margin: 0;
  padding-left: 1.05rem;
  font-size: .8rem;
  line-height: 1.5;
  color: var(--text-strong);
}

/* The two dates, side by side and labelled — the pairing the table forced the
   reader to reconstruct from column headers twenty rows up. */
.acc-dates {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .5rem;
  margin-bottom: .75rem;
}
.acc-dates > div {
  padding: .5rem .6rem;
  border: 1px solid var(--line-soft);
  border-radius: 10px;
}
.acc-dates .k {
  display: block;
  margin-bottom: .2rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .55rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.acc-dates .v {
  display: block;
  font-size: .8rem;
  line-height: 1.35;
  color: var(--text-strong);
}
/* "(Initial Accreditation)" and the like — the register's own qualifier, kept
   with the date it qualifies but set down so it does not read as part of it. */
.acc-dates .v small {
  display: block;
  font-size: .68rem;
  color: var(--text-muted);
}

/* The register's own footnote on a row ("Joint Accreditation with ACEN",
   "Accreditation Canada Status - DIAMOND"). Verbatim, and marked as a note. */
.acc-note {
  display: flex;
  align-items: flex-start;
  gap: .4rem;
  margin: 0 0 .75rem;
  font-size: .78rem;
  line-height: 1.45;
  color: var(--text-muted);
}
.acc-note svg {
  width: 13px;
  height: 13px;
  margin-top: .18rem;
  flex: 0 0 auto;
  color: var(--maroon);
}

.acc-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin-top: auto;
  padding-top: .3rem;
}

/* --- §26.5 Status pill --------------------------------------------------- */
.acc-status {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .28rem .7rem;
  border-radius: var(--radius-pill);
  font-size: .74rem;
  font-weight: 600;
}
.acc-status::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}
.acc-status.is-active {
  background: var(--success-bg);
  color: var(--success-text);
}
.acc-status.is-process {
  background: var(--warning-bg);
  color: var(--warning-text);
}

/* --- §26.6 Links · same affordance as §24's "Visit website" -------------- */
.acc-link {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-size: .82rem;
  font-weight: 600;
  color: var(--maroon);
  text-decoration: none;
}
.acc-link:hover { text-decoration: underline; }
.acc-link svg { width: 13px; height: 13px; flex: 0 0 auto; }
.acc-link.is-quiet {
  font-weight: 500;
  color: var(--text-muted);
}
.acc-link.is-quiet:hover { color: var(--maroon); }
.acc-nolink {
  font-size: .76rem;
  color: var(--text-muted);
}

/* --- §26.7 Toolbar, count and empty state -------------------------------
   NOT re-declared. The page reuses §24's .mou-toolbar / .mou-search /
   .mou-tier / .mou-chip / .mou-count / .mou-empty verbatim, so the two
   registers stay identical by construction rather than by discipline. */

/* --- §26.8 Rankings strip -------------------------------------------------
   Moved here from an inline <style id="rank-strip-css"> in the page on
   2026-07-31 (hard rule 4: the override layer is brand.css, not the page).
   The one behavioural change is --radius-pill in place of the literal 999px. */
.rank-strip .rank-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: .5rem;
  padding: 1.35rem 1rem;
  height: 100%;
}
.rank-strip .rank-emblem {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 3rem;
  margin-bottom: .15rem;
}
.rank-strip .rank-badge {
  min-width: 2.7rem;
  height: 2.7rem;
  padding: 0 .7rem;
  border-radius: var(--radius-pill);
  background: var(--ivory);
  border: 1px solid var(--line);
  font-family: "IBM Plex Mono", monospace;
  font-weight: 700;
  font-size: 1rem;
  color: var(--maroon);
  letter-spacing: .04em;
}
.rank-strip .rank-logo {
  max-height: 3rem;
  max-width: 100%;
  width: auto;
  object-fit: contain;
}
.rank-strip .rank-value {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 700;
  font-size: 1.18rem;
  line-height: 1.15;
  color: var(--maroon);
}
.rank-strip .rank-stars {
  color: var(--gold);
  letter-spacing: .1em;
  font-size: 1.05rem;
  line-height: 1;
}
.rank-strip .rank-label {
  font-size: .8rem;
  color: var(--text-muted);
  line-height: 1.35;
}
/* Two positions on ONE card — the Arab Ranking awards the University both 56th
   across the Arab region and 5th in the UAE. Mirrors .rb-fig in the homepage
   recognition bar, and exists for the same reason: a rank figure must never sit
   on a card of its own with no awarding body named. Two such orphan cards
   ("Ranked 13", "Ranked 5") were merged into their own bodies' cards on
   2026-08-10, following the homepage, which did it on 2026-08-05
   (docs/homepage-build-notes.md). */
.rank-strip .rank-figs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: .1rem .9rem;
}
.rank-strip .rank-fig {
  display: inline-flex;
  align-items: baseline;
  gap: .3rem;
}
.rank-strip .rank-fig__n {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 700;
  font-size: 1.18rem;
  line-height: 1.15;
  color: var(--maroon);
}
.rank-strip .rank-fig__l {
  font-size: .78rem;
  color: var(--text-muted);
  line-height: 1.2;
}

@media (min-width: 576px) {
  .acc-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 575.98px) {
  .acc-auth-grid,
  .acc-grid { grid-template-columns: 1fr; }
  /* Two date boxes side by side get about nine characters each on a 320px
     phone, which breaks "28 November 2025 (Initial Accreditation)" into a
     column of single words. Stack them. */
  .acc-dates { grid-template-columns: 1fr; }
}

/* ===========================================================================
   §27 — GOVERNANCE ROSTER  (students-governance.php)
   ---------------------------------------------------------------------------
   Replaces ten stacked Bootstrap `.table.table-bordered` grids (2026-08-01).
   The page is a roster — ten bodies, each mapping a college to the student who
   sits for it — and a bordered table is the wrong instrument for it twice over:
   ten identical grey grids read as a spreadsheet dump rather than as the
   University's student representation, and a two-column table on a 360px phone
   either scrolls sideways or crushes a long Arabic-transliterated name into a
   column three words wide. It is a description list, and it is now marked up as
   one: the college code is the term, the student is the description, and a body
   with two representatives is one `<dt>` with two `<dd>`s — which is exactly
   what a `<dl>` is for, and what the old `<br>` inside a table cell was faking.

   The cards sit two-up from 768px, so ten bodies occupy five rows instead of
   ten full-width slabs. Nothing here sets a colour literal; every value is a
   semantic token, so the roster follows the identity automatically.
   ======================================================================== */
.gov-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1.25rem;
  /* Cards size to their own roster. Stretched to equal height, a two-name body
     sitting beside a four-name one shows a third of a card of empty surface,
     which reads as a mistake rather than as rhythm. */
  align-items: start;
}
@media (min-width: 768px) {
  .gov-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* .card-soft (theme.css) supplies the surface, radius and reveal behaviour —
   this only adds the padding and the full-height stretch the grid needs. */
.gov-card {
  padding: 1.4rem 1.5rem 1.25rem;
}
.gov-card__t {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.12rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--accent);
  margin: 0 0 .9rem;
}

.gov-roster { margin: 0; }

/* One representative row. Stacked below 480px — at that width a fixed label
   column would leave a name four words wide — and a two-column grid above it,
   where the mono college code acts as the rail the names align against. */
.gov-row {
  padding: .6rem 0;
  border-top: 1px solid var(--border-soft);
}
.gov-row:first-child {
  border-top: 0;
  padding-top: 0;
}
.gov-row dt {
  font-family: "IBM Plex Mono", monospace;
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: .15rem;
}
.gov-row dd {
  margin: 0;
  color: var(--text-strong);
  font-size: .97rem;
  line-height: 1.45;
}
.gov-row dd + dd { margin-top: .2rem; }

@media (min-width: 480px) {
  .gov-row {
    display: grid;
    grid-template-columns: 7.25rem minmax(0, 1fr);
    gap: .2rem 1rem;
    align-items: baseline;
  }
  .gov-row dt { grid-column: 1; margin-bottom: 0; }
  .gov-row dd { grid-column: 2; }
}

@media print {
  .gov-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .6rem; }
  .gov-card { break-inside: avoid; box-shadow: none; padding: .6rem .7rem; }
}

/* ===========================================================================
   §28 — PROGRAM CARD TITLE  (college-*.php, colleges.php, admission-programs)
   ---------------------------------------------------------------------------
   Added 2026-08-04. The "Programs Offered" cards showed a photograph and then
   went straight into the list of entry routes, so the card never actually named
   the program it was for — the name was only in the alt text and in the routes
   beneath. `.prog-card-title` is that name, and it has to out-rank the route
   list directly under it, which is itself bold serif at ~1.02rem. Hence: larger,
   heavier, brand-coloured, and separated by a short accent rule so the eye
   reads title → routes rather than one run-on list.
   Colour comes from the token, never a hex (docs/BRAND-COLOR-STANDARD.md).
   =========================================================================== */
.prog-card-title {
  font-size: 1.3rem;
  line-height: 1.25;
  letter-spacing: -.01em;
  font-weight: 700;
  padding-bottom: .6rem;
  position: relative;
}
.prog-card-title::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 34px;
  height: 3px;
  border-radius: 2px;
  background: var(--gold, var(--maroon));
}
.prog-card-title a { color: inherit; }
.prog-card-title a:hover,
.prog-card-title a:focus-visible { color: var(--primary-hover); text-decoration: underline; }

/* ===========================================================================
   §29 — QAIE ORGANIZATIONAL CHART  (qaie.php)
   ---------------------------------------------------------------------------
   Added 2026-08-04. The University supplied this chart as a PNG with every unit
   name and function rendered into the pixels — the same fault §28 was written to
   remove from the programme cards. Rebuilt here as a nested list so the names
   are real text: readable by a screen reader, findable by a search engine,
   correctable without redrawing, and safe at any width.

   Structure: one head box (Dean QAIE) over a responsive grid of unit boxes,
   each optionally carrying its list of functions. The connector is a short rule
   under the head plus a left border on each unit — enough to read as a
   hierarchy without SVG or absolute positioning that would break on a phone.
   Colour comes from tokens only (docs/BRAND-COLOR-STANDARD.md); no hex here.
   =========================================================================== */
.qaie-chart { margin: 0 0 .5rem; }

.qaie-chart-head {
  display: block;
  margin: 0 auto 1.75rem;
  max-width: 320px;
  padding: .85rem 1.25rem;
  text-align: center;
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.15rem;
  font-weight: 700;
  color: var(--paper);
  background: var(--maroon);
  border-radius: 12px;
  position: relative;
}
/* the drop line from the head down to the unit row */
.qaie-chart-head::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -1.75rem;
  width: 2px;
  height: 1.75rem;
  background: var(--border-strong);
  transform: translateX(-1px);
}

.qaie-units {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

.qaie-unit {
  border: 1px solid var(--border);
  border-top: 3px solid var(--maroon);
  border-radius: 12px;
  padding: 1rem 1.1rem;
  background: var(--paper);
}

.qaie-unit-name {
  display: block;
  font-family: "Fraunces", Georgia, serif;
  font-weight: 700;
  font-size: 1.02rem;
  line-height: 1.3;
  color: var(--maroon);
}

.qaie-fn {
  list-style: none;
  margin: .75rem 0 0;
  padding: .75rem 0 0 0;
  border-top: 1px solid var(--line-soft);
}
.qaie-fn li {
  position: relative;
  padding-left: .95rem;
  margin-bottom: .35rem;
  font-size: .92rem;
  line-height: 1.45;
  color: var(--neutral-700);
}
.qaie-fn li:last-child { margin-bottom: 0; }
.qaie-fn li::before {
  content: "";
  position: absolute;
  left: 0;
  top: .6em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--gold);
}

/* ===========================================================================
   §30 — COLLEGE "WELCOME" PHOTO WASH  (college-*.php — the Welcome + stats band)
   ---------------------------------------------------------------------------
   The four college welcome photographs shipped with a colour wash BAKED INTO the
   JPEG, one per college, from the retired per-college accent system: medicine
   red, dentistry blue, pharmacy green, nursing gold. Those hues were withdrawn on
   2026-07-28 (docs/BRAND-COLOR-STANDARD.md — all four colleges now share the one
   university accent #36298B), and a hue baked into a photograph is invisible to
   `tools/check-brand-color.sh`, which reads source and not pixels. So the gate
   passed while the page still showed a retired colour.

   All four photographs are now published clean — no wash in any file, each one
   re-exported from an original the University supplied — and the brand treatment
   is applied HERE instead, to all four, as a token-driven overlay:
     · it is one shared rule, not four hand-tinted files;
     · it follows `--accent`, so the next identity change repaints it for free;
     · it can be removed, re-tuned or lifted to the other three colleges without
       re-exporting a photograph.
   The wash is deliberately light (.20 → .06, top to bottom) — enough to read as
   brand treatment, not enough to hide faces or the room. `pointer-events: none`
   keeps it out of the way of anything interactive underneath.

   ⛔ THE CLASS ASSUMES A CLEAN PHOTOGRAPH. Adding `.college-welcome` to a card
   whose image still has a wash baked in tints it twice. If a college welcome
   photo is ever replaced, publish it in natural colours through
   `tools/prepare-image.php` — never hand-tinted.
   =========================================================================== */
.college-welcome { position: relative; }
.college-welcome::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg,
              rgba(var(--maroon-rgb), .20) 0%,
              rgba(var(--maroon-rgb), .06) 100%);
}
@media print {
  .college-welcome::after { content: none; }
}

/* ===========================================================================
   §31 — PEOPLE CARDS  (includes/people.php — board-of-trustees.php,
   advisory-board.php, and any governance body added after them)
   ---------------------------------------------------------------------------
   The governance pages present the same object — a person, a photograph, a name,
   sometimes a designation — so the markup lives in ONE include (includes/people.php,
   hard rule 3) and its appearance lives here. Board of Trustees was rebuilt into it
   on 5 Aug; Advisory Board followed the same day, out of a legacy Sr-No / Name /
   Designation / Photo table.

   What this section fixes, and why each rule is the way it is:
     1. ONE FRAME FOR EVERYONE. The Trustees page drew the Chairman with `img-fluid`
        + `height:100%` + `min-height:240px` and the members in a `1/1` box — one
        photograph in a different shape from the other six. `.person-frame` is now
        the only frame any of them is drawn in. It is SQUARE, not the taller portrait
        crop a card grid would normally take, for a reason that cannot be designed
        around: every supplied original is 300x300, a 4:5 crop would throw away 20%
        of the width across every face, and `tools/prepare-image.php` refuses to
        enlarge. If larger originals are ever supplied, change the aspect-ratio HERE
        and re-cut them — this is the only place the shape is stated.
     2. A NEUTRAL SURFACE BEHIND THE PHOTOGRAPH. The portraits carry different studio
        backgrounds (one pale lavender, several near-black), which no CSS can
        reconcile — but a shared frame, radius and inset hairline stop a grid of them
        reading as unrelated cut-outs. `--surface-media` / `--border-soft` from
        tokens.css; nothing new is introduced.
     3. NO ROLE ON A CARD. Every Trustees member card used to repeat "Member" under
        the name, directly below a "MEMBERS" eyebrow; it distinguished nobody from
        anybody. `person_card()` prints no role at all — the grid heading carries it.
        The role belongs to `person_feature()`, where it names the one person whose
        position differs from everyone else's on the page.
     4. NO HOVER LIFT. The theme lifts `.card-soft` 4px on hover. A card of a person
        is not a link, and a lift promises a destination that is not there.
   =========================================================================== */
.person-frame {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--surface-media);
  box-shadow: inset 0 0 0 1px var(--border-soft);
}
.person-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* No photograph on file -> the University seal, contained and inset on the frame's
   own surface (see includes/people.php for why it is the seal and not a stock
   portrait or an empty box). `contain` because a seal must not be cropped. */
.person-frame--seal { display: grid; place-items: center; padding: 18%; }
.person-frame--seal img { object-fit: contain; opacity: .55; }
/* On the Chairman band the frame is a column of the row, so it fills the band's
   height rather than staying square — the portrait is the tallest thing in the row
   only when the text wraps short. */
@media (min-width: 576px) {
  .person-feature .person-frame { aspect-ratio: auto; height: 100%; }
}
.person-role {
  font-size: .7rem;
  letter-spacing: .16em;
  text-transform: uppercase;
}
/* A card of a person is not a link; the theme's 4px card lift would promise a
   destination that is not there. Depth only, as in §20. */
.person-card:hover { transform: none; box-shadow: var(--shadow-1); }

/* ============================================================================
   §32 — Above-the-fold text must never wait for JavaScript
   ----------------------------------------------------------------------------
   theme.css:467-486 puts `section h1` in the scroll-reveal net: opacity 0 and
   translateY(26px) while <html> is .reveal-ready and not yet .reveal-all. That
   is right for content further down the page — it is what makes the staggered
   scroll-in read — but an h1 is the page's MAIN HEADING and is above the fold by
   definition. It cannot be "scrolled into", so the reveal buys nothing there and
   costs first paint: the browser paints the hero image, then holds the headline
   invisible until site.js's observer adds `.in`, or until the 2000 ms failsafe in
   head.php:211 adds `.reveal-all`.

   Measured, not assumed. DebugBear/Lighthouse on the live site (6 Aug 2026,
   emulated Moto G Power): "Page H1 content was hidden using CSS until at least
   3.22 s", against FCP 2.44 s — i.e. the headline was still invisible almost a
   second after first paint. Reproduced locally: with .reveal-all removed the
   hero h1 computes to opacity 0 / translateY(26px) once its 0.7 s transition
   settles. Lighthouse reports this as "Don't hide page content using CSS".

   Specificity is IDENTICAL to theme.css's rule (0,2,2), so this wins purely by
   loading later — brand.css is last (hard rule 4). No !important, and nothing
   theme.css does is re-skinned: the hide rule still governs every other selector
   in its list (.rv, section h2, .card-soft, .stat, …), so the scroll-reveal
   effect is untouched everywhere it actually applies.

   Deliberately h1 ONLY. `section > .eyebrow` sits above the fold on some interior
   pages too and is a candidate, but it was left in the net because it is small,
   decorative, and not what the audit measured — widening this rule without a
   measurement to justify it is how a targeted fix turns into a silent redesign.
   ========================================================================== */
html.reveal-ready:not(.reveal-all) section h1 {
  opacity: 1;
  transform: none;
  transition: none;
  will-change: auto;
}

/* ===========================================================================
   FAQ ACCORDION — brand the open state (2026-08-06)
   ---------------------------------------------------------------------------
   theme.css:298-302 already tints the FAQ accordion, but it stops at the text
   colour, so two things were wrong when a question was opened:

     1. THE CHEVRON WAS BOOTSTRAP BLUE. `.accordion-button::after` is a data-URI
        SVG with the stroke baked into the markup, and Bootstrap swaps it for
        `--bs-accordion-btn-active-icon` on expand — measured as `#052c65`, navy,
        on a page whose every other accent is indigo. No amount of `color:` fixes
        it, because the colour is inside the image.
     2. THE OPEN ROW WAS BARELY DISTINGUISHABLE. Open vs closed was ivory vs
        paper — a ~2% lightness step, which is not a state change a reader can
        see, and the only other signal was the (blue) arrow.

   The chevron is therefore rebuilt from borders rather than recoloured: drawn in
   `currentColor`, it simply inherits whatever the button's own colour already is
   (ink when closed, brand when open) and no hex is introduced anywhere — the
   colour standard keeps its single source (includes/brand-colors.php).

   The open row now also carries the soft brand tint, a brand-coloured border and
   a 3px bar down its leading edge, so the state reads at a glance and from the
   layout rather than from one small arrow. `:has()` is not used — the bar is an
   inset box-shadow on the BUTTON, which needs no parent selector and so needs no
   fallback. Logical `inset-inline-start` keeps it on the correct side if this
   ever renders RTL. */
.faq .accordion-button::after {
  background-image: none;
  width: .55rem;
  height: .55rem;
  flex: 0 0 auto;
  margin-inline-end: .3rem;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: rotate(45deg);
  transform-origin: 60% 60%;
  transition: transform .2s ease;
}
.faq .accordion-button:not(.collapsed)::after { transform: rotate(-135deg); }

.faq .accordion-button:not(.collapsed) {
  background: var(--primary-soft);
  font-weight: 600;
  box-shadow: inset 3px 0 0 0 var(--maroon);
}
.faq .accordion-item:has(> h3 > .accordion-button:not(.collapsed)),
.faq .accordion-item:has(> .accordion-header > .accordion-button:not(.collapsed)) {
  border-color: var(--maroon);
}

/* The focus ring theme.css removes (`box-shadow:none`) is put back as a visible
   outline — a keyboard user has to be able to see which question they are on,
   and the open-state box-shadow above now occupies the shadow slot anyway. */
.faq .accordion-button:focus-visible {
  outline: 3px solid var(--maroon);
  outline-offset: -3px;
}

@media (prefers-reduced-motion: reduce) {
  .faq .accordion-button::after { transition: none; }
}

/* ===========================================================================
   FAQ CATEGORY FILTER + "FULL DETAILS" LINKS (2026-08-06)
   ---------------------------------------------------------------------------
   theme.css:209 gives `.chip` a base pill and nothing else — no `:hover`, and
   no rule for `.active` at all. The FAQ filter is built from chips and toggles
   that exact class, so the selected category was styled identically to the nine
   it had just filtered out: the control told you nothing about its own state,
   and the only clue you had filtered was that the list got shorter.
   `aria-pressed` was correct throughout, so this was purely a visual gap — it
   is matched on as well as `.active`, so the two can never disagree.

   Selected = filled brand indigo. Hover/focus = brand border over the soft
   tint. Both are state changes you can see across a nine-chip row, which a
   border-colour-only treatment is not. */
.faq-filter-btn {
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.faq-filter-btn:hover {
  background: var(--primary-soft);
  border-color: var(--maroon);
  color: var(--maroon);
}
.faq-filter-btn:focus-visible {
  outline: 3px solid var(--maroon);
  outline-offset: 2px;
}
.faq-filter-btn.active,
.faq-filter-btn[aria-pressed="true"] {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--paper);
  font-weight: 600;
}
/* Hovering the chip that is already selected must not throw it back to the
   unselected look — without this the `:hover` rule above wins on source order
   and the active chip appears to deselect under the cursor. */
.faq-filter-btn.active:hover,
.faq-filter-btn[aria-pressed="true"]:hover {
  background: var(--primary-hover);
  border-color: var(--primary-hover);
  color: var(--paper);
}

/* The per-category "Full details:" row. Deliberately a LINK chip, visually
   distinct from the filter chips directly above it (which are buttons that do
   something to this page) — outlined in brand rather than neutral, so a reader
   can tell "this takes me somewhere" from "this filters what I am reading". */
.faq-more-label {
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .72rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--neutral-700);
}
.faq-more-link {
  text-decoration: none;
  border-color: var(--border-primary-soft, var(--line));
  color: var(--maroon);
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.faq-more-link:hover,
.faq-more-link:focus-visible {
  background: var(--primary-soft);
  border-color: var(--maroon);
  color: var(--maroon);
}
.faq-more-link:focus-visible { outline: 3px solid var(--maroon); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .faq-filter-btn, .faq-more-link { transition: none; }
}

/* ===========================================================================
   §33 — 360° VIRTUAL CAMPUS TOUR LAUNCH TILE  (includes/tour-launch.php)
   Used by index.php (#home-tour) and rakmhsu-campus-tour.php. One include, one
   rule set — the tile cannot drift between the two pages (hard rule 3). §25 holds
   only what is specific to the campus tour page: its block rhythm and headings.
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The homepage used to embed the campus-tour VIDEO in this slot. On 2026-08-06
   it was replaced by a still of the 360° tour that LAUNCHES the Kuula
   collection (TOUR_360_URL) — so the block is no longer a player but a link,
   and it has to say so before it is clicked. A bare photograph in a card reads
   as decoration; people do not click decoration.

   Three affordances, none of them animated on their own, do that job:
     · a badge naming what this is ("Interactive · 360°"),
     · a centred cue disc carrying the panorama mark — the universal
       "this opens something" target, placed where the eye lands,
     · a footer bar stating the action and the consequence ("opens in a new
       tab"), over a scrim so the white type survives whatever the photograph
       does underneath it.

   The whole tile is ONE <a>. Not a card with a button in it: a 16:9 photograph
   with a small button inside it gives a large dead zone that looks clickable
   and is not. Everything inside is therefore an inline <span> — an <a> may not
   contain block-level flow content, and `display:` is set here in CSS instead.

   The 360° panorama is NOT embedded on either page any more. Beyond the layout
   consistency the tile buys, that also removes a real defect: a panorama is a
   drag surface inside a scroll surface, and on a phone the first swipe was
   captured by the panorama, so the page stopped scrolling. The tour now opens
   in its own tab, where the whole viewport is the drag surface. If it is ever
   wanted inline again, the <iframe> and the frame sizing live in git history at
   §25 — it is not something to rebuild from scratch.

   Token-driven throughout — no hex here (docs/BRAND-COLOR-STANDARD.md).
   ========================================================================= */

/* --- §33.1 The tile ------------------------------------------------------- */
.tour-launch {
  display: block;
  padding: .5rem;                     /* the white card frame the video had */
  overflow: hidden;
  text-decoration: none;
  color: inherit;
}
@media (min-width: 768px) { .tour-launch { padding: .75rem; } }

.tour-launch-media {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;               /* matches the 720x405 source exactly */
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface-primary-dark);
}
.tour-launch-media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .5s cubic-bezier(.16,.84,.44,1);
}
.tour-launch:hover .tour-launch-media img,
.tour-launch:focus-visible .tour-launch-media img { transform: scale(1.045); }

/* The scrim is the LAST child in paint order (::after), so every overlay below
   claims a stacking position above it rather than the other way round. */
.tour-launch-media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top,
              rgba(var(--maroon-rich-rgb), .86) 0%,
              rgba(var(--maroon-rich-rgb), .30) 26%,
              rgba(var(--maroon-rich-rgb), 0)   52%);
}

/* --- §33.2 Badge ---------------------------------------------------------- */
.tour-launch-badge {
  position: absolute;
  z-index: 2;
  top: .8rem;
  inset-inline-start: .8rem;
  display: inline-flex;
  align-items: center;
  padding: .34rem .68rem;
  border-radius: 999px;
  background: rgba(var(--maroon-rgb), .94);
  color: var(--paper);
  font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1;
}

/* --- §33.3 Centre cue ----------------------------------------------------- */
.tour-launch-cue {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 76px;
  height: 76px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(var(--paper-rgb), .93);
  color: var(--maroon);
  box-shadow: 0 0 0 8px rgba(var(--paper-rgb), .17),
              0 12px 30px rgba(var(--maroon-rich-rgb), .38);
  transition: transform .3s cubic-bezier(.16,.84,.44,1), box-shadow .3s ease;
}
.tour-launch:hover .tour-launch-cue,
.tour-launch:focus-visible .tour-launch-cue {
  transform: translate(-50%, -50%) scale(1.09);
  box-shadow: 0 0 0 12px rgba(var(--paper-rgb), .22),
              0 14px 34px rgba(var(--maroon-rich-rgb), .44);
}

/* --- §33.4 Footer bar ----------------------------------------------------- */
.tour-launch-foot {
  position: absolute;
  z-index: 2;
  inset-inline: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: .15rem;
  padding: 1rem 1.15rem;
  color: var(--paper);
}
.tour-launch-title {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.18rem;
  font-weight: 600;
  line-height: 1.25;
}
.tour-launch-hint {
  font-size: .84rem;
  color: rgba(var(--paper-rgb), .84);
}

/* theme.css strips the focus ring from links; a link this large must keep one. */
.tour-launch:focus-visible {
  outline: 3px solid var(--maroon);
  outline-offset: 3px;
}

@media (max-width: 575.98px) {
  .tour-launch-cue  { width: 60px; height: 60px; }
  .tour-launch-cue svg { width: 24px; height: 24px; }
  .tour-launch-foot { padding: .8rem .9rem; }
  .tour-launch-title { font-size: 1.02rem; }
  .tour-launch-hint  { font-size: .78rem; }
}

@media (prefers-reduced-motion: reduce) {
  .tour-launch-media img,
  .tour-launch-cue { transition: none; }
  .tour-launch:hover .tour-launch-media img { transform: none; }
}

/* ===========================================================================
   §34 — PDF DOWNLOAD CONTROL  (includes/doc-download.php — .doc-dl)
   ---------------------------------------------------------------------------
   WHY THIS EXISTS
   The migrated pages carried assets/img/pdf-download-icon.png — the stock Adobe
   clipart badge: a red "PDF" flag over a green download arrow, on a white
   sheet. Three problems, all of them visible on the page:
     · it is off-palette. That red and that green are not brand colours and
       cannot be made into brand colours, because the colour is baked into the
       pixels — check-brand-color.sh reads source, not images, so it never saw
       them (docs/IMAGE-STANDARD.md says exactly this about composed graphics);
     · it is a raster at a fixed 60px, so it softens on every HiDPI screen and
       cannot follow the surrounding colour token;
     · it costs ~8.6 KB and an extra request on every page that shows it.

   The replacement is an inline line-icon in the University's own colour, drawn
   from the SAME sheet geometry as the hub-icons 'file-*' family so a document
   glyph reads identically wherever it appears, plus a short "PDF" chip so the
   control still names its own file type at a glance.

   ICON ONLY — no file-type word on screen (instruction, 2026-08-07). The glyph
   is a page with a download arrow, which is the affordance; the format is not
   spelled out. It still reaches assistive technology through the anchor's
   aria-label, which doc_download_link() builds from the document's description,
   because an icon-only link has no accessible name of its own.

   Token-driven throughout — no hex here (docs/BRAND-COLOR-STANDARD.md).
   ========================================================================= */

.doc-dl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* A square 44x44 target: the WCAG 2.2 minimum, and the whole tap area in a
     table cell. The 60px image it replaces was comfortably tappable only by
     accident of its size. */
  width: 44px;
  height: 44px;
  border: 1px solid var(--maroon-200);
  border-radius: 50%;
  background: var(--maroon-25);
  color: var(--maroon);
  text-decoration: none;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}

.doc-dl-ico {
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
}

/* The arrow alone moves on hover — the sheet stays put, so the motion reads as
   "this downloads" rather than as the whole control drifting. */
.doc-dl-ico path:nth-child(3),
.doc-dl-ico path:nth-child(4) {
  transition: transform .18s ease;
  transform-origin: 12px 14px;
}

.doc-dl:hover,
.doc-dl:focus-visible {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--text-on-primary);
}

@media (prefers-reduced-motion: no-preference) {
  .doc-dl:hover .doc-dl-ico path:nth-child(3),
  .doc-dl:hover .doc-dl-ico path:nth-child(4) {
    transform: translateY(1.5px);
  }
}

/* theme.css strips the focus ring from links; this one is a button in all but
   name and must keep a visible one. */
.doc-dl:focus-visible {
  outline: 3px solid var(--maroon);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .doc-dl,
  .doc-dl-ico path { transition: none; }
}

/* =============================================================================
 *  §27 · Programme catalogue archive (program-catalogs-archive.php)
 * -----------------------------------------------------------------------------
 *  Deliberately mirrors the MoU register (§24): filter panel → announced count →
 *  card grid. It is the same SHAPE of page — a filtered list of documents — and
 *  inventing a second visual language for it would be arbitrary.
 *
 *  One difference, and it is the reason these are not simply the §24 classes:
 *  the chips here are LINKS, not buttons. Each filter state is its own URL, so a
 *  programme page can link straight into "postgraduate" and the reader arrives
 *  filtered. That also means no JavaScript, and a state that can be bookmarked
 *  and shared. Selection is therefore aria-current, not aria-pressed.
 * ========================================================================== */

.cat-toolbar {
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--surface-card);
  padding: 1.1rem 1.15rem;
  margin-bottom: 1.4rem;
}
.cat-tier-lbl {
  display: block;
  margin-bottom: .45rem;
  font-family: "IBM Plex Mono", monospace;
  font-size: .62rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.cat-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
}
.cat-chip {
  display: inline-flex;
  align-items: center;
  min-height: 38px;
  padding: .4rem .95rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-strong);
  font-family: "Hanken Grotesk", system-ui, sans-serif;
  font-weight: 600;
  font-size: .85rem;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
}
.cat-chip:hover { border-color: rgba(var(--maroon-rgb), .45); color: var(--maroon); }
.cat-chip[aria-current="true"] {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--text-on-primary);
}
.cat-chip:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.cat-count {
  margin: 0 0 1.15rem;
  font-size: .92rem;
  color: var(--text-muted);
}
.cat-count b { color: var(--text-strong); }

.cat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 1.15rem;
}
.cat-card {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: 1.35rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface-card);
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.cat-card:hover {
  border-color: rgba(var(--maroon-rgb), .35);
  box-shadow: 0 12px 30px rgba(var(--charcoal-rgb), .08);
  transform: translateY(-2px);
}
.cat-card .cat-year {
  font-family: "IBM Plex Mono", monospace;
  font-size: .62rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.cat-card .cat-title {
  font-family: "Fraunces", Georgia, serif;
  font-weight: 600;
  font-size: 1.05rem;
  line-height: 1.25;
  color: var(--text-strong);
}
.cat-card .cat-level { font-size: .82rem; color: var(--text-muted); }
/* Pushes the action to the bottom so cards of unequal text still line up. */
.cat-card .cat-act { margin-top: auto; padding-top: .35rem; }

@media (prefers-reduced-motion: reduce) {
  .cat-card { transition: none; }
  .cat-card:hover { transform: none; }
}

/* =============================================================================
 *  §28 · Branded play button over an inline <video>
 * -----------------------------------------------------------------------------
 *  The campus film used to advertise itself with a play button BAKED INTO the
 *  poster JPEG, in a retired red (#B6121B). Two things were wrong with that:
 *  the colour could not be fixed in CSS because it was pixels, and the same
 *  poster is reused as a plain campus photograph in six other places, where a
 *  play button had no business appearing at all — including a breadcrumb banner
 *  and a circular avatar.
 *
 *  The poster is now a clean frame, and the play affordance is this overlay:
 *  markup, in the brand indigo, on top of the video. Same shape and shadow as
 *  the homepage testimonial button (.rak-vt-play-icon, index.php) so the site
 *  has one play button, not two — kept here rather than inline because
 *  apply.php shows the same film and should adopt it.
 * ========================================================================== */

.video-frame { position: relative; }

.video-play {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  z-index: 2;
  transition: opacity .25s ease;
}
.video-play-icon {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--maroon);
  color: var(--paper);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 10px 26px rgba(var(--maroon-rgb), .48), 0 0 0 6px rgba(var(--paper-rgb), .20);
  transition: transform .28s ease, box-shadow .28s ease;
}
/* No optical nudge: the triangle's own path is already balanced about the middle
   of its 24-unit box (its centre of mass sits at x=12), so shifting it right as
   well left it visibly off-centre in the disc. */
.video-play-icon svg { display: block; }
.video-play:hover .video-play-icon,
.video-play:focus-visible .video-play-icon { transform: scale(1.08); }
.video-play:focus-visible { outline: 3px solid var(--gold-bright); outline-offset: -3px; }

/* Hidden once the film is running; returns when it is paused or ends, so the
   button is never a dead overlay sitting on top of a playing video. */
.video-frame.is-playing .video-play { opacity: 0; visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .video-play, .video-play-icon { transition: none; }
  .video-play:hover .video-play-icon { transform: none; }
}

/* ---------------------------------------------------------------------------
   §35 — MEDIA HUB CARDS  (media.php)
   ---------------------------------------------------------------------------
   The three section cards are each one big link, so the arrow at the bottom is
   a <span> rather than its own <a> and never sees a hover of its own. These two
   rules move it when the card is hovered, which is what a reader is pointing at.
   ------------------------------------------------------------------------ */
.media-hub a.card-soft .link-arrow::after { transition: transform .2s ease; }
.media-hub a.card-soft:hover .link-arrow::after { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
  .media-hub a.card-soft:hover .link-arrow::after { transform: none; }
}

/* ---------------------------------------------------------------------------
   §37 — NEWS / EVENTS / ACHIEVEMENTS LIST  (includes/news-feed.php)
   ---------------------------------------------------------------------------
   The search box, the grid/list switch, and the two shapes an item can take.
   Every card is the same height whatever it contains: the picture box is a
   fixed ratio, the headline is clamped to two lines and the brief to two, so a
   long headline never pushes one card taller than the one beside it.
   ------------------------------------------------------------------------ */
.nf-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .75rem 1.5rem;
  margin-top: 1.75rem;
}

/* Search, year and page size — one form, one Apply button */
.nf-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  flex: 1 1 480px;
}
.nf-select { display: inline-flex; align-items: center; gap: .45rem; }
.nf-select label {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .66rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--neutral-600, var(--neutral-700));
  margin: 0;
}
.nf-select select {
  padding: .55rem 2rem .55rem .75rem;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--card-bg);
  color: var(--neutral-850);
  font-size: .9rem;
  font-weight: 600;
  line-height: 1.3;
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 1.05rem center, right .8rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  cursor: pointer;
}
.nf-select select:focus {
  outline: none;
  border-color: var(--maroon);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--maroon) 16%, transparent);
}

.nf-search {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 1 1 240px;
  max-width: 420px;
  position: relative;
}
.nf-search__ico {
  position: absolute;
  left: .85rem;
  width: 16px;
  height: 16px;
  color: var(--neutral-500);
  pointer-events: none;
}
.nf-search input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  padding: .6rem 2.3rem .6rem 2.4rem;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--card-bg);
  color: var(--neutral-850);
  font-size: .95rem;
}
.nf-search input[type="search"]::placeholder { color: var(--neutral-500); }
.nf-search input[type="search"]:focus {
  outline: none;
  border-color: var(--maroon);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--maroon) 16%, transparent);
}
/* The cross that empties the box, sitting inside it on the right. Rendered by
   PHP only when there is something to clear, so it works without scripting. */
.nf-search__clear {
  position: absolute;
  right: .55rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  color: var(--neutral-500);
  background: transparent;
  transition: background .18s ease, color .18s ease;
}
.nf-search__clear svg { width: 13px; height: 13px; }
.nf-search__clear:hover { color: var(--maroon); background: var(--primary-soft); }
/* Chrome draws its own cross on a search field; ours is the one that stays. */
.nf-search input[type="search"]::-webkit-search-cancel-button { display: none; }

/* Grid / list switch */
/* No `overflow: hidden` here: it clipped the focus ring off both buttons and
   left only a 2px sliver in the same colour as the pressed state, so a keyboard
   reader could not see where they were. The children carry the rounding. */
.nf-views {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 9px;
  flex: 0 0 auto;
}
.nf-views__btn:first-child { border-radius: 8px 0 0 8px; }
.nf-views__btn:last-child { border-radius: 0 8px 8px 0; }
.nf-views__btn:focus-visible { outline-offset: -2px; z-index: 1; }
.nf-views__btn {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .55rem .9rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--neutral-700);
  text-decoration: none;
  background: var(--card-bg);
  transition: background .18s ease, color .18s ease;
}
.nf-views__btn + .nf-views__btn { border-left: 1px solid var(--border); }
.nf-views__btn svg { width: 15px; height: 15px; flex: none; }
.nf-views__btn:hover { color: var(--maroon); }
.nf-views__btn.is-current { background: var(--maroon); color: var(--paper); }

/* College filter — one button per college, never a dropdown, so a reader sees
   every college and how much each has published without opening anything.
   It wraps rather than scrolls: a hidden button is a button nobody presses. */
.nf-colleges {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-bottom: 1.1rem;
}
.nf-colleges__btn {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .5rem .95rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--neutral-700);
  text-decoration: none;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background .18s ease, color .18s ease, border-color .18s ease;
}
.nf-colleges__btn:hover { color: var(--maroon); border-color: var(--maroon); }
.nf-colleges__btn:focus-visible {
  outline: 2px solid var(--maroon);
  outline-offset: 2px;
}
.nf-colleges__btn.is-current {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--paper);
}
/* How many items sit behind the button. Muted, because the label is the thing
   being pressed and the count is only there to set expectations. */
.nf-colleges__n {
  font-family: var(--font-mono, monospace);
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .04em;
  opacity: .62;
}
.nf-colleges__btn.is-current .nf-colleges__n { opacity: .8; }

/* ---------------------------------------------------------------------------
   TERMS OF REFERENCE  (includes/sustainability-tor.php)
   The labelled rows that set out a governance body's purpose, scope,
   responsibilities and meeting rules. A description list rather than a table:
   the content is label/value pairs, and on a phone the pairs stack instead of
   being squeezed into two narrow columns.
   --------------------------------------------------------------------------- */
.tor {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 1.6rem 1.6rem 1.2rem;
}
.tor__title {
  font-size: clamp(1.25rem, 2.4vw, 1.6rem);
  line-height: 1.25;
  color: var(--ink);
  margin: .35rem 0 .4rem;
}
.tor__lead {
  color: var(--neutral-700);
  font-size: .98rem;
  line-height: 1.65;
  margin: 0 0 1.4rem;
  max-width: 62ch;
}
.tor__list { margin: 0; }
/* A hairline between rows rather than a full grid: the eye follows the labels
   down the left edge, and a boxed cell for an eleven-item list reads as noise. */
.tor__row {
  display: grid;
  grid-template-columns: minmax(9rem, 15rem) 1fr;
  gap: 0 1.75rem;
  padding: 1.05rem 0;
  border-top: 1px solid var(--border);
}
.tor__row:first-child { border-top: 0; padding-top: .25rem; }
.tor__label {
  font-family: var(--font-mono, monospace);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--maroon);
  margin: .2rem 0 0;
}
.tor__value {
  margin: 0;
  color: var(--neutral-800);
  font-size: .97rem;
  line-height: 1.75;
}
.tor__value ul,
.tor__value ol { margin: 0; padding-left: 1.15rem; }
.tor__value li { margin-bottom: .45rem; }
.tor__value li:last-child { margin-bottom: 0; }
.tor__value p { margin: 0 0 .6rem; }
.tor__value p:last-child { margin-bottom: 0; }

@media (max-width: 767.98px) {
  .tor { padding: 1.15rem 1.15rem .9rem; border-radius: 12px; }
  /* Stacked: two columns at this width give the value about 20 characters. */
  .tor__row { grid-template-columns: 1fr; gap: .35rem; padding: .9rem 0; }
  .tor__label { margin: 0; }
}

/* The Centre for Health Workforce Development's own band on News and Events.
   Tinted so it reads as a section belonging to somewhere else, rather than as
   more of the University's own list. */
.chwd-band {
  background: var(--primary-soft);
  border-color: transparent;
}
.chwd-band .nf-card { background: var(--card-bg); }

/* The picture, or the tile that stands in for one */
/* The media box is positioned and the picture fills it absolutely. A plain
   `height:100%` does not resolve against a parent sized by `aspect-ratio`, and
   a tall source then set its own height and made one card in the row taller
   than the rest. */
.nf-card__media,
.nf-row__media { position: relative; overflow: hidden; }
.nf-media {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--primary-soft);
}
/* <picture> is an inline box by default, which leaves the image inside it with
   no definite height to fill and reopens the uneven-card problem above. */
.nf-media picture { display: block; width: 100%; height: 100%; }
.nf-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.nf-media--blank {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--maroon) 8%, transparent), transparent 60%),
    var(--primary-soft);
}
.nf-media__mark {
  font-family: "Fraunces", Georgia, serif;
  font-size: 1.05rem;
  letter-spacing: .04em;
  color: color-mix(in srgb, var(--maroon) 55%, transparent);
}
.nf-media__kind {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .6rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--maroon) 45%, transparent);
}

/* The University photograph a card falls back to when the item published none
   of its own. It is held back and signed so it reads as the University filling
   the space, not as a picture of that particular item. */
.nf-media--stock img { filter: saturate(.82) brightness(.92); }
.nf-media--stock::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, transparent 45%, color-mix(in srgb, var(--ink) 45%, transparent));
}
/* A picture that is not the card's shape — a conference poster, a portrait, a
   square social post — shown whole rather than cropped to a band out of its
   middle. The panel behind it is the same picture blurred, so the card keeps
   that picture's own colours instead of a flat block. */
.nf-media--fit picture {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: .5rem;
}
/* Sized by its own proportions inside the box, and never blown up past
   --nf-fit-w — the cap the page works out from the file's real width. */
.nf-media--fit img {
  width: auto;
  height: auto;
  max-width: min(100%, var(--nf-fit-w, 100%));
  max-height: 100%;
  object-fit: contain;
  border-radius: 4px;
}
.nf-media__wash {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  filter: blur(18px) saturate(1.15) brightness(.92);
  transform: scale(1.15);
}
/* The blur samples the edge pixels, which on a white poster leaves a pale
   panel with no edge to it. A thin veil gives the picture a boundary. */
.nf-media--fit::before {
  content: "";
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--ink) 14%, transparent);
}
.nf-media--fit picture { position: relative; }

.nf-media--stock .nf-media__mark {
  position: absolute;
  /* The gradient above is generated content and so is painted last. The mark
     has to be lifted over it or the veil lands on the lettering. */
  z-index: 1;
  inset: auto 0 .6rem 0;
  text-align: center;
  color: var(--paper);
  font-size: .95rem;
  text-shadow: 0 1px 6px color-mix(in srgb, var(--ink) 55%, transparent);
}

/* Shared card text: two lines of headline, two lines of brief, every time */
.nf-meta {
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  margin-bottom: .5rem;
}
.nf-title {
  font-size: 1.1rem;
  line-height: 1.35;
  margin-bottom: .5rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* `overflow: hidden` is what makes the clamp work, and it also clips the focus
   ring of the link inside. Drawing the ring inward keeps it fully visible. */
.nf-title a { color: var(--ink); text-decoration: none; }
.nf-title a:hover { color: var(--maroon); }
.nf-title a:focus-visible { outline-offset: -2px; }
.nf-brief {
  color: var(--neutral-700);
  font-size: .93rem;
  line-height: 1.55;
  margin-bottom: .9rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Grid */
.nf-card__media { display: block; aspect-ratio: 16 / 10; }
.nf-card .nf-title { min-height: 2.97rem; }

/* List — a fixed-height strip so a hundred rows scan as one column */
.nf-list { display: flex; flex-direction: column; gap: 1rem; }
/* A fixed height, not a floor: with `min-height` a one-line headline made a
   172px row and a two-line one made 189px, so a page of ten rows stepped up and
   down instead of scanning as one column. Below 768px the row stacks and the
   height is released. */
.nf-row { display: flex; align-items: stretch; height: 196px; }
.nf-row__media { flex: 0 0 260px; display: block; align-self: stretch; }
.nf-row__body {
  padding: 1.25rem 1.5rem;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
}
.nf-row .nf-title {
  font-size: 1.2rem;
  line-height: 1.3;
  -webkit-line-clamp: 2;
  min-height: 0;
}
.nf-row .nf-brief { -webkit-line-clamp: 2; margin-bottom: .75rem; }
.nf-row__cta { margin-top: auto; align-self: flex-start; }

@media (min-width: 1200px) {
  .nf-row__media { flex-basis: 300px; }
  .nf-row .nf-brief { -webkit-line-clamp: 3; }
}

@media (max-width: 767.98px) {
  .nf-row { flex-direction: column; height: auto; }
  .nf-row__media { flex: 0 0 auto; aspect-ratio: 16 / 9; }
  .nf-row__body { padding: 1.1rem 1.15rem; }
  .nf-toolbar { gap: .75rem; }
  .nf-filters { flex: 1 1 100%; }
  .nf-search { flex: 1 1 100%; max-width: none; }
  .nf-select { flex: 1 1 45%; }
  .nf-select select { width: 100%; }
  .nf-views { width: 100%; }
  .nf-views__btn { flex: 1 1 50%; justify-content: center; }
  .nf-colleges { gap: .35rem; }
  .nf-colleges__btn { flex: 1 1 auto; justify-content: center; padding: .5rem .7rem; }
}

/* ---------------------------------------------------------------------------
   §38 — ONE ITEM'S OWN PAGE  (includes/news-feed.php · news_feed_single)
   ---------------------------------------------------------------------------
   A News story, an Event or an Achievement read on its own: the text, then the
   photographs, both running the full width of the section.

   The photographs reuse the album grid (§23) and the album's photo viewer
   verbatim — .gal-grid / .gal-item / #galLightbox / assets/js/gallery.js — so
   there is ONE gallery interface on this site rather than two that drift. That
   grid is CSS columns with height:auto images, which is also what publishes an
   event set whole: the frames are portrait, landscape and square, and a crop
   would cut people out of the picture.
   ------------------------------------------------------------------------ */
/* The text and the photographs both run the full width of the section. */
.nf-article,
.nf-photos { max-width: none; margin-inline: 0; }

.nf-article__head {
  margin-bottom: 2rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--border);
}
.nf-article__back {
  display: inline-block;
  margin-bottom: 1.25rem;
  font-weight: 600;
  font-size: .92rem;
  color: var(--maroon);
  text-decoration: none;
}
.nf-article__back:hover { color: var(--maroon-deep); }

.nf-article__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin: 0;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--neutral-700);
}
.nf-article__kind {
  padding: .2rem .5rem;
  border-radius: 4px;
  background: var(--primary-soft);
  color: var(--maroon);
  font-weight: 600;
}
.nf-article__dot { color: var(--neutral-400); }

/* The prose. The stored text is CMS HTML — bare <p>, the odd <div><br></div>
   left over from a rich-text editor, and occasional inline styles — so the
   spacing is set here rather than trusted to what is in the string. */
.nf-article .news-body {
  color: var(--neutral-800);
  font-size: 1.05rem;
  line-height: 1.75;
}
.nf-article .news-body > :first-child { margin-top: 0; }
.nf-article .news-body > :last-child { margin-bottom: 0; }
.nf-article .news-body p { margin: 0 0 1.15rem; }
.nf-article .news-body h2,
.nf-article .news-body h3,
.nf-article .news-body h4 {
  margin: 2rem 0 .75rem;
  font-family: "Fraunces", Georgia, serif;
  line-height: 1.3;
  color: var(--ink);
}
.nf-article .news-body h2 { font-size: 1.4rem; }
.nf-article .news-body h3 { font-size: 1.2rem; }
.nf-article .news-body h4 { font-size: 1.05rem; }
.nf-article .news-body ul,
.nf-article .news-body ol { margin: 0 0 1.15rem; padding-left: 1.35rem; }
.nf-article .news-body li { margin-bottom: .4rem; }
.nf-article .news-body a { color: var(--maroon); text-decoration: underline; text-underline-offset: .15em; }
.nf-article .news-body a:hover { color: var(--maroon-deep); }
.nf-article .news-body img { max-width: 100%; height: auto; border-radius: 10px; }
.nf-article .news-body table { width: 100%; margin-bottom: 1.15rem; border-collapse: collapse; }
.nf-article .news-body th,
.nf-article .news-body td { padding: .55rem .7rem; border: 1px solid var(--border); text-align: left; }
.nf-article .news-body blockquote {
  margin: 0 0 1.15rem;
  padding: .25rem 0 .25rem 1.1rem;
  border-left: 3px solid var(--primary-soft);
  color: var(--neutral-700);
}
/* An editor's trailing empty line is not spacing anybody chose. */
.nf-article .news-body > div:empty,
.nf-article .news-body > p:empty { display: none; }

.nf-article__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: 2rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--border);
}

/* The photographs, under the text in the same section. */
.nf-photos { margin-top: 3.5rem; }
.nf-photos__head { margin-bottom: 1.5rem; }
.nf-photos__count {
  margin: .5rem 0 .25rem;
  font-size: clamp(1.25rem, 2.2vw, 1.6rem);
  line-height: 1.25;
}
.nf-photos__hint {
  margin: 0;
  font-size: .92rem;
  color: var(--neutral-700);
}

@media (max-width: 767.98px) {
  .nf-photos { margin-top: 2.5rem; }
  .nf-article .news-body { font-size: 1rem; }
}

/* ===========================================================================
   §39 — SHARED CHROME: CONTRAST AND FOCUS  (includes/header.php · footer.php)
   ---------------------------------------------------------------------------
   Two corrections that apply to every page of the site, because they belong to
   the header and footer every page shares.
   =========================================================================== */

/* The footer column headings — Study, Discover, Connect. --gold is a hairline
   accent, not an ink: at 3.18:1 on the charcoal footer it did not reach the
   4.5:1 that text of this size has to clear. --footer-heading is the on-dark
   gold the palette already keeps for exactly this (8.84:1). */
.site-footer .col-head { color: var(--footer-heading); }

/* ===========================================================================
   §40 — SITE SEARCH: THE HEADER BUTTON AND THE PANEL IT OPENS
   ---------------------------------------------------------------------------
   The search box used to live in the 38px utility bar, at 9rem wide, on a dark
   strip, below the eye line — the smallest target in the header for the task
   visitors reach for most. It is now a magnifier on the main bar beside Apply
   Now (includes/header.php), and it opens a panel over the page with a
   full-width box, the searches people actually run, and room for the
   as-you-type suggestions enhance.js draws under it.
   Order: the button -> the overlay and its scrim -> the panel -> the form ->
   the hints and chips -> phones -> reduced motion.
   =========================================================================== */

/* "Apply Now" is two words on one line, always. The magnifier beside it costs
   the header bar 50px, and on a section whose menu carries seven items that was
   enough to break the button across two lines and stretch the bar past 80px. */
.btn-apply-pin { white-space: nowrap; }

/* The same 50px on the menu side: a nav of seven or more sections — the
   Excellence Hub's own menu, which replaces the site's five — tightens its
   links so the logo, the whole menu, the magnifier and Apply Now still sit on
   one line. Browsers without :has() simply wrap a label, as they did before. */
@media (min-width: 992px) {
  .main-nav:has(.nav-item:nth-child(7)) .nav-link {
    padding-left: .55rem;
    padding-right: .55rem;
    font-size: .92rem;
  }
}

/* The button. Square, hairline-bordered so it reads as a control on white, and
   the same 42px at every width — phones had no search at all before this. */
.btn-search-pin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 42px;
  height: 42px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
}
.btn-search-pin .search-ico { width: 19px; height: 19px; }
.btn-search-pin:hover,
.btn-search-pin:focus-visible {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--paper);
}

/* The overlay. Above the sticky header (1030), the mega menus (1040) and the
   mobile drawer, because nothing may sit over an open dialog. */
.site-search {
  position: fixed;
  inset: 0;
  z-index: 1090;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  opacity: 0;
}
/* display:flex above would defeat the hidden attribute the mark-up ships with. */
.site-search[hidden] { display: none; }
.site-search.is-open { opacity: 1; }

/* While the panel is open the page behind it does not scroll — the class is put
   on <html> by site.js and removed the moment the panel closes. */
html.search-open { overflow: hidden; }

.site-search__scrim {
  position: absolute;
  inset: 0;
  background: rgba(var(--charcoal-rgb), .62);
}

/* The panel. Wide enough for a result line to read as a sentence, held near the
   top so the suggestion list has the rest of the viewport to open into. */
.site-search__panel {
  position: relative;
  width: min(760px, calc(100% - 2rem));
  margin: clamp(1rem, 9vh, 6rem) 1rem 2rem;
  padding: clamp(1.5rem, 3vw, 2.5rem);
  background: var(--paper);
  border-radius: 14px;
  box-shadow: 0 30px 80px rgba(var(--charcoal-rgb), .38);
}

.site-search__close {
  position: absolute;
  top: .75rem;
  right: .75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--neutral-700);
}
.site-search__close svg { width: 20px; height: 20px; }
.site-search__close:hover,
.site-search__close:focus-visible { background: var(--ivory); color: var(--charcoal); }

.site-search__eyebrow {
  font-size: .7rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--maroon);
}
.site-search__title {
  margin: .35rem 0 1.1rem;
  font-size: clamp(1.35rem, 3vw, 1.9rem);
  line-height: 1.2;
  color: var(--charcoal);
  padding-right: 2.5rem;   /* clears the close button on a narrow panel */
}

/* The box. position:relative is declared here rather than left to enhance.js,
   which only patches a static parent at runtime — the suggestion list is
   absolutely positioned against this row and must line up with its edges. */
.site-search__form {
  position: relative;
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .5rem .75rem;
  border: 1.5px solid var(--line-strong);
  border-radius: 10px;
  background: var(--paper);
}
.site-search__form:focus-within {
  border-color: var(--maroon);
  box-shadow: 0 0 0 3px rgba(var(--maroon-rgb), .16);
}
.site-search__ico {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  color: var(--maroon);
}
.site-search__input {
  flex: 1 1 auto;
  min-width: 0;
  padding: .4rem .1rem;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 1.05rem;
  color: var(--charcoal);
}
/* The ring is drawn on the whole row by :focus-within above, so the input does
   not draw a second one inside it. */
.site-search__input:focus { outline: none; }
.site-search__input::-webkit-search-cancel-button { cursor: pointer; }
.site-search__go { flex: 0 0 auto; }

.site-search__hint {
  margin: .8rem 0 0;
  font-size: .85rem;
  color: var(--text-muted);
}
.site-search__hint kbd {
  padding: .1rem .35rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--ivory);
  color: var(--charcoal);
  font-size: .78rem;
}

/* The "Continue exploring" section — the page a returning visitor looked at
   last, offered inside the panel as one button. The section shell is the
   labelled, hairline-separated block the chips already use, and the button is
   the site's own `.btn .btn-outline-maroon` — secondary to the solid Search
   button beside the box, and inverting to solid on hover like every other
   outline button on the site. Only the things a page CTA cannot supply are
   added here: the panel's smaller scale, the leading history icon, the trailing
   arrow and the ellipsis that keeps a long page title on one line.

   assets/js/enhance.js writes the label and the button into #last-viewed, then
   removes [hidden]. Until it does, the section is not in the mark-up at all —
   there is no empty state to style. */
.site-search__resume {
  margin-top: 1.35rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line-soft);
}
.site-search__resume[hidden] { display: none; }
/* Same treatment as the chips label, so the two sections read as a pair. */
.site-search__resume-label {
  display: block;
  margin-bottom: .55rem;
  font-size: .7rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
/* theme.css sizes .btn for a page CTA (1rem type, .78rem/1.7rem padding) — too
   large for a panel whose chips are .87rem. Everything else, including the
   hover inversion and the lift, comes from .btn-outline-maroon untouched. */
.site-search__resume-btn {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  max-width: 100%;
  padding: .58rem 1.1rem;
  font-size: .95rem;
  text-decoration: none;
}
.site-search__resume-btn .site-search__resume-ico {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
}
.site-search__resume-title {
  overflow: hidden;
  min-width: 0;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The house arrow (theme.css .link-arrow), so the button reads as a way onward
   rather than an action performed here. */
.site-search__resume-btn::after {
  content: "→";
  flex: 0 0 auto;
  font-weight: 600;
}
.site-search__resume-btn:focus-visible {
  outline: 2px solid var(--maroon);
  outline-offset: 2px;
}

.site-search__chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-top: 1.35rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line-soft);
}
.site-search__chips-label {
  margin-right: .25rem;
  font-size: .7rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.site-search__chip {
  padding: .35rem .8rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: .87rem;
  color: var(--charcoal);
  text-decoration: none;
}
.site-search__chip:hover,
.site-search__chip:focus-visible {
  background: var(--maroon);
  border-color: var(--maroon);
  color: var(--paper);
}

/* On a phone the panel is the screen: a sheet with nothing of the page left to
   reach for, and no rounded corners floating in a 4px margin. */
@media (max-width: 575.98px) {
  .site-search__panel {
    width: 100%;
    min-height: 100%;
    margin: 0;
    padding: 1.25rem 1.1rem 2rem;
    border-radius: 0;
  }
  .site-search__form { flex-wrap: wrap; }
  .site-search__go { width: 100%; margin-top: .5rem; }
}

@media (prefers-reduced-motion: no-preference) {
  .site-search { transition: opacity .18s ease; }
  .site-search__scrim { backdrop-filter: blur(3px); }
  .site-search__panel { transform: translateY(-12px); transition: transform .22s ease; }
  .site-search.is-open .site-search__panel { transform: none; }
  .btn-search-pin,
  .site-search__chip,
  .site-search__close { transition: background .18s ease, color .18s ease, border-color .18s ease; }
  .site-search__resume-btn::after { display: inline-block; transition: transform .2s ease; }
  .site-search__resume-btn:hover::after { transform: translateX(4px); }
}

/* ===========================================================================
   §41 — FEATURE-ICON GLYPH  (site-wide — any .feature-icon chip that holds an SVG)
   ---------------------------------------------------------------------------
   theme.css sizes only `.feature-icon .dot` (the 12px square). A chip that
   carries an inline line icon instead — contact.php's address / phone / email /
   hours rows today — needs the <svg> given its own box, or it takes the 300x150
   default intrinsic size of a replaced element and bursts the 38px chip.
   Same 18px glyph, same currentColor stroke, as the homepage ledger in §17;
   §17 stays as written because it is deliberately scoped to body.home #why.
   =========================================================================== */
.feature-icon > .feat-ico {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  display: block;               /* removes the inline-baseline gap */
  color: var(--maroon);         /* the SVGs stroke with currentColor */
}
.feature-icon.on-dark > .feat-ico { color: var(--gold); }
