/* Generated from _build/tokens.py — do not edit by hand. */
:root {
  --gkh-p9: #301934;
  --gkh-p8: #66023C;
  --gkh-mag: #AA0061;
  --gkh-violet: #84329B;
  --gkh-olive: #88A23A;
  --gkh-orchid: #C38FCC;
  --gkh-logo-plum: #4C0132;
  --gkh-canvas: #E6E6FA;
  --gkh-surface: #ffffff;
  --gkh-ink: #301934;
  --gkh-ink2: #476287;
  --gkh-ink3: #5C5470;
  --gkh-line: #BDBED2;
  --gkh-soft: #DEDCEA;
  --gkh-action: #AA0061;
  --gkh-focus: #84329B;
  --gkh-shadow: 0 1px 2px rgba(48,25,52,.05), 0 8px 28px rgba(48,25,52,.08);
  --gkh-f-head: Roboto Slab, Georgia, Times New Roman, serif;
  --gkh-f-body: Arial, Helvetica Neue, Helvetica, system-ui, sans-serif;
  --gkh-header-sticky-offset: 84px;
}

/* Gender Knowledge Hub — site stylesheet.
 *
 * Pushed to Builder Settings.style by _build/run.py, which prepends a :root
 * block generated from the design tokens in _build/tokens.py. Every colour,
 * font and measure below is therefore a var(--gkh-*) rather than a literal:
 * change the token in Python and it lands here.
 *
 * This used to be a 925-line f-string inside blocks.py. As CSS in a .css
 * file it is lintable and highlightable, and — more to the point — every
 * brace is just a brace. In the f-string each one had to be doubled, and a
 * single missed escape once turned content: "\\2212" into Python's octal
 * \\221 followed by a stray '2', which shipped as "Other industries 2".
 */
:root{ color-scheme: light; }
*{ box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body{ background: var(--gkh-canvas); color: var(--gkh-ink); font-family: var(--gkh-f-body); font-size: 16px; line-height: 1.6;
  -webkit-font-smoothing: antialiased; }
/* font-weight:700 is NOT redundant. Builder ships Tailwind's preflight
   (assets/builder/reset.css), which sets h1-h6 { font-weight: inherit } —
   so headings silently inherit the body's 400 and render thin. The wireframe
   declares no weight on its headings either, but there the browser default
   (bold) applies, so it looks correct. Restoring it here is what makes the
   two match. A block that sets its own fontWeight still wins: Builder emits
   per-block class selectors, which outrank this element selector. */
h1, h2, h3, h4 { font-family: var(--gkh-f-head); font-weight: 700; margin: 0; text-wrap: balance; }
a { color: var(--gkh-action); }
/* text-decoration only, deliberately NOT color here — "a p"/"a span" with a
   color would beat any button/link's own inline color (e.g. white text on a
   dark ghost button) by specificity, since a bare child <p>/<span> usually
   carries no color rule of its own to out-rank it. Regression found and
   fixed during QA: ghost-button and footer link text were forced pink. */
a, a p, a span, a b { text-decoration: none !important; }
button { font: inherit; cursor: pointer; border: 0; background: none; color: inherit; }
:focus-visible { outline: 3px solid var(--gkh-focus); outline-offset: 2px; border-radius: 3px; }

/* Header mobile nav — pure-CSS checkbox toggle (no client script needed).
   Builder's own responsive system only has one breakpoint (max-width:576px,
   confirmed by inspecting its rendered output) and no notion of client-side
   state, so below that width the nav/CTA can't just be hidden — they need a
   real disclosure control.

   Breakpoint is 1120px, not 576px. Measured: logo 252 + six nav items 493 +
   CTA 185 + padding/gap 66 = ~996px before the row wraps. At 576px the header
   overflowed for 420px of viewport width — which is exactly what IPE reported
   on 28 Aug ("we see a hamburger", a 1440px laptop at 100% zoom lands under
   the old wireframe's 1300px rule). 1120 keeps the full row on a 1280px window
   with headroom, and collapses before anything can wrap. #gkh-nav-toggle/#gkh-burger/#gkh-nav-wrap are
   siblings inside header.py's `inner` block; ID selectors so this survives
   regardless of whatever class Builder itself generates for each block. */
#gkh-nav-toggle { display: none; }
#gkh-burger { display: none; }
@media only screen and (max-width: 1120px) {
  #gkh-header-inner { position: relative; flex-wrap: nowrap; }
  #gkh-burger {
    display: inline-flex !important; align-items: center; justify-content: center;
    width: 38px; height: 38px; margin-left: auto; border-radius: 7px; cursor: pointer;
    flex-shrink: 0;
  }
  #gkh-burger:hover { background: var(--gkh-soft); }
  #gkh-nav-wrap {
    display: none !important;
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: #fff; border-bottom: 1px solid var(--gkh-line); box-shadow: var(--gkh-shadow);
    padding: 6px 20px 18px; z-index: 5;
  }
  #gkh-nav-toggle:checked ~ #gkh-nav-wrap { display: flex !important; }
  #gkh-nav-wrap #gkh-nav {
    flex-direction: column !important; align-items: stretch !important; gap: 0 !important;
    margin-left: 0 !important; width: 100%;
  }
  #gkh-nav-wrap #gkh-nav > a, #gkh-nav-wrap #gkh-nav > span {
    width: 100%; padding: 13px 4px !important; border-bottom: 1px solid var(--gkh-soft);
  }
  #gkh-nav-wrap #gkh-auth { margin-left: 0 !important; margin-top: 14px !important; width: 100%; }
  #gkh-nav-wrap #gkh-auth a { display: block; text-align: center; }
}

/* Shared section "kicker" label (small uppercase caption above a heading) —
   identical style dict was previously repeated inline on 5+ blocks across
   home.py/about.py. One real CSS class here, referenced via classes=["gkh-kicker"]
   (see kicker_label() below), instead of Builder auto-generating a separate
   fb-<hash> style class per block for the same rules. */
.gkh-kicker {
  /* 13px, matching the wireframe's .k. The build had 11.5px, which made every
     kicker on the site a size small. */
  font-size: 13px; letter-spacing: .15em; text-transform: uppercase;
  color: var(--gkh-action); font-weight: 700; margin-bottom: 11px;
}

/* Resource detail page — video click-to-play thumbnail. Same checkbox-hack
   pattern as the mobile nav (Builder has no native disclosure block — see
   frappe-builder-development skill's global-css-and-reuse.md). Shows a
   static cover-image thumbnail with a play button until clicked, then swaps
   to the real embed — avoids ever rendering a live iframe at a size it can
   overflow, and gives a genuine click event to count a real video play. */
#gkh-preview-toggle { display: none; }
#gkh-preview-iframe-wrap { display: none; }
#gkh-preview-toggle:checked ~ #gkh-preview-iframe-wrap { display: block; }
#gkh-preview-toggle:checked ~ #gkh-preview-thumb { display: none; }
#gkh-preview-toggle:checked ~ #gkh-preview-playbtn { display: none; }

/* ── E3, E8 · section transitions on scroll ───────────────────────────────
   IPE, 3 Sep: "when we are skipping from a section to the other, it's coming
   out as a long scroll… is it possible to add some sort of animation or some
   very subtle transition between the sections so that it doesn't look like one
   long page being just scrolled through." Then, 3 Sep: "can we have fade in or
   much better animation on scroll for landing page?"

   TWO paths, and which one runs is decided by the .gkh-js class that GLOBAL_JS
   puts on <html> before first paint.

   WITH script (.gkh-js) — the good one. A real fade: opacity, a longer rise and
   a whisper of scale, driven by an IntersectionObserver that adds .is-in.
   Children stagger behind their section via --gkh-d.

   WITHOUT script — the transform-only scroll-driven fallback kept from the
   earlier build. It never touches opacity, for the reason recorded under
   ISSUE-001 below.

   ISSUE-001 · why opacity is safe HERE but was not before. A scroll-driven
   animation (animation-timeline: view()) sits at its START value until its
   scroll range is entered, so an opacity-from-0 keyframe left three whole
   sections blank in anything that renders without scrolling — full-page
   screenshots, print, crawlers. IPE review this site by screenshot, so that was
   the review path, not a corner case. An IntersectionObserver has the opposite
   failure mode: a full-page capture makes the viewport the whole document, so
   every section intersects and every section reveals. The script also reveals
   everything outright on print, on prefers-reduced-motion, and after a 2.5s
   watchdog — so no path through this code can leave content hidden. */
@media (prefers-reduced-motion: no-preference) {
  /* -- scripted path ----------------------------------------------------
     Note what is NOT here: a bare `.gkh-reveal { opacity: 0 }`. Hiding is
     applied only by the .gkh-armed class, which the script adds to a section
     only after the visitor has actually started scrolling AND only while that
     section is still well below the fold. A page that is never scrolled — a
     screenshot, a print, a crawler — is therefore never hidden at all. */
  html.gkh-js .gkh-reveal {
    transition: opacity .66s cubic-bezier(.22,.61,.36,1),
                transform .66s cubic-bezier(.22,.61,.36,1);
    transition-delay: var(--gkh-d, 0ms);
  }
  html.gkh-js .gkh-reveal.gkh-armed {
    opacity: 0;
    transform: translateY(30px) scale(.988);
    will-change: opacity, transform;
  }
  html.gkh-js .gkh-reveal.gkh-armed.is-in {
    opacity: 1;
    transform: none;
    /* Arrived: stop promoting it to its own compositing layer. */
    will-change: auto;
  }

  /* ── Section-wise arrival ───────────────────────────────────────────────
     IPE, 7 Sep: "when we transition from one section to another, there's some
     sort of dynamic transition so that you know that the other thing is coming
     up ... the spotlight pops a little out in terms of some animated effect,
     rather than like a flat scroll."

     The section moving as one block is what read as flat: everything arrived
     together, so nothing announced itself. Now the parts arrive in order --
     heading, then body, then the cards or figures -- each a beat behind the
     last. It is the same reveal, sequenced.

     The delay is per-CHILD and rides on top of the section's own --gkh-d, so a
     section that arrives late in a batch still staggers internally from its own
     start rather than from the top of the page. */
  /* Most sections wrap everything in one max-width container, so their only
     DIRECT child is that wrapper -- staggering "> *" moved the whole section
     as one block again, which was the thing being fixed. Both levels are
     addressed: a section's own children, and, where the section has a single
     wrapper, that wrapper's children. */
  html.gkh-js .gkh-reveal.gkh-armed > *,
  html.gkh-js .gkh-reveal.gkh-armed > *:only-child > * {
    opacity: 0;
    transform: translateY(16px);
    transition: opacity .5s cubic-bezier(.22,.61,.36,1),
                transform .5s cubic-bezier(.22,.61,.36,1);
  }
  html.gkh-js .gkh-reveal.gkh-armed.is-in > *,
  html.gkh-js .gkh-reveal.gkh-armed.is-in > *:only-child > * {
    opacity: 1; transform: none;
  }
  html.gkh-js .gkh-reveal.gkh-armed.is-in > :nth-child(1),
  html.gkh-js .gkh-reveal.gkh-armed.is-in > *:only-child > :nth-child(1) { transition-delay: calc(var(--gkh-d, 0ms) + 60ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in > :nth-child(2),
  html.gkh-js .gkh-reveal.gkh-armed.is-in > *:only-child > :nth-child(2) { transition-delay: calc(var(--gkh-d, 0ms) + 170ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in > :nth-child(3),
  html.gkh-js .gkh-reveal.gkh-armed.is-in > *:only-child > :nth-child(3) { transition-delay: calc(var(--gkh-d, 0ms) + 260ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in > :nth-child(n+4),
  html.gkh-js .gkh-reveal.gkh-armed.is-in > *:only-child > :nth-child(n+4) { transition-delay: calc(var(--gkh-d, 0ms) + 330ms); }

  /* The figures pop rather than slide. IPE, 7 Sep: "the data, if it pops,
     it'll be nice ... if it pops up like how it is with the pictures."
     A short overshoot on scale reads as a pop where a translate reads as a
     slide, and each figure follows the one before it. */
  html.gkh-js .gkh-reveal.gkh-armed .gkh-stat {
    opacity: 0;
    transform: scale(.86);
    transition: opacity .42s ease-out, transform .42s cubic-bezier(.34,1.56,.64,1);
  }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-stat { opacity: 1; transform: none; }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-stat:nth-child(1) { transition-delay: calc(var(--gkh-d, 0ms) + 300ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-stat:nth-child(2) { transition-delay: calc(var(--gkh-d, 0ms) + 380ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-stat:nth-child(3) { transition-delay: calc(var(--gkh-d, 0ms) + 460ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-stat:nth-child(4) { transition-delay: calc(var(--gkh-d, 0ms) + 540ms); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-stat:nth-child(n+5) { transition-delay: calc(var(--gkh-d, 0ms) + 620ms); }

  /* -- unscripted fallback ---------------------------------------------- */
  html:not(.gkh-js) .gkh-reveal {
    animation: gkh-reveal-in linear both;
    animation-timeline: view();
    animation-range: entry 8% cover 26%;
  }
  @keyframes gkh-reveal-in {
    from { transform: translateY(22px); }
    to   { transform: none; }
  }

  .gkh-hero-in { animation: gkh-hero-in .62s cubic-bezier(.22,.61,.36,1) both; }
  .gkh-hero-art-in { animation: gkh-hero-in .78s cubic-bezier(.22,.61,.36,1) .1s both; }
  @keyframes gkh-hero-in {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: none; }
  }

  /* ── The hero collage ──────────────────────────────────────────────────
     IPE, 3 Sep: "can we have some animation for these images in the banner?"

     Two layers, on two different elements on purpose. The ARRIVAL is on each
     group; the continuous drift is on the shape inside it. One element cannot
     carry both, because both write `transform` and the later declaration would
     simply win — so the entrance would be discarded the moment the idle motion
     began.

     Every rotation restates its base angle (-25, 88, -52). A CSS transform
     REPLACES the SVG transform attribute rather than composing with it, so a
     keyframe of `rotate(4deg)` would not nudge an arc four degrees — it would
     snap it from 88 to 4 first. The base angle belongs in every keyframe.

     transform-box: fill-box puts each origin at the shape's own centre, which
     for these circles is exactly (cx, cy) — the same point their SVG rotate()
     already used. Without it the origin would be the whole viewBox corner and
     the arcs would swing across the composition.

     Idle motion is deliberately slower than it looks like it should be: 9 to
     60 seconds a cycle, a few degrees either way. It should be noticed on the
     second look, not the first, and it must never compete with the headline
     next to it. */
  .gkh-h-in,
  .gkh-h-photo {
    animation: gkh-h-arrive .72s cubic-bezier(.22,.61,.36,1) both;
    animation-delay: var(--d, 0s);
  }
  .gkh-h-photo {
    transform-box: fill-box;
    transform-origin: center;
    animation-name: gkh-h-arrive-photo;
    animation-duration: .84s;
  }
  @keyframes gkh-h-arrive {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  /* The photograph settles INTO its circle rather than fading on top of it. */
  @keyframes gkh-h-arrive-photo {
    from { opacity: 0; transform: scale(1.10); }
    to   { opacity: 1; transform: scale(1); }
  }

  .gkh-h-spin, .gkh-h-sway-m, .gkh-h-sway-o, .gkh-h-breathe,
  .gkh-h-kb-a, .gkh-h-kb-b {
    transform-box: fill-box;
    transform-origin: center;
  }
  /* Every piece SWAYS around its design angle; nothing orbits. A full rotation
     was tried on this thin ring and it looked good on its own, but a minute
     later the arc is on the other side of the composition — the arrangement
     IPE approved is not what a visitor would be looking at. Oscillation keeps
     the collage within a few degrees of the drawing at all times. The thin
     ring gets the widest travel because 2px at 55% opacity can afford it. */
  .gkh-h-spin { animation: gkh-h-spin 24s ease-in-out infinite alternate; }
  @keyframes gkh-h-spin {
    from { transform: rotate(-31deg); }
    to   { transform: rotate(-19deg); }
  }
  /* The two heavy arcs only sway. A full rotation of a 42px stroke would drag
     the eye off the headline. */
  .gkh-h-sway-m { animation: gkh-h-sway-m 9s ease-in-out infinite alternate; }
  @keyframes gkh-h-sway-m {
    from { transform: rotate(88deg); }
    to   { transform: rotate(91.5deg); }
  }
  .gkh-h-sway-o { animation: gkh-h-sway-o 11s ease-in-out infinite alternate; }
  @keyframes gkh-h-sway-o {
    from { transform: rotate(-52deg); }
    to   { transform: rotate(-48.5deg); }
  }
  .gkh-h-breathe { animation: gkh-h-breathe 13s ease-in-out infinite alternate; }
  @keyframes gkh-h-breathe {
    from { transform: scale(1); }
    to   { transform: scale(1.045); }
  }
  /* Ken Burns. The clip is on the parent group, so these move behind a fixed
     circle. Different durations and opposite directions, or the two
     photographs would breathe in lockstep and read as one object. */
  .gkh-h-kb-a { animation: gkh-h-kb-a 26s ease-in-out infinite alternate; }
  @keyframes gkh-h-kb-a {
    from { transform: scale(1) translate(0, 0); }
    to   { transform: scale(1.075) translate(-1.5%, 1%); }
  }
  .gkh-h-kb-b { animation: gkh-h-kb-b 21s ease-in-out infinite alternate; }
  @keyframes gkh-h-kb-b {
    from { transform: scale(1.07) translate(1%, 0.6%); }
    to   { transform: scale(1) translate(0, 0); }
  }
}

/* ── Hover zoom on the two photographs ────────────────────────────────────
   IPE, 3 Sep: "can we have a zoom in effect as well when we take cursor to
   the images in the banner?"

   On its OWN element, between the clip and the image. The image is already
   running the Ken Burns drift, and a CSS animation outranks a plain
   declaration for the same property — a :hover transform sharing that element
   would never apply at all. One element, one transform: nested, the two
   compose (drift x zoom) instead of one silently winning.

   The clip stays on the group above, so the photograph grows INSIDE a circle
   that does not move. Scaling the circle instead would push the photograph
   over the arcs it is threaded through and pull the collage apart.

   Outside the animation block on purpose: this one is a transition, not an
   animation, so it must still work for someone who has entrance animations
   turned off. It is muted under reduced motion below, not removed — a zoom
   with no easing is still useful feedback.

   hover: hover keeps it off touch screens, where :hover latches on after a
   tap and would leave a photograph stuck zoomed. */
.gkh-h-zoom {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform .55s cubic-bezier(.22,.61,.36,1);
}
@media (hover: hover) {
  .gkh-h-clip:hover .gkh-h-zoom { transform: scale(1.13); }
}
@media (prefers-reduced-motion: reduce) {
  .gkh-h-zoom { transition: none; }
  .gkh-h-clip:hover .gkh-h-zoom { transform: scale(1.05); }
}

/* Reduced motion and print both get the composition at rest — the arrival
   already ran or never needed to, and nothing perpetual is left going. The
   photographs sit at scale 1 inside their circles, which is the geometry the
   collage was drawn at. */
@media (prefers-reduced-motion: reduce) {
  .gkh-h-in, .gkh-h-photo, .gkh-h-spin, .gkh-h-sway-m, .gkh-h-sway-o,
  .gkh-h-breathe, .gkh-h-kb-a, .gkh-h-kb-b {
    animation: none !important;
    opacity: 1 !important;
  }
}
@media print {
  .gkh-h-in, .gkh-h-photo, .gkh-h-spin, .gkh-h-sway-m, .gkh-h-sway-o,
  .gkh-h-breathe, .gkh-h-kb-a, .gkh-h-kb-b {
    animation: none !important;
    opacity: 1 !important;
  }
}

/* ── One section per scroll ───────────────────────────────────────────────
   IPE, 3 Sep: "On the landing page we are expecting to see one section at one
   scroll."

   PROXIMITY, not mandatory, and this is the whole design decision. Measured on
   the current page: the "Shared Learning Platform" band is 1015px against a
   900px laptop viewport (1.41x on a 720px one), and on a phone EVERY section
   runs 1x to 3x the screen. Mandatory snapping on content taller than the
   viewport traps the reader — the browser drags them back to the section start
   and the bottom two thirds become unreachable. Proximity only settles a scroll
   that already ended near a boundary, so a tall section still scrolls freely
   while a flick lands cleanly on the next one.

   scroll-snap-stop: always is what actually delivers "one section at one
   scroll": a snap point may not be passed over in a single gesture, so a hard
   flick advances one section rather than three. It does that WITHOUT mandatory
   snapping's trapping, because proximity still leaves tall content free to
   scroll within itself.

   Gated on the viewport being big enough for a section to be worth snapping to
   at all. Measured at 1440x900: all five sections fit the 816px left under the
   header, so snapping lands one per screen. At 720px tall the journey section
   (685px) overflows slightly and proximity simply lets it scroll. On a phone
   nothing fits, so snapping is off entirely rather than fighting the reader.

   scroll-padding-top clears the sticky header, or a snapped section would
   settle with its heading tucked underneath it. */
@media (min-width: 900px) and (min-height: 700px) {
  html {
    scroll-snap-type: y proximity;
    scroll-padding-top: var(--gkh-header-sticky-offset);
  }
  /* section.gkh-reveal, not .gkh-reveal: the class is also carried by the
     PLFS band inside the About section so it can arm and pop on its own, and
     as a bare .gkh-reveal that turned it into a snap point halfway through a
     section -- scrolling then stopped on the figures instead of on the
     section. Snapping belongs to whole sections. */
  section.gkh-reveal, .gkh-snap {
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-snap-type: none; }
}

/* Print: no entrance animations, nothing offset, nothing transparent. The
   !important on opacity is the belt to the script's braces — if a print is
   triggered before the observer has run, the page still prints complete. */
@media print {
  .gkh-reveal, .gkh-reveal > *, .gkh-reveal .gkh-stat,
  .gkh-hero-in, .gkh-hero-art-in {
    animation: none !important; transition: none !important;
    transform: none !important; opacity: 1 !important;
  }
}

/* ── E5 · a distinct ground for "Browse the Hub" ──────────────────────────
   IPE, 3 Sep: "the full home page being in lavender makes it look like a long
   scroll instead of section jumps. Can we highlight the Browse the Hub section
   with a different background?" A full-bleed white band breaks the run of
   lavender and gives the component tiles a cleaner ground to sit on.

   BRAND.md 3.4 makes this more than a colour swap: ink is chosen by ground.
   #301934 on lavender, #476287 on white. The band therefore restates its own
   body ink rather than inheriting the canvas value. */
.gkh-band { background: var(--gkh-surface); width: 100%; }
.gkh-band .gkh-prose { color: var(--gkh-ink2); }

/* ── About · a ground of its own ──────────────────────────────────────────
   IPE, 3 Sep, twice. Flat canvas lavender: "this looks very plain with this
   background." Then pure white: "white background looks very odd."

   Both readings are right, and they bracket the answer. Lavender gave the one
   section on the page no surface of its own, so it read as a gap between the
   deep-purple hero above and the deep-purple journey band below. Pure white
   went the other way — nothing else on this page is white, so the section
   stopped belonging to it.

   So: a lavender-TINTED surface, a soft vertical wash from near-white down to
   a whisper of the canvas colour. It reads as a distinct ground beside the
   lavender sections without ever leaving the palette, and the gradient keeps
   it from being a flat slab. The faint magenta rising from the bottom centre
   sits behind the figures, where the icons already carry that accent.

   Ink follows the ground per BRAND.md 3.4. Measured at the DARKEST point of
   the wash, #F2F1FB, which is the end that has to pass: body and labels
   5.57:1, figures 11.44:1, icons and kicker 6.51:1, heading 14.27:1. */
.gkh-band-soft {
  background:
    radial-gradient(126% 74% at 50% 122%, rgba(170,0,97,.05) 0%, rgba(170,0,97,0) 64%),
    linear-gradient(180deg, #FBFBFE 0%, #F2F1FB 100%);
  width: 100%;
}
.gkh-band-soft .gkh-prose { color: var(--gkh-ink2); }

/* ── The journey section's deep ground and its rail ───────────────────────
   IPE, 3 Sep: "in future we are going to replace this with a whiteboard
   design which is going to be a more sort like a video ... so in the sense
   the white background will not look very eye pleasing."

   Exactly right, and for a sharper reason than taste: a whiteboard animation
   is itself white, so on a white band it has no edge — it dissolves into the
   page instead of reading as something you watch. The band was only ever
   there to break the run of lavender (E5), and a video does that by itself.

   The ground is the HERO's gradient, not a new colour: the page then bookends
   on the same deep purple, and a light animation sits on it like a lit stage.
   Ink is chosen by ground per BRAND.md 3.4 — on this one, white. */
.gkh-band-deep {
  background: linear-gradient(115deg, var(--gkh-p9) 0%, var(--gkh-p8) 100%);
  color: #F3EDF7;
  width: 100%;
}
.gkh-band-deep .gkh-prose { color: rgba(243,237,247,.82); }
/* The kicker is ACTION magenta on light grounds, which measures 1.76:1 against
   this one -- invisible. ORCHID is the brand's own lighter member of the same
   family and measures 4.96:1 at the gradient's lighter end, so it reads as the
   same accent without failing AA. Ink by ground, BRAND.md 3.4. */
.gkh-band-deep .gkh-kicker { color: var(--gkh-orchid); }

/* The stage. A hairline ring so the frame exists independently of whatever
   is inside it — today a photograph with its own edges, tomorrow a white
   animation with none. */
.gkh-stage {
  border-radius: 16px;
  box-shadow: 0 0 0 1px rgba(255,255,255,.14), 0 18px 40px -18px rgba(0,0,0,.55);
  overflow: hidden;
  width: 100%;
}

/* The rail. "Five Parts, One Connected Journey" was drawn as five separate
   boxes, which stated the opposite of the heading, and gave four fifths of
   the section's weight to things nobody can use yet. One line, five stops,
   the live one marked. */
.gkh-rail {
  display: flex;
  align-items: flex-start;
  width: 100%;
  margin-top: 34px;
}
.gkh-rail-item {
  flex: 1 1 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 9px;
  padding: 0 6px;
  min-width: 0;
}
/* The connector belongs to the item on its RIGHT and reaches back to the
   previous badge's centre, so it is drawn once per gap and never dangles off
   either end of the rail. */
.gkh-rail-item + .gkh-rail-item::before {
  content: "";
  position: absolute;
  top: 15px;
  left: -50%;
  right: 50%;
  height: 1px;
  background: rgba(255,255,255,.26);
}
.gkh-rail-badge {
  position: relative;   /* lifts the badge over the connector */
  z-index: 1;
  width: 30px; height: 30px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12.5px; font-weight: 700;
  border: 1px solid rgba(255,255,255,.34);
  background: var(--gkh-p9);
  color: rgba(255,255,255,.72);
  flex-shrink: 0;
}
/* Live. Olive, the same mark the "Available now" pill already uses, so the
   rail and the stage agree about which component is open. */
.gkh-rail-item.is-live .gkh-rail-badge {
  background: var(--gkh-olive);
  border-color: var(--gkh-olive);
  color: #1E2A08;
}
.gkh-rail-name { font-size: 13.5px; font-weight: 700; color: rgba(255,255,255,.72); line-height: 1.3; }
.gkh-rail-item.is-live .gkh-rail-name { color: #ffffff; }
/* Every alpha here is measured, not eyeballed. At .42 this label came out at
   3.16:1 -- below AA for 9.5px text -- and OLIVE itself is 4.44:1, which also
   fails. Both are raised to values that clear 4.5:1 against the LIGHTER end of
   the gradient, which is the harder of the two. */
.gkh-rail-state {
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255,255,255,.62);
}
.gkh-rail-item.is-live .gkh-rail-state { color: #B7D15F; }
a.gkh-rail-item { text-decoration: none; }
a.gkh-rail-item:hover .gkh-rail-name { color: #ffffff; }
a.gkh-rail-item:hover .gkh-rail-badge { transform: translateY(-2px); }
.gkh-rail-badge { transition: transform .18s ease; }
@media (prefers-reduced-motion: reduce) {
  .gkh-rail-badge { transition: none; }
}

/* On a phone the rail turns: a vertical line down the left, stops beside it.
   Five labels side by side at 375px would be four characters wide each. */
@media (max-width: 700px) {
  .gkh-rail { flex-direction: column; align-items: stretch; gap: 0; margin-top: 26px; }
  .gkh-rail-item {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: center;
    text-align: left;
    gap: 13px;
    padding: 9px 0;
  }
  .gkh-rail-item + .gkh-rail-item::before {
    top: -50%; bottom: 50%; left: 15px; right: auto;
    width: 1px; height: auto;
  }
  .gkh-rail-state { margin-left: auto; }
}

/* ── E7 · hover on the PLFS tiles and the resource cards ──────────────────
   Call of 3 Sep: "if these numbers can be made a little more interactive… like
   if I hover on it, can it maybe zoom out or something… it's very static", and
   then agreed to apply the same treatment to the Library cards "so it will be
   consistent across the landing page". One rule set, both surfaces. */
.gkh-hex {
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.gkh-hex:hover {
  transform: translateY(-3px) scale(1.03);
  box-shadow: 0 6px 20px rgba(48,25,52,.13);
  border-color: var(--gkh-action);
}
@media (prefers-reduced-motion: reduce) {
  .gkh-hex { transition: none; }
  .gkh-hex:hover { transform: none; }
}

/* ── PLFS indicators · a full-width band ──────────────────────────────────
   IPE, 3 Sep, twice: "the cards look very clutter", then "suggest much better,
   this is not looking very eye pleasing".

   Nothing here draws a box, a rule between items or a fill. The only thing
   holding the band together is alignment and air — which is what the reference
   IPE sent does, and why its numbers read as a headline rather than a table.

   The icons take currentColor, so the band sets the ink once rather than every
   glyph carrying its own; at 34px they are big enough to register beneath a
   50px figure without a chip to sit in.

   The hover raises the figure a little. E7 asked for these numbers to feel less
   static; there is no card left to lift, and no cursor change, because none of
   this is clickable. */
.gkh-stat-icon svg { width: 44px; height: 44px; }
/* Builder renders a text block as <div class="__text_block__">, never <p>, so
   a p:nth-of-type selector here matches nothing at all. The figure is the
   second child of the item: icon, figure, label. */
.gkh-stat > :nth-child(2) { transition: transform .2s ease; }
.gkh-stat:hover > :nth-child(2) { transform: translateY(-3px); }
@media (prefers-reduced-motion: reduce) {
  .gkh-stat > :nth-child(2) { transition: none; }
  .gkh-stat:hover > :nth-child(2) { transform: none; }
}

/* ── ISSUE-004 · the filter panel scrolls, and should look like it ────────
   The panel is capped to the viewport and scrolls inside itself. With eleven
   industries, six audience brackets and the year group it runs about 200px
   past a 900px window, so the last group was sliced mid-word with nothing to
   say more was there — it read as broken rather than as scrollable. A short
   fade along the bottom edge is the affordance; it disappears once you reach
   the end (background-attachment: local on the gradient pair). */
#gkh-filters-panel {
  background-image:
    linear-gradient(to top, var(--gkh-surface), var(--gkh-surface) 70%, rgba(255,255,255,0)),
    linear-gradient(to top, rgba(48,25,52,.10), rgba(48,25,52,0));
  background-position: bottom center, bottom center;
  background-size: 100% 26px, 100% 14px;
  background-repeat: no-repeat;
  background-attachment: local, scroll;
}

/* ── Library filter groups ────────────────────────────────────────────────
   B8, call of 3 Sep. Each facet section collapses, so a visitor can shorten a
   panel that had grown longer than the results it filters. Open by default --
   collapsing is a convenience, not a way to hide a filter from someone who has
   not thought to look for it. Checkbox hack, as everywhere else here: Builder
   has no scripting block. */
.gkh-fg-toggle { display: none; }
.gkh-fg-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  cursor: pointer; margin-bottom: 8px; color: var(--gkh-ink);
}
.gkh-fg-caret { display: flex; color: var(--gkh-ink3); transition: transform .16s ease; }
.gkh-fg-toggle:checked ~ .gkh-fg-head .gkh-fg-caret { transform: rotate(-90deg); }
.gkh-fg-toggle:checked ~ .gkh-fg-body { display: none; }

/* B5 · the industries held back behind "Other". IPE listed eleven priority
   industries and asked for the rest to move out of the way; this reveals them
   in place rather than on another screen. */
/* IPE, 7 Sep: "the tag colors to be synchronized - and some transition on
   hovering on it." The tags are links back into a filtered Library, so the
   hover has to read as "this is clickable": the tint deepens and the border
   takes the action colour. Ink stays put, so nothing shifts under the pointer. */
/* a.gkh-dtag, not .gkh-dtag: one class beats Builder's per-block .fb-xxxx at
   equal specificity only by document order, which is not something to rely on.
   The element selector puts this at (0,1,1) so it wins outright. */
a.gkh-dtag {
  background: #F6EFF7;
  border: 1.5px solid #CFC0D8;
  color: var(--gkh-p8);
  transition: background-color .18s ease, border-color .18s ease;
}
a.gkh-dtag:hover { background: #F0E4F2; border-color: var(--gkh-action); }
@media (prefers-reduced-motion: reduce) { a.gkh-dtag { transition: none; } }

/* ── How to cite · the copy button ────────────────────────────────────────
   The label swaps to "Copied" for a moment after a successful copy. Doing it
   with a class rather than by rewriting textContent means the button keeps its
   width and the row does not reflow under the pointer. */
.gkh-cite-copy { transition: background-color .18s ease, color .18s ease; }
.gkh-cite-copy:hover { background: var(--gkh-action) !important; color: #ffffff !important; }
.gkh-cite-copy.is-copied {
  background: var(--gkh-olive) !important; border-color: var(--gkh-olive) !important; color: #1E2A08 !important;
}
.gkh-cite-copy.is-copied .gkh-cite-label::after { content: "Copied"; }
.gkh-cite-copy.is-copied .gkh-cite-label { font-size: 0; }
.gkh-cite-copy.is-copied .gkh-cite-label::after { font-size: 13px; }
@media (prefers-reduced-motion: reduce) { .gkh-cite-copy { transition: none; } }

#gkh-ind-more { display: none; }
#gkh-ind-more-head {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  margin-top: 9px; font-size: 12.5px; font-weight: 700; color: var(--gkh-logo-plum);
}
#gkh-ind-more-head::after { content: "+"; font-size: 14px; line-height: 1; }
/* SINGLE backslash. This is a real stylesheet, so \2212 is the CSS escape for
   U+2212 MINUS SIGN and the browser reads it directly.
   The doubling this rule used to carry belonged to the old life of this file,
   when it was a Python f-string and "\2212" was eaten as the octal escape
   \221 + "2" -- which shipped "Other industries 2" to staging and was
   reported as "What do you mean by 2 in other industries?". Moving to a .css
   file removed the Python layer, and the doubling that had been the fix
   became the bug: "\\2212" is an escaped backslash, so the marker rendered
   as the literal text \2212. Caught in review, not by the suite: nothing
   asserted this marker at all, at either level. Both exist now -- the source
   is checked for a single backslash, and a browser check reads the rendered
   ::after.
   Escaped rather than written as a literal minus so the rule does not depend
   on the stylesheet's charset being honoured. */
#gkh-ind-more:checked ~ #gkh-ind-more-head::after { content: "\2212"; }
#gkh-ind-more-body { display: none; margin-top: 4px; }
#gkh-ind-more:checked ~ #gkh-ind-more-body { display: flex; }

/* B10 · clearing a filter reloads the page, which on a fast connection looks
   like nothing happened. A short fade on the results makes the reset legible
   as a reset rather than a frozen screen. */
@keyframes gkh-results-in { from { opacity: 0; transform: translateY(4px); }
                             to { opacity: 1; transform: none; } }
.gkh-results { animation: gkh-results-in .22s ease-out both; }
@media (prefers-reduced-motion: reduce) { .gkh-results { animation: none; } }

/* ── Search field: icon inside, no button ─────────────────────────────────
   IPE, 3 Sep: "Remove search button and add a search icon in the search field.
   The page should respond to whatever user searches in the field in real time."

   The icon is positioned against the FORM rather than the input, because an
   <input> is a void element and cannot hold a child. pointer-events: none so
   the magnifier never eats a click that was aimed at the field behind it.

   WebKit draws its own clear (×) button inside type="search"; left alone it
   collides with the spinner. Hidden, since Escape and simply deleting both
   already clear the field, and the results follow either way. */
.gkh-search-icon {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; color: var(--gkh-ink3); pointer-events: none;
}
.gkh-search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
.gkh-search input[type="search"]:focus {
  outline: none; border-color: var(--gkh-mag); box-shadow: 0 0 0 3px rgba(170,0,97,.13);
}
.gkh-search input[type="search"]:focus ~ .gkh-search-icon,
.gkh-search:focus-within .gkh-search-icon { color: var(--gkh-mag); }

/* In flight. Only ever visible on the .is-busy class the script sets, so with
   script off it is an empty span that occupies nothing. */
.gkh-search-spin {
  position: absolute; right: 11px; top: 50%; margin-top: -7px;
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid rgba(170,0,97,.22); border-top-color: var(--gkh-mag);
  opacity: 0; transition: opacity .15s ease;
}
.gkh-search.is-busy .gkh-search-spin { opacity: 1; animation: gkh-spin .6s linear infinite; }
@keyframes gkh-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .gkh-search.is-busy .gkh-search-spin { animation: none; }
}

/* The swap itself. Results dip slightly while a query is in flight so a
   same-length result set still reads as "that changed", and the region keeps
   its height so the page does not jump under the pointer mid-type. */
#gkh-results-region { transition: opacity .16s ease; }
#gkh-results-region.is-stale { opacity: .45; }

/* ── Library filter panel, mobile ─────────────────────────────────────────
   Reported by users: on a phone the panel covered the results and could not be
   dismissed. The cause was the panel keeping position:sticky once the grid
   collapsed to one column. Beside a results column that is right; stacked ABOVE
   one it pins to the viewport at up to calc(100vh - 104px) and the results
   scroll underneath it, unreachable.

   So on small screens the panel stops being sticky and becomes a disclosure,
   collapsed by default, with the count on the bar so applied filters are
   visible without opening it. Every facet is a LINK, and search submits a form,
   so applying anything navigates -- the checkbox resets and the panel closes on
   its own, which is the "does not close after searching" report. */
#gkh-filters-toggle { display: none; }
#gkh-filters-bar, #gkh-filters-done { display: none; }

@media only screen and (max-width: 576px) {
  #gkh-filters-bar {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; width: 100%; cursor: pointer; margin-bottom: 14px;
    background: #ffffff; border: 1px solid var(--gkh-line); border-radius: 9px;
    padding: 13px 15px; color: var(--gkh-ink);
  }
  #gkh-filters-done { display: block; }
  .gkh-filters-caret { display: flex; transition: transform .18s ease; }
  #gkh-filters-toggle:checked ~ #gkh-filters-bar .gkh-filters-caret { transform: rotate(180deg); }
  /* ID beats Builder's generated .fb-* class, so these override the block's own
     styles rather than needing !important. */
  #gkh-filters-panel {
    display: none; position: static; max-height: none; overflow: visible;
    margin-bottom: 18px;
  }
  #gkh-filters-toggle:checked ~ #gkh-filters-panel { display: block; }
}

/* ── Document thumbnail ───────────────────────────────────────────────────
   The sidebar preview is a link that opens the PDF in its own tab, so the
   browser's native viewer handles reading, download and print. It replaced a
   checkbox-hack full-screen overlay with its own control bar: IPE, 31 Aug --
   the browser's PDF toolbar already carries Download and Print, so the custom
   bar restated controls the reader already had. Removing it also retires a
   stacking bug, since the overlay lived inside the page's sticky right column
   and position:sticky always creates a stacking context. */
#gkh-pdf-open { display: block; cursor: pointer; position: relative; }
#gkh-pdf-open .gkh-pdf-cue {
  position: absolute; inset: auto 0 0 0; padding: 9px 12px; font-size: 12.5px;
  font-weight: 700; color: #ffffff; background: linear-gradient(0deg, rgba(30,16,34,.88), rgba(30,16,34,0));
  display: flex; align-items: center; gap: 7px;
}

/* ── Select chevron ───────────────────────────────────────────────────────
   Builder's reset.css sets appearance:none on <select> and supplies its own
   grey arrow. Any block that sets the `background` SHORTHAND resets
   background-image to none and wipes it, which is why the type and sort
   controls had no chevron at all. Those blocks now set background-COLOR, and
   the arrow is drawn here so it matches the wireframe: brand ink #301934,
   14px in from the right edge, in place of the browser's own. */
select {
  background-repeat: no-repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.6 6 6.4 11 1.6' fill='none' stroke='%23301934' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-position: right 14px center;
  background-size: 12px 8px;
  /* The right padding that keeps a long label off the chevron lives on each
     select's own block styles, not here: a Builder class rule outranks this
     element selector, so a padding shorthand there would silently win. */
}

/* ── Hover and focus states ───────────────────────────────────────────────
   A Builder block's styles compile to one flat class rule, so :hover cannot
   be expressed there at all — the build had zero hover rules while the
   wireframe has thirty. These carry the wireframe's states across, keyed on
   the literal classes set via blk(classes=[...]). */
.gkh-cta { transition: background-color .16s ease, color .16s ease, border-color .16s ease; }
.gkh-cta:hover { background-color: var(--gkh-action); color: #ffffff; border-color: var(--gkh-action); }

/* Button variants, mapped one-for-one onto the wireframe's .btn rules:
   .btn (solid brand) -> --hover  ·  .btn.lt (white on the dark hero) -> #F3E8F1
   .btn.gh (ghost on dark) -> white 14%  ·  .btn.out (brand outline) -> filled. */
.gkh-btn { transition: background-color .16s ease, color .16s ease, border-color .16s ease,
                       transform .16s ease, box-shadow .16s ease; cursor: pointer; }
.gkh-btn-solid:hover  { background-color: var(--gkh-p8); }
.gkh-btn-light:hover  { background-color: #F3E8F1; }
.gkh-btn-out:hover    { background-color: var(--gkh-action); color: #ffffff; }
.gkh-btn-ghost:hover  { background-color: rgba(255,255,255,.14); }

/* Spotlight carousel arrows — wireframe .sparrow:hover */
.gkh-arrow { transition: border-color .16s ease, color .16s ease; cursor: pointer; }
.gkh-arrow:hover { border-color: var(--gkh-action); color: var(--gkh-action); }

/* Filter facet rows — wireframe .fo:hover darkens the label only */
.gkh-facet { transition: color .14s ease; }
.gkh-facet:hover { color: var(--gkh-ink); }

/* Pagination — wireframe .pg picks up the action colour on hover */
.gkh-page { transition: border-color .16s ease, color .16s ease; }
.gkh-page:hover { border-color: var(--gkh-action); color: var(--gkh-action); }

/* Resource and spotlight cards — wireframe .card:hover */
.gkh-card-lift { transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; }
.gkh-card-lift:hover { border-color: var(--gkh-action); transform: translateY(-3px);
                       box-shadow: 0 10px 26px rgba(48,25,52,.14); }

/* The two select controls get the same border cue as every other control */
.gkh-select { transition: border-color .16s ease, box-shadow .16s ease; cursor: pointer; }
.gkh-select:hover { border-color: var(--gkh-action); }

/* Text inputs, so the search box behaves like its neighbouring button */
input[type="text"], input[type="search"], input[type="email"], input[type="password"] {
  transition: border-color .16s ease, box-shadow .16s ease;
}
input[type="text"]:hover, input[type="search"]:hover,
input[type="email"]:hover, input[type="password"]:hover { border-color: var(--gkh-action); }

.gkh-navlink { transition: background-color .16s ease, color .16s ease; }
.gkh-navlink:hover { background-color: var(--gkh-canvas); color: var(--gkh-ink); }

.gkh-card { transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; }
.gkh-card:hover { transform: translateY(-3px); box-shadow: 0 14px 34px rgba(48,25,52,.22); }

.gkh-chip { transition: background-color .16s ease, color .16s ease, border-color .16s ease; }
.gkh-chip:hover { border-color: var(--gkh-action); color: var(--gkh-action); }

footer a { transition: color .16s ease; }
footer a:hover { color: #ffffff; }

/* ── Justified body copy ──────────────────────────────────────────────────
   IPE, 29 Aug 2026: justify the body text across the build. Applied through a
   class rather than a bare `p` rule because Builder wraps EVERY text block in
   a <p> -- button labels, nav items, chips, counters and table values
   included -- and justifying those would stretch two-word labels across their
   container.

   hyphens/-webkit-hyphens are not decoration here: justified text without
   hyphenation opens "rivers" of white space in narrow columns, which is the
   usual reason justification looks wrong on the web. lang="en" is set on the
   document, so the browser can hyphenate.

   Justification is suppressed below 640px: at phone widths a measure is short
   enough that inter-word gaps become severe even with hyphenation. */
.gkh-prose {
  text-align: justify;
  text-justify: inter-word;
  hyphens: auto;
  -webkit-hyphens: auto;
}
@media only screen and (max-width: 640px) {
  .gkh-prose { text-align: left; hyphens: manual; }
}

/* Publisher logo. The mark is the whole affordance -- there is no arrow and no
   link text -- so it needs a hover cue of its own to read as clickable. */
.gkh-publogo { transition: opacity .16s ease, transform .16s ease; }
.gkh-publogo:hover { opacity: .78; transform: translateY(-1px); }

/* Inline links inside body copy (e.g. "Explore the Library" on the coming-soon
   pages) are text, not buttons, so they take an underline rather than a fill.
   Scoped to <p> so it cannot reach nav items, cards or buttons. */
/* Builder renders most copy as a text block rather than a real <p>, so the
   class is the reliable hook; the p rule stays for genuine paragraphs. */
p a, .gkh-textlink { transition: color .14s ease; }
p a:hover, .gkh-textlink:hover { text-decoration: underline; }

/* Keyboard users get the same affordance, per BRAND.md 3.4's focus ring. */
a:focus-visible, button:focus-visible, select:focus-visible, input:focus-visible {
  outline: 3px solid var(--gkh-violet); outline-offset: 2px;
}


/* Thin scrollbar for a panel that genuinely has to scroll.
   Windows draws a classic scrollbar -- always visible, chrome grey, about 15px
   wide and taking real layout width. macOS draws an overlay one that fades away
   and takes none. Identical markup, so a panel that looks clean on a Mac was
   reported from Windows on 10 Sep as having a scrollbar down the side of a card
   that read as a rendering fault.

   Applied to two elements, for two different reasons:

     Library filter panel  1209px tall, needs a 1313px window. It can never fit,
                           so it always scrolls and this rule is load-bearing.
     Resource page rail    549px, and pinned only above a 720px window where it
                           fits -- see .gkh-siderail. It should never scroll at
                           all; this is insurance for content that outgrows the
                           budget before test_siderail.mjs catches it.

   Only the standard properties, deliberately. ::-webkit-scrollbar can opt an
   element out of overlay scrollbars, which would put a permanent bar on Macs
   that currently have none -- fixing one platform by breaking the other.
   scrollbar-width and scrollbar-color restyle a bar the platform has already
   decided to draw, and do nothing where it has not. Chromium 121+ and Firefox.

   Note what this rule is NOT. It was the whole fix at first, and that answered
   the wrong question: IPE asked on 7 Sep to remove the scroll from the resource
   page's cards, not to make it prettier. Removing it needed the media query in
   .gkh-siderail. A thinner scrollbar is what is left for the panel that cannot
   avoid one. */
.gkh-scrollpane {
  scrollbar-width: thin;
  scrollbar-color: var(--gkh-line) transparent;
}

/* ── Masked type reveal ───────────────────────────────────────────────────
   Reference from IPE, 10 Sep: the Bence Linski portfolio template, whose two
   named features are "complex type animations" and a "smooth bg change". This
   is the first of those -- the move that gives that genre its character.

   Each word of a section heading sits in its own overflow-hidden mask and
   rises from behind it, a beat after the word before. Nothing else on the page
   moves this way, so the heading announces the section instead of the whole
   band sliding as one piece -- which is what IPE called "a flat scroll" on
   7 Sep, and what the per-section fade still reads as.

   Every rule here hangs off .gkh-armed, exactly like the section reveal it
   rides on. .gkh-armed is added by script only after the visitor has actually
   scrolled, and only to sections still below the fold, so a page that is never
   scrolled -- a screenshot, a print, a crawler -- never hides a heading. That
   is ISSUE-001's rule and it is not negotiable: a masked reveal is precisely
   the shape of animation that blanked three sections last time.

   The mask clips descenders (g, y, p) because overflow: hidden cuts at the box,
   so it is opened up by .14em and pulled back by the same amount. */
@media (prefers-reduced-motion: no-preference) {
  html.gkh-js .gkh-w {
    display: inline-block;
    overflow: hidden;
    vertical-align: bottom;
    padding-bottom: .14em;
    margin-bottom: -.14em;
  }
  html.gkh-js .gkh-w-i {
    display: inline-block;
    transition: transform .66s cubic-bezier(.22,.61,.36,1);
    transition-delay: var(--gkh-wd, 0ms);
  }
  /* Hidden ONLY once armed. See the note above. */
  html.gkh-js .gkh-reveal.gkh-armed .gkh-w-i { transform: translateY(108%); }
  html.gkh-js .gkh-reveal.gkh-armed.is-in .gkh-w-i { transform: none; }

  /* Images settle rather than arrive flat: a shade over-scaled on the way in,
     resolving as the section lands. Excluded from the hero collage, which runs
     its own ambient transforms and would fight this one. */
  html.gkh-js .gkh-reveal.gkh-armed img:not([class*="gkh-h-"]) {
    transform: scale(1.045);
    transition: transform .9s cubic-bezier(.22,.61,.36,1);
    transition-delay: var(--gkh-d, 0ms);
  }
  html.gkh-js .gkh-reveal.gkh-armed.is-in img:not([class*="gkh-h-"]) {
    transform: none;
  }
}

/* The masks must never survive into a static render or a reduced-motion one:
   without the transition the inner span would simply sit translated out of
   sight. Unwrapping to plain inline text is the safe resting state. */
@media (prefers-reduced-motion: reduce) {
  .gkh-w, .gkh-w-i { display: inline; overflow: visible; transform: none !important;
                     padding-bottom: 0; margin-bottom: 0; transition: none !important; }
}
@media print {
  /* transition: none as well as transform: none. Without it the reset ANIMATES
     over .66s, and a print snapshot taken while that is in flight catches the
     words part-way up -- measured at 6.8px, 8.8px, 11px on three of them. Print
     has to be an instant resting state, not a fast one. */
  .gkh-w, .gkh-w-i { display: inline; overflow: visible; transform: none !important;
                     padding-bottom: 0; margin-bottom: 0; transition: none !important; }
  html.gkh-js .gkh-reveal.gkh-armed img:not([class*="gkh-h-"]) {
    transform: none !important; transition: none !important;
  }
}

/* ── The resource page's right-hand rail ──────────────────────────────────
   Named siderail, not rail: .gkh-rail is already the journey component above,
   and its mobile rule would have restyled this panel at 640px.
   Sticky and internally scrollable was the wrong shape, and Windows users are
   the ones who noticed. IPE asked on 7 Sep to "remove the scroll option along
   the right-side cards"; the answer at the time was to keep the scroll but
   bound it, because dropping the bound outright put the Copy citation button
   off screen for the whole article. Both of those are true, and the way out of
   the contradiction is to stop pinning a panel that cannot fit.

   Above the breakpoint the rail fits inside the window, so it pins and never
   scrolls. Below it, it is an ordinary block that scrolls with the page: no
   inner scrollbar on any platform, and every card reachable. Nothing is pinned
   that has no room to pin.

   The breakpoint is measured, not chosen. Every rail in the catalogue is now
   549px -- uniformly, because the citation string was the only part whose
   height varied and it came out of the layout on 10 Sep. With the 84px offset
   and the 24px foot margin a rail fits any viewport of 657px or more, so 720
   pins it with 63px of headroom and still covers the case that was reported:
   a 1080p Windows laptop at 125% scaling, whose viewport is about 730px, gets
   a pinned rail with no scrollbar rather than the chunky one it had.

   tests/browser/test_siderail.mjs fails if any resource grows past the budget,
   which is the only way this can silently regress -- and it is how the first
   attempt at this number was caught: 800 looked reasonable and left four
   resources still scrolling.

   Below 720 the rail is an ordinary block. At 625px of viewport there is no
   room to pin a 549px panel anyway, and scrolling with the page beats a
   scrollbar inside a card. */
.gkh-siderail { align-self: start; }
@media (min-height: 720px) {
  .gkh-siderail {
    position: sticky;
    top: var(--gkh-header-sticky-offset);
    max-height: calc(100vh - var(--gkh-header-sticky-offset) - 24px);
    overflow-y: auto;
  }
}

/* Present to assistive tech and to script, absent from the page.
   The clip pattern rather than display:none on purpose: a display:none element
   returns "" from innerText, and the copy button reads innerText first. */
.gkh-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
