/* ===== NO UA LINK COLOURS — 26 Jul 2026 (Benjie, urgent) ==================
   §2A.3: no blue or purple hues outside the ink/teal/gold system. No `:visited`
   was declared anywhere, so any anchor an author rule did not reach fell
   through to the browser defaults — #0000EE unvisited, #551A8B PURPLE once
   visited. Lowest specificity in the file and placed FIRST, so every existing
   author rule still wins.
   ======================================================================== */
a { color: inherit; }
a:visited { color: inherit; }

/* CoreBridge University Intelligence — shared enhancement layer (enhance.css)
   Companion to enhance.js. Uses only canonical tokens already defined in each hub's styles.css
   (--accent, --ink, --ink-3, --paper-bright, --success, --danger, --chart-gold, --rule, radii,
   shadows). No new colour values are introduced here: the canonical is the sole authority and a
   hex code in this file would be a second source of truth.

   Motion respects prefers-reduced-motion and pauses when the tab is hidden, so a pulse never
   burns a laptop battery in a background tab. */

/* ---------------------------------------------------------------- pulse marks */
.cbx-pulse { display: inline-flex; width: 8px; height: 8px; position: relative; vertical-align: baseline; }
.cbx-pulse i {
  position: absolute; inset: 0; border-radius: 50%; background: currentColor; opacity: .85;
}
.cbx-pulse i::after {
  content: ""; position: absolute; inset: -3px; border-radius: 50%;
  border: 1.5px solid currentColor; opacity: 0;
  /* THREE CYCLES, NOT FOREVER — see the note at the DS14 block. */
  animation: cbx-ping 2.4s cubic-bezier(0, 0, .2, 1) 3;
}
.cbx-pulse--up    { color: var(--success); }
.cbx-pulse--down  { color: var(--danger); }
.cbx-pulse--flat  { color: var(--ink-3, #6B6557); }
.cbx-pulse--live  { color: var(--accent); }
@keyframes cbx-ping {
  0%   { transform: scale(.6); opacity: .75; }
  70%  { transform: scale(1.9); opacity: 0; }
  100% { transform: scale(1.9); opacity: 0; }
}

/* ---------------------------------------------------------------- pulsing sparkline */
.cbx-spark { display: block; margin-top: 6px; overflow: visible; }
/* Also three cycles. Measured on the UK finance view: five of these dots were breathing at 2.6s
   with no end, on a page the reader is there to read. The dot is coloured and placed, which is
   what says "latest point"; the motion only has to draw the eye to it once. */
.cbx-spark__now { animation: cbx-breathe 2.6s ease-in-out 3; transform-origin: center; transform-box: fill-box; }
.cbx-spark__peak   { fill: var(--success); opacity: .9; }
.cbx-spark__trough { fill: var(--danger);  opacity: .9; }
@keyframes cbx-breathe {
  0%, 100% { r: 2.4; opacity: 1; }
  50%      { r: 3.4; opacity: .55; }
}

/* ---------------------------------------------------------------- bullet row */
.cbx-bullet { margin-top: 9px; }
.cbx-bullet__track {
  position: relative; height: 9px; border-radius: 3px;
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  overflow: visible;
}
.cbx-bullet__band {
  position: absolute; top: 0; bottom: 0; border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 17%, transparent);
}
.cbx-bullet__bar {
  position: absolute; top: 2px; bottom: 2px; left: 0; border-radius: 2px;
  background: var(--accent);
}
.cbx-bullet__bar.is-below { background: color-mix(in srgb, var(--ink) 42%, transparent); }
.cbx-bullet__mark {
  position: absolute; top: -3px; bottom: -3px; width: 2px; border-radius: 1px;
  background: var(--ink); transform: translateX(-1px);
}
.cbx-bullet__legend {
  display: flex; gap: 7px; align-items: baseline; margin-top: 6px;
  font-size: 11.5px; color: var(--ink-3, #6B6557);
}
.cbx-bullet__legend b { font-variant-numeric: tabular-nums; color: var(--ink); font-size: 12.5px; }

/* ---------------------------------------------------------------- distribution strip */
.cbx-dist { margin-top: 10px; }
.cbx-dist__axis {
  position: relative; height: 22px; border-radius: 3px;
  background: color-mix(in srgb, var(--ink) 4%, transparent);
}
.cbx-dist__t {
  position: absolute; top: 6px; height: 10px; width: 1px;
  background: color-mix(in srgb, var(--ink) 26%, transparent);
}
.cbx-dist__med {
  position: absolute; top: 2px; bottom: 2px; width: 1.5px;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
}
.cbx-dist__me {
  position: absolute; top: 0; bottom: 0; width: 3px; border-radius: 2px;
  background: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);
}
.cbx-dist__legend {
  display: flex; justify-content: space-between; gap: 10px; margin-top: 6px;
  font-size: 11px; color: var(--ink-3, #6B6557); font-variant-numeric: tabular-nums;
}
.cbx-dist__legend b { color: var(--ink); font-weight: 600; text-align: center; }

/* ---------------------------------------------------------------- tick flash */
@keyframes cbx-flash-up   { from { background: color-mix(in srgb, var(--success) 26%, transparent); } to { background: transparent; } }
@keyframes cbx-flash-down { from { background: color-mix(in srgb, var(--danger)  26%, transparent); } to { background: transparent; } }
.cbx-flash-up   { animation: cbx-flash-up   .85s ease-out 1; }
.cbx-flash-down { animation: cbx-flash-down .85s ease-out 1; }

/* ---------------------------------------------------------------- story cards */
.cbx-stories {
  display: grid; gap: var(--gap, 16px); margin-top: 16px;
  grid-template-columns: repeat(auto-fit, minmax(268px, 1fr));
}
/* No coloured left bar: `_canonical/SKILL.md` is explicit that containment relies on fills, type
   and neutral hairlines, and that boxes take no coloured left/top accent bars. The card's category
   is already stated in words by __eyebrow, so the bar was carrying nothing the reader could not
   already read. The --watch/--good/--note variants are kept as no-ops rather than deleted: the
   classes are emitted by enhance.js at ~15 call sites and are the record of which reading is which. */
.cbx-story {
  background: var(--paper-bright, #FEFCF7); border: 1px solid var(--rule, rgba(3,22,2,.10));
  border-radius: var(--r-card, 10px); padding: var(--pad-card, 17px 19px);
  box-shadow: var(--shadow-tile);
}
.cbx-story__eyebrow {
  margin: 0 0 5px; font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--ink-3, #6B6557); font-weight: 600;
}
.cbx-story__h { margin: 0 0 7px; font-size: 15px; line-height: 1.32; font-weight: 600; }
.cbx-story__b { margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--ink-2, var(--ink)); }
.cbx-story__b b { font-variant-numeric: tabular-nums; }
.cbx-story__src { margin: 9px 0 0; font-size: 10.5px; color: var(--ink-3, #6B6557); }

/* ---------------------------------------------------------------- global position band */
.cbx-gb__wrap { overflow-x: auto; margin-top: 14px; }
.cbx-gb__t { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 560px; }
.cbx-gb__t th, .cbx-gb__t td { padding: 9px 12px; text-align: left; vertical-align: top; }
.cbx-gb__t thead th {
  border-bottom: 1.5px solid var(--rule, rgba(3,22,2,.14)); font-weight: 600; font-size: 13px;
  line-height: 1.3;
}
.cbx-gb__t thead th.is-me { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.cbx-gb__t tbody th {
  font-weight: 500; color: var(--ink-2, var(--ink)); width: 34%;
  border-bottom: 1px solid var(--rule, rgba(3,22,2,.08));
}
.cbx-gb__t tbody td {
  border-bottom: 1px solid var(--rule, rgba(3,22,2,.08)); font-variant-numeric: tabular-nums;
}
.cbx-gb__c.is-me { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.cbx-gb__v { display: block; font-size: 15px; font-weight: 600; }
.cbx-gb__y { display: block; font-size: 10.5px; color: var(--ink-3, #6B6557); margin-top: 1px; }
.cbx-gb__n { display: block; font-size: 10.5px; font-weight: 400; color: var(--ink-3, #6B6557); }
.cbx-gb__here {
  display: block; font-size: 9.5px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--accent); font-weight: 700; margin-bottom: 2px;
}
.cbx-gb__note {
  font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-3, #6B6557);
  border: 1px solid var(--rule, rgba(3,22,2,.14)); border-radius: 3px; padding: 0 4px; cursor: help;
}
.cbx-gb__demand {
  margin-top: 20px; padding: 16px 18px; border-radius: var(--r-card, 10px);
  background: color-mix(in srgb, var(--accent) 5%, var(--paper-bright, #FEFCF7));
  border: 1px solid var(--rule, rgba(3,22,2,.10));
}
.cbx-gb__demand h4 { margin: 0 0 6px; font-size: 14.5px; line-height: 1.34; font-weight: 600; }
.cbx-gb__demandnote { margin: 0 0 10px; font-size: 12.5px; color: var(--ink-2, var(--ink)); }
.cbx-gb__top { margin: 0; padding: 0; list-style: none; display: grid; gap: 5px; }
.cbx-gb__top li {
  display: flex; justify-content: space-between; gap: 12px; font-size: 13px;
  padding: 5px 0; border-bottom: 1px solid var(--rule, rgba(3,22,2,.07));
}
.cbx-gb__top li span { font-variant-numeric: tabular-nums; color: var(--ink-2, var(--ink)); }
.cbx-gb__top li i { font-style: normal; color: var(--ink-3, #6B6557); font-size: 11px; }
.cbx-gb__basis { margin: 10px 0 0; font-size: 11px; line-height: 1.5; color: var(--ink-3, #6B6557); }

/* ---------------------------------------------------------------- student tiles */
.cbx-stu__hint { margin-top: 7px; font-size: 11px; line-height: 1.45; color: var(--ink-3, #6B6557); }

/* ---------------------------------------------------------------- motion + density */
@media (prefers-reduced-motion: reduce) {
  .cbx-pulse i::after, .cbx-spark__now { animation: none; }
  .cbx-flash-up, .cbx-flash-down { animation: none; }
}
@media (max-width: 640px) {
  .cbx-gb__t { font-size: 12px; }
  .cbx-gb__t th, .cbx-gb__t td { padding: 7px 9px; }
  .cbx-stories { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------- global band, tier two */
.cbx-gb__why {
  font-size: 9.5px; letter-spacing: .05em; text-transform: uppercase; color: var(--ink-3, #6B6557);
  border-bottom: 1px dotted var(--ink-3, #6B6557); cursor: help; margin-left: 5px; white-space: nowrap;
}
.cbx-gb__h3 { margin: 26px 0 6px; font-size: 16px; line-height: 1.3; font-weight: 600; }
.cbx-gb__owns {
  display: grid; gap: var(--gap, 16px); margin-top: 13px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.cbx-gb__own {
  background: var(--paper-bright, #FEFCF7); border: 1px solid var(--rule, rgba(3,22,2,.10));
  border-radius: var(--r-card, 10px); padding: 14px 16px; box-shadow: var(--shadow-tile);
}
.cbx-gb__own.is-me { border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.cbx-gb__own h5 { margin: 0 0 9px; font-size: 13.5px; font-weight: 600; }
.cbx-gb__own ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 11px; }
.cbx-gb__own li { display: block; }
.cbx-gb__ol { display: block; font-size: 11.5px; color: var(--ink-3, #6B6557); }
.cbx-gb__own b { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
.cbx-gb__own i { font-style: normal; font-size: 11px; color: var(--ink-3, #6B6557); margin-left: 4px; }
.cbx-gb__ob { display: block; font-size: 10.5px; line-height: 1.45; color: var(--ink-3, #6B6557); margin-top: 3px; }

/* ---------------------------------------------------------------- student lens pairs */
.cbx-stu__pairs { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 7px; }
.cbx-stu__pair { font-size: 11.5px; color: var(--ink-3, #6B6557); }
.cbx-stu__pair b { font-size: 13.5px; color: var(--ink); font-variant-numeric: tabular-nums; }
.cbx-stu__pair i { font-style: normal; opacity: .75; }
.cbx-stugrid { grid-template-columns: repeat(auto-fit, minmax(268px, 1fr)); }
.cbx-stu__pair.is-stale b { color: var(--ink-3, #6B6557); }
.cbx-stu__pair.is-stale i { font-weight: 600; }
/* Coloured left bar removed per `_canonical/SKILL.md` (no coloured left/top accent bars on boxes).
   The 7% gold tint already contains the block, so the radius goes back to all four corners. */
.cbx-gb__breadth {
  margin: 12px 0 0; padding: 10px 12px; font-size: 12.5px; line-height: 1.5;
  border-radius: var(--r-chip, 7px);
  background: color-mix(in srgb, var(--chart-gold) 7%, transparent);
}

/* ---------------------------------------------------------------- command palette: the answer row */
.cbk-item.cbk-answer {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border-left: 3px solid var(--accent);
}
.cbk-item.cbk-answer:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); }
.cbk-item.cbk-answer .cbk-lbl b { font-size: 15px; font-variant-numeric: tabular-nums; }
.cbk-type--answer { background: var(--accent); color: var(--paper-bright, #FEFCF7); }
.cbk-type--hub { background: color-mix(in srgb, var(--chart-gold) 22%, transparent); color: var(--warn-text, #8E6E2E); }

/* ---------------------------------------------------------------- analytics pack (25 Jul 2026) */
.an-bridge__tbl, .an-cagr__tbl { overflow-x: auto; margin-top: 14px; }
.an-bridge__tbl table, .an-cagr__tbl table { width: 100%; border-collapse: collapse; font-size: 12.5px; min-width: 520px; }
.an-bridge__tbl th, .an-cagr__tbl th { text-align: left; font-weight: 600; padding: 8px 10px;
  border-bottom: 1.5px solid var(--rule, rgba(3,22,2,.14)); }
.an-bridge__tbl td, .an-cagr__tbl td { padding: 7px 10px; border-bottom: 1px solid var(--rule, rgba(3,22,2,.07)); }
.an-bridge__tbl .num, .an-cagr__tbl .num { text-align: right; font-variant-numeric: tabular-nums; }
.an-bridge__tbl tr.is-total td { font-weight: 700; border-top: 1.5px solid var(--rule, rgba(3,22,2,.16)); border-bottom: none; }
.an-bridge__tbl tr.is-residual td { color: var(--ink-3, #6B6557); font-style: normal; }
.an-cagr__tbl td.is-up { color: var(--success, #3D5A3D); font-weight: 600; }
.an-cagr__tbl td.is-down { color: var(--danger, #8E2E2E); font-weight: 600; }

/* ---------------------------------------------------------------------------------------------
   Section chrome, merged (DS2, 25 Jul 2026). Pairs with the merge step in enhance.js.

   The banner used to stack an eyebrow over a title in 20px/19px of padding, with a separate pill
   row and a 14px gap above it. Title and pills now share one line and the padding comes down to
   13px/14px. On the Australian Rank view at 1280px this takes the chrome above the first content
   section from 578px to about 430px, and the first data mark rises with it.

   The band keeps its glass treatment, its per-section colour and its copy-link. This is a
   density change, not a restyle.
--------------------------------------------------------------------------------------------- */
/* Banners with a pill row to absorb become one line; banners without one keep their stacked
   eyebrow-over-title, but lose the same excess padding. Both are attribute-selected so a band
   that has not been through the merge step is left exactly as authored. */
#main .page-hero-band[data-chrome-merged="1"] { padding: 11px 18px 12px; gap: 0; margin-bottom: 14px; }
#main .page-hero-band[data-chrome-merged="solo"] { padding: 13px 18px 14px; gap: 6px; margin-bottom: 14px; }
#main .page-hero-band .ph-row {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: 10px 16px;
  /* keep clear of the absolutely positioned copy-link at the top right */
  padding-right: 118px;
}
#main .page-hero-band[data-chrome-merged] .ph-title { font-size: clamp(17px, 2vw, 22px); }
#main .page-hero-band[data-chrome-merged] > .ph-copy.ph-copy { top: 50%; transform: translateY(-50%); }

/* Pills, once inside the band, read against the dark ground rather than the paper. */
.cmp-modes--inband { margin: 0 !important; gap: 6px; }
.cmp-modes--inband .cmp-mode,
.cmp-modes--inband button,
.cmp-modes--inband a {
  background: rgba(255, 255, 255, .09);
  border: 1px solid rgba(255, 255, 255, .18);
  color: var(--paper-on-ink-2, #CBC7BB);
  padding: 5px 12px;
  font-size: 11.5px;
}
.cmp-modes--inband .cmp-mode:hover,
.cmp-modes--inband button:hover,
.cmp-modes--inband a:hover { background: rgba(255, 255, 255, .16); color: var(--paper-on-ink, #F2F0EF); }
.cmp-modes--inband .cmp-mode.is-on,
.cmp-modes--inband button.is-on,
.cmp-modes--inband a.is-on,
.cmp-modes--inband [aria-selected="true"] {
  background: var(--paper-on-ink, #F2F0EF);
  border-color: var(--paper-on-ink, #F2F0EF);
  color: var(--ink, #031602);
  font-weight: 600;
}
@media (max-width: 640px) {
  #main .page-hero-band .ph-row { padding-right: 0; }
  #main .page-hero-band[data-chrome-merged] > .ph-copy.ph-copy { position: static; transform: none; align-self: flex-start; }
}

/* The eyebrow rides the title line rather than sitting above it, so the band is one row deep. */
#main .page-hero-band[data-chrome-merged] .ph-row > .ph-kick { align-self: center; flex: none; }

/* ---------------------------------------------------------------------------------------------
   Canonical conformance: no italics (DS5, 25 Jul 2026).

   `_Resources/_canonical/SKILL.md` v4.1, amendment 2: Spectral replaces Cormorant as the serif
   accent register — "always upright. No italics anywhere (em, i { font-style: normal })". The
   retired-specs list in the same file names "italic-serif heading accents" as a thing not to
   reintroduce.

   These hubs still carried italic Spectral in several places, most visibly on the university and
   section names down the left of the report's contents page and in the glossary marks. Under
   Rule 0 the canonical wins over anything a lower-tier file says, so this normalises them rather
   than arguing with each declaration. Emphasis in prose keeps its semantics for a screen reader;
   it simply stops leaning.
--------------------------------------------------------------------------------------------- */
em, i,
.gloss, .advisory-box .advisory-lede, .sh-section-split .sh-lede,
[class^="rp-"], [class*=" rp-"],
[class^="rp-"] em, [class*=" rp-"] em,
[class^="rp-"] i, [class*=" rp-"] i { font-style: normal; }

/* ---------------------------------------------------------------------------------------------
   MOVEMENT CUES AND READING GUIDANCE (DS14, 25 July 2026)

   Three instruments, one idea: the page should point at the thing worth looking at, once, and
   then stop. Everything here is finite: three cycles at 2.6-2.8s, then settle.

   AND SO IS EVERYTHING ABOVE IT, SINCE 26 JULY. `cbx-ping` and `cbx-spark__now` used to run
   `infinite`, on the argument that they say "this is the live reading" for as long as the tile is
   on screen. Counted on the UK finance view that argument produced eight elements in permanent
   motion on a page whose entire purpose is reading, and the same principle that makes the DS14
   cues finite applies to them: the dot is coloured and placed, which is what identifies the latest
   point; the motion only has to draw the eye there once. A page where something is always moving
   has no way left to say "look here".

   THE EDITORIAL RULE THAT SHAPES THE COLOUR. Green and red are a verdict, not a description, and
   this suite only earns that verdict where the metric register declares a favourable direction.
   The Australian and UK registers do (`METRIC_DEFS[mk].dir`), so a significant move there gets
   `--up` or `--down`. New Zealand's register carries no direction, and Asia's markets are demand
   pools rather than performers, so those hubs get `--move`: the same motion in the signature
   accent, saying "this moved a long way" and stopping short of saying whether that is good. A
   green pulse on a rising personnel-cost line would be an editorial claim the data cannot carry.

   Motion is emphasis, never information. Under prefers-reduced-motion every animation here goes
   to `none`, and each cue leaves behind the static mark that carries the meaning: the badge keeps
   its colour, the chart halo keeps its centre dot, the call to action keeps its ring. A reader who
   has asked for stillness loses the attention-getting and none of the content.
--------------------------------------------------------------------------------------------- */

/* ---- 1 · significance ring on an inline badge (stat-card change, rank movement) --------------
   A ring OUTSIDE the badge rather than a wash behind it: a background needs its own stacking
   context to sit under the text, and getting that wrong tints the number itself. The ring never
   overlaps the glyph, so it cannot touch legibility or contrast at any size. */
.cbx-sig { position: relative; --cbx-sig-c: currentColor; }
.cbx-sig--up   { --cbx-sig-c: var(--success, #3D5A3D); }
.cbx-sig--down { --cbx-sig-c: var(--danger,  #8E2E2E); }
.cbx-sig--move { --cbx-sig-c: var(--accent,  #234C50); }
.cbx-sig::after {
  content: ""; position: absolute; inset: -2px -5px; border-radius: var(--r-chip, 5px);
  border: 1.5px solid var(--cbx-sig-c); opacity: 0; pointer-events: none;
  animation: cbx-sig-ring 2.8s cubic-bezier(0, 0, .2, 1) 3;
}
@keyframes cbx-sig-ring {
  0%   { transform: scale(.94); opacity: 0; }
  16%  { opacity: .5; }
  68%  { transform: scale(1.14); opacity: 0; }
  100% { transform: scale(1.14); opacity: 0; }
}
/* Hovering is the reader taking over. Stop cueing the moment they do, on the badge itself or
   anywhere on the card that carries it, and stay stopped. */
.cbx-sig:hover::after,
.card:hover .cbx-sig::after,
.kpi:hover .cbx-sig::after,
.cbx-story:hover .cbx-sig::after { animation: none; }

/* ---- 2 · movement halo on a chart line ------------------------------------------------------
   Positioned by CBX.chartHalo over a Chart.js point, so it works on a canvas the way the
   Australian and UK hubs' .chart-pulse does. New Zealand and Asia had the .chart-pulse styles in
   their stylesheets and no script that ever placed one; this lives in the shared layer, so all
   four get the instrument from one file.

   Two rings on one offset delay read as a single expanding pulse rather than a blink. */
.cbx-halo {
  position: absolute; width: 9px; height: 9px; border-radius: 50%;
  transform: translate(-50%, -50%); pointer-events: none; z-index: 4;
  background: var(--cbx-halo-c, var(--accent, #234C50));
  box-shadow: 0 0 0 2px var(--paper-bright, #FEFCF7);
}
.cbx-halo--up   { --cbx-halo-c: var(--success, #3D5A3D); }
.cbx-halo--down { --cbx-halo-c: var(--danger,  #8E2E2E); }
.cbx-halo--move { --cbx-halo-c: var(--accent,  #234C50); }
.cbx-halo::before, .cbx-halo::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  border: 1.5px solid var(--cbx-halo-c, var(--accent, #234C50));
  opacity: 0; animation: cbx-halo-ring 2.8s cubic-bezier(0, 0, .2, 1) 3;
}
.cbx-halo::after { animation-delay: .5s; }
@keyframes cbx-halo-ring {
  0%   { transform: scale(1);   opacity: .6; }
  70%  { transform: scale(3.2); opacity: 0; }
  100% { transform: scale(3.2); opacity: 0; }
}
.cbx-halo__lbl {
  position: absolute; left: 50%; bottom: 13px; transform: translateX(-50%);
  font-size: 10px; font-weight: 700; letter-spacing: .03em; white-space: nowrap;
  color: var(--cbx-halo-c, var(--accent, #234C50));
  background: var(--paper-bright, #FEFCF7); border-radius: var(--r-chip, 5px);
  padding: 1px 4px; font-variant-numeric: tabular-nums;
}
/* A chart the reader is already interrogating does not need to be pointed at. */
.chart-wrap:hover .cbx-halo::before, .chart-wrap:hover .cbx-halo::after,
.card:hover  .cbx-halo::before, .card:hover  .cbx-halo::after { animation: none; }

/* ---- 3 · glow on a sparkline line ------------------------------------------------------------
   The one place a real line pulse is possible rather than a mark beside one: the shared
   sparkline is inline SVG, so the stroke itself can breathe. stroke-width and filter are both
   animatable in CSS on an SVG element. Chart.js draws to a canvas and cannot do this, which is
   why canvas charts get the halo above instead. */
.cbx-spark__line.is-move { animation: cbx-line-glow 2.8s ease-in-out 3; }
/* The glow colour is the verdict, so it comes from the significance kind rather than from the
   stroke: the stroke's own rule is "accent if it ended higher, danger if lower", which on a measure
   with no favourable direction would pulse red at a reader for no defensible reason. */
.cbx-spark__line--up   { --cbx-line-c: var(--success, #3D5A3D); }
.cbx-spark__line--down { --cbx-line-c: var(--danger,  #8E2E2E); }
.cbx-spark__line--move { --cbx-line-c: var(--accent,  #234C50); }
@keyframes cbx-line-glow {
  0%, 100% { stroke-width: 1.7; filter: none; }
  50%      { stroke-width: 2.6; filter: drop-shadow(0 0 3px var(--cbx-line-c, currentColor)); }
}
.card:hover .cbx-spark__line.is-move, .cbx-spark:hover .cbx-spark__line.is-move { animation: none; }

/* ---- 4 · next-step cue on a call to action ---------------------------------------------------
   `border-radius: inherit` so the ring follows whatever shape the link already has: the role
   tiles are cards, the continue cue is a pill, the starter links are text. */
.cbx-cue { position: relative; }
.cbx-cue::after {
  content: ""; position: absolute; inset: -3px; border-radius: inherit;
  border: 1.5px solid var(--accent, #234C50); opacity: 0; pointer-events: none;
  animation: cbx-cue-ring 2.6s cubic-bezier(0, 0, .2, 1) 3;
}
@keyframes cbx-cue-ring {
  0%   { transform: scale(.99); opacity: 0; }
  20%  { opacity: .45; }
  72%  { transform: scale(1.035); opacity: 0; }
  100% { transform: scale(1.035); opacity: 0; }
}
/* The trailing arrow leans toward the destination, which is the direction the cue is about. */
.cbx-cue__go { display: inline-block; animation: cbx-cue-nudge 2.6s ease-in-out 3; }
@keyframes cbx-cue-nudge {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(3px); }
}
.cbx-cue:hover::after, .cbx-cue:focus::after, .cbx-cue:focus-visible::after,
.cbx-cue:hover .cbx-cue__go, .cbx-cue:focus .cbx-cue__go { animation: none; }

/* Tab heads sit in the nav bar, where a ring would fight the bar's own geometry. A wash that
   breathes twice is enough to say "there is more here than the page you landed on". */
.cbx-cue--tab { animation: cbx-tab-hint 2.8s ease-in-out 3; }
@keyframes cbx-tab-hint {
  0%, 100% { box-shadow: inset 0 0 0 1px transparent; }
  50%      { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #234C50) 55%, transparent); }
}
.cbx-cue--tab:hover, .cbx-cue--tab:focus, .cbx-cue--tab:focus-visible { animation: none; }

/* ---------------------------------------------------------------------------------------------
   ONE CARD PER ROW ON A PHONE (DS21, 26 July 2026 — supersedes the DS15/DS15a bands below)

   Benjie's instruction, and the rule this block now enforces without exception: on a phone every
   card is full width. Two columns start at 768px, which is a tablet, and not one pixel earlier.

   WHAT THIS REPLACES. DS15 (25 July) put the single-column floor at 480px and left 481-767px on
   two columns, reasoning from the widest figure in the suite: at 480px a two-column card still
   leaves 180px of content box for a 120px number, so the number could not be clipped. That test
   was the wrong test. Nothing was clipped in the 481-767 band, and it still read badly — the
   common phones are 390-430px CSS wide, and any of them held sideways, or any of the several
   grids DS15's selector list never named, landed in that band on two columns. A figure that fits
   is not the same as a figure that is comfortable to read, and "some sections stack, some don't"
   is worse than either answer applied consistently. One column to 767px, everywhere, is the
   answer that needs no per-section reasoning.

   WHY THE SELECTOR LIST IS LONG AND NOT A BLANKET `[class*="grid"]`. Not every grid on these
   hubs is a row of cards. `.trd-row`, `.pf-bar`, `.pickrow`, `.rp-toc-row`, `.mobile-nav-main`
   and the report's `.rp-*` page furniture are column layouts — a rank chip beside a name beside a
   bar — and collapsing those to one column turns a compact strip into a stack of orphaned
   fragments. So the list below names every card/tile row on all four hubs, gathered by auditing
   every `grid-template-columns` declaration in the four `styles.css` files, `asia.css`, and the
   `<style>` blocks the hubs inject from `app.js` at render time, plus every `.html` file in every
   hub. The audit is the reason the list grew from DS15's nine selectors to what is here:
   `.sh-dg` (`repeat(3, 1fr)`, 144 of them on `sector-health.html`, 80px a column at 390px),
   `.sh-uni-card__metrics` (also `repeat(3, 1fr)`), `.gw-stats` (`minmax(170px)` — two columns
   from 352px up), `.scn-out` (`minmax(150px)` — three), `.eif-grid` (an explicit `repeat(2, 1fr)`
   under its own `max-width: 520px` rule; it is dead code today — `eifCarouselRoot` is looked up
   in `app.js` and never emitted by any markup — and is named here so it lands correctly if the
   gallery is ever restored), and the seven runtime-injected grids `.glvl-grid` `.gcg-grid`
   `.mixsm-grid` `.rsa-grid` `.sc-grid` `.atec-grid` `.pf-help-grid`, none of which any
   hand-written stylesheet had ever named.

   NO EXCLUSIONS, WHICH WAS A DECISION AND NOT AN OMISSION. Two candidates were argued for and
   both were rejected. `.kc-grid` is the benchmark strip inside a KPI card ("National median |
   State median") — its cells are not cards, they are a paired comparison, and the pairing is
   arguably the point. Measured at 390px it gets 131.5px a cell on the Research view. `.sh-dg` is
   the three-up metric strip inside each university card on `sector-health.html`: 80px a cell at
   390px, a 9px label over a 13px figure over a 9px sub-line that wraps to two lines. Neither is a
   card, and both were nonetheless two or three abreast on a phone, which is the thing Benjie was
   looking at when she asked for this. A rule with a "cards yes, cells no" carve-out is a rule
   that has to be re-argued every time someone adds a section. This one does not.

   THE PAGE THAT WAS NEVER REACHABLE FROM HERE. `sector-health.html` on the Australian hub linked
   `styles.css` and nothing else — no `enhance.css`, so no shared layer, so every fix made in this
   file since the layer was created had silently never applied to it. It is the page with the
   worst of the symptom on the whole suite: 144 `.sh-dg` strips at three 80px columns each, plus a
   `.sh-bench-grid` of description cards at 158px. The stylesheet is now linked from that page
   (unpinned, matching its unstamped `styles.css` — `stamp_release.py` only stamps `index.html`),
   and its own grid classes are named in the list below. Worth knowing for the next shared-layer
   change: this was the only page in the four hubs missing the link, and it was found by auditing
   every `.html` in every hub, not by reading the page.

   `minmax(0, 1fr)` AND NOT A BARE `1fr`. A bare `1fr` is `minmax(auto, 1fr)`, and that `auto`
   floor is the item's max-content width. `.kpi .value` is `white-space: nowrap`, so its
   max-content is however wide the figure and its label want to be, and the track is pushed to fit
   the content instead of the content being asked to fit the track. Measured before DS15 on Asia's
   India page at 451px: a 332px grid holding three 423px cards, each spilling 91px past its own
   container. `minmax(0, ...)` floors the track at zero so the container wins. The base sheet knew
   this for its two-column rule and forgot it for its single-column one.

   `!important` throughout, deliberately: several of the declarations being corrected are
   themselves `!important` in the base sheet or inline `style=` attributes emitted by `app.js`, so
   anything less specific would silently lose at some widths and hold at others.

   THREE THINGS THIS BLOCK ALSO CARRIES, all from DS15 and all still needed:

   1. THE CAROUSEL IS RETIRED. The M3 block sets `#main .grid.kpis { display: flex }`, and
      `grid-template-columns` means nothing to a flex container — which is why the base sheet's
      two `!important` stacking rules had never once applied to a headline figure row. Every
      flex-era declaration is unwound explicitly rather than left to be inert.

   2. A GRID ITEM'S DEFAULT `min-width: auto` IS ITS OWN MIN-CONTENT WIDTH, so a card can still
      push out of a track floored at zero. Both floors are needed, not either.

   3. THE NUMBER SHARES ITS LINE WITH A 64px SPARKLINE. `.kpi .value` holds the figure and, on the
      Australian and UK hubs, an inline `.kpi-spark`. Below 768px the sparkline drops to its own
      line and hands the whole content width back to the number. It is `aria-hidden` decoration,
      so a screen reader loses nothing by the move.
--------------------------------------------------------------------------------------------- */

/* The full card-grid list, in one declaration so there is exactly one place to add to it.
   Anything that renders a row of cards, tiles or stat blocks belongs here. */
@media (max-width: 767px) {
  #main .grid.kpis,
  .grid, .grid.g2, .grid.g3, .grid.g4, .grid.cards, .grid.kpis, .grid.signal-cols,
  .kpis, .cards,
  .mkt-grid, .peer-grid, .rp-glance, .sh-kpi-row, .sh-chart-grid, .sh-compare-grid,
  .sh-force-grid, .sh-section-split, .sh-formula-row,
  .signal-cols, .sig-standout-grid, .cbx-stugrid, .cbx-stories, .cbx-gb__owns,
  .campuscards, .role-router, .home-personas, .home-local-cols, .lowses-band,
  .scd-stats, .scd-cols, .scd-grid, .sc-grid, .ds-grid, .sm-grid, .pick-grid,
  .scale-explainer, .scn-out, .eif-grid, .guess-pair, .guess-reveal .gw-stats,
  .glvl-grid, .gcg-grid, .mixsm-grid, .rsa-grid, .atec-grid, .pf-help-grid, .pf-cols,
  /* sector-health.html, which had never linked this stylesheet at all — see the note below */
  .sh-bench-grid, .sh-uni-grid, .sh-uni-card__metrics, .sh-geo-grid, .sh-dg,
  /* the in-card benchmark strip, stacked too: zero exceptions, see the note below */
  .kc-grid {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* Retire the carousel: back to a grid that stacks. See note 1 above. */
  #main .grid.kpis {
    display: grid !important;
    gap: 12px;
    overflow-x: visible !important;
    overflow-y: visible !important;
    scroll-snap-type: none !important;
  }
  #main .grid.kpis > .card {
    flex: none !important;
    max-width: none !important;
    scroll-snap-align: none !important;
  }

  /* See note 2 above: the track floor is only half of it. A card whose figure is wider than the
     column should let the figure scroll inside the card rather than drag the layout sideways. */
  .grid > .card, .kpis > .card, .cards > .card, .sh-kpi-row > .card,
  .mkt-grid > .card, .peer-grid > .card { min-width: 0 !important; }

  /* See note 3 above: the figure gets the whole line, the sparkline takes the next one. The
     figure keeps `nowrap` so a long money value can never break across lines mid-number; a
     block-level child starts a new line regardless of the parent's white-space. */
  .kpi .value .kpi-spark,
  .card .value .kpi-spark {
    display: block !important;
    margin: 4px 0 0 !important;
  }

  /* Charts were never the problem and were already full width; named here only so that a future
     reader does not have to check whether the blanket above changed them. */
  .grid.charts, .charts, .grid.kpis.kpis--wide { grid-template-columns: minmax(0, 1fr) !important; }
}

/* ---- 5 · stillness on request ---------------------------------------------------------------
   Every cue drops its motion and keeps its meaning. The two that carry information in the mark
   rather than the movement (the halo's centre dot, the call-to-action ring) are held visible at
   a fixed opacity instead of animating to nothing. */
@media (prefers-reduced-motion: reduce) {
  .cbx-sig::after { animation: none; opacity: .45; }
  .cbx-halo::before { animation: none; opacity: .35; transform: scale(1.9); }
  .cbx-halo::after  { animation: none; opacity: 0; }
  .cbx-spark__line.is-move { animation: none; stroke-width: 2.4; }
  .cbx-cue::after { animation: none; opacity: .4; }
  .cbx-cue__go { animation: none; }
  .cbx-cue--tab { animation: none; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #234C50) 45%, transparent); }
}

/* ---------------------------------------------------------------------------------------------
   The provenance flag's styles lived here and were REMOVED on 26 July 2026 (DS19), along with the
   tooltip machinery in enhance.js. Benjie's ruling: no visible marker on any view, one statement on
   the Method page instead. `CBX.provenance()` now returns an empty string, so `.provflag`,
   `.provflag__mark` and `.provflag__tip` have nothing left to match; about ninety lines of rules
   and a keyframe went with them. Each hub's own styles.css carries a pointer comment where its
   older copy of these rules used to sit, and that pointer now leads nowhere, which is correct.
   --------------------------------------------------------------------------------------------- */

/* ===============================================================================================
   DS17 · WAYFINDING (25 July 2026)

   The four hubs had a tab bar and, on two of them, a sticky "On this tab" strip, and between the
   two there was no statement anywhere on the page of WHERE THE READER WAS. The tab bar highlights
   a view inside a closed dropdown; the section strip was labelled "On this tab" and never named
   the tab. A reader three scroll-screens into the Australian Finance view saw a strip reading "ON
   THIS TAB" and nine pill links, with the answer to "which tab?" 900px above them.

   THE STRIP IS NOT DUPLICATED, IT IS FILLED IN. Adding a second sticky bar for the path would have
   bought wayfinding at the cost of another 40px of chrome on every view, which is the opposite of
   what the July compaction work was for. So the path goes where the label was: the strip now reads
   "My University › Profile › At a glance" and keeps its jump links and its collapse control. One
   sticky bar, one more fact, no extra height.

   New Zealand and Asia had no strip at all, so `CBX.secNav` builds them one from the same markup
   the Australian hub emits, which means every rule below and in each hub's own styles.css applies
   to all four without a fork.
   =============================================================================================== */

.cbx-crumbs { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0 3px; padding-top: 1px; }
.cbx-crumb {
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 11.5px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--ink-3, #6B6557); white-space: nowrap; text-decoration: none;
  background: none; border: 0; padding: 2px 3px; cursor: default; border-radius: 3px;
}
/* The cluster crumb is the one that DOES something: it opens the menu it names, so a reader who
   has read where they are can move from that same place. The view crumb is the destination and is
   deliberately inert, and the section crumb tracks the scroll. */
.cbx-crumb--group { cursor: pointer; }
.cbx-crumb--group:hover, .cbx-crumb--group:focus-visible {
  color: var(--accent, #234C50); background: var(--accent-wash, rgba(35, 76, 80, .08));
}
.cbx-crumb--view { color: var(--ink, #031602); font-weight: 700; letter-spacing: .1em; }
.cbx-crumb--sec { color: var(--ink-3, #6B6557); font-weight: 400; letter-spacing: .09em; text-transform: none; font-size: 10px; }
/* QA7, 26 Jul 2026: the separator measured 3.06:1 on paper at --rule. It is
   decorative, so it is now hidden from assistive tech outright and set at
   --ink-3 (5.10:1) for everyone reading it with their eyes. */
.cbx-crumb-sep { font-style: normal; color: var(--ink-3, #6B6557); font-size: 10px; padding: 0 1px; }
/* On the dark section strip the hubs already invert the label colour; carry that through. */
body:not(.is-home) #main .section-nav .cbx-crumb { color: var(--paper-on-ink-2, #CBC7BB); }
body:not(.is-home) #main .section-nav .cbx-crumb--view { color: #FEFCF7; }
body:not(.is-home) #main .section-nav .cbx-crumb-sep { color: rgba(255, 255, 255, .34); }

/* ---- the forward step ------------------------------------------------------------------------
   "Pulsing buttons should guide users through natural reading flow." The cue that already exists
   (`CBX.cueCtas`) pulses a link the VIEW happens to render, which means the views that render no
   continue link — every view on New Zealand and Asia — got no guidance at all. This is the one
   control that exists on every view of every hub because it is derived from the tab bar itself:
   the next view in this cluster, or the first view of the next cluster when this is the last.
   It reuses `.cbx-cue` from DS14, so it obeys the same finite cycle and the same reduced-motion
   off switch, and it is rationed by the same "once per route per session" memory. */
.cbx-next {
  display: inline-flex; align-items: center; gap: 7px; margin-left: auto;
  font: 600 11.5px var(--display, inherit); letter-spacing: .01em;
  color: var(--ink-2, #2E2A22); background: var(--surface, #FEFCF7);
  border: 1px solid var(--rule-soft, #DDD9CF); border-radius: 999px;
  padding: 5px 13px 5px 14px; cursor: pointer; text-decoration: none; white-space: nowrap;
  box-shadow: var(--shadow-card, 0 1px 2px rgba(3, 22, 2, .06));
  transition: border-color .15s, color .15s;
}
.cbx-next:hover, .cbx-next:focus-visible {
  border-color: var(--accent, #234C50); color: var(--accent-deep, #17383B);
}
.cbx-next__lbl { font-family: var(--mono, ui-monospace, monospace); font-size: 9.5px;
  letter-spacing: .13em; text-transform: uppercase; color: var(--ink-3, #6B6557); }
.cbx-next__go { transition: transform .18s var(--ease-out, ease); }
.cbx-next:hover .cbx-next__go { transform: translateX(3px); }
/* The strip wraps on narrow desktops; without this the button lands under the pills with the
   left margin still auto and reads as an orphan. */
@media (max-width: 900px) { .cbx-next { margin-left: 0; } }

/* ---- the tab bar says which cluster you are in, in the bar itself -----------------------------
   Australia and the UK hide their views inside dropdowns, so the highlighted thing was a menu item
   nobody could see with the menu shut, and all four cluster heads looked identical whichever view
   you were on. The current cluster now carries the accent and names the view it is holding. */
/* 26 Jul 2026 — this was the last teal fill on a black bar, and the most
   prominent one: the "you are here" group head. Teal #234C50 on ink #031602 is
   1.98:1, so the one control telling you where you are was the hardest to see.
   Paper fill, ink text: 11.1:1. */
nav.tabs .tab-group.cbx-here > .tab-head,
nav.tabs a.tab-solo.cbx-here,
nav.tabs .dd.cbx-here > .dd-btn {
  background: var(--paper-bright, #FEFCF7);
  border-color: var(--paper-bright, #FEFCF7);
  color: var(--ink, #031602);
  box-shadow: none;
}
nav.tabs .tab-group.cbx-here > .tab-head .tab-n,
nav.tabs a.tab-solo.cbx-here .tab-n { background: var(--accent-wash, #E1ECEA);
  color: var(--accent-deep, #16363A); opacity: 1; }
/* The view name rides beside the cluster name, in the mono face so it reads as a value against a
   label rather than as a second title. */
.tab-head-now {
  display: inline-flex; align-items: center; max-width: 132px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
  margin-left: 7px; padding-left: 8px; border-left: 1px solid rgba(255, 255, 255, .34);
  font: 700 10px var(--mono, ui-monospace, monospace); letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255, 255, 255, .90);
}
@media (max-width: 1100px) { .tab-head-now { display: none; } }

/* ---- mobile: the switcher moves to the thumb ---------------------------------------------------
   The view switcher was a sticky bar at the TOP of a phone screen, which is the one part of a
   handset a thumb cannot reach on any device sold in the last five years. The sheet it opens
   already rises from the bottom, so the control and the thing it opens were at opposite ends of
   the screen. Pinned to the bottom the two meet, and the reading column gains the top 56px back.

   `env(safe-area-inset-bottom)` keeps it clear of the home indicator; the padding on `body` keeps
   it clear of the footer, which otherwise sat permanently underneath it. */
@media (max-width: 767px) {
  /* 26 Jul 2026, Benjie: "jump to the view, move back to the top on the mobile".
     This bar was pinned to the bottom of the screen as a floating pill. It is
     back where the hubs' own rule puts it — sticky at the top of the reading
     column, in flow, so it scrolls with the page until it reaches the top and
     then stays. That also returns the sticky table headers to reserving the
     bar's real measured height (--mnavh) instead of the flat 50px they were
     given while it lived at the bottom, and removes the 84px of dead space that
     was being held at the foot of every page to clear it. */
  .mobile-nav-wrap {
    position: sticky;
    top: 0;
    bottom: auto;
    left: auto; right: auto;
    margin: 0 12px 14px;
    z-index: 90;
  }
  /* Prev and next flank the trigger so the commonest move (one step along the reading order) is a
     single thumb tap and never opens a sheet at all. 44px is the WCAG 2.2 target minimum. */
  .mnav-step {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; padding: 0;
    border: 1.5px solid rgba(255, 255, 255, .55); border-radius: var(--r-ctl, 5px);
    background: rgba(255, 255, 255, .12); color: #FEFCF7; cursor: pointer;
  }
  .mnav-step:disabled { opacity: .34; cursor: default; }
  .mnav-step:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
  .mnav-row { display: flex; align-items: center; gap: 8px; }
  .mnav-row .mnav-trigger { flex: 1 1 auto; min-width: 0; }
  /* The cue on the phone rides the forward step, which is the whole of the reading-order guidance
     on a handset: the section strip is `position: static` here and scrolls away. */
  .mnav-step.cbx-cue { border-color: #fff; }
}

/* ===============================================================================================
   DS18 · PROGRESSIVE DISCLOSURE AND EDITORIAL CLEANLINESS (25 July 2026)

   Benjie's read: too much text, especially on Method, and the fix is disclosure rather than air.
   Three separate faults sat under that one sentence.

   ONE. The fold that already existed was gated to phones. `foldNotesMobile()` on the Australian and
   UK hubs put every section caveat behind a tap at 640px and below, and did nothing at all above
   it, which is where the reading happens. Measured on the Monash profile at 1280px: eleven caveat
   paragraphs, 297 to 743 characters each, roughly 3,900 characters of standing explanation between
   the reader and nine sections of data. New Zealand and Asia never had the fold in any form.

   TWO. The button said nothing. "About these figures" is a label for a control, not a summary of
   what is behind it, so the only way to find out whether a caveat mattered was to open it. The
   fold now leads with the caveat's own first sentence and hides the rest, which is the "heading
   plus one-line summary" pattern applied to prose rather than to sections.

   THREE. The Method page is one continuous wall. The Australian one runs seventeen h3 sections
   inside two blocks with no structure between them; the UK thirteen. Every one is now a disclosure
   with its own first line showing, and the first is open so the page still has something to read.

   NOTHING IS DELETED. Every sentence that was on the page is still on the page, one interaction
   away, and the provenance markers are lifted OUT of the folded prose onto the summary line so the
   "no human has reviewed this" warning cannot be hidden by the fold that hides its sentence.
   =============================================================================================== */

/* The hubs' own rule is `.note-fold { display: none }` with an override inside a 640px media
   query. This block loads after, so the fold is simply promoted to every width. */
/* IT HAS TO READ AS A FOOTNOTE, NOT AS A WIDGET. The first version gave each fold a full border,
   a tinted fill, a 5px radius and 33px of height. One of those is a tidy control. Nine down a
   profile view is a repeating band of furniture, and the whole point of the fold was to take
   furniture OFF the page. So the box goes and a single muted line stays: a caret, the caveat's own
   first sentence, and a hairline under it. The line is quieter than the body copy it sits above,
   which is what a caveat should be. */
.note-fold {
  display: flex !important; align-items: baseline; gap: 8px; width: 100%;
  margin: 0 0 12px; padding: 3px 0 6px; text-align: left;
  border: 0; border-bottom: 1px solid var(--rule-soft, #DDD9CF); border-radius: 0;
  background: none;
  color: var(--ink-3, #6B6557); font: 400 12.5px var(--text, inherit); cursor: pointer;
  transition: color .15s, border-color .15s;
}
.note-fold::before {
  content: ""; flex: 0 0 auto; align-self: center;
  width: 5px; height: 5px; margin-right: 1px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform .18s;
}
.note-fold[aria-expanded="true"]::before { transform: rotate(45deg); }
.note-fold:hover, .note-fold:focus-visible {
  color: var(--ink-2, #2E2A22); border-bottom-color: var(--accent, #234C50);
}
.note-fold[aria-expanded="true"] { margin-bottom: 6px; }
.note-fold__sum { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.note-fold[aria-expanded="true"] .note-fold__sum { white-space: normal; }
/* The caret carries the affordance, so the word is only there for the screen reader and for the
   fold whose summary runs the full width, where a right-aligned cue confirms there is more. */
.note-fold__more {
  flex: 0 0 auto; font-family: var(--mono, ui-monospace, monospace); font-size: 9px;
  letter-spacing: .13em; text-transform: uppercase; color: var(--ink-3, #6B6557); opacity: .72;
}
.note-fold:hover .note-fold__more, .note-fold:focus-visible .note-fold__more {
  color: var(--accent, #234C50); opacity: 1;
}
section.block > .note.note-folded { display: none; }
section.block > .note.note-folded.note-open { display: block; }
/* The hubs' 640px block repeats these two at the same specificity and later in nothing, so the
   pill styling they carried is overridden above; this keeps the open state honest at every width. */
@media (max-width: 640px) {
  .note-fold { display: flex !important; border-radius: 0; padding: 3px 0 6px; }
}

/* ---- Method: seventeen headings become seventeen disclosures -----------------------------------
   The accordion keeps the real `h3`, with its `id`, as the heading element and puts the button
   inside it. That is the ARIA accordion pattern, and it is also the only shape that survives the
   deep links: `#method` plus a scroll to `#copyright` still resolves, because the element the
   anchor names never moved. `CBX.openAcc` opens whichever one holds the target. */
.cbx-acc { border-top: 1px solid var(--rule-soft, #DDD9CF); }
.cbx-acc:last-of-type { border-bottom: 1px solid var(--rule-soft, #DDD9CF); }
.cbx-acc > h3, .cbx-acc > h4 { margin: 0 !important; }
.cbx-acc__h {
  display: flex; align-items: baseline; gap: 12px; width: 100%;
  margin: 0; padding: 13px 4px 13px 0; text-align: left;
  background: none; border: 0; cursor: pointer; color: inherit; font: inherit;
}
.cbx-acc__h:hover .cbx-acc__t, .cbx-acc__h:focus-visible .cbx-acc__t { color: var(--accent-deep, #17383B); }
.cbx-acc__t { flex: 0 1 auto; color: var(--ink, #031602); }
/* The summary is the heading's own first sentence, clamped to one line. It is what makes the
   collapsed page readable as an index rather than as a list of titles. */
.cbx-acc__sum {
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 400 12.5px var(--text, inherit); color: var(--ink-3, #6B6557);
}
.cbx-acc__i {
  flex: 0 0 auto; width: 8px; height: 8px; margin-left: auto; margin-right: 4px;
  border-right: 2px solid var(--ink-3, #6B6557); border-bottom: 2px solid var(--ink-3, #6B6557);
  transform: rotate(45deg); transition: transform .18s;
}
.cbx-acc__h[aria-expanded="true"] .cbx-acc__i { transform: rotate(-135deg); }
.cbx-acc__h[aria-expanded="true"] .cbx-acc__sum { visibility: hidden; }
.cbx-acc__body { padding: 0 0 16px; }
.cbx-acc__body[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .cbx-acc__i, .cbx-next__go { transition: none; } }
/* Two disclosures' worth of controls at the head of the Method page, so the whole register can be
   opened for printing or searching in one action. */
.cbx-acc-all {
  display: inline-flex; gap: 8px; margin: 0 0 6px;
}

/* ---- consistent card heights within a row -----------------------------------------------------
   Measured on the Monash profile at 1280px: a row of ten stat cards at 338, 338, 338, 352, 338,
   338, 352, 423, 338, 352 pixels. The grid was packing them at content height, so a card with a
   two-line reference note stood 85px taller than its neighbours and the row read as a ragged
   shelf. `1fr` on the auto rows makes each ROW settle on its own tallest card, which is the ask,
   and does not lock every row on a view to one height. `min-height: 0` is on the item because a
   grid item defaults to `min-height: auto` and refuses to shrink into a stretched track. */
#main .grid.kpis, #main .grid.cards, #main .grid.charts { align-items: stretch; }
@media (min-width: 768px) {
  #main .grid.kpis { grid-auto-rows: 1fr; }
  #main .grid.kpis > * { height: 100%; min-height: 0; }
}

/* ---- one clear entry point per view ------------------------------------------------------------
   Every section on a view carried identical weight, so a reader arriving on a nine-section page
   had no first place to look. The opening section takes a hairline accent rule and a slightly
   larger heading: the magazine convention of a standfirst, which costs 3px of height and settles
   where the eye starts. */
/* The accent rule that used to sit here is gone: `_canonical/SKILL.md` forbids coloured left/top
   accent bars on boxes, and this was the most visible one on the estate (the teal line across the
   top of the opening section of every view, on every hub). The larger heading below does the
   standfirst job on its own, which is the canonical's "containment relies on type" case. */
#main > section.block:first-of-type > h2 { margin-top: 0; }
#main > section.block:first-of-type > h2 { font-size: clamp(23px, 2.4vw, 29px); }
/* A heading that only repeats the title in the banner above it is not an entry point, it is the
   same sentence twice. Hidden from sight, kept for the screen reader, which announces the banner
   and the heading in different contexts. */
/* A DUPLICATE HEADING IS HIDDEN FROM SIGHT AND KEPT FOR ASSISTIVE TECHNOLOGY, because a screen
   reader navigates by heading and losing one from the outline costs something a sighted reader
   does not lose. */
.cbx-dupe-h { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
/* A DUPLICATE EYEBROW IS REMOVED OUTRIGHT, which is a different judgement for a different element.
   It is not a heading, it is not in the outline, and the crumb that replaced it carries the same
   words plus an `aria-label` reading "You are here: …", so nothing is lost by taking it out of the
   accessibility tree as well. Clipping it instead left a 22 x 8px box in the banner's flex row and
   a gap where the chip used to be: hidden, but still costing layout. */
.ph-kick.cbx-kick-dupe { display: none !important; }

/* ---- the site header overflowed the page, and only on one hub ----------------------------------
   Found during the DS18 sweep, not caused by it: the UK hub scrolled sideways by 14px at 1280,
   with or without this stylesheet. `.site-head__inner` is a flex row whose children keep the
   default `min-width: auto`, so `.head-search` — 286px of brand mark, six nav links, a hint
   reading "Search: Manchester, days of cash…" and a ⌘K chip — cannot shrink below its max-content
   width and simply hangs past the edge. Australia fits the same row at 1280 and so never showed
   it; the UK container went to 1440px on 24 July and the row stopped fitting.

   The fix is the standard one for this fault and it is safe on all four hubs: let the item shrink
   and let the one piece of prose in the row truncate. Where the row already fits, nothing moves.

   AND THE RULE IS ON THE SEARCH CONTROL ONLY, which the first attempt got wrong. Applying
   `min-width: 0` to every child of the row let `.nav` shrink below its own content too, and flex
   does not clip an over-full inline row, it lets it run on: at 1280 the Australian header put the
   search box straight over the word "Contact". The search control is the one item in the row with
   a piece of prose in it that can be shortened without losing anything, so it is the one item that
   is allowed to give. */
.site-head__inner > .head-search { min-width: 0; }
.head-search-hint { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- the path needs its own line on a phone ----------------------------------------------------
   Caught at 390px on the Asia hub and it is a regression this block caused. The strip is a flex row
   of a label and a `flex: 1` chip list, and the label used to read "ON THIS TAB" in 9.5px mono,
   which is narrow enough to leave the chips the rest of the bar. Replacing it with a path made it
   roughly 270px of the 364 available, and the chip list — which is a nowrap horizontal scroller on
   a phone — was left a 87px window onto 1,521px of chips.

   The strip already wraps, so the label only has to claim a row. Desktop is untouched: there the
   bar is wide enough for both and the two on one line is the compact arrangement. */
@media (max-width: 767px) {
  #main .section-nav > .sn-label { flex: 1 0 100%; padding-top: 0; margin-bottom: 2px; }
  /* The chip scroller takes a row of its own for the same reason: sharing one with the forward
     step left it 205px of a 366px bar to scroll 1,500px of chips through. */
  #main .section-nav > .sn-links { flex: 1 0 100%; }
  /* AND THE FORWARD STEP COMES OFF THE STRIP ENTIRELY ON A PHONE, because the thumb bar's ▶ does
     the same job and is fixed to the bottom of the screen where it is always in reach. Two controls
     for one move, one of which scrolls away, is the same duplication the desktop strip already
     resolved in the hub's favour. Dropping it takes a whole row off the strip: measured 139px to
     97px on the Asia Method view at 390px. */
  #main .section-nav .cbx-next { display: none; }
}


/* ============================================================================
   THE SELECTION PANEL + THE HUB SWITCHER (26 Jul 2026)
   ----------------------------------------------------------------------------
   Companion to the CBX.picker / CBX.hubSwitcher block in enhance.js. Tokens
   only — no hex values here, the canonical is the sole authority.

   The rules being applied, from Benjie's brief:
     · generous panel, not a cramped dropdown
     · typography with room between items
     · scannable at a glance even at 157 items
     · quiet hover: an underline or an opacity shift, never a colour block
     · grouped, with quiet section headers, where the list is long
     · paper surface, hairline, no heavy border or shadow
   ========================================================================== */

/* The native control stays in the DOM as the source of truth and the no-JS
   fallback. It is taken out of the pointer and AT layer only once its panel
   exists, so a failed script leaves a working select rather than nothing. */
/* 26 Jul 2026 — this rule was losing a specificity fight and taking the mobile
   layout with it. `.cbx-sel--behind` is one class (0-1-0); each hub's own
   `.instselect select` and `.controls select` are class-plus-element (0-2-0)
   and set `width: 100%`. So the native control kept its full width while the
   clip-path went on hiding it: an invisible 375px-wide box sitting at the
   picker's x-offset, its right edge landing 208px past the viewport. That was
   the initiator of the horizontal scroll on every phone-width dashboard view —
   the fixed overlays then locked the widened scroll width in place.
   Selector raised to `select.cbx-sel--behind` (0-2-1) so it outranks both, with
   !important on the two properties a future hub rule is most likely to reset.
   Visually-hidden must beat everything: it is not decoration, it is layout. */
.cbx-sel--behind,
select.cbx-sel--behind {
  position: absolute;
  left: 0; top: 0;
  width: 1px !important; height: 1px !important;
  min-width: 0 !important; max-width: 1px !important;
  margin: -1px; padding: 0; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* Short lists keep the native control. A five-option selector does not need an
   overlay; it needs to stop looking like a 2009 form field. House type, a
   hairline under it, no box. */
select.cbx-sel--quiet {
  font-family: var(--mono);
  font-size: 14px;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  padding: 7px 22px 7px 0;
  min-height: 40px;
  appearance: none; -webkit-appearance: none;
  /* A chevron, drawn as two hairlines, matching .cbx-pick__caret exactly. The
     native filled triangle read as a different control language beside the
     picker triggers, and the two sit in the same row. */
  background-image:
    linear-gradient(45deg, transparent 49%, var(--ink-3) 49%, var(--ink-3) 62%, transparent 62%),
    linear-gradient(-45deg, transparent 49%, var(--ink-3) 49%, var(--ink-3) 62%, transparent 62%);
  background-position: right 8px center, right 3px center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  transition: border-color 180ms ease;
}
select.cbx-sel--quiet:hover { border-bottom-color: var(--ink); }
select.cbx-sel--quiet:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---- The trigger --------------------------------------------------------- */
/* Reads as a line of type with a rule under it, the same as the quiet select.
   The two treatments have to agree: a reader should not be able to tell which
   controls got a panel and which did not until they open one. */
.cbx-pick__trigger {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  max-width: 44ch;
  padding: 7px 0;
  min-height: 40px;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  text-align: left;
  cursor: pointer;
  transition: border-color 180ms ease;
}
.cbx-pick__trigger:hover { border-bottom-color: var(--ink); }
.cbx-pick__trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.cbx-pick__val {
  flex: 1 1 auto;
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.3;
  color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cbx-pick__val.is-empty { color: var(--ink-3); }
.cbx-pick__caret {
  flex: 0 0 auto;
  width: 7px; height: 7px;
  border-right: 1px solid var(--ink-3);
  border-bottom: 1px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
}

/* ---- The panel ----------------------------------------------------------- */
.cbx-pick-scrim {
  position: fixed; inset: 0; z-index: 300;
  background: rgba(3, 22, 2, 0.28);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
body.cbx-pick-locked { overflow: hidden; }

.cbx-pick {
  position: fixed;
  z-index: 301;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(720px, calc(100vw - 2 * var(--gutter, 24px)));
  max-height: min(78vh, 760px);
  display: flex;
  flex-direction: column;
  /* Paper surface, one hairline, one faint lift. Not a heavy container. */
  background: var(--paper-bright);
  border: 1px solid var(--rule-hairline);
  border-radius: 10px;
  box-shadow: 0 2px 6px rgba(20, 17, 13, .04), 0 12px 28px -12px rgba(20, 17, 13, .10);
  overflow: hidden;
}
.cbx-pick--in { animation: cbx-pick-in 180ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
@keyframes cbx-pick-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)); }
  to   { opacity: 1; transform: translate(-50%, -50%); }
}
@media (prefers-reduced-motion: reduce) { .cbx-pick--in { animation: none; } }

.cbx-pick__head {
  flex: 0 0 auto;
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 22px 26px 14px;
  border-bottom: 1px solid var(--rule-hairline);
}
.cbx-pick__kick {
  font-family: var(--mono);
  font-size: 11px; font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3);
}
.cbx-pick__x {
  background: none; border: 0; padding: 0 0 2px; cursor: pointer;
  font-family: var(--mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--rule-hairline);
  transition: color 180ms ease, border-color 180ms ease;
}
.cbx-pick__x:hover { color: var(--ink); border-bottom-color: var(--ink); }

.cbx-pick__find {
  flex: 0 0 auto;
  display: flex; align-items: baseline; gap: 16px;
  padding: 16px 26px;
  border-bottom: 1px solid var(--rule-hairline);
}
.cbx-pick__input {
  flex: 1 1 auto;
  font-family: var(--mono); font-size: 17px;
  color: var(--ink); background: none;
  border: 0; padding: 4px 0;
}
.cbx-pick__input::placeholder { color: var(--ink-4); }
.cbx-pick__input:focus { outline: none; }
.cbx-pick__find:focus-within { box-shadow: inset 0 -1px 0 var(--ink); }
.cbx-pick__count {
  flex: 0 0 auto;
  font-family: var(--mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3);
}

.cbx-pick__list {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* No top padding: the group headings are sticky, and an 8px band above the
     first one let scrolled rows show through over it. The first heading's own
     padding provides the air instead. */
  padding: 0 0 14px;
}

/* Section heading. Quiet: caption scale, tracked, grey, sitting on a hairline
   with air above it. A label, not a band — the retired treatment put a filled
   coloured strip here and it read as chrome. */
.cbx-pick__grp {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  margin: 0;
  padding: 20px 26px 7px;
  font-family: var(--mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--rule-hairline);
  position: sticky; top: 0;
  background: var(--paper-bright);
}
.cbx-pick__list > .cbx-pick__grp:first-child { padding-top: 8px; }
/* --ink-4 is 2.91:1 on paper-bright, under AA. This is a live count a reader
   uses to judge a group's size, not decoration, so it takes --ink-3 (5.65:1). */
.cbx-pick__grpn { color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* The row. Generous height, full-width hit area, left-aligned to one grid line.
   Hover is an opacity/colour shift on the type plus a hairline, never a filled
   block: a coloured band under a name is the thing the brief rules out. */
.cbx-pick__opt {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  padding: 13px 26px;
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.35;
  color: var(--ink-2);
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease;
}
.cbx-pick__opt:hover { color: var(--ink); border-bottom-color: var(--rule-hairline); }
.cbx-pick__opt:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.cbx-pick__opt[disabled] { color: var(--ink-4); cursor: default; }
.cbx-pick__opt[disabled]:hover { border-bottom-color: transparent; }

/* The current choice. Marked by weight and a rule, not by a fill. */
.cbx-pick__opt.is-on {
  color: var(--ink);
  font-weight: 650;
  box-shadow: inset 2px 0 0 var(--accent);
}

.cbx-pick__none {
  margin: 0; padding: 28px 26px;
  font-family: var(--serif, Georgia, serif);
  font-size: 15px; color: var(--ink-3);
}

/* Phones: the panel becomes a sheet. Same type, same rhythm, full width. */
@media (max-width: 640px) {
  .cbx-pick {
    left: 0; right: 0; bottom: 0; top: auto;
    transform: none;
    width: auto;
    max-height: 86vh;
    border-radius: 10px 10px 0 0;
    border-bottom: 0;
  }
  .cbx-pick--in { animation: cbx-pick-up 200ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
  @keyframes cbx-pick-up { from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } }
  .cbx-pick__head, .cbx-pick__find { padding-left: 20px; padding-right: 20px; }
  .cbx-pick__grp, .cbx-pick__opt { padding-left: 20px; padding-right: 20px; }
}

/* ---- The hub switcher ---------------------------------------------------- */
/* Four names, right-aligned in the tab bar the page already has. It went in the
   masthead first and overflowed it: the site header at 1280px already carries a
   brand lockup and a six-item nav, and four more links wrapped on top of them.
   Putting it in nav.tabs adds no surface at all — the bar is already there, the
   switcher just sits at the far end of it, after a hairline. */
.cbx-hubs {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 3px 14px;
  margin-left: auto;                       /* push to the end of the tab bar */
  padding-left: 16px;
  border-left: 1px solid rgba(246, 242, 236, 0.14);
}
.cbx-hubs__kick {
  font-family: var(--mono); font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase;
  /* 50% was 3.55:1 on the ink bar — under AA, and at 10.5px this does not
     qualify as large text. 72% is 6.15:1 and still reads as a quiet label. */
  color: rgba(203, 199, 187, 0.72);
}
.cbx-hubs__a {
  font-family: var(--mono);
  font-size: 12.5px;
  color: rgba(203, 199, 187, 0.72);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color 180ms ease, border-color 180ms ease;
}
.cbx-hubs__a:hover { color: var(--paper-on-ink); border-bottom-color: rgba(246, 242, 236, 0.4); }
.cbx-hubs__a.is-on { color: var(--paper-on-ink); border-bottom-color: var(--paper-on-ink); }
.cbx-hubs__a:focus-visible { outline: 2px solid var(--accent-soft, var(--accent)); outline-offset: 3px; }

@media (max-width: 1080px) {
  .cbx-hubs { margin-left: 0; padding-left: 0; border-left: 0; width: 100%;
              padding-top: 8px; border-top: 1px solid rgba(246, 242, 236, 0.14); }
}
@media (max-width: 620px) { .cbx-hubs__kick { display: none; } }


/* ---- The section dropdowns (nav.tabs) ------------------------------------ */
/* These were a dark panel with a 2px coloured top border, a deep drop shadow,
   filled hover blocks and a filled active state — five devices where one rule
   and a weight will do. Brought to the same language as the selection panel:
   paper surface, one hairline, generous rows, hover as a colour shift on the
   type, current item marked by weight and a hairline rather than a fill.
   They keep their position, their open/close behaviour and their keyboard
   handling — this is presentation only. */
nav.tabs .tab-menu {
  min-width: 260px;
  gap: 0;
  background: var(--paper-bright);
  border: 1px solid var(--rule-hairline);
  border-top: 1px solid var(--rule-hairline);
  border-radius: 10px;
  padding: 6px 0;
  box-shadow: 0 2px 6px rgba(20, 17, 13, .04), 0 12px 28px -12px rgba(20, 17, 13, .10);
}
nav.tabs .tab-menu a {
  gap: 12px;
  padding: 11px 18px;
  border-radius: 0;
  border-bottom: 1px solid transparent;
  color: var(--ink-2);
  font: 500 14px var(--mono);
  transition: color 150ms ease, border-color 150ms ease;
}
nav.tabs .tab-menu a:hover {
  background: none;
  color: var(--ink);
  border-bottom-color: var(--rule-hairline);
}
nav.tabs .tab-menu a.active {
  background: none;
  color: var(--ink);
  font-weight: 650;
  box-shadow: inset 2px 0 0 var(--accent);
}
nav.tabs .tab-menu a.active .tab-let { background: var(--accent); color: var(--paper-bright); }
nav.tabs .tab-menu .tab-let {
  background: var(--paper-3, rgba(20, 17, 13, .07));
  color: var(--ink-3);
  border-radius: 3px;
}
/* Group labels inside a menu get the same quiet heading as the panel's. */
nav.tabs .tab-menu .tab-menu-grp {
  padding: 14px 18px 6px;
  font: 500 10.5px var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom: 1px solid var(--rule-hairline);
}

/* 26 Jul 2026 — no link on a CoreBridge page renders in browser-default blue.
   Found on the Asia market view: the "Asia Markets" breadcrumb was
   rgb(0,0,238), the UA default, because nothing in the cascade ever gave it a
   colour. §2 rules out blue outside the ink/teal/gold system entirely. Four
   more were latent in hidden markup (the cross-hub comparison table's header
   links and the mobile-nav brand), which would have surfaced the moment those
   states rendered. Fixed at the pattern level rather than one at a time. */
#main .eyebrow a,
.mobile-nav-brand a { color: inherit; text-decoration: none; }
#main .eyebrow a { border-bottom: 1px solid var(--rule-soft); }
#main .eyebrow a:hover { color: var(--ink); border-bottom-color: var(--ink); }
.cbx-gb__t a { color: var(--accent-deep); }
/* Safety net: any anchor inside the reading column that no rule has coloured
   takes the accent, never the UA blue. */
:where(#main) a:not([class]) { color: var(--accent-deep); }
/* 26 Jul: `:where()` zeroes the #main specificity so this stays a last
   resort (0-1-0) and can never outrank a component's own ground-aware
   colour. Written with an ID first time round, it repainted the 39
   university links on the dark landing card at 1.45:1. */

/* =========================================================================
   GOOD / BAD PULSES — added 26 Jul 2026 (Benjie: "add more green and red
   pulses on key elements on cards and charts, also moving lines when
   required, and pulses across the dashboard").

   Rules this obeys, so it stays a signal and not decoration:

   1. It only ever marks a reading the data already calls good or bad. Every
      hook below is an existing semantic class (.good/.bad, --up/--down,
      quartile flags). Nothing pulses because it is important-looking; it
      pulses because the dataset declares a favourable direction and this
      value sits at one end of it. Measures with no declared direction — the
      whole New Zealand register, for one — never pulse.
   2. It runs three cycles and stops. Perpetual motion in a reading tool is
      noise: after the third cycle the eye stops seeing it and it becomes a
      flicker you cannot turn off. Three says "here", then leaves you alone.
   3. It fires when the element scrolls into view, not on load, so a pulse
      below the fold is not spent before it is seen.
   4. prefers-reduced-motion removes all of it and leaves the colour, which
      carried the meaning in the first place.
   ========================================================================= */
@keyframes cb-pulse-good {
  0%   { box-shadow: 0 0 0 0 rgba(61, 90, 61, .45); }
  70%  { box-shadow: 0 0 0 9px rgba(61, 90, 61, 0); }
  100% { box-shadow: 0 0 0 0 rgba(61, 90, 61, 0); }
}
@keyframes cb-pulse-bad {
  0%   { box-shadow: 0 0 0 0 rgba(142, 46, 46, .45); }
  70%  { box-shadow: 0 0 0 9px rgba(142, 46, 46, 0); }
  100% { box-shadow: 0 0 0 0 rgba(142, 46, 46, 0); }
}
/* the JS adds .cb-lit when the element enters the viewport */
.cb-pulse-good.cb-lit { animation: cb-pulse-good 1.6s ease-out 3; }
.cb-pulse-bad.cb-lit  { animation: cb-pulse-bad  1.6s ease-out 3; }

/* A chart line that draws itself in. Only for inline SVG series — the Chart.js
   canvases have animation:false set deliberately in app.js and are left alone,
   because re-enabling it there re-animates on every tooltip and legend toggle. */
@keyframes cb-draw { to { stroke-dashoffset: 0; } }
.cb-drawline.cb-lit {
  animation: cb-draw 1.1s cubic-bezier(.4, 0, .2, 1) forwards;
}

@media (prefers-reduced-motion: reduce) {
  .cb-pulse-good.cb-lit,
  .cb-pulse-bad.cb-lit,
  .cb-drawline.cb-lit { animation: none; }
  .cb-drawline { stroke-dashoffset: 0 !important; }
}

/* =========================================================================
   BLOOMBERG DISCIPLINE — dense tables. 26 Jul 2026.
   Benjie asked for "a Bloomberg approach to key parts of the dashboard", and
   chose its information discipline inside the house skin rather than the
   terminal look. So: none of this is dark, monospaced or amber. What it takes
   from a terminal is how figures BEHAVE.

     · figures right-align and share one width, so a column of numbers can be
       compared down its last digit instead of read one at a time
     · the header stays on screen, because a long table you have to scroll back
       up to interpret is a table you read twice
     · a density control, because the right row height depends on whether you
       are scanning thirty-nine rows or reading three
     · sort on any column, because the question "who is top" should not require
       a different view
     · a stripe so faint it is a texture, not a band (§2A.4 rules out coloured
       bands; 2% ink is a scanning aid, not decoration)
   ========================================================================= */
table.data { font-variant-numeric: tabular-nums; }
table.data th, table.data td { font-variant-numeric: tabular-nums; }

/* numeric columns: right-aligned, tabular. .is-num is set by the enhance JS from
   the actual cell contents, so a column of names never gets pushed right. */
table.data th.is-num, table.data td.is-num { text-align: right; }

/* a texture, not a band */
table.data tbody tr:nth-child(even) td { background: rgba(20, 17, 13, .02); }
table.data tbody tr:hover td { background: var(--accent-wash, #E1ECEA); }
table.data tr.me td, table.data tbody tr.me:hover td { background: var(--accent-wash); font-weight: 600; }

/* the header stays put. --headh/--navh are the measured sticky stack. */
table.data thead th {
  position: sticky;
  top: calc(var(--headh, 0px) + var(--navh, 0px) + var(--mnavh, 0px));
  z-index: 3;
}

/* sortable affordance — a caret that only appears once the column is sortable */
table.data th[data-sortable] { cursor: pointer; user-select: none; white-space: nowrap; }
table.data th[data-sortable]::after {
  content: "";
  display: inline-block; width: 0; height: 0; margin-left: 6px; vertical-align: middle;
  border-left: 3.5px solid transparent; border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor; opacity: .28;
}
table.data th[aria-sort="ascending"]::after  { border-top: 0; border-bottom: 4px solid currentColor; opacity: 1; }
table.data th[aria-sort="descending"]::after { opacity: 1; }
table.data th[data-sortable]:hover::after { opacity: .7; }
table.data th[data-sortable]:focus-visible { outline: 2px solid var(--accent-soft); outline-offset: -2px; }

/* density: the comfortable default stays the default; dense is opt-in */
table.data.is-dense th { padding: 5px 10px; }
table.data.is-dense td { padding: 4px 10px; font-size: var(--fs-chrome-sm, 12.5px); }
table.data.is-dense tbody tr td { line-height: 1.3; }

.tbl-tools {
  display: flex; align-items: center; justify-content: flex-end; gap: 10px;
  margin: 0 0 6px;
}
.tbl-tools__btn {
  font-family: var(--display); font-size: var(--fs-caption, 10.5px); font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3);
  background: transparent; border: 1px solid var(--rule-soft); border-radius: 999px;
  padding: 5px 12px; cursor: pointer; transition: border-color .15s, color .15s;
}
.tbl-tools__btn:hover, .tbl-tools__btn[aria-pressed="true"] { color: var(--accent-deep); border-color: var(--accent); }
.tbl-tools__btn[aria-pressed="true"] { background: var(--accent-wash); }

@media (max-width: 640px) {
  /* on a phone the sticky header costs more than it returns — it eats a third
     of a short viewport — and the tables already scroll in their own box. */
  table.data thead th { position: static; }
  .tbl-tools { justify-content: flex-start; }
}

/* --- the key-metrics tape -------------------------------------------------
   Terminal discipline, house materials: figures at display size, tabular, on
   one baseline; the label above in the mono register; the movement beside it
   with its sign and its baseline year, because a percentage with no baseline
   is not a fact. Paper ground, hairline rules — the only colour is the green
   and red on the deltas, which is the one thing here that carries a judgement
   the data itself makes. */
.cb-tape {
  /* #main carries no horizontal padding on these hubs — the section blocks
     supply their own — so the tape, as a direct child, has to carry the gutter
     itself or it sits on the screen edge. */
  margin: 0 0 clamp(20px, 2.4vw, 30px);
  /* 18px, was 12px: the closing rule sat closer to the release stamp than the stamp sat to the
     figures above it, so the line read as attached to the wrong thing. */
  padding: 0 var(--col-pad, 16px) 18px;
  border-bottom: 1px solid var(--rule-soft);
}
/* SPACE AROUND THE RULES, 28 Jul 2026 (Benjie: "the lines and the text don't have space").
   The cells carried `padding: 12px 18px 12px 0` with a `gap: 0` grid, so each figure had 18px
   before the rule on its right and NOTHING after the rule on its left: every value but the first
   started hard against a vertical line. The space now comes from the grid's column-gap instead
   of from asymmetric padding, which fixes two things at once. The rule ends up centred in the
   gap — 20px of clear air either side — and because no cell carries left padding, every cell's
   text starts exactly at its track edge. That second part matters because the row is `auto-fit`:
   the column count changes with the viewport, so a first-cell exception would align the top row
   and leave any wrapped row indented by 20px against it. */
.cb-tape__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  row-gap: 0;
  column-gap: 20px;
}
.cb-tape__cell {
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 20px 12px 0;
  border-right: 1px solid var(--rule-soft);
}
.cb-tape__cell:last-child { border-right: 0; }
.cb-tape__k {
  font-family: var(--mono); font-size: var(--fs-caption, 10.5px); font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cb-tape__v {
  font-family: var(--display); font-weight: 700;
  font-size: clamp(22px, 2.2vw, 30px); line-height: 1.05;
  letter-spacing: -.02em; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.cb-tape__d {
  font-family: var(--display); font-size: var(--fs-chrome-sm, 12.5px); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.cb-tape__d--up   { color: var(--success, #3D5A3D); }
.cb-tape__d--down { color: var(--danger,  #8E2E2E); }
.cb-tape__d--flat { color: var(--ink-4); font-weight: 500; }
.cb-tape__since { color: var(--ink-3); font-weight: 500; }
.cb-tape__stamp {
  margin-top: 14px;
  font-family: var(--mono); font-size: var(--fs-caption, 10.5px);
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-4);
}
@media (max-width: 640px) {
  .cb-tape__row { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 14px; }
  .cb-tape__cell { padding: 10px 14px 10px 0; }
  .cb-tape__cell:nth-child(2n) { border-right: 0; padding-right: 0; }
  .cb-tape__v { font-size: 21px; }
}

/* the one-line read under every chart */
.cb-chartread {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px;
  margin-top: 10px; padding-top: 8px;
  border-top: 1px solid var(--rule-soft);
}
.cb-chartread__v {
  font-family: var(--display); font-weight: 700; font-size: var(--fs-small, 14px);
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.cb-chartread__d { font-family: var(--display); font-weight: 600; font-size: var(--fs-chrome-sm, 12.5px);
  font-variant-numeric: tabular-nums; }
.cb-chartread__d--up   { color: var(--success, #3D5A3D); }
.cb-chartread__d--down { color: var(--danger,  #8E2E2E); }
/* ink-4 at 10.5px measured 2.91:1 on paper-bright, well under the 4.5 AA floor
   for text this size. ink-3 is 5.57:1 and still reads as the quiet label it is.
   Caught 26 Jul in the site-wide contrast audit. */
.cb-chartread__span, .cb-chartread__series {
  font-family: var(--mono); font-size: var(--fs-caption, 10.5px);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
}

/* --- profile figures: the last of the tabular pass ------------------------
   The headline value and the context grid were already tabular; the movement
   badge and the reference year were not. Down a column of fifteen cards that
   is the difference between percentages that line up on the decimal and
   percentages that wander. Nothing else on the profile needed changing: the
   context is already unfolded, the ranks and medians already sit inline.

   Note on what is deliberately NOT here — a "no movement" row on the cards
   that lack one. Those five are the TEQSA status cards: registration period,
   provider category, self-accrediting authority. They are categorical facts,
   not series. A delta slot on them would invite a reading that does not exist,
   which is the same error as the chart trend that had to be pulled today. */
.card.kpi .kpi-yoy,
.card.kpi .ref,
.kpi .kpi-yoy,
.kpi .ref { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   MENU TYPOGRAPHY AND BAND SPACING, ONE ANSWER FOR ALL FOUR HUBS
   27 Jul 2026 (Benjie: "review the font size across all the menus, they are
   all different, fix to ensure the same approach across all").

   Measured before writing this, on the live build, at 1440px. The chrome was
   carrying SEVEN different sizes: 14px on the Australian tab links, 13.5px on
   Asia's dropdown buttons, 13px on its dropdown items and market chips and on
   the cross-hub links, 12.5px on the section jump links, 11.5px on the forward
   step, 11px on the solo tabs. Nothing chose those numbers together; they are
   the residue of four hubs forked at different times and patched separately.

   Every clickable item in the menu chrome now takes --fs-chrome, the house
   token, which is what the Australian solo tabs and the cross-hub row were
   already closest to. Two things are deliberately NOT normalised and should
   stay that way: the numerals in .tab-n / .tab-let, which are mono index chips
   rather than labels, and .cbx-crumb, which is a breadcrumb and reads as
   apparatus above the bar rather than as an item in it.
   ========================================================================== */
nav.tabs a,
nav.tabs button,
nav.tabs .tab-solo,
nav.tabs .tab-head,
nav.tabs .tab-kicker,
nav.tabs .dd-btn,
nav.tabs .dd-item,
nav.tabs .mkt-tab,
#main .section-nav .sn-link,
#main .section-nav .cbx-next,
.cbx-hubs__a { font-size: var(--fs-chrome, 13px); }

/* The two-level selectors have to be restated, because a single-level rule cannot reach past
   them. `nav.tabs .tab-menu a` earlier in this same file sets `font: 500 14px var(--mono)`, and
   a shorthand at (0,2,1) beats a longhand at (0,1,1) whatever the order. Only the SIZE is taken
   back here; the mono family those cluster menus use is deliberate and is left alone. */
nav.tabs .tab-menu a,
nav.tabs .tab-cluster a,
nav.tabs .tab-group > .tab-head,
nav.tabs .tab-cluster .tab-kicker { font-size: var(--fs-chrome, 13px); }

/* The band sits flush against whatever follows it. The 14px it carried was
   invisible on Australia and the UK, which drop a hero band underneath, and
   read as a stray gap between two bars on New Zealand and Asia, which do not.
   Spacing below the band is now owned by the element that follows, so there is
   one behaviour rather than one that depends on what a hub happens to render. */
nav.tabs { margin-bottom: 0; }

/* Asia's market sections belong ON the menu line, not stacked under it. Given
   their own row they doubled the band's height (126px against Australia's 56px),
   which is what "not working in the design" was: the same bar, twice as tall,
   on one hub. They now sit inline with the Views, Markets and Method pills and
   the band scrolls sideways if the row runs long, which is the behaviour the
   band already had on a phone. */
/* The bar is kept in the DOM because it still owns the panel switching, but it is driven from
   the Markets dropdown now and never laid out. */
nav.tabs .mkt-tabs--inband,
.mkt-tabs--offstage { display: none !important; }
/* Nothing in the band may wrap onto a second row: a menu that changes height between views is
   the fault this whole pass was chasing. */
nav.tabs .cbx-hubs { flex-wrap: nowrap; }

/* =========================================================================
   27 Jul 2026, Benjie: "the font size for the sub page headings on the dashboard is to big",
   then "a complete font size and font approach across the dashboard — it needs to be same
   approach across all four — remember refined style".

   WHY THIS EXISTS. A generic ladder was appended to every hub's styles.css on 27 Jul
   (h1 clamp(34,5.4vw,72) down to h4), but nothing on a dashboard ever used it: every heading was
   already claimed by a more specific ad-hoc rule, so the ladder was decorative. Measured on NZ at
   1440px before this block:

     landing title    52px   body.is-home .page-hero h1        clamp(30, 4.4vw, 52)
     SUB-PAGE title   25px   body:not(.is-home) .page-hero h1  clamp(19, 2.3vw, 25)
     section head     29px   #main > section.block:first-of-type > h2   <- first section only
     section head     26px   section.block > h2 / .sh-h2               <- every other section
     card head        15px   .chartcard h3 (= --fs-card, the CARD BODY size)

   Three faults in that: the first section's heading is a different size from every other one; a
   card heading is set to the same token as card body, so it is a heading in name only; and on a
   sub page the section heading (29) is LARGER than the page title (25), which inverts the
   hierarchy on every drilled-in view. That is the "too big" Benjie is seeing.

   THE LADDER. Every step below is a step on the canonical scale in `_canonical/SKILL.md` §3
   (11 / 12 / 14 / 18 / 22 / 24 / 32 / 48 / 72) — no invented sizes, which is what makes it the
   same approach rather than four sets of near-misses. The dashboard sits one register down from
   the editorial pages because the canonical has analytical pages default dense:

     48  landing page title      (canonical H2)
     32  sub-page title          (canonical H3)   was 25, now clears the section head
     24  section heading         (canonical H4)   was 26/29, single value
     18  card heading            (canonical H5)   was 15, now distinct from card body
     14  body and card body      (canonical body)
     12  meta, captions, axes    (canonical meta) was 10.5, under the 11px floor
     11  eyebrow and kicker      (canonical label)

   Authored in enhance.css, not in a hub styles.css, for a reason: enhance.css is the shared layer
   distributed to all four hubs by build_protected.py and it is the last sheet to load, so one
   edit here is by construction identical on AU, UK, NZ and Asia and needs no specificity war.
   ========================================================================= */

/* The 11px floor. `_canonical/SKILL.md` §3: "nothing below 11px on screen". --fs-caption sat at
   10.5px and carries source lines and provenance labels, the text most likely to be squinted at. */
:root { --fs-caption: 12px; }

/* --- 48 / 32 — the page title, and the end of the inversion ------------------------------- */
body.is-home #main .page-hero h1,
body.is-home .page-hero h1 { font-size: clamp(30px, 4.0vw, 48px); line-height: 1.06; letter-spacing: -0.018em; }
body:not(.is-home) #main .page-hero h1,
body:not(.is-home) .page-hero h1 { font-size: clamp(22px, 2.4vw, 32px); line-height: 1.12; letter-spacing: -0.014em; }

/* --- 24 — the section heading, one size for every section --------------------------------- */
/* The first-of-type bump above is overridden rather than deleted: same specificity, later in the
   same file, so the rule that produced 29px loses on order and the history stays readable. */
/* `.sec-head` is in every one of these selectors deliberately (29 Jul 2026). The UK hub wraps
   its section headings in `<div class="sec-head">`, which the other three do not, and a CHILD
   combinator stops matching the moment a wrapper appears. The rule silently missed every UK
   section heading, which then fell back to the page-level `h2` clamp and rendered at 43.5px
   against 24px everywhere else — the same view, twice the heading, on one hub. Matching both
   shapes is what makes this "one size for every section" true rather than aspirational. */
#main > section.block:first-of-type > h2,
#main > section.block:first-of-type > .sec-head > h2,
#main section.block > h2,
#main section.block > .sec-head > h2,
#main .sh-h2 { font-size: clamp(20px, 1.9vw, 24px); line-height: 1.18; letter-spacing: -0.014em; }

/* --- 18 — the card heading, now a step above card body rather than equal to it ------------- */
#main .chartcard h3,
#main .sh-chart-card h3,
#main .sh-compare-card h3,
#main .campus h3,
#main .sig-head { font-size: 18px; line-height: 1.25; letter-spacing: -0.010em; }

/* Minor headings inside a card sit at body size and are carried by weight, which is the
   canonical's "hierarchy survives greyscale" case rather than another size step. */
#main .sh-force h3,
#main .sh-scrolly__step h4,
#main .cbx-gb__demand h4 { font-size: 14px; line-height: 1.34; }
#main .cbx-gb__h3 { font-size: 18px; line-height: 1.25; }

/* Screen text below the 11px floor. The print packs (.rp-*, .ppk-*) set their own pt sizes and
   are not touched. */
#main .sh-compare-card h3 .country-code { font-size: 11px; }

/* Method anchor arrival (item 31/32, 28 Jul 2026). A deep link into a long reference page has to
   show the reader WHICH heading it chose: the scroll itself is evidence-free once it settles, and
   Method's headings look alike. A brief tint, then nothing. Honours reduced-motion by simply not
   animating: the tint still appears and clears, it just does not fade. */
.m-anchor-hit { animation: cbxAnchorHit 2.2s ease-out 1; border-radius: var(--radius-control, 5px); }
@keyframes cbxAnchorHit {
  0%, 55% { background: color-mix(in srgb, var(--accent, #234C50) 14%, transparent); box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent, #234C50) 14%, transparent); }
  100%    { background: transparent; box-shadow: 0 0 0 6px transparent; }
}
@media (prefers-reduced-motion: reduce) { .m-anchor-hit { animation-duration: 0.01s; } }

/* ---- the mobile drawer opened where the reader was not (28 Jul 2026) ------------------------
   Reported on Leadership Evolution, found on all four hubs and 227 generated pages: `.drawer` is
   a body-level sibling of a STICKY header and carried no positioning, so it opened in normal
   document flow just under the header. At the top of the page that is exactly right. Scrolled
   down, the element is still at document y~73 while the reader is thousands of pixels below it,
   so the button toggled `is-open`, the panel duly appeared, and nobody ever saw it.
   Loaded after each hub's styles.css, so this wins on order at equal specificity. The generated
   static pages carry the same declaration inline, patched in their five builders. */
.drawer {
  position: fixed;
  top: var(--header-h, var(--headh, 73px));
  left: 0; right: 0;
  max-height: calc(100dvh - var(--header-h, var(--headh, 73px)));
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: 49;
}


/* ── `hidden` must actually hide (29 Jul 2026) ─────────────────────────────────
   The user-agent sheet's `[hidden] { display: none }` is the weakest rule in the
   cascade, so ANY class that sets display beats it. Setting el.hidden = true in JS
   therefore does nothing to an element carrying such a class, silently.

   This is what kept the four market KPIs — GDP per capita, GNI, population growth,
   R&D — on screen at the top of every Asia market section after they were hidden
   everywhere but "Market position": the header row is .grid, and .grid is
   display:grid. The eyebrow and the h2 beside it disappeared correctly, which is
   what made the bug read as a scoping mistake rather than a cascade one.

   Two one-off patches for this same fault already existed (.mkt-panel[hidden] and
   .cbx-acc__body[hidden]). This is the general rule they were each standing in for,
   so the next element given `hidden` in JS does not need a third. */
[hidden] { display: none !important; }

/* ── The forward cue breathes (Benjie, 4 Aug 2026) ────────────────────────────────────
   "some of the pages have next with an arrow — make this pulse so it's obvious to the
   reader to move to the next section."
   WHY IT WAS STILL: `.cbx-next` is the forward control on every view that has no hub-specific
   `.tab-next`, and it carried no animation at all. The note above its rule says it "reuses
   .cbx-cue from DS14, so it obeys the same finite cycle" — that was never wired: CUE_SELECTORS
   in enhance.js lists `.tab-next`, `.starter .role-tile` and `.starter-link`, and not this.
   So the one control whose whole job is to say "go here next" was the only one not saying it.
   WHY CONTINUOUS, NOT THE 3-CYCLE CUE: Benjie set `.tab-next` to breathe indefinitely on
   22 Jul 2026 — "the continue cue breathes so the onward path is noticed" — and this is the
   same control on the views where that one does not exist. It matches its sibling rather than
   the rationed once-per-route cue, which is easy to miss and is designed for optional side
   paths, not the main way forward.
   The keyframes carry the card shadow through both stops: box-shadow replaces rather than
   layers, so animating it alone would strobe the pill's own shadow off and on.
   Hovering or tabbing in is already the reader answering it, so the ring stands down. */
/* RING COLOUR IS --accent-soft, NOT --accent. Measured on all four hubs: `.section-nav` is
   ink `rgb(3, 22, 2)` on every one of them, and the ring is drawn OUTSIDE the pill, so it lands
   on that near-black bar. The signature teal at 35% was invisible there. --accent-soft is the
   canonical token for accent on dark grounds, which is exactly this case. */
@keyframes cbx-next-pulse {
  0%, 100% { box-shadow: var(--shadow-card, 0 1px 2px rgba(3, 22, 2, .06)),
                         0 0 0 0 color-mix(in srgb, var(--accent-soft, #9CC2C0) 60%, transparent); }
  55%      { box-shadow: var(--shadow-card, 0 1px 2px rgba(3, 22, 2, .06)),
                         0 0 0 9px color-mix(in srgb, var(--accent-soft, #9CC2C0) 0%, transparent); }
}
.cbx-next { animation: cbx-next-pulse 2.6s ease-out infinite; }
.cbx-next:hover, .cbx-next:focus-visible { animation: none; }
@media (prefers-reduced-motion: reduce) { .cbx-next { animation: none; } }
