/* ============================================================================
   MyEO palette — THE RISING INDEX
   ----------------------------------------------------------------------------
   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 MyEO's own identity.

   THE IDENTITY: the competition's own mark, read as a page. The MyEO logo is
   three colours and nothing else — navy for the letters, gold for the bars, a
   sky-blue trendline cutting up through them — so the palette is those three
   rather than a fourth thing invented alongside them. Navy ink on a cool ice
   ground, gold for everything that asks to be acted on, sky for the analytical
   half of the subject. All six Explorer sites share this layout engine, so the
   palette is the main thing that tells them apart, and this one is chosen to
   sit clear of all five 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
       MyCLO  plum and rose, on lilac paper
       MyEO   plum and gold, on the badge's violet   <- here

   TWO SIBLINGS TO KEEP A DISTANCE FROM, since the ground went violet.

   MyCLO is "plum and rose, on lilac paper". Three things hold them apart and
   all three are load-bearing: this violet is the badge's magenta-violet rather
   than a plum-and-rose pair; the ink over it is the plum with GOLD CTAs, never
   rose; and the ornament layer here is the ledger and the plot. Change any of
   those three and the two sites start to converge.

   MyGeo is also dark-inked, and gold and orange are neighbours. The GROUND was
   already what held them apart — MyGeo's paper is warm because an atlas is
   printed on warm stock — and the violet widens that gap rather than narrowing
   it. The THIRD COLOUR is the second guard: the sky-blue trendline has no
   counterpart on MyGeo, and it is what carries every rising line, chart rule
   and stage marker on this site.

       #5c1c58  Explorer plum     headings, links, hairlines  11.9:1 on white
       #7a2a76  plum mid          secondary links and rules    8.7:1
       #6a2168  Explorer violet   the badge plate             10.3:1
       #9a2183  Explorer magenta  the badge ring               7.2:1
       #45163f  deep plum ink     on gold and lilac fills     14.6:1
       #0f5578  market sky        the analytical side          8.1:1
       #8a5a06  gold ink          gold that has to be read     5.9:1
       #f7b72d  gold              primary accent / CTAs        1.8:1 — FILL ONLY
       #f4eef7  lilac paper       section washes
       #e2d6e8  lilac slate       chips and pale fills
       #2b0f28  deep plum         footer and dark grounds
       #c9a6d0  pale lilac        highlights

   Contrast is load-bearing here, not decoration. Gold is a FILL colour on this
   brand and never carries text — the same rule MyGeo's orange and MyBO's amber
   both have, for the same reason. Anything that needs to be read runs in the
   plum, or in the gold ink, which is a different colour with a different job.

   The primary CTA is the one place the two meet: gold fill with deep-plum text,
   at 9.8:1, which is how the badge itself reads and well clear of the floor.
   White on gold would be 1.8:1 and is never used.

   Run `php docs/contrast.php` after any palette edit.

   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 on the site this was
   forked from. Until that audit --brand computed to the inherited turquoise and
   every border with it, so the site's own palette only ever reached the
   components that set literal hex values.
   --------------------------------------------------------------------------- */
:root,
:root[data-theme="warm"] {
    /* brand ramp — gold through to the Explorer plum the site uses for headings */
    --brand: #f7b72d;
    --brand-100: #ecf0f9;
    --brand-200: #e3e9fa;
    --brand-300: #fbdd9e;
    --brand-400: #fbc757;
    --brand-500: #f7b72d;
    --brand-600: #e3931b;
    --brand-700: #7a2a76;
    --brand-800: #3a1136;
    --brand-900: #5c1c58;

    /* accents */
    --accent: #5c1c58;
    --accent-2: #0f5578;   /* market sky — the palette’s second accent, readable at 8.1:1 */
    --accent-3: #c9a6d0;

    /* THE EXPLORER PURPLE, and where it came from.
       ------------------------------------------------------------------
       Sampled off the Economics Explorer badge itself — the deer roundel in
       the header and the home hero. Quantised, the mark is #602060 and
       #902080 over #f0c060: a violet-magenta plate, a gold deer, and nothing
       else. The gold on it is within a shade of this site's own --brand-500,
       which is why the two sit together without either being retuned.

       The palette had no purple at all until now. It was built off the MyEO
       wordmark — navy letters, gold bars, a sky trendline — and the badge that
       names the whole competition was the one brand asset it did not read. The
       ground is now that badge's violet, and the ink stays navy.

       KEEPING CLEAR OF MyCLO. The sibling table in the README lists MyCLO as
       "plum and rose, on lilac paper", so purple is not a free colour in this
       family. Three things hold them apart and all three are load-bearing:
       this violet is the badge's magenta-violet rather than a plum; the ink
       over it stays Explorer plum with gold CTAs, not plum and rose; and the
       ornament layer is still the ledger and the plot. Change any of those
       three and the two sites start to converge. */
    --plum-900: #45163f;   /* deepest — ink on lilac fills */
    --plum-700: #6a2168;   /* the badge plate */
    --plum-500: #9a2183;   /* the badge magenta */
    --plum-200: #ddc9db;   /* lilac fill */
    --plum-050: #f4eef7;   /* lilac paper */

    /* Retargeted onto the violet ground: these are the pale fills behind chips,
       count pills and spec cards, and on ice they now read as a leftover blue. */
    --water: #e2d6e8;
    --water-soft: #f4eef7;

    /* Link ink. Gold is a FILL colour on this brand, never a text colour:
       --brand is 1.78:1 on white and --brand-600 only 2.48:1, so neither is
       within reach of body copy at any weight. Links and the active nav
       item run in the Explorer plum instead, which measures 11.92:1 and is the
       colour the palette opens on. */
    --link: #5c1c58;
    --link-hover: #3f1039;

    /* Inherited names that only warm-theme.css declares, retargeted here so no
       rule downstream can reach back into the turquoise ramp. */
    --teal: #f7b72d;                      /* accent hairlines, ring progress arc */
    --gold: #0f5578;                      /* kept name → market sky */
    --violet: #5c1c58;                    /* kept name → Explorer plum */
    --pink: #e3931b;
    --muted: #545b70;                     /* 6.8:1 on white, clear of the AA floor */
    --brand-glow: rgba(43, 15, 40, .14);  /* navy shadow, not a sky 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, #f7b72d 0%, #e3931b 100%);
    --grad-gold:  linear-gradient(135deg, #f7b72d, #0f5578);
    /* The tail was #5c1c58 (--accent) until it was measured against the text it
       paints. A gradient from a saturated warm gold to a dark COOL colour has to
       cross the neutral axis to get there, so the middle of the ramp lands on
       desaturated olive-grey. On a short accent the dark end never arrives and it
       looks fine; on a long one — "Explorer" in the 64px hero lockup — the last
       letters sit right in the crossing and read as mud rather than as a colour.
       The tail is #7a2a76 (--brand-700) now: same job, same brand, but analogous to
       the gold, so the ramp stays chromatic end to end and never passes through
       grey. The 3px rule on .eo-facts keeps the old cool sweep on purpose —
       a hairline reads as a colour sweep, not as text. */
    --grad-text:  linear-gradient(92deg, #e3931b 0%, #f7b72d 45%, #7a2a76 100%);
    --grad-title: linear-gradient(92deg, #e3931b 0%, #f7b72d 45%, #7a2a76 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
   MyEO palette it is deep plum with the gold 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, #2b0f28 0%, #1b0819 100%);
    --footer-text: #f4ecf3;
    --footer-muted: #cba9c7;
    --footer-border: rgba(255, 255, 255, .14);
    --footer-accent: #f7b72d;
}

/* Light theme — the Explorer badge's violet paper, not the ice one it opened on.
   `:root` prefix required; see the selector note above. */
:root[data-theme="warm"] {
    --bg: #faf8fc;
    --bg-2: #efe7f3;
    --surface: #ffffff;
    --surface-2: #f2edf6;
    --surface-3: #e6dcec;

    /* 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 violet wash so the
       backdrop is on-palette even before this file's own body rules apply. */
    --bg-950: #faf8fc;
    --bg-900: #ece2f0;
    --bg-850: #e0d2e6;

    --text: #1d1120;
    --text-soft: #4b3d4b;
    --text-mute: #5e4d5c;   /* 5.4:1 on white — the palest ink allowed to carry copy */

    --border: rgba(69, 22, 63, .14);
    --border-strong: rgba(69, 22, 63, .28);

}

/* =============================================================================
   THE SURFACE LANGUAGE — outlined, flat, cast on a hard plate
   =============================================================================
   Every card, panel, chip, tab and button on this site is built from the four
   tokens below. Change them here and the whole site changes; that is the point
   of them being here rather than repeated down the file.

   WHERE IT COMES FROM. The Economics Explorer badge — the deer roundel in the
   header, the mark that names the competition — is drawn in exactly one way:
   flat colour, a heavy dark outline, no gradient anywhere in it, and no glow.
   The interface it sat on was drawn the opposite way in every respect: 28px
   radii, pale gradient fills, translucent glass, and wide blurred drop shadows
   at very low opacity. The badge and the page it sits on now agree.

   THE FOUR RULES, and none of them is decoration:

     1. FLAT FILLS. A card is one colour. Every `linear-gradient` that was
        painting a surface has gone; the ones that remain are painting a MARK —
        the cohort strip's segments, the primary button, the gold flags — where
        the gradient is the thing being drawn rather than the paper under it.

     2. A VISIBLE OUTLINE. 2px, not 1px. A hairline reads as a seam between two
        similar tones; 2px reads as a drawn edge, which is what the badge has.
        Ordinary surfaces take --eo-edge; the ones that have to be found without
        reading take --eo-edge-ink, the full navy.

     3. A HARD CAST, NEVER A BLUR. `4px 4px 0` — an offset plate with no blur
        radius at all. This is the single biggest departure and the one that does
        most of the work: a blurred shadow says "floating above the page", and
        every interface drawn in the last five years says it. A hard cast says
        "printed, one layer over another", which is what a competition paper is.

     4. SMALL RADII. 6px on surfaces, 4px on the small ones. Pills stay pills —
        999px on chips and buttons — because a pill is a shape, not a rounded
        rectangle, and the badge has plenty of them.

   HOVER LIFTS OFF THE PLATE. The card moves up and left by 2px while its cast
   grows to 6px, so the gap between card and cast opens. Nothing fades, nothing
   blurs, nothing scales. Suspended under prefers-reduced-motion, where the cast
   still grows but the card stays put.

   THE CAST IS VIOLET, not black or navy. It is the ground's own colour at low
   alpha, so on the lilac page it reads as the card lifting off the paper rather
   than as a grey smear laid on top of it. */
:root[data-theme="warm"] {
    /* The one radius, and its small sibling. Overriding redesign.css's ramp
       (12 / 18 / 28) rather than dodging it, so every inherited component that
       reaches for --r-md follows without being listed here. */
    --r-sm: 4px;
    --r-md: 6px;
    --r-lg: 8px;

    --eo-r: 6px;
    --eo-r-sm: 4px;
    --eo-line: 2px;

    /* Outlines. .30 is the lightest an edge can be and still read as drawn
       rather than as a join; the ink is for the cards that carry the section. */
    --eo-edge: rgba(69, 22, 63, .42);
    --eo-edge-strong: rgba(69, 22, 63, .68);
    --eo-edge-ink: #5c1c58;

    /* The cast. Zero blur in every one of these — if a blur radius ever appears
       in one, the whole system stops being this system. */
    --eo-cast: rgba(69, 22, 63, .22);
    --eo-cast-2: rgba(69, 22, 63, .32);
    --eo-drop: 4px 4px 0 var(--eo-cast);
    --eo-drop-lift: 6px 6px 0 var(--eo-cast-2);
    --eo-drop-sm: 3px 3px 0 var(--eo-cast);
    --eo-drop-ink: 4px 4px 0 rgba(69, 22, 63, .22);

    /* redesign.css's blurred shadow ramp, retargeted so anything still reaching
       for --shadow or --shadow-md casts a plate instead of a cloud. */
    --shadow-sm: 3px 3px 0 var(--eo-cast);
    --shadow: 4px 4px 0 var(--eo-cast);
    --shadow-md: 4px 4px 0 var(--eo-cast);
    --shadow-lg: 6px 6px 0 var(--eo-cast-2);
}

/* ---- The panel ------------------------------------------------------------
   .gx-glass is the shared surface: every page's big panels are one. The name is
   now a lie — there is no glass left in it, no blur and no translucency — and it
   is kept because it is on seven templates and renaming it would be a diff with
   no reader. What it means here is "the site's panel". */
[data-theme="warm"] .gx-glass {
    background: var(--surface);
    border: var(--eo-line) solid var(--eo-edge);
    border-radius: var(--eo-r);
    box-shadow: var(--eo-drop);
    /* the inherited blur cost a compositor layer on every panel and, with an
       opaque fill above it, blurred nothing */
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
/* Panels do not lift. Only things a visitor can click do — a page whose every
   panel moves under the cursor reads as loose. */
[data-theme="warm"] .gx-glass:hover {
    border-color: var(--eo-edge);
    box-shadow: var(--eo-drop);
}
/* …except the ones that are themselves links. */
[data-theme="warm"] a.gx-glass:hover,
[data-theme="warm"] a.gx-glass:focus-visible {
    border-color: var(--eo-edge-strong);
    box-shadow: var(--eo-drop-lift);
    transform: translate(-2px, -2px);
}

/* ---- Buttons --------------------------------------------------------------
   Same construction as a card, at pill radius: flat fill, drawn edge, hard cast.
   The primary keeps its gold gradient — that one IS the mark, it is how the
   logo's bars are drawn, and it is the pairing contrast.php checks. */
[data-theme="warm"] .btn {
    border: var(--eo-line) solid var(--eo-edge-ink);
    box-shadow: var(--eo-drop-ink);
    transition: transform .16s ease, box-shadow .16s ease, background-color .16s ease;
}
[data-theme="warm"] .btn:hover,
[data-theme="warm"] .btn:focus-visible {
    transform: translate(-2px, -2px);
    box-shadow: 6px 6px 0 rgba(69, 22, 63, .28);
    filter: none;
}
[data-theme="warm"] .btn:active {
    transform: translate(2px, 2px);
    box-shadow: 1px 1px 0 rgba(69, 22, 63, .22);
}
[data-theme="warm"] .btn-ghost {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
[data-theme="warm"] .btn-ghost:hover { background: var(--plum-050); }

/* ---- Chips ----------------------------------------------------------------
   The eyebrow over every section title, and every count and label pill that
   follows its shape. Pill radius, drawn edge, small cast. */
[data-theme="warm"] .eyebrow {
    color: var(--accent);
    background: var(--surface);
    border: var(--eo-line) solid var(--eo-edge);
    box-shadow: var(--eo-drop-sm);
}

@media (prefers-reduced-motion: reduce) {
    [data-theme="warm"] .btn,
    [data-theme="warm"] a.gx-glass { transition: none; }
    [data-theme="warm"] .btn:hover,
    [data-theme="warm"] .btn:focus-visible,
    [data-theme="warm"] .btn:active,
    [data-theme="warm"] a.gx-glass:hover,
    [data-theme="warm"] a.gx-glass:focus-visible { transform: none; }
}

/* The page backdrop: the Explorer badge's violet paper, palest at the head and
   settling into a deeper lilac, so the badge's own colour reads across the whole
   page rather than only inside the roundel in the header. */

/* …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. Ice paper
   at the top corners, a gold bloom low and right, over the pale base. The 40s
   drift is inherited and already suspended under prefers-reduced-motion. */

/* ============================================================================
   THE GROUND — a plum wash, and paper grain over it. Nothing else.
   ============================================================================
   There are no drawings on the background any more. Not a scatter, not drifting
   marks, not margin figures. What is behind the words is paper.

   WHY, after all the work that went into those. The motif was right — a ledger,
   a plotted series, a crossing — and each layer was defensible on its own. The
   trouble was that there were three of them at once, tuned separately and never
   judged together: a tiled scatter, ~28 drifting canvas marks and a haze of
   tally specks, all behind body copy. Every attempt to calm it treated a
   symptom — fewer marks, lower alpha, a wider tile, a mask, a white sheet under
   the stages row. The honest read is that a competition site does not need
   anything behind its text, and this one has a badge, a plum ground and a set of
   outlined cards to carry its identity. The motif still appears where it is
   attached to something: the ruled bands in the hero, the seam and baseline on
   every tinted band, the coins in their corners, the tally rail in the footer.

   WHAT REPLACED IT. Fractal noise from an SVG filter, tiled at 180px and held at
   very low opacity. It is a texture rather than an image: at this strength you
   cannot see a grain, you can only tell that the surface is not flat. That is
   the point — it makes the ground read as stock rather than as a screen, which
   is the same argument the outlined cards and their hard cast are making.

   IT IS PAINTED, NOT DOWNLOADED. The whole thing is one data URI and one
   gradient, so the ground costs no request, no image file and no script. What it
   replaced was 24 KB of JavaScript running a requestAnimationFrame loop on every
   page for the life of the visit.

   baseFrequency .9 with 4 octaves gives a fine, even grain with no visible
   repeat at 180px. Coarser than that and the tile starts to show; finer and it
   disappears into the display's own pixel grid on a high-DPI screen. */
[data-theme="warm"] body {
    background:
        radial-gradient(1100px 620px at 12% -8%, #f2eaf6 0%, rgba(242, 234, 246, 0) 62%),
        radial-gradient(900px 520px at 92% 4%, #f6eff7 0%, rgba(246, 239, 247, 0) 58%),
        linear-gradient(180deg, #faf8fc 0%, #f1e9f4 46%, #e9dcee 100%);
    background-attachment: fixed;
}

/* The wash sheet that warm-theme.css paints — emptied out. It is a fixed,
   inset-0, z-index -3 layer, and leaving it painting anything would put a second
   ground under the one above. The rule stays rather than being deleted because
   warm-theme.css sets four turquoise blooms on it, and without something here
   those come back. */
[data-theme="warm"] body::before {
    background: none;
    opacity: 1;
}

/* The motif — irregular inside the tile, and drifting.
   ---------------------------------------------------------------------------
   THIS IS THE THIRD ARRANGEMENT AND THE OTHER TWO ARE WORTH NAMING, because it
   sits deliberately between them.

   Random placement came first: figures at random positions, random sizes, three
   parallax depths, with a canvas drifting more over the top. At that density
   randomness does not read as decoration, it reads as noise.

   A strict grid came second — four icons, one per quadrant, identical size and
   weight. That fixed the noise and bought a new problem: rows and columns. A
   page ruled into squares reads as regimented, and the eye finds the lattice
   before it finds anything else.

   So: irregular WITHIN the tile, regular in how the tile repeats. Five icons at
   unaligned positions, each rotated a few degrees off square and at two sizes,
   so nothing lines up with anything. The tile still repeats on a fixed grid —
   that is what keeps it calm — but there is no visible row, column or diagonal
   inside it to find.

   TWO LAYERS AT DIFFERENT SCALES, drifting opposite ways. One tile at 300px and
   the same tile at 220px, offset from each other and moving in opposite
   directions at different speeds. Two repeats whose periods do not divide into
   each other never re-align in any position a visitor will see, so the field
   reads as unrepeating even though both halves of it repeat perfectly. It is
   also the cheapest parallax there is: two elements, no script.

   THE DRIFT IS IN TRANSFORM, NOT background-position. Both layers are inset by
   -12% so they overhang the viewport, and each translates by exactly one tile
   width before looping — so the loop is seamless and the browser composites it
   on the GPU instead of repainting a full-viewport background every frame.

   90s and 140s. Slow enough that nothing appears to move while you are reading
   and only a returning eye notices the field has shifted, which is the whole
   idea of a float. */
:root[data-theme="warm"] {
    --eo-motif: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='300' height='300' viewBox='0 0 300 300'><g fill='none' stroke='%235c1c58' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55'><g transform='rotate(-11 55 78)'><circle cx='55' cy='78' r='13'/><circle cx='55' cy='78' r='8'/><path d='M55 65v-3M55 91v3M42 78h-3M68 78h3'/></g><g transform='rotate(7 200 48)'><path d='M187 58h26'/><path d='M191 58v-9M200 58v-15M209 58v-22'/></g><g transform='rotate(13 250 175)'><circle cx='244' cy='168' r='4.5'/><circle cx='258' cy='182' r='4.5'/><path d='M261 163 241 187'/></g><g transform='rotate(-6 110 215)'><path d='M97 225l9-10 8 5 10-14'/><path d='M119 206h5v5'/></g><g transform='rotate(16 45 245)'><circle cx='45' cy='245' r='8'/><path d='M45 237v-2M45 253v2M37 245h-2M53 245h2'/></g></g></svg>");
    --eo-grain: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/></filter><rect width='220' height='220' filter='url(%23g)' opacity='0.42'/></svg>");
}

/* Layer one — the larger repeat, drifting up and left. */
[data-theme="warm"] body::before {
    content: "";
    position: fixed;
    inset: -12%;
    z-index: -3;
    pointer-events: none;
    background-image: var(--eo-motif);
    background-size: 300px 300px;
    opacity: .5;
    mix-blend-mode: multiply;
    animation: eo-float-a 90s linear infinite;
}

/* Layer two — the smaller repeat drifting the other way, with the grain over
   it. The grain tile matches this layer's travel so it loops seamlessly too. */
[data-theme="warm"] body::after {
    content: "";
    position: fixed;
    inset: -12%;
    z-index: -2;
    pointer-events: none;
    background-image: var(--eo-grain), var(--eo-motif);
    background-size: 220px 220px, 220px 220px;
    background-position: 0 0, 70px 130px;
    opacity: .42;
    mix-blend-mode: multiply;
    animation: eo-float-b 140s linear infinite;
}

/* One tile width each, so the frame the loop restarts on is identical to the
   one before it and there is no jump. */
@keyframes eo-float-a {
    to { transform: translate3d(-300px, -300px, 0); }
}
@keyframes eo-float-b {
    to { transform: translate3d(220px, -220px, 0); }
}

/* The drift carries nothing. The pattern stays. */
@media (prefers-reduced-motion: reduce) {
    [data-theme="warm"] body::before,
    [data-theme="warm"] body::after { animation: none; }
}

/* A turbulence fill and a motif grid are a page of grey toner for nothing. */
@media print {
    [data-theme="warm"] body::before,
    [data-theme="warm"] body::after { display: none; }
}
/* Gradient text used for the second half of most headings. */
.grad-text,
.grad {
    background: linear-gradient(92deg, #e3931b 0%, #f7b72d 45%, #7a2a76 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, #f7b72d 0%, #dea425 100%);
    border-color: #e3931b;
    color: #2b0f28;
}

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

/* 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 MyEO
   palette: navy through sky into gold, the site's three inks in the order the
   palette lists them, so the sites are told apart at the first pixel of the page. */
[data-theme="warm"] .site-header::before {
    background: linear-gradient(135deg, #5c1c58 0%, #0f5578 45%, #f7b72d 100%);
}

/* ---- Navigation chrome ------------------------------------------------------
   warm-theme.css paints all four of these in `rgba(245, 250, 252)` / `#f5f8fe` — 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(251, 252, 254, .78); border-bottom-color: var(--border); }
[data-theme="warm"] .site-header.scrolled { background: rgba(251, 252, 254, .96); box-shadow: var(--shadow-sm); }
[data-theme="warm"] .dropdown { background: rgba(251, 252, 254, .99); border-color: var(--border-strong); box-shadow: var(--shadow-md); }

/* The header runs wider than the 1180px reading column.

   MyEO's nav is comfortably inside it — four top-level items, one of them a
   menu — so the extra width is headroom rather than a fix. It is kept because
   the row it has to hold is not the row an editor leaves it as: every menu item
   is behind page_visible(), and a longer label typed at /admin lands here. The
   reading column stays at 1180: prose does not want to be wider, and only the
   header ever 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" — which is how the miss showed up
   both times it happened on the sibling site this was forked from. 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 inherited. On the sibling site
       the row held eight items and needed 7px padding at .86rem plus a shrunken
       CTA to fit. Four items fit here with room to spare, so this only takes the
       edge off — and it is what absorbs a longer label before the nowrap above
       has to. */
    .nav-links > li > a { padding-inline: 11px; }
}
/* A .dropdown-rule lived here on the sibling site: a hairline dividing one menu
   where it stopped listing one page's anchors and started listing another page.
   The single menu here lists six pages of one subject, so there is nothing 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: #f9fafd; border-bottom-color: var(--border-strong); }
}

/* Scroll-to-top. This kept warm-theme's flat turquoise (#e3931b) — 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, #f7b72d, #dea425);
    border-color: #e3931b;
    color: #2b0f28;
    box-shadow: var(--shadow-md);
}
[data-theme="warm"] .to-top:hover { background: linear-gradient(140deg, #fdce6a, #e6a720); }

/* ---- 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 MyEO gold (#e3931b) would have left every body link and the active nav
   item sitting on the AA floor rather than clear of it. So text links take --link
   (the Explorer plum, 11.92:1) and gold 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); }
/* Prose links are UNDERLINED, and that is not decoration.

   The link colour is the Explorer plum (#5c1c58) and body copy is --text-soft
   (#414757). Those two measure 1.48:1 against each other — WCAG asks 3:1 before
   colour alone may distinguish a link, and 1.48 is not close. Without a second
   signal the IEO link in the About introduction, and every link inside an FAQ
   answer, are invisible until the pointer happens to cross one.

   Darkening the link is not the fix: it is already nearly black, and pulling the
   body text lighter to make room would cost the paragraph's own contrast.

   The underline is drawn in GOLD under navy text, which does two things at once:
   it is unmistakable at a glance, and it uses the gold the way this palette
   allows gold to be used — as a mark rather than as type. The same reason the
   registration steps have underlined their links all along. */
[data-theme="warm"] .prose a:not(.btn) {
    color: var(--link);
    text-decoration: underline;
    text-decoration-color: rgba(247, 183, 45, .55);
    text-decoration-thickness: 2px;
    /* clear of descenders — an underline through a 'g' is harder to read than
       no underline at all */
    text-underline-offset: 3px;
    transition: color .2s ease, text-decoration-color .2s ease;
}
[data-theme="warm"] .prose a:not(.btn):hover,
[data-theme="warm"] .prose a:not(.btn):focus-visible {
    color: var(--link-hover);
    text-decoration-color: var(--brand);
}
@media (prefers-reduced-motion: reduce) {
    [data-theme="warm"] .prose a:not(.btn) { transition: none; }
}

/* Widows: no heading left with one word on its last line.
   ---------------------------------------------------------------------------
   redesign.css already balances .section-title, the hero titles and the h2/h3
   inside a glass panel. That covers the headings the sibling sites had and
   misses the ones this site added: the FAQ questions (long, and many of them
   end on "…Economics Explorer Competition?", which lands alone), the stage and
   legend names that end in a bracketed acronym, and the card headings on the
   home page.

   `balance` for headings — it evens the line lengths, which is what pulls the
   last word back up. `pretty` for the leads and body copy, which is the value
   built for this exact problem: it leaves the line breaking alone except to
   avoid a short final line.

   Both are progressive: a browser without them breaks lines as it always did.
   Neither can fix a heading whose last word is longer than the space left on
   the line before it — those are down to the wording. */
h1, h2, h3,
.eo-step-name, .eo-legend-name, .faq-q, summary h2 {
    text-wrap: balance;
}
p.lead, .prose p, .eo-head-text p {
    text-wrap: pretty;
}
/* Sub-headings inside rich text.

   The IEO 2025 write-up runs to three of them ("Silver Medallists", "Bronze
   Medallists" and "The Australasian Economics Olympiad"), and 2024 to two.
   They are h3, one level under the h2 the write-up opens on; upstream sets them
   as bold paragraphs, which are headings to a reader and nothing at all to a
   screen reader.

   Both levels are listed so that moving one does not silently drop its styling —
   redesign.css sets .prose h3 in the sibling sites' navy at 1.25rem and has no
   rule for h4 at all, which meant an h4 drew at bold body size, indistinguishable
   from a paragraph that already bolds every name in it.

   Sized just under .prose h3's 1.25rem and set in the Explorer plum rather than
   that navy — the whole palette moved and these headings should not be the one
   thing still pointing back at it. */
[data-theme="warm"] .prose h3,
[data-theme="warm"] .prose h4 {
    color: var(--brand-900);
    font-size: 1.12rem;
    letter-spacing: -.005em;
    /* Asymmetric on purpose: a heading belongs to what follows it, so the space
       above it has to be clearly larger than the space below, or it reads as a
       caption on the paragraph it has just ended. */
    margin: 1.7em 0 .45em;
}
/* 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(69, 22, 63, .22);
    color: #5c1c58;
}

/* Section headings sit in the Explorer plum rather than the inherited teal. */
[data-theme="warm"] h1,
[data-theme="warm"] h2,
[data-theme="warm"] h3 {
    color: #2b0f28;
}
/* …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); }

/* Panel fill. Flat, and one line rather than a gradient: see THE SURFACE
   LANGUAGE near the top of this file for why nothing here fades any more. The
   edge and the cast are set there too; this rule owns the colour only. */
[data-theme="warm"] .gx-glass {
    background: var(--surface);
}

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

/* ---- Page-level helpers shared across the site ----------------------------- */

/* 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: #5c1c58; 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 — Galaxy's timeline, in MyEO's colours: numbered discs
   threaded on a vertical rail, one plain paragraph of text hanging off each.

   Nothing on this site uses it today: MyEO has no registration page, so the
   steps it was written for live on the sibling sites. Kept rather than deleted
   because it is the shape any "do this, then this" section on this site would
   want, and re-deriving it costs more than carrying it.

   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, #f7b72d 0%, #dea425 100%);
    color: #2b0f28;
    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: #5c1c58;
    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: #e2a41d; }

/* 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: #e2a41d;
}

/* 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(247, 183, 45, .45), rgba(247, 183, 45, .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 — but the MyEO mark is a landscape
   speech bubble with lettering inside it, and at 88px the lettering is a smudge.
   --wide gives it the width the letters need, capped against the viewport.

   200px, down from 260. At 260 the mark was the largest thing in the hero and
   outweighed the h1 below it, which is the page's actual title; the eyebrow and
   the heading between them already say what competition this is, so the mark is
   identifying a page the reader has arrived at rather than announcing it. The
   floor is the strapline inside the artwork — below about 180px "Malaysian
   Economics Olympiad" stops resolving, and that line is not repeated anywhere
   else in the hero. */
.page-hero .badge-mini { height: auto; }
.page-hero .badge-mini--wide { width: min(200px, 58vw); }

/* ---- Explorer → MyEO lockup ----------------------------------------------
   The competition badge feeding the olympiad it leads into, over the copy that
   describes that route — the siblings' .about-lockup, in MyEO's colours. */
.about-lockup {
    /* One place the plate height is written, because three rules need to agree
       on it: the plate itself, and the arrow that has to sit on the plate's
       centreline rather than on the row's. */
    --al-plate-h: clamp(120px, 13vw, 168px);
    display: flex;
    /* flex-start, not center. The two captions wrap to different numbers of
       lines — "Economics Explorer Competition" fits on one, "Malaysian
       Economics Olympiad" may take two — so centring the row
       centres two figures of different heights and the PLATES come out on
       different lines. Topped, the plates align and the captions hang below at
       whatever height they need. */
    align-items: flex-start;
    justify-content: center;
    gap: clamp(14px, 2.6vw, 34px);
    margin-bottom: clamp(22px, 3vw, 34px);
}
/* The figure is exactly as wide as its plate, and the caption wraps inside it.

   It used to be `flex: 0 0 auto` with no width, so each figure sized to its
   widest child — which is the caption, not the plate. "MALAYSIAN ECONOMICS
   OLYMPIAD" is 28 characters on one line, so that figure came out far wider
   than the 264px plate it held, and the plate sat against its left edge. `text-align: center` did not help: it centres inline content, and a
   plate with a set width is a block.

   Two things came out of fixing it rather than one. The plate is centred under
   its caption, and both figures are now the same width — before, the two
   captions were different lengths, so the pair was lopsided around the arrow
   between them. */
.al-item {
    flex: 0 0 auto;
    width: clamp(190px, 21vw, 264px);
    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: 100%;
    height: var(--al-plate-h);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(14px, 1.8vw, 22px);
    border-radius: var(--eo-r);
    background: var(--surface);
    border: 1px solid rgba(69, 22, 63, .12);
    box-shadow: var(--eo-drop);
}
.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;
    /* Tightened from .14em. The caption now wraps to the plate's width rather
       than running on one line, and at 44 characters over two or three lines the
       wider tracking was costing a whole extra line. */
    letter-spacing: .1em;
    line-height: 1.45;
    text-transform: uppercase;
    color: var(--text-soft);
    text-wrap: balance;
}

/* the connector: a dashed trail running toward MyEO, ending in a gold chip.

   Pushed down to the plates' centreline and then pulled back by half its own
   height, so it lands on that line whatever height it happens to be. Doing it
   with align-items on the row instead would put it in the middle of the tallest
   FIGURE — caption included — which is about 26px below the plates. */
.al-arrow {
    flex: 1 1 auto;
    max-width: 190px;
    min-width: 54px;
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: calc(var(--al-plate-h) / 2);
    transform: translateY(-50%);
}
.al-trail {
    flex: 1 1 auto;
    height: 3px;
    border-radius: 999px;
    background: repeating-linear-gradient(90deg, #e3931b 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 #f7b72d is
       nowhere near readable at an 18px glyph */
    color: #2b0f28;
    background: linear-gradient(135deg, #f7b72d 0%, #dea425 100%);
    box-shadow: var(--eo-drop);
    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-item { width: min(260px, 78vw); }
    .al-plate { height: 132px; }
    /* Stacked, the arrow sits between the two plates in the flow, so the
       centreline offset above is not just unnecessary — it would push it into
       the second plate. */
    .al-arrow {
        flex: 0 0 auto; max-width: none; min-width: 0;
        flex-direction: column; height: 74px;
        margin-top: 0; transform: none;
    }
    .al-trail {
        width: 3px;
        height: auto;
        background: repeating-linear-gradient(180deg, #e3931b 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 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 block takes the panel's full width. No measure cap.
   ---------------------------------------------------------------------------
   ASKED FOR, AND KEPT ON PURPOSE — do not put the cap back without checking.

   This was capped at 75ch on the container for a long time, which centred the
   write-up inside its panel with a clear margin either side. The reasoning was
   the usual one: past about 80 characters the eye starts losing its place on the
   return sweep, and justified setting makes that worse rather than better,
   because every line ends flush and there is no ragged edge left to navigate by.
   The site owner wanted the copy to fill its card, and that is the call being
   honoured here.

   Note what the cap was NOT doing, in case it looks like the fix for something
   else: it was never holding the paragraphs to the panel's padding. `.eo-justify
   p` already clears the `--measure` cap that `.prose p` and the `.gx-glass`
   fallback set, so removing the container cap lets paragraphs, sub-headings and
   lists all range to the same edge — which is the alignment the cap was
   originally introduced to protect. That still holds; only the width changed.

   The one thing the wide measure does buy back: justification gets EASIER as the
   line gets longer, not harder. With hyphens off (see above) the only slack a
   line has is its word spaces, and a long line has more of them to spread the
   slack across. The ugly case for this block was always the narrow one, which is
   why the phone breakpoint further down drops to ragged-right. */
.eo-justify {
    margin-inline: auto;
}
.eo-justify p,
.eo-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: var(--eo-r);
    border: var(--eo-line) solid var(--eo-edge);
    background: #fafbfd;
    box-shadow: var(--eo-drop);
    transition: transform .3s ease, box-shadow .3s ease;
}
.fee-card:hover { transform: translate(-2px, -2px); box-shadow: var(--eo-drop-lift); }

/* ---- header: the tier's name on the brand band ----------------------------
   Navy into gold, not the navy-into-teal the hue rotation produced. Both ends
   were on the palette, but the interpolation between a plum mid and a sky runs
   through desaturated grey at the midpoint — which is exactly where the band is
   widest, so the header read as muddy rather than as either colour. Navy and gold
   are neighbours on the wheel, so the whole band stays in the brand. */
.fee-card-head {
    padding: 15px 18px;
    background: linear-gradient(160deg, #5c1c58 0%, #b5790f 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 dark brand band, where it needs the
   light ink instead */
.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, #7a2a76 0%, #2b0f28 100%);
    color: #fff;
    font-size: .8rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
    text-align: center;
    box-shadow: var(--eo-drop);
}

/* 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(69, 22, 63, .1);
    border: 1px solid rgba(69, 22, 63, .28);
    color: #5c1c58;
    font-size: .72rem;
    font-weight: 800;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.fee-pill--muted {
    background: rgba(43, 15, 40, .07);
    border-color: rgba(43, 15, 40, .16);
    color: var(--text-soft);
}
.fee-card.is-ended { opacity: .72; }
/* An ended tier keeps the shape and loses the colour: the same band desaturated
   toward the page's own ink, so the row still reads as two cards of one kind
   rather than as one card and a mistake. */
/* Flat, like every other fill. Grey rather than plum is the point of it — a
   window that has closed should not carry the brand colour — but it was the one
   remaining two-stop surface gradient in the file. */
.fee-card.is-ended .fee-card-head { background: #5f5a63; }
/* A closed window does not lift. It keeps its resting cast, which is the
   difference between "not interactive" and "interactive but not moving". */
.fee-card.is-ended:hover { transform: none; box-shadow: var(--eo-drop); }

/* ---- 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: #5c1c58; }

/* 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 an Explorer home page runs twelve sections where the inherited
   engine assumes three or four.

   It is gone, and nothing here should bring it back. The Explorer sites are meant
   to read as one family, and every value it overrode is identical in this site'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 "MyEO 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). */
.eo-head-text .eyebrow { margin-bottom: 14px; }
.eo-head + .lead { margin-bottom: 12px; text-align: center; margin-inline: auto; }
/* A lead directly under a head belongs TO that head — eyebrow, title, rule, then
   the sentence — so it closes up to it. .eo-head is spaced for the gap between a
   head and the section BODY (clamp(26px, 4vw, 44px), Biology's .gx-head to the
   pixel), and at 44px under the rule the sentence read as a stray paragraph
   floating above the section rather than as part of the heading.
   :has(), as .eo-links already uses: which heads are followed by a lead is a
   property of the markup, and a browser without it simply keeps the wider gap. */
.eo-head:has(+ .lead) { margin-bottom: clamp(14px, 1.6vw, 20px); }
/* 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. */
.eo-head-text { min-width: 0; }
.eo-head-text .eyebrow { margin-bottom: 10px; }

/* ---- Introduction ----------------------------------------------------------
   The paragraph, then the objectives it promises. Nothing else.

   This section was the sibling sites' "opening leaf of an edition": a ruled
   margin of tally strokes down the left, a dropped initial on the first
   paragraph, and the objectives numbered in lower-case roman with the numerals
   hanging in their own gutter. It is a careful piece of typography and all
   three of its ornaments were borrowed.

   A ruled margin and a drop cap belong to a site about reading manuscripts.
   The roman numerals were that site's way of keeping its apparatus distinct
   from its 01/02 plates — a problem this site does not have, and a solution
   that reads here as a numbering style nobody chose.

   What the section has to do is state what the competition is and list what it
   is for, and the ornament was competing with both: the margin strokes sat
   closer to the text than anything else on the page, and a reader met the drop
   cap before the sentence.

   So: a plain panel, a paragraph at reading size, and a ticked list. The width
   cap is the one thing kept from the leaf, and for the reason given below.

   Section headings sit on the page field, not inside the panel, and the panel
   holds only content — twelve sections each boxing their own title read as a
   stack of near-identical cards.
   ========================================================================== */
/* NARROWER than the container, and that is the one piece of the old leaf worth
   keeping.

   Every other panel on this page runs the full 1180px because every other panel
   holds cards, and cards want width. This one holds a paragraph, and a paragraph
   wants a measure — at 1180px it would run to about 110 characters a line, which
   is roughly double what anybody reads comfortably.

   880px is what makes the text block right. */
.eo-intro {
    max-width: 880px;
    margin-inline: auto;
    border-radius: var(--eo-r);
}

/* The opening paragraph, a size above body copy: this is the first prose on the
   page and the only paragraph on it anybody reads start to finish.

   No max-width. `.prose` sets 66ch and that is right everywhere else on the site,
   but here the PANEL is what holds the measure — capping the paragraph as well
   would stop it short of the panel it sits in and leave a stripe of empty card
   down its right side, which is what a second measure inside a measured column
   always does. One thing sets the width, and it is the thing you can see. */
.eo-lede p {
    max-width: none;
    font-size: clamp(1rem, 1.05vw, 1.08rem);
    line-height: 1.75;
    color: var(--text-soft);
}
.eo-lede p + p { margin-top: .85em; }

/* The label over the list, with a hairline running off it to the right. Not a
   section title: this sits INSIDE the panel and must not compete with the one
   over it. */
.eo-aims-label {
    display: flex;
    align-items: center;
    gap: 14px;
    max-width: none;          /* a <p> in a glass panel inherits the 66ch measure */
    margin: clamp(24px, 3.2vw, 36px) 0 clamp(16px, 2vw, 22px);
    font-size: .76rem;
    font-weight: 700;
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--accent);
}
.eo-aims-label::after {
    content: "";
    flex: 1;
    height: 1px;
    background: linear-gradient(90deg, rgba(69, 22, 63, .3), transparent);
}

/* The objectives: a tick, a sentence, a hairline between.

   The same shape /about-us uses for its own ticked lists, which is the point —
   this site already had a way of setting "a short list of things that are true",
   and the Introduction was the one place not using it.

   Ticks rather than numerals because the five are a SET, not a sequence: nothing
   on the site refers to "objective three", and the moment they are numbered the
   question of which numbering style comes back. */
.eo-aims { list-style: none; margin: 0; padding: 0; display: grid; }
.eo-aim {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr);
    align-items: start;
    gap: 12px;
    padding: clamp(12px, 1.6vw, 16px) 0;
    border-top: 1px solid var(--border);
}
.eo-aim:first-child { border-top: 0; padding-top: 0; }
.eo-aim:last-child { padding-bottom: 0; }

/* The tick sits on the first line of its sentence rather than centred on the
   block — a two-line objective with a vertically centred mark reads as a mark
   between two lines rather than at the head of one. */
.eo-aim-tick {
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    margin-top: -1px;
    border-radius: var(--eo-r-sm);
    color: var(--accent-2);
    background: rgba(247, 183, 45, .16);
    border: 1px solid rgba(69, 22, 63, .12);
}
.eo-aim p {
    margin: 0;
    max-width: none;
    color: var(--text-soft);
    line-height: 1.6;
    align-self: center;
}

/* ---- Feature rows ----------------------------------------------------------
   A mark on a tinted tile, a sentence beside it, a hairline between.

   Only .eo-feature-ico is still in use, by the two "how to register" rows in
   the pricing panel. The row rules themselves are kept because they are the
   shape any "a mark, then a sentence" list on this site would want, and because
   two sections have already been built out of them and taken back out again —
   the Introduction's objectives (now its own ticked list) and the
   Benefits section (removed: hidden upstream, see ContentSeeder). Deleting them
   would mean re-deriving them the next time. */
.eo-features { list-style: none; margin: 0; padding: 0; display: grid; }
.eo-feature {
    display: grid;
    grid-template-columns: 42px 1fr;
    gap: 14px;
    align-items: center;
    padding: 13px 0;
    border-bottom: 1px solid rgba(69, 22, 63, .10);
}
.eo-feature:last-child { border-bottom: 0; padding-bottom: 0; }
.eo-feature p {
    margin: 0;
    max-width: none;
    color: var(--text-soft);
    font-size: 1rem;
    line-height: 1.55;
}
.eo-feature-ico {
    display: grid; place-items: center;
    width: 42px; height: 42px; border-radius: var(--eo-r);
    background: rgba(247, 183, 45, .18);
    border: 1px solid rgba(69, 22, 63, .16);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .8);
    color: #5c1c58;
    transition: transform .3s ease, border-color .3s ease;
}
.eo-feature:hover .eo-feature-ico {
    transform: scale(1.07);
    border-color: rgba(247, 183, 45, .55);
}

@media (prefers-reduced-motion: reduce) {
    .eo-feature-ico { transition: none; }
    .eo-feature:hover .eo-feature-ico { transform: none; }
}

/* ---- Competition format: THE SPEC CARDS ------------------------------------
   Two cards to a row, and a long value takes the row to itself.

   Three arrangements came before this. The first was a flex wrap of
   content-sized cells, which packed three then two and left the second row
   centred on nothing. The second was a full-width table of hairline rows, which
   fixed the ragged edge and introduced a worse problem: these values are mostly
   two or three words, so more than half of every row was empty card, and a page
   of hairlines directly under five saturated plates read as unfinished.

   So: cards again, but on a real grid rather than a wrap — and the width of a
   card is decided by the length of its own value, which is the thing the first
   two attempts each got half right. See the note in x-eo-spec for where the
   threshold comes from.

   The section still has a job the quick-facts bar under the hero does not. That
   bar carries four of these five values as big figures; this is the
   specification a teacher checks before the day, and it is the only place the
   fifth (Necessity) appears at all. Same values, different question — which is
   why the two are drawn differently rather than merged.
   ========================================================================== */
.eo-spec {
    max-width: 1000px;
    margin: clamp(20px, 3vw, 30px) auto 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(12px, 1.4vw, 16px);
    /* Dense, or a full-width card leaves a hole beside the one before it: the
       grid will not put a later short card into a gap a wide card could not
       fit. With five specs and one wide one that is two holes, which is what
       the layout looked like before this line.

       Dense reorders what you SEE against source order, which is a real cost
       and is affordable here for one reason: nothing in this list is a
       sequence. They are five independent facts about the same paper, and none
       of them is focusable, so nothing tabs through them in an order that could
       now disagree with the page. Do not carry this line to a list where the
       order means something. */
    grid-auto-flow: dense;
}

/* Three short specs go three to a row instead of two-and-a-hole.
   ---------------------------------------------------------------------------
   The MyEO paper on /about-us is exactly three: Paper 1, Paper 2 and Mode. In
   two columns the third sat alone with an empty half-row beside it, which reads
   as a card that failed to load rather than as a list of three.

   x-eo-spec adds this class only when there are three rows and none of them is
   wide — a wide card spans the whole row, so on a three-column grid it would
   have to break across two of the three and the hole comes straight back.

   Between 980px and the phone breakpoint the three do not fit, so it falls back
   to two columns and the last card takes the row rather than half of it. Below
   720px the shared rule beneath this one puts every card on its own row and
   there is nothing left for either to decide. */
.eo-spec--trio { grid-template-columns: repeat(3, minmax(0, 1fr)); }

@media (max-width: 980px) {
    .eo-spec--trio { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .eo-spec--trio .eo-spec-item:last-child { grid-column: 1 / -1; }
}

/* A <dl>'s <dt> and <dd> are siblings, so each pair is wrapped to make a card.
   The wrapper is the grid item; the pair inside it gets its own two-column
   grid, with the mark spanning both rows down the left. */
.eo-spec-item {
    display: grid;
    grid-template-columns: 52px minmax(0, 1fr);
    align-items: center;
    gap: 2px 16px;
    padding: clamp(14px, 1.8vw, 18px) clamp(16px, 2vw, 20px);
    border: var(--eo-line) solid var(--eo-edge);
    border-radius: var(--eo-r);
    background: var(--surface);
    box-shadow: var(--eo-drop);
    transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
}
.eo-spec-item:hover {
    transform: translate(-2px, -2px);
    border-color: var(--border-strong);
    box-shadow: var(--eo-drop-lift);
}
/* The long ones. A sentence in half a row wraps to four lines; across the whole
   row it is one or two. */
.eo-spec-item.is-wide { grid-column: 1 / -1; }

/* The mark, on a tinted tile, spanning the label and the value beside it. The
   tile is back after the table experiment dropped it: with hairlines gone, the
   drawings had nothing to sit on and floated in the corner of each card. */
.eo-spec-ico {
    grid-row: span 2;
    display: grid;
    place-items: center;
    width: 52px;
    height: 52px;
    border-radius: var(--eo-r);
    background: rgba(247, 183, 45, .18);
    border: 1px solid rgba(69, 22, 63, .13);
}

/* 1.05rem/.08em. Was .76rem/.13em, then .9rem/.1em to match .eso-fmt-label and
   .geo-fmt-label on the sibling sites; now raised again on this site only, so MyEO
   no longer matches those two — sync them if the spec sheet is ever compared
   side by side. The reasoning behind the first rise still holds and is why the
   tracking keeps coming down as the size goes up: "PAPER 1" and "MODE" are
   uppercase, and caps under wide tracking are the hardest thing to read small,
   so tracking was compensating for size. With the size raised it is needed less,
   not more.

   .eo-gate-label and .eo-route-label move with it — see the note on the first of
   them. They are the same object, a small-caps label over its value, and they
   were unified at .9rem precisely because they had drifted to .76/.8/.76. */
.eo-spec-label {
    align-self: end;
    margin: 0;
    font-size: 1.05rem;
    font-weight: 800;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--accent);
}
/* The [data-theme] prefix is not decoration: warm-theme.css sets a colour on
   every <dd> at a weight this would otherwise lose to. */
[data-theme="warm"] .eo-spec-value {
    align-self: start;
    margin: 0;
    max-width: none;
    color: var(--text);
    font-weight: 600;
    font-size: 1.04rem;
    line-height: 1.4;
}

/* One column below 720px: two cards at that width give "Stable internet
   connection and scientific calculator" about four words a line. Every card is
   full width there, so `is-wide` has nothing left to say. */
@media (max-width: 720px) {
    .eo-spec { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
    .eo-spec-item { transition: none; }
    .eo-spec-item:hover { transform: none; }
}


/* ---- 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 its own drawing: a first reading for
   Primary, a worked correspondence 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. */
.eo-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) {
    .eo-cats {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        align-items: stretch;
        gap: clamp(18px, 2.2vw, 28px);
    }
}
.eo-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: var(--eo-r);
    transition: transform .3s ease, box-shadow .3s ease, border-color .3s ease;
}
.eo-cat:hover {
    transform: translate(-2px, -2px);
    box-shadow: var(--eo-drop-lift);
    border-color: rgba(247, 183, 45, .45);
}
.eo-cat-art {
    display: grid; place-items: center;
    filter: drop-shadow(0 10px 18px rgba(43, 15, 40, .22));
}
.eo-cat-art .eo-cat-art-svg { display: block; width: clamp(84px, 11vw, 112px); height: auto; }
.eo-cat-body { min-width: 0; }
.eo-cat h3 { margin: 0 0 12px; font-size: clamp(1.25rem, 2.2vw, 1.6rem); }
.eo-cat ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.eo-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);
}
.eo-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 */
.eo-cat li svg { justify-self: center; color: #5c1c58; position: relative; top: 3px; }
.eo-cat-txt { line-height: 1.5; }
.eo-cat li b { color: #2b0f28; }
.eo-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 */
    .eo-cat { grid-template-columns: 1fr; justify-items: center; text-align: center; }
    .eo-cat-body { width: 100%; }
    .eo-cat li { text-align: left; }
}
@media (prefers-reduced-motion: reduce) {
    .eo-cat { transition: none; }
    .eo-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. */
.eo-counters {
    display: flex; justify-content: center;
    gap: clamp(28px, 7vw, 90px); flex-wrap: wrap;
}
.eo-count { text-align: center; }
.eo-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(247, 183, 45, .22), rgba(255, 255, 255, .9) 66%);
    border: 1px dashed rgba(69, 22, 63, .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. */
.eo-ring::before {
    content: ""; position: absolute; inset: -9px; border-radius: 50%;
    border: 3px solid rgba(69, 22, 63, .16);
    border-top-color: #e3931b;
    border-right-color: rgba(222, 164, 37, .55);
    filter: drop-shadow(0 0 4px rgba(222, 164, 37, .35));
    animation: eo-ring-spin 12s linear infinite;
}
@keyframes eo-ring-spin { to { rotate: 1turn; } }
@media (prefers-reduced-motion: reduce) { .eo-ring::before { animation: none; } }
.eo-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, #e3931b, #5c1c58);
    -webkit-background-clip: text; background-clip: text;
    -webkit-text-fill-color: transparent; color: transparent;
}
.eo-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. */
.eo-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 */
.eo-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(69, 22, 63, .35), #0f5578 45%, #f7b72d);
    opacity: .5;
}
.eo-station { position: relative; z-index: 1; text-align: center; }

/* The station disc.

   Kept white rather than tinted, and now for a different reason than it was: it
   held full-colour illustrations that a tint would have muddied, and it holds a
   navy line icon that a tint would flatten against. White is what gives the
   stroke somewhere to sit on a page whose own ground is pale ice. */
.eo-station-node {
    position: relative;
    display: grid; place-items: center;
    width: 78px; height: 78px; margin: 0 auto 14px;
    border-radius: 50%;
    background: #fff;
    /* the drawn edge the rest of the site has, on the one ornament that is a
       circle rather than a card */
    border: var(--eo-line) solid var(--eo-edge);
    /* the cream halo punches the rail out behind each disc */
    box-shadow: var(--eo-drop);
    color: #5c1c58;
    transition: transform .3s ease, box-shadow .3s ease;
}
.eo-station:hover .eo-station-node {
    transform: translate(-2px, -2px);
    box-shadow: var(--eo-drop-lift);
}
.eo-station h3 {
    margin: 0 0 8px; font-size: .98rem; line-height: 1.35;
    color: #2b0f28; text-wrap: balance;
}
.eo-station-date {
    display: inline-block; margin: 0;
    padding: 5px 12px; border-radius: 999px;
    background: var(--water-soft);
    border: 1px solid rgba(69, 22, 63, .16);
    font-size: .86rem; font-weight: 700; color: #5c1c58;
}

/* Competition day: the one date a visitor must not miss. An amber ring and glow
   rather than an amber fill — gold is a fill colour on this brand and a navy
   icon on a gold disc at 30px is the one place it would have to carry a line
   weight rather than a block. The icon itself takes the gold instead. */
.eo-station.is-hl .eo-station-node {
    border: 2px solid #dea425;
    box-shadow: var(--eo-drop);
    color: #b5790f;
}
/* 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 */
.eo-station.is-hl .eo-station-date {
    background: linear-gradient(135deg, #f7b72d, #dea425);
    border-color: #e3931b;
    color: #2b0f28;
    font-weight: 800;
    box-shadow: var(--eo-drop);
}
.eo-station.is-hl h3 { color: #5c1c58; 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) {
    .eo-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. */
    .eo-rail::before { display: none; }
    .eo-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(122, 42, 118, .42);
    }
    .eo-station:first-child::before { top: 50%; }
    .eo-station:last-child::before { bottom: 50%; }
    .eo-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. */
    .eo-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. */
    .eo-station-node {
        position: absolute;
        left: 0;
        top: 50%;
        margin: -37px 0 0;
    }
    .eo-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. */
    .eo-station-date { align-self: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
    .eo-station-node { transition: none; }
    .eo-station:hover .eo-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 figure 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 coin in each bottom corner, the bars-and-grid
   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. */
.eo-workshop {
    position: relative;
    margin: clamp(18px, 3vw, 36px) 0;
    padding-block: clamp(34px, 5vw, 62px);
    background:
        var(--eo-coin-light),
        var(--eo-coin-light),
        var(--eo-ground-tex-light),
        radial-gradient(120% 90% at 12% -10%, rgba(247, 183, 45, .20), transparent 58%),
        radial-gradient(110% 90% at 92% 4%, rgba(201, 166, 208, .16), transparent 55%),
        linear-gradient(165deg, #2b0f28 0%, #5c1c58 46%, #3a1136 100%);
    /* the two seals sit past the bottom edge so they read as pressed into 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(247, 183, 45, .35);
    color: #ebeef4;
}
/* A ruled baseline 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. */
.eo-workshop::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 22px;
    background: var(--eo-baseline-tile-light) bottom center / 220px 22px repeat-x;
    pointer-events: none;
}
/* the ruled seam the other bands use, in the light ink this one needs */
.eo-workshop::before {
    content: "";
    position: absolute; inset-inline: 0; top: 0; height: 6px;
    background-image: radial-gradient(circle at 3px 3px, rgba(247, 183, 45, .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. */
.eo-workshop .eo-head .section-title,
[data-theme="warm"] .eo-workshop .eo-head .section-title { color: #fff; }

/* "Economics Explorer Preparatory Workshop 2026/2027" is the longest heading on
   the site, and it was setting as THREE lines with "Preparatory" alone in the
   middle. Two things were doing it, and both had to go.

   The first is .eo-head's shared max-width: 720px. That is right for a two-word
   title over a centred lead, and this heading needs 803px at its own size — the
   band has well over a thousand to give it. The year heads on /about-us release
   the same cap for the same reason; see the note there.

   The second is text-wrap: balance, set on .section-title in redesign.css. Once
   a heading needs a second line, balance evens the two rather than filling the
   first — so widening the box alone just bought a longer, still-balanced pair
   ("Economics Explorer" / "Preparatory Workshop 2026/2027"). `pretty` fills
   greedily and only guards the last line, which is what puts the break where
   the words want it: the name, then the year.

   MEASURED: "Economics Explorer Preparatory" is 16.19em in this face, so it
   needs 803px at the 49.6px this heading takes from 1180px up. 880 leaves it
   77px of margin. Use docs/measure-workshop-heading.html — copy it into public/ to run, as with
   the hero harness.

   Below ~600px it wraps again and that is left alone: the column is 509px at a
   560px viewport and the run needs 622px there, so holding it would mean
   shrinking the band's own title to something smaller than the body copy under
   it. */
.eo-workshop .eo-head {
    max-width: 880px;
}
.eo-workshop .eo-head .section-title {
    text-wrap: pretty;
}
/* The band's own gradient for the title, and both selectors are needed.
   ---------------------------------------------------------------------------
   `.section-title .grad` is the site's usual arrangement — a span wrapping the
   second half of a heading the template writes in two pieces. The workshop title
   is one content block typed at /admin, so it has no second half and the class
   goes on the heading itself: `.section-title.grad`, no space.

   A DIFFERENT RAMP FROM THE PAGE'S. The site's `--grad-title` ends in Explorer plum,
   which is the right close on white paper and invisible on this band — the one
   section that is solid deep plum with light text. This one runs pale lilac
   through the gold and out to a lilac tint, so every stop sits well clear of the
   ground: #ede4f0 is 15.5:1 on #2b0f28, the gold 9.5:1, and #c9a6d0 6.8:1, all
   above the 4.5 floor for body text and far above the 3:1 a heading needs. */
/* 1.3 on the workshop title alone. Its second line carries a "/", and Poppins
   Bold's slash rises .95em while a descender on the line above falls .28em: they
   need 1.23em between baselines, and the site's tight 1.1 leaves them 6px into
   each other. 1.3 clears by 3px — the value MyBO's title already uses. */
.eo-workshop .section-title { line-height: 1.3; }
.eo-workshop .section-title .grad,
.eo-workshop .section-title.grad {
    background: linear-gradient(92deg, #ede4f0 0%, #f7b72d 50%, #c9a6d0 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.
   #d6def2 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 #e9edf7, 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"] .eo-workshop .eyebrow {
    background: rgba(0, 0, 0, .20);
    border-color: rgba(247, 183, 45, .5);
    color: #d6def2;
}
/* .eo-badge is kept legible on the band in case the eligibility chip is ever put
   back, but nothing renders it on this page today. */
.eo-workshop .eo-badge {
    background: linear-gradient(135deg, #f7b72d, #dea425);
    border-color: #e3931b;
    color: #2b0f28;
}
/* 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"] .eo-workshop .gx-glass {
    background: var(--surface);
    border-color: rgba(255, 255, 255, .45);
    box-shadow: var(--eo-drop);
    /* 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"] .eo-workshop .gx-glass p,
[data-theme="warm"] .eo-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. */
.eo-workshop .eo-tabs {
    justify-content: center; gap: 10px;
    border-bottom: 0;
    margin-bottom: clamp(18px, 2.4vw, 26px);
}

/* The tabs, and they were the wrong way round.
   ---------------------------------------------------------------------------
   Unselected tabs were filled with the brand gold and the selected one was
   plum, so the two tabs you are NOT on shouted and the one you were on went
   quiet. Gold is this brand's "act on this" colour — it is the primary button
   and nothing else — and a tab you are already reading is state, not an action.

   Selected is now the solid plum with the cast, which is the loudest thing in
   the row; the others are paper with the drawn edge, like every other surface
   on the site. Gold is left for the buttons. */
.eo-workshop .eo-tab {
    border-radius: 999px;
    padding: 10px 20px;
    background: var(--surface);
    border: var(--eo-line) solid var(--eo-edge);
    color: var(--accent);
    box-shadow: var(--eo-drop-sm);
}
.eo-workshop .eo-tab:hover {
    background: var(--plum-050);
    border-color: var(--eo-edge-strong);
    color: var(--accent);
}
.eo-workshop .eo-tab[aria-selected="true"] {
    background: #5c1c58;
    border-color: #45163f;
    color: #fff;
    box-shadow: var(--eo-drop);
}
.eo-workshop .eo-tab:focus-visible { outline: 2px solid #f7b72d; outline-offset: 2px; }

/* Blocks inside the panel — the workshop's own material runs as titled blocks,
   not as loose prose. */
.eo-wk-block {
    padding: clamp(16px, 2vw, 24px);
    border-radius: var(--eo-r);
    background: #fff;
    border: 1px solid rgba(69, 22, 63, .14);
    box-shadow: var(--eo-drop);
}
.eo-wk-block + .eo-wk-block,
.eo-wk-facts + .eo-wk-block,
.eo-wk-block + .eo-wk-facts { margin-top: 16px; }
.eo-wk-block h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 10px; font-size: 1.15rem; }
.eo-wk-block h4 { display: flex; align-items: center; gap: 9px; margin: 18px 0 8px; font-size: 1rem; }
.eo-wk-block h3 + p, .eo-wk-block h4 + ul { margin-top: 0; }
.eo-wk-block ul { margin: 0; padding-left: 1.15em; }
.eo-wk-block li { margin-bottom: .4em; }
.eo-wk-note {
    margin: 14px 0 0; padding: 10px 14px;
    border-left: 3px solid #f7b72d; border-radius: 0 var(--eo-r) var(--eo-r) 0;
    background: rgba(247, 183, 45, .12);
    font-size: .92rem; font-style: italic;
}
.eo-wk-cta { margin: 18px 0 0; }
/* the Infopack tab is a centred sheet of posters, so its button centres with them */
.eo-wk-posters + .eo-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. */
.eo-wk-facts {
    display: grid; gap: 14px; margin-top: 16px;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.eo-wk-facts--two { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
/* Amber cards, flat. They were a #fdce6a-to-#e6a720 gradient, which is the
   primary button's own ramp — three saturated buttons' worth of gold sitting
   in a row carrying no action at all. Flat and paler, with the gold drawn edge
   the rest of the site would give them, so they still read as the amber set the
   section wants without competing with the button under them. */
.eo-wk-fact {
    padding: 16px 18px;
    border-radius: var(--eo-r);
    background: #fbe3ad;
    border: var(--eo-line) solid #e3931b;
    color: #2b0f28;
    box-shadow: var(--eo-drop-sm);
}
/* .gx-glass is an ancestor of these cards, so the panel's light-ink rules above
   out-specify a plain `.eo-wk-fact p`. Matched through the panel here, which is
   what keeps the ink dark on the amber fill. */
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact h4 { margin: 0 0 8px; color: #2b0f28; font-size: .95rem; }
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact p { margin: 0; color: #2b0f28; font-weight: 600; }
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact li { color: #2b0f28; }
.eo-wk-fact dl { margin: 0; }
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact dt { margin-top: 8px; font-weight: 700; color: #2b0f28; }
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact dt:first-child { margin-top: 0; }
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact dd { margin: 0 0 0 14px; color: #3f1039; }
[data-theme="warm"] .eo-workshop .gx-glass .eo-wk-fact b { color: #2b0f28; }

/* Contact + social, two columns inside one block */
.eo-wk-contact { display: grid; gap: clamp(14px, 3vw, 40px); grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.eo-wk-contact h4:first-child { margin-top: 0; }
[data-theme="warm"] .eo-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. */
.eo-wk-posters {
    display: grid; gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(210px, 300px));
    justify-content: center;
}
.eo-wk-posters a {
    display: block; border-radius: var(--eo-r); overflow: hidden;
    border: 1px solid rgba(69, 22, 63, .18);
    box-shadow: var(--eo-drop);
    transition: transform .3s ease;
}
.eo-wk-posters a:hover { transform: translateY(-4px); }
.eo-wk-posters img { display: block; width: 100%; height: auto; }
@media (prefers-reduced-motion: reduce) {
    .eo-wk-posters a { transition: none; }
    .eo-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. */
.eo-tabs {
    display: flex; gap: 6px; flex-wrap: wrap;
    margin: 0 0 18px;
    border-bottom: 1px solid rgba(69, 22, 63, .18);
}
.eo-tab {
    appearance: none; background: transparent; cursor: pointer;
    border: 0; border-bottom: 2px solid transparent; border-radius: var(--eo-r) var(--eo-r) 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;
}
.eo-tab:hover { color: var(--accent); background: rgba(247, 183, 45, .10); }
.eo-tab[aria-selected="true"] { color: #5c1c58; background: rgba(247, 183, 45, .14); border-bottom-color: #f7b72d; }
.eo-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. */
.eo-panel[hidden] { display: none !important; }

/* ---- Syllabus --------------------------------------------------------------
   Four statement cards, two up. The sibling sites run a Primary block beside a
   Secondary one because their syllabuses genuinely differ by age; this one does
   not — see the note on the section in home.blade.php — so what is laid out here
   is four claims about the paper rather than two lists of topics.

   auto-fit with a 300px floor rather than a fixed two columns: at three cards
   the row would otherwise leave a hole, and an editor adding a fifth point
   should not have to touch CSS. */
.eo-syl-points {
    display: grid;
    /* 240px, not 300: the home page has four points and the container is 1148px
       of usable width, so a 300px floor gives three columns and leaves the fourth
       card alone on a row of its own. At 240 all four sit on one line at desktop
       width, and the pages that carry three points still get three columns. */
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
    gap: 16px;
}
.eo-syl-point {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: 16px;
    padding: clamp(18px, 2.2vw, 26px);
    border-radius: var(--eo-r);
}
/* The mark sits on a tinted disc, the same treatment .eo-feature-ico gives the
   objectives — the two sections are both "a mark and a sentence", so they read
   as one family rather than two inventions. */
.eo-syl-ico {
    display: grid; place-items: center;
    width: 46px; height: 46px; flex: none;
    border-radius: var(--eo-r);
    color: var(--accent);
    background: #fbf1de;
    border: var(--eo-line) solid var(--eo-edge);
}
.eo-syl-point p { margin: 0; max-width: none; }
.eo-syl-point h3 { margin: 0 0 8px; font-size: 1.05rem; }

/* A ticked list inside one of those cards. Not <ul>'s own bullets: the tick is a
   drawn icon that takes the accent, and a marker plus an icon is two bullets. */
.eo-ticks { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.eo-ticks li {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: 10px;
    max-width: none;
}
.eo-ticks li > svg { margin-top: .28em; color: var(--accent-2); flex: none; }

/* ---- Page banner -----------------------------------------------------------
   The single decorative image the Format and Study Guide pages open on, both of
   which the live site leads with. Rounded and edged so it reads as a plate on the
   paper rather than as a photograph bleeding to the container's edges, and capped
   in height so a wide screen does not turn a decorative header into the page. */
.eo-banner {
    display: block;
    width: 100%;
    max-height: 340px;
    object-fit: cover;
    border-radius: var(--eo-r);
    border: var(--eo-line) solid var(--eo-edge);
    box-shadow: var(--shadow-sm);
}

/* ---- About: what it costs --------------------------------------------------
   Two column headings on the section's own ground, with a card under each
   holding one sentence — the live page's arrangement, and the right one.

   Two shapes came before it. Two .eo-syl-point cards drew both answers
   identically, so a reader had to read each in full to find out which was
   "free" and which was "you may have to pay" — the one distinction the section
   exists to make. Putting the heading inside the card fixed nothing: the
   headings are long (one names three olympiads) and inside a card the heading
   became the largest thing in it, leaving the sentence it introduces reading as
   a footnote to its own title.

   Out on the ground the headings behave as what they are — a pair of column
   headings — and each card holds one short answer.

   The answers are told apart by colour, not by a label: sky is this palette's
   settled ink (the ticks in the section above, the solved cells in the
   ornament), gold is the one that asks for attention. It lands on the phrase
   upstream itself emphasises, so nothing is invented to carry it.
   ========================================================================== */
.eo-costs {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Two explicit rows so both headings share the first and both cards start at
       the top of the second. A column wrapper each would let a three-line
       heading push its card below its neighbour's. */
    grid-template-rows: auto auto;
    align-items: start;
    gap: clamp(14px, 1.8vw, 20px) clamp(16px, 2.4vw, 30px);
    margin-top: clamp(20px, 3vw, 30px);
}
.eo-cost-title {
    grid-row: 1;
    margin: 0;
    text-align: center;
    font-size: clamp(1.05rem, 1.6vw, 1.28rem);
    line-height: 1.32;
    color: #2b0f28;
    text-wrap: balance;
}
.eo-cost-title.is-free { grid-column: 1; }
.eo-cost-title.is-payable { grid-column: 2; }

.eo-cost {
    grid-row: 2;
    align-self: start;
    padding: clamp(18px, 2.2vw, 26px) clamp(20px, 2.4vw, 30px);
    border: var(--eo-line) solid var(--eo-edge);
    /* the answer, drawn: a band along the top edge in the colour of the answer */
    border-top-width: 4px;
    border-radius: var(--eo-r);
    background: var(--surface);
    box-shadow: var(--eo-drop);
}
.eo-cost.is-free { grid-column: 1; }
.eo-cost.is-payable { grid-column: 2; }
.eo-cost p {
    margin: 0;
    max-width: none;
    text-align: center;
    color: var(--text-soft);
    line-height: 1.62;
}
/* The emphasis upstream puts on the answering phrase, in the colour of that
   answer. This is the whole distinction: no badge, no label, no extra words. */
.eo-cost strong { font-weight: 800; }
.eo-cost.is-free { border-top-color: var(--accent-2); }
.eo-cost.is-free strong { color: var(--accent-2); }
.eo-cost.is-payable { border-top-color: var(--brand); }
/* the gold ink, not the fill gold — #f7b72d is 3.98:1 and this is body text */
.eo-cost.is-payable strong { color: #8a5a06; }

@media (max-width: 760px) {
    /* Stacked, each heading has to sit with its own card, so the two explicit
       rows go and the four children fall into source order — which is already
       heading, card, heading, card. */
    .eo-costs { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
    .eo-cost-title,
    .eo-cost { grid-column: 1; grid-row: auto; }
}

/* ---- About: the gate, and the pair -----------------------------------------
   "Who It Is For, and Why" holds three things that are not the same kind of
   thing, and it used to draw them as three equal cards.

   "Who can participate?" is a RULE — one sentence, and the only fact on the page
   a reader has to check against themselves before the rest of it matters.
   Objectives and Advantages are lists, and they answer a question nobody asks
   until they know they qualify. Equal cards said all three were the same weight,
   and gave a one-line rule the same box as a four-item list, which left the rule
   floating in empty card and the list cramped.

   These classes are this section's own rather than the .eo-syl-point cards
   below — those still serve the Costs section on this page and the syllabus on
   /format-of-question, where three genuinely equal things ARE the content.
   ========================================================================== */

/* The gate. A gold edge down the left rather than a border all round: it is a
   called-out passage, not a card, and a full border would put it back in the
   family this section was taken out of. */
.eo-gate {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: clamp(16px, 2vw, 22px);
    /* Full width, and the sentence inside it runs the full width too. It was
       capped at 62ch for a while, which is a good measure and looked like a
       mistake: the bar is one gold-edged block and two-thirds of it was empty.
       The panel below it is uncapped for the same reason — see .eo-justify. */
    margin: clamp(20px, 3vw, 30px) 0 clamp(26px, 3.4vw, 40px);
    padding: clamp(18px, 2.4vw, 26px) clamp(20px, 2.6vw, 30px);
    border-left: 4px solid var(--brand);
    border-radius: 0 var(--eo-r) var(--eo-r) 0;
    background: #fdf6e6;
}
.eo-gate-mark {
    display: grid;
    place-items: center;
    width: 54px;
    height: 54px;
    border-radius: var(--eo-r);
    color: #fff;
    background: linear-gradient(140deg, #dea425, #a06a0c);
    box-shadow: var(--eo-drop);
}
/* 1.05rem/.08em, the same as .eo-spec-label — these are all the same thing, a
   small caps label over its value, and they were drifting at .76, .8 and .76.
   Uppercase under heavy tracking is the hardest combination to read small, and the
   tracking was compensating for the size rather than the other way round; both were
   raised together so the family stays one decision rather than three. */
.eo-gate-label {
    margin: 0 0 6px;
    font-size: 1.05rem;
    font-weight: 800;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--accent);
}
/* The largest body type in the section, because this is the sentence the whole
   section is gating on. */
.eo-gate-text {
    margin: 0;
    font-size: clamp(1.02rem, 1.2vw, 1.12rem);
    line-height: 1.6;
    color: var(--text);
    font-weight: 600;
}

/* ---- The route: gate → connector → panel -----------------------------------
   The Selection section's heading asks two questions, and this is the pair that
   answers them: the gate ("Who can participate?") and the panel ("How you get
   in"), joined by a short gold connector so the second reads as following from
   the first rather than as a second box.

   The connector is the home page's stages rail, stood up and used once. Same
   two marks — a 2px gold line and a filled tip — because this is the same idea
   in a smaller place: one thing leading to the next. It is aria-hidden and the
   labels either side carry the meaning, so nothing is lost when it is not seen.

   Both halves take the panel's full width. The gate's sentence used to be held
   to 62ch, which left the bar two-thirds full and the rest empty; the panel
   under it now runs edge to edge (see .eo-justify), and a capped gate beside an
   uncapped panel read as a mistake rather than as a measure.
   ========================================================================== */
.eo-route .eo-gate { margin-bottom: 0; }

.eo-route-link {
    position: relative;
    display: grid;
    place-items: center;
    height: clamp(44px, 4.4vw, 58px);
}
/* Fading in from the gate and arriving solid at the tip: the line has a
   direction, and this is the cheapest way to say so on a run this short. */
.eo-route-line {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 2px;
    transform: translateX(-50%);
    background: linear-gradient(180deg, rgba(247, 183, 45, .18), rgba(247, 183, 45, .85));
}
.eo-route-tip {
    position: relative;
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    border-radius: 999px;
    color: #2b0f28;
    background: linear-gradient(135deg, #f7b72d, #dea425);
    border: 1px solid #e3931b;
    box-shadow: var(--eo-drop);
}
/* One arrow drawing in the set, turned. A separate arrow-down case would be the
   same path at a different angle and one more thing to keep in step. */
.eo-route-tip .gx-ico { transform: rotate(90deg); }

/* The panel's own head. Small caps and a hairline, so it names the panel
   without competing with the section title above it. */
.eo-route-head {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 clamp(14px, 1.8vw, 20px);
    padding-bottom: clamp(12px, 1.6vw, 16px);
    border-bottom: 1px solid var(--border);
}
.eo-route-mark {
    flex: none;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    border-radius: var(--eo-r);
    color: var(--accent-2);
    background: rgba(247, 183, 45, .18);
    border: 1px solid rgba(69, 22, 63, .13);
}
/* Matches .eo-gate-label and .eo-spec-label — see the note on the first of them. */
.eo-route-label {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 800;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--accent);
}

/* The IEO mark at the head of the "Malaysia at the IEO" panel.
   ---------------------------------------------------------------------------
   It used to carry .badge-mini, and every .badge-mini rule on this site is
   scoped to .page-hero — which is where the two results pages use it. Inside a
   panel none of them matched, so the file rendered at its intrinsic 947px: the
   full width of the panel, with the paragraph closing up under it.

   Sized here on its own class, and set on a hairline so it reads as the head of
   the panel rather than as a picture that happens to be first. Same rule the
   selection panel's head uses on this page, so the two panels open the same way.

   min(240px, 58%) rather than a flat width: at 240px the wordmark is legible and
   the roundel beside it is about the size of a section eyebrow, and the
   percentage takes over on a phone where 240px would be most of the panel. */
.eo-ieo-head {
    margin: 0 0 clamp(16px, 2vw, 22px);
    padding-bottom: clamp(14px, 1.8vw, 18px);
    border-bottom: 1px solid var(--border);
}
.eo-ieo-mark {
    display: block;
    width: min(240px, 58%);
    height: auto;
}

/* ---- the pair ------------------------------------------------------------
   Two halves of one answer, divided by a hairline rather than by two card
   edges. */
.eo-pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(24px, 3.4vw, 46px);
}
.eo-pair-col + .eo-pair-col {
    padding-left: clamp(24px, 3.4vw, 46px);
    border-left: 1px solid var(--border);
    /* the gap is already between them, so the divider needs the padding back
       rather than a second gap's worth of space */
    margin-left: calc(clamp(24px, 3.4vw, 46px) * -1);
}
.eo-pair-title {
    display: flex;
    align-items: center;
    gap: 9px;
    margin: 0 0 14px;
    font-size: 1.02rem;
    color: #2b0f28;
}
.eo-pair-title > svg { color: var(--accent); flex: none; }

@media (max-width: 760px) {
    /* Stacked, the divider becomes a rule above the second list — a left border
       on a full-width column is a stripe down the page. */
    .eo-pair { grid-template-columns: minmax(0, 1fr); gap: clamp(20px, 3vw, 26px); }
    .eo-pair-col + .eo-pair-col {
        padding: clamp(20px, 3vw, 26px) 0 0;
        margin-left: 0;
        border-left: 0;
        border-top: 1px solid var(--border);
    }
}

/* ---- Downloads: THE SPECIMEN -----------------------------------------------
   Four files, beside a drawing of the paper they are about.

   The sibling sites run a Syllabus here — four claims about what the paper is
   like, which the page then has to be believed on. MyEO publishes the papers
   themselves, one sample for each category, so the claims are unnecessary: a
   student settles the question in thirty seconds by opening the file. The
   layout is inherited and the content is not.

   It also breaks a rhythm that had run too long. By the time a reader reaches
   this section they have passed the quick-facts strip, the objectives, the date
   rail, five plates and five spec cards — every one of them a grid of boxes.
   A fourth grid here would have been the point at which the page stops having
   sections and starts having a texture.

   The drawing comes SECOND in the source and first on screen. The files are the
   content; a phone and a screen reader both get them before the picture.
   ========================================================================== */
.eo-specimen {
    display: grid;
    /* The art column is fixed rather than fractional. It is a drawing at a fixed
       aspect ratio, and giving it a fraction of the row means it grows on a wide
       monitor into the largest thing on the page — which it is not. */
    grid-template-columns: minmax(0, 1fr) 340px;
    align-items: center;
    gap: clamp(28px, 4vw, 56px);
    margin-top: clamp(20px, 3vw, 30px);
}
/* Second in the source, first on screen. */
.eo-specimen-art { grid-column: 2; grid-row: 1; }
.eo-downloads { grid-column: 1; grid-row: 1; }

@media (min-width: 981px) {
    /* The drawing to the LEFT of the files on a wide screen: it is the thing
       they are downloads OF, and English reads the subject before the comment
       on it. */
    .eo-specimen-art { grid-column: 1; }
    .eo-downloads { grid-column: 2; }
    .eo-specimen { grid-template-columns: 340px minmax(0, 1fr); }
}

.eo-specimen-svg {
    display: block;
    width: 100%;
    height: auto;
    /* the leaf sits at a slight angle, the way a sheet lies on a desk — the one
       thing that stops a rectangle of ruled lines reading as a screenshot */
    transform: rotate(-1.6deg);
    filter: drop-shadow(0 18px 34px rgba(43, 15, 40, .16));
}

/* ---- the four downloads ---------------------------------------------------
   Rows, not cards. Four bordered tiles beside a drawing would have made the
   section a grid inside a grid, and these are a LIST — four files a reader
   works down, two of which they will open and two of which they will send to
   somebody else.

   The whole row is the link. A title-only hit area on a row this wide leaves
   most of it dead to the pointer, and the note under each title is exactly the
   thing somebody aims at when they cannot remember which sample is which. */
.eo-downloads .lead { margin-bottom: clamp(16px, 2vw, 22px); }
.eo-dl-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.eo-dl-list li { max-width: none; }
.eo-dl {
    display: grid;
    grid-template-columns: 46px minmax(0, 1fr) auto;
    align-items: center;
    gap: 16px;
    padding: 14px 16px;
    border: var(--eo-line) solid var(--eo-edge);
    border-radius: var(--eo-r);
    background: var(--surface);
    color: inherit;
    text-decoration: none;
    transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease;
}
.eo-dl:hover,
.eo-dl:focus-visible {
    border-color: rgba(247, 183, 45, .75);
    transform: translate(-2px, -2px);
    box-shadow: var(--eo-drop-lift);
}
.eo-dl-ico {
    display: grid;
    place-items: center;
    width: 46px;
    height: 46px;
    border-radius: var(--eo-r);
    color: var(--accent);
    background: rgba(247, 183, 45, .18);
    border: 1px solid rgba(69, 22, 63, .13);
}
.eo-dl-text { display: grid; gap: 2px; min-width: 0; }
.eo-dl-text b { font-weight: 700; color: var(--accent); line-height: 1.35; }
.eo-dl-text span { color: var(--text-soft); font-size: .93rem; line-height: 1.5; }
/* The download mark is not a second link and says nothing the row does not —
   the row is already announced as a link to the file. */
.eo-dl-go { color: var(--accent-2); flex: none; }
.eo-dl:hover .eo-dl-go { color: var(--brand-600); }

@media (prefers-reduced-motion: reduce) {
    .eo-dl { transition: none; }
    .eo-dl:hover, .eo-dl:focus-visible { transform: none; }
}

/* On a phone the mark is smaller, and the row is what it is for.
   ---------------------------------------------------------------------------
   The 46px tile is sized for the desktop row, where it sits beside two lines of
   comfortable width. At 390px the same tile takes an eighth of the row and the
   title beside it — "Sample Questions — Secondary" — is pushed onto a third
   line to get past it. 36px keeps the mark legible and gives the words back the
   width they were losing to it.

   The glyph is set to 22 by the template, which writes width/height ATTRIBUTES
   on the <svg>. CSS beats a presentation attribute, so it can be scaled here
   per breakpoint without the template needing to know about the phone. */
@media (max-width: 640px) {
    .eo-dl {
        grid-template-columns: 36px minmax(0, 1fr) auto;
        gap: 12px;
        padding: 12px 13px;
    }
    .eo-dl-ico {
        width: 36px;
        height: 36px;
        border-radius: var(--eo-r);
    }
    .eo-dl-ico svg { width: 18px; height: 18px; }
    .eo-dl-go svg { width: 16px; height: 16px; }
}

/* Below 980px the drawing goes over the files and comes in off its fixed
   column, capped so it does not become the section. */
@media (max-width: 980px) {
    .eo-specimen { grid-template-columns: minmax(0, 1fr); }
    .eo-specimen-art { grid-column: 1; grid-row: 1; justify-self: center; width: min(300px, 78vw); }
    .eo-downloads { grid-column: 1; grid-row: 2; }
}

/* ---- How to register ------------------------------------------------------
   Sits inside the pricing panel, under the fee cards: the live page puts it
   there, and it is the right place — the question a teacher has after the price
   is "how", not "what next".

   Three columns per row so the button lines up down the block whatever length
   the sentence beside it runs to. Below 720px the button drops under its own
   sentence rather than squeezing the copy into a ribbon. */
.eo-howto { display: grid; gap: 12px; margin-top: clamp(20px, 3vw, 30px); }
.eo-howto-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 16px;
    padding: 14px 18px;
    border: var(--eo-line) solid var(--eo-edge);
    border-radius: var(--eo-r);
    background: var(--surface);
}
.eo-howto-row p { margin: 0; max-width: none; }
.eo-howto-row .btn { flex: none; white-space: nowrap; }
@media (max-width: 720px) {
    .eo-howto-row { grid-template-columns: auto minmax(0, 1fr); }
    .eo-howto-row .btn { grid-column: 1 / -1; justify-self: start; }
}


/* ---- 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. */
.eo-facts {
    position: relative;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    overflow: hidden;
}
.eo-facts::after {
    content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
    background: linear-gradient(135deg, #f7b72d 0%, #0f5578 55%, #5c1c58 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. */
.eo-fact { position: relative; text-align: center; padding: clamp(18px, 2.4vw, 26px) clamp(16px, 1.8vw, 28px); }
.eo-fact + .eo-fact::before {
    content: ""; position: absolute; left: 0; top: 22%; height: 56%; width: 1px;
    background: rgba(69, 22, 63, .16);
}
.eo-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: #5c1c58;
}
/* Its own class rather than `.eo-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. */
.eo-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) {
    .eo-facts { grid-template-columns: repeat(2, 1fr); }
    /* with two per row, cells 1 and 3 start a row and must not carry a divider */
    .eo-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 `.eo-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. */
.eo-card-center { text-align: center; }
.eo-card-center p,
.eo-card-center .prose,
.eo-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. */
.eo-card-center .prose :is(ul, ol) {
    width: fit-content;
    max-width: 100%;
    margin-inline: auto;
    text-align: left;
}

/* ---- Quick links -----------------------------------------------------------
   The onward routes: three above the home page's closing CTA, two under the
   registration procedure.

   ROUTES, NOT BUTTONS. Each card used to be a centred sentence over a filled
   gold pill. On the home page that put three saturated buttons directly above
   the closing CTA — the one thing the whole page is asking for — and the real
   button lost its place among three copies of itself. The words are unchanged;
   only the drawing is. One gold button is left on that screen and it is the one
   that matters.

   The <a> is the card, not the pill inside it, so the tap target is the card:
   the pills measured 182×34 and left most of a card dead to a thumb.

   THREE ACROSS WHEN THERE ARE THREE. Two-up left the third centred under the
   pair, which is the tidiest version of a broken row and still a broken row.
   :has() rather than a modifier class, because how many cards there are is a
   fact about the markup and not something a template should have to declare
   twice; a browser without :has() falls back to exactly the two-up-with-a-
   spanning-third this section had before.
   ========================================================================== */
.eo-links {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(12px, 1.4vw, 16px);
}
/* An odd one out takes the row rather than half of it. In the three-up state
   below this is turned back off — there is no odd one out there. */
.eo-links > :last-child:nth-child(odd) { grid-column: 1 / -1; }

@media (min-width: 941px) {
    .eo-links:has(> :nth-child(3)) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .eo-links:has(> :nth-child(3)) > :last-child:nth-child(odd) { grid-column: auto; }
}

@media (max-width: 700px) {
    .eo-links { grid-template-columns: minmax(0, 1fr); }
}

/* The card. Left-aligned, not centred: three centred columns of two-line
   headings read as a poster, and a list of places to go reads as a list. */
.eo-link {
    /* Tighter than .gx-pad's clamp(24px, 3.4vw, 44px). That figure is set for a
       panel holding paragraphs; these cards hold a mark, two lines and a route,
       and at 44px the content sat in a pool of its own card. */
    padding: clamp(16px, 1.7vw, 21px) clamp(18px, 2vw, 24px) clamp(15px, 1.6vw, 20px);
    /* THE MARK SITS BESIDE THE TEXT, not above it. Stacked, the icon took a
       whole row of its own — 42px plus its gap — on a card whose content is two
       lines and a link, which is most of a third line of height spent on
       furniture. Beside the heading it costs nothing but the column it occupies,
       and that column was empty space in the stacked version anyway.

       The mark spans both rows and ranges TOP, so it sits on the heading's first
       line rather than floating between the two blocks. Row 2 is 1fr and the
       route ranges to its end, which is what keeps the three routes on one line
       across cards of unequal heading length. */
    display: grid;
    grid-template-columns: 42px minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    align-items: start;
    column-gap: 14px;
    row-gap: 10px;
    text-align: left;
    text-decoration: none;
    color: inherit;
    transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
}
.eo-link:hover,
.eo-link:focus-visible {
    transform: translate(-2px, -2px);
    border-color: var(--border-strong);
    box-shadow: var(--eo-drop-lift);
}

/* The mark, on the same tinted tile the spec cards and the route panel use, so
   a reader meets one kind of small mark on this site rather than three. */
.eo-link-ico {
    grid-column: 1;
    grid-row: 1 / span 2;
    display: grid;
    place-items: center;
    width: 42px;
    height: 42px;
    border-radius: var(--eo-r);
    color: var(--accent-2);
    background: rgba(247, 183, 45, .18);
    border: 1px solid rgba(69, 22, 63, .13);
}

.eo-link-title {
    grid-column: 2;
    grid-row: 1;
    margin: 0;
    font-size: var(--fs-h4);
    line-height: 1.3;
    color: var(--accent);
    text-wrap: balance;
}

/* align-self:end on a 1fr row, so the route sits on the foot of the card
   whatever the heading above it does — three cards of unequal heading length
   still line their destinations up with each other.

   It stays in the TEXT column rather than returning to the card's own edge: the
   mark, the heading and the route then share one left edge, and the card reads
   as one block instead of as a mark with two things loose beside it. */
.eo-link-go {
    grid-column: 2;
    grid-row: 2;
    align-self: end;
    justify-self: start;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding-top: 2px;
    font-size: .93rem;
    font-weight: 800;
    color: var(--link);
}
.eo-link:hover .eo-link-go,
.eo-link:focus-visible .eo-link-go { text-decoration: underline; }
.eo-link-arrow { transition: transform .25s ease; }
.eo-link:hover .eo-link-arrow { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
    .eo-link, .eo-link-arrow { transition: none; }
    .eo-link:hover, .eo-link:focus-visible { transform: none; }
    .eo-link:hover .eo-link-arrow { transform: 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, #e3931b, #5c1c58);
    -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.eo-addr` on purpose: `.tile p` above is (0,1,1), so a bare
   `.eo-addr` at (0,1,0) would lose to it and keep inheriting `pretty`. */
.tile p.eo-addr { text-wrap: balance; }

/* ---- An ordered run of named things, set as a legend -----------------------
   Used by the five stages on /about-us.

   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. */

.eo-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;
}
.eo-legend > li { max-width: none; break-inside: avoid; }
.eo-legend-entry {
    display: grid;
    grid-template-columns: 34px minmax(0, 1fr);
    align-items: start;
    gap: 12px;
    padding: 14px 0;
    border-top: 1px solid rgba(69, 22, 63, .13);
}
.eo-legend-key {
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: #fdfdfe;
    border: 2px solid #5c1c58;
    color: #5c1c58;
    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;
}
.eo-legend-name {
    margin: 0 0 .15em;
    font-size: 1rem;
    line-height: 1.3;
    color: #2b0f28;
}
.eo-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. */
    .eo-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. */

.eo-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;
}
.eo-index > li { max-width: none; break-inside: avoid; }
.eo-index-entry {
    padding: 13px 0;
    border-top: 1px solid rgba(69, 22, 63, .13);
}
.eo-index-topic {
    margin: 0 0 .2em;
    font-size: .98rem;
    line-height: 1.3;
    color: #2b0f28;
    /* 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 eo-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. */
.eo-index-topic { padding-left: 32px; }
.eo-index-num {
    display: inline-block;
    width: 32px;
    margin-left: -32px;
    color: #0f5578;
    font-weight: 700;
    /* tabular figures so 01 and 12 occupy the same width */
    font-variant-numeric: tabular-nums;
}
.eo-index-note {
    margin: 0;
    padding-left: 32px;
    font-size: .9rem;
    line-height: 1.5;
    color: var(--text-soft);
}

@media (min-width: 820px) {
    .eo-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: var(--eo-r); 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 (study guide, 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: var(--eo-line) solid var(--eo-edge);
    border-radius: var(--eo-r);
    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: #e3931b; }
.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. */
.eo-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. */
.eo-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: var(--eo-line) solid var(--eo-edge-strong);
    border-radius: var(--eo-r);
    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. */
.eo-note {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin: 22px 0 0;
    padding: 13px 16px;
    border: 1px solid rgba(122, 42, 118, .22);
    border-left: 4px solid #7a2a76;
    border-radius: var(--eo-r);
    background: rgba(201, 166, 208, .15);
    color: #5c1c58;
    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;
}
.eo-note .gx-ico { flex: 0 0 auto; color: #7a2a76; 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 ------------------------------------------------
   Two sections on /about-us that differ only by a year need a heading you can
   identify at a glance while scrolling, not three lines of navy and a line of
   grey.

   Two things do the work, and neither adds a word of content:
     - the heading itself, larger and set tighter than a normal section title,
       so it reads as a lockup rather than as a sentence that wrapped;
     - the year in the heading in the brand gradient, picked out of the navy,
       with the host city under it as a pin on a chip because it is a place.

   The sibling sites have a third: the year again, set enormous and nearly
   transparent behind the block. It is not here — see the note further down. */
/* max-width: none, because .eo-head caps every section head at 720px. That is
   right for a two-word title over a centred lead, and it is what broke these
   headings across an extra line — they need about 850px at this size and the
   panel has 944, so the only thing standing between them and one fewer line was
   the shared cap. */
/* .eo-head.eo-year-head, not .eo-year-head: both are one class, so they tie
   on specificity and .eo-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. */
.eo-head.eo-year-head {
    position: relative;
    max-width: none;
    padding-top: clamp(8px, 1.6vw, 22px);
    /* THE GAP TO THE CITY CHIP IS SET HERE, and it has to be: .eo-head is a
       column flex container, so the space between the heading and the chip is
       its `gap`, not a margin on either of them.

       .eo-head .section-title sets `margin: 0` about 800 lines below this at
       the same specificity as .eo-year-head .section-title — so a margin-bottom
       written there loses on source order and does nothing at all. It was tuned
       three times before anyone measured the rendered pixels and found the gap
       had not moved once.

       Two classes here, so this is (0,2,0) and beats .eo-head`s (0,1,0)
       whatever the order. 22px against the shared 6px: this heading runs at
       3.15rem with descenders that drop a long way, and the chip below it has a
       border they were landing on. */
    gap: 22px;
}

/* No divider under these two.

   Every other section head on the site closes with the bar, and it earns its
   place there because the heading is the last thing in the block. These two are
   not: the host-city chip sits under the heading, and it is already a self-
   contained object on the same centre line. A bar beneath it made three marks
   stacked one above another — title, chip, bar — which reads as a heading that
   could not decide where it ended.

   The chip closes the block on its own. Written as `content: none` rather than
   by taking .eo-head off the element, because the rest of that class — the
   centring, the spacing, the eyebrow rules — is exactly what these blocks want.

   BOTH classes in the selector, for the reason spelled out at the top of this
   block: .eo-year-head alone is (0,1,0) and ties with .eo-head, which defines
   ::after about 1,500 lines further down and therefore wins on order. Written
   as one class this rule looked correct and did nothing. */
.eo-head.eo-year-head::after { content: none; }

/* NO WATERMARK, and this note is here so nobody adds one back.

   The sibling Geography site draws the year enormous and nearly transparent
   behind this block, via a data-year attribute. It works there because its
   headings are short — "Malaysian Team at iGeo 2025" sets on two lines and
   leaves the numeral clean space to sit in.

   MyEO’s are not. "Malaysian Team in the International Economics Olympiad
   (IEO) 2025" runs to three lines and fills the block, so the numeral had
   nowhere to go but directly behind the words — and at any opacity dark enough
   to see, it read as a rendering fault rather than as texture.

   Dropping it costs nothing the section needs. The year is already picked out
   of the heading in the brand gradient, which is the same emphasis stated once
   instead of twice. */
/* 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. */
.eo-year-head .section-title {
    font-size: clamp(1.9rem, 4.4vw, 3.15rem);
    /* 1.15, not the 1.04 this was set at.

       1.04 is tight enough that the descenders of one line land in the space the
       next line’s capitals rise through — the y of "Malaysian" against the E of
       "Economics" beneath it. It reads as two lines fighting rather than as a
       lockup, and no amount of margin under the block fixes it, because the
       collision is INSIDE the heading.

       Still tight by body-copy standards: at this size a heading wants less
       leading than a paragraph, and 1.15 keeps the two lines reading as one
       object while giving the descenders somewhere to go. */
    line-height: 1.15;
    letter-spacing: -.015em;
    /* No margin-bottom here. It would be dead: .eo-head .section-title sets
       `margin: 0` further down this file at the same specificity, and the space
       under this heading is .eo-head`s flex `gap` — set on
       .eo-head.eo-year-head above. */
}
/* 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`. */
.eo-year-head .section-title .grad {
    background: linear-gradient(96deg, #fec23f 0%, #f7b72d 45%, #e3931b 100%);
    background-clip: text;
    -webkit-background-clip: text;
}
.eo-place {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin: 0;
    padding: 5px 14px 5px 11px;
    border-radius: 999px;
    border: 1px solid rgba(69, 22, 63, .18);
    background: #fdfdfe;
    box-shadow: 0 1px 0 rgba(255, 255, 255, .8) inset;
    color: #5c1c58;
    font-size: .92rem;
    font-weight: 600;
    letter-spacing: .01em;
}
.eo-place .gx-ico { color: #f7b72d; 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. */
.eo-place-flag {
    flex: 0 0 auto;
    height: 1.05em;
    width: auto;
    margin-left: 3px;
    border-radius: 2px;
    box-shadow: 0 0 0 1px rgba(69, 22, 63, .25);
}


/* 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: var(--eo-r);
    overflow: hidden;
    box-shadow: var(--eo-drop);
}
.video-stage iframe {
    display: block;
    width: 100%;
    height: 100%;
    aspect-ratio: 16 / 9;
    border: 0;
    background: #12171a;
}

.video-side {
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: linear-gradient(170deg, #2b0f28 0%, #1b0819 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(247, 183, 45, .16);
    border: 1px solid rgba(247, 183, 45, .42);
    color: #d6def2;
    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(247, 183, 45, .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: var(--eo-r);
    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(247, 183, 45, .17);
    border-left-color: #f7b72d;
}
.video-thumb {
    display: block;
    width: 92px;
    height: 52px;
    object-fit: cover;
    border-radius: var(--eo-r-sm);
}
.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: var(--eo-r); }
@media (max-width: 640px) { .map-embed { aspect-ratio: 4 / 3; } }

/* ============================================================================
   HOME HERO — THE RULED PAGE
   ----------------------------------------------------------------------------
   Five ruled bands stacked behind a two-column split (copy left, brand mark
   right). The bands are gradients rather than an image, so they scale to any
   viewport and cost nothing to load.

   They are the ruling of a page before anything is written on it: the faint
   guide bands a scribe lays down, palest at the head where the text will sit and
   deepening toward the foot where the leaf falls into shadow. The ruled baseline
   at the very bottom (.eo-hero::after, in the glyph layer below) is the one line
   that HAS been written on — so the hero reads as a page in the middle of being
   filled in, which is the state every problem in this competition is in.

   It replaces the sibling Geography site's hypsometric relief, which drew the
   same five slabs as elevation tints: the right motif for that competition, and
   the wrong one here.
   ========================================================================== */

.eo-hero {
    position: relative;
    isolation: isolate;
    overflow: hidden;
    /* Desktop only. Both clamps keep their mobile minimums, so a phone renders
       exactly as it did; what changed is the caps, which were set before the stats
       bar moved under the hero. At 1440x900 — the commonest laptop — 68/80px here
       plus .section--tight's 40px pushed that bar half off the bottom of the
       screen, so the first thing a visitor saw ended on a sliced card with nothing
       to signal there was more page. 44/40 here and 28 there brings the whole bar
       above the fold and lets the next section's eyebrow show under it. About
       72px, all of it padding — copy, badge and type scale are untouched. */
    padding: clamp(28px, 5vw, 44px) 0 clamp(34px, 6vw, 40px);
    margin-bottom: clamp(10px, 2vw, 24px);
}

/* The ruled bands. Each is a horizontal slab with a slight tilt — the tilt is
   what makes them read as a sheet lying on a desk rather than as CSS stripes.

   The ramp deepens DOWNWARD, which is a legibility choice rather than a
   decorative one: 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
   deepest land in the empty space at the foot, under the written line.

   Every band is a wash — .06 to .22 alpha — so the whole ramp reads as tinted
   paper rather than as five stripes. */
.eo-hero-relief { position: absolute; inset: 0; z-index: -1; }
.eo-hero-relief span {
    position: absolute;
    left: -8%; right: -8%;
    transform: rotate(-1.6deg);
    border-radius: 40%;
}
/* the head margin — the strip above the first ruled line, left empty on a page */
.eo-hero-relief span:nth-child(1) { top: 0;   height: 20%; background: linear-gradient(180deg, rgba(196, 166, 203,.22), rgba(196, 166, 203,0)); }
/* first ruling */
.eo-hero-relief span:nth-child(2) { top: 16%; height: 26%; background: linear-gradient(180deg, rgba(233, 220, 239,.60), rgba(233, 220, 239,0)); }
/* second ruling */
.eo-hero-relief span:nth-child(3) { top: 40%; height: 24%; background: linear-gradient(180deg, rgba(221, 201, 219,.46), rgba(221, 201, 219,.06)); }
/* third ruling */
.eo-hero-relief span:nth-child(4) { top: 62%; height: 22%; background: linear-gradient(180deg, rgba(154, 33, 131,.16), rgba(154, 33, 131,.03)); }
/* the foot of the leaf, where the sheet falls into shadow */
.eo-hero-relief span:nth-child(5) { top: 80%; height: 26%; background: linear-gradient(180deg, rgba(106, 33, 104,.22), rgba(106, 33, 104,.02)); }

.eo-hero-grid {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    gap: clamp(24px, 4vw, 56px);
    align-items: center;
}

/* The hero lockup is TWO lines, and the type is sized to keep it that way.
   ---------------------------------------------------------------------------
   The <h1> carries a hard <br> before "Competition", so the intended shape is
   "Economics Explorer" over "Competition". Anything that makes the first line
   wrap turns it into three, with "Explorer" stranded in the middle.

   MEASURED, not estimated — and the estimate is what got this wrong twice. The
   value below is the one number the whole rule turns on:

       "Economics Explorer" renders 9.86em wide in this face.

   That was taken off the page with the harness named at the foot of this note,
   at a width where the run actually fits on one line — measure it where it has
   already wrapped and you get the width of "Economics" alone, which is how the
   first estimate came out a third too small. Guessing it at 9.72em put the
   ceiling 0.3px over the column: invisible in a screenshot, and it wraps every
   time.

   The copy column is 1.05fr of a two-column grid inside a 1180px container, so
   it settles at 565px once the container stops growing, and narrows with the
   viewport below that. The type has to clear col / 9.86 at every width:

       viewport   column   max font    set to
       1300+       565px    57.3px      56.0px  (3.5rem ceiling)
       1150        547px    55.5px      52.9px
       1000        471px    47.8px      46.0px
        901        421px    42.7px      41.4px

   Hence clamp(2rem, 4.6vw, 3.5rem). The vw coefficient is set by the 901–1150
   band, which is the tightest — 4.74vw is the true limit there and 4.6 leaves a
   margin for a font that loads slightly wider than the fallback.

   Below ~900px the grid collapses to one column and the <br> is hidden, so the
   title wraps freely — but the 2rem floor still keeps "Economics Explorer"
   whole down to a 375px phone, where the column is 324px and 2rem needs 316.

   At 320px it does wrap, and that is left alone: the column is 269px there, so
   holding the line would need about 1.7rem, which is no longer a hero. 375 is
   the narrowest width the runtime audit tests and the narrowest phone still in
   use.

   So: if this face is ever changed, re-measure with the harness. A wider one
   breaks the lockup again and it will look like a wrapping bug rather than a
   font change. The harness lives at docs/measure-hero.html and has to be copied
   into public/ to run, because it reads into an iframe and needs same-origin. */
.eo-hero-copy h1 { font-size: clamp(2rem, 4.6vw, 3.5rem); margin: 10px 0 14px; line-height: 1.06; }
.eo-hero-copy .lead { margin: 0 0 22px; max-width: 52ch; }
.eo-hero-copy .hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 0; }

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

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

@media (max-width: 900px) {
    .eo-hero-grid { grid-template-columns: minmax(0, 1fr); text-align: center; }
    .eo-hero-copy .lead { margin-inline: auto; }
    .eo-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. */
    .eo-hero-copy h1 br { display: none; }
    /* art first would push the copy below the fold on a phone */
    .eo-hero-art { order: 2; max-width: 460px; margin: 0 auto; }
}

/* Motion is decorative here — none of it conveys information. */
@media (prefers-reduced-motion: reduce) {
    .eo-hero-mark { 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(69, 22, 63, .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-eo-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-fit is how many columns the VIEWPORT can take; --pg-max is how many
       the gallery has photographs for, set inline by x-eo-photo-grid. The grid
       uses whichever is smaller, so a two-photograph gallery lays out as two
       half-width tiles instead of two quarter-width ones stranded in the middle
       of a four-column row — and still collapses to one column on a phone,
       because --pg-fit wins there. */
    --pg-fit: 4;
    --pg-cols: min(var(--pg-fit), var(--pg-max, 99));
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--pg-gap);
    margin-top: 26px;
}
/* `> *`, not `figure`.
   ---------------------------------------------------------------------------
   This said `.photo-grid figure` and matched NOTHING on this site: the tiles are
   <a class="ph-link">, because every one of them opens the lightbox. So the
   column arithmetic below never applied, the flex items fell back to sizing on
   their images' intrinsic widths, and the wall has been laying out as ragged
   centred rows of three this whole time rather than as the four-column grid the
   variable names describe. It looks deliberate enough that nobody questioned it
   until the same grid was reused under the write-ups with eight photographs and
   two, where three uneven rows and a stranded pair are unmistakable.

   The selector is left loose on purpose — a tile that is not a link (a figure
   with a visible caption, say) should land in the same column. */
.photo-grid > * {
    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-fit: 3; } }
@media (max-width:  830px) { .photo-grid { --pg-fit: 2; } }
@media (max-width:  550px) { .photo-grid { --pg-fit: 1; } }
.photo-grid img {
    width: 100%; height: 190px; object-fit: cover;
    border-radius: var(--eo-r);
    border: var(--eo-line) solid var(--eo-edge);
    transition: transform .25s, box-shadow .25s;
}
.photo-grid > *:hover img { transform: translateY(-3px); box-shadow: var(--shadow); }
/* One or two photographs are not a wall, and cropping them to the wall's 190px
   band makes them 2.4:1 strips — which is what the pair under the 2023 write-up
   looked like, two letterboxes where the live site has two photographs. The
   fixed height exists so that rows of thumbnails line up with each other; with
   one row of two there is nothing to line up with. */
.photo-grid--few img { height: auto; }

/* 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-eo-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-max: 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 > * { 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: var(--eo-r); }
.ph-link:focus-visible { outline: 2px solid #e3931b; 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: var(--eo-r);
    box-shadow: var(--eo-drop);
}
.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, #f7b72d, #dea425);
    border-color: #e3931b;
    color: #2b0f28;
}
.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. Index navy rather than the gold used for
   invitations, because it narrows who something is for rather than inviting. */
.eo-badge {
    display: inline-block;
    margin-left: 8px;
    padding: 5px 12px;
    border-radius: 99px;
    background: #5c1c58;
    color: #dde5fb;
    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, 24, 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 → ledger blue →
   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, #f7b72d 0%, #0f5578 55%, #5c1c58 100%);
    box-shadow: var(--eo-drop);
}
.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(247, 183, 45, .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: #f7b72d;
    color: #2b0f28;
    box-shadow: var(--eo-drop);
    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: #f7b72d; border-color: rgba(255, 255, 255, .85); }
[data-theme="warm"] .popup-close:hover { background: #fdce6a; 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 page-setting line: sections alternate
   between an open "surface" treatment and a tinted "seam" band, separated by a
   ruled baseline. The page then reads as a run of leaves rather than one long
   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. */
.eo-band {
    position: relative;
    background: linear-gradient(180deg, rgba(221, 201, 219, .34) 0%, rgba(250, 248, 252, .55) 100%);
    border-block: 1px solid rgba(69, 22, 63, .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,
   a gold hairline top and bottom, and a seal 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 plum, 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. */
.eo-band--deep {
    background:
        var(--eo-coin-dark),
        var(--eo-coin-dark),
        radial-gradient(120% 90% at 10% -12%, rgba(247, 183, 45, .30), transparent 58%),
        radial-gradient(110% 90% at 92% 6%, rgba(154, 33, 131, .14), transparent 55%),
        linear-gradient(180deg, rgba(247, 183, 45, .22) 0%, rgba(106, 33, 104, .14) 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 .eo-band's border-block
       shorthand and leaves its 1px/solid alone */
    border-block-color: rgba(222, 164, 37, .40);
}

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

/* Centred on the page field, above the panel — the sibling sites' arrangement. */
.eo-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;
}
.eo-head .section-title { margin: 0; }

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

/* ---- Stages: THE NUMBERED RAIL ----------------------------------------------
   Four cards on one row, all standing level, strung on a connector rail: a line
   across the top of the row with a numbered node above each card and a chevron
   in each gap pointing on to the next.

   THE RAIL IS WHAT CARRIES THE ORDER, which is why the numeral sits on it rather
   than inside the card. Left to right is stage 1 to stage 4, so the sequence and
   the reading run the same way — the whole reason this is columns and not rows.

   THE CARDS ARE LEVEL. They were a staircase for months: each card lifted 26px
   above the one before it, the MyEO logo's rising bars turned into a layout.
   Read on the page rather than described, four cards at four different heights
   look like four misaligned boxes, not a climb — a stagger only reads as a
   staircase when the treads are close enough to join, and these are 280px apart.
   It also cost the columns width they could not spare: "Malaysian Economics
   Olympiad (MyEO)" is the longest thing in the section and had to fit whatever
   the ascent left. The rail states the sequence more plainly than the ascent
   did, and takes nothing from the cards to do it.

   NOTHING HERE IS MEASURED. The rail is a connector, not a scale. The chip on
   each card is the only figure in the section: Top 10%, 15–20, 5, 5 — a funnel
   that stops narrowing at stage 3, and so a poor thing to draw. Same division of
   labour the Awards podium makes: the drawing gives the shape, the number gives
   the truth.

   Two earlier arrangements are worth naming, because both are the obvious thing
   to try again.

   FULL-WIDTH ROWS, a tinted number plate down the left. Widest measure of any
   option, and what this section was for months. It reads as a list rather than
   as a journey, and on a page that already runs a date rail, an awards podium
   and a fee row, a fourth stack of full-width rows was the third time the same
   shape had been used.

   A THREAD WITH MEASURED BARS showed the narrowing and cost too much for it. The
   bars had to be invented, and once invented a horizontal bar under a heading
   reads as a progress bar — something the visitor is part-way through — which is
   the wrong idea entirely.
   ========================================================================== */
.eo-steps {
    /* Named, because the rail's connectors bridge the gap with negative offsets
       of exactly half of it and have to stay in step with the grid. */
    --eo-step-gap: clamp(12px, 1.4vw, 20px);

    list-style: none;
    margin: clamp(28px, 3.6vw, 44px) 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    /* stretch, so the four cards are one height as well as one baseline: the
       row squares off top and bottom and the rail runs dead straight. */
    align-items: stretch;
    /* …and 1fr rows, so every ROW is the height of the tallest one too.

       stretch alone equalises cards WITHIN a row and says nothing across rows,
       which is invisible at four-up where there is only one row and plain to see
       between 640px and 1120px where the run wraps to two. There, card 1 has
       three lines of bullets and card 3 has two, so the first row stood taller
       than the second and four bottoms became two pairs of bottoms.

       Not applied on a phone: at one card per row this would stretch every card
       to the height of the longest, and card 4 carries a single bullet. See the
       breakpoint at the foot of this block. */
    grid-auto-rows: 1fr;
    gap: var(--eo-step-gap);
}
/* Belt and braces on the flex chain the card height hangs off, so an equal row
   cannot be undone by a step that failed to fill it. */
.eo-step { height: 100%; }

/* A sheet of paper under the row, so the cards have an edge to be seen against.
   ---------------------------------------------------------------------------
   The page ground carries the figure scatter and the drifting canvas marks, and
   under a row of four outlined cards that texture runs right up to every edge —
   a 2px plum outline on a ground with plum plot marks behind it has to compete
   to be read as an edge at all.

   A radial wash rather than a panel: it is opaque enough under the cards to mute
   the pattern and fades to nothing before it reaches a boundary, so the section
   gains contrast without gaining a box. Inset negatively so it clears the cast
   on the bottom-right of the last card and the badge rail above the first.

   z-index and the stacking context: .eo-step is already position:relative, so
   the cards sit above this without needing one of their own. */
.eo-steps { position: relative; }
/* A sheet of paper under the whole row, so the cards have an edge to be seen
   against.
   ---------------------------------------------------------------------------
   The page ground carries the figure scatter and the drifting canvas marks, and
   under a row of outlined cards that texture runs right up to every edge — a 2px
   plum outline with plum plot marks behind it competes to be read as an edge at
   all.

   UNIFORM, not a radial fade. The first attempt at this was a radial wash that
   fell to zero before the boundary, which looked tidier and left the pattern at
   full strength under the outer half of the first and last cards — exactly the
   edges that needed the help most. .82 across the whole rectangle takes the
   pattern behind the row down by more than half everywhere, which is the point
   of it, and the corners are rounded so it reads as a sheet rather than a band. */
.eo-steps::before {
    content: "";
    position: absolute;
    inset: -26px -22px -18px;
    z-index: 0;
    border-radius: 18px;
    background: rgba(253, 251, 254, .82);
    pointer-events: none;
}

/* The column: rail on top, card below, card taking whatever height is left so
   all four bottom edges line up. */
.eo-step {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* ---- The rail ------------------------------------------------------------ */
.eo-step-rail {
    position: relative;
    display: grid;
    place-items: center;
    height: 34px;
    margin-bottom: 12px;
}

/* The connector. Each column draws its own segment out to the middle of the gap
   on either side, so neighbouring segments meet and the four read as one line.
   The run deepens with --tone, left to right. */
.eo-step-rail::before {
    content: "";
    position: absolute;
    top: 50%;
    left: calc(var(--eo-step-gap) / -2);
    right: calc(var(--eo-step-gap) / -2);
    height: 2px;
    transform: translateY(-50%);
    background: rgba(247, 183, 45, calc(.42 + var(--tone) * .14));
}
/* The line starts at the first node and ends at the last: no stub hanging off
   either end of the row. */
.eo-step:first-child .eo-step-rail::before { left: 50%; }
.eo-step:last-child  .eo-step-rail::before { right: 50%; }

/* The chevron sitting in each gap, on the junction of two segments. It is the
   one mark that says which way the run goes without relying on the numerals. */
.eo-step-rail::after {
    content: "";
    position: absolute;
    top: 50%;
    right: calc(var(--eo-step-gap) / -2);
    width: 6px;
    height: 6px;
    border-top: 2px solid rgba(247, 183, 45, calc(.5 + var(--tone) * .14));
    border-right: 2px solid rgba(247, 183, 45, calc(.5 + var(--tone) * .14));
    transform: translate(50%, -50%) rotate(45deg);
}
.eo-step:last-child .eo-step-rail::after { content: none; }

/* The node. A pill rather than a disc, because "01" is two figures wide and a
   disc big enough to hold them would out-weigh the card beneath it. */
.eo-step-node {
    position: relative;
    z-index: 1;
    display: grid;
    place-items: center;
    min-width: 46px;
    height: 30px;
    padding: 0 12px;
    border-radius: 999px;
    /* The badge ramp: paper, then two deepening lilacs, then the gold on the
       arrival (set with .is-goal below). Four steps of one material rather than
       three of one and one of another — same argument as the cards themselves. */
    background: rgba(226, 214, 232, calc((var(--tone) - 1) * .34));
    border: var(--eo-line) solid rgba(69, 22, 63, calc(.16 + var(--tone) * .11));
    box-shadow: var(--eo-drop-sm);
    font-family: var(--font-display);
    font-size: 1rem;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -.01em;
    /* The numeral deepens with the tone as well, so a reader scanning only the
       numbers still sees the run go somewhere. Bottom of the ramp is .72 alpha
       of the plum on paper, which is 7.4:1 — clear of AA for text this size. */
    color: rgba(69, 22, 63, calc(.68 + var(--tone) * .08));
}

/* ---- The card ------------------------------------------------------------
   FOUR CARDS OF ONE KIND, and that is the change here.

   The fourth used to be a solid plum card with light text while the other three
   were paper. It read as a different component that had been dropped into the
   row rather than as the last step of one sequence — the run stopped being four
   of a thing and became three-plus-one. The note that used to sit here argued
   the arrival "has to end on something other than a bit more of the same", and
   that was right about the goal and wrong about the means: a different FILL
   makes a different object, where a deeper EDGE makes the same object, further
   on.

   So every card now takes the same paper, the same radius and the same cast, and
   the run is carried by intensity instead:

       edge     .30 → .66 alpha, deepening with --tone
       cast     .10 → .22 alpha, so the later cards sit heavier on the page
       tread    the gold rule already deepened; unchanged
       node     white → lilac → gold, the badge ramp below
       chip     pale lilac → gold on the arrival

   Four ramps all moving the same way, which is what makes the fourth card read
   as the end of a sequence rather than as an exception to one.

   AA COMES FREE FROM THIS. The old arrangement needed its own light-ink rules
   for the name, the bullets and the chip because they sat on a dark fill; every
   one of those was a place the contrast could be got wrong later. On paper the
   ink is the site's ordinary ink — plum at 11.9:1 and --text-soft at 7.8:1 — and
   the only pairing left to check is the gold node and chip, which carry deep
   plum at 9.8:1, the same pairing as the primary button. */
.eo-step-card {
    flex: 1;
    display: flex;
    flex-direction: column;
    /* calc on the alpha, so the four cards are one rule and cannot drift apart */
    border: var(--eo-line) solid rgba(69, 22, 63, calc(.18 + var(--tone) * .12));
    border-radius: var(--eo-r);
    background: var(--surface);
    box-shadow: 4px 4px 0 rgba(69, 22, 63, calc(.06 + var(--tone) * .04));
    overflow: hidden;
    transition: transform .25s ease, box-shadow .25s ease;
}
.eo-step:hover .eo-step-card {
    transform: translate(-2px, -2px);
    box-shadow: 6px 6px 0 rgba(69, 22, 63, calc(.12 + var(--tone) * .05));
}

/* The tread: a rule across the top of each card, deepening down the run. It ties
   the cards back to the rail above them, and it is drawn in the gold because the
   gold is what the logo's bars are. */
.eo-step-card::before {
    content: "";
    display: block;
    height: 4px;
    background: linear-gradient(90deg,
        rgba(247, 183, 45, calc(.35 + var(--tone) * .16)),
        rgba(222, 164, 37, calc(.45 + var(--tone) * .14)));
}

/* Mark and figure on one row across the head of the card. The numeral is on the
   rail, so this row is two items wide instead of three and the name below it
   gets the full column. */
.eo-step-plate {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: clamp(13px, 1.5vw, 16px) clamp(14px, 1.6vw, 18px) 0;
}
.eo-step-ico {
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border-radius: var(--eo-r);
    color: var(--accent-2);
    background: rgba(247, 183, 45, .14);
    border: 1px solid rgba(69, 22, 63, .1);
}
/* The chip ranges right on its own row-end, away from the mark. */
/* The chip ramps with everything else: pale lilac deepening toward the arrival,
   where .is-goal takes it to gold. */
.eo-step-count {
    margin-left: auto;
    padding: 4px 10px;
    border-radius: 999px;
    background: rgba(226, 214, 232, calc(.34 + var(--tone) * .16));
    border: 1px solid rgba(69, 22, 63, calc(.14 + var(--tone) * .07));
    font-size: .78rem;
    font-weight: 700;
    white-space: nowrap;
    color: #5c1c58;
}

.eo-step-main {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: clamp(10px, 1.2vw, 13px) clamp(14px, 1.6vw, 18px) clamp(16px, 1.8vw, 20px);
    min-width: 0;
}
.eo-step-name {
    margin: 0;
    min-width: 0;
    font-size: clamp(.98rem, 1.15vw, 1.06rem);
    line-height: 1.3;
    color: #2b0f28;
    text-wrap: balance;
}
.eo-step-body ul { margin: 0; padding-left: 16px; }
.eo-step-body li {
    max-width: none;
    margin-bottom: 5px;
    font-size: .9rem;
    line-height: 1.5;
    color: var(--text-soft);
}
.eo-step-body li:last-child { margin-bottom: 0; }

/* The arrival, marked by intensity rather than by a different fill.
   ---------------------------------------------------------------------------
   Everything the goal card used to override — the fill, the light name, the
   light bullets — is gone, because it is the same card as the other three now.
   What is left is the ramp arriving: the gold tread thickens, and the badge and
   the chip take the gold that the run has been deepening toward.

   Gold is the right end for the ramp rather than a deeper plum, and not only
   because it is the brand's emphasis colour: the node above this card is where
   the connector rail terminates, and the rail is gold. The run ends on the
   colour it was drawn in.

   BOTH GOLD PAIRINGS CARRY DEEP PLUM at 9.8:1 — the same pairing as the primary
   button, and the one contrast.php checks by name. Nothing here needs a light-ink
   rule of its own, which is the point: the card is paper like its neighbours. */
.eo-step.is-goal .eo-step-card::before { height: 6px; }
/* The optional gold cast the brief allows. It replaces the plum cast rather
   than adding to it, so card 4 still has exactly one shadow like its
   neighbours — a second one would make it a different object again. */
.eo-step.is-goal .eo-step-card { box-shadow: 4px 4px 0 rgba(227, 147, 27, .34); }
.eo-step.is-goal:hover .eo-step-card { box-shadow: 6px 6px 0 rgba(227, 147, 27, .44); }
.eo-step.is-goal .eo-step-ico {
    color: #2b0f28;
    background: #f7b72d;
    border-color: #e3931b;
}
.eo-step.is-goal .eo-step-count {
    background: #f7b72d;
    border-color: #e3931b;
    color: #2b0f28;
}
.eo-step.is-goal .eo-step-node {
    background: #f7b72d;
    border-color: #e3931b;
    color: #2b0f28;
}

/* Four columns hold to 1120px, where each card still has about 265px — enough
   for "Malaysian Economics Olympiad (MyEO)" in three lines. Below that, two by
   two. The rail keeps running inside each row of two, so the chevron after
   stage 2 has to go: it would point off the end of a line into a row break. */
@media (max-width: 1120px) {
    .eo-steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .eo-step:nth-child(2) .eo-step-rail::after { content: none; }
    .eo-step:nth-child(2) .eo-step-rail::before { right: 50%; }
    .eo-step:nth-child(3) .eo-step-rail::before { left: 50%; }
}

/* One column on a phone, and the rail stands up with it: the connector runs
   down through each node instead of across, from the card above to the card
   below, so the sequence still reads as one line rather than as four cards with
   a pill floating over each. The chevron goes — a line that only ever runs one
   way down a page does not need one — and the numerals carry the direction. */
@media (max-width: 640px) {
    .eo-steps { grid-template-columns: minmax(0, 1fr); }
    /* One card per row: an equal-rows grid here would stretch every card to the
       height of the longest, and card 4 carries a single bullet. Nothing is out
       of alignment at one column, so there is nothing for it to fix. */
    .eo-steps { grid-auto-rows: auto; }
    .eo-step-rail { height: 32px; margin-bottom: 10px; }
    .eo-step-rail::before {
        /* The run bridges the grid gap above and the rail's own margin below, so
           the line meets both cards rather than stopping short of either. */
        top: calc(var(--eo-step-gap) * -1);
        bottom: -10px;
        left: 50%;
        right: auto;
        width: 2px;
        height: auto;
        transform: translateX(-50%);
    }
    /* Nothing above the first node to reach for. */
    .eo-step:first-child .eo-step-rail::before { top: 50%; }
    /* …but the last one still runs down into its card: the line ends on the
       arrival, not in the air above it. */
    .eo-step:last-child .eo-step-rail::before { right: auto; bottom: -10px; }
    .eo-step .eo-step-rail::after { content: none; }
}

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

/* ---- Awards: THE COHORT STRIP ----------------------------------------------
   One bar carrying the whole entry at true proportions, then the three
   certificates level beneath it, each keyed to its segment by a coloured spine.

   WHY THIS AND NOT A PODIUM. Three centred cards with a medal on each and the
   prize raised out of the row is what the sibling sites draw, and it is what an
   awards page looks like more or less everywhere. It is also the one shape this
   section had no reason to borrow: what the section holds is a distribution of a
   cohort, and distributions are the subject of this site. Drawn as a share bar
   it is both more honest and unmistakably MyEO's own.

   WIDTH IS TRUE ON THE BAR AND NOWHERE ELSE. The 10% segment is a tenth of the
   bar. That is exactly why the cards below are equal and level: the old
   arrangement drew the split three times over, which put the widest, loudest
   mark on "remaining 50% — certificate of participation" and left the prize the
   smallest thing in the section. Here weight descends as width grows — gold on
   the tenth, the Explorer badge's violet on the next two fifths, pale lilac on
   the half — so the eye still lands on the award worth winning while the figures
   stay true.

   The violet is the badge's, and this is the section it was brought in for: an
   award page is where a competition prints its own colours.
   ========================================================================== */
.eo-cohort {
    margin: 0 0 clamp(22px, 2.8vw, 32px);
}
/* The bar's label, set as an axis caption rather than a heading: it names what
   the bar measures and nothing else. */
.eo-cohort-cap {
    margin: 0 0 10px;
    font-size: .74rem;
    font-weight: 800;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--accent);
}
.eo-cohort-bar {
    display: flex;
    height: clamp(38px, 4vw, 46px);
    border-radius: var(--eo-r);
    overflow: hidden;
    border: 1px solid rgba(69, 22, 63, .16);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), 0 8px 20px -14px rgba(69, 22, 63, .8);
}
/* flex-basis, not width: the segments have to fill the bar exactly, and a
   rounding error on a percentage width would show as a hairline of card
   through the middle of a chart. */
.eo-cohort-seg {
    flex: 0 0 var(--w);
    display: grid;
    place-items: center;
    min-width: 0;
    font-size: .84rem;
    font-weight: 800;
    letter-spacing: .02em;
}
.eo-cohort-seg + .eo-cohort-seg { border-left: 1px solid rgba(255, 255, 255, .45); }

.eo-cohort-seg.is-gold {
    background: linear-gradient(135deg, #f7b72d, #dea425);
    color: #2b0f28;                       /* 9.4:1 — the primary button's pairing */
}
.eo-cohort-seg.is-honorable {
    background: linear-gradient(135deg, #7c2878, #6a2168);
    color: #fff;                          /* 10.5:1 */
}
.eo-cohort-seg.is-participation {
    background: linear-gradient(135deg, #e8dcea, #ddc9db);
    color: #45163f;                       /* 9.5:1 */
}

/* ---- The three certificates ---------------------------------------------- */
.eo-awards {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    /* stretch, not start: the cards are equal citizens now. The podium step the
       old arrangement used is on the bar above instead, where it is measured. */
    align-items: stretch;
    gap: 16px;
}
/* The spine is the key back to the bar: the card and its segment carry the same
   colour, which is the only thing tying the two drawings together. */
.eo-award {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    padding: clamp(18px, 2vw, 24px) clamp(18px, 2vw, 24px) clamp(18px, 2vw, 22px);
    border: 1px solid rgba(69, 22, 63, .14);
    border-left: 5px solid var(--spine, #c9a6d0);
    border-radius: 0 var(--eo-r) var(--eo-r) 0;
    background: var(--surface);
    box-shadow: var(--eo-drop);
}
.eo-award-ico {
    display: grid;
    place-items: center;
    width: 62px;
    height: 62px;
    border-radius: var(--eo-r);
    background: rgba(255, 255, 255, .72);
    border: 1px solid rgba(69, 22, 63, .1);
}
.eo-award-band {
    margin: 0;
    color: #2b0f28;
    font-weight: 800;
    line-height: 1.15;
    font-size: clamp(1.3rem, 2.4vw, 1.6rem);
}
/* No max-width: the card is the measure now that the text ranges left, and a
   30ch cap inside a 340px card only pulled the last line short. */
.eo-award-name { margin: 0; color: var(--text-soft); }

.eo-award-share {
    margin: 4px 0 0;
    font-size: .76rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: #5a5f6a;
}
/* With the bar on screen this line is saying the segment over again, so it is
   taken off the eye — but NOT out of the accessibility tree. The bar is
   aria-hidden, so display:none here would take the figure off the page for a
   screen reader entirely. Clipped, not hidden. */
@media (min-width: 761px) {
    .eo-awards--cohort .eo-award-share {
        position: absolute;
        width: 1px; height: 1px;
        margin: -1px; padding: 0; border: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}
/* …and below that width the 10% segment is about 34px, which will not hold the
   figure. The bar keeps its shape, the labels come off it, and the share line on
   each card is doing the talking again. */
@media (max-width: 760px) {
    .eo-cohort-seg b { display: none; }
}

/* The flag rides the top edge of the prize card, so the row reserves the space
   it hangs into. It sits right rather than centred — the card ranges left now,
   and a centred flag over left-ranged content reads as a mistake. */
.eo-award-flag {
    position: absolute; top: -13px; right: 18px;
    padding: 5px 14px; border-radius: 99px; white-space: nowrap;
    background: linear-gradient(135deg, #fcca5d, #f7b72d);
    color: #2b0f28;
    font-size: .7rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
    box-shadow: var(--eo-drop);
}

/* Gold, silver or bronze — the prize band. Gold spine, a gold ring and the one
   card with a warm fill: the strip says a tenth, this says which tenth. */
.eo-award--gold {
    --spine: #dea425;
    border-color: rgba(247, 183, 45, .75);
    background: #fdf4dd;
    box-shadow: var(--eo-drop);
}
.eo-award--gold .eo-award-ico { background: rgba(255, 255, 255, .82); }
.eo-award--gold .eo-award-share { color: #6d4708; }   /* 5.6:1 on the warm fill */

/* Honourable mention — the badge violet, the middle weight. */
.eo-award--honorable {
    --spine: #6a2168;
    background: #f7eff8;
}

/* Participation — everyone receives it, so it sits quietest and nearest the
   panel it is printed on. */
.eo-award--participation {
    --spine: #c39cc0;
    background: var(--surface);
}

@media (min-width: 880px) {
    /* Three across. Below this the cards are too narrow for "will receive a
       Gold, Silver or Bronze Award Certificate" to break tidily, so they stack. */
    .eo-awards { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
}
/* Stacked or in a row, the flag hangs above the first card and the row needs its
   own clearance for it. */
.eo-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. */

.eo-closing {
    text-align: center;
    background: var(--plum-050);
    border-color: rgba(222, 164, 37, .25);
}
.eo-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. */
.eo-closing p { max-width: none; margin: clamp(20px, 3vw, 28px) auto 0; }
.eo-closing .btn { padding: 16px 34px; font-size: 1.05rem; }

/* ============================================================================
   THE FIGURE LAYER — ledger leaves, plots, axis corners and tally marks
   ----------------------------------------------------------------------------
   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 MyEO palette landed on the colours — navy, gold, ice
   paper — but every ornament on the site was still a circle, a ring or a
   gradient band inherited from the siblings. Nothing was shaped like the
   subject. These are the shapes, and they are taken from the WORKING of
   economics: a ruled ledger line, a plotted series, one curve crossing another,
   a struck coin, a tally of entries.

   That distinction is the whole design rule here, and it is worth keeping.
   MyEO's subject is not money as wealth and it is not business as a career: it
   is what you can say about a column of figures once you have drawn the right
   line through it. So there are no banknotes, coins raining down or piggy banks
   in this set, and no briefcases, handshakes or skylines either. Anything added
   later should come off a ledger, a plot, or the working somebody did beside
   one.

   FOUR surfaces carry them, and all four are ATTACHED to something. That is the
   rule the set is down to: an ornament belongs to a surface, never to the page.

       .eo-hero        a ruled baseline along the foot of the home page's hero
       .eo-head        a rule under every section title
       .eo-band        the same ruled baseline along the foot of every band,
                       with a struck coin pressed into each bottom corner
       .site-footer    tally ticks along the top edge, like a ledger's count

   THREE ARE GONE, and it is worth knowing what they were before anybody adds
   another. A tiled scatter across the page backdrop; ledger leaves, crossings
   and plots standing in the margins; and an axis corner either side of every
   inner-page title. Each was defensible alone. Together — plus a canvas drifting
   nine more figures over the top — they were four independent layers behind the
   words, tuned separately and never judged together, and the page read as busy
   no matter how faint any one of them was made. The ground is paper now.

   NOTHING HERE IS A REAL SERIES, and nothing added here ever should be. This
   site publishes results; a chart in its furniture is a chart somebody will
   eventually try to read. Every plotted shape below is drawn without scale,
   axis labels or numbers, and the one figure that could be mistaken for data —
   the bar run in the baseline tile — is 22px tall and half-transparent.

   The artwork is held in custom properties so a tile used in two places (the
   ruled baseline 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 {
    /* No --eo-rule token any more: the section divider is a plain CSS shape,
       drawn at .eo-head::after further down this file. See the note there for
       the two ornaments it replaced and why both were wrong. */

    /* The ruled baseline: a ledger rule with a run of bars standing on it,
       tiled horizontally along the foot of a band. The seam is at x=0/220,
       where nothing sits.

       The bars are of uneven height and unevenly spaced. That is the whole
       reason this reads as a series rather than as a fence — a comb of equal
       teeth is a border, and at 22px tall the unevenness is the only thing the
       eye has to go on. They do not trend in any direction either: a rise here
       would be a claim, and the one place this site makes that claim is the
       logo. */
    --eo-baseline-tile: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 22'><g fill='none' stroke='%23545b70' stroke-linecap='round'><path d='M0 18h220' stroke-width='1.4' opacity='.3'/><path d='M0 6h220' stroke-width='1' opacity='.14'/></g><g fill='%23545b70' opacity='.3'><rect x='8' y='8' width='5' height='10' rx='1'/><rect x='19' y='4' width='5' height='14' rx='1'/><rect x='30' y='11' width='5' height='7' rx='1'/><rect x='41' y='6' width='5' height='12' rx='1'/><rect x='56' y='9' width='5' height='9' rx='1'/></g><g fill='%23545b70' opacity='.22'><rect x='74' y='5' width='5' height='13' rx='1'/><rect x='85' y='12' width='5' height='6' rx='1'/><rect x='96' y='7' width='5' height='11' rx='1'/><rect x='111' y='10' width='5' height='8' rx='1'/></g><g fill='%23545b70' opacity='.27'><rect x='134' y='6' width='5' height='12' rx='1'/><rect x='145' y='13' width='5' height='5' rx='1'/><rect x='156' y='4' width='5' height='14' rx='1'/><rect x='167' y='10' width='5' height='8' rx='1'/><rect x='182' y='7' width='5' height='11' rx='1'/><rect x='197' y='11' width='5' height='7' rx='1'/></g></svg>");

    /* The hero axis: the corner of a plot — a value axis up, a time axis across
       — with a short series drawn inside it and a filled point where the
       reading is taken. Drawn opening to the right; the left-hand copy is
       mirrored in CSS, which is safe here BECAUSE this figure carries no trend.
       Its series rises and falls back, so a mirror of it is another series and
       not the opposite claim. (The margin pieces are not mirrored for exactly
       that reason — see the gutter layer below.) */
    --eo-axis-art: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 80'><g fill='none' stroke='%232d4891' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.7'><path d='M14 8v64h72'/></g><g fill='none' stroke='%231a2b58' stroke-width='1.4' stroke-linecap='round' opacity='.28'><path d='M14 28h72M14 50h72'/></g><g fill='none' stroke='%231a2b58' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='.55'><path d='M24 58 40 40 54 48 70 22'/></g><g fill='none' stroke='%232d4891' stroke-width='1.7' stroke-linecap='round' opacity='.35' stroke-dasharray='5 5'><path d='M14 22h56'/></g><circle cx='70' cy='22' r='2.8' fill='%23f7b72d' opacity='.9'/></svg>");

    /* Tally ticks along the footer's top edge — the entry count ruled down the
       margin of a ledger, where every fifth line is marked long. The dark panel
       then reads as the leaf the page is written on.

       Navy, 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 ice
       section above — in --footer-muted (#9fb1e1, chosen for dark ground) they
       came out as faint scratches on nothing. */
    --eo-tick-tile: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 18'><g fill='none' stroke='%231a2b58' 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: five figures on a 440px tile. Deliberately sparse and
       almost transparent — at ~.05-.06 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 of every page.

       WHAT IS DRAWN, and why it changed. This used to be two bar runs, a small
       plot grid and three plus-marks. Every one of those is true to the subject
       and not one of them is only economics: a bar run is any dashboard, a plot
       grid is any science, and a plus is nothing at all. Across a whole page at
       a twentieth of the ink the tile said "charts" and stopped.

       It now carries the diagrams that belong to no other subject — a crossing
       with a line held across it, a distribution sagging under its own line of
       equality, a curve shifting outward with the shift drawn on it — alongside
       an index with its average, and a tally of five. A reader never reads any
       of it; what changes is what the page feels like it is about.

       440, not the 320 the sibling site drew its scatter at. The shapes are the
       right size at 320 but the repeat put ~50 of them on a 1600x1100 screen,
       and eo-bg.js is already drifting marks 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. */
    --eo-ground-tex: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 620 620'><g fill='%231a2b58' opacity='.038'><rect x='52' y='100' width='7' height='20' rx='1.5'/><rect x='66' y='88' width='7' height='32' rx='1.5'/><rect x='80' y='106' width='7' height='14' rx='1.5'/><rect x='94' y='94' width='7' height='26' rx='1.5'/></g><g fill='none' stroke='%236a2168' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='.042'><path d='M55 94 69 82 83 98 97 88'/></g><g fill='none' stroke='%236a2168' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.045'><path d='M330 118 Q352 174 408 184'/><path d='M330 184 408 118'/><path d='M324 156h92'/></g><g fill='none' stroke='%232d4891' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.042'><path d='M64 388 152 300'/><path d='M64 388 Q126 374 152 300'/><path d='M64 388h88M64 388V300'/></g><g fill='none' stroke='%230f5578' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.042'><path d='M286 300 Q312 352 350 362'/><path d='M316 300 Q342 352 380 362'/><path d='M322 334h28M344 328l6 6M344 340l6-6'/></g><g fill='none' stroke='%23545b70' stroke-width='2' stroke-linecap='round' opacity='.038'><path d='M196 206v18M204 206v18M212 206v18M220 206v18M192 222 224 202'/><path d='M254 60v16M261 60v16M268 60v16'/></g></svg>");

    /* ---- Light-ink set, for the deep workshop band --------------------------
       The same five figures, redrawn for a dark ground: ice paper, gold and
       the lilac tint instead of navy, sky and violet. A recolour is
       genuinely needed rather than a filter or an opacity tweak: the dark set is
       #5c1c58 on ice, and #5c1c58 on a #5c1c58 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. */
    --eo-ground-tex-light: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 620 620'><g fill='%23e3e9fa' opacity='.09'><rect x='52' y='100' width='7' height='20' rx='1.5'/><rect x='66' y='88' width='7' height='32' rx='1.5'/><rect x='80' y='106' width='7' height='14' rx='1.5'/><rect x='94' y='94' width='7' height='26' rx='1.5'/></g><g fill='none' stroke='%23c9a6d0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='.10'><path d='M55 94 69 82 83 98 97 88'/></g><g fill='none' stroke='%23f7b72d' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.10'><path d='M330 118 Q352 174 408 184'/><path d='M330 184 408 118'/><path d='M324 156h92'/></g><g fill='none' stroke='%239fb1e1' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.10'><path d='M64 388 152 300'/><path d='M64 388 Q126 374 152 300'/><path d='M64 388h88M64 388V300'/></g><g fill='none' stroke='%23c9a6d0' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='.10'><path d='M286 300 Q312 352 350 362'/><path d='M316 300 Q342 352 380 362'/><path d='M322 334h28M344 328l6 6M344 340l6-6'/></g><g fill='none' stroke='%239fb1e1' stroke-width='2' stroke-linecap='round' opacity='.09'><path d='M196 206v18M204 206v18M212 206v18M220 206v18M192 222 224 202'/><path d='M254 60v16M261 60v16M268 60v16'/></g></svg>");

    /* A struck coin rising out of the band's bottom edge — two milled rings with
       a rule and a short entry across the middle, the stamp pressed at the foot
       of a finished page. Cropped by the band's own box. Placed as a background
       layer rather than a pseudo-element because the band's ::before is the top
       seam and its ::after is the ruled baseline, and backgrounds clip to the
       border box for free so nothing can spill past the section.

       No currency sign on it, in any of the three. A dollar sign would name a
       currency this competition does not use, an RM would date the ornament to
       one country and one year, and either would put a word into furniture that
       is otherwise wordless. The milling and the rule say "coin" without
       claiming whose. */
    --eo-coin-light: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 124 124'><g fill='none' stroke='%23e3e9fa' stroke-width='2.2' opacity='.12'><circle cx='62' cy='62' r='42'/><circle cx='62' cy='62' r='33'/></g><g fill='none' stroke='%23e3e9fa' stroke-width='2.4' stroke-linecap='round' opacity='.14'><path d='M62 30v-8M62 94v8M30 62h-8M94 62h8M40 40l-6-6M84 84l6 6M84 40l6-6M40 84l-6 6'/></g><g fill='none' stroke='%23e3e9fa' stroke-width='2.6' stroke-linecap='round' opacity='.16'><path d='M46 62h32'/><path d='M50 74V68M58 74V66M66 74V70M74 74V67'/></g></svg>");

    /* The same coin in navy, for the gold .eo-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. */
    --eo-coin-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 124 124'><g fill='none' stroke='%232d4891' stroke-width='2.2' opacity='.12'><circle cx='62' cy='62' r='42'/><circle cx='62' cy='62' r='33'/></g><g fill='none' stroke='%231a2b58' stroke-width='2.4' stroke-linecap='round' opacity='.10'><path d='M62 30v-8M62 94v8M30 62h-8M94 62h8M40 40l-6-6M84 84l6 6M84 40l6-6M40 84l-6 6'/></g><g fill='none' stroke='%231a2b58' stroke-width='2.6' stroke-linecap='round' opacity='.13'><path d='M46 62h32'/><path d='M50 74V68M58 74V66M66 74V70M74 74V67'/></g></svg>");

    /* The ruled baseline for the band's foot, in pale ink rather than the muted
       grey the light-ground bands use. Same marks, same 220x22 tile. */
    --eo-baseline-tile-light: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 220 22'><g fill='none' stroke='%23e3e9fa' stroke-linecap='round'><path d='M0 18h220' stroke-width='1.4' opacity='.26'/><path d='M0 6h220' stroke-width='1' opacity='.12'/></g><g fill='%23e3e9fa' opacity='.28'><rect x='8' y='8' width='5' height='10' rx='1'/><rect x='19' y='4' width='5' height='14' rx='1'/><rect x='30' y='11' width='5' height='7' rx='1'/><rect x='41' y='6' width='5' height='12' rx='1'/><rect x='56' y='9' width='5' height='9' rx='1'/></g><g fill='%23e3e9fa' opacity='.2'><rect x='74' y='5' width='5' height='13' rx='1'/><rect x='85' y='12' width='5' height='6' rx='1'/><rect x='96' y='7' width='5' height='11' rx='1'/><rect x='111' y='10' width='5' height='8' rx='1'/></g><g fill='%23e3e9fa' opacity='.24'><rect x='134' y='6' width='5' height='12' rx='1'/><rect x='145' y='13' width='5' height='5' rx='1'/><rect x='156' y='4' width='5' height='14' rx='1'/><rect x='167' y='10' width='5' height='8' rx='1'/><rect x='182' y='7' width='5' height='11' rx='1'/><rect x='197' y='11' width='5' height='7' rx='1'/></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 ice 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. */
/* THE WASH ONLY. The scatter used to be the first layer of this stack and now
   lives on body::after, because a mask applies to a whole element and there was
   no way to keep the texture out of the reading column while leaving the wash
   across the whole page. Two elements, two jobs. */

/* THE SCATTER, MASKED TO THE MARGINS.
   ---------------------------------------------------------------------------
   This is the change that answers "the background is messy": the figures no
   longer run underneath the words. The tile repeats across the whole viewport as
   before, and a mask then removes it from the reading column — opaque out to
   640px either side of centre, gone by 590px, which is the container's own half
   width. The fade between the two happens in the 50px of gutter nobody reads in.

   BELOW ~1280px THE MASK ERASES IT ENTIRELY, and that is intended rather than a
   side effect. On a narrow screen `calc(50% - 640px)` is negative, the stops
   clamp, and the layer resolves to fully transparent — so a phone gets plain
   violet paper and composites no tile at all. The in-page motifs still carry the
   theme there: the hero's ruled bands, the seam and baseline on every tinted
   band, the struck coins in their corners, and the footer's tally rail.

   z-index -2 puts it above the wash on ::before (-3) and below everything on the
   page. Fixed, so it does not scroll — the figures sit in the margin the way
   marginalia sits on a page, rather than sliding past like wallpaper. */

/* NOTE — no per-page ground override lives here, and that is a decision rather
   than an omission.

   The sibling Geography site washes its three international pages in a second
   colour, because there the international olympiad is three pages of its own and
   the ground can carry the split. MyEO keeps its four international write-ups —
   IEO 2025 through 2022 — as sections of /about-us, which is where the live site
   keeps them. A ground that changed four times down one page would read as a
   rendering fault, not as a category, so the split is carried inside the page by
   .eo-band instead. Seven pages, one paper.
   ========================================================================== */

/* ---- Margin figures: REMOVED -----------------------------------------------
   Ledger leaves, a supply-and-demand crossing and a plotted series used to stand
   in the empty margins either side of the container on screens wide enough to
   have them. They were the best-drawn ornament on the site and they went with
   the rest of the background: the ground is paper now, and a drawing that floats
   beside the reading column is the thing that was making the page feel busy.

   The partial that carried the SVG is deleted too — resources/views/redesign/
   partials/eo-figures.blade.php. Recovering it is one `git show` away, and its
   markup is worth reading before anybody redraws this from scratch: the two
   margins were deliberately NOT mirrored, because a supply-and-demand crossing
   flipped puts the price axis on the wrong side and a rising series flipped
   becomes a falling one, on a site whose logo is that line going up. */
/* ---- Hero axes: REMOVED ----------------------------------------------------
   Every inner page used to open with an axis corner either side of its title —
   a mirrored pair pinned to the container's outer edges, enclosing the heading
   the way a plot frame encloses a series. It was the neatest ornament in the
   set and it is gone with the rest of them.

   The reason is consistency rather than dislike. The ground is paper now: no
   scatter, no drifting marks, no margin figures. A pair of chart brackets
   floating beside a heading is the same kind of object as the marks that were
   removed — a drawing behind the words, unattached to anything on the page —
   and leaving two of them standing would make the decision look like fatigue
   rather than a decision.

   What still carries the motif does so because it is ATTACHED to something: the
   ruled bands inside the home hero, the seam along the top of every tinted band
   and the baseline along its foot, the struck coins pressed into a band's
   corners, and the tally rail across the top of the footer. Each of those is
   part of a surface. None of them floats.

   --eo-axis-art is left defined. It is one data URI, it costs nothing sitting
   unused, and it is the drawing anybody would want back first. */

/* ---- Hero foot -------------------------------------------------------------
   The home page has no .page-hero — it opens on .eo-hero, whose ground is a
   stack of ruled bands. Axis corners would fight the brand mark, so
   it gets the ruled baseline instead: a run of bars set along the foot of the
   page, which is what the bands above it are ruled FOR. .eo-hero already clips
   its overflow and isolates its stacking context, so this stays inside both. */
.eo-hero::after {
    content: "";
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 30px;
    background: var(--eo-baseline-tile) bottom center / 300px 30px repeat-x;
    opacity: .55;
    pointer-events: none;
}

/* ---- Section divider -------------------------------------------------------
   .eo-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. */
/* The divider itself: a short rounded bar, gold into ice.

   Two ornaments came before it and both were wrong in the same way — they were
   trying to say something.

   The first was a hairline with ticks at each end, which is the Geography site's
   neatline with different end-caps: same line, same three marks, same silhouette
   at a glance.

   The second replaced it with three marks from this site’s figure set, set
   as a printer's asterism. It was genuinely MyEO's and it did not read as a
   divider — at 20px on a page of real text, three unlabelled marks read as
   symbols somebody had left there, not as a break.

   A divider's whole job is to be recognised instantly and then ignored. It
   repeats a dozen times on the home page; it is furniture. So it is a bar: no
   data URI, no marks, nothing to decode. The gradient is the only brand in it,
   and that is enough. */
.eo-head::after {
    content: "";
    width: 64px;
    height: 4px;
    margin-top: 14px;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--brand), var(--accent-3));
}
/* The preparatory-workshop section runs on a dark panel, where the gold end of
   the gradient sits too close to the ground behind it. Flattened to white and
   dropped back. */
.eo-workshop .eo-head::after {
    filter: brightness(0) invert(1);
    opacity: .45;
}

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

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

/* ---- Footer edge -----------------------------------------------------------
   Tally ticks along the top of the footer — the line count ruled down a
   ledger's margin. 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(--eo-tick-tile) bottom center / 160px 18px repeat-x;
    pointer-events: none;
}

/* ---- Study Guide: the download cards ---------------------------------------
   A drawn mark, the title and its note, and a chevron. The live MyEO page
   presents its three preparation documents as icon-over-title tiles; this keeps
   the icon but turns the tile on its side, because the three titles — "Types of
   OL Problems", "Sample Problems", "Sample Solutions" — are told apart by their
   words, not by their marks, and a row gives the words the width to be read at a
   glance.

   Grid rather than flex on the row, because the three parts have different
   jobs: the mark 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. */
.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: var(--eo-line) solid var(--eo-edge);
    border-radius: var(--eo-r);
    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: var(--eo-r-sm);
    background: var(--surface-2);
    border: var(--eo-line) solid var(--eo-edge);
    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 sibling Geography site carries a second copy of the year block sized for
   a page HERO, because there each olympiad year is a page of its own. MyEO
   keeps its two international write-ups as sections of /about-us — which is
   where the live site keeps them — so the year block only ever appears inside a
   card, and the hero variant has been dropped rather than left as dead rules
   nothing on nine pages can select.
   ========================================================================== */

/* ---- 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; }

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

/* NO inverted variant inside .eo-workshop. The band behind that section is dark,
   but the chip does not sit on the band — it sits inside .eo-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"]
   .eo-workshop .gx-glass .eo-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 .eo-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"] .eo-workshop .gx-glass .eo-wk-fact .eo-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: 14px;
    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: clamp(210px, 24vw, 300px);
    /* 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: var(--eo-r);
    border: var(--eo-line) solid var(--eo-edge);
}
.moment-strip figcaption {
    margin-top: 8px;
    max-width: clamp(210px, 24vw, 300px);
    font-size: .85rem;
    color: var(--text-soft);
}
.moment-strip:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 4px;
    border-radius: var(--eo-r);
}

/* 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: #2b0f28;
}
.moment-sub .gx-icon { color: var(--accent); flex: none; }
/* 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.
   ---------------------------------------------------------------------------
   .eo-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 — a closing
   line of eight words stretched the full width, with gaps wide enough to read as
   columns.

   This does NOT contradict the decision documented on .eo-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) {
    .eo-justify p,
    .eo-justify li {
        text-align: left;
    }
}

/* The last two tap targets under 44px on a touch device.
   ---------------------------------------------------------------------------
   redesign.css already floors the footer links, the contact rows, .btn-sm,
   .faq-btn and every <summary> under (pointer: coarse). Measuring what was left
   after those floors and after WCAG 2.5.8's exemption for links inside a
   sentence, exactly two controls remained across all eight pages:

     .brand            the header logo, 203x40 — four pixels short, on every page
     .contact-card a   the mailto on /contact-us, 123x23 — the phone numbers
                       beside it are in <li> and exempt as inline links; this one
                       sits alone in its own <p> and is not

   Both are floored here rather than in redesign.css because that file is the
   shared engine and these two are this site's own markup. */
@media (pointer: coarse) {
    .brand { min-height: 44px; }
    .contact-card p a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* ---- The playlist on a narrow phone -------------------------------------
   The row is a three-column grid: a 92px still, the title, and the run time.
   At 320-430px that left the TITLE about 55px wide — one word per line for a
   45-character tutorial name, five rows of it down the panel.

   Below 480px the still drops to 64px and the run time moves under the title,
   so the title gets the full remaining width instead of sharing the row with
   two fixed columns. Nothing is hidden: the still is decorative (alt="") and
   the run time is still read, just on its own line.

   align-items: start here rather than centre, because the right column now
   holds two stacked lines and centring them against a 36px still left the
   title sitting below the top of the row. */
@media (max-width: 480px) {
    .video-item {
        grid-template-columns: 64px minmax(0, 1fr);
        align-items: start;
        gap: 9px;
    }
    .video-thumb { width: 64px; height: 36px; grid-row: span 2; }
    /* break-word, not the `anywhere` this span INHERITS from its <li>.
       overflow-wrap is an inherited property, and redesign.css sets `anywhere`
       on li — so the title was breaking at any character and set
       "Teacher-in-Char / ge" even though the content spells it with
       non-breaking hyphens precisely to hold it together. break-word breaks a
       word only when it genuinely cannot fit, which at this column width it
       can. */
    .video-name { grid-column: 2; overflow-wrap: break-word; }
    .video-time { grid-column: 2; justify-self: start; }
}

/* ---- …and the still goes entirely on the narrowest phones ----------------
   Measured, not guessed: at 320px the 64px still leaves the title a 124px
   column, and "Teacher-in-Charge" needs 143px in this face. It cannot fit, so
   it breaks mid-word as "Teacher-in-Char / ge" — and the content spells that
   name with NON-BREAKING hyphens (U+2011) precisely to stop it being split.
   No wrap setting can honour that in a column narrower than the word.

   So below 400px the still is dropped and the title takes the whole row,
   which gives it about 197px and lets the non-breaking hyphens do their job.
   The still is the only thing lost and it is decorative — alt="" — while the
   title is the thing a reader picks the video by. */
@media (max-width: 400px) {
    .video-item { grid-template-columns: minmax(0, 1fr); }
    .video-thumb { display: none; }
    .video-name, .video-time { grid-column: 1; }
}

/* Ragged right on a wide screen — the other end of the same fallback.
   ---------------------------------------------------------------------------
   The justify rule names TWO supported states, "justify + hyphens:none" and
   "ragged-right … the fallback if the spacing is ever too loose", and the note
   on the uncapped measure spells out when the second is needed:

       "at 1440px a line runs to roughly 100 characters, which is past the ~80
        where the eye starts losing its place on the return sweep, and justified
        text has no ragged edge left to navigate by. If that ever reads badly,
        the fix is the fallback already named above — ragged right — NOT a
        re-cap of the measure."

   That condition is met. Measured on /about-us: the justified column is 542px
   (~59ch) at 640px wide, 737px (~80ch) at 860, and 1042px (~113ch) from 1200
   up, where the container stops growing. 113 characters with no hyphenation and
   no ragged edge is the case the note describes.

   So this is the SAME fallback the narrow end already uses — there is a
   @media (max-width: 640px) rule doing exactly this, for the opposite reason
   (at ~40ch the word gaps open too wide). Justification now holds the band
   between them, 641-999px, which is ~59-93ch: the range where it sets cleanly.
   The note's own line covers why the two ends differ — "the break is where the
   spacing goes, not where the layout does."

   1000px, measured rather than picked: at 960 the measure is ~90ch and the
   audit passes it; at 1024 it is ~96ch and the audit flags it. The breakpoint
   sits where the measurement crosses, not on a device boundary.

   THE MEASURE IS STILL NOT CAPPED, deliberately, and re-capping it is the one
   thing the note rules out: it was tried, and it centred the copy inside a
   panel whose photo grids and year headings span the full width, so the text
   read as a narrow column that had missed its box.

   Reverting is deleting this block. */
@media (min-width: 1000px) {
    .eo-justify p,
    .eo-justify li {
        text-align: left;
    }
}
