/* ============================================================================
   MyGeo palette — THE PRINTED ATLAS
   ----------------------------------------------------------------------------
   The base stylesheets (redesign.css + warm-theme.css) are inherited from the
   sibling Explorer sites and are built on a turquoise/gold ramp. Rather than
   fork ~93 KB of CSS, this file retargets the handful of custom properties the
   whole theme is expressed through onto MyGeo's own identity.

   THE IDENTITY: a printed atlas. Navy chart ink on warm parchment, orange
   waypoints, sky-blue water. Every one of the four Explorer sites shares this
   layout engine, so the palette is the main thing that tells them apart, and
   this one is chosen to sit clear of all three siblings:

       MyAO   deep space — blue into violet, on dark
       MyBO   turquoise and gold
       MyESO  deep green and amber, on cream
       MyGeo  navy and orange, on parchment          <- here

   The parchment stays WARM on purpose. Navy on cool grey is a corporate
   dashboard; navy on warm paper is an atlas, and that warmth is the thing
   keeping this clear of MyAO at a glance.

       #123c63  chart navy    headings, links, hairlines   11.3:1 on white
       #0a5299  chart blue    links, routes, water lines     7.8:1
       #ff980d  orange        primary accent / CTAs          2.2:1 — FILL ONLY
       #fbe6c8  parchment     section washes
       #f6cf9e  sand          cards and panels
       #08243f  deep navy     footer and dark grounds
       #7fb2f0  water blue    highlights
       #a8752b  chart ochre   secondary accent (#8a5d1e for text)

   Contrast is load-bearing here, not decoration. Orange is a FILL colour on
   this brand and never carries text — the same rule the amber it replaced had,
   for the same reason. Anything that needs to be read runs in the navy.

   Loaded last in the layout so it wins on specificity ties.
   ========================================================================== */

/* ---------------------------------------------------------------------------
   SELECTOR NOTE — read before editing either token block below.

   warm-theme.css declares its palette under `:root[data-theme="warm"]`, which is
   specificity (0,2,0). A bare `[data-theme="warm"]` is (0,1,0) and LOSES to it no
   matter which file the browser loads last. Both blocks here therefore have to
   carry `:root` as well, or they are dead rules that compute to turquoise.

   This is not hypothetical: it is exactly what shipped. Until the audit, --brand
   computed to #0d867b, --text to #123b3a and every border to teal, so the MyGeo
   palette only ever reached the components that set literal hex values.
   --------------------------------------------------------------------------- */
:root,
:root[data-theme="warm"] {
    /* brand ramp — orange through to the chart navy the site uses for headings */
    --brand: #ff980d;
    --brand-100: #fff2e0;
    --brand-200: #fbe6c8;
    --brand-300: #f9cf96;
    --brand-400: #ffa445;
    --brand-500: #ff980d;
    --brand-600: #c96806;
    --brand-700: #1d5a94;
    --brand-800: #10334f;
    --brand-900: #123c63;

    /* accents */
    --accent: #123c63;
    --accent-2: #8a5d1e;   /* darkened from #a8752b — that was 4.14:1 on white, under AA */
    --accent-3: #7fb2f0;
    --water: #cfe0f5;
    --water-soft: #eff4fb;

    /* Link ink. Amber is a FILL colour on this brand, never a text colour:
       --brand-600 is 2.70:1 on white and --brand is 1.91:1, both far under the
       4.5:1 AA floor. Body links and the active nav item run in the brand deep
       green instead, which measures 7.91:1 and is already in the palette. */
    --link: #123c63;
    --link-hover: #0c2b4b;

    /* Inherited names that only warm-theme.css declares, retargeted here so no
       rule downstream can reach back into the turquoise ramp. */
    --teal: #ff980d;                      /* accent hairlines, ring progress arc */
    --gold: #a8752b;
    --violet: #123c63;                    /* kept name → chart navy */
    --pink: #c96806;
    --muted: #5d6b7a;                     /* was #4f6e6c teal; also raised to 5.2:1 */
    --brand-glow: rgba(16, 40, 62, .14);  /* neutral warm shadow, not a teal halo */

    /* Gradients. --grad-title is what paints the second half of every section
       heading ("Intro|duction"), via `.section-title .grad` in redesign.css at
       (0,2,0) — which outranks this file's own `.grad` rule, so the token is the
       only place the fix can live. */
    --grad-brand: linear-gradient(135deg, #ff980d 0%, #c96806 100%);
    --grad-gold:  linear-gradient(135deg, #ff980d, #a8752b);
    --grad-text:  linear-gradient(92deg, #c96806 0%, #ff980d 45%, #123c63 100%);
    --grad-title: linear-gradient(92deg, #c96806 0%, #ff980d 45%, #123c63 100%);
}

/* Footer. warm-theme.css ends the page on a teal ink panel with a turquoise
   accent — the sibling sites' colours, and the last thing a visitor sees. On the
   MyGeo palette it is navy with the orange accent, so the page closes in
   the same colours it opened in.

   Set through `:root[data-theme="warm"]`, not the plain `[data-theme="warm"]`
   above: warm-theme.css declares these under that same compound selector, which
   out-specifies a bare attribute selector no matter which file loads last. */
:root[data-theme="warm"] {
    --footer-bg: linear-gradient(165deg, #08243f 0%, #05192b 100%);
    --footer-text: #eef4fa;
    --footer-muted: #9db9d6;
    --footer-border: rgba(255, 255, 255, .14);
    --footer-accent: #ff980d;
}

/* Light theme — a warm map-paper wash instead of the aqua one.
   `:root` prefix required; see the selector note above. */
:root[data-theme="warm"] {
    --bg: #fffaf1;
    --bg-2: #fbf0df;
    --surface: #ffffff;
    --surface-2: #fcf2e4;
    --surface-3: #f6ead3;

    /* The --bg-9xx ramp is warm-theme's aqua-white stack, and redesign.css builds
       the fallback page backdrop out of it. Retargeted to the cream wash so the
       backdrop is warm even before this file's own body rules apply. */
    --bg-950: #fffaf1;
    --bg-900: #f8eeda;
    --bg-850: #f2e5cf;

    --text: #0d151d;
    --text-soft: #4a5a69;
    --text-mute: #5d6b7a;   /* was #6d7b89 — 4.04:1 on white, under the AA floor */

    --border: rgba(18, 60, 99, .14);
    --border-strong: rgba(18, 60, 99, .28);

    --shadow-sm: 0 2px 8px rgba(16, 40, 62, .07);
    --shadow: 0 10px 28px rgba(16, 40, 62, .11);
    --shadow-md: 0 14px 34px rgba(16, 40, 62, .12);
    --shadow-lg: 0 24px 60px rgba(16, 40, 62, .15);
}

/* The page backdrop: pale cream settling into warm parchment, so the map-paper
   palette reads across the whole page rather than only on the accents. */
[data-theme="warm"] body {
    background:
        radial-gradient(1200px 600px at 15% -10%, #e0ebf8 0%, rgba(246, 207, 158, 0) 60%),
        radial-gradient(900px 500px at 95% 5%, #ffedd4 0%, rgba(255, 240, 205, 0) 55%),
        linear-gradient(180deg, #fffaf1 0%, #f7f2e2 100%);
}

/* …and the sheet that was covering it.

   warm-theme.css paints `body::before` — fixed, inset:0, z-index:-3, opacity:1 —
   with four turquoise/aqua blooms over an opaque aqua-white gradient. That is a
   LAYER ABOVE the body's own background, so the warm wash directly above was
   computed correctly and then hidden on every page. It is the single reason the
   site read as turquoise despite an orange-and-navy component set.

   Repainted rather than removed: the ambient field is what stops the page feeling
   like a flat wall, and it is worth keeping — in the right colours. Lowland cream
   at the top corners, warm gold low and right, over the cream base. The 40s drift
   animation is inherited and already suspended under prefers-reduced-motion. */
[data-theme="warm"] body::before {
    background:
        radial-gradient(900px 700px at 84% -8%, rgba(251, 230, 200, .55), transparent 62%),
        radial-gradient(780px 640px at  2% 22%, rgba(246, 207, 158, .42), transparent 60%),
        radial-gradient(860px 660px at 98% 56%, rgba(255, 152, 13, .16), transparent 62%),
        radial-gradient(900px 720px at 10% 88%, rgba(145, 123,  41, .10), transparent 62%),
        linear-gradient(180deg, #fffaf1 0%, #faf1dd 48%, #f4e7d0 100%);
    background-repeat: no-repeat;
}

/* Gradient text used for the second half of most headings. */
.grad-text,
.grad {
    background: linear-gradient(92deg, #c96806 0%, #ff980d 45%, #123c63 100%);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
}

/* Primary CTA — amber fill, navy text, which is how the live buttons read. */
[data-theme="warm"] .btn-primary {
    background: linear-gradient(135deg, #ff980d 0%, #e07a0c 100%);
    border-color: #c96806;
    color: #101b26;
}

[data-theme="warm"] .btn-primary:hover {
    background: linear-gradient(135deg, #ffab50 0%, #ef8410 100%);
    color: #0d151d;
}

/* The accent line along the top edge of the header. The base stylesheet paints it
   with --grad-brand, which this file never retargets (the primary CTA is overridden
   directly instead), so it was still running the inherited turquoise — identical to
   MyBO's, since both sites inherit the same ramp. Retargeted here onto the MyGeo
   palette: the hypsometric ramp the hero uses, lowland cream through gold to
   orange, so the two sites are told apart at the very first pixel of the page. */
[data-theme="warm"] .site-header::before {
    background: linear-gradient(135deg, #123c63 0%, #a8752b 45%, #ff980d 100%);
}

/* ---- Navigation chrome ------------------------------------------------------
   warm-theme.css paints all four of these in `rgba(245,252,251)` / `#f6fdfc` — a
   turquoise-tinted white. Against the cream page above they read visibly cool, so
   they are restated here on the warm white the rest of the site uses. */
[data-theme="warm"] .site-header { background: rgba(255, 253, 246, .78); border-bottom-color: var(--border); }
[data-theme="warm"] .site-header.scrolled { background: rgba(255, 253, 246, .96); box-shadow: var(--shadow-sm); }
[data-theme="warm"] .dropdown { background: rgba(255, 253, 246, .99); border-color: var(--border-strong); box-shadow: var(--shadow-md); }

/* The header runs wider than the 1180px reading column.
   MyGeo's nav is three menus plus five links — MyGeo, iGeo, Moments, and Home /
   Registration / Contact Us / FAQs / Shop — and inside 1180px, minus the wordmark
   and the Register button, the row has about 705px to live in and needs more. The
   reading column stays at 1180: prose does not want to be wider, and only the
   header has this problem.

   white-space: nowrap is the other half. A flex row that has run out of width
   wraps INSIDE the words — "Registratio / n", "MyGe / o" — which is how the miss
   showed up both times it happened here. With nowrap a future miss overflows
   instead, which is at least legible as a bug rather than looking like a design. */
.site-header > .container { max-width: 1320px; }
.nav-links > li > a { white-space: nowrap; padding-inline: 12px; }
@media (min-width: 1200px) and (max-width: 1439px) {
    /* A gentle tightening, not the squeeze this used to be. The row held eight
       items — including a Registration link beside a Register button, and a Shop
       link to another site — and needed 7px padding at .86rem plus a shrunken
       CTA to fit. Both extras moved (Registration into MyGeo, Shop to the
       footer), so six items fit with room and this only takes the edge off. */
    .nav-links > li > a { padding-inline: 11px; }
}
/* A .dropdown-rule lived here: a hairline dividing the iGeo menu where it stopped
   listing /igeo and started listing /olympiad-moments. Each menu holds one page
   now, so there is nothing left to divide and the rule went with the arrangement
   that needed it. */
@media (max-width: 1199px) {
    /* the open burger menu is a full overlay panel — fully opaque, or the hero
       ghosts through it */
    [data-theme="warm"] .nav-links { background: #fffaf1; border-bottom-color: var(--border-strong); }
}

/* Scroll-to-top. This kept warm-theme's flat turquoise (#c96806) — it was the last
   piece of the sibling palette still visible on every page after a scroll. */
[data-theme="warm"] .to-top {
    background: linear-gradient(140deg, #ff980d, #e07a0c);
    border-color: #c96806;
    color: #101b26;
    box-shadow: var(--shadow-md);
}
[data-theme="warm"] .to-top:hover { background: linear-gradient(140deg, #ffab50, #ef8410); }

/* ---- Link ink --------------------------------------------------------------
   Every one of these rules previously resolved to --brand-600. While that token
   was still the inherited teal it passed contrast by accident; retargeting it to
   the MyGeo orange (#c96806, 2.70:1 on white) would have made every body link and
   the active nav item fail WCAG AA. So text links take --link (the brand deep
   navy, 11.3:1) and orange stays a fill colour.

   Selectors mirror warm-theme.css exactly rather than out-specifying it — this
   file loads last, so an equal-specificity rule wins, and keeping them identical
   means a future edit there is easy to find here. */
[data-theme="warm"] .nav-links a:hover { color: var(--link-hover); background: var(--surface-2); }
[data-theme="warm"] .nav-links a.active { color: var(--link); }
[data-theme="warm"] .gx-glass a:not(.btn) { color: var(--link); }
[data-theme="warm"] .gx-glass.prose a:not(.btn),
[data-theme="warm"] .gx-glass .prose a:not(.btn) { color: var(--link); }
[data-theme="warm"] .step-row a { color: var(--link); }
[data-theme="warm"] .prose a:not(.btn) { color: var(--link); }
[data-theme="warm"] .prose a:not(.btn):hover { color: var(--link-hover); }
/* the small-caps tagline under the wordmark — orange at 10px was the worst
   offender on the page for size and contrast together */
[data-theme="warm"] .brand-sub { color: var(--accent); }

/* The eyebrow chip above each page title. */
[data-theme="warm"] .eyebrow {
    background: var(--water-soft);
    border-color: rgba(18, 60, 99, .22);
    color: #123c63;
}

/* Section headings sit in the deep green rather than the inherited teal. */
[data-theme="warm"] h1,
[data-theme="warm"] h2,
[data-theme="warm"] h3 {
    color: #08243f;
}
/* …but not on the dark surfaces. The rule above is a bare element selector, so it
   ties with `.footer h2` on specificity and wins only because this file loads
   last — which painted EXPLORE / GET IN TOUCH / FOLLOW US navy on the dark
   footer panel, where they were all but invisible. The footer keeps its own ink. */
[data-theme="warm"] .footer h2,
[data-theme="warm"] .footer h3 { color: var(--footer-text); }

/* Glass panels pick up the warm cream instead of the aqua tint. */
[data-theme="warm"] .gx-glass {
    background: linear-gradient(180deg, rgba(255, 255, 255, .92) 0%, rgba(253, 247, 232, .92) 100%);
    border-color: var(--border);
}

/* The icon chips on tiles. */
[data-theme="warm"] .gx-icon {
    background: linear-gradient(140deg, var(--a, #ff980d) 0%, rgba(18, 60, 99, .85) 130%);
    color: #fff;
}

/* ---- Page-level helpers shared by the MyGeo pages -------------------------- */

/* Two-column "label | value" tables used for the competition format, categories
   and award bands. Plain definition rows, not a full table widget. */
.kv-list { display: grid; gap: 10px; margin: 0; }
.kv-row {
    display: grid;
    grid-template-columns: minmax(140px, 34%) 1fr;
    gap: 8px 18px;
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
}
.kv-row:last-child { border-bottom: 0; }
.kv-row dt { font-weight: 700; color: #123c63; margin: 0; }
.kv-row dd { margin: 0; color: var(--text-soft); }
@media (max-width: 560px) {
    .kv-row { grid-template-columns: 1fr; gap: 2px; padding: 10px 0; }
}

/* Numbered step list (registration procedure) — Galaxy's timeline, in MyGeo's
   colours: numbered discs threaded on a vertical rail, one plain paragraph of
   text hanging off each.

   Everything the earlier version did to a step is gone. No bold title with a
   muted detail beside or beneath it: at two weights and two columns, a step
   with no detail read as a different kind of row from one that had a detail,
   and the panel filled with ragged half-width blocks. Set as one sentence at
   one weight, every row is the same shape and the rail carries the sequence. */
.step-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 20px;
    counter-reset: step;
    line-height: 1.65;
    color: var(--text-soft);
}
.step-list li {
    counter-increment: step;
    position: relative;
    padding-left: 52px;
    min-height: 32px;
}
.step-list li::before {
    content: counter(step);
    position: absolute;
    left: 0; top: -3px;    /* centres the disc on the first line of text */
    display: grid;
    place-items: center;
    width: 32px; height: 32px;
    border-radius: 50%;
    background: linear-gradient(135deg, #ff980d 0%, #e07a0c 100%);
    color: #101b26;
    font-weight: 800;
    font-size: .95rem;
}
/* the two things a step marks up: the buttons and menu items to look for, and
   the address to go to. Both are darker than the surrounding line, since that
   line is set in the soft body colour. */
.step-list strong { color: var(--text); font-weight: 700; }
.step-list a {
    color: #123c63;
    font-weight: 700;
    text-decoration: underline;
    text-decoration-thickness: 1.5px;
    text-underline-offset: 3px;
    transition: color .2s ease;
}
.step-list a:hover, .step-list a:focus-visible { color: #cf7409; }

/* sub-actions inside a step: own line, arrow hanging in the margin so a wrapped
   one lines up with the text above it rather than with the arrow */
.step-list .step-sub {
    display: block;
    margin-top: 2px;
    padding-left: 1.35em;
    text-indent: -1.35em;
}
.step-list .step-sub::before {
    content: "→";
    display: inline-block;
    width: 1.35em;
    text-indent: 0;
    color: #cf7409;
}

/* the rail: disc centre is 16px in, so a 2px line sits at 15px. It runs from
   the bottom of this disc to the top of the next one (20px gap, less the 3px
   the disc is raised by), and the last step has none to run to. */
.step-list li:not(:last-child)::after {
    content: "";
    position: absolute;
    left: 15px;
    top: 34px;
    bottom: -17px;
    width: 2px;
    border-radius: 1px;
    background: linear-gradient(180deg, rgba(255, 152, 13, .45), rgba(255, 152, 13, .14));
}

/* the panel around it: capped so the lines keep a readable measure, centred in
   the section, and — with the heading gone — holding only the steps */
.step-panel { max-width: 880px; margin-inline: auto; }

/* The mark over a results-page hero. redesign.css sets .badge-mini to 88px, which
   suits the sibling sites' round badges and the Geography Explorer one — but MyGeo's
   own mark is a wordmark carrying a strapline, and at 88px that line is a smudge.
   --wide gives it the width the lettering needs, capped against the viewport. */
.page-hero .badge-mini { height: auto; }
.page-hero .badge-mini--wide { width: min(260px, 66vw); }

/* ---- Explorer → MyGeo lockup ----------------------------------------------
   The competition badge feeding the olympiad it leads into, over the copy that
   describes that route — Biology's .about-lockup, in MyGeo's colours. */
.about-lockup {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(14px, 2.6vw, 34px);
    margin-bottom: clamp(22px, 3vw, 34px);
}
.al-item { flex: 0 0 auto; margin: 0; text-align: center; }
/* One shared plate height, so the square badge and the wide wordmark sit on the
   same line. One box for both, not one per artwork: the plates used to be sized
   height-led with width:auto, so each came out as wide as its own logo — a
   square badge in a narrow plate beside a 2:1 wordmark in a wide one, which read
   as two different kinds of card rather than as a pair.

   The logo is fitted inside the box instead (max-width and max-height, never
   stretched), so the square one fills the height, the wide one fills the width,
   and both sit on identical plates. */
.al-plate {
    width: clamp(190px, 21vw, 264px);
    height: clamp(120px, 13vw, 168px);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(14px, 1.8vw, 22px);
    border-radius: 18px;
    background: linear-gradient(180deg, #fff 0%, #fcf2e4 100%);
    border: 1px solid rgba(18, 60, 99, .12);
    box-shadow: 0 14px 34px rgba(16, 40, 62, .16);
}
.al-plate img {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}
.al-cap {
    margin: 10px 0 0;
    font-weight: 700;
    font-size: .72rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--text-soft);
}

/* the connector: a dashed trail running toward MyGeo, ending in an orange chip */
.al-arrow {
    flex: 1 1 auto;
    max-width: 190px;
    min-width: 54px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.al-trail {
    flex: 1 1 auto;
    height: 3px;
    border-radius: 999px;
    background: repeating-linear-gradient(90deg, #c96806 0 9px, transparent 9px 18px);
    background-size: 18px 100%;
    animation: al-flow 1.1s linear infinite;
}
.al-tip {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    /* dark ink on orange, as everywhere else on this palette — white on #ff980d is
       nowhere near readable at an 18px glyph */
    color: #101b26;
    background: linear-gradient(135deg, #ff980d 0%, #e07a0c 100%);
    box-shadow: 0 6px 16px rgba(176, 110, 12, .4);
    animation: al-nudge 1.7s ease-in-out infinite;
}
@keyframes al-flow  { to { background-position: 18px 0; } }
@keyframes al-nudge { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(4px); } }
@keyframes al-flow-down  { to { background-position: 0 18px; } }
@keyframes al-nudge-down { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(4px); } }
/* the motion is pure decoration — nothing is lost by holding it still */
@media (prefers-reduced-motion: reduce) {
    .al-trail, .al-tip { animation: none; }
}
@media (max-width: 760px) {
    /* stack the lockup and turn the arrow to point down the column */
    .about-lockup { flex-direction: column; }
    /* stacked, the plates have the column's full width to share, so they can be
       squarer without either logo shrinking — but still the same box as each other */
    .al-plate { width: min(260px, 78vw); height: 132px; }
    .al-arrow { flex: 0 0 auto; max-width: none; min-width: 0; flex-direction: column; height: 74px; }
    .al-trail {
        width: 3px;
        height: auto;
        background: repeating-linear-gradient(180deg, #c96806 0 9px, transparent 9px 18px);
        background-size: 100% 18px;
        animation-name: al-flow-down;
    }
    .al-tip { animation-name: al-nudge-down; }
    .al-tip .gx-ico { transform: rotate(90deg); }
}

/* Justified body copy — the About page's introduction and every write-up under
   it, the same treatment the home page's introduction gets.

   max-width is released ON THE PARAGRAPH because .prose p is otherwise held to a
   reading measure, and justifying a column that stops short of the panel edge
   just looks like a ragged block that missed. text-wrap:pretty keeps the closing
   lines from stranding a single word on its own.

   Lists come with it: a justified paragraph beside a ragged list reads as an
   inconsistency rather than a distinction.

   Justified at EVERY width, phones included. This used to be gated behind
   min-width 45em, which meant the About page's write-ups were squared off on a
   desktop and ragged on a phone — the same copy set two different ways depending
   on the device, which is the inconsistency that gate was meant to prevent.

   Hyphens stay OFF, so words are never broken. That is a deliberate choice, not
   an oversight — please do not "fix" it by switching hyphens:auto back on.

   Know the cost before touching either line. With no hyphenation the only thing
   a justified line can stretch is its word spaces, so they open wider than
   normal, and the narrower the column the more it shows. hyphens:auto would
   tighten that up (it gives the breaker somewhere to break other than a space,
   and <html lang="en"> supplies the language it needs) — it was tried here and
   rejected, because whole words were preferred to perfectly even spacing.

   So there are two supported states, and mixing them is what to avoid:
       justify + hyphens:none   <- current: whole words, looser spacing
       ragged-right             <- the fallback if the spacing is ever too loose
   text-wrap:pretty stays either way; with hyphenation gone it is the only thing
   left helping the line breaks.

   THE MEASURE IS NOT CAPPED HERE, and that is a decision, not an omission.

   It used to be. This block carried `max-width: var(--measure); margin-inline:
   auto`, which held the column to 66ch and centred it inside the panel — the
   paragraph still filled its column edge to edge, so justification had a right
   edge to reach, and the copy sat at a readable width. That reasoning was sound
   and the result was not: on a panel spanning the container it left 180-280px of
   empty card either side of every paragraph, worst of all on /igeo, where the
   floated .igeo-mark pushed the centred column further off the left edge. Read as
   a page rather than as a paragraph, the panel looked like a frame around a
   column that had missed it — which is precisely the failure the cap was there
   to avoid, arriving from the other direction.

   So the block fills its panel and the panel decides the width. Know the cost:
   these run 102-118ch at 1440px (measured, in the panels on About, /igeo, both
   Olympiad pages and Explorer Results) where comfortable is 60-75ch. Requested
   deliberately, with the white space understood to be the bigger problem of the
   two.

   If that ever needs pulling back, narrow the PANEL, not this block — see
   .moment-card, which sets its card to the width of three photographs and lets
   the copy fill it. Re-capping here just puts the gutters back inside the card.

   Not applied to .geo-intro-lead, which has its own justify rule further down:
   the Introduction sheet caps that copy at 66ch rather than letting it fill its
   panel, because on that layout the copy has the whole field to itself and the
   panel is the full container width. */
.geo-justify p,
.geo-justify li {
    max-width: none;
    hyphens: none;
    text-align: justify;
    text-wrap: pretty;
}

/* ---- Fee tiers ------------------------------------------------------------
   The live site's registration-fee cards: a filled header naming the tier, the
   figure large underneath it, then the two facts that decide the tier — who it
   is for and the window it is open — and the button.

   Not filled cards with the price on a coloured ground, which is what these
   were: at two tiers, colour has nothing left to encode, and a rate is easier
   to read as dark text on white than as white text on gold. The colour moves to
   the header band, where it separates the tiers without touching the figures. */
.fee-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
    gap: clamp(16px, 2vw, 26px);
    /* stretch, not start: a closed tier carries no Register button and so came out
       visibly shorter than the open one beside it. Two rates are two alternatives,
       and drawing one of them as a smaller card says something about them that is
       not true — it is closed, not lesser. */
    align-items: stretch;
}
.fee-card {
    position: relative;
    display: flex;
    flex-direction: column;
    /* clips the ribbon's tails to the card's corner */
    overflow: hidden;
    border-radius: 16px;
    border: 1px solid var(--border);
    background: #fffaf2;
    box-shadow: 0 12px 30px rgba(16, 40, 62, .12);
    transition: transform .3s ease, box-shadow .3s ease;
}
.fee-card:hover { transform: translateY(-4px); box-shadow: 0 20px 44px rgba(16, 40, 62, .2); }

/* ---- header: the tier's name on brand gold -------------------------------- */
.fee-card-head {
    padding: 15px 18px;
    background: linear-gradient(160deg, #1d5a94 0%, #8a5610 100%);
    text-align: center;
}
/* both selectors: warm-theme.css paints every h3 inside a .gx-glass in the dark
   body colours, and this one sits on the gold band — see the same note on the
   video playlist header */
.fee-card-head h3,
[data-theme="warm"] .gx-glass .fee-card-head h3 {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 800;
    letter-spacing: .01em;
    color: #fff;
}

/* the corner ribbon on the tier that closes. Rotated through the card's clipped
   corner, so it reads as a band laid over the card rather than a label stuck to
   it. Earth brown: the header is orange, and the ribbon has to carry across it.

   Enlarged from .64rem (~10.2px). This is the only thing on the page telling a
   teacher the RM40 rate expires, and at 10px uppercase, tracked out and rotated
   45°, it was the least readable text on the site — the exact inverse of its
   importance. .8rem is the floor for anything carrying words rather than acting
   as a mark; the band grows with it. */
.fee-flag {
    position: absolute;
    top: 18px;
    right: -42px;
    width: 168px;
    padding: 6px 0;
    transform: rotate(45deg);
    background: linear-gradient(135deg, #1a5183 0%, #08243f 100%);
    color: #fff;
    font-size: .8rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: center;
    box-shadow: 0 3px 10px rgba(8, 36, 63, .35);
}

/* Which rate applies today. Exactly one card is ever "current"; a closed one is
   drained of colour rather than hidden, so the price history still reads. */
.fee-pill {
    align-self: center;
    margin-bottom: 12px;
    padding: 4px 12px;
    border-radius: 999px;
    background: rgba(18, 60, 99, .1);
    border: 1px solid rgba(18, 60, 99, .28);
    color: #123c63;
    font-size: .72rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.fee-pill--muted {
    background: rgba(16, 40, 62, .07);
    border-color: rgba(16, 40, 62, .16);
    color: var(--text-soft);
}
.fee-card.is-ended { opacity: .72; }
.fee-card.is-ended .fee-card-head { background: linear-gradient(160deg, #8d8168 0%, #746a55 100%); }
.fee-card.is-ended:hover { transform: none; box-shadow: 0 12px 30px rgba(16, 40, 62, .12); }

/* ---- body: figure, then what the figure buys ------------------------------ */
.fee-card-body {
    /* takes the height the card was stretched to, so the header bands stay level
       across the row and the difference falls at the foot of the card */
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: clamp(20px, 2.4vw, 28px) clamp(16px, 2vw, 24px);
    text-align: center;
}
/* the button sits on the card's floor rather than directly under the dates, so the
   two cards' buttons line up whatever is written above them */
.fee-card-body .btn { margin-top: auto; }
.fee-price,
[data-theme="warm"] .gx-glass .fee-price {
    margin: 0;
    font-size: clamp(2.6rem, 4.4vw, 3.4rem);
    font-weight: 800;
    line-height: 1;
    letter-spacing: -.02em;
    color: var(--text);
}
/* the currency rides small and high against the figure, the way a price is set */
.fee-cur {
    font-size: .38em;
    font-weight: 800;
    vertical-align: .95em;
    margin-right: .12em;
    letter-spacing: 0;
}
.fee-note,
[data-theme="warm"] .gx-glass .fee-note {
    margin: 8px 0 0;
    font-size: .92rem;
    color: var(--text-soft);
}

/* the two facts that pick the tier, ruled apart so neither is skimmed past */
.fee-points {
    list-style: none;
    width: 100%;
    margin: 20px 0 22px;
    padding: 0;
    display: grid;
    gap: 10px;
}
.fee-points li {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding-top: 10px;
    border-top: 1px solid var(--border);
    font-size: .95rem;
    color: var(--text-soft);
}
.fee-points li:first-child { padding-top: 0; border-top: 0; }
.fee-points .gx-ico { flex: 0 0 auto; width: 19px; height: 19px; color: #123c63; }

/* Stat counters ("previous competition"). */
/* ============================================================================
   SCALE — deliberately none
   ----------------------------------------------------------------------------
   This file used to carry a density pass: section padding, panel padding, the
   section title and the ring counters were all retargeted smaller, on the
   argument that MyGeo's home page runs twelve sections where the inherited
   engine assumes three or four.

   It is gone, and nothing here should bring it back. The three Explorer sites
   are meant to read as one family, and every value it overrode is identical in
   MyGeo's own redesign.css and in Biology's:

       .section        clamp(32px, 5vw, 64px)      both
       .section--tight clamp(22px, 3.5vw, 40px)    both
       .gx-pad         clamp(24px, 3.4vw, 44px)    both
       .section-title  clamp(1.9rem, 4.2vw, 3.1rem) both (--fs-h2)

   So the fix for "MyGeo looks smaller than Biology" is always to delete an
   override here, never to add one. If the page ever needs to be shorter, cut
   sections or content — not the scale everything is drawn at.
   ========================================================================== */

/* the heading block above each section's body: eyebrow chip over title. Matches
   Biology's .gx-head (margin 0 auto clamp(26px, 4vw, 44px); eyebrow 14px). */
.geo-head-text .eyebrow { margin-bottom: 14px; }
.geo-head + .lead { margin-bottom: 12px; text-align: center; margin-inline: auto; }
/* A lead that is one sentence and just over the reading measure, so `.lead`'s
   `max-width: var(--measure)` broke it onto a second line carrying three words —
   which looks like a mistake rather than a paragraph. Off the cap, it sets on one
   line on a desktop and still wraps to the container on a phone, where wrapping is
   what it should do.

   Opt-in per sentence, not a change to `.lead`. The measure is right for every
   lead that is actually a paragraph; this is for the ones that are a single line
   and miss by a few words. Check the sentence still fits before adding it — a
   genuinely long lead set to the full container width is much worse than a wrap. */
.geo-lead-wide { max-width: none; }
/* A sentence set apart inside a lead. The lead is deliberately soft, so its bold
   has to be both heavier and darker than the line around it to read as emphasis
   rather than as a rendering accident. */
.lead strong { font-weight: 800; color: var(--text); }

/* ---- Section head: eyebrow + title ----------------------------------------
   The eyebrow chip above each title is the sibling sites' pattern, so a section
   is labelled by kind ("Timeline", "Eligibility") before its name is read. */
.geo-head-text { min-width: 0; }
.geo-head-text .eyebrow { margin-bottom: 10px; }

/* ---- Introduction: the map sheet -------------------------------------------
   A sheet laid out the way a printed map sheet is: the statement across the
   FIELD with the compass rose out in the right margin, and the objectives ruled
   into a LEGEND BLOCK along the foot.

   WHY THIS ONE IS NOT A .gx-glass PANEL, AND NOT A COLUMN SPLIT.
   ----------------------------------------------------------------------------
   All four Explorer sites share this layout engine, and each is meant to carry
   its own design rather than its own hue. The first attempt at that changed only
   the trim — a sheet instead of a glass card, waypoint markers instead of
   rounded icon tiles — and kept the sibling composition underneath: paragraph in
   a left column, list in a right column, ornament under the paragraph. It still
   read as their section, because arrangement is what the eye takes in before any
   of the detail.

   So the arrangement is what changed here. Stacked, not split: the copy owns the
   full width of the field at a reading measure, and the objectives run ACROSS the
   foot in a bordered key divided by dashed rules, rather than DOWN a column
   beside the copy. It is also shorter than the split, because the paragraph is no
   longer setting the panel's height against a list standing next to it.

   Not the Important Dates rail with different trim either: that is stations
   threaded on a line with nothing boxing them, and its own comment says the rail
   is the shape the siblings use. This is a ruled key with a titled tab.

   A deliberate ONE-SECTION treatment, not a panel style to roll out. The rest of
   the page stays on .gx-glass — a sheet everywhere is a sheet nowhere. */
.geo-intro-sheet {
    /* the paper tone, named because the sheet, the legend block and anything that
       has to punch a hole in the graticule all have to agree on it exactly */
    --sheet-paper: #fdf7ec;
    position: relative;
    padding: clamp(24px, 3vw, 40px) clamp(22px, 2.8vw, 38px);
    border: 1px solid rgba(18, 60, 99, .45);
    border-radius: 3px;   /* all but square — a trimmed sheet, not a card */
    background:
        repeating-linear-gradient(0deg,  rgba(18, 60, 99, .06) 0 1px, transparent 1px 56px),
        repeating-linear-gradient(90deg, rgba(18, 60, 99, .06) 0 1px, transparent 1px 56px),
        var(--sheet-paper);
    box-shadow: 0 12px 30px rgba(16, 40, 62, .07);
}
/* the inner neatline */
.geo-intro-sheet::before {
    content: "";
    position: absolute; inset: 8px;
    border: 1px solid rgba(18, 60, 99, .20);
    pointer-events: none;
}
/* Corner ticks, where a sheet's neatline is trimmed. Eight background layers on
   one pseudo-element: a horizontal and a vertical bar pinned to each corner of
   the inner rule. */
.geo-intro-sheet::after {
    --tick: rgba(18, 60, 99, .55);
    content: "";
    position: absolute; inset: 8px;
    pointer-events: none;
    background:
        linear-gradient(var(--tick), var(--tick)) 0    0    / 18px 2px  no-repeat,
        linear-gradient(var(--tick), var(--tick)) 0    0    / 2px  18px no-repeat,
        linear-gradient(var(--tick), var(--tick)) 100% 0    / 18px 2px  no-repeat,
        linear-gradient(var(--tick), var(--tick)) 100% 0    / 2px  18px no-repeat,
        linear-gradient(var(--tick), var(--tick)) 0    100% / 18px 2px  no-repeat,
        linear-gradient(var(--tick), var(--tick)) 0    100% / 2px  18px no-repeat,
        linear-gradient(var(--tick), var(--tick)) 100% 100% / 18px 2px  no-repeat,
        linear-gradient(var(--tick), var(--tick)) 100% 100% / 2px  18px no-repeat;
}

/* ---- The field: statement, and the rose in the margin ----------------------
   `auto` on the second track, not a fraction: the rose is a fixed ornament and
   should take exactly its own width, leaving the copy the rest up to its measure
   and the slack between them as margin. A fractional track would have grown the
   rose on wide screens, which is the one thing a marginal ornament must not do. */
.geo-intro-field {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: clamp(24px, 4vw, 56px);
    align-items: center;
}
.geo-intro-lead p {
    /* A reading measure, capped. The copy has the field to itself now, and a
       justified line running the full 1130px of the sheet would be unreadable —
       the cap is what keeps this a text block on a sheet rather than a banner. */
    max-width: 66ch;
    margin: 0;
    font-size: clamp(1rem, 1.15vw, 1.08rem);
    line-height: 1.75;
    color: var(--text-soft);
    /* Justified, with whole words — hyphens deliberately off, as in .geo-justify.
       ------------------------------------------------------------------------
       Worth knowing what changed: in the old column split this was the tightest
       justified measure on the site, about 45-50 characters, and the loose word
       spacing that produced was weighed and accepted. On the field it runs to the
       66ch cap, which is a forgiving measure — so the setting that was the risky
       one here is now the comfortable one. If it ever reads as too loose again,
       the fix is ragged-right, not hyphens. */
    text-align: justify;
    hyphens: none;
    text-wrap: pretty;
}
.geo-intro-lead p + p { margin-top: .8em; }
/* the sheet is not a .gx-glass, so the panel link rule up in the palette block
   no longer reaches this copy — say it here instead */
.geo-intro-lead a:not(.btn) { color: var(--link); }

/* ---- The compass rose ------------------------------------------------------
   MyGeo's drawn map sheet inside a surveyor's bearing ring: two hairline circles
   with graduations between them, one every 15°, and an orange north mark on the
   limb. Decoration only.

   In the margin beside the copy, which is where a map carries its rose. It used
   to sit under the paragraph, filling the foot of a column — a job that no longer
   exists now that the copy is not in a column. */
.geo-intro-art {
    position: relative;
    justify-self: end;
    width: clamp(116px, 12vw, 150px);
    aspect-ratio: 1;
    display: grid; place-items: center;
}
/* The limb, and the inner circle the graduations are struck against. `inset: 7%`
   is the radius arithmetic: inset from all four edges of a square shrinks the
   radius by the same fraction, so 7% in gives a circle at r=43% — exactly the
   inner edge of the tick band below. Keep the two numbers in step or the ticks
   float free of their circle. */
.geo-intro-art::before {
    content: "";
    position: absolute; inset: 0; border-radius: 50%;
    border: 1px solid rgba(18, 60, 99, .38);
}
.geo-intro-art::after {
    content: "";
    position: absolute; inset: 7%; border-radius: 50%;
    border: 1px solid rgba(18, 60, 99, .22);
}
/* The graduations: a repeating conic gradient masked down to the band between
   the two circles, so 24 ticks fall at 15° each. One slow rotation, which reads
   as a bezel being turned rather than as anything asking for attention.

   The ticks are 2° wide, not the 1.1° first tried: at this size that came out
   about a pixel across, and at .5 opacity a one-pixel navy hairline on cream is
   effectively invisible. Widen the tick before reaching for more contrast.

   `closest-side` IS LOAD-BEARING, and the percentages are doubled because of it.
   A radial-gradient sizes itself `farthest-corner` by default, so on a square box
   100% is the corner — 70.7% of the width, not 50%. Written the obvious way, as
   `transparent 0 43%, #000 43% 49.5%`, the band lands at r=30-35% of the box
   instead of 43-49.5% and the graduations are struck straight through the drawn
   sheet. `closest-side` puts 100% on the edge midpoint, so the stops are simply
   twice the radius wanted: 43% -> 86%, 49.5% -> 99%. Anything reading this to
   move the band should convert the same way. */
.geo-rose-ticks {
    position: absolute; inset: 0; border-radius: 50%;
    background: repeating-conic-gradient(from 0deg, #123c63 0 2deg, transparent 2deg 15deg);
    -webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 0 86%, #000 86% 99%, transparent 99%);
            mask: radial-gradient(circle closest-side at 50% 50%, transparent 0 86%, #000 86% 99%, transparent 99%);
    opacity: .6;
    animation: geo-ring-spin 26s linear infinite;
}
/* Unmasked, that gradient is a solid striped disc sitting over the drawing —
   far worse than no graduations at all, so drop them rather than let it render. */
@supports not ((-webkit-mask: radial-gradient(#000, #000)) or (mask: radial-gradient(#000, #000))) {
    .geo-rose-ticks { display: none; }
}
/* The north mark: one orange arrowhead in the clear paper between the inner
   circle (r=43%, i.e. 7% in) and the drawn sheet (r=31%, 19% in), pointing out at
   the graduations. Orange as a FILL, which is the only thing this brand's orange
   is allowed to do. */
.geo-rose-north {
    position: absolute; top: 8.5%; left: 50%;
    width: 0; height: 0;
    transform: translateX(-50%);
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-bottom: 9px solid #ff980d;
    filter: drop-shadow(0 1px 2px rgba(16, 40, 62, .28));
}
/* the drawn sheet fills the inner circle */
.geo-intro-art-core {
    display: grid; place-items: center;
    width: 70%; height: 70%; border-radius: 50%;
    box-shadow: 0 10px 24px rgba(16, 40, 62, .18);
}
.geo-sheet { display: block; width: 100%; height: 100%; overflow: visible; }

/* ---- The legend block ------------------------------------------------------
   The objectives along the foot of the sheet: a titled tab, then one cell per
   objective, divided by dashed rules and read ACROSS. A map's legend.

   Read across, not down, is the whole difference. A vertical list of icon-and-
   sentence rows beside the copy is the sibling shape no matter what the icon is
   drawn as, and it is what made the first pass at this section still read as
   theirs. It is also the cheaper layout by height: three sentences side by side
   cost one block of three lines instead of three blocks of two. */
.geo-key {
    margin-top: clamp(20px, 2.6vw, 30px);
    border: 1px solid rgba(18, 60, 99, .30);
    background: rgba(255, 255, 255, .40);   /* lifts the block off the graticule */
}
.geo-key-label {
    margin: 0;
    padding: 9px clamp(14px, 1.6vw, 18px);
    border-bottom: 1px solid rgba(18, 60, 99, .22);
    max-width: none;          /* a <p> in a prose column inherits the 66ch measure */
    font-size: .72rem; font-weight: 700;
    letter-spacing: .16em; text-transform: uppercase;
    color: #123c63;
}
/* `--keys` comes from the row count in the markup, the way the Important Dates
   rail takes `--stations`. Fixed tracks rather than auto-fit on purpose: the
   dashed dividers below are drawn with an adjacent-sibling selector, which has no
   way to know which cell starts a wrapped row, so the row must not wrap. Below
   900px it becomes one column and the dividers turn horizontal. */
.geo-key-row {
    display: grid;
    grid-template-columns: repeat(var(--keys, 3), minmax(0, 1fr));
    list-style: none;
    margin: 0;
    padding: 0;
    max-width: none;
}
.geo-key-cell {
    display: grid;
    grid-template-columns: 32px minmax(0, 1fr);
    gap: 12px;
    align-items: start;
    padding: 15px clamp(14px, 1.6vw, 18px);
    max-width: none;
}
.geo-key-cell + .geo-key-cell { border-left: 1px dashed rgba(18, 60, 99, .26); }
.geo-key-cell p {
    margin: 0;
    max-width: none;
    color: var(--text-soft);
    font-size: .95rem;
    line-height: 1.55;
}
/* A legend swatch: square, because that is what a key uses, and because the
   rounded chip and the circle are both already spoken for on this page — the
   chip is the siblings' feature tile, the circle is the Important Dates station. */
.geo-key-sym {
    display: grid; place-items: center;
    width: 32px; height: 32px;
    border-radius: 3px;
    color: #123c63;
    background: var(--sheet-paper, #fdf7ec);
    border: 1.5px solid #123c63;
    transition: background-color .3s ease;
}
/* orange as a FILL under the navy icon stroke, never as the ink itself */
.geo-key-cell:hover .geo-key-sym { background: #ffb44a; }

/* One column below 900px: at that width the cells are each too narrow to hold a
   sentence without breaking it into three or four words a line, and the field's
   two tracks are too narrow to sit side by side. */
@media (max-width: 900px) {
    .geo-intro-field { grid-template-columns: 1fr; gap: clamp(18px, 3vw, 26px); }
    .geo-intro-art { justify-self: center; }
    .geo-intro-lead p { max-width: none; }
    .geo-key-row { grid-template-columns: 1fr; }
    .geo-key-cell + .geo-key-cell {
        border-left: 0;
        border-top: 1px dashed rgba(18, 60, 99, .26);
    }
}
@media (prefers-reduced-motion: reduce) {
    .geo-rose-ticks { animation: none; }
    .geo-key-sym { transition: none; }
}

/* ---- Competition format ----------------------------------------------------
   Five specs, a handful of words each. As full-width label/value rows they made
   a panel that was mostly rule and empty space; as cells they fit two to a row
   and the section is about a third of the height.

   TWO EQUAL COLUMNS, not content-sized chips. These packed like chips before —
   flex, each cell as wide as its own words, the row wrapping where it ran out.
   That reads as a tag cloud rather than a spec sheet: no two cells lined up, so
   the eye had no column to run down, and the last row centred itself under
   whatever the row above happened to be. Two equal columns give the labels one
   left edge each and the section reads as a table without being one.

   An odd last spec takes the whole row rather than sitting half-width with a gap
   beside it — five specs come out 2/2/1, which is also the row that suits the
   longest value. .geo-links uses this same :last-child:nth-child(odd) trick for
   the same reason.

   Which spec lands there is the CONTENT's business, not this rule's: it is
   whichever one is last in admin. That is now Necessity — the longest value —
   which is the one that suits the wide row, so the two agree. This note used to
   record the opposite (Languages last, Necessity stranded in a half cell); the
   rows were reordered in admin since. If they are reordered again the wide row
   follows, which is the point: it is the editor's call, not this rule's, because
   it changes which spec the section emphasises. */
/* Narrower than the container, and centred. At the full 1132px of content width
   the cells came out 559px each against ~78px tall — wide, flat slabs holding
   two short words, which is the proportion this was asked to move away from.
   1040 puts a cell at 511px, and the padding and icon below bring it to ~94px
   tall, which is about 5.4:1 rather than 7:1. The section still spans wider than
   the copy anywhere else on the page, so it does not read as inset. */
.geo-format {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 18px;
    max-width: 1040px;
    margin-inline: auto;
}
.geo-fmt {
    display: grid;
    grid-template-columns: 54px minmax(0, 1fr);
    align-items: center;
    gap: 16px;
    padding: 20px 22px;
    border-radius: 18px;
}
.geo-fmt:last-child:nth-child(odd) { grid-column: 1 / -1; }
/* One column on a phone: at 560px a half cell is ~250px, which is narrower than
   the longest value's first word run and breaks the label off its own line. */
@media (max-width: 560px) {
    .geo-format { grid-template-columns: minmax(0, 1fr); }
}
.geo-fmt-ico {
    display: grid; place-items: center;
    width: 54px; height: 54px; border-radius: 15px;
    background: linear-gradient(140deg, rgba(255, 152, 13, .18), rgba(251, 230, 200, .38));
    border: 1px solid rgba(18, 60, 99, .14);
}
.geo-fmt-text { min-width: 0; }
/* set at the same size as the stage labels, so the two card sets on this page
   name themselves at one size rather than two */
.geo-fmt-label {
    display: block;
    font-size: .9rem; font-weight: 800;
    letter-spacing: .1em; text-transform: uppercase;
    color: #123c63;
    margin-bottom: 3px;
}
[data-theme="warm"] .geo-fmt p {
    margin: 0; max-width: none;
    color: var(--text); font-weight: 600; font-size: 1.04rem; line-height: 1.4;
}

/* ---- Category cards --------------------------------------------------------
   Artwork on the left, the levels on the right. The two categories are the
   page's fork in the road, so each one gets a drawn globe: the globe in its sky for
   Primary, cut away to the core for Secondary. */
/* One card per row, not two side by side. Side by side, each card got about a
   third of the page for its text, and every line broke — "Standard 4, 5, and 6"
   left "6" alone on a second line, and Secondary's "Year 7, 8, 9, 10, 11, and 12"
   was worse. Full width, each school and its levels sit on one line. */
/* One column sized to the widest card's content, centred in the panel — so each
   card wraps its planet and levels instead of stretching the full width and
   carrying empty space on both sides. max-content on the column (rather than a
   width on each card) is what keeps the two cards the same width, so the planets
   line up; minmax(0, …) lets them shrink on a phone rather than overflow. */
.geo-cats {
    display: grid; gap: 18px;
    grid-template-columns: minmax(0, 1fr);
}
/* Side by side once there is room for it. The two categories are the page's fork in
   the road — a visitor belongs to exactly one of them — and stacked they read as a
   sequence rather than a choice, with the section running to twice the height it
   needs.

   One pair of equal cards, like the fee tiers: 1fr columns rather than max-content,
   and stretched rather than aligned to the top. Sized to their own content they came
   out visibly different — Secondary carries three rows and a line that wraps, Primary
   two — so a choice between two equal options was drawn as one big box and one small
   one. Equal cards say the two are alternatives; unequal cards say one matters more. */
@media (min-width: 900px) {
    .geo-cats {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        align-items: stretch;
        gap: clamp(18px, 2.2vw, 28px);
    }
}
.geo-cat {
    display: grid;
    /* planet, then the text taking whatever is left. 1fr rather than max-content now
       that the cards are equal: the rules stretch to the card's edge instead of
       stopping at the longest line and leaving a ragged gap beside them. */
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    gap: clamp(14px, 2.4vw, 26px);
    padding: clamp(18px, 2.4vw, 26px);
    border-radius: 20px;
    transition: transform .3s ease, box-shadow .3s ease, border-color .3s ease;
}
.geo-cat:hover {
    transform: translateY(-4px);
    box-shadow: 0 18px 40px rgba(16, 40, 62, .13);
    border-color: rgba(255, 152, 13, .45);
}
.geo-cat-art {
    display: grid; place-items: center;
    filter: drop-shadow(0 10px 18px rgba(16, 40, 62, .22));
}
.geo-cat-art .geo-cat-art-svg { display: block; width: clamp(84px, 11vw, 112px); height: auto; }
.geo-cat-body { min-width: 0; }
.geo-cat h3 { margin: 0 0 12px; font-size: clamp(1.25rem, 2.2vw, 1.6rem); }
.geo-cat ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.geo-cat li {
    max-width: none;
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr);
    gap: 9px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--border);
}
.geo-cat li:last-child { padding-bottom: 0; border-bottom: 0; }
/* the icon is the only thing on the row that is not text, so it aligns to the
   text baseline rather than the row box */
.geo-cat li svg { justify-self: center; color: #123c63; position: relative; top: 3px; }
.geo-cat-txt { line-height: 1.5; }
.geo-cat li b { color: #08243f; }
.geo-cat-txt span { color: var(--text-soft); }
@media (max-width: 520px) {
    /* stacked: at this width the planet and a wrapped school name fight for room */
    .geo-cat { grid-template-columns: 1fr; justify-items: center; text-align: center; }
    .geo-cat-body { width: 100%; }
    .geo-cat li { text-align: left; }
}
@media (prefers-reduced-motion: reduce) {
    .geo-cat { transition: none; }
    .geo-cat:hover { transform: none; }
}

/* ---- Ring counters ---------------------------------------------------------
   The figure sits inside a dashed orbit, as on the sibling sites, so last
   cycle's reach reads as an achievement rather than as another spec row. */
/* Geometry matched to Biology's .gx-count: same ring diameter, same gap, same
   label and figure sizes — only the colours differ, because the palette is what
   is supposed to tell the two sites apart, not the scale. */
.geo-counters {
    display: flex; justify-content: center;
    gap: clamp(28px, 7vw, 90px); flex-wrap: wrap;
}
.geo-count { text-align: center; }
.geo-ring {
    position: relative;
    width: clamp(130px, 20vw, 168px); aspect-ratio: 1;
    margin: 0 auto 8px;
    display: grid; place-items: center; border-radius: 50%;
    /* a near-white disc under the figure, as on the sibling sites: it separates the
       ring from the page wash, which is what the travelling arc needs to read against */
    background: radial-gradient(circle at 50% 42%, rgba(255, 152, 13, .22), rgba(255, 255, 255, .9) 66%);
    border: 1px dashed rgba(18, 60, 99, .42);
}
/* The travelling arc. At 1px in pale orange it was invisible against a pale
   background — the ring looked static. It is now 3px, in the darker end of the
   brand ramp, and runs over two borders so the arc is a quarter of the circle
   rather than a tick: a longer, darker mark is what makes the rotation readable
   at a glance. The track it runs on is darkened to match. */
.geo-ring::before {
    content: ""; position: absolute; inset: -9px; border-radius: 50%;
    border: 3px solid rgba(18, 60, 99, .16);
    border-top-color: #c96806;
    border-right-color: rgba(217, 141, 24, .55);
    filter: drop-shadow(0 0 4px rgba(217, 141, 24, .35));
    animation: geo-ring-spin 12s linear infinite;
}
@keyframes geo-ring-spin { to { rotate: 1turn; } }
@media (prefers-reduced-motion: reduce) { .geo-ring::before { animation: none; } }
.geo-ring b {
    font-family: var(--font-display); font-weight: 800;
    font-size: clamp(2.4rem, 5.5vw, 3.4rem); line-height: 1;
    background: linear-gradient(92deg, #c96806, #123c63);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
}
.geo-count > span {
    display: block; margin-top: 10px;
    font-size: .86rem; font-weight: 600;
    letter-spacing: .14em; text-transform: uppercase; color: var(--text-soft);
}

/* ---- Timeline --------------------------------------------------------------
   The dates are a sequence, so they run as cards along a rule rather than as
   stacked label/value pairs. Competition day is the one a visitor must not miss,
   so it is pulled out with the orange wash. */
.geo-rail {
    list-style: none; margin: 0; padding: 0;
    position: relative;
    display: grid;
    grid-template-columns: repeat(var(--stations, 4), minmax(0, 1fr));
    gap: 18px;
}
/* the rail: one line threaded behind the discs, stopped short of the first and
   last so it does not dangle past the run */
.geo-rail::before {
    content: "";
    position: absolute; z-index: 0;
    left: 10%; right: 10%; top: 37px; height: 3px;   /* half the disc */
    border-radius: 2px;
    background: linear-gradient(90deg, rgba(18, 60, 99, .35), #a8752b 45%, #ff980d);
    opacity: .5;
}
.geo-station { position: relative; z-index: 1; text-align: center; }

/* the station disc: icon, with the step number tucked into its corner. Kept white
   rather than tinted — the icons inside are full-colour illustrations, and a tinted
   disc muddies them. */
.geo-station-node {
    position: relative;
    display: grid; place-items: center;
    width: 74px; height: 74px; margin: 0 auto 14px;
    border-radius: 50%;
    background: #fff;
    border: 1px solid rgba(18, 60, 99, .2);
    /* the cream halo punches the rail out behind each disc */
    box-shadow: 0 0 0 7px var(--bg), 0 0 0 8px rgba(18, 60, 99, .12), 0 8px 18px rgba(16, 40, 62, .1);
    color: #123c63;
    transition: transform .3s ease, box-shadow .3s ease;
}
.geo-station:hover .geo-station-node {
    transform: translateY(-4px);
    box-shadow: 0 0 0 7px var(--bg), 0 0 0 8px rgba(255, 152, 13, .45), 0 14px 26px rgba(16, 40, 62, .16);
}
.geo-station h3 {
    margin: 0 0 8px; font-size: .98rem; line-height: 1.35;
    color: #08243f; text-wrap: balance;
}
.geo-station-date {
    display: inline-block; margin: 0;
    padding: 5px 12px; border-radius: 999px;
    background: var(--water-soft);
    border: 1px solid rgba(18, 60, 99, .16);
    font-size: .86rem; font-weight: 700; color: #123c63;
}

/* competition day: the one date a visitor must not miss. Marked with an orange ring
   and glow rather than an orange fill — a fill would sit under the illustration and
   flatten it. */
.geo-station.is-hl .geo-station-node {
    border: 2px solid #e07a0c;
    box-shadow: 0 0 0 7px var(--bg), 0 0 0 9px rgba(217, 141, 24, .4), 0 10px 26px rgba(217, 141, 24, .3);
}
/* the date itself, not just the disc: a tinted pill sat too close to the pale-water
   ones beside it to register as the date that matters, so this one is a solid amber
   chip with the CTA's own fill */
.geo-station.is-hl .geo-station-date {
    background: linear-gradient(135deg, #ff980d, #e07a0c);
    border-color: #c96806;
    color: #101b26;
    font-weight: 800;
    box-shadow: 0 6px 16px rgba(217, 141, 24, .34);
}
.geo-station.is-hl h3 { color: #123c63; font-weight: 800; }

/* Below 780px the rail turns vertical: four stations across a phone leaves each
   title three words wide. Same run, rotated — not a different component. */
@media (max-width: 780px) {
    .geo-rail {
        grid-template-columns: 1fr; gap: 4px;
        /* the run is only as wide as a disc plus a title, so left-aligned it left
           half the panel empty and read as pushed to one side. Capped and centred
           instead, so it sits in the middle of the panel at any width. */
        max-width: 470px; margin-inline: auto;
    }
    /* One line down the whole list overshot the last disc and dangled. Drawn per
       station instead: each segment runs between two discs, so the run starts at
       the first disc's centre and stops at the last one's. */
    .geo-rail::before { display: none; }
    .geo-station::before {
        content: "";
        position: absolute; z-index: 0;
        left: 35.5px; width: 3px;
        top: -4px; bottom: -4px;          /* -4px = the row gap, so segments meet */
        border-radius: 2px;
        background: rgba(145, 123, 41, .42);
    }
    .geo-station:first-child::before { top: 50%; }
    .geo-station:last-child::before { bottom: 50%; }
    .geo-station:only-child::before { display: none; }
    /* Title and date are ONE stacked block beside the disc, not two grid rows.
       ------------------------------------------------------------------------
       They used to be: `grid-template-columns: 74px 1fr` with the title placed in
       column 2 row 1 and the pill in column 2 row 2. Row 1 is as tall as the disc
       that shares it — 74px — so `align-items: center` floated the title in the
       middle of that 74px and dropped the pill onto a row that begins BELOW the
       whole disc. The two lines of one station ended up ~30px apart, which read
       as a date belonging to nothing rather than to the title above it.

       Spanning the disc across both rows does not fix it: a 74px item spanning
       two rows whose content only adds up to ~59px makes the grid share the
       surplus between them, which pushes the title and pill apart again.

       So the disc comes out of flow entirely and the station becomes a centred
       flex column holding just the two text lines. Their spacing is then the h3's
       own margin and nothing else, and `justify-content: center` centres the pair
       against the disc as a unit — which is what the row was always meant to be.

       Height comes from min-height alone, with no vertical padding — under
       border-box the two would argue about whether the 74px includes the padding,
       and the disc is centred on the station box, so one number is easier to
       reason about than three.

       110px, not 74px. 74 is the disc, and at 74 the stations stack so tightly
       that each disc's 7px cream halo meets the next one's and erases the rail
       running between them — the sequence stops looking like a sequence. 110
       leaves 18px clear above and below the disc, so with the 4px row gap there
       is ~26px of visible rail between one halo and the next. Still shorter than
       the old grid version, which ran 131px a station because the pill sat on a
       row of its own below the disc. */
    .geo-station {
        display: flex;
        flex-direction: column;
        justify-content: center;
        min-height: 110px;
        padding: 0 0 0 90px;           /* 74px disc + the 16px gutter it used to have */
        text-align: left;
    }
    /* Centred with a negative margin, NOT translateY(-50%): the hover rule owns
       `transform` on this element and would wipe the centring the moment a finger
       or cursor touched the station. -37px is half the 74px disc. */
    .geo-station-node {
        position: absolute;
        left: 0;
        top: 50%;
        margin: -37px 0 0;
    }
    .geo-station h3 { margin: 0 0 6px; }
    /* Flex blockifies the inline-block pill and stretch would run it the full
       column width — same reason the grid version needed justify-self. */
    .geo-station-date { align-self: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
    .geo-station-node { transition: none; }
    .geo-station:hover .geo-station-node { transform: none; }
}

/* ============================================================================
   PREPARATORY WORKSHOP — the one section that is not on the page field
   ----------------------------------------------------------------------------
   Everything else on this page is a panel on the cream wash. The workshop is the
   only paid add-on and the only thing besides registering that a visitor can act
   on, so it gets the page's one strong visual break: a deep-green band, its own
   panel, and amber cards inside it. That is the whole point of keeping the rest
   of the page quiet — a page where every section shouts has no way to say
   "this one is different".
   ========================================================================== */
/* The band carries the cartography motif too, in the light-ink set. It is the one big
   opaque surface on the site, so the page-wide backdrop scatter (painted on
   body::before, far below this) cannot reach it — left alone it was the only
   section on any page with nothing drawn on it.

   Six layers, front to back: a compass rose in each bottom corner, the pin and
   graticule scatter tiled over the whole field, then the two blooms and the base
   that were already here. Restated as one `background` shorthand
   rather than layered on with a second rule: a stray `background-image` would
   win the image list while leaving this declaration's repeat/position/size in
   place, and the two would then disagree about how many layers exist.

   The three list properties below must stay the same length as the image list —
   six entries each, in the same order. Add a layer and all four lines change. */
.geo-workshop {
    position: relative;
    margin: clamp(18px, 3vw, 36px) 0;
    padding-block: clamp(34px, 5vw, 62px);
    background:
        var(--geo-rose-light),
        var(--geo-rose-light),
        var(--geo-ground-tex-light),
        radial-gradient(120% 90% at 12% -10%, rgba(255, 152, 13, .20), transparent 58%),
        radial-gradient(110% 90% at 92% 4%, rgba(127, 178, 240, .14), transparent 55%),
        linear-gradient(165deg, #08243f 0%, #123c63 46%, #0a2b48 100%);
    /* the two roses sit past the bottom edge so they read as continuing off the
       band's foot rather than as stickers floating on it; the negative insets are
       clipped by the border box, which is exactly what is wanted */
    background-position:
        left -26px bottom -30px,
        right -34px bottom -38px,
        left top, 0 0, 0 0, 0 0;
    background-size:
        188px 140px,
        224px 166px,
        440px 440px, auto, auto, auto;
    background-repeat:
        no-repeat, no-repeat, repeat, no-repeat, no-repeat, no-repeat;
    border-block: 1px solid rgba(255, 152, 13, .35);
    color: #f6efe2;
}
/* A contour line along the foot, the same close the tinted bands get — in pale
   ink, and sitting inside the band's own bottom padding so it never meets the
   panel. */
.geo-workshop::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 22px;
    background: var(--geo-contour-tile-light) bottom center / 220px 22px repeat-x;
    pointer-events: none;
}
/* the relief seam the other bands use, in the light ink this one needs */
.geo-workshop::before {
    content: "";
    position: absolute; inset-inline: 0; top: 0; height: 6px;
    background-image: radial-gradient(circle at 3px 3px, rgba(255, 152, 13, .45) 1px, transparent 1.2px);
    background-size: 12px 6px;
    pointer-events: none;
}
/* Head: on the dark band the deep-green heading colour is invisible, and the
   gradient half of the title reads as a smudge, so both are set in light ink. */
.geo-workshop .geo-head .section-title,
[data-theme="warm"] .geo-workshop .geo-head .section-title { color: #fff; }
.geo-workshop .section-title .grad {
    background: linear-gradient(92deg, #f6cf9e 0%, #ff980d 50%, #7fb2f0 100%);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
}
/* The chip is darkened, not lightened, and that is the whole fix.
   #f6cf9e on a rgba(255,255,255,.12) chip over this band measured 4.37:1, and
   3.53:1 over the band's lightest patch (the amber bloom in the top-left corner)
   — under the 4.5 AA floor for text this small, which the eyebrow is at .72rem.
   Lifting the text instead does not get there: even #ffefdb, almost white and
   well off the amber, only reaches 4.29:1 on that lightest patch, because the
   white chip is carrying the background up as fast as the text goes. Tinting the
   chip DOWN moves the two apart instead of together — .20 black gives 7.72:1
   average and 6.28:1 worst case with the amber text left exactly as designed. */
[data-theme="warm"] .geo-workshop .eyebrow {
    background: rgba(0, 0, 0, .20);
    border-color: rgba(255, 152, 13, .5);
    color: #f6cf9e;
}
/* .geo-badge is kept legible on the band in case the eligibility chip is ever put
   back, but nothing renders it on this page today. */
.geo-workshop .geo-badge {
    background: linear-gradient(135deg, #ff980d, #e07a0c);
    border-color: #c96806;
    color: #101b26;
}
/* The panel is a light sheet laid on the dark band, not another translucent green
   layer. Two tones, not one: the band alone carries the "this section is
   different" signal, and the reading happens on cream — the workshop is the
   longest run of text on the page, and it was all set on dark green. */
[data-theme="warm"] .geo-workshop .gx-glass {
    background: linear-gradient(180deg, rgba(255, 255, 255, .97) 0%, rgba(253, 247, 232, .97) 100%);
    border-color: rgba(255, 255, 255, .45);
    box-shadow: 0 24px 60px rgba(10, 30, 18, .4);
    /* the band sets light ink for the heading above; the panel has to set it back,
       or list items — which no base rule colours — inherit white onto white */
    color: var(--text-soft);
}
[data-theme="warm"] .geo-workshop .gx-glass p,
[data-theme="warm"] .geo-workshop .gx-glass li { max-width: none; color: var(--text-soft); }

/* Tabs: the underline treatment is built for a light panel and vanishes on the
   band, so on this section they become pills — amber for the tabs you can go to,
   dark for the one you are on, which is how the workshop's own material sets
   them out. Centred, because the panel below is symmetrical. */
.geo-workshop .geo-tabs {
    justify-content: center; gap: 10px;
    border-bottom: 0;
    margin-bottom: clamp(18px, 2.4vw, 26px);
}
.geo-workshop .geo-tab {
    border-radius: 999px;
    padding: 10px 20px;
    background: linear-gradient(150deg, #fbac53, #ef8410);
    border: 1px solid #c96806;
    color: #101b26;
}
.geo-workshop .geo-tab:hover {
    background: linear-gradient(150deg, #ffbe85, #ff980d);
    color: #0d151d;
}
/* Solid, not translucent: this was tuned when the panel behind it was dark green,
   and on the cream panel a translucent fill washes out to grey. */
.geo-workshop .geo-tab[aria-selected="true"] {
    background: linear-gradient(150deg, #1d5a94, #0c3050);
    border-color: #123c63;
    color: #fff;
}
.geo-workshop .geo-tab:focus-visible { outline: 2px solid #f6cf9e; outline-offset: 2px; }

/* Blocks inside the panel — the workshop's own material runs as titled blocks,
   not as loose prose. */
.geo-wk-block {
    padding: clamp(16px, 2vw, 24px);
    border-radius: 16px;
    background: #fff;
    border: 1px solid rgba(18, 60, 99, .14);
    box-shadow: 0 6px 18px rgba(16, 40, 62, .05);
}
.geo-wk-block + .geo-wk-block,
.geo-wk-facts + .geo-wk-block,
.geo-wk-block + .geo-wk-facts { margin-top: 16px; }
.geo-wk-block h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; font-size: 1.15rem; }
.geo-wk-block h4 { display: flex; align-items: center; gap: 9px; margin: 18px 0 8px; font-size: 1rem; }
.geo-wk-block h3 + p, .geo-wk-block h4 + ul { margin-top: 0; }
.geo-wk-block ul { margin: 0; padding-left: 1.15em; }
.geo-wk-block li { margin-bottom: .4em; }
.geo-wk-note {
    margin: 14px 0 0; padding: 10px 14px;
    border-left: 3px solid #ff980d; border-radius: 0 10px 10px 0;
    background: rgba(255, 152, 13, .12);
    font-size: .92rem; font-style: italic;
}
.geo-wk-cta { margin: 18px 0 0; }
/* the Infopack tab is a centred sheet of posters, so its button centres with them */
.geo-wk-posters + .geo-wk-cta { text-align: center; margin-top: 22px; }

/* Date / Time / Venue and Dates / Fees — amber cards, the one place on the page
   that colour is used as a fill rather than an accent. */
.geo-wk-facts {
    display: grid; gap: 14px; margin-top: 16px;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.geo-wk-facts--two { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.geo-wk-fact {
    padding: 16px 18px;
    border-radius: 14px;
    background: linear-gradient(155deg, #fbac53, #ef8410);
    border: 1px solid #c96806;
    color: #101b26;
}
/* .gx-glass is an ancestor of these cards, so the panel's light-ink rules above
   out-specify a plain `.geo-wk-fact p`. Matched through the panel here, which is
   what keeps the ink dark on the amber fill. */
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact h4 { margin: 0 0 8px; color: #101b26; font-size: .95rem; }
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact p { margin: 0; color: #13202d; font-weight: 600; }
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact li { color: #13202d; }
.geo-wk-fact dl { margin: 0; }
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact dt { margin-top: 8px; font-weight: 700; color: #101b26; }
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact dt:first-child { margin-top: 0; }
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact dd { margin: 0 0 0 14px; color: #35301a; }
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact b { color: #101b26; }

/* Contact + social, two columns inside one block */
.geo-wk-contact { display: grid; gap: clamp(14px, 3vw, 40px); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.geo-wk-contact h4:first-child { margin-top: 0; }
[data-theme="warm"] .geo-wk-contact li span { color: var(--text-mute); }

/* Infopack posters. Capped and centred rather than stretched to the panel: they
   are portrait, and auto-fit with one or two posters blew a single sheet up to
   the full width of the section. */
.geo-wk-posters {
    display: grid; gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(210px, 300px));
    justify-content: center;
}
.geo-wk-posters a {
    display: block; border-radius: 14px; overflow: hidden;
    border: 1px solid rgba(18, 60, 99, .18);
    box-shadow: 0 12px 26px rgba(16, 40, 62, .14);
    transition: transform .3s ease;
}
.geo-wk-posters a:hover { transform: translateY(-4px); }
.geo-wk-posters img { display: block; width: 100%; height: auto; }
@media (prefers-reduced-motion: reduce) {
    .geo-wk-posters a { transition: none; }
    .geo-wk-posters a:hover { transform: none; }
}

/* ---- Workshop tabs ---------------------------------------------------------
   The workshop carries more content than the rest of the page combined, so it is
   split across three panels rather than run as one column. */
.geo-tabs {
    display: flex; gap: 6px; flex-wrap: wrap;
    margin: 0 0 18px;
    border-bottom: 1px solid rgba(18, 60, 99, .18);
}
.geo-tab {
    appearance: none; background: transparent; cursor: pointer;
    border: 0; border-bottom: 2px solid transparent; border-radius: 10px 10px 0 0;
    font-family: var(--font-display); font-weight: 600; font-size: .95rem;
    padding: 9px 15px;
    color: var(--text-soft);
    transition: color .2s, background .2s, border-color .2s;
}
.geo-tab:hover { color: var(--accent); background: rgba(255, 152, 13, .10); }
.geo-tab[aria-selected="true"] { color: #123c63; background: rgba(255, 152, 13, .14); border-bottom-color: #ff980d; }
.geo-tab:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* !important: [hidden] loses to any display rule that lands on the panel later —
   the popup overlay was invisible-but-clickable for exactly this reason. */
.geo-panel[hidden] { display: none !important; }

/* ---- Syllabus --------------------------------------------------------------
   Two full-width blocks rather than two columns: Primary is three subject groups
   and Secondary is four, and each group needs the width to hold a topic name. */
.geo-syl { display: grid; gap: 16px; }
.geo-syl-block { padding: clamp(18px, 2.2vw, 26px); border-radius: 18px; }
.geo-syl-block h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; }

/* The legend: a track per subject group, numbered topics down each.

   auto-fit rather than a track count from the markup, and the count is left to
   come out of the content. The grid lays as many 210px tracks as fit and
   collapses the ones nothing lands in, so Primary's three groups make three
   columns and Secondary's four make four, off the same rule — and a fifth group
   added in the admin gets a track without anyone editing this file. It steps
   down to two tracks and then one as the viewport narrows, which is the whole
   of the responsive behaviour; no media query below.

   min() on the minimum: a bare minmax(210px, 1fr) cannot go under 210px, so on a
   narrow phone the track would overflow the panel it sits in rather than
   shrink. */
.geo-syl-map {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr));
    gap: clamp(14px, 2vw, 28px) clamp(18px, 2.4vw, 34px);
}
/* The rule over each group is the legend's key line. Every group carries the
   same one — nothing here ranks them, and the order is the editor's.

   Amber rather than --brand-300, which was the first choice and measured 1.36:1
   against the panel. That is a line you have to look for. A key line that has to
   be looked for is not doing the one job it has, which is to say where a group
   starts. --brand is 2.02:1 and carries on hue besides, and amber is a FILL here
   — never text — which is the only way this palette permits it. */
.geo-syl-group { border-top: 2px solid var(--brand); padding-top: 11px; }
.geo-syl-group-name {
    margin: 0 0 9px;
    font-size: .76rem;
    font-weight: 800;
    letter-spacing: .07em;
    text-transform: uppercase;
}
/* The colour has to carry the theme and the panel, or it does not land at all.
   warm-theme.css paints every h4 inside a .gx-glass with `[data-theme="warm"]
   .gx-glass h4`, which is (0,2,1); a bare `.geo-syl-group-name` is (0,1,0) and
   loses to it no matter which file the browser reads last. Written the short
   way, this declaration was dead and the names rendered --text — legible, so
   nothing looked broken, which is exactly how the palette bug in the :root note
   at the top of this file shipped. Same shape, same fix. */
[data-theme="warm"] .gx-glass .geo-syl-group-name { color: var(--accent); }
.geo-syl-topics { list-style: none; margin: 0; padding: 0; }
/* Grid, not a bullet: the number sits in its own track so a topic that wraps
   hangs under itself instead of running back beneath its marker. The second
   track is the anonymous item the topic text forms — which is also why the
   space before it in the markup has to stay. */
.geo-syl-topics li {
    display: grid;
    grid-template-columns: 22px 1fr;
    gap: 9px;
    align-items: start;
    max-width: none;
    margin: 0 0 7px;
    font-size: .95rem;
    line-height: 1.45;
}
.geo-syl-topics li:last-child { margin-bottom: 0; }
/* A waypoint, squared off at one corner so it reads as a marker dropped on a
   map rather than as a bullet. Navy on amber measures 5.2:1 — amber stays a
   fill here, which is the one way this palette is allowed to use it. */
.geo-syl-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-radius: 8px 8px 8px 3px;
    background: var(--brand);
    color: var(--accent);
    font-size: .72rem;
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* The fallback path, for a block syllabus_groups() cannot read: the editor's
   rich text as written, in two text columns, each subject kept whole across the
   break. This is what both blocks used to be. */
.geo-syl-cols ul { columns: 2; column-gap: clamp(24px, 3vw, 48px); margin: 0; padding-left: 18px; }
.geo-syl-cols li { break-inside: avoid; margin-bottom: .55em; max-width: none; }
.geo-syl-cols li:last-child { margin-bottom: 0; }
@media (max-width: 760px) {
    .geo-syl-cols ul { columns: 1; }
}

/* ---- Syllabus chips --------------------------------------------------------
   Subject keywords, not a sequence — chips let the eye count the ground covered. */
.geo-chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.geo-chips li {
    max-width: none;
    padding: 5px 12px; border-radius: 999px;
    background: var(--water-soft);
    border: 1px solid rgba(18, 60, 99, .18);
    font-size: .88rem; font-weight: 600; color: #123c63;
}

/* ---- Quick facts bar -------------------------------------------------------
   The four-up strip under the hero, as on both sibling sites. Hairline dividers
   between cells rather than four separate cards: it is one fact read four ways,
   not four unrelated tiles. The amber rule along the top ties it to the header's
   accent line. */
.geo-facts {
    position: relative;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    overflow: hidden;
}
.geo-facts::after {
    content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
    background: linear-gradient(135deg, #ff980d 0%, #a8752b 55%, #123c63 100%);
}
/* Padding left at the sibling sites' value, not tightened with the rest of the page:
   this bar is the first thing under the hero and carries only four figures, so it is
   the one place on the page where the air is doing work.

   The side padding is the exception — 12px was not enough for the longest label
   ("Objective Questions"), which ran within a few pixels of the hairline divider and
   made a four-up bar read as cramped. It scales with the viewport now, so the gutter
   holds at the widths where the labels are closest to wrapping. */
.geo-fact { position: relative; text-align: center; padding: clamp(18px, 2.4vw, 26px) clamp(16px, 1.8vw, 28px); }
.geo-fact + .geo-fact::before {
    content: ""; position: absolute; left: 0; top: 22%; height: 56%; width: 1px;
    background: rgba(18, 60, 99, .16);
}
.geo-fact b {
    display: block;
    font-family: var(--font-display); font-weight: 800;
    font-size: clamp(1.5rem, 2.8vw, 2.1rem); line-height: 1.1;
    color: #123c63;
}
/* Its own class rather than `.geo-fact span`. That selector is (0,1,1) and used to also
   match the icon's span at (0,1,0), overriding it and turning the disc into a block —
   which is why the icons once sat hard left of centred figures. The icons are gone now,
   but the class stays: naming the label directly is what stops the next element added
   to this cell from inheriting label styling by accident. */
.geo-fact-label {
    display: block; margin-top: 8px;
    font-size: .8rem; font-weight: 600; letter-spacing: .02em;
    color: var(--text-soft);
    text-wrap: balance;
}
@media (max-width: 700px) {
    .geo-facts { grid-template-columns: repeat(2, 1fr); }
    /* with two per row, cells 1 and 3 start a row and must not carry a divider */
    .geo-fact:nth-child(odd)::before { display: none; }
}

/* ---- Centred card ----------------------------------------------------------
   The closing-CTA pattern: a glass card whose heading, copy and button all sit on
   the centre line.

   The second rule is the one that matters. redesign.css gives every paragraph in a
   glass card `max-width: var(--measure)` (66ch) to keep long prose readable. That
   constrains the box but does not move it, so in a centred card the words inside
   were centring while the box itself stayed flush left — the copy and the button
   ended up left of the heading, which is full-width and genuinely centred. Putting
   the box back on the centre line is what margin-inline does here.

   redesign.css already carries `.books-empty p, .pyq-empty p { margin-inline: auto }`
   for the same reason; those two classes do not exist on this site, so this is the
   general form rather than a third one-off.

   NOT written as `.geo-card-center :where(p, .prose)`. :where() contributes zero
   specificity, which put that selector at (0,1,0) — under `.prose p` at (0,1,1), whose
   `margin: 0 0 1rem` SHORTHAND resets margin-inline back to 0. Richtext copy (which is
   always wrapped in .prose) therefore stayed flush left while plain <p> in the same
   card centred fine. Each selector below is deliberately specific enough to win on its
   own rather than on file order. */
.geo-card-center { text-align: center; }
.geo-card-center p,
.geo-card-center .prose,
.geo-card-center .prose p { margin-inline: auto; }

/* A list is the one thing that should NOT have its items centred: bullets have to line
   up under each other or the list stops reading as one. The block is centred and the
   items inside it stay left-aligned. Neither card carries a list today — this is here
   because the copy is editor-written richtext, so one can appear without a deploy. */
.geo-card-center .prose :is(ul, ol) {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;
}

/* ---- Quick links -----------------------------------------------------------
   Three onward routes above the closing CTA. Two-up, with the third centred
   across the full width rather than left hanging in an empty second column. */
.geo-links { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.geo-links > :last-child:nth-child(odd) { grid-column: 1 / -1; justify-self: center; max-width: 520px; width: 100%; }
.geo-link { text-align: center; }
.geo-link h3 { margin: 0 0 14px; font-size: var(--fs-h4); color: var(--accent); }
@media (max-width: 700px) {
    .geo-links { grid-template-columns: minmax(0, 1fr); }
    .geo-links > :last-child:nth-child(odd) { max-width: none; }
}

.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 18px; }
.stat { text-align: center; padding: 22px 16px; }
.stat-num {
    display: block;
    font-size: clamp(2.2rem, 6vw, 3.4rem);
    font-weight: 800;
    line-height: 1;
    background: linear-gradient(92deg, #c96806, #123c63);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
}
.stat-label { display: block; margin-top: 8px; font-size: .9rem; letter-spacing: .1em;
    text-transform: uppercase; font-weight: 600; color: var(--text-soft); }

/* Generic responsive tile grid used by categories, stages, disciplines, awards. */
.tile-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.tile { padding: 20px 22px; }
/* flex so a leading icon sits on the text baseline row rather than pushing the
   heading onto two lines */
.tile h3 { display: flex; align-items: center; gap: 9px; margin: 0 0 8px; font-size: 1.05rem; }
.tile h3 .gx-ico { flex: 0 0 auto; color: var(--accent); }
/* text-wrap: pretty as the tile default — these are the narrowest text column on the
   site (240px minimum in the auto-fit grid), so they are where a wrapping line is most
   likely to strand its last word. The address overrides this to `balance` just below;
   see there for why the two differ. */
.tile p, .tile li { color: var(--text-soft); text-wrap: pretty; }
.tile ul { margin: 0; padding-left: 18px; }

/* The postal address. Its longest line is 41 characters and this tile's text column
   holds about 32 (the "Taman Danau Kota…" line fits exactly), so the street line
   cannot fit and will wrap whatever we do here — the only question is where.
   `pretty` optimises against a stranded last word, which broke it as
   "…Off Jalan / Genting Klang," and split the road name. `balance` evens the two
   halves instead, which puts the break before "Jalan" and keeps "Jalan Genting Klang"
   whole. Balanced per segment, so the hard <br> breaks the editor typed still stand.

   Written as `.tile p.geo-addr` on purpose: `.tile p` above is (0,1,1), so a bare
   `.geo-addr` at (0,1,0) would lose to it and keep inheriting `pretty`. */
.tile p.geo-addr { text-wrap: balance; }

/* ---- The five MyGeo objectives, set as a chart legend ----------------------
   Three cards across with the last two centred left an orphan pair and told the
   reader nothing about order. A plotted route fixed the order but cost a screen
   and a half of height: alternating cards means every row carries an empty side,
   so half the section was blank by construction.

   A legend is what a chart uses when it has to explain five things in the least
   room possible — a key, one line per entry, symbols in a column down the left.
   It keeps the numbering, so the order still reads, and it has no facing side to
   leave empty. Two columns on a wide screen, one on a phone. About 340px tall
   against the route's 1250px, for the same five objectives and the same words.

   The rule between entries is a hairline, not a border: a legend's entries are
   separated, not boxed, and five boxes inside a panel that is itself a box was
   the visual noise the cards had in the first place. */

.geo-legend {
    list-style: none;
    margin: 0;
    padding: 0;
    /* redesign.css caps `.gx-glass li` at the reading measure, which is right for
       a bullet of prose and wrong for a list item that is the layout. It is
       written in :where(), so it carries no specificity and nothing was
       overriding it. */
    max-width: none;
}
.geo-legend > li { max-width: none; break-inside: avoid; }
.geo-legend-entry {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr);
    align-items: start;
    gap: 12px;
    padding: 14px 0;
    border-top: 1px solid rgba(18, 60, 99, .13);
}
.geo-legend-key {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: #fffdf7;
    border: 2px solid #123c63;
    color: #123c63;
    font-weight: 800;
    font-size: .82rem;
    /* tabular figures so 01 and 05 are the same width and the discs do not jitter */
    font-variant-numeric: tabular-nums;
}
.geo-legend-name {
    margin: 0 0 .15em;
    font-size: 1rem;
    line-height: 1.3;
    color: #08243f;
}
.geo-legend-note {
    margin: 0;
    font-size: .92rem;
    line-height: 1.5;
    color: var(--text-soft);
}

/* No entry is picked out. The last one was filled orange while this section was a
   plotted route, where it read as the destination the line ended at. Without the
   line it only reads as a rank, and the source claims none: the live site lists
   five objectives with no emphasis on any. It was also keyed to :last-child —
   position, not meaning — so a sixth objective would have moved the emphasis to
   whatever happened to end up last. */

@media (min-width: 820px) {
    /* Two COLUMNS, not a two-column grid. A grid makes both columns share a row
       height, so the tallest entry of each pair sets the height of the entry
       beside it and leaves a hole under the shorter one — with entries running
       from two lines to five, that hole was most of the saving. Column flow lets
       each entry take only the height it needs and fills down then across, so
       01–03 sit left and 04–05 right with nothing padded to match.

       Every entry keeps its top rule here, including the first of each column:
       with column flow there is no way to know which entry starts column two,
       and a rule across the top of both columns reads as the legend's own head
       rule rather than as one column being ruled and the other not. */
    .geo-legend {
        columns: 2;
        column-gap: clamp(24px, 3vw, 44px);
    }
}

/* ---- The twelve syllabus topics, set as an index ---------------------------
   See the note in about-us.blade.php for why this is not a grid of cards. The
   short version: four cards across gave each topic 215px, which turned a
   forty-word description into twelve lines and broke the topic names mid-word,
   and every grid row was as tall as its tallest card.

   Column flow rather than a two-column grid, for the same reason the Objectives
   legend uses it: a grid would make each pair of entries share a height, and
   these run from three lines to six. */

.geo-index {
    list-style: none;
    margin: 0;
    padding: 0;
    /* redesign.css caps `.gx-glass li` at the reading measure — right for a
       bullet of prose, wrong for a list item that is the layout, and written in
       :where() so it carries no specificity to notice. */
    max-width: none;
}
.geo-index > li { max-width: none; break-inside: avoid; }
.geo-index-entry {
    padding: 13px 0;
    border-top: 1px solid rgba(18, 60, 99, .13);
}
.geo-index-topic {
    margin: 0 0 .2em;
    font-size: .98rem;
    line-height: 1.3;
    color: #08243f;
    /* redesign.css sets `overflow-wrap: anywhere` on every heading, which breaks
       INSIDE a word as soon as the word does not fit and also shrinks the
       heading's min-content — which is how "resource management" came to be set
       as "resource manageme / nt" in the old narrow cards. `break-word` breaks
       only when a word genuinely cannot fit and leaves min-content alone, the
       same correction geo-theme.css already makes for table cells. */
    overflow-wrap: break-word;
}
/* The number sits in the margin rather than in the text, so the topic names line
   up with each other instead of starting at four different x positions behind
   "1", "10", "11", "12". Padding on the heading makes room for it. */
.geo-index-topic { padding-left: 32px; }
.geo-index-num {
    display: inline-block;
    width: 32px;
    margin-left: -32px;
    color: #8a5d1e;
    font-weight: 700;
    /* tabular figures so 01 and 12 occupy the same width */
    font-variant-numeric: tabular-nums;
}
.geo-index-note {
    margin: 0;
    padding-left: 32px;
    font-size: .9rem;
    line-height: 1.5;
    color: var(--text-soft);
}

@media (min-width: 820px) {
    .geo-index {
        columns: 2;
        column-gap: clamp(28px, 3.4vw, 48px);
    }
}

/* Discipline / partner imagery. */
.media-tile img { width: 100%; height: 168px; object-fit: cover; border-radius: 12px; margin-bottom: 14px; }
/* Partner logos. One box per logo rather than a shared height cap: the marks run
   from a 3.2:1 wordmark (Universiti Malaya) to a near-square crest (USM), and
   capping height alone gave the wide ones three times the width of the narrow
   ones. That is what pushed the seventh partner onto a row of its own — the row
   was 6 + 1 not because seven is too many, but because two of the seven were
   taking the space of six.

   Boxed and gapped as below, all seven fit the panel's 1044px at full width with
   room to spare (7 x 128 + 6 x 20 = 1016). Narrower than that it wraps centred,
   which is the right behaviour once one row genuinely cannot hold them. */
.logo-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: clamp(14px, 1.6vw, 20px) clamp(14px, 1.6vw, 20px);
}
.logo-strip img {
    flex: 0 0 auto;
    width: clamp(100px, 10vw, 128px);
    height: clamp(44px, 4.2vw, 56px);
    /* contain, so a logo is fitted into its box rather than cropped to it */
    object-fit: contain;
    opacity: .85;
}

/* Download / link rows (reading materials, past year papers, results). */
.link-list { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.link-row {
    display: flex; align-items: center; gap: 12px;
    padding: 13px 16px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    text-decoration: none;
    color: inherit;
    transition: border-color .2s, box-shadow .2s, transform .2s;
}
.link-row:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); transform: translateY(-1px); }
.link-row svg { flex: 0 0 auto; color: #c96806; }
.link-row span { font-weight: 600; font-size: .95rem; }

/* ---- Past-year papers, one column per year --------------------------------
   The years come from the database, so auto-fit rather than a stated column
   count: two years give two columns, three give three, and a single year fills
   the panel rather than sitting in half of it with a hole alongside.

   320px is the floor because a paper title plus its icon needs about that
   much before the row starts wrapping; below it the columns stack.

   align-items: start so a year with three papers does not stretch the heading
   of the year beside it that has one. */
.geo-years {
    display: grid;
    gap: clamp(20px, 3vw, 36px);
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    align-items: start;
}
/* The year is a column head now, not a section heading with the panel to
   itself, so it drops from the h2 display size to something proportionate to
   the one or two rows beneath it. */
.geo-year-title {
    margin: 0 0 12px;
    font-size: clamp(1.35rem, 2.2vw, 1.7rem);
}
/* The paper itself, previewed in the browser's own PDF viewer.
   ---------------------------------------------------------------------------
   A4 by aspect-ratio rather than a fixed height, so the preview keeps the
   proportions of the thing it is previewing at any column width.

   Hidden below 820px, where the columns have stacked: a phone would be showing
   a page of A4 at ~340px wide — unreadable — and iOS Safari renders only the
   first page of a PDF in an iframe, often nothing at all. The download link
   underneath is the whole interaction there, which is what it was before. */
.paper { display: grid; gap: 10px; }
.paper + .paper { margin-top: 18px; }
.paper-preview {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1.414;
    border: 1px solid var(--border-strong);
    border-radius: 12px;
    background: #fff;
    box-shadow: var(--shadow-sm);
}
/* Shown by <object> only when the browser has no PDF viewer, so it is never on
   screen beside a working preview — it replaces it. */
.paper-fallback {
    margin: 0;
    display: grid;
    place-content: center;
    height: 100%;
    padding: 24px;
    text-align: center;
    font-size: .92rem;
    color: var(--text-soft);
}
@media (max-width: 819px) {
    .paper-preview { display: none; }
}

/* ---- Instruction note ------------------------------------------------------
   For the one line on a page that tells the reader what to DO, as opposed to
   what happened. On the results page that is "press CTRL + F and find your
   school name", which used to be the last paragraph of the announcement, in
   italics — where it read as prose trailing off rather than as the instruction
   for the buttons directly beneath it.

   Water blue rather than orange: orange is this brand's call-to-action, and the
   buttons under this note are the actual action. */
.geo-note {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin: 22px 0 0;
    padding: 13px 16px;
    border: 1px solid rgba(10, 82, 153, .22);
    border-left: 4px solid #0a5299;
    border-radius: 12px;
    background: rgba(127, 178, 240, .13);
    color: #123c63;
    font-size: .95rem;
    line-height: 1.55;
    /* the shared .gx-glass measure cap would hold this to 66ch inside a panel it
       is meant to span */
    max-width: none;
}
.geo-note .gx-ico { flex: 0 0 auto; color: #0a5299; margin-top: 2px; }

/* Centred wrap for a link list whose count does not divide by its column count.
   The default .link-list is an auto-fit grid, which is right when the rows come
   out full — four links in a three-wide row do not, and grid cannot centre a
   part-full last row. Flex can. Each link keeps a third of the row so the
   orphan matches the width of the three above it rather than stretching. */
.link-list--centred {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
}
.link-list--centred > .link-row { flex: 0 1 calc((100% - 2 * 10px) / 3); }
@media (max-width: 820px) {
    .link-list--centred > .link-row { flex-basis: calc((100% - 10px) / 2); }
}
@media (max-width: 560px) {
    .link-list--centred > .link-row { flex-basis: 100%; }
}

/* ---- The year title blocks on /igeo ---------------------------------------
   Three sections that differ only by a year need a heading you can identify at
   a glance while scrolling, not two lines of navy and a line of grey.

   Four things do the work, and none of them adds a word of content:
     - the year, set enormous and nearly transparent behind the block, the way a
       chapter number sits behind a chapter opening;
     - the heading itself, larger and set tighter than a normal section title,
       so "Malaysian Team / at iGeo 2025" reads as a two-line lockup rather than
       as a sentence that wrapped;
     - the year in the heading in warm gradient, picked out of the navy;
     - the host city as a pin on a chip, because it is a place.

   The watermark comes from data-year via a pseudo-element, so it cannot be
   selected, copied or read aloud — it is texture, and the same digits are
   already in the heading for anyone reading the page rather than looking at it. */
/* max-width: none, because .geo-head caps every section head at 720px. That is
   right for a two-word title over a centred lead, and it is what broke
   "Malaysian Team at iGeo 2025" across two lines — the heading needs about
   850px at this size and the panel has 944, so the only thing standing between
   it and one line was the shared cap. */
/* .geo-head.geo-year-head, not .geo-year-head: both are one class, so they tie
   on specificity and .geo-head — which is 1000 lines further down this file —
   wins on order. Written as one class it lost silently and the heading kept
   wrapping with the override sitting right there looking correct. */
.geo-head.geo-year-head {
    position: relative;
    isolation: isolate;
    max-width: none;
    /* room above the heading for the part of the watermark that rises past it */
    padding-top: clamp(8px, 1.6vw, 22px);
}
.geo-year-head::before {
    content: attr(data-year);
    position: absolute;
    left: 50%;
    /* centred on the heading, not on the block: at the block's centre the
       numeral's baseline crossed the scale-bar divider underneath it and made
       the bar hard to read */
    top: 34%;
    transform: translate(-50%, -50%);
    z-index: -1;
    /* Sized to fit the block it sits behind. At 12.5rem the numeral stood taller
       than the heading area, so its top was cut off by the panel edge above —
       a watermark with a flat top reads as a graphic that failed to load. */
    font-size: clamp(5rem, 11vw, 9.5rem);
    font-weight: 800;
    line-height: 1;
    letter-spacing: -.05em;
    white-space: nowrap;
    color: rgba(18, 60, 99, .06);
    pointer-events: none;
}
/* Bigger and tighter than the shared section title: this one is a lockup, and a
   heading set at normal leading over two lines leaves a gap the watermark shows
   straight through. */
.geo-year-head .section-title {
    font-size: clamp(1.9rem, 4.4vw, 3.15rem);
    line-height: 1.04;
    letter-spacing: -.015em;
    margin-bottom: .34em;
}
/* The site gradient runs orange into navy, which reads as a deliberate ramp
   across a word like "Objectives" — but a year is four characters, so the navy
   end landed on the last digit and "2025" came out with a grey 5 that looked
   like a rendering fault. Warm the whole run instead: over four characters the
   ramp should read as one colour deepening, not as two colours meeting.
   (0,3,0) so it outranks redesign.css's own `.section-title .grad`. */
.geo-year-head .section-title .grad {
    background: linear-gradient(96deg, #ffa726 0%, #ff980d 45%, #c96806 100%);
    background-clip: text;
    -webkit-background-clip: text;
}
.geo-place {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin: 0;
    padding: 5px 14px 5px 11px;
    border-radius: 999px;
    border: 1px solid rgba(18, 60, 99, .18);
    background: #fffdf7;
    box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
    color: #123c63;
    font-size: .92rem;
    font-weight: 600;
    letter-spacing: .01em;
}
.geo-place .gx-ico { color: #ff980d; flex: 0 0 auto; }
/* The host country's flag, closing the chip. width:auto because these are not
   all one ratio — Ireland is 2:1, Thailand and Indonesia 3:2 — so fixing the
   width would stretch two of the three. The hairline is what keeps a flag with
   a white band (all three of them) from bleeding into the chip behind it. */
.geo-place-flag {
    flex: 0 0 auto;
    height: 1.05em;
    width: auto;
    margin-left: 3px;
    border-radius: 2px;
    box-shadow: 0 0 0 1px rgba(18, 60, 99, .25);
}

@media (max-width: 560px) {
    /* At 390px the watermark is wider than the panel and its edges bleed past
       the card. Smaller, and fainter still, so it stays texture. */
    .geo-year-head::before { font-size: 4.6rem; color: rgba(18, 60, 99, .05); }
}
@media (prefers-reduced-motion: no-preference) {
    /* nothing to animate here — declared so the block is not mistaken for one
       that lost its motion query */
}

/* Video tutorial grid. */
/* ---- Tutorial playlist ----------------------------------------------------
   Player and playlist as one unit: a single rounded, clipped block with the
   list flush against the player, its own titled header and a count. The list
   is set on navy — the footer's colour — so the panel reads as one
   object against the cream card rather than as a player with some links next
   to it, and the playing entry can be picked out in amber against it.

   The list scrolls to the player's height rather than growing the section, so
   a sixth tutorial costs the page no height at all. */
.video-playlist {
    display: grid;
    grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
    border-radius: 18px;
    overflow: hidden;
    box-shadow: 0 18px 44px rgba(30, 40, 25, .22);
}
.video-stage iframe {
    display: block;
    width: 100%;
    height: 100%;
    aspect-ratio: 16 / 9;
    border: 0;
    background: #121a14;
}

.video-side {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: linear-gradient(170deg, #08243f 0%, #05192b 100%);
}
.video-side-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 16px 16px 12px;
    border-bottom: 1px solid rgba(255, 255, 255, .12);
}
/* both selectors: warm-theme.css colours every h3 inside a .gx-glass dark, and
   this one sits on the dark panel — see the .fee-card note above */
.video-side-head h3,
[data-theme="warm"] .gx-glass .video-side-head h3 {
    margin: 0;
    font-size: 1rem;
    font-weight: 700;
    color: #fff;
}
.video-count {
    flex: 0 0 auto;
    padding: 4px 11px;
    border-radius: 999px;
    background: rgba(255, 152, 13, .16);
    border: 1px solid rgba(255, 152, 13, .42);
    color: #f6cf9e;
    font-size: .74rem;
    font-weight: 800;
    letter-spacing: .04em;
    text-transform: uppercase;
    white-space: nowrap;
}

.video-list {
    list-style: none;
    margin: 0;
    padding: 8px;
    display: grid;
    gap: 6px;
    align-content: start;
    overflow-y: auto;
    /* the player's height, so the two columns end level */
    max-height: min(46vw, 400px);
    scrollbar-width: thin;
    scrollbar-color: rgba(255, 152, 13, .5) transparent;
}
.video-item {
    display: grid;
    grid-template-columns: 92px minmax(0, 1fr) auto;
    gap: 11px;
    align-items: center;
    width: 100%;
    padding: 7px;
    border: 0;
    border-left: 3px solid transparent;
    border-radius: 10px;
    background: transparent;
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: background .2s ease, border-color .2s ease;
}
.video-item:hover { background: rgba(255, 255, 255, .08); }
.video-item.is-playing {
    background: rgba(255, 152, 13, .17);
    border-left-color: #ff980d;
}
.video-thumb {
    display: block;
    width: 92px;
    height: 52px;
    object-fit: cover;
    border-radius: 7px;
}
.video-name {
    font-size: .88rem;
    font-weight: 700;
    line-height: 1.35;
    color: rgba(255, 255, 255, .86);
}
.video-item.is-playing .video-name { color: #fff; }
/* the run time, where one is written — right-aligned so the titles keep a
   straight edge whether or not their neighbour has one */
.video-time {
    font-size: .78rem;
    font-weight: 700;
    color: rgba(255, 255, 255, .6);
    white-space: nowrap;
}

@media (max-width: 900px) {
    .video-playlist { grid-template-columns: 1fr; }
    .video-stage iframe { height: auto; }
    .video-list { max-height: 300px; }
}

/* Map embed on the contact page. */
.map-embed { width: 100%; aspect-ratio: 16 / 8; border: 0; border-radius: 14px; }
@media (max-width: 640px) { .map-embed { aspect-ratio: 4 / 3; } }

/* ============================================================================
   HOME HERO
   ----------------------------------------------------------------------------
   Hypsometric tints: five relief bands stacked behind a two-column split (copy
   left, key visual right). The bands are gradients rather than an image, so they
   scale to any viewport and cost nothing to load.

   Hypsometric tinting is the technique a physical map uses to show elevation —
   a graded fill between contour lines, running from the blue of the sea through
   lowland green or cream, upland tan, and into brown for high ground. That is
   what these five bands are, in the MyGeo palette. It replaces the sibling Earth
   Science site's geological cross-section, which drew the same five slabs as
   rock strata: the right motif for that competition, and the wrong one here.
   ========================================================================== */

.geo-hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: clamp(28px, 5vw, 68px) 0 clamp(34px, 6vw, 80px);
    margin-bottom: clamp(10px, 2vw, 24px);
}

/* The relief bands. Each is a horizontal slab with a slight tilt, stacked in
   hypsometric order — shelf, lowland, upland, highland, mountain.

   Elevation increases DOWN the hero, which is the opposite of the way a map
   legend prints the same ramp. That is deliberate and it is a legibility
   choice, not an oversight: the headline and the Register button sit in the top
   third, and the ramp's dark end under 3rem type would have cost either the
   contrast or the tint. Running it downward puts the palest bands where the
   text is and lets the brown land in the empty space at the foot.

   Every band is a wash — .06 to .22 alpha — so the whole ramp reads as tinted
   paper rather than as five stripes. */
.geo-hero-relief { position: absolute; inset: 0; z-index: -1; }
.geo-hero-relief span {
    position: absolute;
    left: -8%; right: -8%;
    transform: rotate(-1.6deg);
    border-radius: 40%;
}
/* shelf — the only cool band, and the one that says the ramp starts at sea */
.geo-hero-relief span:nth-child(1) { top: 0;   height: 20%; background: linear-gradient(180deg, rgba(127, 178, 240,.20), rgba(127, 178, 240,0)); }
/* lowland */
.geo-hero-relief span:nth-child(2) { top: 16%; height: 26%; background: linear-gradient(180deg, rgba(251, 230, 200,.55), rgba(251, 230, 200,0)); }
/* upland */
.geo-hero-relief span:nth-child(3) { top: 40%; height: 24%; background: linear-gradient(180deg, rgba(246, 207, 158,.42), rgba(246, 207, 158,.06)); }
/* highland */
.geo-hero-relief span:nth-child(4) { top: 62%; height: 22%; background: linear-gradient(180deg, rgba(216, 164, 92,.24), rgba(216, 164, 92,.04)); }
/* mountain */
.geo-hero-relief span:nth-child(5) { top: 80%; height: 26%; background: linear-gradient(180deg, rgba(18, 60, 99,.20), rgba(18, 60, 99,.02)); }

/* 1.12/.88, not 1.05/.95: the lockup is "Geography Explorer" over "Competition"
   and at the old split the first line was a few pixels too wide for its column,
   so it broke into three — Geography / Explorer / Competition — which is not the
   lockup and reads as a wrap rather than a mark. Widening the copy column is half
   the fix; the h1 clamp below is the other half. Anything that narrows this column
   again has to check the title still holds two lines. */
.geo-hero-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.12fr) minmax(0, .88fr);
    gap: clamp(24px, 4vw, 56px);
    align-items: center;
}

/* The vw term matters as much as the cap. Trimming only the maximum fixes 1440px
   and leaves the title breaking again around 1000-1200px, where the viewport term
   is what is driving the size — the column narrows with the viewport while the
   type does not narrow as fast. Both came down together. */
.geo-hero-copy h1 { font-size: clamp(2.1rem, 4.8vw, 3.6rem); margin: 10px 0 14px; line-height: 1.06; }
.geo-hero-copy .lead { margin: 0 0 22px; max-width: 52ch; }
.geo-hero-copy .hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 0; }

/* The workshop ribbon that used to sit under the two CTAs — a pill with a pulsing
   dot linking to #preparatory-workshop — is gone, along with its .geo-ribbon,
   .geo-ribbon-dot and geo-pulse rules. The `hero_ribbon` content block it printed
   is still seeded and still editable in admin; nothing renders it. */

/* Key visual — the competition mark, drifting on a long, slow bob. */
.geo-hero-art { position: relative; min-height: clamp(240px, 34vw, 430px); }
.geo-hero-globe {
    display: block; width: 100%; height: auto;
    filter: drop-shadow(0 26px 50px rgba(16, 40, 62,.24));
    animation: geo-bob 9s ease-in-out infinite;
}

@keyframes geo-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-12px); }
}

@media (max-width: 900px) {
    .geo-hero-grid { grid-template-columns: minmax(0, 1fr); text-align: center; }
    .geo-hero-copy .lead { margin-inline: auto; }
    .geo-hero-copy .hero-actions { justify-content: center; }
    /* The lockup's hard break exists for the two-column hero, where the copy has only
       half the width. Once the hero is a single centred column the title has the whole
       container, so the break is dropped and it sets on one line — falling back to a
       natural wrap on a phone, where one line no longer fits. */
    .geo-hero-copy h1 br { display: none; }
    /* art first would push the copy below the fold on a phone */
    .geo-hero-art { order: 2; max-width: 460px; margin: 0 auto; }
}

/* Motion is decorative here — none of it conveys information. */
@media (prefers-reduced-motion: reduce) {
    .geo-hero-globe { animation: none; }
}

/* ---- Galleries ------------------------------------------------------------ */

/* ---- Table cells: whole words -------------------------------------------
   redesign.css sets `overflow-wrap: anywhere` on every td and th, to stop a
   pasted URL pushing a table wider than its card. It has a second effect that is
   not wanted here: `anywhere` counts toward a cell's MIN-CONTENT width, so the
   browser is told a column can be one character wide. In an auto-layout table it
   then squeezes the columns toward equal widths and breaks words to fit —
   "Stage" set as "Sta / ge", "Registration" as "Registratio / n".

   `break-word` is the same protection without that: it breaks a word only when
   the word genuinely cannot fit, and it does NOT shrink min-content. So a column
   is at least as wide as its longest word, and the columns size to what they
   hold. The overflow-x on .faq-answer stays as the backstop for anything wider. */
.prose :is(td, th) { overflow-wrap: break-word; }

/* Flag artwork in prose.
   Windows ships no font that composes a regional-indicator pair (U+1F1F2 U+1F1FE
   for Malaysia) into a flag, so the emoji renders there as the bare letters "MY"
   — which is what the copy was doing at the end of "Good luck, Team Malaysia!".
   The flag is public/images/emoji/flag-my.svg now, drawn rather than typed.

   Matched on the path, not only on .flag-my: an editor pasting a flag into the
   admin will not know to add a class, and the path is what the swap writes. The
   class is on it too, so the engine's own .eyebrow .flag-my rule still applies if
   one ever lands in an eyebrow.

   The width override is the point of the rule: redesign.css sizes inline emoji
   images 1em SQUARE, and a flag is 2:1, so without this it arrives squashed. */
.prose img[src*="/emoji/flag-"], .prose img.flag-my {
    width: auto; height: .95em;
    vertical-align: -.1em;
    border-radius: 2px;
    box-shadow: 0 0 0 1px rgba(18, 60, 99, .22);
}


/* ---- The medallists' list -------------------------------------------------
   A bronze medal for a marker instead of a bullet, on the one list that names
   people who won one. Drawn in the same construction as x-geo-award-icon's
   medal — ribbon pair, disc, star — so it belongs to the set the site already
   draws rather than arriving as a stock emoji.

   A background image on the <li>, not ::marker: ::marker takes almost no
   properties in a way browsers agree on, and background-size is the one thing
   this needs. list-style: none removes the bullet it replaces.

   The class rides the <ul> in the content block, so the same list can be
   pasted into any richtext field and mark itself. */
.prose ul.medal-list {
    list-style: none;
    padding-left: 4px;
}
.prose ul.medal-list li {
    position: relative;
    padding-left: 34px;
    min-height: 24px;
    background: url("/images/emoji/medal-bronze.svg") left .12em / 22px 22px no-repeat;
}

/* Captioned photo grid (About page iGeo team write-ups).

   Flex, not grid, and the reason is the last row. These sets are whatever length
   the content team uploaded — the 2023 camp has six photos in four columns — so
   a trailing row is usually short. Grid places those leftovers into columns 1
   and 2, which pins them to the left edge under a full row and reads as a
   layout that ran out rather than a set that ended. `justify-content: center`
   on a flex line centres them, and there is no grid equivalent: the tracks are
   fixed by the container, so items cannot be centred within a row.

   flex-grow is 0, which is the other half of it. Left to grow, the two orphans
   would stretch to half the panel each and the last row would be photos twice
   the size of the ones above. Every figure keeps the width it would have had in
   a full row, whichever row it lands on.

   --pg-cols is the whole responsive story: one basis formula, and a breakpoint
   only says how many columns fit. The counts below are where the old
   `auto-fit, minmax(230px, 1fr)` changed its own mind, so nothing reflows at a
   different width than it used to.

   The -1px is a rounding guard. Four bases plus three gaps come to exactly 100%,
   and a sub-pixel overshoot there would wrap the fourth photo onto its own line;
   a pixel shaved off each basis makes that impossible, and `justify-content`
   absorbs the slack. */
.photo-grid {
    --pg-gap: 16px;
    --pg-cols: 4;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--pg-gap);
    margin-top: 26px;
}
.photo-grid figure {
    margin: 0;
    min-width: 0;
    flex: 0 1 calc((100% - (var(--pg-cols) - 1) * var(--pg-gap) - 1px) / var(--pg-cols));
}
@media (max-width: 1090px) { .photo-grid { --pg-cols: 3; } }
@media (max-width:  830px) { .photo-grid { --pg-cols: 2; } }
@media (max-width:  550px) { .photo-grid { --pg-cols: 1; } }
.photo-grid img {
    width: 100%; height: 190px; object-fit: cover;
    border-radius: 12px;
    border: 1px solid var(--border);
    transition: transform .25s, box-shadow .25s;
}
.photo-grid figure:hover img { transform: translateY(-3px); box-shadow: var(--shadow); }

/* A single photo shown whole, rather than as one cell of a grid.
   ---------------------------------------------------------------------------
   The grid above fixes every image at 190px and crops to fill, which is what
   makes a row of thumbnails line up. For a lone photo it is the wrong rule
   twice over: the 4:3 iGeo Task Force group shot came out as a 5.5:1 strip with
   the heads cropped off the nine people its caption names.

   height:auto with the intrinsic width/height that x-geo-img writes onto the
   tag means the browser reserves the right box before the file arrives, so
   nothing shifts as it loads. No object-fit: with no fixed height there is
   nothing to fit into. */
.photo-grid--full { --pg-cols: 1; }
.photo-grid--full img {
    height: auto;
    max-height: none;
    object-fit: initial;
}
/* Whole, but not enormous. Uncropped at the full 1030px panel this 4:3 photo is
   770px tall and takes most of a screen on its own. Capped at 660 it comes out
   about 495 — large enough to pick out nine faces, small enough that the
   write-up above and the years below stay in view with it. The row is a centred
   flex, so the cap centres the figure without anything else. */
.photo-grid--full figure { max-width: 660px; }

/* Captions centre under their photo, which is what the rest of the figure now
   does — the row itself is centred, so a caption pinned to the photo's left edge
   was the one element still aligning to something else.

   text-wrap: balance is not optional here. Four of these captions wrap to two
   lines, and centred text that breaks into a long line over a two-word stub
   reads as a wrapping accident rather than a decision; balance splits the lines
   evenly so the block sits as a tidy centred wedge. It is safe to lean on for
   this: the spec caps balancing at a handful of lines, and no caption in the set
   is longer than two. Where the browser does not support it the caption simply
   wraps normally, still centred. */
.photo-grid figcaption {
    margin-top: 8px;
    font-size: .86rem;
    color: var(--text-soft);
    text-align: center;
    text-wrap: balance;
}

/* ---- Photo lightbox -------------------------------------------------------
   Every gallery photo is wrapped in a link to its full-size file; the script in
   partials/lightbox.blade.php shows it in place instead. zoom-in is the cursor
   that says what the click does before it is made. */
.ph-link { display: block; cursor: zoom-in; border-radius: 12px; }
.ph-link:focus-visible { outline: 2px solid #c96806; outline-offset: 3px; }

.lightbox {
    width: 100%;
    max-width: 100%;
    height: 100%;
    max-height: 100%;
    margin: 0;
    padding: clamp(14px, 3vw, 40px);
    border: 0;
    background: transparent;   /* the ::backdrop is the surface, so a click beside
                                  the photo lands on the dialog and closes it */
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: clamp(8px, 2vw, 20px);
    overflow: hidden;
}
/* Display belongs ONLY on [open]. The browser hides a closed dialog with
   `dialog:not([open]) { display: none }` in its own stylesheet, and any author
   `display` on the element beats that no matter how specific it is — set it here
   and the arrows and the counter stay on the page after the photo is closed. */
.lightbox[open] { display: grid; }
.lightbox::backdrop { background: rgba(18, 26, 20, .88); backdrop-filter: blur(3px); }

.lb-figure {
    grid-column: 2;
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    min-height: 0;
}
.lb-img {
    max-width: 100%;
    /* leaves room for the caption under it and the dialog's own padding */
    max-height: calc(100vh - 150px);
    object-fit: contain;
    border-radius: 12px;
    box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
}
.lb-cap {
    margin: 0;
    max-width: 62ch;
    text-align: center;
    font-size: .92rem;
    color: rgba(255, 255, 255, .88);
}

.lb-btn {
    display: grid;
    place-items: center;
    width: 46px;
    height: 46px;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, .28);
    border-radius: 50%;
    background: rgba(255, 255, 255, .12);
    color: #fff;
    cursor: pointer;
    transition: background .2s ease, border-color .2s ease, color .2s ease;
}
.lb-btn:hover {
    background: linear-gradient(135deg, #ff980d, #e07a0c);
    border-color: #c96806;
    color: #101b26;
}
.lb-prev { grid-column: 1; }
.lb-next { grid-column: 3; }
.lb-close { position: absolute; top: 14px; right: 14px; }
.lb-count {
    position: absolute;
    left: 50%;
    bottom: 12px;
    transform: translateX(-50%);
    margin: 0;
    font-size: .82rem;
    font-weight: 700;
    letter-spacing: .06em;
    color: rgba(255, 255, 255, .72);
}

@media (max-width: 620px) {
    /* the arrows move off the sides and onto the bottom corners, so the photo
       gets the full width rather than a third of it back in gutters */
    .lightbox { grid-template-columns: minmax(0, 1fr); padding: 12px; }
    .lb-figure { grid-column: 1; }
    .lb-img { max-height: calc(100vh - 190px); }
    .lb-prev, .lb-next { position: absolute; bottom: 44px; grid-column: 1; }
    .lb-prev { left: 18px; }
    .lb-next { right: 18px; }
}

/* An eligibility / constraint chip. Deep green rather than the amber used for
   invitations, because it narrows who something is for rather than inviting. */
.geo-badge {
    display: inline-block;
    margin-left: 8px;
    padding: 5px 12px;
    border-radius: 99px;
    background: #123c63;
    color: #e0ebf8;
    font-size: .76rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    vertical-align: middle;
}

/* ============================================================================
   HOME POPUP POSTER
   ----------------------------------------------------------------------------
   Opens over the hero on arrival. The overlay is display:grid so the dialog
   centres, which means the [hidden] attribute alone would not hide it — grid
   beats hidden's implicit display:none — hence the explicit [hidden] rule.
   ========================================================================== */

/* The overlay is a real <dialog> opened with showModal(), so the browser puts it in
   the top layer and paints the dim behind it as ::backdrop. That means: no z-index
   to maintain, no position:fixed, no [hidden] handling (a closed <dialog> is
   display:none by default), and nothing left to swallow clicks on the page beneath.
   The UA's own centring and border are reset here; the frame is .popup-dialog. */
.popup-overlay {
    display: grid;
    place-items: center;
    max-width: 100vw;
    max-height: 100vh;
    width: 100%;
    height: 100%;
    padding: 24px;
    border: 0;
    background: transparent;
    overflow: visible;
    opacity: 0;
    transition: opacity .3s ease;
}
.popup-overlay::backdrop {
    background: rgba(13, 21, 29, .72);
    backdrop-filter: blur(3px);
    opacity: 0;
    transition: opacity .3s ease;
}
.popup-overlay.open,
.popup-overlay.open::backdrop { opacity: 1; }
/* warm-theme.css fills .popup-overlay with a teal dim at (0,2,0), written when this
   was a plain positioned div. The dimming is ::backdrop's job now, so the element
   itself has to stay transparent or the page is dimmed twice. */
[data-theme="warm"] .popup-overlay { background: transparent; }

/* Framed like the sibling sites: the poster is inset inside a gradient border, so
   the announcement reads as a mounted card rather than a bare image floating on the
   dimmed page. Same construction as MyAO and MyBO — 10px of padding, the frame drawn
   as the dialog's own background.

   The colour half lives in the [data-theme="warm"] rule below, not here. warm-theme.css
   already paints this frame turquoise/gold at [data-theme="warm"] .popup-dialog —
   specificity (0,2,0) — which an unqualified .popup-dialog (0,1,0) cannot beat no
   matter that this file loads later. Same trap as the header accent line. */
.popup-dialog {
    position: relative;
    max-width: min(92vw, 560px);
    max-height: 86vh;
    padding: 10px;
    border-radius: calc(var(--r-md) + 10px);
    transform: scale(.94);
    transition: transform .3s cubic-bezier(.2, .9, .3, 1);
}
/* MyGeo's ramp — orange through gold to earth brown — where MyAO runs blue → violet →
   gold and MyBO turquoise → gold. Literal hex, not var(--brand): warm-theme.css
   re-points those tokens under :root[data-theme="warm"] (0,1,1), which outranks this
   file's :root, [data-theme="warm"] block (0,1,0), so --brand here is still turquoise.
   That is why the rest of this stylesheet hard-codes its colours too. */
[data-theme="warm"] .popup-dialog {
    background: linear-gradient(135deg, #ff980d 0%, #a8752b 55%, #123c63 100%);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .5),
                0 30px 80px rgba(28, 26, 18, .45),
                0 0 60px rgba(255, 152, 13, .28);
}
.popup-overlay.open .popup-dialog { transform: scale(1); }

.popup-link { display: block; cursor: pointer; }
/* The white keyline separates the poster from the frame behind it — without it a
   poster whose own edge is dark or amber bleeds into the gradient. The drop shadow
   that used to sit here is gone: the frame now carries the depth. */
.popup-posters img {
    display: block;
    width: 100%;
    height: auto;
    max-height: calc(86vh - 20px);
    object-fit: contain;
    border-radius: var(--r-md);
    border: 2px solid rgba(255, 255, 255, .9);
}

/* More than one poster: a scroll-snapping strip, so all of them are reachable
   without a carousel library. Scrolls with the wheel, swipes on touch, and is
   keyboard-focusable via tabindex on the container. */
.popup-posters.is-strip {
    display: flex;
    gap: 14px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    padding-bottom: 10px;
    -webkit-overflow-scrolling: touch;
}
.popup-posters.is-strip .popup-link {
    flex: 0 0 100%;
    scroll-snap-align: center;
}
.popup-posters.is-strip:focus-visible { outline: 2px solid var(--brand); outline-offset: 4px; }
.popup-posters.is-strip::-webkit-scrollbar { height: 8px; }
.popup-posters.is-strip::-webkit-scrollbar-thumb {
    background: rgba(255, 152, 13, .55);
    border-radius: 99px;
}

.popup-close {
    position: absolute;
    top: -14px;
    right: -14px;
    z-index: 1;
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    cursor: pointer;
    /* white ring, as on the sibling sites — the button now sits on the gradient
       frame rather than on the dimmed page, and amber on gold needs the separation */
    border: 2px solid rgba(255, 255, 255, .85);
    background: #ff980d;
    color: #101b26;
    box-shadow: 0 6px 18px rgba(0, 0, 0, .35);
    transition: background .2s, transform .2s;
}
.popup-close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* Scoped to beat warm-theme.css, which paints this button turquoise at
   [data-theme="warm"] .popup-close — see the note on .popup-dialog above. */
[data-theme="warm"] .popup-close { background: #ff980d; border-color: rgba(255, 255, 255, .85); }
[data-theme="warm"] .popup-close:hover { background: #ffab50; transform: scale(1.06); }

@media (max-width: 560px) {
    .popup-overlay { padding: 18px; }
    .popup-close { top: -10px; right: -10px; width: 34px; height: 34px; }
}

/* Someone who asked for less motion still gets the popup — it carries real
   information — just without the scale-in. */
@media (prefers-reduced-motion: reduce) {
    .popup-overlay,
    .popup-dialog { transition: none; }
    .popup-dialog { transform: none; }
}

/* ============================================================================
   SECTION RHYTHM
   ----------------------------------------------------------------------------
   The inherited engine gives every section the same treatment — glass panel,
   centred head, tile grid — so a long page reads as one texture repeated a
   dozen times and the eye stops finding the boundaries.

   These classes break that up along a cartographic line: sections alternate
   between an open "surface" treatment and a tinted "seam" band, separated by a
   contour rule. The page then reads as a traverse across a sheet, which is the
   same idea the hero opens with.
   ========================================================================== */

/* A tinted band. Used on every other section so the page alternates instead of
   repeating one surface. Full-bleed via the padding trick rather than a
   negative-margin hack, so it never causes a horizontal scrollbar. */
.geo-band {
    position: relative;
    background: linear-gradient(180deg, rgba(251, 230, 200, .28) 0%, rgba(253, 247, 232, .55) 100%);
    border-block: 1px solid rgba(18, 60, 99, .10);
}
/* The highlighted band — Awards on the home page, Practice Books on Reading
   Materials. It was a .13/.09 amber wash, which on cream is close enough to
   nothing that the section read as unmarked; this gives it a real amber field,
   an orange hairline top and bottom, and a compass rose in each lower
   corner, so it is visibly a called-out section rather than a faint tint.

   Deliberately kept BELOW the workshop band, and it must stay that way. The
   workshop is the page's one strong break — solid deep green, light text — and
   the reason the rest of the page is quiet is so that break can register. This
   band is a warm wash under dark text: clearly emphasised, clearly second. Push
   it much further and the page has two things shouting and no hierarchy.

   Same rule as the workshop stack: the three list properties are one entry per
   image, in order. Add a layer and all four lines change together. */
.geo-band--deep {
    background:
        var(--geo-rose-dark),
        var(--geo-rose-dark),
        radial-gradient(120% 90% at 10% -12%, rgba(255, 152, 13, .30), transparent 58%),
        radial-gradient(110% 90% at 92% 6%, rgba(127, 178, 240, .20), transparent 55%),
        linear-gradient(180deg, rgba(255, 152, 13, .22) 0%, rgba(145, 123, 41, .15) 100%);
    background-position:
        left -22px bottom -26px,
        right -30px bottom -34px,
        0 0, 0 0, 0 0;
    background-size:
        168px 125px,
        200px 148px,
        auto, auto, auto;
    background-repeat: no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
    /* longhand, so it overrides only the colour of .geo-band's border-block
       shorthand and leaves its 1px/solid alone */
    border-block-color: rgba(217, 141, 24, .40);
}

/* Contour seam — a hairline with a dotted survey pattern beneath it, the way
   a bedding plane shows in a cross-section. Sits at the top edge of a band. */
.geo-band::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: 0;
    height: 6px;
    background-image: radial-gradient(circle at 3px 3px, rgba(18, 60, 99, .16) 1px, transparent 1.2px);
    background-size: 12px 6px;
    pointer-events: none;
}

/* Centred on the page field, above the panel — the sibling sites' arrangement. */
.geo-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    max-width: 720px;
    margin: 0 auto clamp(26px, 4vw, 44px);   /* Biology's .gx-head, to the pixel */
    text-align: center;
}
.geo-head .section-title { margin: 0; }
/* For a title too long for the 720px measure above. "Geography Explorer
   Preparatory Workshop 2026/2027" sets its first line at about 815px at the h2's
   full size, so at 720 it broke into three lines with "Preparatory" stranded alone
   in the middle — a wrap, not a lockup. At 880 it falls into the two lines it
   should: the name, then "Workshop 2026/2027".

   Widening the head, not shrinking the type: this is the section the page is
   selling hardest, and it was already the same size as every other heading.
   880px still clears the container's inner width at every viewport where the h2
   is at full size, and below that the h2's own clamp takes the title down with
   the viewport faster than the head narrows. */
.geo-head--wide { max-width: 880px; }

@media (max-width: 560px) {
    .geo-head { gap: 10px; }
}

/* ---- Stages: four cards over a relief cross-section -----------------------
   Four stages read left to right as four cards, and under them the ground rises
   in four terraces — lowland, upland, highland, mountain, the same hypsometric
   ramp .geo-hero-relief paints behind the hero. Each card's foot carries its own
   elevation tint, so a card and the terrace beneath it are visibly the same
   stage. Reading across is reading uphill.

   WHY NOT THE 2×2 CARD GRID IT REPLACED.
   ----------------------------------------------------------------------------
   Four rounded cards with icon tiles and bulleted bodies on a 2×2 is the shape
   every Explorer site reaches for. Two problems went with it. It had to carry its
   order in the STAGE labels alone, because no arrow can tell the truth about a
   wrap that travels down AND back to the left — a single row of four cannot be
   misread and needs no arrows. And it ran to about twice this height.

   WHY THE CARDS CAME BACK.
   ----------------------------------------------------------------------------
   A pass without them — four bare columns of label, name and bullets over the
   profile — was legible line by line and unreadable as a section: nothing said
   where a stage ended, so the eye had four headings and no edges to hang them on.
   The card is the fix. It is squared and ruled rather than softly rounded, and
   the tint on its foot is the thing that keeps it part of the drawing instead of
   being a panel with a picture underneath.

   THE PROFILE IS A SEPARATE ELEMENT, AND HAS TO BE. Cards need a gap to read as
   separate; terrain needs none to read as terrain. One grid cannot give a row a
   gap and the row under it none, so the cards and the ground are two grids —
   .geo-climb and .geo-climb-profile — sharing one gap token so their columns line
   up. See .geo-climb-seg for how the terraces are closed back up. */
.geo-climb,
.geo-climb-profile {
    /* one token, two grids: the profile's negative margins are written against
       this, so the two cannot drift out of step */
    --climb-gap: clamp(12px, 1.4vw, 18px);
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--climb-gap);
}
.geo-climb { list-style: none; margin: 0 0 var(--climb-gap); padding: 0; }
/* The card. Square-ish and ruled — the soft rounded panel is the siblings' card
   and is already what .gx-glass does everywhere else on this page. The foot is
   this stage's elevation tint, at a weight that reads as the card standing on its
   own ground. */
.geo-climb-stage {
    display: flex;
    flex-direction: column;
    min-width: 0;
    padding: clamp(15px, 1.8vw, 20px) clamp(15px, 1.8vw, 20px) clamp(12px, 1.4vw, 15px);
    background: #fffdf7;
    border: 1px solid rgba(18, 60, 99, .28);
    border-bottom: 4px solid var(--tint, #f7dcb2);
    border-radius: 3px;
}
.geo-climb-label {
    margin: 0 0 6px;
    max-width: none;
    font-size: .8rem; font-weight: 800;
    letter-spacing: .12em; text-transform: uppercase;
    color: #123c63;
}
.geo-climb-body { min-width: 0; }
.geo-climb-body p { margin-bottom: .4rem; max-width: none; }
/* The stage's name, which is the first line of every stage block.
   `min-height` is two lines of it: two of the four names wrap at four columns and
   two do not, which started every bulleted list on a different line and left the
   row looking ragged. Reserving the second line squares them up. A name long
   enough to take three lines simply pushes its own bullets down — the reserve is
   a floor, not a clamp.

   The line-height is set here as well as on the <strong> inside it, and the two
   numbers depend on each other: the line box is the PARAGRAPH's line-height, not
   the strong's, so leaving the paragraph on the inherited 1.75 made a "two line"
   reserve of 2.7rem come out at about one and a half lines and the squaring-up
   only half worked. 1.3 × 1.04rem × 2 lines = 2.7rem. Change one, change both. */
.geo-climb-body p:first-child {
    margin-top: 0;
    line-height: 1.3;
    min-height: 2.7rem;
}
.geo-climb-body p:first-child strong { font-size: 1.04rem; color: #08243f; line-height: 1.3; }
/* A `ul` inside an `li` takes `circle` from the UA stylesheet — hollow rings here
   against filled bullets everywhere else on the page. Said explicitly. */
.geo-climb-body ul { margin: 6px 0 0; padding-left: 17px; list-style-type: disc; }
.geo-climb-body li {
    color: var(--text-soft);
    max-width: none;
    font-size: .94rem;
    line-height: 1.5;
    margin-bottom: .25em;
}
/* The symbol and the figure, pushed to the foot of the card by `margin-top: auto`
   so all four sit on one line however long the bodies run. The rule above them
   separates the figure from the prose without boxing it. */
.geo-climb-mark {
    margin: auto 0 0;
    max-width: none;
    padding-top: 12px;
    border-top: 1px solid rgba(18, 60, 99, .14);
    display: flex;
    align-items: center;
    gap: 9px;
}
.geo-climb-ico {
    display: grid; place-items: center;
    width: 30px; height: 30px;
    border-radius: 50%;
    background: #fffaf1;
    border: 2px solid #123c63;
    color: #123c63;
    flex: 0 0 auto;
}
/* the figure the funnel is read from */
.geo-climb-spot {
    font-family: var(--font-display);
    font-weight: 800; font-size: .98rem; line-height: 1.2;
    color: #08243f;
    white-space: nowrap;
}

/* ---- The ground ------------------------------------------------------------
   One strip, four segments, each filled from the bottom up to its own `--rise`.
   The fill's top border is the ground surface, so the four together draw one
   stepped profile. */
.geo-climb-profile {
    height: 76px;
    /* the datum — without a line to stand on, the lowland terrace is the palest
       tint on the palest page background and reads as nothing at all */
    border-bottom: 2px solid rgba(18, 60, 99, .45);
}
/* Closing the gap the cards need. Each segment reaches one gap-width to its right,
   so the terraces butt into a continuous landform while their steps still break
   exactly under the card edges above. The last one is left alone, or it would run
   past the container. */
.geo-climb-seg {
    display: flex;
    align-items: flex-end;
    min-width: 0;
    margin-right: calc(var(--climb-gap) * -1);
}
.geo-climb-seg:last-child { margin-right: 0; }
.geo-climb-fill {
    display: block;
    width: 100%;
    height: var(--rise, 26%);
    background: var(--tint, #f7dcb2);
    border-top: 3px solid #123c63;
}
/* The risers. Each step past the first cuts a face where it climbs out of the one
   before it, which is what makes the four read as one landform rather than four
   bars standing next to each other. */
.geo-climb-seg + .geo-climb-seg .geo-climb-fill { border-left: 3px solid #123c63; }

/* One column below 900px: four cards holding a stage name and two bullets each are
   too narrow to set. The ground keeps its steps at a shorter profile, so the
   sequence still climbs as the page scrolls. */
@media (max-width: 900px) {
    .geo-climb { grid-template-columns: minmax(0, 1fr); }
    /* The two-line reserve on the name squares up four narrow columns. In one
       full-width card every name fits on one line, so the reserve only opens a
       blank line above half the bulleted lists — the very raggedness it exists to
       prevent, upside down. */
    .geo-climb-body p:first-child { min-height: 0; }
    .geo-climb-profile { height: 54px; }
}


/* ---- Awards: the podium ---------------------------------------------------
   Three cards, one per band, each showing the award at the size the award is
   worth. The prize band is raised out of the row, ringed in its own orange and
   flagged; the two behind it step down in icon size, type size and lift.

   This was three bars whose WIDTH carried the 10/40/50 split and nothing else.
   Width alone put the largest, loudest mark in the section on "remaining 50% —
   certificate of participation", the least of the three awards, and left the
   prize tier the smallest thing on screen. So the split moved to a meter along
   the foot of each card, where it stays honest, and the card itself carries the
   worth. Freeing the label from having to fit inside its own bar is also what
   let the 10% band finally be drawn at a true 10% rather than a floored 22%.

   Keyed by the KIND of award, the same way the icons are, never by row
   position: reorder the bands in admin and each keeps its own metal, its own
   size and its flag.

   The middle card is deliberately COOL. Every other surface here is warm — the
   band wash, the panel, the prize card — so a third warm card was the reason
   the three read as one gradient rather than as three tiers. The grey-blue is
   the palette's water side, separating on temperature where sand on sand could
   not separate on lightness alone.

   Navy #08243F carries every label; orange stays fill-only, the brand's rule. */

.geo-awards {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    /* start, not stretch: the cards are deliberately different heights, and
       stretching them to match would undo the step down the podium. */
    align-items: start;
    gap: 18px;
}
.geo-award {
    position: relative;
    display: grid;
    justify-items: center;
    text-align: center;
    gap: 10px;
    padding: 30px 22px 26px;
    border-radius: 20px;
    border: 1px solid rgba(18, 60, 99, .16);
    background: linear-gradient(180deg, #fffdf8, #fbf3e6);
    box-shadow: 0 16px 32px -24px rgba(18, 60, 99, .55);
}
.geo-award-ico { display: grid; place-items: center; }
.geo-award-band {
    margin: 0;
    color: #08243f;
    font-weight: 800;
    line-height: 1.1;
    font-size: clamp(1.45rem, 3vw, 1.85rem);
}
/* 30ch: long enough for "will receive a Gold, Silver or Bronze Award
   Certificate" to break into three tidy lines rather than one thin column. */
.geo-award-name { margin: 0; max-width: 30ch; color: var(--text-soft); }

/* The split, drawn. Sized from the band label, so it cannot drift from the
   words above it. */
.geo-award-meter {
    width: 100%; max-width: 250px; height: 9px; margin-top: 6px;
    border-radius: 99px; overflow: hidden;
    background: rgba(18, 60, 99, .10);
}
.geo-award-meter span { display: block; height: 100%; width: var(--w); border-radius: inherit; }
.geo-award-share {
    margin: 0;
    font-size: .78rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: #6b6252;
}

/* The flag rides the top edge, so it must clear the card: the row below reserves
   the space it hangs into. */
.geo-award-flag {
    position: absolute; top: -14px; left: 50%; transform: translateX(-50%);
    padding: 6px 16px; border-radius: 99px; white-space: nowrap;
    background: linear-gradient(135deg, #ffc24d, #ff980d);
    color: #08243f;
    font-size: .72rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
    box-shadow: 0 10px 20px -10px rgba(214, 122, 8, .95);
}

/* Gold / Silver / Bronze — the prize band. Deepest fill, largest medal, largest
   type, a ring of its own colour, and the only card that leaves the row. */
.geo-award--gold {
    border-color: rgba(255, 152, 13, .8);
    /* Deep enough to hold its own against the panel. A paler wash read as cream
       on cream: the card the section is built around cannot be the one you have
       to look for. */
    background: linear-gradient(180deg, #fff1d6, #ffd699);
    box-shadow: 0 0 0 3px rgba(255, 152, 13, .26),
                0 28px 48px -24px rgba(214, 122, 8, .95);
}
.geo-award--gold .geo-award-band { font-size: clamp(1.6rem, 3.4vw, 2.1rem); }
.geo-award--gold .geo-award-meter { background: rgba(154, 92, 8, .16); }
.geo-award--gold .geo-award-meter span { background: linear-gradient(90deg, #ffc24d, #ff980d); }
/* Darker than the palette's #8A5D1E ochre-for-text, on purpose: that token is
   measured against white and the card fill is deeper than white, where it falls
   to 4.18:1. This clears AA on the fill it actually sits on — 5.9:1. */
.geo-award--gold .geo-award-share { color: #6f4712; }

/* Honourable mention — pewter. */
.geo-award--honorable {
    border-color: rgba(18, 60, 99, .26);
    background: linear-gradient(180deg, #f7fbff, #dee9f4);
}
.geo-award--honorable .geo-award-meter { background: rgba(18, 60, 99, .13); }
.geo-award--honorable .geo-award-meter span { background: linear-gradient(90deg, #c3d0dd, #8fa4b8); }
.geo-award--honorable .geo-award-share { color: #4b5c6d; }

/* Participation — everyone receives it, so it sits quietest and nearest the
   panel it is printed on. */
.geo-award--participation {
    border-color: rgba(168, 117, 43, .3);
    background: linear-gradient(180deg, #fffaf2, #f7ead6);
}
.geo-award--participation .geo-award-meter { background: rgba(168, 117, 43, .16); }
.geo-award--participation .geo-award-meter span { background: linear-gradient(90deg, #e8cfa8, #c99a5f); }

@media (min-width: 880px) {
    /* Three across. Below this the cards would be too narrow for "will receive a
       Gold, Silver or Bronze Award Certificate" to break tidily, so they stack. */
    .geo-awards { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; padding-top: 14px; }
    /* The podium step. Only here: stacked, a raised card is just a gap. */
    .geo-award--gold { transform: translateY(-14px); }
}
@media (max-width: 879px) {
    /* Stacked, the flag hangs into the gap above the first card rather than into
       the panel's own padding, so the row needs its own clearance. */
    .geo-awards { padding-top: 14px; }
}


/* ---- Closing CTA ----------------------------------------------------------
   The last panel on the home page. Its wording is a full sentence, so it is set
   a step below the section titles and capped at ~46ch: three readable lines
   rather than four display-sized ones spanning the whole panel. The amber wash
   and the glowing CTA are what carry the weight here, not the type size. */

.geo-closing {
    text-align: center;
    background: linear-gradient(135deg, var(--brand-100) 0%, var(--water-soft) 100%);
    border-color: rgba(217, 141, 24, .25);
}
.geo-closing h2 {
    max-width: 46ch;
    margin: 0 auto;
    font-family: var(--font-display);
    font-weight: var(--fw-bold);
    font-size: clamp(1.3rem, 2.3vw, 1.8rem);
    line-height: 1.3;
    text-wrap: balance;
    color: var(--accent);
}
/* margin-inline auto because .gx-glass p inherits a 66ch measure — without it the
   button would centre inside that box rather than inside the panel. */
.geo-closing p { max-width: none; margin: clamp(20px, 3vw, 28px) auto 0; }
.geo-closing .btn { padding: 16px 34px; font-size: 1.05rem; }

/* ============================================================================
   CARTOGRAPHY — pins, meridians, compass roses and contours
   ----------------------------------------------------------------------------
   Everything in this section is BACKGROUND. Not one rule here adds a box to the
   flow, and not one of these figures can be reached by a pointer or a screen
   reader: they are either the fixed gutter layer at z-index -1 (a sibling plane
   to #space-bg), a background-image on the page backdrop, or a pseudo-element
   painted underneath the section's own content. Delete the whole block and the
   layout is byte-for-byte what it was.

   Why it exists. The MyGeo palette landed on the colours — orange, cream, earth
   brown — but every ornament on the site was still a circle, a ring or a
   gradient band inherited from the siblings. Nothing was shaped like geography.
   These are the shapes, and they are taken from the MAP rather than from the
   landscape: a map pin, a meridian, a compass rose, a contour line.

   That distinction is the whole design rule here, and it is worth keeping. The
   sibling Earth Science site drew the ground itself — leaves, branches, scree.
   Geography is the discipline that *represents* ground, so its furniture is the
   chart's: graticules, contours, waypoints, north arrows. Anything added later
   should come off a map, not out of a field.

   Six surfaces carry them, chosen so that every page gets some without any page
   template being touched:

       page backdrop   a whisper-faint pin-and-graticule scatter, tiled
       .geo-carto      meridians, compass roses and contour hills in the margins
       .page-hero      a graticule quadrant either side of every inner page hero
       .geo-hero       a contour line along the foot of the home page's relief
       .geo-head       a surveyed route rule under every section title
       .geo-band       a contour line along the foot of every tinted band
       .site-footer    graticule ticks along the top edge

   The artwork is held in custom properties so a tile used in two places (the
   contour line runs along both the home hero and every band) exists once.
   Property names are new, so none of the specificity traps documented at the top
   of this file apply — nothing else declares them at any weight.

   Encoding note: `#` is percent-escaped to %23 inside every data URI. It is the
   one character that MUST be, since a bare # ends the URL and starts a fragment
   — an un-escaped colour turns the whole tile into a 404. Angle brackets are
   left literal; they are legal inside a quoted url() and the file is served
   as-authored, with no minifier in the path to disagree.
   ========================================================================== */

:root {
    /* The section-title rule: a MAP SCALE BAR.

       This is the one ornament that repeats on every section of every page — a
       dozen times on the home page alone — so it is the piece that decides what
       the site's section rhythm feels like. The sibling Earth Science site put a
       twig with a leaf pair there and MyGeo inherited the shape of it.

       A scale bar stood here for a while — the mark no other kind of document
       has — and it was the right idea drawn too loud: filled and open cells with
       a tick over every join is a lot of figure to repeat under twenty-nine
       headings, and at that size it read as an object rather than a rule.

       This is the neatline instead: the fine border a chart is ruled with. A
       hairline, a full-height tick at each end, a half tick at the centre.
       Formal enough to sit under every title on the site and still off the map
       rather than out of a stationery set. 200x20 and used at ~200px, so every
       hairline lands on one physical pixel. */
    --geo-route-rule: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 20'><g fill='none' stroke='%23123c63' stroke-linecap='square'><path d='M10 12h180' stroke-width='1.2' opacity='.42'/><path d='M10 5v7M190 5v7' stroke-width='1.6' opacity='.7'/><path d='M100 7.5v4.5' stroke-width='1.4' opacity='.55'/></g></svg>");

    /* Contour line: nested arcs closing on low summits, tiled horizontally along
       the foot of a band. The seam is at x=0/220, where nothing sits.

       Two index lines and one intermediate per summit, the weights a contour map
       actually uses — heavier every fifth line, lighter between. Drawn as open
       arcs rising off the baseline rather than closed rings: at 22px tall a
       closed ring is a blob, and the eye reads the rise-and-fall of an arc as
       terrain at a size where it cannot read anything else. */
    --geo-contour-tile: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 22'><g fill='none' stroke='%235d6b7a' stroke-linecap='round'><path d='M2 22C10 8 26 2 40 2s30 6 38 20' stroke-width='1.7' opacity='.34'/><path d='M12 22C18 12 29 8 40 8s22 4 28 14' stroke-width='1.4' opacity='.24'/><path d='M22 22c4-5 11-8 18-8s14 3 18 8' stroke-width='1.2' opacity='.18'/><path d='M92 22c6-9 17-14 27-14s21 5 27 14' stroke-width='1.7' opacity='.3'/><path d='M102 22c4-6 12-9 17-9s13 3 17 9' stroke-width='1.3' opacity='.2'/><path d='M160 22c5-8 14-12 22-12s17 4 22 12' stroke-width='1.6' opacity='.28'/><path d='M170 22c3-5 9-7 12-7s9 2 12 7' stroke-width='1.2' opacity='.18'/></g></svg>");

    /* The hero graticule: a quadrant of meridians and parallels with a waypoint
       pin on the outermost crossing. Drawn opening up and to the right; the
       left-hand copy is mirrored in CSS rather than redrawn. */
    --geo-graticule-art: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 80'><g fill='none' stroke='%231d5a94' stroke-width='2' stroke-linecap='round' opacity='.7'><path d='M4 78C22 62 40 44 58 26 68 16 78 9 96 3'/><path d='M14 78C28 60 44 42 62 26'/><path d='M30 78C42 62 56 48 72 34'/></g><g fill='none' stroke='%23123c63' stroke-width='1.7' stroke-linecap='round' opacity='.5' stroke-dasharray='6 5'><path d='M2 58C24 54 46 44 64 30'/><path d='M10 72C32 68 54 58 74 44'/></g><g fill='%23123c63' opacity='.7'><path d='M0 0c0 0-9-11.6-9-17a9 9 0 0 1 18 0c0 5.4-9 17-9 17z' transform='translate(62 30) scale(.6)'/></g><circle cx='96' cy='3' r='2.6' fill='%23ff980d' opacity='.85'/></svg>");

    /* Graticule ticks along the footer's top edge — a map's neatline, where the
       border is ruled off at regular intervals and every fifth tick runs long.
       The dark panel then reads as the chart the page is drawn inside.

       Earth brown, NOT the footer's own pale ink. The ticks are rooted on the
       footer's top border but rise out of it, so every pixel of them is drawn on
       the cream section above — in --footer-muted (#9db9d6, chosen for dark
       ground) they came out as faint scratches on nothing. */
    --geo-tick-tile: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 18'><g fill='none' stroke='%23123c63' stroke-width='1.8' stroke-linecap='round' opacity='.34'><path d='M8 18v-5M24 18v-5M40 18v-5M56 18v-5M72 18v-5M88 18v-5M104 18v-5M120 18v-5M136 18v-5M152 18v-5'/><path d='M8 18V6M88 18V6' stroke-width='2.1'/></g></svg>");

    /* The backdrop scatter: two pins, a contour ring and three graticule crosses
       on a 440px tile. Deliberately sparse and almost transparent — at ~.055
       alpha it registers as texture in the paper, not as a pattern anyone is
       meant to read. This is the layer that puts the motif on EVERY pixel.

       440, not the 320 it was first drawn at. The shapes are the right size at
       320 but the repeat put ~50 of them on a 1600x1100 screen, and geo-bg.js is
       already drawing contours and pins on the canvas above this. Widening the
       tile without touching a coordinate keeps every figure exactly the size it
       was and drops the count by ~45%. If this ever needs thinning again, grow
       the viewBox and the background-size together — never scale the shapes, or
       the marks start competing with the ones in the margins. */
    --geo-ground-tex: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 440 440'><g fill='%23123c63' opacity='.055'><path d='M0 0c0 0-9-11.6-9-17a9 9 0 0 1 18 0c0 5.4-9 17-9 17z' transform='translate(58 104) scale(1.5)'/><path d='M0 0c0 0-9-11.6-9-17a9 9 0 0 1 18 0c0 5.4-9 17-9 17z' transform='translate(330 372) scale(1.2)'/></g><g fill='none' stroke='%231d5a94' stroke-width='2.2' stroke-linecap='round' opacity='.05'><path d='M368 150c-22 0-40-7-40-16s18-16 40-16 40 7 40 16-18 16-40 16Z'/><path d='M368 142c-14 0-25-4-25-9s11-9 25-9 25 4 25 9-11 9-25 9Z'/></g><g fill='none' stroke='%235d6b7a' stroke-width='2' stroke-linecap='round' opacity='.05'><path d='M158 290h14M165 283v14M258 66h14M265 59v14M98 344h14M105 337v14'/></g></svg>");

    /* ---- Light-ink set, for the deep workshop band --------------------------
       Same three figures, redrawn in cream instead of earth brown. A recolour is
       genuinely needed rather than a filter or an opacity tweak: the dark set is
       #123c63 on cream, and #123c63 on a #123c63 band is nothing at all. The
       band is the one big opaque surface on the site, so the page-wide backdrop
       scatter cannot show through it either — without these it is the only
       section with no motif on it, which is what prompted them.

       Alphas run higher than the light-ground originals (.05 -> .09ish). Pale
       ink on dark ground reads weaker at the same opacity, and the band is a
       large unbroken field where too little just looks like a rendering
       artefact. */
    --geo-ground-tex-light: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 440 440'><g fill='%23fbe6c8' opacity='.09'><path d='M0 0c0 0-9-11.6-9-17a9 9 0 0 1 18 0c0 5.4-9 17-9 17z' transform='translate(58 104) scale(1.5)'/><path d='M0 0c0 0-9-11.6-9-17a9 9 0 0 1 18 0c0 5.4-9 17-9 17z' transform='translate(330 372) scale(1.2)'/></g><g fill='none' stroke='%23ff980d' stroke-width='2.2' stroke-linecap='round' opacity='.10'><path d='M368 150c-22 0-40-7-40-16s18-16 40-16 40 7 40 16-18 16-40 16Z'/><path d='M368 142c-14 0-25-4-25-9s11-9 25-9 25 4 25 9-11 9-25 9Z'/></g><g fill='none' stroke='%237fb2f0' stroke-width='2' stroke-linecap='round' opacity='.09'><path d='M158 290h14M165 283v14M258 66h14M265 59v14M98 344h14M105 337v14'/></g></svg>");

    /* A compass rose rising out of the band's bottom edge — the same figure the
       side margins use, cropped by the band's own box. Placed as a background
       layer rather than a pseudo-element because the band's ::before is the
       relief seam and its ::after is the contour line, and backgrounds clip to
       the border box for free so nothing can spill past the section. */
    --geo-rose-light: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 124 124'><g fill='none' stroke='%23fbe6c8' stroke-width='2.2' opacity='.12'><circle cx='62' cy='62' r='42'/><circle cx='62' cy='62' r='33'/></g><g fill='%23fbe6c8' opacity='.10'><path d='M62 62 83 41 76 62Zm0 0 21 21-21-7Zm0 0-21 21 7-21Zm0 0-21-21 21 7Z'/></g><g fill='%23fbe6c8' opacity='.14'><path d='M62 62 62 14l7 48Zm0 0 0 48 7-48Z'/><path d='M62 62 14 62l48 7Zm0 0 48 0-48 7Z' opacity='.7'/></g></svg>");

    /* The same rose in earth brown, for the orange .geo-band--deep on light
       ground. Kept as its own token rather than filtering the light one: these
       two are the same drawing in the site's two inks, and a filter chain on a
       background image cannot be scoped to one layer of a stack anyway. */
    --geo-rose-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 124 124'><g fill='none' stroke='%231d5a94' stroke-width='2.2' opacity='.12'><circle cx='62' cy='62' r='42'/><circle cx='62' cy='62' r='33'/></g><g fill='%23123c63' opacity='.09'><path d='M62 62 83 41 76 62Zm0 0 21 21-21-7Zm0 0-21 21 7-21Zm0 0-21-21 21 7Z'/></g><g fill='%23123c63' opacity='.12'><path d='M62 62 62 14l7 48Zm0 0 0 48 7-48Z'/><path d='M62 62 14 62l48 7Zm0 0 48 0-48 7Z' opacity='.7'/></g></svg>");

    /* Contour line for the band's foot, in pale ink rather than the warm grey
       the light-ground bands use. Same arcs, same 220x22 tile. */
    --geo-contour-tile-light: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 22'><g fill='none' stroke='%23fbe6c8' stroke-linecap='round'><path d='M2 22C10 8 26 2 40 2s30 6 38 20' stroke-width='1.7' opacity='.30'/><path d='M12 22C18 12 29 8 40 8s22 4 28 14' stroke-width='1.4' opacity='.22'/><path d='M22 22c4-5 11-8 18-8s14 3 18 8' stroke-width='1.2' opacity='.16'/><path d='M92 22c6-9 17-14 27-14s21 5 27 14' stroke-width='1.7' opacity='.26'/><path d='M102 22c4-6 12-9 17-9s13 3 17 9' stroke-width='1.3' opacity='.18'/><path d='M160 22c5-8 14-12 22-12s17 4 22 12' stroke-width='1.6' opacity='.24'/><path d='M170 22c3-5 9-7 12-7s9 2 12 7' stroke-width='1.2' opacity='.16'/></g></svg>");
}

/* ---- The page backdrop -----------------------------------------------------
   body::before is the site's ambient wash: fixed, inset 0, z-index -3, four
   radial blooms over an opaque cream base (see the block near the top of this
   file). The scatter is prepended to that stack, so it lies ON the wash and
   UNDER everything else on the page, and inherits the wash's 40s drift for free.

   The whole existing stack is restated rather than layered on with a second
   rule: setting background-image alone would win the image list but leave the
   inherited `background` shorthand's other components in place, and the two
   would then disagree about how many layers there are — exactly the shorthand
   trap this file warns about further up. One declaration, one source of truth.
   Keep this in step with the [data-theme="warm"] body::before block above. */
[data-theme="warm"] body::before {
    background:
        var(--geo-ground-tex),
        radial-gradient(900px 700px at 84% -8%, rgba(251, 230, 200, .55), transparent 62%),
        radial-gradient(780px 640px at  2% 22%, rgba(246, 207, 158, .34), transparent 60%),
        radial-gradient(860px 660px at 98% 56%, rgba(255, 152, 13, .16), transparent 62%),
        radial-gradient(900px 720px at 10% 88%, rgba(127, 178, 240, .12), transparent 62%),
        linear-gradient(180deg, #fffaf1 0%, #faf1dd 48%, #f4e7d0 100%);
    /* the scatter tiles; every gradient under it stays a single sheet */
    background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
    background-size: 440px 440px, auto, auto, auto, auto, auto;
}


/* ---- /igeo: the backdrop figure becomes a globe graticule -----------------
   Every other page scatters map pins and grid crosses behind it. This one is
   about the INTERNATIONAL olympiad, so its backdrop is the thing an
   international competition is drawn on: a globe with its parallels and
   meridians, plus a couple of loose parallels so the tile is not two objects
   on an empty field.

   Only the token is overridden. body::before already resolves it through a
   var(), so restating that background stack here would mean maintaining two
   copies of a six-layer shorthand — the exact trap the note on that rule
   warns about. One custom property, one source of truth, and the wash, the
   drift and the tile size all carry over untouched.

   The warm parchment is deliberately NOT changed: it is what keeps this site
   clear of MyAO at a glance, so /igeo differs by motif, not by palette. */
.page-igeo,
.page-olympiad-moments,
.page-olympiad-journey-2024 {
    --geo-ground-tex: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 440 440'><g fill='none' stroke='%231d5a94' stroke-width='2' opacity='.13'><circle cx='118' cy='122' r='78'/><path d='M40 122 h156'/><path d='M50.45 161 a 67.55 12.48 0 0 0 135.1 0'/><path d='M50.45 83 a 67.55 12.48 0 0 0 135.1 0'/><ellipse cx='118' cy='122' rx='32.76' ry='78'/><ellipse cx='118' cy='122' rx='62.4' ry='78'/><path d='M118 44 v156'/></g><g fill='none' stroke='%23123c63' stroke-width='1.9' opacity='.10'><circle cx='330' cy='330' r='54'/><path d='M276 330 h108'/><path d='M283.23 357 a 46.77 8.64 0 0 0 93.54 0'/><path d='M283.23 303 a 46.77 8.64 0 0 0 93.54 0'/><ellipse cx='330' cy='330' rx='22.68' ry='54'/><ellipse cx='330' cy='330' rx='43.2' ry='54'/><path d='M330 276 v108'/></g><g fill='none' stroke='%235d6b7a' stroke-width='1.8' opacity='.08'><path d='M296 66 a 130 50 0 0 1 140 0'/><path d='M6 336 a 130 50 0 0 0 130 9'/></g></svg>");
}


/* ---- The international pages: a cooler ground --------------------------------
   /igeo and /olympiad-moments share this; every other page keeps the warm
   parchment. The blue is not "this page is special", it is a category: the
   national competition is on paper, the international olympiad is on water.

   Three pages, one block, on purpose. Giving any of them a colour of its own
   would make the ground mean "which page you are on" rather than "which half of
   the site you are in", and at twelve pages that is decoration nobody can learn.
   All three are the same subject — the same teams, the same years, the same
   #igeo2025 anchors, and the journey pages link across to /igeo for the fuller
   write-ups — so they share one ground.

   A swap of figure alone was tried first and rejected: the backdrop scatter
   became a globe graticule and nobody could see it, because a texture drawn at
   5% opacity behind body text cannot carry a page's identity no matter what it
   depicts. Colour is what the eye reads at a glance; ornament is what it finds
   after it arrives. The globe stays, on top of this.

   THE RISK, recorded rather than hidden: the README names warm parchment as
   what keeps MyGeo clear of MyAO, which is blue. This is kept pale and keeps
   every orange accent — the chips, the gradient year, the CTA — so it reads as
   the same brand on different paper, not as the sibling site. If MyGeo and MyAO
   ever end up side by side and this page is the one that confuses them, this is
   the block to revert.

   Both layers have to move together. The body paints the ground and body::before
   paints the sheet over it, each with its own gradient stack, and changing one
   alone leaves a warm sheet over a cool ground. The six-layer shorthand is
   restated in full for the same reason the note upstairs gives: setting
   background-image alone would win the image list and leave the inherited
   repeat/size lists disagreeing about how many layers exist. */
[data-theme="warm"] body.page-igeo,
[data-theme="warm"] body.page-olympiad-moments,
[data-theme="warm"] body.page-olympiad-journey-2024 {
    background:
        radial-gradient(1200px 600px at 15% -10%, #dbe9fb 0%, rgba(219, 233, 251, 0) 60%),
        radial-gradient(900px 500px at 95% 5%, #eaf2fd 0%, rgba(234, 242, 253, 0) 55%),
        linear-gradient(180deg, #f8fbff 0%, #e9f0fa 100%);
}
[data-theme="warm"] body.page-igeo::before,
[data-theme="warm"] body.page-olympiad-moments::before,
[data-theme="warm"] body.page-olympiad-journey-2024::before {
    background:
        var(--geo-ground-tex),
        radial-gradient(900px 700px at 84% -8%, rgba(127, 178, 240, .22), transparent 62%),
        radial-gradient(780px 640px at  2% 22%, rgba(10, 82, 153, .10), transparent 60%),
        /* one warm radial kept on purpose: with none of them the page turns
           clinical and stops reading as this site */
        radial-gradient(860px 660px at 98% 56%, rgba(255, 152, 13, .10), transparent 62%),
        radial-gradient(900px 720px at 10% 88%, rgba(127, 178, 240, .16), transparent 62%),
        linear-gradient(180deg, #f9fcff 0%, #eef4fc 48%, #e4edf8 100%);
    background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat;
    background-size: 440px 440px, auto, auto, auto, auto, auto;
}

/* ---- The gutter layer ------------------------------------------------------
   Fixed, so the margins keep their chart furniture while the page scrolls past.
   Same plane as #space-bg (z-index -1) and placed after it in the layout, so the
   meridians sit over the contour canvas and under every section.

   --gut is the width of one empty margin. Resolving `100%` against a fixed
   inset:0 box gives the viewport's *client* width, which excludes the scrollbar
   — `100vw` includes it, and would have pushed every piece ~8px inward on
   Windows. Percentages inside a custom property resolve at use, and both
   properties it is used in (width, and left/right) resolve against that box. */
.geo-carto {
    --gut: calc((100% - var(--container)) / 2);
    position: fixed;
    inset: 0;
    z-index: -1;
    overflow: hidden;
    pointer-events: none;
    /* Below the breakpoint further down there is no margin to plant in, and a
       meridian would land on the text. The in-page motifs carry the theme there. */
    display: none;
}
/* <defs> renders nothing, but an inline <svg> is still a replaced element and
   would take a line box in the flow if left to itself. */
.geo-carto-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.geo-carto-piece {
    position: absolute;
    /* never wider than the margin it stands in, less a 14px kerb, and never so
       wide on an ultrawide monitor that it becomes the subject of the page */
    width: min(calc(var(--gut) - 14px), 118px);
    height: auto;
    transform-origin: center center;
}
/* Two inks: ruled lines — meridians, roses — take the brand brown, and ground
   drawn from above takes the muted earth grey the body copy uses. Both sit far
   enough back that they never read as content. */
.geo-carto--line { color: #123c63; opacity: .15; }
.geo-carto--land { color: #5d6b7a; opacity: .19; }

/* Left margin. The meridian runs past the foot of the viewport so it reads as a
   line continuing off the chart rather than as an object standing on the edge. */
.f-meridian-l { left: 8px;  bottom: -26px; }
.f-compass-l  { left: 12px; top: 15%; }
.f-contour-l  { left: 4px;  top: 58%; }

/* Right margin. `scale` — the independent property, not a transform function —
   is what mirrors these: the drift animation below owns `transform` outright,
   and a flip written there would be wiped the moment the animation started. The
   two compose, in the order translate, rotate, scale, then transform. The
   meridian is turned twice (-1 -1) so it runs down from the top edge. */
.f-meridian-r { right: 10px; top: -30px; scale: -1 -1; }
.f-contour-r  { right: 6px;  top: 42%;   scale: -1 1; }
.f-compass-r  { right: 12px; bottom: 7%; scale: -1 1; }

/* Contours do not move — ground does not drift, and a wandering contour line
   would read as an error rather than as decoration. Only the ruled figures
   breathe, each on its own phase so the two margins never move in step.

   A slow rotation rather than the siblings' sway: these are instruments, and a
   compass rose settling onto north is the motion the figure already implies. */
.geo-carto--line { animation: geo-drift 14s ease-in-out infinite; }
.f-compass-l  { animation-duration: 11s; animation-delay: -3s; }
.f-meridian-r { animation-duration: 16s; animation-delay: -6s; }
.f-compass-r  { animation-duration: 12s; animation-delay: -1.5s; }
@keyframes geo-drift {
    0%, 100% { transform: rotate(-1.4deg); }
    50%      { transform: rotate(1.4deg); }
}

/* 1360px is where one margin first reaches ~90px — enough for a compass rose to
   be a compass rose rather than a smudge. Below it the layer is off entirely, so
   phones never composite any of it. */
@media (min-width: 1360px) {
    .geo-carto { display: block; }
}

/* ---- Hero graticules -------------------------------------------------------
   Every inner page opens with `section.container.page-hero`, so one pair of
   pseudo-elements here reaches About, Contact, FAQs, Registration, Reading
   Materials, Past-Year Questions, both results pages and the 404.

   They are pinned to the container's outer edges. The hero's text is centred and
   measure-capped well inside that, so the graticules sit over the 24px padding
   and the slack beside it — they cannot reach the title at any width where they
   are visible. */
.page-hero::before,
.page-hero::after {
    content: "";
    position: absolute;
    top: 0;
    z-index: 0;              /* under the hero's text, which follows in the flow */
    width: clamp(72px, 8vw, 116px);
    aspect-ratio: 100 / 80;
    background: var(--geo-graticule-art) center / contain no-repeat;
    opacity: .5;
    pointer-events: none;
}
.page-hero::before { left: 2px; scale: -1 1; }   /* mirrored, so the pair opens outward */
.page-hero::after  { right: 2px; }
/* Under ~1000px the hero's copy starts to use the full container and the
   graticules would crowd it. They go, and the section divider carries the
   motif. */
@media (max-width: 1000px) {
    .page-hero::before, .page-hero::after { display: none; }
}

/* ---- Hero ground -----------------------------------------------------------
   The home page has no .page-hero — it opens on .geo-hero, four hypsometric
   relief bands. Graticules in its corners would fight the globe artwork, so it
   gets the contour line instead: the survey drawn over the elevation tints,
   which is exactly what a relief map layers on top of them. .geo-hero already
   clips its overflow and isolates its stacking context, so this stays inside
   both. */
.geo-hero::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 30px;
    background: var(--geo-contour-tile) bottom center / 300px 30px repeat-x;
    opacity: .55;
    pointer-events: none;
}

/* ---- Section divider -------------------------------------------------------
   .geo-head is the eyebrow-over-title block above every section on every page,
   and it is a column flex container — so an ::after simply becomes its last
   item, centred, with no positioning needed.

   This is the one motif that repeats often enough to set a rhythm: on the home
   page it marks all twelve section boundaries, which is precisely the "one
   texture repeated a dozen times" problem the SECTION RHYTHM block above was
   written to break up. */
.geo-head::after {
    content: "";
    width: min(200px, 64%);
    height: 20px;              /* the tile's own 200x20, so `contain` scales 1:1 */
    margin-top: 10px;
    background: var(--geo-route-rule) center / contain no-repeat;
}
/* The scale bar is a measuring instrument, so it must not be the widest thing
   under a short heading — at min(200px, 64%) it sits inside the title above it
   at every width, which is what stops it reading as an underline. */
/* The preparatory-workshop section runs on a dark panel — the same bar in the
   same ink would vanish into it. Flattened to white and dropped back, which
   reads as a scale printed in chalk on dark ground. */
.geo-workshop .geo-head::after {
    filter: brightness(0) invert(1);
    opacity: .45;
}

/* ---- Band contour ----------------------------------------------------------
   A tinted band already carries a relief seam at its top edge (.geo-band::before
   above). This closes the other edge with the contour line the relief is
   surveyed by. It lives in the band's bottom padding — clamp(32px, 5vw, 64px) —
   so there is always room for 22px of contour under the last element.

   `.section > .container` is z-index 1, so a band's content already sits above
   anything the band itself paints. */
.geo-band::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 22px;
    background: var(--geo-contour-tile) bottom center / 220px 22px repeat-x;
    opacity: .75;
    pointer-events: none;
}

/* ---- Footer edge -----------------------------------------------------------
   Graticule ticks along the top of the footer — the neatline the chart is ruled
   off at. Offset by its own height so the ticks rise OUT of the dark panel into
   the section above rather than standing inside it — which needs the footer to
   be a positioning context, and redesign.css leaves it static. The 18px it
   borrows falls in the preceding section's bottom padding, and that section's
   .container is z-index 1, so any content there still wins. */
.site-footer { position: relative; }
.site-footer::before {
    content: "";
    position: absolute;
    inset-inline: 0;
    top: -18px;
    height: 18px;
    background: var(--geo-tick-tile) bottom center / 160px 18px repeat-x;
    pointer-events: none;
}

/* The drift is decoration and carries nothing; the figures themselves stay. */
@media (prefers-reduced-motion: reduce) {
    .geo-carto-piece { animation: none; }
}

/* ---- Reading materials: the shelf -----------------------------------------
   A cover, the title and its subject, and a chevron — replacing the row of
   line-icon-plus-title this page shipped with. The live MyGeo page presents
   these as book jackets and it is right to: four references whose titles all
   begin "Geography" are told apart by their covers long before their text.

   Grid rather than flex on the row, because the three parts have different
   jobs: the cover is a fixed 72px column, the meta takes whatever is left, and
   the chevron is pinned to the end. A flex row with margin-auto on the chevron
   does the same thing until the title wraps, at which point the chevron drifts.

   The cover box is fixed-size with object-fit: contain, NOT cover. These are
   jackets of different aspect ratios — a tall paperback next to a square icon —
   and cropping them to a common box would cut the titles off the artwork.
   Contain letterboxes instead, which is the correct trade for a book. */
.book-list { display: grid; gap: 12px; }
.book-row {
    display: grid;
    grid-template-columns: 72px minmax(0, 1fr) auto;
    align-items: center;
    gap: 16px;
    padding: 12px 16px;
    border: 1px solid var(--border);
    border-radius: 14px;
    background: var(--surface);
    color: inherit;
    text-decoration: none;
    transition: border-color .2s, box-shadow .2s, transform .2s;
}
.book-row:hover {
    border-color: var(--border-strong);
    box-shadow: var(--shadow-sm);
    transform: translateY(-2px);
}
.book-cover {
    display: flex; align-items: center; justify-content: center;
    width: 72px; height: 88px;
    border-radius: 8px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    overflow: hidden;
}
.book-cover img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
.book-cover--icon { color: var(--accent); }
.book-meta { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.book-meta b { font-size: 1.02rem; line-height: 1.35; }
.book-sub { font-size: .85rem; color: var(--text-mute); }
.book-go { color: var(--accent); flex: none; transition: transform .2s; }
.book-row:hover .book-go { transform: translateX(3px); }

@media (max-width: 520px) {
    /* the chevron is decoration and the whole row is the target, so it goes
       first when width is short — the cover and the title are the content */
    .book-row { grid-template-columns: 56px minmax(0, 1fr); gap: 12px; padding: 10px 12px; }
    .book-cover { width: 56px; height: 70px; }
    .book-go { display: none; }
}

/* The iGeo mark above the About page's iGeo section. A plate, like the lockup
   at the top of the page: the logo is drawn for white stock and reads as a
   smudge straight onto the parchment. Floated right of the prose on a wide
   screen so the paragraph still starts at the top of the panel, and centred
   above it when there is no room to sit beside. */
.igeo-mark {
    float: right;
    margin: 0 0 18px 26px;
    padding: 16px 20px;
    width: min(260px, 34%);
    display: flex; align-items: center; justify-content: center;
    background: #fff;
    border: 1px solid var(--border);
    border-radius: 16px;
    box-shadow: var(--shadow-sm);
}
.igeo-mark img { display: block; width: 100%; height: auto; }
@media (max-width: 700px) {
    /* float + a 92% width is just a centred block with extra steps */
    .igeo-mark { float: none; width: min(260px, 70vw); margin: 0 auto 20px; }
}

/* ---- The same mark over a journey-page hero -------------------------------
   /olympiad-moments and /2024-igeo-myteam open the way the live …-igeo-myteam
   pages do: the iGeo mark, then the year and host city as the page title. On a
   page that is one year of one olympiad, the mark says which olympiad before a
   word is read.

   No plate around it, unlike .igeo-mark above: that one floats inside a white
   card and needs an edge to sit on, while the hero ground is already the cream
   the artwork is drawn against — a box here would read as a card that had lost
   its content.

   Not .badge-mini either. That is 88px under a green drop-shadow, which suits
   the round competition badges on the two results pages; this is a 1.8:1
   wordmark that carries its own shadow in the file. */

/* ---- The year block when it IS the page, not a section in one ---------------
   In a hero it sits under a mark and an eyebrow and over a lead, so it needs
   different spacing from the same block inside a card: the title has to clear
   its own chip, and the watermark has to stay behind the title rather than
   reaching down into the chip and the rule beneath it. */
.page-hero .geo-year-head { padding-top: 0; margin-bottom: 0; }
.page-hero .geo-year-head .section-title { margin-bottom: .34em; }
/* No rule under a hero. The neatline closes a section HEADING — it belongs
   over the content it introduces. Here it fell between the host chip and the
   opening paragraph and divided two things that were never joined. */
.page-hero .geo-year-head::after { content: none; }
/* smaller and higher than the in-card watermark: the block below it is three
   short centred things, and a numeral sized for a card ran straight through
   them */
.page-hero .geo-year-head::before {
    top: 38%;
    font-size: clamp(4.2rem, 8.5vw, 7.5rem);
}
/* the lead is the reading matter; give it air from the furniture above */
.page-hero .geo-year-head + .lead { margin-top: 22px; }

/* The iGeo mark on a journey page. Bare — no plate, no padding: the artwork
   and its webp derivatives are transparent, so it takes whatever ground the
   page has. It sits under the eyebrow, which is why it needs no space above.

   232px rather than the 300 it had: over an eyebrow, a title, a chip and a
   rule, the hero was five stacked objects and the logo was the largest. */
/* Bigger than it was, because it now carries the word. The heading beneath it
   shows the year alone — the mark is what says iGeo, and printing both put the
   same word on the page twice, drawn and typed, one above the other. */
.page-hero .hero-mark { width: min(320px, 68vw); margin: 0 auto -6px; }
.page-hero .hero-mark img { display: block; width: 100%; height: auto; }

/* In the hero the year head is the page title, so it takes the hero's spacing
   rather than a section head's: .geo-head's 26–44px bottom margin is sized to
   hold a section off the cards beneath it, and above a lead it was just a hole.
   .page-hero .lead supplies the 12px that belongs there. */
.page-hero .geo-head { margin-bottom: 0; }

/* ---- The closed-registration chip -----------------------------------------
   Matched to the sibling sites: a lock, Title Case, and a SOLID border.

   redesign.css gives this a dashed border, which on a pill the size of a button
   reads as a dropzone — the one thing it is not. It is a statement, not an
   invitation, and a solid hairline says that without shouting.

   Centred when it stands alone in its block. The workshop's is the one that
   spends most of the year closed, and left-aligned under a centred "How to
   register" list it read as a button someone had forgotten to finish. The
   :has() rule catches every other lone chip — the hero and header keep their
   flex rows, where text-align does nothing, so nothing there moves. */
.btn-closed {
    border-style: solid;
    border-color: var(--border-strong);
    background: var(--surface-2);
    color: var(--text-soft);
    letter-spacing: .01em;
}
.btn-closed .gx-icon { color: currentColor; opacity: .85; }

.geo-wk-cta { text-align: center; }
p:has(> .btn-closed:only-child) { text-align: center; }

/* NO inverted variant inside .geo-workshop. The band behind that section is dark,
   but the chip does not sit on the band — it sits inside .geo-wk-block, which is
   background: #fff. An earlier version inverted it here on the assumption that
   "dark section" meant "dark ground", and painted white text on a white card:
   the chip rendered as pale ghost text with no pill and no border at all.

   If a chip ever does need to sit on the band itself, scope it to that element
   rather than to the section. */

/* The venue's sub-note — the Zoom-link aside. Small and italic so the Venue
   column stays level with Date and Time beside it: as a second full-size line it
   made that card twice the height of its neighbours and the row of three read as
   one long fact and two short ones.

   Selector matched to the rule it has to beat. `[data-theme="warm"]
   .geo-workshop .gx-glass .geo-wk-fact p` above is (0,4,1) and sets margin: 0
   and font-weight: 600 on every <p> in these cards, so a bare .geo-wk-fact-note
   at (0,1,0) lost both — the note sat flush against the venue line at full
   weight, which is most of what made it read as a second line rather than an
   aside. The weight is dropped back here too, for the same reason. */
[data-theme="warm"] .geo-workshop .gx-glass .geo-wk-fact .geo-wk-fact-note {
    margin: 5px 0 0;
    font-size: .84rem;
    font-style: italic;
    font-weight: 500;
    line-height: 1.45;
    opacity: .85;
}

/* Olympiad Moments — the sub-headings inside a year, and the cross-link out to
   the full write-up on About. A year runs lead → camp → result, and without a
   marked break the three read as one long column of prose. */

/* ---- Camp photographs, as a strip -----------------------------------------
   The live …-igeo-myteam pages run these in a Swiper carousel showing three at
   a time. Ours was the full grid: 21 photographs stacked six rows deep between
   the training-camp card and the result, which pushed the result off the screen
   and made the year read as a photo album with some text attached.

   A scroll-snapping strip, not a carousel with JS. It swipes on touch, scrolls
   with the wheel, and takes arrow keys because it is focusable — which is
   everything the Swiper was doing, minus the library. The site had exactly this
   pattern before as .faces-strip; this is that idea back where it earns its
   keep, on the one gallery long enough to need it.

   proximity, not mandatory: mandatory re-snaps during a flick, so a fast swipe
   is dragged back to the nearest photo instead of coasting. */
.moment-strip {
    display: flex;
    gap: var(--moment-gap);
    overflow-x: auto;
    scroll-snap-type: x proximity;
    /* a swipe that runs past the end stays in the strip rather than turning into
       the browser's back gesture */
    overscroll-behavior-x: contain;
    margin: clamp(18px, 2.4vw, 26px) 0 0;
    padding-bottom: 12px;
    scrollbar-width: thin;
}
.moment-strip figure { margin: 0; flex: 0 0 auto; scroll-snap-align: start; }
.moment-strip img {
    display: block;
    width: var(--moment-photo);
    /* One ratio at every width: clamping width and height independently made the
       crop change between breakpoints on the same photograph. */
    aspect-ratio: 4 / 3;
    height: auto;
    object-fit: cover;
    border-radius: 12px;
    border: 1px solid var(--border);
}
.moment-strip figcaption {
    margin-top: 8px;
    max-width: var(--moment-photo);
    font-size: .85rem;
    color: var(--text-soft);
}
.moment-strip:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 4px;
    border-radius: 12px;
}

/* A part heading, sitting on the page between two cards rather than inside one —
   the shape the live …-igeo-myteam pages use, and the reason a year now reads as
   three parts instead of one wall of prose. Centred, because it labels the card
   under it and the year heading above is centred too.

   Larger than it was: at 1.08rem inside a panel it was a run-in label, and out
   here it has to hold its own between two cards. */
.moment-sub {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin: clamp(30px, 4vw, 46px) auto clamp(14px, 1.6vw, 18px);
    font-size: clamp(1.15rem, 1.9vw, 1.5rem);
    line-height: 1.2;
    text-align: center;
    color: #08243f;
}
.moment-sub .gx-icon { color: var(--accent); flex: none; }

/* ---- The card, sized to the column it holds --------------------------------
   .geo-justify caps the copy at the reading measure and centres it (see the long
   note beside that rule — the cap is deliberate and stays). In a card spanning
   the full container that left roughly 280px of empty panel either side of every
   paragraph at 1440px: the card was the width of the page while the text was the
   width of a book, so the panel read as a frame with nothing much in it.

   So the card moves instead of the measure. It stops where the column stops,
   plus its own padding, and the panel wraps the text rather than floating around
   it.

   The photograph strips come with it. They are not text and have no measure of
   their own, but they sat at the full container width and the cards no longer
   do — left alone they overhung the cards by ~230px a side, and the year read as
   two different column widths alternating down the page. One edge, top to
   bottom: card, strip, card, strip.

   WHERE THAT EDGE IS: three photographs and the two gaps between them, so a
   strip shows exactly three and cuts the fourth off at the frame rather than
   stranding a sliver of it. The card is set to the same figure, which is why the
   two share a variable instead of each carrying its own number — change
   --moment-photo or --moment-gap and both the strip and the card follow.

   This is NOT the reading measure, and the difference is deliberate. Three
   photographs come to 928px at 1440, against 674px for --measure at .prose's
   size, so the copy runs ~82ch where the rest of the site runs 66. The long note
   beside .geo-justify explains why 66 is the number, and everything it says
   still holds — the wider column here buys the one edge down the page, and it is
   a trade, not a correction. If the copy ever reads too loose, the fix is to
   narrow the photographs, not to re-cap the text: capping the text alone puts
   the gutters back inside the card, which is what this rule removed.

   .gx-pad's padding stays out of the sum on purpose: the card is set to the
   width of three photographs, so its outer edge and the strip's line up and the
   padding is spent inside that frame rather than added to it. */
.moment-card,
.moment-strip {
    --moment-photo: clamp(210px, 24vw, 300px);
    --moment-gap: 14px;
    max-width: calc(3 * var(--moment-photo) + 2 * var(--moment-gap));
    margin-inline: auto;
}
/* The copy fills it, which .geo-justify now does by itself — it no longer caps
   its own block, so there is nothing here to undo. This card is the reason that
   rule points at "narrow the panel, not the block": the panel is three
   photographs wide and the copy takes what it is given. */
/* Cards no longer butt against each other: each part is its own block, so the
   gap between them is what separates the passages. */
.moment-card + .moment-sub { margin-top: clamp(30px, 4vw, 46px); }
.moment-more { margin: 14px 0 0; font-size: .94rem; }
.moment-more a { font-weight: 600; }

/* Ragged right on a phone.
   ---------------------------------------------------------------------------
   .geo-justify sets justified text at every width, and the note beside that
   rule names ragged-right as the fallback "if the spacing is ever too loose".
   On a 390px screen it is: the panel measure drops to ~340px, roughly 40
   characters, and with `hyphens: none` the only place the browser can take up
   slack is the word gaps. A one-line paragraph is the worst of it — "Get ready
   for an epic geography showdown!" stretched the full width with gaps wide
   enough to read as columns.

   This does NOT contradict the decision documented on .geo-intro-lead, which
   deliberately justifies at all widths. That block is the reverse case: it
   shares the Introduction panel with the objectives list above 900px and takes
   the full container below it, so its measure is WIDEST on a phone. Here the
   measure is narrowest on a phone, so the same reasoning lands the other way.

   640px, not 900: at 640 the measure is still ~590px and justifies cleanly.
   The break is where the spacing goes, not where the layout does. */
@media (max-width: 640px) {
    .geo-justify p,
    .geo-justify li {
        text-align: left;
    }
}
