/* Organic — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
/* Urbanist is the whole interface, at 400/500/600. Noto Sans Ethiopic is not an
   "additional interface font" — it is the same system carrying Amharic, and it
   is fetched rather than merely named. It was named for a long time without
   being fetched, so Amharic fell to whatever the reader's OS shipped: Kefa on
   macOS, Ebrima on Windows, tofu on plenty of Android and Linux. Italic 500 is
   pulled because the display headline sets one phrase in italic Heritage Clay. */
@import url('https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,400;0,500;0,600;1,500&family=Noto+Sans+Ethiopic:wght@400;500;600&display=swap');

:root {
  /* ── The brand palette ────────────────────────────────────────────────────
     Nine colours, named as the brand names them. Nothing else is an approved
     colour. These are the only literal hexes in the system: every role, ramp
     and derived value below resolves to one of them, so retuning a brand
     colour here moves the whole site. */
  --eca-morning-mist: #faf7f2;    /* main light background */
  --eca-highland-mist: #f2eee4;   /* cards, sections, secondary surfaces */
  --eca-heritage-clay: #a63a2a;   /* primary CTA and interactive accent */
  --eca-roasted-earth: #3b2a24;   /* coffee-focused rich dark surfaces */
  --eca-volcanic-stone: #333333;  /* structural dark surfaces, secondary text */
  --eca-charcoal: #1f1f1f;        /* primary typography, deepest neutral */
  --eca-forest-canopy: #2f5d50;   /* nature, sustainability, supporting accent */
  --eca-woven-taupe: #b9ad98;     /* borders, dividers, subtle neutral */
  --eca-brass: #c8a24e;           /* RESTRICTED premium accent — see below */

  /* ── Roles ────────────────────────────────────────────────────────────────
     What a component asks for. Components reference these and never a brand
     name, so the dark theme can re-map them in one place. */
  --color-bg: var(--eca-morning-mist);
  --color-surface: var(--eca-highland-mist);
  --color-text: var(--eca-charcoal);
  --color-text-secondary: var(--eca-volcanic-stone);
  --color-accent: var(--eca-heritage-clay);
  --color-accent-2: var(--eca-forest-canopy);
  --color-coffee: var(--eca-roasted-earth);  /* a SURFACE, not a type colour */

  /* Woven Taupe is the border colour of the system — dividers, inputs, tables,
     cards, charts. One value, so structure reads as one decision. */
  --color-taupe: var(--eca-woven-taupe);
  --color-divider: var(--color-taupe);

  --color-stone: var(--eca-volcanic-stone);

  /* ── Information ──────────────────────────────────────────────────────────
     A light blue, and the one colour in the system that is NOT a brand colour.
     It is a semantic UI affordance rather than brand expression: an ⓘ says
     "there is a record behind this", the way an error field says "this is
     wrong", and neither is the association speaking. Heritage Clay could not
     do it — that is the action colour, and an ⓘ is not an action on the page.

     It appears in exactly one place: the atlas chip's info button. If it starts
     appearing anywhere else, that is the moment to take it back to the
     association rather than to spread it quietly. */
  --color-info: #dbeafe;
  --color-info-strong: #bfdbfe;
  --color-info-ink: #1e4b8f;

  /* Brass communicates distinction, never interaction. It belongs on Charcoal
     and Roasted Earth, for premium membership, awards, certification and
     leadership content. It is NOT a CTA, a link, a nav item, a heading, a card
     or a general decoration, and it never competes with Heritage Clay. */
  --color-brass: var(--eca-brass);

  /* Atlas palette — the Coffee Atlas's Kew-zone fills, qualitative colour for
     data, not a theme role, so it stays the SAME hex in both themes: a reader
     comparing the map light and dark should still recognise "that's the
     Harar colour." Four of these are literally the dark swatch's own hex
     (Cherry Berry, Forest Leaf, Harvest Gold) plus one new to the file (Moss
     Green). They get their own --atlas-* names here rather than reusing
     --color-cherry-berry etc.: those three live inside
     :root[data-theme="dark"] specifically so the Figma sync, which only
     reads this first block, can never see or delete them (see the comment
     there). Duplicating the same name at this level would defeat that. North
     keeps using --color-bg-dark (Deep Espresso) directly instead of gaining
     a fifth name here — one authority for one colour, per the rule below. */
  --atlas-cherry-berry: #7a1e22;
  --atlas-forest-leaf: #1e4d2b;
  --atlas-moss-green: #556b2f;
  --atlas-harvest-gold: #b88a4a;
  --atlas-charcoal: var(--eca-charcoal);   /* North */

  /* ── The dark theme's roles ───────────────────────────────────────────────
     Dark mode is the same identity at a deeper exposure, not a black-and-white
     version of the site: Charcoal ground, Volcanic Stone surface, Roasted Earth
     for warmth, Morning Mist type, Heritage Clay still the only action colour.
     Every value here is a brand colour. */
  --color-bg-dark: var(--color-espresso-ground);
  /* ESPRESSO, not anthracite. The brand's website usage guide makes Roasted
     Earth a PRIMARY colour — "apply primary colours for key brand elements and
     sections" — and Volcanic Stone a SECONDARY one, to be used sparingly for
     "highlights, buttons and interactive elements". This had them the wrong way
     round: Volcanic Stone carried every section band and covered more of the
     dark page than the ground did, while Roasted Earth, the coffee brown the
     whole palette is built around, appeared nowhere at all. */
  --color-surface-dark: var(--color-espresso-band);
  --color-text-dark: var(--eca-morning-mist);
  /* Woven Taupe, not Highland Mist. Highland Mist reads 16.2 on the ground
     where Morning Mist reads 17.5 — the two were indistinguishable, so the
     dark theme had a secondary ink in name only and no type hierarchy at all.
     Woven Taupe gives 8.5: a real second step, still far above 4.5, and warm. */
  --color-text-secondary-dark: var(--eca-woven-taupe);

  /* The two accents, one step lighter than their base, because they are TEXT
     here. Heritage Clay reads 2.5:1 as type on Charcoal and Forest Canopy 2.1
     — the brand spec allows Forest Canopy on dark "where sufficient contrast is
     maintained", and this is what maintaining it costs. Both are steps of the
     accent's own ramp, so they are recognisably the same colour.
     FILLS are different and stay exact: a Heritage Clay button keeps #a63a2a
     on dark, pinned in the dark block below, because Morning Mist on that fill
     reads 5.4 and the brand's own dark example shows precisely that button. */
  --color-accent-dark: #d1604a;    /* accent-350; 4.89 on the ground */
  --color-accent-2-dark: #6e9a8c;  /* accent-2-400; 5.42 on Charcoal */

  /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
     same step of any role matches the others in visual value. */
  /* The neutral ramp runs Morning Mist to Charcoal and passes through the two
     neutrals the brand names: Highland Mist at 200 and Woven Taupe at 500. It
     was a warm-brown ladder ending at #2a1d18; the ink is Charcoal now, so the
     ladder is cooler and the ends are brand colours rather than inventions. */
  --color-neutral-100: #faf7f2;  /* Morning Mist */
  --color-neutral-200: #f2eee4;  /* Highland Mist */
  --color-neutral-300: #e4ddce;
  --color-neutral-400: #cfc5b2;
  --color-neutral-500: #b9ad98;  /* Woven Taupe */
  --color-neutral-600: #948a78;
  --color-neutral-700: #6b6459;
  --color-neutral-800: #45403a;
  --color-neutral-900: #1f1f1f;  /* Charcoal */
  /* Below Charcoal, for the dark theme's ground and its floor. The ramp had
     nowhere to go under the darkest brand colour, so the dark ground WAS
     Charcoal and the surface was Volcanic Stone — twenty points apart, with
     nothing receding and nothing reading as raised. These two are steps of the
     same ladder, not new colours: 950 is the ground, 1000 is where the page
     bottoms out. 1000 is the single value in the system that is not a brand
     colour or a step between two of them, and it is only ever the footer. */
  --color-neutral-950: #121212;
  --color-neutral-1000: #000000;

  /* ── The dark theme's grounds, pulled out of Roasted Earth ────────────────
     The dark theme is a COFFEE dark, not a grey one. Every ground below is
     Roasted Earth mixed toward black, so the page is warm all the way down and
     the espresso the palette is built around is present rather than missing.

     This replaces a ladder that ran ground #121212 / surface Volcanic Stone:
     anthracite covered more of the page than the ground did and read as light
     grey, and Roasted Earth — a PRIMARY colour in the brand's own website
     guide — appeared nowhere at all. Volcanic Stone is a secondary colour and
     is used sparingly now, on controls rather than on half the page.

     Stated as literal steps, not color-mix: build-check computes the theme's
     contrast from these values and cannot evaluate a mix, so a mixed ground
     makes every dark pairing unresolvable and the gate fails. Same discipline
     as the accent ramp — a step of a brand colour, written out. */
  --color-espresso-ground: #18110e;   /* Roasted Earth 40% toward black */
  --color-espresso-band:   #291d19;   /* Roasted Earth 70% toward black */

  --color-accent-100: #faeae6;
  --color-accent-200: #f2cfc7;
  --color-accent-300: #e09482;
  --color-accent-400: #c9553f;
  --color-accent-500: #a63a2a;
  --color-accent-600: #8e2f22;
  --color-accent-700: #73261b;
  --color-accent-800: #571c14;
  --color-accent-900: #3b130e;

  --color-accent-2-100: #e8f0ed;
  --color-accent-2-200: #cbdcd6;
  --color-accent-2-300: #a3c0b7;
  --color-accent-2-400: #6e9a8c;
  --color-accent-2-500: #2f5d50;
  --color-accent-2-600: #274e43;
  --color-accent-2-700: #1f3e36;
  --color-accent-2-800: #172e28;
  --color-accent-2-900: #10201c;

  /* Urbanist throughout — one family, no second interface font. The two token
     names survive because the whole sheet reads through them and a component
     should still be able to say "this is display type"; they resolve to the
     same stack. Noto Sans Ethiopic is the same system carrying Amharic, not an
     additional face.

     Hierarchy comes from size, spacing, weight and colour. Never from a thin
     weight and never from low opacity: 300 is not loaded and must not be used
     for site content. Headings are 500 Medium, buttons and card titles 600
     SemiBold, body 400 Regular. */
  --font-heading: "Urbanist", "Noto Sans Ethiopic", system-ui, sans-serif;
  --font-heading-weight: 500;
  --font-body: "Urbanist", "Noto Sans Ethiopic", system-ui, sans-serif;

  /* The weights the system sets, named so a component never types a number. */
  --weight-body: 400;
  --weight-medium: 500;   /* headings, nav, eyebrow, labels */
  --weight-semibold: 600; /* buttons, card titles */

  /* Spacing — a 4px base. The ladder now runs past 32 to the section-scale
     steps the layout actually needs: 48-64 inside a section, 80-96 between
     them. Those were arbitrary numbers typed into rules before they were
     tokens. --space-7 (28px) is kept because rules reference it; prefer 24 or
     32 in anything new. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 28px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;

  /* Structured and editorial, not soft. Buttons and inputs 4, cards 6-8,
     large feature surfaces 8-12. Nothing rounder, and no pills. */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;
  --radius-xl: 12px;

  /* Shared page containers, so a section is never positioned on its own. */
  --container-max: 1440px;
  --content-max: 1200px;
  /* The page's own rhythm and gutter. Both are stated here and mirrored in
     scripts/eca_elementor.py, because Elementor writes its padding into the
     page data and cannot read a custom property — change one, change the
     other. The section pad steps up at 48rem; see `.section`.

     `--gutter` was already here, declared 20px under a comment saying "the
     media queries below step it to 32/48/64". There are no such media queries
     and nothing read the token, so the comment described a ladder that was
     never built. It is one clamp now, and `.wrap` reads it. 24 at the top is
     the Elementor gutter, so a band a plugin draws starts on the same line as
     the band above it. */
  --section-pad-y: 44px;
  --gutter: clamp(20px, 2vw, 24px);

  /* Transitions stay subtle. No dramatic animation, no unrelated hover colour. */
  --transition-fast: 150ms ease;
  --transition-default: 200ms ease;

  /* Type scale — the brand's desktop ladder, under the step names the sheet
     already reads through. The ladder is shorter than the names suggest because
     the brand collapses the bottom of it: eyebrow and caption are both 12, and
     navigation, buttons and body-small are all 14. Steps therefore repeat a
     value. That is the scale being honest rather than the names being wrong —
     the old ramp ran down to 10px and 11px, which is the "too small, too thin"
     the spec asks to normalise away. Nothing in the system sets type below 12.

       12  eyebrow, caption          20  H4
       14  nav, button, body small   24  H3
       16  body                      30  H2
       17  body large                36  H1
                                     44  display
                                     52  display XL  */
  --text-2xs: 12px;
  --text-xs: 12px;
  --text-sm: 12px;
  --text-md: 14px;
  --text-base: 14px;
  --text-lg: 14px;
  --text-xl: 16px;
  --text-2xl: 17px;
  --text-3xl: 20px;
  --text-4xl: 24px;
  --text-5xl: 30px;
  --text-6xl: 36px;
  --text-display: 44px;
  --text-display-xl: 52px;

  /* Line heights, which the brand sets per step rather than globally. */
  --leading-display: 1.10;
  --leading-heading: 1.20;
  --leading-body: 1.60;
  --leading-tight: 1.20;   /* nav, buttons, eyebrow sits at 1.4 */

  /* Named for the one place each is used, so it can retune alone. */
  --text-mark-sub: 12px;     /* .mark-sub */
  --text-card-title: 18px;   /* .card-title — 18/600 */
  --text-stat: 44px;         /* .stat-num — display */

  /* Elevation, used sparingly. Hierarchy on this site comes from spacing,
     surface colour, borders, typography and imagery — a shadow is not added to
     a card, a button or a surface by default, and these three are deliberately
     shallow so that reaching for one costs almost nothing visually. They were
     three to four times this depth. */
  --shadow-sm: 0 1px 2px color-mix(in srgb, var(--eca-charcoal) 6%, transparent);
  --shadow-md: 0 2px 6px color-mix(in srgb, var(--eca-charcoal) 7%, transparent);
  --shadow-lg: 0 6px 20px color-mix(in srgb, var(--eca-charcoal) 10%, transparent);

  /* ── Derived colours ──────────────────────────────────────────────────────
     Every translucent value the system uses, resolved once here. A tool that
     reads tokens (Figma, a theme editor) cannot evaluate a color-mix() buried
     in a component rule; it can read these. Never write a bare color-mix() in
     the layers below — add a token here and use it. */

  /* Text weights are COLOURS now, not opacities. The brand sets two type
     colours — Charcoal primary, Volcanic Stone secondary — and asks explicitly
     that hierarchy come from size, spacing, weight and colour rather than from
     low opacity. A translucent ink also changes value with whatever it happens
     to sit on, which is how these three came to be a measured ladder rather
     than a decision. The three names survive because the sheet reads through
     them; there are only two distinct values behind them.

     Woven Taupe is NOT a third step here. The brand's reference calls it muted
     text, but it reads 2.1:1 on Morning Mist and fails AA in every direction —
     it is the border colour, and ./build-check.sh enforces the floor. */
  --text-soft: var(--color-text);
  --text-muted: var(--color-text-secondary);
  --text-subtle: var(--color-text-secondary);

  /* interaction tints on the light ground */
  --tint-faint: color-mix(in srgb, var(--color-text) 4%, transparent);
  --tint-hover: color-mix(in srgb, var(--color-text) 8%, transparent);
  --tint-active: color-mix(in srgb, var(--color-text) 14%, transparent);
  --border-strong: var(--color-stone);   /* the emphatic border; the default is Woven Taupe */

  /* interaction tints in the accent */
  --tint-accent-hover: color-mix(in srgb, var(--color-accent) 10%, transparent);
  --tint-accent-active: color-mix(in srgb, var(--color-accent) 18%, transparent);
  --tint-accent-selection: color-mix(in srgb, var(--color-accent) 30%, transparent);

  /* the same three jobs on an inverted ground — a band, a scrim, a photograph */
  --tint-on-dark-hover: color-mix(in srgb, var(--color-neutral-100) 16%, transparent);
  --tint-on-dark-active: color-mix(in srgb, var(--color-neutral-100) 22%, transparent);
  --border-on-dark: color-mix(in srgb, var(--color-neutral-100) 30%, transparent);
  --border-on-dark-strong: color-mix(in srgb, var(--color-neutral-100) 55%, transparent);

  /* scrims and translucent grounds */
  --scrim: color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 50%, transparent);
  --scrim-soft: color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 32%, transparent);
  --scrim-chip: color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 62%, transparent);
  --scrim-strong: color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 55%, transparent);
  --surface-glass: color-mix(in srgb, var(--color-bg) 92%, transparent);

  /* The header is fixed and the opener runs under it, so its height is a value
     several rules have to agree on rather than a number typed three times. */
  --head-h: 96px;

  /* ── Surfaces that were glass ─────────────────────────────────────────────
     The system used to render cards, panels and overlays as translucent blurred
     glass at three weights. The brand asks for the opposite: restrained,
     editorial surfaces where hierarchy comes from surface colour, borders,
     spacing and typography, and it names generic treatments of that kind among
     the things to avoid.

     The token names survive so the ~66 rules that read them keep working; the
     material behind them does not. Surfaces are now opaque brand colours with a
     Woven Taupe hairline, and the only blur left is the fixed header's, where
     content genuinely scrolls underneath. Set --glass-blur back above zero and
     the material returns everywhere at once — this is one decision, in one
     place, and it is reversible. */
  --glass-blur-thin: 8px;   /* the fixed header only */
  --glass-blur: 0px;
  --glass-blur-heavy: 0px;
  --glass-saturate: 1;

  --glass-bg: var(--color-surface);
  --glass-bg-thin: color-mix(in srgb, var(--color-bg) 94%, transparent);
  --glass-bg-heavy: var(--color-surface);
  --glass-border: var(--color-taupe);
  --glass-edge: none;

  /* The footer is a structural dark surface, so it is Charcoal — not a role
     token. --color-text re-maps to Morning Mist on the dark theme, and this
     rule once painted a light footer with light text on it for exactly that
     reason. A brand colour holds still across both themes. */
  --footer-bg: var(--eca-charcoal);

  color-scheme: light;
}

/* ── The dark theme ────────────────────────────────────────────────────────
   Only the ROLE tokens re-map. The four ramps stay absolute, because they are
   brand colours and a name must mean one thing — inverting them would make
   --color-neutral-100 two different colours and break the Figma sync's
   one-name-one-authority rule. Everything under "Derived colours" above is
   written as a color-mix() off a role token, and custom properties resolve at
   use rather than at definition, so every tint, scrim and text weight follows
   this block automatically. That is why it is short.

   NOTE: scripts/sync-figma-tokens.py reads only the FIRST `:root {` block, so
   nothing below is visible to it. The theme's five role colours DO round-trip:
   they live above as --color-{bg,surface,text,accent,accent-2}-dark and are
   fed by the On Dark mode of the Figma Color collection through EXTRA_MODES
   in the export script. What this block adds beyond those five — the swatch
   bases, glass grammar, shadows, grain — is maintained by hand and the sync
   can neither clobber nor update it.

   The accent is a light step, not the base, and the reason has not changed
   with the palette: Cherry Berry #7a1e22 measures 1.67 on this ground, worse
   than Coffee Cherry's old 3.09, and fails both the chrome floor and the
   button-label floor. readme.md's inverted-ground rule predicts exactly this.
   The tint below reads 5.28. */
:root[data-theme="dark"] {
  /* ── The dark swatch, folded back onto the brand ──────────────────────────
     This theme used to run on five colours of its own — Deep Espresso, Rich
     Coffee, Cherry Berry, Harvest Gold, Forest Leaf — none of which is an
     approved brand colour. Dark mode is the same identity at a deeper
     exposure, so each of those names now forwards to the brand colour doing
     its job. The names survive because ~30 rules below reference them; there
     is no separate dark palette behind them any more.

       Cherry Berry  -> Heritage Clay,  the one action colour, in both themes
       Forest Leaf   -> Forest Canopy,  the nature accent
       Harvest Gold  -> Brass,          the restricted premium accent, which is
                                        exactly what a gilding/edge-light role is

     The two _-tint names still forward to the --color-*-dark authorities in
     the first :root: those are the lifted steps used where the accent is TYPE
     rather than a fill. build-check follows var() chains, so the gate resolves
     them. */
  --color-cherry-berry: var(--eca-heritage-clay);  /* fills, edges, scrims — never type */
  --color-harvest-gold: var(--eca-brass);          /* premium detail only, never a CTA */
  --color-forest-leaf: var(--eca-forest-canopy);

  --color-cherry-berry-tint: var(--color-accent-dark);
  --color-forest-leaf-tint: var(--color-accent-2-dark);
  --color-cherry-berry-hover: color-mix(in srgb, var(--color-cherry-berry) 82%, white);

  --color-bg: var(--color-bg-dark);
  --color-surface: var(--color-surface-dark);
  --color-text: var(--color-text-dark);
  --color-text-secondary: var(--color-text-secondary-dark);
  --color-accent: var(--color-cherry-berry-tint);
  --color-accent-2: var(--color-forest-leaf-tint);

  /* Roasted Earth is the warm dark surface the brand asks dark mode to keep,
     so the coffee role stays warm here rather than following the ground. */
  --color-coffee: var(--eca-roasted-earth);

  /* Elevation stays sparing on dark too. A separated surface here is a lighter
     surface with a hairline on it, not a deep drop shadow. */
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.25);
  --shadow-md: 0 2px 6px rgb(0 0 0 / 0.30);
  --shadow-lg: 0 6px 20px rgb(0 0 0 / 0.38);

  /* Surfaces are opaque here as well: Volcanic Stone panels on a Charcoal
     ground, separated by a border rather than by a blur. */
  --glass-bg: var(--color-surface);
  --glass-bg-thin: color-mix(in srgb, var(--color-bg) 94%, transparent);
  --glass-bg-heavy: var(--color-surface);
  --glass-bg-band: var(--color-forest-leaf);
  --glass-border: var(--border-on-dark);
  --glass-edge: none;

  /* Same as light: the header blurs because content scrolls under it, and
     nothing else does. */
  --glass-blur-thin: 8px;
  --glass-blur: 0px;
  --glass-blur-heavy: 0px;
  --glass-saturate: 1;

  /* ── Grain ────────────────────────────────────────────────────────────────
     Removed. The dark grounds carried a lit, blended noise tile that gave every
     surface a leather texture. It is a generic material treatment, it is not in
     the brand's visual language, and the brand asks for restraint and for
     hierarchy to come from surface colour, spacing, borders and typography.

     The token stays defined as `none` rather than deleted because the four
     ground rules further down compose it into a `background` shorthand; `none`
     is a valid background-image and the blend modes beside it become no-ops.
     Put a tile back here and the texture returns everywhere at once. */
  --grain: none;

  /* ── The scrim ink ────────────────────────────────────────────────────────
     Every scrim in this sheet mixes off one ink. On the light theme that ink is
     --color-neutral-900 and nothing here changes it: the gradients name this
     token with neutral-900 as their fallback, so a theme that never defines it
     renders exactly the values it rendered before.

     Dark keeps the ink NEUTRAL, and it is Charcoal — the brand's own deepest
     neutral, and the same ink the light theme reaches through
     --color-neutral-900. It carried 38% Cherry Berry once, and every scrim laid
     a burgundy cast over the photograph beneath it: a treatment by another
     name, and photographs run untreated. */
  --scrim-ink: var(--eca-charcoal);

  /* The ambient field is off. Dark used to lay five large coloured blobs down
     the page so that translucent panels had something to blur; with opaque
     surfaces there is nothing to light, and a drifting multi-hue wash is the
     kind of generic treatment the brand asks to avoid. `none` keeps the
     body::before rule valid — put gradients back here and the field returns. */
  --ambient-field: none;

  /* The footer is Charcoal in both themes; on dark that makes it continuous
     with the ground, so the Woven Taupe hairline above it is what separates it
     rather than a step down into near-black. */
  /* The page bottoms out. The footer was Charcoal on a Charcoal ground — the
     same colour as the page, so it did not read as a footer at all. */
  --footer-bg: var(--color-neutral-1000);

  /* A hairline on a deep ground is a LIFT, not a light line. --color-divider
     is Woven Taupe for the light theme and nothing redefined it here, so every
     rule and card edge on the dark theme was drawn in a colour that reads as
     type. A step between the ground and Volcanic Stone instead. */
  --color-divider: color-mix(in srgb, var(--eca-volcanic-stone) 62%, var(--color-neutral-950));

  color-scheme: dark;
}

body {
  position: relative;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
}
h1, h2, h3, h4 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); }

/* ── Motion ────────────────────────────────────────────────────────────────
   Three effects, all decorative, all switched off under prefers-reduced-motion
   at the bottom of this block. There were no motion guards in this stylesheet
   before; a 25s drift over a full-viewport photograph is exactly the thing the
   media query exists for. */
@keyframes ken-burns { from { transform: scale(1); } to { transform: scale(1.08); } }
@keyframes fade-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* backwards, not both. The to-state is the natural state, so forwards fill
   changes nothing you can see, but it keeps the transform keyframe applied to
   <main> forever, and an element with an applied transform is the containing
   block for every position: fixed descendant. The viewport-pinned leather on
   the grounds below was silently becoming a 9000px document-pinned one. */
main { animation: fade-in 0.7s cubic-bezier(0.16, 1, 0.3, 1) backwards; }

/* Photographs lift slightly under the cursor. The wrapper clips, the image
   moves — so the frame stays put and only the picture inside it grows. */
.card-gallery .media, .zoom { overflow: hidden; }
.card-gallery .media img, .zoom img { transition: transform 0.8s cubic-bezier(0.25, 1, 0.5, 1); }
.card-gallery:hover .media img, .zoom:hover img { transform: scale(1.05); }

/* A 6px rail in the accent rather than the platform's default slab. */
* { scrollbar-width: thin; scrollbar-color: var(--color-accent) transparent; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-accent); border-radius: 3px; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .photo-hero.opener::before { animation: none; }
  main { animation: none; }
  .card-gallery .media img, .zoom img { transition: none; }
  .card-gallery:hover .media img, .zoom:hover img { transform: none; }
  /* The nav states stay, they carry meaning. Only their travel is dropped. */
  :root[data-theme="dark"] .nav-links a::after,
  :root[data-theme="dark"] .nav-panel a,
  :root[data-theme="dark"] .nav-panel a,
  :root[data-theme="dark"] .nav-panel a::before { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   Components — built with the tokens above. Plain CSS
   on plain HTML: no JavaScript, no build step. Each class is documented in
   readme.md and demonstrated in foundations/ and components/.
   ══════════════════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-size: var(--text-xl); line-height: var(--leading-body); font-weight: var(--weight-body); }
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: var(--leading-heading); letter-spacing: 0; margin: 0 0 var(--space-2);
  color: var(--color-text);
}
/* 36 / 30 / 24 / 20, each with the line height the brand sets for it. h4 no
   longer switches family or jumps to 700: there is one family now, and every
   heading is 500 Medium. h5 and h6 are below the brand's ladder and are
   labels rather than headings, so they take the label weight. */
h1 { font-size: var(--text-6xl); line-height: 1.15; }
h2 { font-size: var(--text-5xl); line-height: 1.20; }
h3 { font-size: var(--text-4xl); line-height: 1.25; }
h4 { font-size: var(--text-3xl); line-height: 1.30; }
h5 { font-size: var(--text-md); font-weight: var(--weight-medium); line-height: var(--leading-tight); }
h6 { font-size: var(--text-sm); font-weight: var(--weight-medium); line-height: var(--leading-tight); }
@media (min-width: 48rem) {
  h1 { font-size: var(--text-6xl); line-height: 1.15; }
  h2 { font-size: var(--text-5xl); line-height: 1.25; }
  h3 { font-size: var(--text-4xl); line-height: 1.35; }
  h4 { font-size: var(--text-2xl); }
}
:lang(am) { font-size: 1.05em; }
h6 { letter-spacing: 0.08em; text-transform: uppercase; }
p { margin: 0 0 var(--space-3); }
a { color: var(--color-accent); text-underline-offset: 3px; }
a:hover { color: var(--color-accent-700); }
img { display: block; max-width: 100%; }
figure { margin: 0; }
figcaption {
  font-size: var(--text-xs); margin-top: var(--space-1);
  color: var(--text-muted);
}
.text-muted { color: var(--text-muted); }
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: var(--tint-accent-selection); }

/* — rules — */
.hr {
  height: 1px; border: 0; margin: var(--space-4) 0;
  background: var(--color-divider);
}

/* — buttons — */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  cursor: pointer; text-decoration: none;
  font-family: var(--font-body); font-weight: var(--weight-semibold);
  font-size: var(--text-base); line-height: var(--leading-tight); color: var(--color-text);
  background: transparent; border: 1px solid transparent;
  /* 44px is the brand's button height and also the touch target floor, so the
     padding is stated as a minimum height rather than as vertical padding that
     happens to add up. */
  min-height: 44px;
  padding: var(--space-3) var(--space-6);
  border-radius: var(--radius-sm);
  transition: background var(--transition-fast), color var(--transition-fast),
              border-color var(--transition-fast);
}
.btn svg { display: block; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* Heritage Clay is the only action colour. Its label is Morning Mist rather
   than the ground token: on dark the ground is Charcoal, and a Charcoal label
   on a Heritage Clay fill reads 2.0:1. Morning Mist on that fill reads 5.4 in
   both themes, which is the whole reason the brand pairs them. */
.btn-primary { background: var(--color-accent); color: var(--eca-morning-mist); }
.btn-primary:hover { background: var(--color-accent-600); }
.btn-primary:active { background: var(--color-accent-700); }
/* A primary button on an <a> is a CONTROL, not a text link. The global link
   hover (a:hover → accent-700 ink) and every scoped link rule (.nav a,
   .site-foot a:hover) outrank a single-class .btn-primary, which is how a
   CTA's label turns accent-700 on the accent-600 hover fill and vanishes.
   The doubled class beats every anchor rule at any source position; the dark
   block re-asserts its own label colour at higher specificity still. */
a.btn.btn-primary, a.btn.btn-primary:hover { color: var(--eca-morning-mist); }
/* The secondary button is an outlined Heritage Clay button, not a neutral one:
   the brand draws both actions in the same colour and separates them by fill. */
.btn-secondary { color: var(--color-accent); border-color: var(--color-accent); }
.btn-secondary:hover { background: var(--tint-accent-hover); }
.btn-secondary:active { background: var(--tint-accent-active); }
/* The text action. Medium, not SemiBold — it is the quietest of the three. */
.btn-ghost { color: var(--color-accent); font-weight: var(--weight-medium);
  min-height: auto; padding: var(--space-1) var(--space-1); }
.btn-ghost:hover { background: var(--tint-accent-hover); }
.btn-ghost:active { background: var(--tint-accent-active); }
.btn-icon { width: 36px; height: 36px; padding: 0; }
.btn-block { width: 100%; margin-top: var(--space-2); }

/* — forms — */
.field > label {
  display: block; font-size: var(--text-sm); margin-bottom: 5px;
  color: var(--text-soft);
}
/* An input is 44px tall at 16px/400, on a Woven Taupe hairline with a 4px
   radius — the same height and radius as a button, so a field and the control
   beside it line up. It was 36px at the interface size, which read as a form
   bolted to the page rather than part of it.

   16px is also the size below which iOS zooms the viewport on focus. */
.input {
  width: 100%; min-height: 44px; padding: var(--space-3) var(--space-4); font: inherit;
  font-size: var(--text-xl); font-weight: var(--weight-body);
  color: var(--color-text); caret-color: var(--color-accent);
  background: var(--color-bg);
  border: 1px solid var(--color-taupe); border-radius: var(--radius-sm);
  transition: border-color var(--transition-fast);
}
.input::placeholder { color: var(--color-text-secondary); opacity: 0.7; }
.input:hover { border-color: var(--border-strong); }
/* Focus is a border colour change and nothing else — no ring, no glow. The
   focus state has to stay clearly visible for keyboard users, which a 1px
   Heritage Clay edge on a taupe field is: the hue changes, not just the value. */
.input:focus, .input:focus-visible {
  border-color: var(--color-accent); outline: 2px solid transparent; outline-offset: 0;
}
/* Error and disabled, both drawn the way the reference draws them: the error
   field keeps the clay edge and says why underneath, and a disabled field fills
   rather than fading, so it reads as unavailable and not as low contrast. */
.input[aria-invalid="true"], .input-error { border-color: var(--color-accent); }
.field-error {
  display: flex; align-items: center; gap: var(--space-2);
  margin-top: var(--space-2); font-size: var(--text-md);
  color: var(--color-accent);
}
.field-error::before {
  content: "!"; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--color-accent); color: var(--eca-morning-mist);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold); line-height: 1;
}
.input:disabled, .input[disabled] {
  background: var(--color-surface); border-color: var(--color-taupe);
  color: var(--color-text-secondary); cursor: not-allowed;
}
textarea.input { min-height: 108px; resize: vertical; }
.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: var(--text-base); }
.radio input, .seg-opt input {
  position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
}
.radio .dot {
  width: 16px; height: 16px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--color-divider);
}
.radio:hover .dot { border-color: var(--color-accent); }
.radio input:checked + .dot {
  border-color: var(--color-accent); background: var(--color-accent);
  box-shadow: inset 0 0 0 4px var(--color-bg);
}
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.seg {
  display: inline-flex; overflow: hidden;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.seg-opt {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; font-size: var(--text-md); cursor: pointer;
  /* The unselected option had no background at all, so a <button> fell through
     to the UA's `buttonface`. On the light theme that resolved to #efefef and
     passed for a surface; the moment color-scheme:dark was declared the same
     default became #6b6b6b and a grey slab appeared mid-page. readme.md's rule
     is that interactive states are themed, never browser defaults — this one
     was a browser default hiding behind a coincidence. */
  background: transparent; color: inherit; border: 0;
}
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt:has(input:checked) { background: var(--color-accent); color: var(--color-bg); }
/* The same selected state for a `.seg` built from buttons rather than radios.
   A radio's checked state is owned by the browser, which suits the language
   switch: it never changes after load. A theme switch does change, and a
   framework-rendered radio does not reliably re-check — so that one is two
   buttons carrying `aria-pressed`, which is the right ARIA for a control that
   reports which of two states is in force. */
.seg-opt[aria-pressed="true"] { background: var(--color-accent); color: var(--color-bg); }
:root[data-theme="dark"] .seg-opt[aria-pressed="true"] { background: var(--color-cherry-berry); color: var(--color-text); }
.seg-opt:not(:has(input:checked)):hover { background: var(--tint-hover); }
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* — the language switch — */
/* `.seg` at the toolbar's control scale, so it sits beside the account door
   rather than towering over it. Two options of three letters cost the header
   about 90px, which is the whole brief: a switch, not a language menu.

   No new control: a language choice IS a radio group, and `.seg` already draws
   one. The only additions are the header's scale and the ENG/AMH label
   treatment. It grows to the 44px floor on a coarse pointer with every other
   `.seg-opt`, from the rule that already covers them. */
/* Sized to the account door beside it: same 34px height, same 14px inside the
   edge, so the two controls sit as a pair rather than as a button and a
   smaller thing next to it. */
.lang-switch { flex: none; }
.lang-switch .seg-opt {
  padding: 0 14px; min-height: 34px;
  font-size: var(--text-md); font-weight: var(--weight-semibold);
  letter-spacing: 0.06em;
}
/* The chosen language is OUTLINED, not filled. A filled accent reads as an
   action, and this reports a state — beside a Heritage Clay "Sign in" the two
   were competing for the same meaning. The ring is an inset shadow rather than
   a border so nothing shifts by a pixel when the choice moves.

   Dark takes the lifted step for the same reason `.role-card-go` does: this
   control sits on the toolbar's ground AND inside the drawer's raised surface,
   and the base accent reads 3.30 on the latter. */
.lang-switch .seg-opt:has(input:checked) {
  background: transparent;
  color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
:root[data-theme="dark"] .lang-switch .seg-opt:has(input:checked) {
  /* `background` is restated, not inherited from the rule above: the dark
     theme re-asserts the filled `.seg` state further down the sheet, and a
     rule that only changes the ink leaves that fill in place. */
  background: transparent;
  color: var(--color-accent-300);
  box-shadow: inset 0 0 0 1px var(--color-accent-300);
}
/* The 44px floor again, restated. The blanket `.seg-opt` rule in the coarse
   block is one class and the rule above is two, so the toolbar scale beat the
   touch floor and the switch shipped at 34px on a phone — the exact trade the
   Touch section says not to make. Restated at equal specificity. */
@media (pointer: coarse) {
  .lang-switch .seg-opt { min-height: 44px; }
}
/* No narrow-width squeeze any more. The switch used to give back its padding
   at 375px to keep the row from overflowing; it moves into the drawer now
   instead, like the links and the account door, so it can stay the size of the
   control it sits beside. */
/* Below 20rem four controls will not share the line however tight the switch
   gets, so the wordmark gives up its words. The roundel IS the mark and stays;
   the name is clipped rather than removed, so the home link keeps the
   accessible text a screen reader announces it by. */
@media (max-width: 20rem) {
  .site-head .mark-name {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}

/* The stub the second option opens. One element is the whole overlay — its own
   backdrop, the card centred on it — because a separate scrim node is a second
   thing to keep in step, and this site already has two of those.

   `[hidden]` is stated because the element carries `display: flex`, and the
   attribute is only `display: none` in the user-agent sheet. Every overlay in
   this codebase has had to learn that once; this one is born knowing it. */
.lang-dialog {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: var(--gutter);
  /* A scrim is always dark. Built on the ink it inverted with the theme, so on
     the dark theme the stub arrived behind a white wash. */
  background: var(--scrim-strong);
}
.lang-dialog[hidden] { display: none !important; }
.lang-dialog > .card { max-width: 46ch; box-shadow: var(--shadow-md); }
.lang-dialog h2 { margin: 0; font-size: var(--text-4xl); }

/* — cards — */
/* A card is a surface colour, a hairline and generous padding — not a shadow.
   24px padding and a 6px radius are the brand's own card values. */
.card {
  display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-6); border-radius: var(--radius-md);
  background: var(--color-surface);
  border: var(--border-width, 1px) solid var(--color-taupe);
}
/* The kicker is a label, not a state. In the accent it put salmon on every card
   on the site, and a colour that appears everywhere stops meaning anything; the
   accent's whole job here is "live, selected or actionable". Labels belong to
   the institutional second voice, which is what --color-accent-2 is for. The
   kickers on photographs keep their pinned light value further down. */
.card-kicker {
  font-size: var(--text-2xs); font-weight: var(--weight-medium); line-height: 1.4;
  letter-spacing: 0.10em; text-transform: uppercase; color: var(--color-text-secondary);
}
/* Card titles are the one heading the brand sets in SemiBold. */
.card-title {
  font-family: var(--font-heading); font-weight: var(--weight-semibold);
  font-size: var(--text-card-title); line-height: 1.3;
}
/* Body copy on a card is body copy: full-strength secondary ink at the body
   size, not 80% opacity at 13px. Opacity is not a hierarchy tool here. */
.card-body { margin: 0; font-size: var(--text-xl); line-height: var(--leading-body);
  color: var(--color-text-secondary); flex: 1; }
.card-meta {
  display: flex; align-items: center; gap: 6px; font-size: var(--text-xs);
  color: var(--text-subtle);
}
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

/* ══ THE BRAND'S OWN COMPONENT LANGUAGE ══════════════════════════════════════
   Five things the reference draws that the written specification does not name.
   They are what makes a page read as ECA rather than as a correct palette.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── The action arrow ─────────────────────────────────────────────────────
   Every content call to action in the reference carries a trailing arrow, set
   apart from the label rather than tucked against it. The one button that does
   NOT is the Member Portal chip in the navigation — a destination, not an
   action — which is why this is opt-in by class rather than blanket on .btn.

   The arrow is drawn by the component, never typed into the copy. It used to
   be typed: eighteen labels in index.html carried their own character, so the
   spacing was whatever the writer left and a label could not be translated
   without carrying punctuation into the translation. */
.btn-primary::after,
.btn-secondary::after,
.btn-ghost::after,
.eca-cta .elementor-button-content-wrapper::after {
  content: "→";
  margin-left: auto;
  padding-left: var(--space-4);
  font-size: 1.05em; line-height: 1;
  transition: transform var(--transition-fast);
}
/* space-between, so the arrow sits at the trailing edge of the button rather
   than beside the word — the reference's buttons are wider than their label. */
.btn-primary, .btn-secondary { justify-content: space-between; min-width: 180px; }
.eca-cta .elementor-button-content-wrapper { display: flex; align-items: center; width: 100%; }
.btn-ghost::after { margin-left: 0; padding-left: var(--space-2); }
.btn-primary:hover::after,
.btn-secondary:hover::after,
.btn-ghost:hover::after,
.eca-cta:hover .elementor-button-content-wrapper::after { transform: translateX(2px); }
/* An icon button has no label to trail. A back link points the other way, and
   draws its arrow LEADING rather than trailing — the direction is the meaning,
   so it is a component too and not a character in the label. */
.btn-icon::after, .btn-block.btn-icon::after { content: none; }
.btn-back::after, .eca-back .elementor-button-content-wrapper::after { content: none; }
.btn-back { justify-content: flex-start; min-width: 0; }
.btn-back::before,
.eca-back .elementor-button-content-wrapper::before {
  content: "←"; padding-right: var(--space-2);
  font-size: 1.05em; line-height: 1;
  transition: transform var(--transition-fast);
}
.btn-back:hover::before,
.eca-back:hover .elementor-button-content-wrapper::before { transform: translateX(-2px); }

/* ── The accent rule ──────────────────────────────────────────────────────
   A short Heritage Clay bar under a display heading, in both themes. It is the
   reference's one piece of pure ornament and it does a real job: it closes the
   headline block before the buttons start, which is why the hero does not need
   a divider there. */
/* Drawn as a border rather than a background: Elementor resets `background` on
   an <hr> inside a widget, which left the rule correctly sized at 48x3 and
   painted transparent. A border survives that reset. */
.rule-accent, .elementor hr.rule-accent {
  display: block; width: 48px; height: 0; flex: none;
  background: none; opacity: 1;
  border: 0; border-top: 3px solid var(--color-accent);
  margin: var(--space-6) 0;
}

/* ── Emphasis inside a headline ───────────────────────────────────────────
   "Ethiopia. / The Origin. / The Future." — the middle line is italic Heritage
   Clay. Heritage Clay may emphasise a word or phrase in a heading; it does not
   replace Charcoal as the heading colour, so this is a span inside a heading
   and never a colour set on the heading itself. */
.hl-accent { color: var(--color-accent); font-style: italic; }

/* ── Dividers ─────────────────────────────────────────────────────────────
   Structure without distraction: 1px Woven Taupe. The reference draws a dashed
   variant beside the solid one, for a separation that should read as lighter
   than a rule — a break inside a block rather than between two. */
/* Elementor resets <hr> twice — `background: none` and `margin: 0` — and its
   `.elementor hr` selector outranks a bare class. Both rules here are written
   to beat it rather than to be silently undone: the same reset already made
   .rule-accent render at exactly 48x3 and paint nothing. Anything this system
   draws as an <hr> has to clear that hurdle. */
.divider, .elementor hr.divider {
  border: 0; border-top: 1px solid var(--color-taupe);
  margin: var(--space-6) 0; opacity: 1;
}
.divider-dashed, .elementor hr.divider-dashed { border-top-style: dashed; }

/* ── Cards that carry an image ────────────────────────────────────────────
   The image is flush to the card's edges and the padding belongs to the text
   block below it, so the photograph reads as one face of the card rather than
   as a picture sitting inside a frame. That is most of what makes the
   reference's cards look built rather than composed.

   .card keeps its 24px padding for text-only cards; a card that opens with a
   media block drops its own padding and hands it to the body. */
.card:has(> .card-media:first-child) { padding: 0; overflow: hidden; }
.card > .card-media {
  display: block; position: relative; margin: 0;
  aspect-ratio: 16 / 10; background-size: cover; background-position: center;
}
.card > .card-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.card > .card-media + .card-body-block { padding: var(--space-6); }
/* Tight: a title and its two lines of meta sit together as one block. The gap
   was --space-2 with a divider between the lines, which spread a three-line
   card over the height of a five-line one. */
.card > .card-body-block {
  display: flex; flex-direction: column; gap: var(--space-1);
}
.card > .card-body-block .card-title { margin-bottom: var(--space-1); }
/* Both meta lines are body size in the secondary ink — the second one used to
   drop to 12px and a lighter ink, which read as a footnote rather than as the
   other half of the same fact. */
.card > .card-body-block .card-body,
.card > .card-body-block .card-meta {
  font-size: var(--text-xl); line-height: 1.5; color: var(--color-text-secondary);
  flex: none; margin: 0;
}

/* The badge that sits on the image. Morning Mist on the photograph, its tick in
   Heritage Clay — a status, not an action, so it is never clickable and never
   carries the arrow. */
.card-badge {
  position: absolute; top: var(--space-3); right: var(--space-3);
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: 5px var(--space-2) 5px 7px; border-radius: var(--radius-sm);
  background: var(--eca-morning-mist); color: var(--eca-charcoal);
  font-size: var(--text-2xs); font-weight: var(--weight-semibold);
  letter-spacing: 0.06em; text-transform: uppercase; line-height: 1;
  box-shadow: var(--shadow-sm);
}
.card-badge::before { content: "✓"; color: var(--color-accent); font-size: 1.1em; line-height: 1; }

/* — tags — */
.tag {
  display: inline-flex; align-items: center; font-size: var(--text-xs);
  letter-spacing: 0.02em; padding: 3px 10px;
  border-radius: var(--radius-sm);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
/* neutral-100 IS --color-surface, so a neutral tag was invisible on any card.
   300 is the first step that reads on both the card and the page ground. */
.tag-neutral { background: var(--color-neutral-300); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }

/* — navigation — */
.nav {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: none;
}
.nav-brand {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: var(--text-2xl); margin-right: auto;
}
.nav a { color: inherit; text-decoration: none; font-size: var(--text-base); }
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }

/* — tables — */
.table { width: 100%; border-collapse: collapse; font-size: var(--text-base); }
.table th {
  text-align: left; font-size: var(--text-xs); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--text-muted);
  padding: var(--space-2); border-bottom: 1px solid var(--color-divider);
}
.table td {
  padding: var(--space-2);
  border-bottom: 1px solid var(--tint-hover);
}
.table tbody tr:hover { background: var(--tint-faint); }

/* — dialog — */
.dialog-backdrop {
  position: fixed; inset: 0; display: grid; place-items: center;
  padding: var(--space-4);
  background: var(--scrim);
}
.dialog {
  width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-lg);
}
.dialog-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: var(--text-3xl);
}
.dialog-body { font-size: var(--text-base); opacity: 0.85; }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-2); }

/* — rounded frame: everything softens, small controls go pill — */
.card, .dialog { border-radius: var(--radius-lg); }
.btn, .tag, .seg, .input { border-radius: var(--radius-md); }
.input { padding-inline: 14px; }

/* ═══════════════════════════════════════════════════════════════════════
   Site layer — page-level patterns. Mobile-first; one breakpoint at 48rem.
   These are the classes a marketing page composes with; components above are
   what sits inside them.
   ═════════════════════════════════════════════════════════════════ */

/* 1600px, matching the reference's max-w-[100rem]. Body copy does not stretch
   with it: .lede caps at 62ch and .measure at 66ch, so the extra width becomes
   whitespace and full-bleed imagery rather than unreadable line lengths. */
/* `--content-max`, not a second number. This carried `max-width: 1600px` and a
   48px gutter while the token beside it said 1200 and every Elementor section
   on the site was built to it — so a band drawn by a plugin came out 240px
   wider than the band above it, with double the gutter. */
.wrap { width: 100%; max-width: var(--content-max); margin: 0 auto; padding-inline: var(--gutter); }
.wrap-narrow { max-width: 720px; }
.section { padding-block: var(--section-pad-y); }
.section-tint { background: var(--color-surface); }
.band { background: var(--color-accent-2); color: var(--color-neutral-100); }
.band .card { background: var(--color-accent-2-700); }
.band h1, .band h2, .band h3 { color: var(--color-neutral-100); }
.band-dark { background: var(--color-text); color: var(--color-neutral-200); }
.band-dark h1, .band-dark h2, .band-dark h3 { color: var(--color-neutral-100); }
/* --color-accent-2 does double duty: a text colour on eyebrows, steps and chips,
   and the BACKGROUND of .band. On dark it re-maps to the light 300 step so the
   text usage stays legible, which would turn the Highland Green band into a pale
   slab. The two backgrounds are therefore pinned to dark ramp steps by hand.
   .band-dark is worse: it paints --color-text, which on this theme is #f4f1ea.
   Set together with the grain layer, in the Depth section below, so a
   ground's colour and its texture have one home instead of two. */

/* The country silhouette is --color-surface, which reads 7.01 against the light
   Highland Green band and 1.30 against the dark one — the shape disappears. A
   warm neutral holds the outline at 4.20 without competing with the green band
   or the sage zones drawn on top of it. Zones are separated from the base by
   the 1.4px .atlas-area stroke, not by fill contrast, on either theme. */
:root[data-theme="dark"] .atlas-outline { fill: var(--color-neutral-600); }
.band .btn-primary:hover, .band-dark .btn-primary:hover { background: var(--color-accent-400); }
.eyebrow {
  /* Refined and understated without becoming faint: the brand sets the eyebrow
     in the primary type colour, not in an accent. It was Highland Green at 600,
     which made a coloured label of what should read as a quiet overline. On an
     inverted ground it flips to the dark type colour further down. */
  display: block; font-family: var(--font-body); font-weight: var(--weight-medium);
  font-size: var(--text-sm); line-height: 1.4;
  letter-spacing: 0.10em; text-transform: uppercase; color: var(--color-text);
  margin-bottom: var(--space-4);
}
.band-dark .eyebrow { color: var(--color-taupe); }
.band .eyebrow { color: var(--color-accent-2-200); }
.lede { font-size: var(--text-2xl); line-height: 1.6; max-width: 62ch; }
.measure { max-width: 66ch; }
.small { font-size: var(--text-base); line-height: 1.5; }
.caption { font-size: var(--text-sm); line-height: 1.4; }
/* Copy the association has not signed off on, written to hold a slot the layout
   needs. Italic is the whole rule: it marks the text as provisional to a reader
   and makes every instance findable with one grep when the real figures land. */
.provisional { font-style: italic; }
.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }

/* A section header: label and title on the left, the "see them all" link on the
   right. Used wherever a section previews a list — the Pulse rail, the events
   rail, the academy rail.

   Wrapping is allowed and the link never shrinks, so the two meet on one line
   while there is room and stack when there is not. The title column is told it
   may shrink below its content, which a flex item will not do by default.

   The Elementor pages express this same layout as `.eca-row--split`, in
   elementor-bridge.css, because an Elementor container cannot take a plain
   class for its flex layout. Change one, look at the other. */
.split-head {
  display: flex; flex-wrap: wrap;
  justify-content: space-between; align-items: flex-end;
  gap: var(--space-4);
}
.split-head > :first-child { min-width: 0; }
.split-head > .btn { flex: 0 0 auto; }

.mark {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--font-heading); font-weight: 600; text-decoration: none;
  color: inherit; line-height: 1.05;
}
.mark-arc {
  width: 34px; height: 34px; flex: none; border-radius: 50%;
  background: var(--color-neutral-100) url('assets/eca-buna-coop-logo.png') center / 76% auto no-repeat;
  /* stamp: circular frame, whole jebena CONTAINED — never cropped.
     The light disc keeps the black mark legible on dark grounds too. */
  box-shadow: inset 0 0 0 1px var(--tint-active);
}
.mark-name { font-size: var(--text-lg); letter-spacing: -0.01em; }
.mark-sub {
  display: block; font-family: var(--font-body); font-weight: 500; font-size: var(--text-mark-sub);
  letter-spacing: 0.14em; text-transform: uppercase;
  /* Ink, not transparency. 0.6 on 12px type is the one thing `.card-body` two
     hundred lines up exists to argue against, and it landed here anyway.

     Naming the colour is what makes the inverted case need naming too: the
     footer lock-up sets its own light ink on `.mark` and the sub-line used to
     inherit it. Volcanic Stone on Charcoal is 1.15:1 — the line was there and
     unreadable for as long as it took to measure it. */
  color: var(--color-text-secondary);
}
.site-foot .mark-sub,
.band-dark .mark-sub,
.photo-hero .mark-sub { color: var(--color-neutral-400); }

/* header */
.site-head {
  position: fixed; top: 0; left: 0; right: 0; z-index: 20;
  background: var(--surface-glass);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--color-divider);
}
/* A hairline that fades out at both ends rather than ruling the full width. */
.site-head::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--color-accent), transparent);
  opacity: 0.45; pointer-events: none;
}
.site-head .nav {
  max-width: 1600px; margin: 0 auto; min-height: var(--head-h);
  padding-inline: clamp(20px, 3.5vw, 48px); gap: var(--space-3);
}
/* The header is fixed, so every view owes it the space back — except the opener,
   which deliberately takes it again so the photograph reaches the top edge. */
main { padding-top: var(--head-h); }
.site-head .mark { margin-right: auto; }
/* `min-width: 0` and `overflow: hidden` are what make the row MEASURABLE: a
   flex item will not shrink below its content without the first, and without
   the second an over-long row pushes the account door off the toolbar instead
   of reporting that it overflowed. `nav.js` reads scrollWidth against
   clientWidth and drops items from the trailing end until they agree.

   The breakpoint is now only the pre-script state — below it there is no room
   for an inline link beside the wordmark and the account door at any count, so
   the drawer carries navigation outright. Above it the row appears and the
   measurement decides how much of it survives. It was 56rem, chosen as the
   width seven items stop fitting, which showed nothing at all at 800px where
   four of them fit comfortably. */
.nav-links { display: none; gap: var(--space-4); align-items: center;
  min-width: 0; overflow: hidden; }
/* A toolbar item NEVER wraps. It flows into the hamburger instead.
   nav.js drops trailing items while `scrollWidth > clientWidth`, so a label
   that is allowed to wrap absorbs the overflow itself: the row then reports
   that it fits, nothing is dropped, and the hamburger never appears. Measured
   at 940px, "Coffee Atlas" and "For buyers" each sat on two lines with
   scrollWidth exactly equal to clientWidth — the whole mechanism defeated by
   one default.
   `flex: none` alongside it, so a flex item cannot be shrunk below its label
   either; shrinking is the other way a row pretends to fit. */
.nav-links > a { white-space: nowrap; flex: none; }
@media (min-width: 40rem) { .nav-links { display: flex; } }
/* `hidden` is only display:none in the UA sheet and .nav-links sets a display
   on its children's parent, so the attribute needs saying again — the same
   trap .claude/rules/elementor.md records for the role blocks. */
/* `hidden` is only display:none in the UA sheet, and every one of these sets a
   display of its own — the switch and the account door join the list now that
   nav.js can drop them. */
.nav-links a[hidden], .nav-toggle[hidden], .theme-toggle[hidden],
.nav > .nav-auth[hidden], .nav > .lang-switch[hidden],
.nav-panel[hidden], .nav-scrim[hidden] { display: none !important; }

/* The theme control on the TOOLBAR.
 *
 * The drawer carries the labelled pair, and that was the whole control until a
 * desktop showed the hole in it: nav.js hides the hamburger the moment the
 * toolbar fits, and the hamburger is the only way into the drawer — so on a
 * wide screen there was no route to the theme at all.
 *
 * This is shown exactly when the hamburger is hidden and hidden when it is
 * shown, so the two are never both offering the same setting, and a phone
 * toolbar gains nothing. `.theme-toggle` already had an active state, a tap
 * highlight and a 44px coarse target in this sheet — the name was reserved for
 * this control and the base rule was never written.
 *
 * It offers the theme you are NOT in, so the icon is the destination. Both
 * icons ship and the theme picks; swapping them in script would leave the
 * wrong one painted for a frame. */
.theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: none; padding: 0;
  background: none; border: 0; border-radius: var(--radius-md); cursor: pointer;
  color: inherit;
}
.theme-toggle:hover { background: var(--tint-hover); }
.theme-toggle__sun { display: none; }
:root[data-theme="dark"] .theme-toggle__sun { display: block; }
:root[data-theme="dark"] .theme-toggle__moon { display: none; }

.nav-toggle {
  /* align-items must be explicit. A <button> computes align-items: flex-start
     from the UA sheet rather than the flex default of stretch, and the three
     bars are empty <span>s — so they collapsed to zero width and the control
     drew nothing at all. It stayed 40x40 and clickable, which is why it was
     never noticed: below 56rem .nav-links is display:none, so an invisible
     hamburger was the only route to six of the navigation links. */
  display: inline-flex; flex-direction: column; justify-content: center;
  align-items: stretch; gap: 4px;
  width: 40px; height: 40px; flex: none; padding: 0 9px;
  background: none; border: 0; border-radius: var(--radius-md); cursor: pointer;
  color: inherit;
}
.nav-toggle span { display: block; height: 2px; background: currentColor; border-radius: 1px; }
.nav-toggle:hover { background: var(--tint-hover); }
/* The account door — Sign in for a new browser, My ECA for a known one. A
   chip, not a full button: 34px, the .chip-main control scale, so it sits
   inside the toolbar instead of towering over it; accent-filled with the
   pressed-chip label colour. The dark theme swaps to the cherry-berry fill
   exactly as .btn-primary does — the dark accent is built for type on the
   ground, not for carrying ink on a fill. */
.nav-auth {
  display: inline-flex; align-items: center; justify-content: center;
  height: 34px; padding: 0 14px; flex: none;
  background: var(--color-accent); color: var(--color-neutral-100);
  font-size: var(--text-md); font-weight: 700; text-decoration: none;
  border-radius: var(--radius-md); white-space: nowrap;
}
/* Resting state re-scoped under .nav: `.nav a { color: inherit }` outranks the
   bare class, which painted the light-theme label header-ink — black type on
   the red fill. Same story the anchor-button guard fixes for .btn-primary. */
.nav .nav-auth { background: var(--color-accent); color: var(--color-neutral-100); }
.nav .nav-auth:hover { background: var(--color-accent-600); color: var(--color-neutral-100); }
:root[data-theme="dark"] .nav-auth { background: var(--color-cherry-berry); color: var(--color-text); }
:root[data-theme="dark"] .nav .nav-auth:hover { background: var(--color-cherry-berry-hover); color: var(--color-text); }
/* The same chip inside the drawer. .nav-panel a styles every link as a list
   row (its own colour, borders, and the dark rail ::before), all at higher
   specificity than a bare class — each is unpicked here or the chip renders
   as a red row with list-ink text, which is exactly the collision this
   control exists to avoid. */
.nav-panel .nav-auth {
  margin-top: var(--space-3); justify-content: center;
  padding: 0 16px; border-bottom: 0;
  font-size: var(--text-base); color: var(--color-neutral-100);
}
.nav-panel .nav-auth:hover { background: var(--color-accent-600); color: var(--color-neutral-100); }
:root[data-theme="dark"] .nav-panel .nav-auth {
  padding-left: 16px; border-bottom: 0; transform: none;
  background: var(--color-cherry-berry); color: var(--color-text);
}
:root[data-theme="dark"] .nav-panel .nav-auth::before { content: none; }
:root[data-theme="dark"] .nav-panel .nav-auth:hover {
  background: var(--color-cherry-berry-hover); color: var(--color-text);
}
/* Theme switch — the menu's footer. A labelled switch instead of a toolbar
   icon: the control says what it does and shows which side it is on, and the
   menu is reachable at every width. It sits after the links and pins to the
   panel's foot when the column is short. */
/* The drawer's foot: everything the toolbar handed over, plus the settings that
   were never on it. ONE thing pins to the bottom — the block, not each row —
   or two rows both claiming `margin-top: auto` push each other apart. */
.nav-foot {
  margin-top: auto;
  display: flex; flex-direction: column; gap: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider);
}
.nav-foot .theme-row { margin-top: 0; padding-top: 0; border-top: 0; }
.theme-row {
  margin-top: auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider);
  font-size: var(--text-base); color: var(--color-text);
}
/* The control is `.seg`, shared with the language switch. A pill toggle lived
   here — a 44x24 track with a sliding knob at `border-radius: 999px` — which
   the brand's Do-list rules out and which nothing ever rendered. A theme choice
   is a radio group; Organic already draws one. */
.theme-seg .seg-opt { gap: 7px; min-height: 34px; padding: 0 12px; font-size: var(--text-sm); font-weight: var(--weight-semibold); }
@media (pointer: coarse) { .theme-seg .seg-opt { min-height: 44px; } }

:root[data-theme="dark"] .theme-row { border-top-color: var(--tint-on-dark-hover); }
.nav-scrim {
  position: fixed; inset: 0; z-index: 40;
  background: var(--scrim-strong);
  backdrop-filter: blur(2px); border: 0;
}
.nav-panel {
  position: fixed; top: 0; left: 0; bottom: 0; z-index: 41;
  width: min(420px, 90vw); overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
  padding: 76px var(--space-4) calc(var(--space-5) + env(safe-area-inset-bottom));
  background: var(--color-surface); border-right: 1px solid var(--color-divider);
  box-shadow: var(--shadow-lg);
}
.nav-panel a {
  padding: 11px 0; font-size: var(--text-xl); color: var(--color-text); text-decoration: none;
  border-bottom: 1px solid var(--color-divider);
}
.nav-panel a:hover { color: var(--color-accent); }
.nav-panel .eyebrow { margin: var(--space-4) 0 2px; }
.nav-panel .eyebrow:first-child { margin-top: 0; }
.nav-close {
  position: absolute; top: 18px; right: 16px;   /* the close stays right, the panel does not */
  width: 40px; height: 40px; font-size: var(--text-4xl); line-height: 1;
  background: none; border: 0; border-radius: var(--radius-md);
  color: var(--color-text); cursor: pointer;
}
.nav-close:hover { background: var(--tint-hover); }

/* ── Navigation, dark ─────────────────────────────────────────────────────────
   The toolbar told you where you could go and never once told you where you
   were. .nav a[aria-current='page'] has been sitting in this sheet the whole
   time with nothing in index.html ever setting the attribute, a rule waiting
   years for a caller. renderVals() already computed the flag for every one of
   the seven views; it just never reached the markup.

   Two states, two colours, and they must not be the same colour: hover is the
   cherry, because hover is a question. Current is the harvest gold, because
   current is a fact. A single accent doing both jobs is how a reader ends up
   thinking the thing under their cursor is the page they are on. */
:root[data-theme="dark"] .nav-links a {
  position: relative;
  padding-block: 6px;
}
:root[data-theme="dark"] .nav-links a::after {
  content: ''; position: absolute; left: 0; right: 100%; bottom: 0; height: 2px;
  background: var(--color-accent); border-radius: 1px;
  transition: right 0.22s cubic-bezier(0.25, 1, 0.5, 1);
}
/* The current page is marked in Heritage Clay, the site's one interactive
   colour. It was Brass, which the brand restricts to premium content and bars
   from standard navigation outright. */
:root[data-theme="dark"] .nav-links a[aria-current='page'],
:root[data-theme="dark"] .nav-panel a[aria-current='page'] {
  color: var(--color-accent);
}
:root[data-theme="dark"] .nav-links a[aria-current='page']::after {
  right: 0; background: var(--color-accent);
}

/* The drawer's own current marker is a rail on the leading edge, not an
   underline, the links are already separated by rules, and a second horizontal
   line at the bottom of one of them reads as a border, not a state. */
/* Both the indent and the rail used to animate padding-left and height, each a
   layout property: the browser has to reflow the row on every tick rather than
   just repaint it. transform costs nothing on the compositor and reads
   identically here, since the indent never has to displace a sibling and the
   rail's own box is already vertically centred, so scaling it from the middle
   looks exactly like growing it from the middle. */
:root[data-theme="dark"] .nav-panel a {
  position: relative; padding-left: 12px; transform: translateX(0);
  transition: transform 0.18s ease, color 0.18s ease;
}
:root[data-theme="dark"] .nav-panel a::before {
  content: ''; position: absolute; left: 0; top: 50%;
  width: 2px; height: 62%; border-radius: 1px;
  background: var(--color-accent);
  transform: translateY(-50%) scaleY(0);
  transform-origin: center;
  transition: transform 0.18s ease;
}
:root[data-theme="dark"] .nav-panel a[aria-current='page']::before {
  transform: translateY(-50%) scaleY(1);
}

/* Two groups, and until now they looked like one list with a word dropped into
   the middle of it. The rule above each label is the second voice, at an alpha
   low enough to divide without ruling. */
:root[data-theme="dark"] .nav-panel .eyebrow {
  padding-top: var(--space-3);
  border-top: 1px solid color-mix(in srgb, var(--color-accent-2) 26%, transparent);
  color: var(--color-accent-2);
}
:root[data-theme="dark"] .nav-panel .eyebrow:first-child { border-top: 0; padding-top: 0; }
:root[data-theme="dark"] .nav-panel a { border-bottom-color: var(--tint-on-dark-hover); }

/* The scrim is a backdrop, not one of the three glass weights, so it moves on
   its own. 2px behind a heavy-blur drawer read as an unblurred page with a
   grey sheet on it. */
:root[data-theme="dark"] .nav-scrim {
  -webkit-backdrop-filter: blur(5px) saturate(1.2);
  backdrop-filter: blur(5px) saturate(1.2);
}

/* The stamp keeps its light disc — that is what holds the black mark legible.
   The gold catch-light ring around it is gone with the rest of the material;
   a Woven Taupe hairline is the structural edge the system uses everywhere. */
:root[data-theme="dark"] .mark-arc {
  box-shadow: inset 0 0 0 1px var(--border-on-dark);
}
/* mobile: the toolbar carries the stamp, the wordmark and the hamburger — the
   inline nav links are already hidden below 48rem, so the width is free */
@media (max-width: 47.99rem) { .mark-name { font-size: var(--text-base); } }

/* grids */
.grid { display: grid; gap: var(--space-3); }
.grid-2 { grid-template-columns: 1fr; }
.grid-3 { grid-template-columns: 1fr; }
@media (min-width: 48rem) {
  /* The rhythm steps up with the layout. Mirrored in
     eca_elementor.SECTION_PAD_Y, which is what the Elementor pages write. */
  :root { --section-pad-y: 88px; }
  .grid-2 { grid-template-columns: 1fr 1fr; gap: var(--space-6); }
  .grid-3 { grid-template-columns: repeat(3, 1fr); }
  .lede { font-size: var(--text-3xl); }
}

/* role picker — snap-scroll on mobile, grid at desktop */
.rail {
  display: grid; grid-auto-flow: column; grid-auto-columns: 80%;
  gap: var(--space-3); overflow-x: auto; scroll-snap-type: x mandatory;
  padding: 4px 20px 14px; margin-inline: -20px;
  scrollbar-width: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; }
@media (min-width: 48rem) {
  .rail {
    grid-auto-flow: row; grid-template-columns: repeat(3, 1fr); grid-auto-columns: auto;
    overflow: visible; padding-inline: 0; margin-inline: 0; gap: var(--space-4);
  }
}

/* A rail that stays a slider at every width.
   `.rail` above is a slider on mobile and a three-column grid at 48rem, which
   is right for three cards and strands the fourth on a row of its own. This
   variant restores the column flow and the snap, sized so the last card peeks —
   a row that reads as scrollable rather than as a grid that miscounted. */
@media (min-width: 48rem) {
  .rail--slider {
    grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: 30%;
    overflow: auto hidden;
  }
}
.role-card {
  text-align: left; width: 100%; cursor: pointer; gap: var(--space-3);
  background: var(--color-surface); border: 1px solid transparent;
  box-shadow: var(--shadow-sm); transition: box-shadow .18s, border-color .18s, transform .18s;
  padding: var(--space-4);
}
.role-card:hover { box-shadow: var(--shadow-md); border-color: var(--color-accent-300); transform: translateY(-2px); }
.role-card:active { box-shadow: var(--shadow-sm); transform: translateY(0); }
.role-card h3 { margin: 0; }
.role-card .card-body { font-size: var(--text-base); opacity: 0.85; }
/* The gallery card's own call to action. Same arrow as a button, drawn by the
   component: it was typed into five labels in the markup. */
.role-card-go {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-body); font-weight: var(--weight-semibold);
  font-size: var(--text-md); color: var(--color-accent); text-decoration: none;
}
.role-card-go::after { content: "→"; transition: transform var(--transition-fast); }
.card-gallery:hover .role-card-go::after { transform: translateX(2px); }

/* photography placeholder — 21:9 by construction, per the art brief */
.photo {
  position: relative; overflow: hidden; border-radius: var(--radius-lg);
  aspect-ratio: 21 / 9; display: grid; place-items: center;
  background: repeating-linear-gradient(115deg, var(--color-neutral-300) 0 8px, var(--color-neutral-200) 8px 16px);
  color: var(--color-neutral-700);
}
.photo-4x3 { aspect-ratio: 4 / 3; }
.photo-1x1 { aspect-ratio: 1 / 1; }
.photo span {
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; font-size: var(--text-xs);
  letter-spacing: 0.04em; text-align: center; padding: 0 16px; max-width: 34ch;
}

/* stats */
.stat-num { font-family: var(--font-heading); font-weight: 600; font-size: var(--text-stat); line-height: 1; display: block; }
.stat-label { font-size: var(--text-md); opacity: 0.75; margin-top: 6px; display: block; }

/* requirements disclosure */
.disclose { border-top: 1px solid var(--color-divider); }
.disclose summary {
  cursor: pointer; list-style: none; padding: var(--space-3) 0;
  font-family: var(--font-body); font-weight: 700; font-size: var(--text-lg);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
}
.disclose summary::-webkit-details-marker { display: none; }
.disclose summary::after { content: '+'; font-size: var(--text-3xl); color: var(--color-accent); line-height: 1; }
.disclose[open] summary::after { content: '–'; }
.disclose summary:hover { color: var(--color-accent-700); }
.req { display: flex; flex-wrap: wrap; gap: var(--space-2); padding-bottom: var(--space-4); margin: 0; list-style: none; }

/* sticky thumb CTA — mobile only */
.thumb-bar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  padding: 10px 20px calc(10px + env(safe-area-inset-bottom));
  background: var(--surface-glass);
  backdrop-filter: blur(8px); border-top: 1px solid var(--color-divider);
}
@media (min-width: 48rem) { .thumb-bar { display: none; } }
@media (max-width: 47.99rem) { body { padding-bottom: 78px; } }

/* footer */
.site-foot { background: var(--footer-bg); color: var(--color-neutral-300); padding-block: 48px 32px; }
.site-foot a { color: var(--color-neutral-200); text-decoration: none; font-size: var(--text-base); }
.site-foot a:hover { color: var(--color-accent-300); }
.site-foot h6 { color: var(--color-neutral-500); margin-bottom: var(--space-3); }
.foot-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); }
@media (min-width: 48rem) { .foot-cols { grid-template-columns: 2fr repeat(3, 1fr); } }
@media (min-width: 64rem) { .foot-cols { grid-template-columns: 1.6fr repeat(4, 1fr); } }
.foot-links { display: flex; flex-direction: column; gap: var(--space-2); }

/* ── Imagery: text sits ON the picture, so a card stays a card ──────────────
   Cards with the image above the copy grow too tall on mobile. These put the
   copy over the photograph with a scrim, and force light type — the scrim is
   what keeps the contrast honest, not the photo.

   THE FALLBACK IS NEUTRAL-900, NOT --color-text. Behind the picture sits a
   flat colour, and the type over it is light in both themes — so the flat
   colour has to be dark in both themes too. It was the ink, which is Charcoal
   on the light theme and Morning Mist on the dark one: the hero's ground
   turned near-white the moment dark was switched on, behind white type. Same
   reasoning as --scrim-ink a few hundred lines up, and the same token family. */
.photo-hero {
  position: relative; isolation: isolate;
  background: var(--color-neutral-900) center / cover no-repeat;
  color: var(--color-neutral-100);
  border-radius: var(--radius-lg); overflow: hidden;
}
.photo-hero::after {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 42%, transparent) 0%,
    color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 72%, transparent) 100%);
}
.photo-hero > * { position: relative; }
.photo-hero h1, .photo-hero h2, .photo-hero h3, .photo-hero p, .photo-hero h6 { color: inherit; }
.photo-hero .lede { color: var(--color-neutral-200); }
/* Over photography the ground is unknowable, so this text is pinned to a light
   value that reads at any exposure — where a mid-tone accent goes muddy on a
   bright frame. It used to be var(--color-bg), which was the same thing while
   the ground was Harvest Parchment. On the dark theme --color-bg is near-black and
   the reasoning inverts: near-black text on a photograph. The photograph is the
   ground here, not the page, so the value must NOT follow the theme. */
.photo-hero .eyebrow, .photo-hero h6, .card-photo .eyebrow { color: var(--color-neutral-100); }
.photo-hero .btn-secondary {
  color: var(--color-neutral-100);
  border-color: var(--border-on-dark-strong);
}
.photo-hero .btn-secondary:hover { background: var(--tint-on-dark-hover); }
.band .btn-secondary, .band-dark .btn-secondary {
  color: var(--color-neutral-100);
  border-color: var(--border-on-dark-strong);
}
.band .btn-secondary:hover, .band-dark .btn-secondary:hover {
  background: var(--tint-on-dark-hover);
}

/* numbered steps — the how-it-works list on a role page */
.steps { counter-reset: step; list-style: none; margin: 24px 0 0; padding: 0;
  display: grid; gap: var(--space-4); }
@media (min-width: 48rem) { .steps { grid-template-columns: repeat(3, 1fr); gap: var(--space-5); } }
.steps li { counter-increment: step; padding-top: 14px; border-top: 2px solid var(--color-accent-2); }
.steps li::before {
  content: counter(step, decimal-leading-zero);
  display: block; margin-bottom: 8px;
  font-family: var(--font-body); font-weight: 600; font-size: var(--text-sm);
  letter-spacing: 0.1em; color: var(--color-accent-2);
}
.steps h3 { margin-bottom: 6px; }
.band-dark .lede { color: var(--color-neutral-200); }

/* full-bleed page opener */
/* ── The opener ───────────────────────────────────────────────────────────
   A full-bleed photograph with the copy over it, running up under the fixed
   header. The base `.photo-hero` already paints the scrim and the light ink,
   so this sets the height, takes the corners off, and nothing else.

   It was a split block for a while — copy on flat ground, the photograph a
   field beside it behind a mask. The association asked for the photograph back
   at full width with no seam, and this is that.

   The scrim is the one thing to watch. Copy over a photograph reads at
   whatever the frame underneath is doing, so a picture swapped in at a much
   brighter exposure needs the gradient checked, not just the crop. That is the
   cost of this arrangement and it was the reason for the split one.

   HEIGHT IS CAPPED, not set in vh alone. At 82vh the opener filled the screen
   on every laptop and pushed the sector figures under the fold — measured at
   870px on a 1440x900 desktop, and 1676px on a tablet, where it stacked. The
   clamp holds it to a band that leaves the figures visible. */
.photo-hero.opener {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: clamp(430px, 60vh, 560px);
  display: flex; align-items: center;
  border-radius: 0;
  /* A fallback keeps the declaration valid where nothing sets the variable —
     the Elementor build, which carries the image as its own setting and
     outranks this rule anyway. Without it the declaration is invalid and
     the browser drops it, which is the same result by luck rather than by
     statement. */
  /* MIRRORED, and by flipping the SECTION rather than a layer inside it.
     A background-image cannot be flipped on its own, and the static build
     carries the picture as this element's background while the Elementor build
     carries it as the container's — one mechanism has to serve both, so the
     element turns and everything that must stay readable turns back.

     The scrim counter-flips too, so its gradient is authored in screen space:
     without that, "dark on the left" would be written and dark on the right
     would be drawn. */
  background-image: var(--hero-image, none);
  background-size: cover; background-position: center;
  background-color: var(--color-neutral-900);
  transform: scaleX(-1);
  margin-top: calc(var(--head-h) * -1);      /* back under the fixed header */
  padding-top: var(--head-h);
}
.photo-hero.opener > .wrap,
.photo-hero.opener > .e-con-inner,
.photo-hero.opener::after { transform: scaleX(-1); }

/* Dark takes the picture down another 30%. The scrim below is tuned for a page
   whose ground is Morning Mist; on a #121212 page the same photograph is the
   brightest thing on the screen by a wide margin and reads as a hole punched
   in the page. A flat wash rather than another gradient — the shaping is
   already done, this only lowers the exposure. */
:root[data-theme="dark"] .photo-hero.opener::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 30%, transparent);
}

/* The scrim reads ACROSS as well as down. The copy sits on the left, so that is
   where the weight goes: a near-solid edge that gives up by the middle and
   leaves the right of the frame almost clear, plus a gentler wash top to bottom
   so the headline has a dark band to sit in and the foot of the picture does
   not glare into the section below.

   Two gradients rather than one flat scrim at this strength: a flat one would
   grey the photograph out everywhere. The picture keeps its colour where
   nothing is written over it. */
.photo-hero.opener::after {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(90deg,
      color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 88%, transparent) 0%,
      color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 66%, transparent) 40%,
      color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 26%, transparent) 76%,
      color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 8%, transparent) 100%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 32%, transparent) 0%,
      transparent 40%,
      color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 50%, transparent) 100%);
}

.photo-hero.opener > .wrap {
  width: 100%;
  padding-block: var(--space-8);
}
.photo-hero.opener .wrap > * { max-width: 54ch; }
/* The static build's column, mirrored for the same reason as the Elementor
   one above: the content sits at the logical end so that it draws at the
   visual start. */
.photo-hero.opener > .wrap { margin-inline: auto 0; }

/* The same measure for the Elementor build, where the copy is widgets in a
   boxed container rather than children of a `.wrap`. Without it the headline
   and the lede both ran the full 1200 — the headline crossed the whole
   photograph and the lede ran past the width the type ladder is drawn for.
   The page data cannot state this: its own `max_ch` lands on the widget
   wrapper, where `ch` resolves against 16px instead of the heading's size.
   Narrowed on the COLUMN, not on each widget. Elementor writes
   `.e-con.e-con > .e-con-inner > .elementor-widget { max-width: 100% }` — it
   doubles its own class to outrank exactly this kind of override, and it wins
   against a per-widget rule. Its hold on `.e-con-inner` is only two classes,
   so this reaches it there and the widgets inside simply fill what is left. */
@media (min-width: 48rem) {
  .photo-hero.opener > .e-con-inner {
    max-width: 44rem;
    /* `auto 0`, not `0 auto`. The section is mirrored, so its inline axis is
       reversed: the column that is logically flush-right renders flush-LEFT,
       which is where the copy belongs and where the scrim puts its weight.
       Counter-flipping the block un-mirrors its text, not its position. */
    margin-inline: auto 0;
  }
}

/* Heritage Clay on a photograph is the one place the accent cannot go: it is a
   mid-tone on an unknowable ground and it goes muddy on a bright frame. The
   headline takes one colour over a picture — the same reasoning that pins the
   eyebrow to a light value a few rules up. */
.photo-hero .hl-accent { color: inherit; }

/* ── The sector figures ───────────────────────────────────────────────────
   Five cards that slide: five in view on desktop, three between, two on a
   phone. The association asked for those counts.

   It is a slider at every width rather than a grid that collapses, because
   five does not divide into a tablet row without leaving a ragged 3 + 2. The
   mechanic is `.rail`'s — snap scrolling, hidden scrollbar — with its own
   column count, which is why it is a component here and not a `.rail`
   modifier: `.rail` is three at desktop and this is five. */
.stat-rail {
  display: grid; grid-auto-flow: column;
  grid-auto-columns: calc((100% - var(--space-5)) / 2);
  gap: var(--space-5);
  overflow-x: auto; scroll-snap-type: x mandatory;
  padding: 4px 0 14px;
  scrollbar-width: none;
}
.stat-rail::-webkit-scrollbar { display: none; }
@media (min-width: 48rem) {
  .stat-rail { grid-auto-columns: calc((100% - var(--space-5) * 2) / 3); }
}
@media (min-width: 64rem) {
  .stat-rail { grid-auto-columns: calc((100% - var(--space-5) * 4) / 5); }
}

.stat-card {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-3); text-align: center;
  scroll-snap-align: start;
}
/* The divider sits in the middle of the gap as a pseudo-element, so it costs
   the card no width. A border would, and five cards whose content boxes differ
   by a pixel read as five different cards. */
.stat-card + .stat-card::before {
  content: ''; position: absolute; inset-block: 0;
  inset-inline-start: calc(var(--space-5) / -2 - 0.5px);
  width: 1px; background: var(--color-divider);
}
/* A circle, not an ellipse. `border-radius: 50%` on a 152x118 box is an oval
   — it read as a squashed circle rather than a deliberate shape. A square box
   makes the same radius a circle, and `object-fit: cover` centre-crops whatever
   the photograph's own aspect is. */
.stat-card__art {
  width: min(100%, 148px); aspect-ratio: 1;
  border-radius: 50%; object-fit: cover; object-position: center;
  background: var(--color-surface);
}
.stat-card__label {
  margin: 0; max-width: 22ch;
  font-family: var(--font-heading); font-size: var(--text-2xl);
  font-weight: var(--weight-semibold); line-height: 1.3;
}
/* Heritage Clay, filled, at the system's own radius. NOT a pill: the brand's
   Do list rules them out — the same note sits on the role tags in the engine
   plugin's stylesheet. */
.stat-card__figure {
  font-family: var(--font-heading); font-size: var(--text-3xl);
  font-weight: var(--weight-semibold); line-height: 1.2;
  color: var(--color-neutral-100); background: var(--color-accent);
  padding: 7px var(--space-4); border-radius: var(--radius-md);
}
.stat-card__unit {
  font-size: var(--text-sm); line-height: 1.4;
  color: var(--color-text-secondary);
}
.stat-source {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  gap: var(--space-3); margin-top: var(--space-5);
  font-size: var(--text-sm); color: var(--color-text-secondary);
}

/* compact card whose photo IS the background */
.card-photo {
  position: relative; isolation: isolate; display: flex; flex-direction: column;
  justify-content: flex-end; min-height: 208px; padding: var(--space-4);
  background: var(--color-neutral-900) center / cover no-repeat;
  color: var(--color-neutral-100); border-radius: var(--radius-lg); overflow: hidden;
  text-align: left;
}
/* .card gives .card-body flex:1, which spreads the copy over the whole card and
   strands the kicker in the thin part of the scrim. On a photograph the copy has
   to be one compact block sitting on the plate. */
.card-photo .card-body { flex: 0 0 auto; }
.card-photo::after {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg,
    transparent 0%,
    color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 3%, transparent) 26%,
    color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 84%, transparent) 42%,
    color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 94%, transparent) 68%,
    color-mix(in srgb, var(--scrim-ink, var(--color-neutral-900)) 97%, transparent) 100%);
}
/* Coffee Cherry is a light-ground colour; on a scrim it reads at 1.87:1. The
   light step of its own ramp is the mirror rule. */
.card-photo .role-card-go { color: var(--color-accent-300); }
.card-photo .card-kicker { color: var(--color-neutral-100); }   /* on the photograph — see .photo-hero above */
.card-photo .card-title, .card-photo h3 { color: var(--color-neutral-100); margin: 4px 0 6px; }
.card-photo .card-body, .card-photo p { color: var(--color-neutral-200); }

/* ── The gallery card — photograph whole, copy underneath ─────────────────
   Text over a photograph could not be made to hold. Measured against a bright
   frame the role name came in at 2.49:1 and the CTA at 1.87:1, and a scrim
   heavy enough to fix them erased the picture — only 8% of the card was still
   legible photograph. Here nothing sits on the image except the role name, in
   a scrimmed chip in the one corner we control, and the copy gets the full
   12.7:1 of Roasted Earth on Morning Mist. */
.card-gallery {
  display: flex; flex-direction: column; text-align: left; width: 100%;
  /* `text-decoration: none` because this component is sometimes an ANCHOR. It
     was a <button> everywhere on the static site, so nothing here ever needed
     saying; the Coffee Pulse card is the first one that links, and the whole
     card came out underlined — title, excerpt and date together, because
     text-decoration propagates to descendants and a child cannot refuse it. */
  padding: 0; font: inherit; color: inherit; cursor: pointer; text-decoration: none;
  background: var(--color-surface);
  border: 1px solid transparent; border-radius: var(--radius-lg);
  overflow: hidden; box-shadow: var(--shadow-sm);
  transition: box-shadow .18s, border-color .18s, transform .18s;
}
.card-gallery:hover {
  box-shadow: var(--shadow-md); border-color: var(--color-accent-300);
  transform: translateY(-2px);
}
.card-gallery:active { box-shadow: var(--shadow-sm); transform: translateY(0); }
.card-gallery .media {
  display: block; position: relative; aspect-ratio: 16 / 10;
  background: var(--color-neutral-800) center / cover no-repeat;
}
/* The chip on the picture is the same plate as .card-badge — a light block with
   dark type, which is how the brand marks something on a photograph. It was a
   dark scrim chip with light type and a blur behind it: legible, but a
   different device from the one the brand draws, and the only blurred surface
   left on the site after the material came off.

   A light plate also stops depending on how dark the photograph happens to be,
   so the shade that used to be painted under it is gone with it. */
.card-gallery .media .card-kicker {
  display: inline-flex; align-items: center; position: absolute;
  left: 12px; top: 12px; z-index: 1;
  padding: 5px var(--space-2); border-radius: var(--radius-sm);
  background: var(--eca-morning-mist); color: var(--eca-charcoal);
  font-weight: var(--weight-semibold); letter-spacing: 0.06em; line-height: 1;
  box-shadow: var(--shadow-sm);
}
.card-gallery .body {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4); flex: 1; width: 100%; box-sizing: border-box;
}
.card-gallery h3 { margin: 0; }
.card-gallery .card-body { font-size: var(--text-base); opacity: 0.85; flex: 1; }
.card-gallery .role-card-go { color: var(--color-accent); }

/* ATLAS-CSS:START */
.atlas-wrap[data-area="A1.1"] .atlas-area[data-kew="A1.1"]{opacity:1!important}
.atlas-wrap[data-area="A1.2"] .atlas-area[data-kew="A1.2"]{opacity:1!important}
.atlas-wrap[data-area="A2.1"] .atlas-area[data-kew="A2.1"]{opacity:1!important}
.atlas-wrap[data-area="A2.2"] .atlas-area[data-kew="A2.2"]{opacity:1!important}
.atlas-wrap[data-area="A2.3"] .atlas-area[data-kew="A2.3"]{opacity:1!important}
.atlas-wrap[data-area="A2.4"] .atlas-area[data-kew="A2.4"]{opacity:1!important}
.atlas-wrap[data-area="A2.5"] .atlas-area[data-kew="A2.5"]{opacity:1!important}
.atlas-wrap[data-area="A2.6"] .atlas-area[data-kew="A2.6"]{opacity:1!important}
.atlas-wrap[data-area="B1.1"] .atlas-area[data-kew="B1.1"]{opacity:1!important}
.atlas-wrap[data-area="B1.2"] .atlas-area[data-kew="B1.2"]{opacity:1!important}
.atlas-wrap[data-area="C1.1"] .atlas-area[data-kew="C1.1"]{opacity:1!important}
.atlas-wrap[data-area="C1.2"] .atlas-area[data-kew="C1.2"]{opacity:1!important}
.atlas-wrap[data-area="C2.1"] .atlas-area[data-kew="C2.1"]{opacity:1!important}
.atlas-wrap[data-area="C2.2"] .atlas-area[data-kew="C2.2"]{opacity:1!important}
.atlas-wrap[data-area="C2.3"] .atlas-area[data-kew="C2.3"]{opacity:1!important}
.atlas-wrap[data-tag="citrus"] .atlas-area[data-kew="A1.1"],.atlas-wrap[data-tag="citrus"] .atlas-area[data-kew="A1.2"],.atlas-wrap[data-tag="citrus"] .atlas-area[data-kew="A2.1"],.atlas-wrap[data-tag="citrus"] .atlas-area[data-kew="A2.3"],.atlas-wrap[data-tag="citrus"] .atlas-area[data-kew="C1.1"]{opacity:1!important}
.atlas-wrap[data-tag="blueberry"] .atlas-area[data-kew="C1.1"],.atlas-wrap[data-tag="blueberry"] .atlas-area[data-kew="C2.1"],.atlas-wrap[data-tag="blueberry"] .atlas-area[data-kew="C2.2"],.atlas-wrap[data-tag="blueberry"] .atlas-area[data-kew="C2.3"]{opacity:1!important}
.atlas-wrap[data-tag="stone-fruit"] .atlas-area[data-kew="A2.2"],.atlas-wrap[data-tag="stone-fruit"] .atlas-area[data-kew="A2.3"],.atlas-wrap[data-tag="stone-fruit"] .atlas-area[data-kew="C2.1"],.atlas-wrap[data-tag="stone-fruit"] .atlas-area[data-kew="C2.3"]{opacity:1!important}
.atlas-wrap[data-tag="tropical"] .atlas-area[data-kew="A1.2"],.atlas-wrap[data-tag="tropical"] .atlas-area[data-kew="A2.2"],.atlas-wrap[data-tag="tropical"] .atlas-area[data-kew="A2.6"]{opacity:1!important}
.atlas-wrap[data-tag="berry"] .atlas-area[data-kew="A1.1"],.atlas-wrap[data-tag="berry"] .atlas-area[data-kew="C1.1"]{opacity:1!important}
.atlas-wrap[data-tag="fruit"] .atlas-area[data-kew="C1.2"]{opacity:1!important}
.atlas-wrap[data-tag="jam"] .atlas-area[data-kew="C2.3"]{opacity:1!important}
.atlas-wrap[data-tag="floral"] .atlas-area[data-kew="A2.1"],.atlas-wrap[data-tag="floral"] .atlas-area[data-kew="A2.3"],.atlas-wrap[data-tag="floral"] .atlas-area[data-kew="C1.1"],.atlas-wrap[data-tag="floral"] .atlas-area[data-kew="C1.2"]{opacity:1!important}
.atlas-wrap[data-tag="jasmine"] .atlas-area[data-kew="A2.6"],.atlas-wrap[data-tag="jasmine"] .atlas-area[data-kew="C1.1"]{opacity:1!important}
.atlas-wrap[data-tag="bergamot"] .atlas-area[data-kew="C1.1"]{opacity:1!important}
.atlas-wrap[data-tag="black-tea"] .atlas-area[data-kew="A2.6"]{opacity:1!important}
.atlas-wrap[data-tag="sweet"] .atlas-area[data-kew="A1.2"],.atlas-wrap[data-tag="sweet"] .atlas-area[data-kew="A2.6"]{opacity:1!important}
.atlas-wrap[data-tag="sweet-nut"] .atlas-area[data-kew="C1.2"]{opacity:1!important}
.atlas-wrap[data-tag="vanilla"] .atlas-area[data-kew="C1.2"]{opacity:1!important}
.atlas-wrap[data-tag="chocolate"] .atlas-area[data-kew="A2.2"]{opacity:1!important}
.atlas-wrap[data-tag="mocha"] .atlas-area[data-kew="C2.2"]{opacity:1!important}
.atlas-wrap[data-tag="mocha-like"] .atlas-area[data-kew="B1.2"]{opacity:1!important}
.atlas-wrap[data-tag="spice"] .atlas-area[data-kew="C2.1"],.atlas-wrap[data-tag="spice"] .atlas-area[data-kew="C2.2"],.atlas-wrap[data-tag="spice"] .atlas-area[data-kew="C2.3"]{opacity:1!important}
.atlas-wrap[data-tag="wine-like"] .atlas-area[data-kew="A2.3"],.atlas-wrap[data-tag="wine-like"] .atlas-area[data-kew="C1.1"]{opacity:1!important}
.atlas-wrap[data-tag="forest-wild"] .atlas-area[data-kew="A2.5"]{opacity:1!important}
.atlas-wrap[data-tag="herbal"] .atlas-area[data-kew="A2.5"]{opacity:1!important}
.atlas-wrap[data-tag="balanced"] .atlas-area[data-kew="A2.4"],.atlas-wrap[data-tag="balanced"] .atlas-area[data-kew="B1.1"],.atlas-wrap[data-tag="balanced"] .atlas-area[data-kew="C1.3"]{opacity:1!important}
.atlas-wrap[data-tag="soft"] .atlas-area[data-kew="A2.4"]{opacity:1!important}
/* ATLAS-CSS:END */

/* the application pack — a numbered document list, not a checklist of ticks */
.pack { counter-reset: pack; list-style: none; margin: 18px 0 0; padding: 0; }
.pack-item {
  counter-increment: pack; position: relative;
  padding: 14px 0 14px 38px; border-top: 1px solid var(--color-divider);
}
.pack-item:last-child { border-bottom: 1px solid var(--color-divider); }
.pack-item::before {
  content: counter(pack, decimal-leading-zero);
  position: absolute; left: 0; top: 16px;
  font-family: var(--font-body); font-weight: 600; font-size: var(--text-sm);
  letter-spacing: 0.08em; color: var(--color-accent-2);
}
.pack-item h4 { margin: 0 0 3px; }
.pack-item p { margin: 0; opacity: 0.85; }

/* Scroll anchoring keeps the reader's place when content loads above them. On a
   view change there is no place to keep — it just drags the new page back down
   as the photographs arrive. */
main { overflow-anchor: none; }

/* ── Atlas drill-down: the bottom sheet ──────────────────────────────────────
   Detail used to render as an inline band under the map. From the region chips
   that band always mounted below the fold and reveal() only scrolled when it was
   ABOVE the viewport — so the ⓘ silently changed something off-screen. A sheet
   fixed to the viewport cannot be off-screen, which removes that failure mode
   rather than patching the scroll condition.
   Stacking: thumb-bar is 30 and the nav scrim/panel are 40/41 — the sheet sits
   between, so the menu still overlays it. */
.atlas-sheet h2, .atlas-sheet h3 { color: var(--color-neutral-100); }
.atlas-sheet .eyebrow { color: var(--color-taupe); }
.atlas-sheet .lede { color: var(--color-neutral-200); }
.atlas-sheet .chip-plain { background: var(--tint-on-dark-hover); color: var(--color-neutral-100); border-color: transparent; }

/* breadcrumb — the only way back up is explicit */
.atlas-crumb { display: flex; align-items: center; gap: var(--space-3); margin-bottom: 14px; }
.atlas-crumb-now {
  font-family: var(--font-body); font-weight: 600; font-size: var(--text-xs);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-2);
}

/* ═══ CHIPS — one component, three variants. THE spec: 34px min-height, 8px 12px
   padding on the action segment, 14px body type, radius-md, surface fill,
   divider border. Every chip on the site uses these numbers; do not fork them.
     .chip                the frame (span): border, radius, clips its segments
     .chip-main           action segment (button) — the name; filters/selects
     .chip-i              info segment (button) — opens the drilldown sheet
     .chip-n              count badge inside .chip-main
     .chip-plain          single-segment chip; a <span> is static, a <button> acts
     .chip-zone           adds the Kew-zone swatch dot to .chip-main
   ═══ */
.chip {
  display: inline-flex; align-items: stretch; overflow: hidden;
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.chip-main, .chip-i, .chip-plain {
  display: inline-flex; align-items: center; gap: 7px;
  background: none; border: 0; color: var(--color-text);
  font-family: var(--font-body); font-size: var(--text-base); line-height: 1;
  transition: background .14s ease;
}
.chip-main { padding: 8px 12px; min-height: 34px; cursor: pointer; }
.chip-main:hover { background: var(--color-accent-100); }
.chip-main[aria-pressed="true"] {
  background: var(--color-accent); color: var(--color-neutral-100);
}
.chip-main[aria-pressed="true"] .chip-n {
  background: var(--tint-on-dark-active);
}
.chip-i {
  padding: 0 9px; border-left: 1px solid var(--color-divider);
  color: var(--color-accent-2); cursor: pointer;
}
.chip-i:hover { background: var(--color-accent-100); color: var(--color-accent); }
.chip-i svg { width: 15px; height: 15px; }
.chip-n {
  font-size: var(--text-xs); font-weight: 600; padding: 2px 5px; border-radius: var(--radius-sm);
  background: var(--tint-hover);
}
.chip-plain { padding: 8px 12px; min-height: 34px; background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: var(--radius-md); }
button.chip-plain { cursor: pointer; }
button.chip-plain:hover { background: var(--color-accent-100); }
.chip-zone .chip-main::before {
  content: ''; width: 9px; height: 9px; flex: none; border-radius: 50%;
  background: var(--swatch, var(--color-taupe));
}
/* The five Kew zones take the atlas palette (see :root) instead of the old
   role/brand tokens (--color-accent, --color-ember, --color-taupe,
   --color-stone) — one qualitative colour per zone, the same in both themes,
   matching the map fills below rather than each theme's own accent. */
.chip-zone[data-zone="South East"] { --swatch: var(--atlas-forest-leaf); }
.chip-zone[data-zone="South West"] { --swatch: var(--atlas-cherry-berry); }
.chip-zone[data-zone="Harar"]      { --swatch: var(--atlas-moss-green); }
.chip-zone[data-zone="Rift"]       { --swatch: var(--atlas-harvest-gold); }
.chip-zone[data-zone="North"]      { --swatch: var(--atlas-charcoal); }
.chip-zone[data-map="no"] { border-style: dashed; }  /* Kew lists it; no boundary digitised */
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
/* The seven membership-type chips on the home page. Same component as the atlas
   filter, one size up: the atlas chip is a secondary control in a dense row of
   sixteen, and these are the first thing on the page anyone is asked to touch.
   34px is the documented chip height and it was written for a desktop design, so
   this takes the 44px tap target instead. The type size stays --text-base,
   because that is the interface size and a chip is interface. */
.role-chips { gap: var(--space-2); }
.role-chips .chip-main { min-height: 44px; padding: 11px 16px; font-weight: 600; }
.chip-group { margin-top: var(--space-3); }
.chip-group .eyebrow { margin-bottom: 8px; }
.band-dark .chip { background: transparent;
  border-color: var(--border-on-dark); }
.band-dark .chip-main, .band-dark .chip-plain { color: var(--color-neutral-200); background: transparent; }
.band-dark .chip-main:hover, .band-dark button.chip-plain:hover { background: var(--tint-on-dark-hover); }

/* ═══ COFFEE ATLAS — the interaction contract lives as a comment on the isAtlas
   block in index.html. What matters to the CSS: selection must never reflow the
   page. Highlighting is data-attribute driven (data-area / data-tag on
   .atlas-wrap, generated per-value rules from build-atlas-data.py), and the
   drilldown is an overlay sheet. The Region|Flavour switch is the ONLY thing
   that changes layout. ═══ */
.atlas-page { padding-block: 20px 28px; }
.atlas-head { text-align: center; margin-bottom: 14px; }
.atlas-head .eyebrow { margin-bottom: 6px; }
/* the atlas rides the Highland Green band, like the Ethiopian Coffee section on
   home — the switch keeps its own light surface so it must not inherit the
   band's light text */
.atlas-page.band .atlas-hint { color: var(--color-neutral-200); }
.atlas-page.band .atlas-outline { stroke: var(--border-on-dark); }
.atlas-switch { display: flex; width: max-content; margin: 0 auto 16px;
  background: var(--color-surface); }
.atlas-switch .seg-opt { min-width: 108px; justify-content: center; cursor: pointer;
  color: var(--color-text); }
.atlas-switch .seg-opt[aria-pressed="true"] { background: var(--color-accent); color: var(--color-bg); }
.atlas-switch .seg-opt:not([aria-pressed="true"]):hover { background: var(--tint-hover); }
.atlas-hint { opacity: 0.7; margin-bottom: 12px; }

.atlas-wrap { position: relative; max-width: 640px; margin-inline: auto; }
.atlas-map { display: block; max-height: 56vh; width: auto; margin-inline: auto; }
.atlas-outline { fill: var(--color-surface); stroke: var(--color-divider); stroke-width: 1.2; }
.atlas-area { fill: var(--fill); stroke: var(--color-bg); stroke-width: 1.4; cursor: pointer;
  transition: opacity .16s ease; }
/* a selection dims the rest to 45% — enough to point, never enough to grey the country */
.atlas-wrap[data-area]:not([data-area=""]) .atlas-area,
.atlas-wrap[data-tag]:not([data-tag=""]) .atlas-area { opacity: 0.45; }
@media (hover: hover) { .atlas-area:hover { opacity: 1 !important; } }
/* the small map that rides the home page — same areas, no selection state */
.atlas-preview { max-width: 100%; }
.atlas-preview .atlas-area { cursor: pointer; }
.atlas-preview .atlas-area:hover { opacity: 1; }
.atlas-labels { pointer-events: none; }
.atlas-label { font: 600 14.5px var(--font-body); text-anchor: middle;
  fill: var(--color-neutral-100);
  paint-order: stroke; stroke: var(--scrim-strong);
  stroke-width: 3px; stroke-linejoin: round; }

/* one column on mobile; two on desktop. The switch swaps which column leads. */
.atlas-flow { display: flex; flex-direction: column; gap: var(--space-4); }
.atlas-flow[data-driver="region"] .atlas-sec-taste { display: none; }
.atlas-flow[data-driver="taste"] .atlas-sec-regions { display: none; }
.atlas-flow[data-driver="taste"] .atlas-sec-map { order: 2; }
@media (min-width: 64rem) {
  .atlas-flow { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: 0 var(--space-6); align-items: start; }
  .atlas-map { max-height: 62vh; }
  /* the map pins to the top of the row while the chip column scrolls */
  .atlas-sec-map { position: sticky; top: 84px; }
  .atlas-flow[data-driver="taste"] .atlas-sec-map { grid-column: 2; }
  /* in taste mode the flavour column is the one that pins */
  .atlas-flow[data-driver="taste"] .atlas-sec-taste {
    grid-column: 1; position: sticky; top: 84px; grid-row: span 2;
  }
  .atlas-flow[data-driver="region"] .atlas-sec-map { grid-row: span 2; }
}

/* the drilldown sheet — an overlay, so opening it moves nothing behind it */
.atlas-scrim { position: fixed; inset: 0; z-index: 60;
  background: var(--scrim); }
.atlas-scrim.is-open { animation: atlas-fade-in 200ms ease both; }
.atlas-scrim.is-closing { animation: atlas-fade-out 220ms ease both; }
.atlas-sheet { position: fixed; left: 0; right: 0; bottom: 0; z-index: 61;
  max-height: 82vh; display: flex; flex-direction: column;
  background: var(--color-text); color: var(--color-neutral-200);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: var(--shadow-lg); }
.atlas-sheet h2 { color: var(--color-neutral-100); }
.atlas-sheet.is-open { animation: atlas-sheet-up 240ms cubic-bezier(0, 0, 0.2, 1) both; }
.atlas-sheet.is-closing { animation: atlas-sheet-down 220ms cubic-bezier(0.4, 0, 1, 1) both; }
@media (min-width: 48rem) {
  .atlas-sheet { left: 50%; right: auto; transform: translateX(-50%);
    width: min(720px, 92vw); }
}
.atlas-sheet-head { position: relative; flex: none; padding: 10px 0 2px; }
.atlas-sheet-grip { display: block; width: 44px; height: 4px; margin: 0 auto;
  border-radius: 2px; background: var(--border-on-dark); }
.atlas-sheet-x { position: absolute; top: 10px; right: 12px; width: 36px; height: 36px;
  display: grid; place-items: center; background: none; border: 0;
  color: var(--color-neutral-200); cursor: pointer; border-radius: var(--radius-md); }
.atlas-sheet-x:hover { background: var(--tint-on-dark-active); }
.atlas-sheet-x svg { width: 16px; height: 16px; }
.atlas-sheet-body { overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 8px var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom)); }
.atlas-facts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px 16px; margin-top: 16px; }
.atlas-facts p { margin: 2px 0 0; font-size: var(--text-base); }
@media (max-width: 47.99rem) { .atlas-facts { grid-template-columns: repeat(2, 1fr); } }
@keyframes atlas-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes atlas-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes atlas-sheet-up { from { transform: translate(var(--sheet-x, 0), 100%); } to { transform: translate(var(--sheet-x, 0), 0); } }
@keyframes atlas-sheet-down { from { transform: translate(var(--sheet-x, 0), 0); } to { transform: translate(var(--sheet-x, 0), 100%); } }
@media (min-width: 48rem) { .atlas-sheet { --sheet-x: -50%; } }

/* ══ GLASS ═══════════════════════════════════════════════════════════════════
   The material, applied. Three rules govern where it goes:

   1. Weight follows job. Chrome that must not compete gets --glass-blur-thin;
      content cards get the regular weight; anything that overlays the page and
      has to win gets heavy. One blur everywhere is a filter, not a material.
   2. It never carries a record. Compliance text, requirement lists and the
      auth hand-off opt out through .record-surface — see below.
   3. It degrades. prefers-reduced-transparency and a no-backdrop-filter
      fallback both land on an opaque surface, because a translucent panel that
      does not blur is unreadable rather than merely plainer.
   ══════════════════════════════════════════════════════════════════════════ */
.glass, .card, .dialog, .nav-panel, .atlas-sheet, .site-head {
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
}
/* Both shadows, not one. box-shadow is a single property, so naming only the
   inset edge-light here left every .card sitting perfectly flat against the
   ground with nothing under it. --shadow-sm is deliberately the smallest step
   on the ramp: enough to lift the pane off the grain, not enough to read as a
   raised button. */
.card {
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-edge), var(--shadow-sm);
}
/* Chrome: thin, so what scrolls under it stays readable as motion. */
.site-head {
  background: var(--glass-bg-thin);
  -webkit-backdrop-filter: blur(var(--glass-blur-thin)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur-thin)) saturate(var(--glass-saturate));
}
/* Overlays: heavy, because they have to beat a whole page. */
.dialog, .nav-panel, .atlas-sheet {
  -webkit-backdrop-filter: blur(var(--glass-blur-heavy)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur-heavy)) saturate(var(--glass-saturate));
}
.dialog, .nav-panel {
  background: var(--glass-bg-heavy);
  box-shadow: var(--shadow-lg), var(--glass-edge);
}
.nav-panel { border-right: 1px solid var(--glass-border); }
.atlas-sheet {
  background: color-mix(in srgb, var(--color-text) 88%, transparent);
  box-shadow: var(--shadow-lg), var(--glass-edge);
}
.chip { background: var(--glass-bg); border-color: var(--glass-border); }

/* ── The record surfaces ──────────────────────────────────────────────────────
   Compliance text, the requirements list and the auth hand-off are the places a
   European buyer, an auditor or a member has to trust rather than enjoy. Glass
   reads as evasive exactly there, and a legal disclosure rendered translucent
   over a moving backdrop is a liability the first time somebody screenshots it
   for a filing. These go flat, opaque and higher-contrast: a paper treatment,
   deliberately a different material from the rest of the page. */
.record-surface {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  border-left: 3px solid var(--color-accent-2);
  border-radius: var(--radius-md);
  box-shadow: none;
}
.record-surface .card,
.record-surface .disclose,
.record-surface .req li {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background: transparent;
  box-shadow: none;
}
.record-surface .card-body, .record-surface p { opacity: 1; }

/* ── Degrading the material ───────────────────────────────────────────────────
   A reader who asks for less transparency, or a browser without backdrop-filter,
   gets an opaque surface rather than a translucent one that never blurs. */
@media (prefers-reduced-transparency: reduce) {
  .glass, .card, .card-gallery, .dialog, .nav-panel, .atlas-sheet, .site-head, .chip {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .card, .chip, .card-gallery { background: var(--color-surface); }
  .site-head { background: var(--color-bg); }
  .dialog, .nav-panel { background: var(--color-surface); }
  .atlas-sheet { background: var(--color-text); }
  /* The grain is a background-image, so killing backdrop-filter does not touch
     it. Left in, it would be the one decorative layer surviving on a surface
     that was just flattened for the reader who asked for less of exactly this. */
  :root[data-theme="dark"] .glass,
  :root[data-theme="dark"] .card,
  :root[data-theme="dark"] .dialog,
  :root[data-theme="dark"] .nav-panel,
  :root[data-theme="dark"] .atlas-sheet,
  :root[data-theme="dark"] .site-head,
  :root[data-theme="dark"] .chip { background-image: none; }
  /* The grain now lives directly on these four grounds' own background
     shorthand, not on a separate ::after; stripping it here means the same
     background-image: none, on the elements themselves. The blend mode is
     reset alongside it: with no image layer left there is nothing to blend,
     but leaving a stray soft-light on the rule is the kind of thing that
     quietly changes behaviour the next time anyone adds a second background
     layer to one of these grounds. */
  :root[data-theme="dark"] body,
  :root[data-theme="dark"] .section-tint,
  :root[data-theme="dark"] .band,
  :root[data-theme="dark"] .band-dark {
    background-image: none;
    background-blend-mode: normal;
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .card, .chip, .card-gallery { background: var(--color-surface); }
  .site-head { background: var(--color-bg); }
  .dialog, .nav-panel { background: var(--color-surface); }
  .atlas-sheet { background: var(--color-text); }
  :root[data-theme="dark"] .card,
  :root[data-theme="dark"] .chip,
  :root[data-theme="dark"] .site-head,
  :root[data-theme="dark"] .dialog,
  :root[data-theme="dark"] .nav-panel,
  :root[data-theme="dark"] .atlas-sheet { background-image: none; }
}

/* ══ TOUCH ═══════════════════════════════════════════════════════════════════
   A touch reader never produces :hover. Every affordance therefore declares a
   pressed state first, and anything decorative that only reads on a pointer is
   gated behind (hover: hover) so a tap does not leave a control stuck in a
   hover style it can never clear. */
* { -webkit-tap-highlight-color: transparent; }

.btn:active,
.chip-main:active, button.chip-plain:active, .chip-i:active,
.nav-toggle:active, .theme-toggle:active, .nav-close:active,
.card-gallery:active, .seg-opt:active, .atlas-sheet-x:active {
  transform: translateY(1px) scale(0.985);
}
.btn, .chip-main, .chip-plain, .chip-i, .nav-toggle, .theme-toggle,
.nav-close, .card-gallery, .seg-opt, .atlas-sheet-x {
  transition: transform 120ms cubic-bezier(0.2, 0, 0, 1),
              background-color 160ms ease, border-color 160ms ease, color 160ms ease;
  touch-action: manipulation;
}
.btn-primary:active { background: var(--color-accent-600); }
.chip-main:active, button.chip-plain:active { background: var(--tint-accent-active); }

/* Decorative lift belongs to a pointer that can hover and leave again. */
@media (hover: hover) and (pointer: fine) {
  /* The wipe only exists for a cursor. A tap would leave it stuck open on the
     last link touched, which is the pressed-state bug the Touch block below
     exists to prevent. */
  :root[data-theme="dark"] .nav-links a:hover::after { right: 0; }
  :root[data-theme="dark"] .nav-panel a:hover { transform: translateX(4px); }
  :root[data-theme="dark"] .nav-panel a:not([aria-current='page']):hover::before {
    transform: translateY(-50%) scaleY(0.645); background: var(--color-accent);
  }

  .card-gallery:hover { border-color: var(--color-accent); }
  .card:hover { border-color: color-mix(in srgb, var(--color-accent) 28%, transparent); }
}

/* ══ DEPTH ═══════════════════════════════════════════════════════════════════
   A near-black page with nothing behind it is not dark design, it is an unlit
   room. Glass has nothing to refract and every surface reads as a rectangle
   drawn on a void. What follows is the light the material needs to exist.

   Two fixed fields, painted behind everything, at alphas low enough that no
   contrast pair in build-check.sh moves: a warm one anchored top-left where the
   reader enters, and a cool Highland Green one bottom-right so the ground is
   not one flat temperature. They are fixed rather than scrolled, so the page
   moves THROUGH the light instead of dragging it along. */
/* `fixed` pinned this to the VIEWPORT, not the page, so five screens down a
   long route it was nowhere: a corner glow near the top of the document doing
   nothing for the frost forty sections later. Glass with nothing behind it to
   blur is just a translucent grey rectangle, which is the exact complaint a
   page built almost entirely of .card grids earns. `absolute` with no
   positioned ancestor above it fills the initial containing block, which for
   a normal scrolling document is the full page height, not the viewport, so a
   field placed at 70% down actually sits behind whatever renders at 70% down.
   Kept to the two shared-token colours here because this base rule also
   serves the light theme; the dark override below replaces it with five
   blobs across the actual swatch, since --color-cherry-berry and its
   neighbours only exist on that theme.

   THE SECOND FIELD IS ESPRESSO, NOT GREEN. Forest Canopy never goes into a
   gradient with another colour — blended it goes muddy, and over a warm ground
   it reads as a stain rather than as light. Where a gradient wants a second
   voice it reaches for Roasted Earth, which is the same warmth the page is
   already made of. Forest Canopy is a FLAT band or nothing. */
body::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(1100px 780px at 8% 4%,
      color-mix(in srgb, var(--color-accent) 11%, transparent) 0%, transparent 62%),
    radial-gradient(900px 700px at 92% 96%,
      color-mix(in srgb, var(--color-coffee) 13%, transparent) 0%, transparent 60%);
}
/* On the light theme the same fields would muddy Harvest Parchment, so they are
   halved rather than reused at full strength. */
:root:not([data-theme="dark"]) body::before { opacity: 0.5; }
:root[data-theme="dark"] body::before { background: var(--ambient-field); }

/* ── Grain ────────────────────────────────────────────────────────────────────
   The light fields above give the room a lamp. This gives it a surface. No
   photograph any more, plain hex under a repeating procedural grain: a small
   tile has no cover-to-viewport scaling to get wrong the way a 4K photo did,
   so it needs none of the fixed-and-clip-path apparatus that required, it
   just repeats in each ground's own background like any other tiling image.
   Undirected noise also has no seam to show at a section boundary the way a
   photographed leather grain running the wrong way did: two tiles of the
   same static look identical wherever they meet, so the seam hairline
   .section-tint::before draws (further down) is safe to keep on dark too,
   nothing here steals that pseudo-element from it any more.

   body used to carry this on a fixed, viewport-pinned ::after instead of its
   own background, left over from when the grain was a 4K photo and needed a
   pseudo-element it could clip separately from the section it sat behind.
   With a plain SVG lit-relief tile that reasoning no longer applies, and the
   fixed pseudo-element turned out to have its own cost: at full document
   height a feDiffuseLighting filter asked to rasterise across the whole
   fixed viewport box intermittently painted nothing at all, while the exact
   same token on a normal, non-fixed .section-tint a few lines below always
   rendered. Whatever Chromium's actual limit is there, the fix is the same
   one every other ground already uses: put it in body's own background,
   scrolling with the document like any other tiling image, not pinned to
   the viewport. Dark only, Harvest Parchment is already a paper, it does
   not need one. */
/* ── The wash ────────────────────────────────────────────────────────────────
   The grain tile alone is even everywhere, which is the one thing the real
   reference is not. Look at the six palette swatches: the colour itself reads
   CLEAREST through the middle of each card and the grain builds up toward the
   edges, so the material looks lit from the front and falls away at its
   borders. That gradient is not something a repeating tile can carry — a tile
   repeats, so any variation baked into it repeats with it. It has to be a
   second, NON-repeating layer sized to the element.

   So each ground stacks three layers, top to bottom:
     1. a radial wash of the ground's own colour, opaque at the centre and
        fading out — this is what hides the grain in the middle and lets it
        through at the edges,
     2. the repeating --grain tile,
     3. the flat ground colour underneath.
   --ground is declared per rule so the wash and the base can never drift
   apart into two different colours.

   The wash geometry is deliberately not identical across the four grounds:
   in the reference no two swatches wear it the same way, and four sections
   stacked down a page with the exact same off-centre highlight would read as
   a repeating template rather than as material. Each gets its own centre and
   radius; none is centred perfectly, because nothing photographed is. */
:root[data-theme="dark"] body,
:root[data-theme="dark"] .section-tint,
:root[data-theme="dark"] .band,
:root[data-theme="dark"] .band-dark {
  /* FIVE layers, each load-bearing. Top to bottom, matching the background
     shorthand and the blend list below it:

     1. --wash, the ground's own colour, solid through the middle and fading
        out over a long ramp. Being solid is the point: it is what hides the
        second grain pass centrally, which is how the grain ends up denser at
        the sides without needing a mask (a background layer cannot carry its
        own mask).
     2. --vignette, transparent in the middle to near-black at the rim,
        blended MULTIPLY rather than normal. That distinction is the whole
        difference between "darker sides" and "denser sides": normal lays flat
        darkness ON TOP of the grain and flattens it, multiply scales every
        pixel proportionally, so the grain's dark pixels fall much further than
        its light ones and its contrast RISES toward the edge.
     3-4. the same --grain tile twice, overlay then soft-light, so what
        accumulates outside the centre wash is a thicker, higher-contrast grain
        than the single soft-light pass surviving in the middle.
     5. the flat ground colour, with nothing beneath it to blend against.

     Two things were deliberately REMOVED here after they read as blotchy, and
     both should stay removed. A third hard-light grain pass: hard-light has a
     hard pivot at mid-grey, so it hugely amplifies any broad light/dark drift
     and turns gentle variation into patches. And the low-frequency "cloud"
     field that --grain used to carry internally (a second feTurbulence
     modulating the grain's density): it was added to make the texture
     irregular, but once there is a vignette doing the large-scale variation
     properly, a second source of large-scale variation only fights it — noise
     is blotchy where a gradient is smooth. Rule of thumb this cost several
     rounds to learn: ALL broad variation belongs to the gradient, ALL fine
     variation belongs to the tile, and neither should do the other's job.

     The vignette darkening is safe for contrast rather than risky: every one
     of these grounds is dark with light type on it, so the rim going darker
     RAISES text contrast. It never goes the other way. */
  /* Both off. The wash and the vignette are what turned each dark ground into a
     lit leather panel — a centre glow falling to 80% black at the rim. The
     brand's dark mode is a flat, calm Charcoal ground, and a section reads as a
     section because of its surface colour and the space around it. Neutral
     values rather than deleted rules, because the same five-layer `background`
     shorthand is composed in four places below. */
  --wash: transparent;
  --vignette: transparent;
  background-blend-mode: normal, normal, normal, normal, normal;
}
:root[data-theme="dark"] body {
  --ground: var(--color-bg);
  background:
    radial-gradient(ellipse 95% 88% at 47% 45%, var(--wash) 0%, var(--wash) 10%, transparent 70%),
    radial-gradient(ellipse 95% 88% at 47% 45%, transparent 20%, var(--vignette) 100%),
    var(--grain) repeat, var(--grain) repeat,
    var(--ground);
}
:root[data-theme="dark"] .section-tint {
  --ground: var(--color-surface);
  background:
    radial-gradient(ellipse 92% 90% at 52% 47%, var(--wash) 0%, var(--wash) 10%, transparent 70%),
    radial-gradient(ellipse 92% 90% at 52% 47%, transparent 20%, var(--vignette) 100%),
    var(--grain) repeat, var(--grain) repeat,
    var(--ground);
}
:root[data-theme="dark"] .band {
  --ground: var(--color-accent-2-800);
  background:
    radial-gradient(ellipse 98% 85% at 45% 52%, var(--wash) 0%, var(--wash) 10%, transparent 70%),
    radial-gradient(ellipse 98% 85% at 45% 52%, transparent 20%, var(--vignette) 100%),
    var(--grain) repeat, var(--grain) repeat,
    var(--ground);
}
:root[data-theme="dark"] .band-dark {
  --ground: var(--color-surface);
  background:
    radial-gradient(ellipse 90% 92% at 54% 49%, var(--wash) 0%, var(--wash) 10%, transparent 70%),
    radial-gradient(ellipse 90% 92% at 54% 49%, transparent 20%, var(--vignette) 100%),
    var(--grain) repeat, var(--grain) repeat,
    var(--ground);
}

/* The gallery card was the one card still painting an opaque surface, which on
   a leather ground is a brown rectangle with a photograph in it. Same pane as
   .card, same edge-light, and the same two degrade paths below.

   box-shadow lists BOTH the inset edge-light and the drop shadow, and that is
   a fix, not a flourish: box-shadow is one property, so naming only
   --glass-edge here silently threw away the --shadow-sm the base .card-gallery
   rule sets, and every one of these cards sat flat on the dark theme with no
   separation from the ground while the light theme kept its lift. Inset first,
   drop second: the specular lip belongs to the pane's own top edge, the shadow
   belongs to the space underneath it. */
:root[data-theme="dark"] .card-gallery {
  background: var(--glass-bg);
  border-color: var(--glass-border);
  box-shadow: var(--glass-edge), var(--shadow-sm);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
}
/* Hover and press have to restate both halves for the same reason — the base
   rules below name a single shadow token and would drop the edge-light. The
   ramp mirrors the light theme exactly: sm at rest, md on hover, sm again on
   press, so the card's lift means the same thing on either ground. */
:root[data-theme="dark"] .card-gallery:hover {
  box-shadow: var(--glass-edge), var(--shadow-md);
}
:root[data-theme="dark"] .card-gallery:active {
  box-shadow: var(--glass-edge), var(--shadow-sm);
}
:root[data-theme="dark"] .band .card { background: var(--glass-bg-band); }

/* A record is not a material. Kept as a guard even though nothing paints
   texture onto a card any more: it is exactly the mistake that was made once
   already, and the compliance pages are where it would cost the most. */
:root[data-theme="dark"] .record-surface,
:root[data-theme="dark"] .record-surface .card,
:root[data-theme="dark"] .record-surface .disclose,
:root[data-theme="dark"] .record-surface .req li {
  background-image: none;
}

/* ── The old light step ───────────────────────────────────────────────────────
   Four rules name --color-accent-300 directly. Every one of them means "the
   accent, but the ground under it is inverted", a hover border, a footer link,
   a call to action on a scrim. On the light theme that is still Coffee Cherry's
   light step and still correct. On this one --color-accent IS already the light
   step, so left alone these would paint last season's salmon beside the cherry
   and put a second red on the page. There is only one red. */
:root[data-theme="dark"] .role-card:hover,
:root[data-theme="dark"] .card-gallery:hover { border-color: var(--color-accent); }
:root[data-theme="dark"] .site-foot a:hover,
:root[data-theme="dark"] .card-photo .role-card-go { color: var(--color-accent); }

/* ── Solid berry, not the readable tint ──────────────────────────────────────
   --color-accent is lightened for one reason: it is text, in places as small as
   a nav link, and small text on Deep Espresso needs to be light to read at all.
   A button fill has no such constraint. It pairs with a label it also controls,
   so the fill can be the actual Cherry Berry from the swatch, #7a1e22, with
   cream doing the reading, 9.14 against it, far past the 4.5 floor. Filled with
   the light text-safe tint instead, every primary action on the site came out
   the same pale salmon pink, which is how a specific, dark, saturated red ends
   up looking like nothing in the swatch at all. Hover and active stay in the
   same family, mixed toward white and black rather than reused from the old
   Coffee Cherry ramp, which would fight the new base on every state change. */
:root[data-theme="dark"] .btn-primary {
  background: var(--color-cherry-berry); color: var(--color-text);
}
/* The sector figure is a FILL, like the button, so it takes the exact accent
   and not the lifted one. --color-accent is raised on the dark theme because
   it is TYPE there; carrying Morning Mist on it reads 3.58 and fails, where
   Heritage Clay reads 6.03. Every filled accent on this theme has to be pinned
   the same way — .btn-primary above and .nav-auth below already are. */
:root[data-theme="dark"] .stat-card__figure {
  background: var(--color-cherry-berry); color: var(--color-text);
}

/* Accent TEXT on a RAISED surface needs another step than accent text on the
   ground. --color-accent-dark is tuned to #121212 and reads 4.89 there; a card
   is Volcanic Stone, twenty points lighter, and the same value reads 3.30 on
   it — so "Read the full role page" failed inside a card while the identical
   link passed on the page. accent-300 clears both: 7.79 on the ground, 5.25 on
   the surface. */
:root[data-theme="dark"] .role-card-go { color: var(--color-accent-300); }

/* A card is Roasted Earth itself — the top of the ladder and the only place
   the espresso is at full strength, so the raised thing is the warmest thing.
   The hairline is what guarantees the separation: 1.20 against the band it
   usually sits on is a real step but a quiet one, and a warm rule makes it
   unambiguous without lightening the card. */
:root[data-theme="dark"] .card,
:root[data-theme="dark"] .card-gallery {
  background: var(--eca-roasted-earth);
  border: 1px solid color-mix(in srgb, var(--eca-woven-taupe) 20%, transparent);
}

/* The two flat colours a photograph sits on, warmed to match. Both are
   neutral greys by default — right for the light theme, and a cold patch on a
   page that is espresso everywhere else. They show while an image loads, or
   wherever one is missing, which is exactly when the page should still look
   like itself. */
:root[data-theme="dark"] .card-gallery .media { background-color: var(--color-espresso-band); }
:root[data-theme="dark"] .photo-hero.opener { background-color: var(--color-espresso-band); }
/* The anchor-button guard, dark reading: same rule as the light one beside
   .btn-primary, re-asserted here so the doubled-class light guard cannot pin
   --color-bg (dark ink) onto the cherry-berry fill. */
:root[data-theme="dark"] a.btn.btn-primary,
:root[data-theme="dark"] a.btn.btn-primary:hover { color: var(--color-text); }
:root[data-theme="dark"] .btn-primary:hover {
  background: color-mix(in srgb, var(--color-cherry-berry) 82%, white);
}
:root[data-theme="dark"] .btn-primary:active,
:root[data-theme="dark"] .btn-primary:active:hover {
  background: color-mix(in srgb, var(--color-cherry-berry) 78%, black);
}
:root[data-theme="dark"] .band .btn-primary:hover,
:root[data-theme="dark"] .band-dark .btn-primary:hover {
  background: color-mix(in srgb, var(--color-cherry-berry) 88%, white);
}

/* The same trade applies wherever a control fills solid and labels itself: the
   segmented switch, the atlas driver tab the readme calls "the only accent on
   that page," and a pressed filter chip. All three already pair the fill with a
   colour they also set, so all three take the real berry instead of the tint. */
:root[data-theme="dark"] .seg-opt:has(input:checked) {
  background: var(--color-cherry-berry); color: var(--color-text);
}
:root[data-theme="dark"] .atlas-switch .seg-opt[aria-pressed="true"] {
  background: var(--color-cherry-berry); color: var(--color-text);
}
:root[data-theme="dark"] .chip-main[aria-pressed="true"] {
  background: var(--color-cherry-berry); color: var(--color-text);
}

/* ── Photography ─────────────────────────────────────────────────────────────
   Photographs run untreated on both themes — actual photos, full colour, no
   grade, no wash. Decision 2026-08-29: on photorealistic imagery a treatment
   reads as a fault, not a style; treatment belongs to an art-directed image
   set, which is its own build (see BRIEF-image-modes.md). A scrim where copy
   sits on a photograph stays — that is legibility, not a look. */

/* A seam, not a hard edge. Alternating section grounds on a dark page produce a
   visible step; a hairline that fades at both ends reads as a fold instead. */
.section-tint { position: relative; }
.section-tint::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--color-text) 14%, transparent) 22%,
    color-mix(in srgb, var(--color-text) 14%, transparent) 78%, transparent);
}

/* A page-wide ambient field spread across a 9000px document leaves dead zones
   between its blobs wherever a section doesn't happen to land near one, and
   the closing Join card is exactly the kind of section that can't be left to
   chance: it is the last thing a reader sees before the footer. This is a
   deliberate, section-scoped glow rather than a hope that the page field
   reaches this far down, sized to the section's own box so it always lands
   regardless of how tall the page above it grows. */
/* ── Section glows ────────────────────────────────────────────────────────
   Off. Five dark sections carried large coloured radial washes behind them —
   cherry into gold, gold into leaf — to keep a near-black page from reading
   flat. The brand's dark mode is a calm Charcoal ground, and a drifting
   multi-hue wash is exactly the generic treatment it asks to avoid; the
   sections are separated by surface colour and space instead. The rules and
   their classes stay so the hooks survive; only the paint is gone. */
:root[data-theme="dark"] .glow-join { position: relative; overflow: hidden; }
:root[data-theme="dark"] .glow-join::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: none;
}

/* .section-tint already fills solid with --color-surface, so a glow behind
   that at z-index:-1 (Join's trick) would render under an opaque colour and
   never be seen. Here the glow has to sit ABOVE the section's own background
   but BELOW the real content, so the content wrapper gets an explicit
   z-index to win the stack instead of relying on paint order to sort it out.

   opacity is set explicitly rather than left to inherit, because .glow-
   compliance also carries .section-tint, and .section-tint::after already
   carries an opacity of its own from the grain system that used to run here.
   With no competing declaration this glow would render at barely a tenth of
   its intended strength. Same pseudo-element, two rules, cascade per property,
   not per block — which still holds now that both paint nothing. */
:root[data-theme="dark"] .glow-compliance::after {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: 1;
  /* The first two attempts here centred on percentages that, on the actual
     rendered grid, landed under the EUDR/Certificates cards themselves, both
     opaque, so every peak was hidden and only the fringe reached daylight.
     Anchored on the section's own corners instead, with a wide falloff: the
     two-column layout reliably leaves the far corners clear regardless of how
     the copy or the card stack reflow. */
  background: none;
}
:root[data-theme="dark"] .glow-compliance > .wrap { position: relative; z-index: 1; }

/* Plain .section, same trick as Join: no opaque fill of its own to hide
   behind, so z-index:-1 is enough. Three bare text columns with nothing
   else on the page is the flattest thing here, worth the same treatment. */
:root[data-theme="dark"] .glow-changing { position: relative; overflow: hidden; }
:root[data-theme="dark"] .glow-changing::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: none;
}

/* .section-tint again, so the opacity collision and the wrap-z-index trick
   both apply exactly as they do on .glow-compliance. */
:root[data-theme="dark"] .glow-pulse::after {
  content: ''; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  opacity: 1;
  background: none;
}
:root[data-theme="dark"] .glow-pulse > .wrap { position: relative; z-index: 1; }

/* Plain .section again, same as .glow-changing: no fill of its own, so the
   cheap z-index:-1 trick is enough, no wrap override needed. */
:root[data-theme="dark"] .glow-about { position: relative; overflow: hidden; }
:root[data-theme="dark"] .glow-about::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background: none;
}

/* ══ THE FEATURE PANEL ═══════════════════════════════════════════════════════
   The "what changes for you" triplets were three naked columns of text with
   nothing holding them. On a light ground whitespace alone can carry a group;
   on a near-black one it reads as three unrelated paragraphs adrift. They
   become the material, and the numeral does the counting the eye was doing. */
.section .stack:has(> h3) {
  position: relative;
  padding: var(--space-5) var(--space-5) var(--space-5) var(--space-6);
  border-radius: var(--radius-lg);
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-edge), var(--shadow-sm);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
}
/* The accent's whole job on this page is "live, selected or actionable". A rule
   marking a panel is none of those, so the marker is drawn in the institutional
   second voice and the accent stays scarce enough to still mean something. */
.section .stack:has(> h3)::before {
  content: ''; position: absolute; left: 0; top: var(--space-5); bottom: var(--space-5);
  width: 2px; border-radius: 1px;
  /* Espresso, not green: a gradient is the one place Forest Canopy may not go.
     See the ambient field above. */
  background: linear-gradient(180deg,
    var(--color-coffee), color-mix(in srgb, var(--color-coffee) 0%, transparent));
}
.section .stack:has(> h3) > h3 { margin: 0; }

/* Headings on a 1600px container were breaking to three lines against a ch cap
   written for an 1180px one, leaving a heading column beside an empty half. The
   cap rises with the viewport instead of being retyped per element. */
@media (min-width: 64rem) {
  .section h2[style*="max-width"] { max-width: 30ch !important; }
}

/* ══ TOUCH TARGETS ═══════════════════════════════════════════════════════════
   Measured at 390px, every interactive element on the site was under the 44px
   floor: buttons at 35, atlas chips at 34, the driver switch at 29, the header
   icons at 40. The chip's 34px is a real documented spec, but it was written
   for a desktop design and a finger is not a cursor.

   Growing the box rather than overlaying an invisible hit area is deliberate.
   An expanded ::after on a 34px chip in a tightly packed row overlaps its
   neighbours, which trades a small target for a mis-tap. Coarse pointers get a
   roomier layout, which is what they should have had; the pointer design is
   untouched, so nothing about the desktop rendering moves. */
@media (pointer: coarse) {
  .btn, .seg-opt, .chip-main, .chip-plain, .disclose summary { min-height: 44px; }
  .chip, .chip-i { min-height: 44px; }
  .chip-i { min-width: 44px; }
  .theme-toggle, .nav-toggle, .nav-close, .atlas-sheet-x { width: 44px; height: 44px; }
  /* Footer link columns are bare anchors at 22px, which is half the floor and
     the densest stack of targets on the page. Padding rather than a gap, so the
     tappable box grows instead of the space between boxes. */
  .foot-links a, .site-foot .foot-links a {
    display: flex; align-items: center; min-height: 44px;
  }
  .foot-links { gap: 0; }
  .mark { min-height: 44px; }
  /* The row gaps grow with the targets, so the 8px separation the guideline
     asks for survives the taller controls. */
  .chip-row, .chip-group { gap: var(--space-2); }
}

/* Figures line up in columns. Proportional digits make a stat row and the atlas
   chip counts jitter as values change width. */
.stat-num, .chip-n, .atlas-facts, .table td { font-variant-numeric: tabular-nums; }

/* ── Coffee Pulse ─────────────────────────────────────────────────────────────
   The association's own writing, which is the one surface on this site whose
   body is not laid out by anybody: a post is typed into the WordPress editor,
   so the system owns the measure, the rhythm and the ladder and the writer
   picks nothing. Everything below is tokens; a post cannot introduce a colour,
   a weight or a face the brand has not approved because there is nowhere to
   type one.

   The cards are `.card-gallery` and the home rail is `.rail`, both defined
   above — a Pulse post is the same object as an event and an academy bundle,
   deliberately, so a rail does not change shape by subject. */

/* The current issue, told apart from the archive by size rather than a badge. */
.pulse-lead { display: grid; gap: var(--space-8); align-items: center; }
@media (min-width: 48rem) {
  .pulse-lead { grid-template-columns: 58fr 42fr; }
}
.pulse-lead__art {
  display: block; position: relative; aspect-ratio: 16 / 10;
  border-radius: var(--radius-lg); overflow: hidden; text-decoration: none;
  background: var(--color-neutral-800) center / cover no-repeat;
}
.pulse-lead__art .card-kicker {
  display: inline-flex; align-items: center; position: absolute;
  left: var(--space-3); top: var(--space-3); z-index: 1;
  padding: 5px var(--space-2); border-radius: var(--radius-sm);
  background: var(--eca-morning-mist); color: var(--eca-charcoal);
  font-weight: var(--weight-semibold); letter-spacing: 0.06em; line-height: 1;
  box-shadow: var(--shadow-sm);
}
.pulse-lead__body { display: flex; flex-direction: column; gap: var(--space-3); }
.pulse-lead__body h2 { margin: 0; max-width: 20ch; }
.pulse-lead__body h2 a { color: var(--color-text); text-decoration: none; }
.pulse-lead__body h2 a:hover { color: var(--color-accent); }
.pulse-lead__body .eyebrow { margin-bottom: 0; }

/* The archive. A grid rather than a `.rail`: these are read by scanning down a
   page, not by swiping along a shelf. */
.pulse-grid { display: grid; gap: var(--space-4); }
@media (min-width: 48rem) {
  .pulse-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Older and newer, and nothing else. A numbered page list is a component this
   system does not have, and a monthly publication has no use for one. */
.pulse-pager {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-4); margin-top: var(--space-8);
  padding-top: var(--space-4); border-top: 1px solid var(--color-divider);
}
.pulse-pager .eyebrow { margin-bottom: 0; color: var(--color-text-secondary); }
.pulse-pager__go { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.pulse-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* Nothing published yet — the state the site is in until the association writes
   the first entry. Dashed, because it is a container waiting for something
   rather than a card carrying something. */
.pulse-empty {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4);
  padding: var(--space-12); border-radius: var(--radius-lg);
  background: var(--color-surface); border: 1px dashed var(--color-taupe);
}
.pulse-empty h2 { margin: 0; max-width: 24ch; }
.pulse-empty .eyebrow { margin-bottom: 0; }

/* One post. 66ch is the brand's long-form measure; the cover is the 21:9 of the
   art brief rather than the 16:10 a card uses, because it is a header and not a
   thumbnail. */
.pulse-article {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4);
  max-width: 66ch; margin-inline: auto;
}
.pulse-article h1 { margin: 0; }
.pulse-article .eyebrow { margin-bottom: 0; color: var(--color-text-secondary); }
.pulse-article .lede { margin: 0; color: var(--color-text-secondary); }
.pulse-cover {
  aspect-ratio: 21 / 9; max-width: var(--content-max); margin-inline: auto;
  border-radius: var(--radius-lg);
  background: var(--color-neutral-800) center / cover no-repeat;
}

/* The body. Written in the editor, so every element it can emit is set here.

   The rhythm is set on the CHILD SELECTOR and nowhere else. It was
   `> * + * { margin-top }` alongside `.pulse-prose p { margin: 0 }`, and the
   second wins on specificity — one class plus a type beats one class plus the
   universal — so every paragraph, heading and list in every post came out
   flush against the one above it. The reset below is deliberately the same
   specificity as the rhythm and deliberately above it. */
.pulse-prose { display: block; gap: 0; }
.pulse-prose > * { margin: 0; }
.pulse-prose > * + * { margin-top: var(--space-6); }
.pulse-prose p { font-size: var(--text-xl); line-height: var(--leading-body); }
.pulse-prose ul, .pulse-prose ol {
  padding-left: var(--space-5);
  font-size: var(--text-xl); line-height: var(--leading-body);
}
.pulse-prose li + li { margin-top: var(--space-2); }
.pulse-prose a { color: var(--color-accent); text-underline-offset: 3px; }
.pulse-prose img, .pulse-prose figure img {
  display: block; width: 100%; height: auto; border-radius: var(--radius-lg);
}
.pulse-prose figure > * { margin: 0; }
.pulse-prose figcaption {
  margin-top: var(--space-2); font-size: var(--text-xs); color: var(--color-text-secondary);
}
/* The only place a quotation is marked, and it is marked with the action colour
   rather than with a glyph — the brand draws no quote marks. */
.pulse-prose blockquote {
  padding: var(--space-1) 0 var(--space-1) var(--space-5);
  border-left: 3px solid var(--color-accent);
  font-size: var(--text-2xl); line-height: 1.6; color: var(--color-text-secondary);
}
.pulse-prose blockquote p { font-size: inherit; line-height: inherit; }
.pulse-prose hr {
  width: 100%; height: 0; border: 0; border-top: 1px solid var(--color-divider);
}
.pulse-article__foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  width: 100%; margin-top: var(--space-10);
  padding-top: var(--space-4); border-top: 1px solid var(--color-divider);
}
.pulse-article__foot .role-card-go { margin-left: auto; }

/* Previous and next. Two cards, because the titles are the only thing that
   tells a reader whether either is worth opening. */
.pulse-nav { display: grid; gap: var(--space-4); }
@media (min-width: 48rem) {
  .pulse-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.pulse-nav__side {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-6); border-radius: var(--radius-lg);
  background: var(--color-bg); border: 1px solid var(--color-taupe);
  color: var(--color-text); text-decoration: none;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.pulse-nav__side:hover { border-color: var(--color-accent-300); box-shadow: var(--shadow-md); }
.pulse-nav__side h3 { margin: 0; font-size: var(--text-3xl); }
.pulse-nav__side .eyebrow { margin-bottom: 0; color: var(--color-text-secondary); }
/* The direction is the meaning, so it is drawn on the label rather than typed
   into it — the same reasoning as `.btn-back`. */
.pulse-nav__side .eyebrow::before { content: "← "; }
.pulse-nav__side--next { text-align: right; }
.pulse-nav__side--next .eyebrow::before { content: none; }
.pulse-nav__side--next .eyebrow::after { content: " →"; }

/* A post that exists to test the template says so. The site is publicly
   indexable — `blog_public` is 1 and the sitemap is live — so placeholder copy
   under the association's name needs to be unmistakable to a reader as well as
   to a crawler. Dashed, like `.pulse-empty`: a container holding something
   provisional rather than a card carrying something settled. */
.pulse-sample {
  margin: 0; width: 100%; padding: var(--space-3) var(--space-4);
  border: 1px dashed var(--color-accent); border-radius: var(--radius-md);
  background: var(--tint-accent-hover);
  font-size: var(--text-base); line-height: var(--leading-body);
  color: var(--color-text);
}
