/* ============================================================
   Kwirkz.net - Marketing site
   Depends on tokens.css. Load tokens.css first.
   Sections are ordered the same way they appear in index.html.
   ============================================================ */

/* ---- base ---------------------------------------------------- */
*{box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{
  margin:0;
  font-family:var(--body);
  color:var(--color-on-surface);
  background:var(--color-surface-sunken);
  -webkit-font-smoothing:antialiased;
  line-height:var(--lh-body);
  font-size:var(--fs-base);
  /* Rule `horizontal-scroll`: nothing may push the page sideways on a
     phone. This is the backstop, not the fix. */
  overflow-x:hidden;
}
/* Type roles. See DESIGN.md, "Typography".
   Curse Casual (--display) is a casual script: large headlines and the
   wordmark ONLY. Nunito (--heading) carries every small title, label and
   button. DM Sans (--body) carries all running copy. Do not widen the
   script's reach - it is the single biggest readability risk on this site
   and this audience is the one that can least afford it. */
h1,h2{
  font-family:var(--display);
  font-weight:900;
  color:var(--navy);
  margin:0;
  line-height:1.08;
  /* balance, not pretty. Running copy uses text-wrap:pretty, which only
     protects the last line from a single orphaned word. A display
     heading is short enough that the browser can even the lines out
     properly, which is what balance does. Without it the mission page
     h1 broke as "Everyone has quirks." / "Everyone deserves a way" /
     "in.", leaving a two-character line in 50px script. Orphans like
     that are a comprehension problem before they are an aesthetic one,
     and this audience reads headings for structure. */
  text-wrap:balance;
}
h1{letter-spacing:-.025em;}
h2{letter-spacing:-.02em;}
h3{
  font-family:var(--heading);
  font-weight:800;
  letter-spacing:-.01em;
  color:var(--navy);
  margin:0;
  line-height:1.15;
}
p{margin:0;}
a{color:var(--navy);text-decoration:none;}
img{display:block;max-width:100%;}
button{font:inherit;cursor:pointer;}

/* Rule `tap-delay`: removes the 300ms double-tap wait on touch devices,
   so a tap on any control registers immediately rather than a third of a
   second later. */
a,button,label,input,summary{touch-action:manipulation;}

.wrap{
  max-width:var(--wrap);margin:0 auto;
  padding-left:var(--gutter-safe-l);padding-right:var(--gutter-safe-r);
}

/* Text present for screen readers but not painted. Carries the word
   "required" next to the asterisk on required fields. */
.sr-only{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
}

/* skip link, keyboard users land here first */
.skip{
  position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--navy);color:#fff;padding:12px 18px;
  border-radius:0 0 var(--r-field) 0;font-weight:700;
}
.skip:focus{left:0;}

:where(a,button,input,label,summary):focus-visible{
  outline:2px solid var(--navy);
  outline-offset:2px;
}

/* ---- buttons ------------------------------------------------- */
/* min-height 44px: the tap-target floor from DESIGN-TOKENS.md, and the
   40-44px minimum that recurs across the reference systems. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  min-height:var(--tap);
  border:1.5px solid transparent;border-radius:var(--r-pill);padding:13px 22px;
  font-family:var(--heading);font-weight:800;font-size:var(--fs-base);
  white-space:nowrap;cursor:pointer;
  /* nowrap keeps short labels from breaking before their arrow, but with
     nothing capping the width a long label simply pushes the page wider
     than the screen. "Report an accessibility problem" wanted 330px on a
     320px phone and took the whole document with it. The cap is the
     guard; the wrap below is what makes the cap survivable. */
  max-width:100%;
  transition:transform var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out);
}
.btn:active{transform:translateY(1px);}
.btn.primary{background:var(--color-primary);color:var(--color-on-primary);}
.btn.primary:hover{background:var(--color-primary-hover);}
/* These were --grass. White on #43B649 measured 2.62:1, well under the
   4.5:1 floor, and these are the two loudest calls to action on the
   site. --color-secondary is the same hue taken deep enough to read. */
.btn.grass{background:var(--color-secondary);color:var(--color-on-secondary);}
.btn.grass:hover{background:var(--color-secondary-hover);}
/* border-color was --color-border (#E7E8F0), which against the white fill
   measures 1.22:1. A white pill on a white surface with an invisible
   outline is not a button, it is bold text, and there is nothing to aim
   at. Rule `color-contrast` puts the floor for a UI boundary at 3:1.
   --ink-3 gives 5.00:1 on white and 4.79:1 on cream, so the shape reads
   on both surfaces this button appears on. */
.btn.ghost{background:var(--color-surface);color:var(--color-primary);border-color:var(--ink-3);}
.btn.ghost:hover{background:var(--navy-50);}
.btn.donate{
  background:var(--color-secondary);color:var(--color-on-secondary);
  border-color:var(--color-secondary);
}
.btn.donate:hover{background:var(--color-secondary-hover);border-color:var(--color-secondary-hover);}
.btn.block{width:100%;}
.btn.lg{padding:16px 26px;font-size:var(--fs-md);}
/* On a phone, a label that cannot fit on one line must be allowed to take
   two. Wrapping a button is far less damaging than clipping it or
   scrolling the whole page sideways, and balance keeps the two lines
   even rather than leaving one orphaned word under a long first line.
   min-height already lets the pill grow instead of squashing the text. */
@media(max-width:520px){
  .btn{white-space:normal;text-wrap:balance;}
}

/* Rule `disabled-states`: 0.38 to 0.5 opacity, a cursor change, and the
   control genuinely stops responding. */
.btn:disabled{opacity:.5;cursor:not-allowed;}
.btn:disabled:active{transform:none;}

/* ---- top nav ------------------------------------------------- */
header.nav{
  position:sticky;top:0;z-index:var(--z-sticky);
  /* The bar is the same liquid glass as everything else, just a denser
     grade of it, exactly like the waitlist card. It was the last surface
     still running its own frosted recipe (blur(10px), no saturate, no
     brightness), which made three materials on one page.

     The tint stays heavier than a copy pane at .86, and that is not
     inconsistency: a copy pane sits over a static canvas, while this bar
     has the whole page moving underneath it. It needs enough body to stay
     legible over a photograph mid-scroll. Same recipe, more of it. */
  background-color:rgba(var(--glass-tint,251,250,246),.86);
  -webkit-backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  border-bottom:1px solid var(--color-border);
  /* Rule `safe-area-awareness`: clears the notch and the status bar so
     the bar is not painted underneath the system chrome. */
  padding-top:env(safe-area-inset-top);
}
.nav-inner{
  display:flex;align-items:center;gap:16px;
  padding:16px var(--gutter-safe-r) 16px var(--gutter-safe-l);
  max-width:var(--wrap);margin:0 auto;
}
.nav .spacer{flex:1;}
.nav .socials{display:flex;align-items:center;gap:7px;}
/* Circular glass discs. The .glass class in the markup supplies the
   surface, the blur and the specular rim; this only sets the shape and
   the ink. Deliberately no background or box-shadow here, or it would
   out-specify .glass:hover and kill the lift. */
.nav .socials a{
  display:grid;place-items:center;
  width:var(--tap);height:var(--tap);border-radius:50%;
  color:var(--color-primary);
}
.nav .socials a:hover{color:var(--color-primary-hover);}
/* flex:none and nowrap: this is a flex item on a crowded row, so it was
   being allowed to shrink and break "501(c)(3) non-profit" across two
   lines, which made the bar taller than the tallest control in it. It
   holds one line and yields whole at 1100px instead (below). */
.nav .chip{
  display:inline-flex;align-items:center;gap:7px;flex:none;
  white-space:nowrap;
  background:var(--grass-50);color:var(--grass-text);
  border:1px solid var(--grass-border);border-radius:var(--r-pill);
  padding:5px 12px;font-weight:800;font-size:var(--fs-caption);
}
/* The 501(c)(3) mark is the strongest trust signal above the fold, and
   it used to vanish on phones, which is where most of this audience
   arrives. It stays now. Only the social icons fold away, and those
   repeat in the footer. */
@media(max-width:768px){
  .nav .socials{display:none;}
  .nav .chip{padding:5px 10px;}
}
/* The bar carries four controls that cannot shrink: .brand and .chip are
   flex:none, .nav-toggle has an explicit 44px min-width, and .btn.donate
   inherits white-space:nowrap from .btn so its min-content width is its
   floor. Nothing in the row yields, so below 431px the only give left is
   the gutter. The row reaches its tightest at 381px, where 24px gutters
   put the Donate button 1.3px past the viewport edge.
   431px, not 380px: at 24px gutters the row only regains full clearance
   at ~407px, so a relief band ending at 380 left 381-406 clipped or
   crowded, and 390px is the iPhone 12-16 width. Rule `horizontal-scroll`:
   the page must not scroll sideways at any width. */
@media(max-width:430px){
  .nav-inner{padding-left:14px;padding-right:14px;}
}
@media(max-width:380px){
  .nav-inner{gap:10px;}
  .nav .chip{gap:5px;padding:5px 9px;}
  .btn.donate{padding:11px 16px;}
}
/* 320px is the narrowest screen this site supports and the bar was still
   13px over there, pushing the Donate button past the edge. Four
   unshrinkable controls do not fit at that width on the tightening above
   alone, so the last few pixels come out of the gap and the button's own
   padding. Rule `horizontal-scroll`. */
@media(max-width:340px){
  .nav-inner{gap:7px;}
  /* The width came out of the type here, which took the chip to 11px and
     broke the 12px floor this site holds everywhere else. On a 320px
     phone that is the smallest text on the page, and it is a trust mark
     for an audience that includes low vision readers. Drop the second
     WORD instead: "501(c)(3)" carries the same signal as
     "501(c)(3) non-profit", costs 30px less, and stays readable. */
  .nav .chip{padding:5px 7px;font-size:var(--fs-caption);}
  .nav .chip .chip-long{display:none;}
  .btn.donate{padding:11px 12px;}
}

/* ---- nav: brand and page links -------------------------------- */
/* Added when the site grew from one page to four. The wordmark moves
   into the bar because interior pages have no hero logo to carry it,
   and it doubles as the route home. */
/* The wordmark IS the Home button. It used to be a bare logo sitting next
   to a separate "Home" pill, which put two adjacent controls with the same
   destination on the most crowded row of the site. Merged into one pill:
   it carries the glass surface like every other nav control, it holds the
   44px floor, and it takes aria-current="page" on the homepage.

   It deliberately lives OUTSIDE .nav-links, so it stays visible in the bar
   when the other links fold away behind the menu button. A phone that
   showed a hamburger and no logo would have nothing identifying the site. */
.nav .brand{
  display:inline-flex;align-items:center;justify-content:center;
  min-height:var(--tap);padding:0 15px;
  border-radius:var(--r-pill);flex:none;
}
/* Ratio matches the source file (420x246, same note as .hero-logo).
   Height-led here because the bar height is the fixed dimension.

   NO mix-blend-mode here, unlike .hero-logo and the footer wordmark.
   Two reasons: the file already carries an alpha channel, so multiply
   buys nothing; and blending a child against a stacking context formed
   by the parent's backdrop-filter made Chromium paint RGB stripes
   across the whole bar mid-scroll. Verified in a real browser
   2026-07-18. Do not add it back. */
/* No hardcoded aspect-ratio. The width and height attributes in the
   markup give the browser the right box to reserve per file, which
   also leaves room to swap the file on one page without squashing it. */
.nav .brand img{height:26px;width:auto;}

/* The home button stays the plain glass pill on EVERY page, homepage
   included. It used to take a solid navy fill on the homepage, which
   meant the one control that is identical on all five pages was the
   one control that looked different depending where you stood. Every
   page now carries the same navy wordmark on the same glass surface.
   aria-current="page" is still on the homepage brand in the markup, so
   a screen reader is still told it is the current page. It simply no
   longer changes how the pill is painted. */
@media(max-width:380px){.nav .brand{padding:0 11px;}}

/* ---- liquid glass -----------------------------------------------
   The shared surface for every control in the bar: the page-link
   pills, the social icons and the menu button. Four things make it
   read as glass rather than as a flat translucent box:

     1. a blur that also lifts saturation, so colour bleeds through
        from behind instead of going grey
     2. a top-to-bottom gradient, brighter at the top, which is where
        a curved surface would catch the light
     3. a 1px inset white highlight on the top edge and a faint dark
        inset on the bottom edge: the specular rim
     4. a soft drop shadow so the pill floats above the bar

   Legibility comes first. Navy text over this measures well past the
   4.5:1 floor because the gradient never drops below 42% white over
   an already-cream header. Two fallbacks below cover the browsers and
   the users that cannot take the blur. */
/* NOTE, and do not undo this without reading it.

   These pills deliberately do NOT carry their own backdrop-filter, even
   though that is the obvious way to build glass.

   header.nav already has backdrop-filter:blur(10px) AND position:sticky.
   Putting a second backdrop-filter on children inside it made Chromium
   smear the pills down the entire page while scrolling: the nav row
   tiled itself vertically over the content, roughly sixty copies, and
   only settled once scrolling stopped. Verified in a real browser on
   2026-07-18, not theorised.

   The nested filter was also nearly invisible, because a child's
   backdrop-filter samples the parent's already-blurred backdrop. It was
   paying a full compositing layer per pill for an effect the header had
   applied a moment earlier.

   So the refraction comes from the header, and these supply the other
   three things that make a surface read as glass: a top-lit gradient,
   a specular rim (the two insets), and a float shadow. */
.glass{
  position:relative;
  background:linear-gradient(180deg,rgba(255,255,255,.86),rgba(255,255,255,.58));
  /* A navy-tinted rim, not a white one. White-on-cream measured 1.03:1,
     which is to say the edge was not there: the pills read as floating
     text rather than as buttons. This composites to about 1.35:1, enough
     to see the capsule at a glance without turning it into a hard border.
     The white inset below is still the specular highlight. */
  border:1px solid rgba(28,19,98,.12);
  box-shadow:
    inset 0 1px 1px rgba(255,255,255,.9),
    inset 0 -1px 1px rgba(28,19,98,.06),
    0 4px 14px -8px rgba(20,22,43,.28);
  transition:background var(--dur) var(--ease-out),
             border-color var(--dur) var(--ease-out),
             box-shadow var(--dur) var(--ease-out),
             transform var(--dur-fast) var(--ease-out);
}
/* Hover moves a channel that is actually visible on a near-white surface.
   The previous version went from rgba(255,255,255,.86) to 1.0, a shift of
   roughly one value per channel: real in the stylesheet, invisible on the
   screen. This tints toward navy-50 instead. */
.glass:hover{
  background:linear-gradient(180deg,#F6F5FC,var(--navy-50));
  border-color:rgba(28,19,98,.22);
  box-shadow:
    inset 0 1px 1px rgba(255,255,255,1),
    inset 0 -1px 1px rgba(28,19,98,.08),
    0 8px 20px -8px rgba(20,22,43,.34);
}
.glass:active{transform:translateY(1px);}

/* Some people turn transparency off at the OS level precisely because
   layered translucency is hard to parse. Honour it: same shape, flat
   surface. Pairs with the prefers-reduced-motion block at the end. */
@media(prefers-reduced-transparency:reduce){
  .glass{
    background:var(--color-surface);
    border-color:var(--color-border);box-shadow:var(--shadow-sm);
  }
  .glass:hover{background:var(--navy-50);}
}

.nav-links{display:flex;align-items:center;gap:6px;}
.nav-links a{
  display:inline-flex;align-items:center;gap:8px;
  min-height:var(--tap);padding:0 13px;
  border-radius:var(--r-pill);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-sm);
  color:var(--color-primary);white-space:nowrap;
}
/* The icon is the thing the user asked to be able to aim at, so give
   it room and let it carry the grass accent while the label stays
   navy. aria-hidden on the svg: the label already names the target. */
.nav-links a svg{flex:none;color:var(--grass-text);}

/* DESIGN.md, "Color independence": no state may be signalled by hue
   alone. There is no underline here by request, so the current page is
   marked three other ways instead, none of which depend on seeing the
   difference between navy and green: the pill goes solid navy (a large
   luminance flip, not a hue shift), its icon inverts to white, and
   aria-current tells a screen reader outright. */
.nav-links a[aria-current="page"]{
  background:var(--color-primary);
  border-color:var(--color-primary);
  color:var(--color-on-primary);
  box-shadow:
    inset 0 1px 1px rgba(255,255,255,.22),
    0 6px 18px -8px rgba(28,19,98,.55);
}
.nav-links a[aria-current="page"] svg{color:var(--color-on-primary);}
.nav-links a[aria-current="page"]:hover{
  background:var(--color-primary-hover);
  box-shadow:
    inset 0 1px 1px rgba(255,255,255,.22),
    0 8px 22px -8px rgba(28,19,98,.6);
}

/* The menu button. Absent on desktop, 44px on touch. */
.nav-toggle{
  display:none;align-items:center;gap:8px;
  min-height:var(--tap);min-width:var(--tap);
  justify-content:center;padding:0 15px;
  color:var(--color-primary);
  border-radius:var(--r-pill);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-sm);
}
/* flex:none, matching .nav-links a svg. min-width:var(--tap) above
   replaces this button's automatic min-content floor, so it is the one
   item in the bar the browser is allowed to squeeze. Without this the
   icon is what gets squeezed: at 200% text-only zoom between 641 and
   678px the hamburger collapsed to 0px wide and the pill painted empty
   (WCAG 2.2 SC 1.4.4, Resize Text). The label is handled below. */
.nav-toggle .bars,
.nav-toggle .x{flex:none;}
.nav-toggle .x{display:none;}
.nav-toggle[aria-expanded="true"] .x{display:block;}
.nav-toggle[aria-expanded="true"] .bars{display:none;}

/* The chip is the first thing to yield horizontal space, because the
   four page links are actions and it is only a signal. It reappears in
   full below, it never disappears on a phone (see the note above). */
@media(max-width:1320px){.nav .chip{display:none;}}
/* Then the social icons, which repeat in the footer. */
@media(max-width:1200px){.nav .socials{display:none;}}

@media(max-width:1100px){
  .nav-toggle{display:inline-flex;}
  /* Both return once the links have folded away and freed the row. */
  .nav .chip{display:inline-flex;}
  .nav-links{
    display:none;
    position:absolute;top:100%;left:0;right:0;
    flex-direction:column;align-items:stretch;gap:7px;
    /* Opaque, not glass. Two reasons, in this order.

       Legibility: this panel covers running copy, and menu labels over
       a half-visible paragraph is exactly the kind of figure/ground
       problem this audience should not be handed.

       Rendering: it is a child of a sticky, already-backdrop-filtered
       header, and a second backdrop-filter in that position is what
       caused the scroll smearing documented in the .glass note above.
       The pills dropped their blur for the same reason. */
    background:var(--color-surface-sunken);
    border-bottom:1px solid var(--color-border);
    box-shadow:0 18px 40px -22px rgba(20,22,43,.5);
    padding:12px var(--gutter-safe-r) 18px var(--gutter-safe-l);
    /* A landscape phone is about 380px tall. Four links plus padding
       fits, but this is the guard if the nav ever grows. */
    max-height:calc(100vh - 100%);
    overflow-y:auto;
  }
  .nav-links.open{display:flex;}
  .nav-links a{padding:0 16px;font-size:var(--fs-md);}
}

/* The chip drops its second word for the whole band where the hamburger
   is showing, not just below 640px. Between 641 and 1024 the bar holds
   the brand, the menu button, the full chip and Donate, and none of them
   can shrink, so " non-profit" was the slack the row did not have: at
   200% text-only zoom it pushed the row 77px past the viewport and the
   browser took the difference out of the menu button, crushing it to
   44px and painting "Menu" outside its own pill (WCAG 2.2 SC 1.4.4).
   Hiding it to 1024px takes the row back to zero overflow across that
   whole band. "501(c)(3)" alone still carries the trust signal, and the
   full wording returns above 1180px where the chip reappears. */
@media(max-width:1024px){
  .nav .chip .chip-long{display:none;}
}
/* Four controls on a 390px row is one too many. The button drops its
   label rather than being removed, so it stays reachable. */
@media(max-width:640px){
  .nav-toggle{padding:0;}
  .nav-toggle .lbl{display:none;}
}
@media(max-width:380px){
  .nav .brand img{height:28px;}
}

/* ---- hero ---------------------------------------------------- */
.hero{position:relative;overflow:hidden;}
.hero::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:
    radial-gradient(60% 50% at 88% -8%, rgba(67,182,73,.20), transparent 60%),
    radial-gradient(50% 45% at -6% 4%, rgba(30,42,110,.12), transparent 60%);
}
/* padding-top carries the gap between the sticky bar and the hero
   wordmark. It was 30px, and because .hero-copy pulls itself up by
   -20px with its scrim margin, the logo was really clearing the bar by
   10px and reading as stuck to it. 56px here nets ~36px of real air. */
/* Two equal tracks and stretch, so the copy pane and the waitlist card
   are one pair of matching panels rather than two loosely related ones.
   They were 1.05fr/.95fr with align-items:start, and because the copy
   pane also broke 25px outward on each side while the card sat flush in
   its track, the rendered panes were 598px against 496px wide and 581px
   against 664px tall, with their top edges 21px out of line. Nothing
   about that read as deliberate. Equal tracks fix the width, stretch
   fixes the height, and the margin and padding overrides just below
   fix the rest. */
.hero-inner{
  position:relative;z-index:var(--z-raised);
  display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:stretch;
  padding:56px var(--gutter-safe-r) 60px var(--gutter-safe-l);
  max-width:var(--wrap);margin:0 auto;
}
/* The copy pane matches the card exactly: same padding, and no outward
   pull. The shared glass rule gives every pane a negative margin so a
   copy panel can grow past its column, which is right in the bands but
   wrong here, where the whole point is that these two agree. Flex so
   the copy sits centred in whatever height the taller pane sets, rather
   than stranded at the top of a half empty pane.

   Selector is .hero-inner > .hero-copy, not .hero-copy: the shared
   glass rule is declared further down this file, so a bare .hero-copy
   ties on specificity and loses on source order. */
.hero-inner > .hero-copy{
  display:flex;flex-direction:column;justify-content:center;
  padding:26px;
  margin:0;
}
/* Uses logo-wordmark-tight.png, not logo-wordmark.png.
   The original file is a 420x246 canvas holding only 355x137 of
   artwork: 22% dead space top and bottom, 7% on the left. That padding
   caused both faults here. Declaring the canvas ratio (420/246) made
   the logo look oversized because half its height was empty, and
   declaring 420/164 to compensate stretched the wordmark horizontally.
   The left padding is also what pushed the logo out of line with the
   headline beneath it.
   Trimming the asset solves all three: the ratio below is the artwork's
   own, nothing is stretched, and the image edge is the artwork edge so
   it sits flush with the copy. Width leads so the logo still scales
   down on narrow viewports instead of being squashed by the global
   img max-width rule (rule `image-dimension`, Core Web Vitals). */
.hero-logo{
  width:min(240px,72%);height:auto;aspect-ratio:355/137;
  margin-bottom:18px;mix-blend-mode:multiply;
}
.hero h1{font-size:clamp(32px,5vw,56px);margin:20px 0 0;}
.hero h1 .hl{position:relative;color:var(--color-primary);}
.hero .lede{
  font-size:var(--fs-lg);color:var(--color-on-surface-muted);margin-top:20px;
  max-width:48ch;text-wrap:pretty;font-family:var(--body);
}
/* align-self:flex-start earns its place. .hero-copy is a flex column now
   so its panes can match the card's height, and a flex child stretches
   on the cross axis by default. That turned this pill into a full width
   bar with the text stranded at the left end. This pins it back to its
   content width. It is inert anywhere the parent is not a flex row or
   column, so it costs nothing on the pages that do not need it. */
.eyebrow{
  display:inline-flex;align-items:center;gap:8px;
  align-self:flex-start;
  background:#fff;border:1px solid var(--line);color:var(--navy);
  border-radius:var(--r-pill);padding:7px 14px;
  font-weight:800;font-size:13px;box-shadow:var(--shadow-sm);
}
.eyebrow .dot{
  width:8px;height:8px;border-radius:50%;background:var(--grass);
  box-shadow:0 0 0 4px rgba(67,182,73,.18);
}
.hero .marks{display:flex;flex-wrap:wrap;gap:10px;margin-top:24px;}
.mark{
  display:inline-flex;align-items:center;gap:8px;
  background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--r-field);
  padding:9px 13px;font-size:var(--fs-label);font-weight:600;color:var(--color-on-surface-muted);
}
.mark svg{color:var(--grass-text);flex:none;}
@media(max-width:1024px){
  .hero-inner{
    /* minmax(0,1fr), not 1fr. A bare 1fr track has an automatic minimum
       of min-content, so any child that cannot shrink below some width
       widens the whole track and pushes the column past the viewport.
       The waitlist card does exactly that: its location row is a flex
       line whose min-content is 301px, and with the card padding and
       borders it demanded 355px inside a 327px track. The hero copy,
       sharing that track, was dragged out with it. The overflow was
       hidden only by body{overflow-x:hidden}, which this file already
       calls the backstop rather than the fix. This is the fix. */
    grid-template-columns:minmax(0,1fr);gap:34px;
    padding:44px var(--gutter-safe-r) 48px var(--gutter-safe-l);
  }
}
@media(max-width:520px){
  .hero-logo{width:min(220px,80%);}
  .hero .lede{font-size:var(--fs-md);}
  .hero .marks{gap:8px;}
  .mark{font-size:var(--fs-caption);padding:8px 11px;}
}
/* Rule `orientation-support`. A phone held sideways has roughly 380px of
   height. Without this the hero alone filled more than two screens and
   the waitlist form, which is the entire point of the page, sat below
   the fold twice over. */
@media(max-height:520px) and (orientation:landscape){
  .hero-inner{padding-top:16px;padding-bottom:28px;gap:22px;}
  .hero-logo{width:min(190px,64%);margin-bottom:10px;}
  .hero h1{font-size:clamp(26px,4vw,34px);margin-top:14px;}
}

/* ---- typewriter ---------------------------------------------- */
/* Paired with assets/js/typewriter.js, which splits a heading marked
   data-typewriter into characters. Every character is in the DOM from
   the first paint and merely hidden, so the heading holds its final
   size and the page never reflows while it types. */
.tw-word{display:inline-block;white-space:nowrap;}
.tw-char{position:relative;visibility:hidden;}
.tw-char.is-typed{visibility:visible;}
/* The caret rides whichever character was typed last. */
.tw-char.tw-caret::after{
  content:"";position:absolute;right:-.07em;bottom:.06em;
  width:.055em;height:.78em;background:currentColor;
  border-radius:1px;opacity:.75;
  animation:tw-blink 1.05s steps(1,end) infinite;
}
@keyframes tw-blink{0%,50%{opacity:.75;}50.01%,100%{opacity:0;}}
/* Each character is its own element, so selecting the typed copy would
   drag the split version to the clipboard. Hand over .tw-sr instead. */
.tw{user-select:none;-webkit-user-select:none;}
.tw-sr{user-select:text;-webkit-user-select:text;}
/* The unsplit sentence, for screen readers only. */
.tw-sr{
  position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
}
/* Belt and braces. The script already reveals everything up front in
   this case, so this only matters if the preference changed mid-visit. */
@media(prefers-reduced-motion:reduce){
  .tw-char{visibility:visible;}
  .tw-char.tw-caret::after{animation:none;opacity:0;}
}

/* ---- liquid glass scrims -------------------------------------- */
/* The neural canvas runs behind the whole page, so any text sitting
   directly on the cream competes with it. These panels give the words
   their own surface.

   This is Liquid Glass, not the frosted Glassmorphism this started as.
   The old panel was a 7px blur over flat cream, which is fog: it hid
   the constellation instead of revealing it, and it went grey because
   blurring drains saturation. The pane is now nearly clear. It holds
   its copy through tint and edge treatment rather than through blur:

     saturate(185%)    the navy and grass nodes behind the pane come
                       back to full strength instead of washing out
     brightness(1.04)  a lens gathers light rather than scattering it
     specular edges    lit rim along the top, navy pooling along the
                       bottom. This inset pair is what makes an edge
                       read as glass, and it is exactly what a frosted
                       panel does not have
     one sheen sweep   a single diagonal highlight, not a rainbow. The
                       skill's Liquid Glass entry offers iridescence and
                       chromatic aberration; both were declined. This is
                       a non-profit for special needs families, and a
                       shimmering multi-hue pane behind body copy is a
                       legibility cost this audience should not pay.

   Depth still resolves the way DESIGN.md asks: surface contrast and a
   hairline carry it, with only a whisper of lift shadow. The paper
   bands are already opaque and need none of this. */
/* .card is in this list on purpose. The waitlist card used to declare
   its own surface: a near opaque white (.97 against this rule's .80
   cream), a single sheen with no refraction wash, and the full
   --shadow-lg instead of the soft glass lift. Same blur, same edge
   token, same radius, but the two panes sit side by side in the hero
   and read as different materials, one glass and one solid card.
   One declaration now, so they cannot drift apart again. Anything that
   has to differ is overridden immediately below, and only geometry
   does: the card keeps its own padding and stays inside its grid
   track instead of breaking outward. */
.hero-copy,
.band-copy,
.card,
.safety .item{
  position:relative;
  /* Layer order matters. Sheen sits above the refraction wash, which
     sits above the tint that actually holds the contrast. */
  background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha,.80));
  background-image:
    linear-gradient(122deg,
      rgba(255,255,255,.50) 0%,
      rgba(255,255,255,.10) 34%,
      rgba(255,255,255,0)   58%),
    linear-gradient(122deg,
      rgba(28,19,98,.045) 0%,
      rgba(67,182,73,.055) 100%);
  -webkit-backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  border:1px solid var(--glass-edge,rgba(255,255,255,.62));
  border-radius:var(--r-lg);
  box-shadow:
    inset 0 1px 0 var(--glass-specular,rgba(255,255,255,.92)),
    inset 0 -1px 0 var(--glass-refract,rgba(28,19,98,.07)),
    var(--glass-lift,0 10px 34px -20px rgba(28,19,98,.30));
  /* box-shadow ONLY. background-color is deliberately NOT transitioned.
     Chrome latched a CSSTransition on the tint that stayed in playState
     "running" with fill "backwards", which pins the computed value at
     the pre-transition transparent and never resolves. The pane then
     renders with no tint at all and body copy drops onto the raw
     constellation at roughly 1.3:1. The tint is the layer carrying the
     contrast floor, so it must never be in an interpolated state. Motion
     lives on the lit edge, which carries nothing but light. */
  transition:box-shadow var(--glass-morph,520ms) var(--ease-out,ease);
  /* The negative margin cancels the padding, so a panel grows outward
     instead of squeezing its column. In the hero that keeps the three
     trust marks on one line. The 1px border is inside the same budget,
     hence 19px/23px against a 20px/24px pull. */
  padding:20px 24px;
  margin:-21px -25px;
}
/* Only as wide as the words. A panel stretched to the full section
   would blur the whole band and hide the network, which is the point
   of having it. Headings already cap at 20ch, blurbs at 62ch. */
.band-copy{
  display:inline-block;
  max-width:min(100%,68ch);
}
/* These two are grid items with their own track widths, so they must
   stay block or they fall out of the mission layout. */
/* These two are grid items with their own track widths, so they must not
   inherit .band-copy's inline-block or its 68ch cap or they fall out of the
   mission layout.

   They are split because they want different display values. The head is a
   plain block. The body must stay a centred flex column: .mission-body sets
   justify-content:center a few lines above, and folding it into this rule as
   display:block silently killed that, which left the pull-quote pinned to the
   top of a container far taller than the text. Two classes beat one, so the
   override won and the centring never applied. */
.mission-head.band-copy{display:block;max-width:none;}
.mission-body.band-copy{
  display:flex;flex-direction:column;justify-content:center;max-width:none;
}
@media(max-width:1024px){
  /* One column here, and the sections already carry a gutter. Breaking
     out of it would run the panels off the screen edge. */
  .hero-copy,.band-copy{margin:0;padding:18px;}
}
/* Safari on older iOS lacks backdrop-filter. With no pane to see
   through, glass has nothing to do, so the panel falls back to a near
   solid surface. The sheen is kept because it is a plain gradient and
   still reads as a lit edge, but the saturation and refraction go,
   since without a backdrop they would only tint the cream. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .hero-copy,.band-copy,.card,.safety .item{
    background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha-solid,.93));
    background-image:
      linear-gradient(122deg,
        rgba(255,255,255,.55) 0%,
        rgba(255,255,255,0) 52%);
  }
}

/* Rule `parallax-subtle` and `reduced-motion`. The sheen is static, so
   there is nothing to stop, but the 520ms material settle is motion and
   goes with everything else under the global reduce block below. The
   pane itself stays, because it is carrying contrast, not decoration.
   Removing it would make the copy LESS readable, which is the opposite
   of what the preference asks for. */

/* ---- page background canvas ---------------------------------- */
/* Fixed behind the entire page, driven by assets/js/constellation.js.
   Negative z-index puts it above the body background but under all
   content, so it shows through the transparent cream bands and is
   covered by the opaque .band.paper sections. The script sizes a
   fixed canvas to the viewport rather than to its parent. */
.page-canvas{
  position:fixed;inset:0;z-index:-1;
  width:100%;height:100%;
  pointer-events:none;
}

/* ---- hero background canvas ---------------------------------- */
/* Shared shell for whichever background is switched on. Currently
   assets/js/constellation.js. bubbles.js and flicker-grid.js drive
   the same element and are swapped in by changing the data attribute
   and the script tag. Sits under .hero-inner, which carries
   z-index 1. */
.hero-canvas{
  position:absolute;inset:0;z-index:0;
  width:100%;height:100%;
  pointer-events:none;
  /* Fade the background out before it reaches running copy. DESIGN.md
     carries depth with surface contrast and hairlines rather than
     pattern, and drifting shapes behind 18px body text are a
     readability cost this audience should not be asked to pay. The
     texture stays in the upper band, where only display type and the
     wordmark sit over it. */
  -webkit-mask-image:linear-gradient(to bottom,#000 0%,#000 20%,rgba(0,0,0,.6) 31%,transparent 41%);
  mask-image:linear-gradient(to bottom,#000 0%,#000 20%,rgba(0,0,0,.6) 31%,transparent 41%);
}

/* ---- form card ----------------------------------------------- */
/* The waitlist card is the same glass as the panes around it, but the
   densest grade of it. It is deliberately NOT as transparent as a copy
   scrim, for two reasons.

   It is the conversion surface, and a form that a family is typing a
   child's details into should feel like a solid object, not a floating
   sheet. And it carries the only inputs on the page: placeholder text,
   helper text and field borders are all near the contrast floor already,
   so lifting the backdrop even slightly would eat the margin the form
   needs. At .97 a constellation node behind the card shifts the surface
   by under one step of 255, so every measured field pairing is unchanged
   from solid white while the pane still catches light at its edges.

   What makes it read as the same material is the edge, not the body: the
   same lit top rim, the same refractive bottom, the same saturating
   backdrop. Cohesion here comes from how the surface meets the light,
   which is exactly how real glass of different thicknesses relates. */
/* Surface comes from the shared glass rule above. Only geometry here.
   margin:0 cancels that rule's outward pull, which exists so a copy
   panel can grow past its column; the card is a grid item and must
   stay in its track. Padding stays at 26px because this pane holds a
   form rather than running copy, and inputs need the extra room. */
.card{
  padding:26px;
  margin:0;
  /* Solid white, by request. The card keeps everything else from the
     shared glass rule above (border, radius, specular insets, lift), so
     it still reads as the same family as the hero copy pane and still
     matches it dimension for dimension. Only the fill changes.

     Two deliberate consequences. The tint is opaque, so the drifting
     constellation no longer shows through behind the form, which is the
     point: this pane holds the inputs. And the navy/green refraction
     wash from the shared rule is dropped here, because layering a
     coloured gradient over white gives you a faintly tinted white
     rather than white. Only the neutral sheen is kept, so the top left
     corner still catches the light like every other glass surface. */
  background-color:#fff;
  background-image:
    linear-gradient(122deg,
      rgba(255,255,255,.60) 0%,
      rgba(255,255,255,.14) 32%,
      rgba(255,255,255,0)   56%);
}
.card h2{font-size:23px;}
.card .sub{
  color:var(--color-on-surface-faint);font-size:var(--fs-sm);
  margin-top:6px;font-family:var(--body);
}
.form{display:flex;flex-direction:column;gap:13px;margin-top:16px;}
.field{display:flex;flex-direction:column;gap:7px;}
.field[hidden]{display:none;}
/* Name and email on one line. minmax(0,1fr) rather than 1fr: a grid
   track sized 1fr will not shrink below its content's min-width, and an
   .input carries one, so plain 1fr let the pair overflow the card.
   Single column under 560px, where half a card is too narrow to type a
   full name or an email address into. */
.field-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px;}
@media(max-width:560px){.field-row{grid-template-columns:1fr;}}
.field label,.field legend{font-size:var(--fs-label);font-weight:700;color:var(--color-on-surface-muted);}
.field label .opt,.field legend .opt{color:var(--color-on-surface-faint);font-weight:600;}
/* Rule `field-grouping`: the role picker and the interest chips are real
   fieldsets now, so their group heading is announced once instead of
   being an orphan <label> pointing at nothing. Browser default border,
   margin and padding are stripped so nothing moves visually. */
fieldset.field{border:0;margin:0;padding:0;min-width:0;}
fieldset.field > legend{padding:0;float:left;width:100%;}
fieldset.field > legend + *{clear:both;}
/* Rule `required-indicators`. The asterisk is paired with visually
   hidden text so the requirement is not carried by a red glyph alone. */
.req{color:var(--color-error);font-weight:800;margin-left:2px;}
/* The resting border on every unselected control used to be
   --color-border, which resolves to the decorative hairline --line at
   #E7E8F0. On the white card that measures 1.22:1, and the --field fill
   sitting inside it only reaches 1.08:1, so a text field had no edge a
   sighted user could actually find. A boundary that carries meaning has
   to clear 3:1. --ink-3 is the nearest token in the system that does, at
   5.00:1 on white, 4.79:1 on cream and 4.63:1 on --field. The design
   already proved the intent on the other states: a focused .input reads
   15.92:1 against the card and a checked .seg label reads 13.52:1 against
   its own fill, so only the resting state was invisible. A dedicated
   --color-control-border would be the better home for this, see the note
   in the report that came with this change. */
.input{
  border:1.5px solid var(--ink-3);background:var(--field);
  border-radius:var(--r-field);padding:13px 15px;
  font:inherit;
  /* Rule `readable-font-size`. This was 15.5px. Anything under 16px
     makes iOS Safari zoom the entire page the moment the field takes
     focus, and the visitor is then stranded at 1.3x with the rest of
     the form off-screen and no obvious way back. */
  font-size:var(--fs-base);
  color:var(--color-on-surface);outline:none;
  min-height:var(--tap);
  transition:border-color var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out);
}
.input::placeholder{color:var(--color-on-surface-faint);}
.input:focus{border-color:var(--color-ring);background:var(--color-surface);}
.input.bad,.input[aria-invalid="true"]{
  border-color:var(--color-error-border);background:var(--color-surface);
}
.err{
  color:var(--color-error);font-size:var(--fs-caption);font-weight:600;
  display:none;align-items:center;gap:6px;line-height:1.35;
}
.err.show{display:flex;}
/* Rule `color-not-only`. The error is red AND carries a glyph AND says
   what to do about it, so none of the meaning rests on colour vision. */
.err::before{
  content:"";flex:none;width:14px;height:14px;background:currentColor;
  -webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v6'/%3E%3Cpath d='M12 16.4v.01'/%3E%3C/svg%3E") center/contain no-repeat;
  mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v6'/%3E%3Cpath d='M12 16.4v.01'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* Submit-level failure. Rule `error-recovery` wants the cause and the
   way out stated in the page. This replaces a browser alert(), which
   blocked the tab, was announced out of context by screen readers, and
   gave the visitor nothing to act on but an OK button. */
.form-error{
  display:none;align-items:flex-start;gap:9px;
  background:#FDF1EE;border:1px solid var(--color-error-border);
  color:var(--color-error);border-radius:var(--r-field);
  padding:11px 13px;font-size:var(--fs-label);font-weight:600;line-height:1.45;
}
.form-error.show{display:flex;}
.form-error svg{flex:none;margin-top:2px;}

/* role selector */
.seg{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.seg.stack{grid-template-columns:1fr;}
.seg input{position:absolute;opacity:0;pointer-events:none;}
.seg label{
  display:flex;flex-direction:column;justify-content:center;gap:2px;
  min-height:var(--tap);
  /* Same 1.22:1 resting border as .input carried, and the same fix. The
     unchecked option needs a visible edge before the checked one can mean
     anything by contrast. */
  border:1.5px solid var(--ink-3);background:var(--field);
  border-radius:var(--r-field);padding:11px 13px;cursor:pointer;
  transition:border-color var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out),
             box-shadow var(--dur-fast) var(--ease-out);
}
.seg label .t{font-family:var(--heading);font-weight:800;font-size:var(--fs-sm);color:var(--color-primary);}
/* --ink-2, not --ink-3. tokens.css certifies --ink-3 on white, cream and
   --field, and on --field this cell does clear it at 4.63:1. But the
   SELECTED card swaps its surface to --navy-50, which is darker than all
   three certified backgrounds, and there --ink-3 falls to 4.24:1 at 12px.
   The parent/guardian option is checked on first paint, so that was the
   failing state a visitor met before touching anything. One value for
   both states rather than a state-specific override, because a
   description that changes weight when you select it reads as a glitch. */
.seg label .d{font-size:var(--fs-caption);color:var(--color-on-surface-muted);line-height:1.35;}
.seg input:checked + label{
  border-color:var(--color-ring);background:var(--navy-50);
  box-shadow:0 0 0 3px rgba(30,42,110,.10);
}
.seg input:focus-visible + label{outline:2px solid var(--color-ring);outline-offset:2px;}
/* Two descriptions side by side inside a 327px card was the tightest
   spot on the whole page. Below 420px the two options stack instead. */
@media(max-width:420px){
  .seg{grid-template-columns:1fr;}
}
.seg.stack label{position:relative;padding:12px 42px 12px 14px;}
.seg.stack label::after{
  content:"";position:absolute;right:14px;top:50%;transform:translateY(-50%);
  width:20px;height:20px;border-radius:50%;border:2px solid var(--color-border);
  transition:border-color var(--dur-fast) var(--ease-out),background var(--dur-fast) var(--ease-out);
}
.seg.stack input:checked + label::after{
  border-color:var(--color-ring);
  background:var(--color-primary) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 12 5 5L20 6'/%3E%3C/svg%3E") center/12px no-repeat;
}

/* interest chips */
.chips{display:flex;flex-wrap:wrap;gap:8px;}
.chips input{position:absolute;opacity:0;pointer-events:none;}
.chips label{
  display:inline-flex;align-items:center;min-height:var(--tap);
  /* An unchecked chip sits on the white card, so its old --color-border
     edge was the worst case of the set at 1.22:1 with no fill behind it
     to help. --ink-3 gives the pill a shape at 5.00:1. */
  border:1.5px solid var(--ink-3);background:var(--color-surface);border-radius:var(--r-pill);
  padding:8px 16px;font-size:var(--fs-label);font-weight:700;color:var(--color-on-surface-muted);
  cursor:pointer;
  transition:border-color var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out),
             color var(--dur-fast) var(--ease-out);
}
/* Selection used to be signalled by a --grass border at 2.51:1 against
   white, below even the 3:1 floor for a UI boundary. */
.chips input:checked + label{
  background:var(--grass-50);border-color:var(--grass-text);color:var(--grass-text);
}
.chips input:focus-visible + label{outline:2px solid var(--grass-text);outline-offset:2px;}

/* location */
/* wrap, so the Detect button drops under the field rather than forcing
   the card wider than the screen. flex-wrap alone is not enough: a flex
   item's default min-width is auto, so the field would still refuse to
   shrink. The min-width:0 on .loc-wrap .input below is what lets it. */
.loc-wrap{display:flex;gap:8px;flex-wrap:wrap;}
.loc-wrap .input{flex:1 1 190px;}
.loc-wrap .loc-btn{flex:0 0 auto;}
.loc-wrap .input{flex:1;min-width:0;}
.loc-btn{
  display:inline-flex;align-items:center;justify-content:center;gap:6px;
  /* Had no height of its own and only reached 44px by stretching next to
     the input. It now holds the floor on its own terms. */
  min-height:var(--tap);
  /* This reads as a button, so its outline is the only thing telling a
     visitor it can be pressed. At 1.22:1 it was not telling anyone. */
  border:1.5px solid var(--ink-3);background:var(--color-surface);color:var(--color-primary);
  border-radius:var(--r-field);padding:0 14px;
  font-family:var(--heading);font-weight:800;font-size:var(--fs-label);
  white-space:nowrap;
  transition:background var(--dur-fast) var(--ease-out),border-color var(--dur-fast) var(--ease-out);
}
/* The hover border was a hardcoded #C9CEEA, which reads 1.32:1 against the
   --navy-50 fill it appears on. Now that the resting border clears the 3:1
   floor, leaving this in place would make the outline disappear at the
   exact moment the visitor reaches for the button. The border holds still
   and the fill alone carries the hover signal. */
.loc-btn:hover{background:var(--navy-50);}
.loc-btn:disabled{opacity:.5;cursor:not-allowed;}

/* success state */
.success{display:none;flex-direction:column;align-items:center;text-align:center;padding:18px 8px 8px;}
.success.show{display:flex;}
/* Focus is moved here on success so a screen reader announces the
   confirmation instead of leaving the user on a submit button that has
   just disappeared (rule `focus-on-route-change`). The ring is
   suppressed because this is a programmatic landing, not a tab stop. */
.success:focus{outline:none;}
.success .ring{
  width:84px;height:84px;border-radius:50%;
  /* White tick on --grass was 2.62:1, under the 3:1 floor for a
     graphic that carries meaning. */
  background:var(--color-secondary);
  display:grid;place-items:center;color:var(--color-on-secondary);
  box-shadow:0 14px 36px -10px rgba(48,130,52,.6);
  animation:pop var(--dur-slow) cubic-bezier(.2,.9,.3,1.4);
}
@keyframes pop{from{transform:scale(.5);opacity:0;}to{transform:scale(1);opacity:1;}}
.success h2{margin-top:18px;font-size:var(--fs-2xl);}
.success p{color:var(--color-on-surface-muted);margin-top:8px;font-size:var(--fs-base);max-width:34ch;}

/* ---- section shell ------------------------------------------- */
section.band{padding:72px 0;}
section.band.paper{
  background:var(--paper);
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
}
/* At 16px and weight 800 this is normal text under WCAG, not large text.
   Large starts at 24px, or 18.66px when bold, so the floor here is 4.5:1
   and not 3:1. Three of the five kickers sit inside a .band-copy panel,
   whose scrim is cream at 74% over the live constellation. When a navy
   node passes behind one the backdrop composites to rgb(210,208,219), and
   --grass-text measured only 3.31:1 against it. It also missed on the
   no-backdrop-filter fallback at 4.39:1. The token comment in tokens.css
   was tuned to flat cream, where --grass-text does pass at 4.82:1, so the
   failure only shows up once the canvas is behind it. This green keeps the
   brand hue and clears every case: 4.55:1 on the worst-case scrim, 6.03:1
   on the .93 fallback, 6.63:1 on flat cream and 6.92:1 on the paper bands.
   Thickening the scrim instead would have meant taking it to 95%, which
   hides the network the panel exists to show. This wants to become a token,
   see the note in the report. */
.kicker{
  font-family:var(--heading);font-weight:800;font-size:16px;
  color:var(--grass-kicker,#216726);letter-spacing:.05em;text-transform:uppercase;
}
/* An icon in front of every section label, one per section, so a reader
   scanning a dense page gets a distinct mark per header rather than
   fourteen identical green lines. This replaced a plain dash rule, which
   anchored the header but told you nothing about which section you were
   looking at.

   The icon lives in the markup, not here, because it differs per
   section. This rule only sizes and aligns it. currentColor means every
   icon is exactly the kicker's own green and no new colour enters the
   system.

   vertical-align rather than a flex parent: one kicker carries an inline
   style="display:block", which would beat any display set here and break
   that section alone. This alignment works whichever way the parent
   computes. */
.kicker svg{
  width:19px;height:19px;
  vertical-align:middle;
  margin-right:9px;
  position:relative;top:-1px;
  flex:none;
}
/* 16px, not 10px. The heading needed to sit a little further from its
   label for the pair to read as a unit with a hierarchy rather than two
   stacked lines. */
.band h2{font-size:clamp(26px,4vw,40px);margin-top:16px;max-width:20ch;}
.band .blurb{
  font-size:var(--fs-md);color:var(--color-on-surface-muted);margin-top:14px;max-width:62ch;
  text-wrap:pretty;font-family:var(--body);
}
.coming-soon{
  display:inline-flex;align-items:center;gap:7px;
  background:var(--color-primary);color:var(--color-on-primary);
  border-radius:var(--r-pill);padding:7px 16px;
  font-family:var(--heading);font-weight:900;font-size:var(--fs-sm);letter-spacing:.01em;
  box-shadow:0 8px 20px -8px rgba(28,19,98,.5);
}
@media(max-width:768px){
  section.band{padding:52px 0;}
}
.coming-soon::before{
  content:"";width:9px;height:9px;border-radius:50%;background:var(--arrow-neon);
  box-shadow:0 0 0 4px rgba(57,231,3,.22);
}

/* ---- pillars ------------------------------------------------- */
.pillars{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-top:34px;}
.pillar{background:var(--cream);border:1px solid var(--line);border-radius:var(--r-lg);padding:20px 16px;}
.pillar .ic{
  width:48px;height:48px;border-radius:14px;display:grid;place-items:center;
  background:var(--grass-50);color:var(--grass-text);
}
.pillar.p2 .ic{background:var(--navy-50);color:var(--navy);}
.pillar.p3 .ic{background:var(--tint-amber-bg);color:var(--tint-amber-fg);}
.pillar.p4 .ic{background:var(--tint-gold-bg);color:var(--tint-gold-fg);}
.pillar.p5 .ic{background:var(--tint-violet-bg);color:var(--tint-violet-fg);}
.pillar h3{font-size:var(--fs-md);margin-top:14px;}
.pillar p{font-size:var(--fs-sm);color:var(--color-on-surface-muted);margin-top:6px;margin-bottom:14px;}
/* Status badge on each pillar.

   Louder than the first pass, which was two pale pills that read as
   footnotes. The live one is now a SOLID grass fill with white text and
   its own coloured shadow, so it carries real weight against the cream
   card; the pending ones are quiet outlines that step back and let it
   win. One badge should jump out of the row of five, and it does.

   Contrast is the constraint, not the styling. White on --grass-ink
   (#308234) is 4.81:1, which is why the fill uses grass-ink rather than
   the brand --grass: white on #43B649 is 2.62:1 and fails. See the note
   in tokens.css. The pending badge takes --ink-2 rather than --ink-3,
   darker than before, because an outline pill gives text less help than
   a filled one.

   The two states still differ by shape as well as colour, per the
   color-independence rule in DESIGN.md: filled dot with a halo versus a
   hollow ring. Greyscale the page and they remain distinguishable.

   Uppercased here, unlike the earlier version. Two short words at 13px
   and weight 800 with tracking is the same treatment .kicker already
   uses on this site, and on a badge this length the word-shape cost is
   small. Do not extend it to anything sentence-length.

   The pillar is a flex column and the badge takes margin-top:auto, so
   all five sit on one line however long each description runs. */
.pillar{display:flex;flex-direction:column;}
.pillar-tag{
  display:inline-flex;align-items:center;gap:8px;
  align-self:flex-start;margin-top:auto;white-space:nowrap;
  padding:7px 15px 7px 12px;border-radius:var(--r-pill);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-label);
  text-transform:uppercase;letter-spacing:.06em;
  background:var(--color-surface);
  border:1.5px solid #D6D9E8;
  color:var(--color-on-surface-muted);
}
/* Hollow ring: this one has not started. */
.pillar-tag::before{
  content:"";flex:none;
  width:9px;height:9px;border-radius:50%;
  background:transparent;
  border:2px solid var(--ink-3);
  opacity:.6;
}
/* Live. Solid fill, white type, grass-tinted lift. */
.pillar-tag.is-now{
  background:var(--grass-ink);
  border-color:var(--grass-ink);
  color:var(--color-on-secondary);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.25),
    0 4px 14px -4px rgba(48,130,52,.55);
}
.pillar-tag.is-now::before{
  background:#fff;border-color:#fff;opacity:1;
  box-shadow:0 0 0 3px rgba(255,255,255,.35);
  animation:pillar-pulse 2.4s ease-in-out infinite;
}
@keyframes pillar-pulse{
  0%,100%{box-shadow:0 0 0 2px rgba(255,255,255,.40);}
  50%    {box-shadow:0 0 0 6px rgba(255,255,255,.08);}
}

@media(max-width:1024px){.pillars{grid-template-columns:repeat(2,1fr);}}
@media(max-width:520px){.pillars{grid-template-columns:1fr;}}

/* ---- phases -------------------------------------------------- */
.phases{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:38px;position:relative;}
.phases .parrow{
  position:absolute;top:50%;transform:translateY(-50%);z-index:3;
  width:34px;height:34px;border-radius:50%;
  background:#fff;border:1.5px solid var(--line);
  display:grid;place-items:center;color:var(--grass-text);
  box-shadow:0 6px 16px -8px rgba(27,20,100,.4);
}
.phases .parrow.a1{left:calc(33.33% - 17px);}
.phases .parrow.a2{left:calc(66.66% - 17px);}
/* The roadmap sits on a plain .band, so the canvas runs behind these
   three cards. They were solid cream, which punched three opaque holes
   in the constellation right where the panels beside them were letting
   it through. They are the same liquid glass as the scrims now, so the
   network is continuous across the whole section.

   The paper bands (Pillars, Tour) are deliberately NOT converted. Per
   DESIGN.md the primary depth mechanism on this site is cream-on-white
   surface contrast, and those two bands are the white half of it. Glass
   there would invert the relationship and flatten the page. */
.phase{
  position:relative;
  background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha,.80));
  background-image:
    linear-gradient(122deg,
      rgba(255,255,255,.42) 0%,
      rgba(255,255,255,.08) 36%,
      rgba(255,255,255,0)   60%),
    linear-gradient(122deg,
      rgba(28,19,98,.04) 0%,
      rgba(67,182,73,.05) 100%);
  -webkit-backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  border:1px solid var(--glass-edge,rgba(255,255,255,.62));
  border-radius:var(--r-lg);padding:26px 22px;overflow:hidden;
  box-shadow:
    inset 0 1px 0 var(--glass-specular,rgba(255,255,255,.92)),
    inset 0 -1px 0 var(--glass-refract,rgba(28,19,98,.07));
  /* box-shadow ONLY. background-color is deliberately NOT transitioned.
     Chrome latched a CSSTransition on the tint that stayed in playState
     "running" with fill "backwards", which pins the computed value at
     the pre-transition transparent and never resolves. The pane then
     renders with no tint at all and body copy drops onto the raw
     constellation at roughly 1.3:1. The tint is the layer carrying the
     contrast floor, so it must never be in an interpolated state. Motion
     lives on the lit edge, which carries nothing but light. */
  transition:box-shadow var(--glass-morph,520ms) var(--ease-out,ease);
}
.phase::before{content:"";position:absolute;top:0;left:0;width:100%;height:5px;background:var(--grass);}
/* "You are here" was signalled by a white fill against cream. Both are
   glass now, so the current phase reads as the thicker, more lit pane:
   a denser tint, a brighter rim, and the lift shadow the others lack.
   The state is still carried by more than one cue, per DESIGN.md's
   colour-independence rule, since it also keeps its "You are here" tag. */
.phase.now{
  background-color:rgba(var(--glass-tint,251,250,246),.92);
  background-image:
    linear-gradient(122deg,
      rgba(255,255,255,.62) 0%,
      rgba(255,255,255,.16) 34%,
      rgba(255,255,255,0)   58%),
    linear-gradient(122deg,
      rgba(28,19,98,.05) 0%,
      rgba(67,182,73,.06) 100%);
  border-color:rgba(255,255,255,.85);
  box-shadow:
    inset 0 1px 0 #fff,
    inset 0 -1px 0 var(--glass-refract,rgba(28,19,98,.07)),
    var(--shadow-lg);
}
.phase .pn{
  display:inline-flex;align-items:center;gap:8px;
  font-family:var(--heading);font-weight:800;font-size:12px;
  /* Was --grass-text. These labels sit on the glass pane, and with a
     constellation node behind it that green measured 4.32:1 on the lit
     card and 3.63:1 on the resting one, both under the 4.5:1 floor for
     12px type. Same token the kickers use, for the same reason. */
  color:var(--grass-kicker,#216726);letter-spacing:.04em;text-transform:uppercase;
}
.phase .pn b{
  width:24px;height:24px;border-radius:50%;background:var(--navy);color:#fff;
  display:grid;place-items:center;font-size:12px;
}
/* White numeral on --grass was 2.62:1. */
.phase.now .pn b{background:var(--color-secondary);}
.phase h3{font-size:23px;margin-top:12px;}
.phase .psub{font-size:var(--fs-sm);color:var(--color-on-surface-muted);margin-top:5px;font-weight:600;}
/* Phase status pill. Same treatment as .pillar-tag on the five pillars,
   and for the same reason: the pale version read as a footnote when it is
   the one thing telling you where the organisation actually is. "You are
   here" now carries a solid grass fill with white type and a coloured
   lift; "Next" is a quiet outline that steps back.

   White on --grass-ink is 4.81:1. Not --grass: white on #43B649 is
   2.62:1 and fails. See tokens.css.

   Shape carries the state as well as colour, per DESIGN.md: the live pill
   has a filled dot with a halo, the pending one a hollow ring. */
.phase .tag{
  display:inline-flex;align-items:center;gap:8px;
  margin-top:8px;padding:6px 14px 6px 11px;
  border-radius:var(--r-pill);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-caption);
  text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;
  background:var(--color-surface);
  border:1.5px solid #D6D9E8;
  color:var(--color-on-surface-muted);
}
.phase .tag::before{
  content:"";flex:none;
  width:8px;height:8px;border-radius:50%;
  background:transparent;border:2px solid var(--ink-3);opacity:.6;
}
.phase.now .tag{
  background:var(--grass-ink);
  border-color:var(--grass-ink);
  color:var(--color-on-secondary);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.25),
    0 4px 14px -4px rgba(48,130,52,.55);
}
.phase.now .tag::before{
  background:#fff;border-color:#fff;opacity:1;
  animation:pillar-pulse 2.4s ease-in-out infinite;
}
.phase ul{list-style:none;margin:14px 0 0;padding:0;display:flex;flex-direction:column;gap:8px;}
.phase li{
  font-size:var(--fs-sm);color:var(--color-on-surface-muted);
  padding-left:20px;position:relative;line-height:1.45;
}
.phase li::before{
  content:"";position:absolute;left:0;top:8px;width:7px;height:7px;
  border-radius:50%;background:var(--grass-text);
}
@media(max-width:768px){.phases{grid-template-columns:1fr;}.phases .parrow{display:none;}}

/* ---- safety -------------------------------------------------- */
.safety{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:34px;}
/* The safety section is a plain .band, not .band.paper, so it is fully
   transparent and these three blocks were sitting straight on the fixed
   constellation canvas with nothing between them and it. A navy node at
   its maximum 0.70 alpha composites the cream to rgb(95,88,142), and
   against that the 14px body copy measured 1.27:1 and the 17px heading
   2.48:1, where both need 4.5:1. A node is 6.5 to 14.5px across against a
   1.4px DM Sans stem, so a single one passing behind a word can swallow a
   whole letter. This is the same readability scrim the hero and the band
   copy already use, and it lifts the paragraphs to 5.34:1 and the headings
   to 10.45:1. Padding rather than the negative-margin trick used on
   .band-copy, because these are grid items and pulling their edges outward
   would collapse the gap between them. */
/* Now on the shared liquid glass surface declared with .hero-copy and
   .band-copy above, so all three panes are one material rather than
   three that merely resemble each other. Only the box differs: padding
   instead of the negative-margin pull, for the grid-gap reason above. */
.safety .item{
  display:flex;flex-direction:column;gap:10px;
  padding:18px;
  margin:0;
}
.safety .ic{
  width:46px;height:46px;border-radius:13px;background:var(--navy);color:#fff;
  display:grid;place-items:center;
}
.safety h3{font-size:var(--fs-md);}
.safety p{color:var(--color-on-surface-muted);font-size:var(--fs-sm);}
@media(max-width:768px){.safety{grid-template-columns:1fr;gap:22px;}}

/* ---- tour ---------------------------------------------------- */
.tour{display:grid;grid-template-columns:1fr 360px;gap:48px;align-items:center;}
.tour .phone{
  justify-self:center;
  /* Was a hard 300x610. On a 320px phone that plus the section gutter
     pushed the layout past the viewport edge. It scales with its column
     now and keeps the handset proportion. */
  width:min(300px,100%);aspect-ratio:300/610;height:auto;
  border-radius:46px;
  background:#0f1020;padding:11px;box-shadow:var(--shadow-lg);position:relative;
}
.tour .screen{
  width:100%;height:100%;border-radius:36px;overflow:hidden;position:relative;
  background:
    radial-gradient(90% 55% at 82% 0%, rgba(67,182,73,.22), transparent 60%),
    linear-gradient(180deg,#fff,#fbfbfe);
  display:flex;flex-direction:column;align-items:center;padding:54px 26px 30px;
}
.tour .island{
  position:absolute;top:14px;left:50%;transform:translateX(-50%);
  width:96px;height:28px;border-radius:18px;background:#0f1020;
}
.tour .screen img{height:42px;width:auto;margin-top:18px;mix-blend-mode:multiply;}
.tour .screen .sh{
  font-family:var(--display);font-weight:900;color:var(--color-primary);
  font-size:var(--fs-2xl);text-align:center;margin-top:22px;line-height:1.1;
}
.tour .screen .ss{
  font-size:var(--fs-caption);color:var(--color-on-surface-muted);
  text-align:center;margin-top:10px;
}
.tour .screen .mini{margin-top:auto;width:100%;display:flex;flex-direction:column;gap:8px;}
.tour .screen .mini .b{
  border-radius:var(--r-field);padding:11px;text-align:center;
  font-family:var(--heading);font-weight:800;font-size:var(--fs-label);
  display:flex;align-items:center;justify-content:center;gap:7px;
}
.tour .screen .mini .b.n{background:var(--color-primary);color:var(--color-on-primary);}
.tour .screen .mini .b.d{background:#111;color:#fff;}
.tour .screen .mini .b.g{background:var(--color-surface);color:var(--color-on-surface);border:1.5px solid var(--color-border);}
.tour .screen .signin{
  font-size:var(--fs-caption);color:var(--color-on-surface-faint);
  text-align:center;margin-top:10px;
}
.tour .screen .signin b{color:var(--color-primary);}
@media(max-width:768px){.tour{grid-template-columns:1fr;gap:34px;}}

/* ---- mission ------------------------------------------------- */
.mission-grid{display:grid;grid-template-columns:1fr 4px 1fr;gap:48px;align-items:stretch;}
.mission-bar{background:var(--grass);border-radius:var(--r-pill);width:4px;}
.mission-body{display:flex;flex-direction:column;justify-content:center;}
.mission-grid h2{max-width:none;font-size:clamp(28px,4vw,50px);line-height:1.1;margin:10px 0 0;}
.mission-grid .blurb{margin-top:0;max-width:none;font-size:var(--fs-xl);line-height:1.6;}
.kwirkz-play{color:var(--grass-text);}
@media(max-width:768px){.mission-grid{grid-template-columns:1fr;gap:24px;}.mission-bar{display:none;}}

/* ---- final cta ----------------------------------------------- */
.final{
  background:linear-gradient(120deg,var(--navy),var(--grass-900));
  border-radius:var(--r-lg);padding:46px;color:#fff;text-align:center;
}
/* CRITICAL. The global focus ring is navy, and .final is a navy to green
   gradient, so on the terminal call to action of every page the ring was
   navy painted on navy: measured 1.37:1, which is no ring at all. A
   keyboard visitor tabbing to the single most important button on the
   page could not tell they had reached it.

   White clears both ends of the sweep comfortably (15.9:1 on the navy
   end, 5.5:1 on the green) and the offset puts it on the gradient rather
   than on the button, so it stays visible even on the white-filled
   .btn.grass variant. The dark outer shadow is the belt to that braces:
   it keeps the ring readable if the gradient is ever lightened. */
.final :where(a,button):focus-visible{
  outline:3px solid #fff;
  outline-offset:3px;
  box-shadow:0 0 0 6px rgba(20,22,43,.55);
  border-radius:var(--r-pill);
}
.final h2{color:var(--color-on-primary);font-size:clamp(25px,3.6vw,38px);max-width:none;margin-left:auto;margin-right:auto;}
/* The copy ran at 85% white. Over the green end of the gradient that
   measured 3.35:1. At 92% over the deeper --grass-900 end it reads
   4.97:1 at the worst point of the sweep. */
.final p{color:rgba(255,255,255,.92);margin:12px auto 22px;max-width:50ch;font-size:var(--fs-base);}
.final .btn.grass{background:var(--color-surface);color:var(--color-primary);}
.final .btn.grass:hover{background:#f1f1f7;}
@media(max-width:520px){
  .final{padding:32px 22px;}
}

/* ---- footer -------------------------------------------------- */
/* Navy, not the faint ink-3 grey. This line sits over the neural
   canvas with nothing behind it, and small grey type on a moving
   field is the least readable combination on the page. */
footer{
  padding:40px 0 30px;color:var(--color-primary);font-size:var(--fs-label);
  /* Rule `safe-area-awareness`: clears the iOS home indicator. */
  padding-bottom:max(30px, env(safe-area-inset-bottom));
}
.foot-inner{display:flex;justify-content:space-between;gap:18px;flex-wrap:wrap;align-items:center;}
/* Colour alone is not enough here; nodes still drift through the
   glyphs. Give the line the same scrim the copy panels use. */
/* Now the shared liquid glass, not the frosted blur(7px) this was built
   with. The site was painting three different backdrop-filter recipes:
   the liquid one on every copy pane, a 7px frost on these two footer
   surfaces, and a 10px frost on the sticky nav. A visitor scrolling from
   hero to footer passed through three materials that were all trying to
   be the same material. */
.foot-inner span{
  background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha,.80));
  background-image:linear-gradient(122deg,
    rgba(255,255,255,.45) 0%,rgba(255,255,255,.08) 38%,rgba(255,255,255,0) 62%);
  -webkit-backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  border:1px solid var(--glass-edge,rgba(255,255,255,.62));
  box-shadow:inset 0 1px 0 var(--glass-specular,rgba(255,255,255,.92));
  border-radius:var(--r-pill);
  padding:8px 16px;
}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .foot-inner span{background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha-solid,.93));}
}
/* width:auto so the wordmark keeps its 420x246 ratio. The markup used
   to declare 102x40, a 2.55:1 box for a 1.71:1 image, which squeezed it
   horizontally. */
footer img{height:40px;width:auto;mix-blend-mode:multiply;}

/* ---- footer nav ----------------------------------------------- */
/* Same scrim as .foot-inner span and for the same reason: these links
   sit directly over the drifting canvas with nothing behind them. */
.foot-links{
  /* gap was 2px. Rule `touch-spacing` puts the minimum gap between two
     adjacent targets at 8px, and these are five different destinations
     sitting a quarter of that apart, with each link's focus ring
     overlapping its neighbour's hit box. On a phone that is a mis-tap
     waiting to happen, and a mis-tap here navigates somewhere else. */
  display:flex;flex-wrap:wrap;gap:8px;
  background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha,.80));
  background-image:linear-gradient(122deg,
    rgba(255,255,255,.45) 0%,rgba(255,255,255,.08) 38%,rgba(255,255,255,0) 62%);
  -webkit-backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  backdrop-filter:blur(var(--glass-blur,3px)) saturate(var(--glass-saturate,185%)) brightness(var(--glass-brightness,1.04));
  border:1px solid var(--glass-edge,rgba(255,255,255,.62));
  box-shadow:inset 0 1px 0 var(--glass-specular,rgba(255,255,255,.92));
  border-radius:var(--r-pill);
  padding:4px 8px;
}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .foot-links{background-color:rgba(var(--glass-tint,251,250,246),var(--glass-alpha-solid,.93));}
}
.foot-links a{
  display:inline-flex;align-items:center;
  min-height:var(--tap);padding:0 13px;border-radius:var(--r-pill);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-sm);
  color:var(--color-primary);
  transition:background var(--dur-fast) var(--ease-out);
}
.foot-links a:hover{background:var(--navy-50);}
@media(max-width:768px){
  .foot-inner{justify-content:flex-start;}
  .foot-links{width:100%;}
}

/* ================================================================
   INTERIOR PAGES
   Everything below is used by /our-mission/, /news-events/ and
   /volunteer/. The homepage does not load any of it. Same tokens,
   same 44px floor, same three-family type split as above.
   ================================================================ */

/* ---- page hero ------------------------------------------------- */
/* The homepage hero is a two-column conversion layout. Interior pages
   get the lighter one: kicker, title, lede, nothing competing. */
/* The bottom padding is load-bearing, not taste. A .band-copy pane grows
   21px past its own layout box on every side, which is how it breaks out
   of the text column without squeezing it. Any container holding one
   therefore needs at least 21px of padding on the bleed sides or the
   glass paints over whatever comes next. This was 12px, so the pane
   crossed 9px into the following section on all four sub-pages. Every
   other container that holds a pane already clears this: section.band
   has 72px and .hero-inner has 60px. */
.page-hero{padding:52px 0 36px;position:relative;}
/* 18ch could not hold "Everyone deserves a way in." on one line, so the
   sentence always split whatever the balancer did. 24ch fits the longest
   clause on this page and still keeps the measure well inside the 35 to
   60 character band the rule set asks for on display type. */
/* margin-top matches .band h2 for the same reason: the interior page
   titles sit under the same kicker and must breathe the same way. */
.page-hero h1{font-size:clamp(30px,4.6vw,50px);margin-top:16px;max-width:24ch;}
.page-hero .lede{
  font-size:var(--fs-lg);color:var(--color-on-surface-muted);
  margin-top:16px;max-width:58ch;text-wrap:pretty;font-family:var(--body);
}
/* Below 1024px the pane stops bleeding (margin:0), so the container no
   longer has to reserve room for it and the padding can come back in. */
@media(max-width:768px){.page-hero{padding:34px 0 8px;}}
@media(max-width:520px){.page-hero .lede{font-size:var(--fs-md);}}

/* ---- prose column ---------------------------------------------- */
/* Running copy. The 66ch cap and 1.55 leading are the readability
   accommodation documented in DESIGN.md, not a stylistic choice. */
.prose{max-width:66ch;font-family:var(--body);}
.prose p{
  font-size:var(--fs-md);color:var(--color-on-surface-muted);
  line-height:var(--lh-body);margin-top:16px;text-wrap:pretty;
}
.prose p:first-child{margin-top:0;}
.prose strong{color:var(--color-on-surface);font-weight:700;}

/* ---- mission and vision statements ----------------------------- */
.statements{display:grid;grid-template-columns:repeat(2,1fr);gap:18px;margin-top:34px;}
.statement{
  position:relative;overflow:hidden;
  background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--r-lg);padding:26px 24px;
}
.statement::before{
  content:"";position:absolute;top:0;left:0;width:100%;height:5px;background:var(--grass);
}
.statement .ic{
  width:46px;height:46px;border-radius:13px;display:grid;place-items:center;
  background:var(--grass-50);color:var(--grass-text);
}
.statement.vision .ic{background:var(--navy-50);color:var(--color-primary);}
.statement h2{font-size:var(--fs-2xl);margin-top:16px;max-width:none;}
.statement p{
  font-family:var(--body);font-size:var(--fs-md);
  color:var(--color-on-surface-muted);line-height:var(--lh-body);margin-top:10px;
}
@media(max-width:768px){.statements{grid-template-columns:1fr;}}

/* ---- fact strip ------------------------------------------------- */
.facts{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px;}
.fact{
  display:inline-flex;flex-direction:column;gap:2px;min-width:150px;
  background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--r-field);padding:12px 16px;
}
.fact .k{
  font-family:var(--heading);font-weight:800;font-size:var(--fs-caption);
  text-transform:uppercase;letter-spacing:.05em;color:var(--grass-text);
}
.fact .v{
  font-family:var(--body);font-size:var(--fs-base);font-weight:600;
  color:var(--color-on-surface);
}

/* ---- team cards --------------------------------------------------- */
/* A portrait, a name and a role. Nothing else, by request.
   These cards previously also carried a "Kwirkz" stat block of three
   traits, a one-line description, and an email on the founder's card.
   All three were removed along with their CSS, so do not go looking for
   .quirks, .quirk-label, .quirk-list or .quirk-line.

   If a stat block ever comes back, the tone bar it was held to is worth
   keeping: affectionate, self-authored, never a diagnosis and never a
   joke at anyone's expense. A special needs organisation naming its own
   quirks fondly is the argument the whole site is making.

   140px portrait, not full-bleed. The three source files are 256x256
   (square-cropped at the source on kwirkz.net/our-team) and that is the
   ceiling. Stretching one across a ~365px card column would upscale it
   1.4x and soften every face. If tighter, larger crops ever arrive this
   can become a full-bleed 4:5 photo panel and the band can go. */
.team{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:34px;}
.member{
  display:flex;flex-direction:column;overflow:hidden;text-align:center;
  background:var(--color-surface);
  border:1px solid var(--color-border);
  border-radius:var(--r-lg);
  box-shadow:var(--shadow-md);
  transition:transform var(--dur,220ms) var(--ease-out,ease),
             box-shadow var(--dur,220ms) var(--ease-out,ease);
}
/* The card picks itself up when you point at it. This is the whole of
   the "fun", and it is deliberately motion rather than more copy: these
   are real named people and the site should not invent personality
   traits for them to fill a card out. Transform only, so nothing
   reflows and the grid never shifts under the cursor. The global
   prefers-reduced-motion block at the end of this file flattens it. */
.member:hover{
  transform:translateY(-4px);
  box-shadow:var(--shadow-lg);
}
.member:hover .avatar{transform:scale(1.04);}
/* Decorative, aria-hidden in the markup. Each card takes a different
   pairing of existing tint tokens. No new colours enter the system. */
/* The dot field is the page constellation, borrowed. Two radial
   gradients on a 26px grid, offset from each other so the dots do not
   line up into rows and read as a halftone. It gives the band something
   to look at without introducing a new motif or a new colour. */
.member-band{
  --band-grad:linear-gradient(140deg,var(--navy-50),var(--grass-50));
  height:88px;position:relative;
  background-image:
    radial-gradient(circle, rgba(28,19,98,.14) 1.6px, transparent 1.7px),
    radial-gradient(circle, rgba(67,182,73,.16) 1.4px, transparent 1.5px),
    var(--band-grad);
  background-size:26px 26px, 26px 26px, auto;
  background-position:0 0, 13px 13px, 0 0;
}
/* Per-card tint. These set the CUSTOM PROPERTY, not the `background`
   shorthand they used to set: the shorthand resets background-image and
   would have wiped the dot field off cards two and three, leaving card
   one visibly different from its siblings. */
.member.m2 .member-band{--band-grad:linear-gradient(140deg,var(--grass-50),var(--navy-50));}
.member.m3 .member-band{--band-grad:linear-gradient(140deg,var(--tint-violet-bg),var(--navy-50));}

.member .avatar{
  width:140px;height:140px;border-radius:50%;
  margin:-70px auto 0;position:relative;z-index:1;
  border:4px solid var(--color-surface);
  box-shadow:0 10px 26px -10px rgba(28,19,98,.45);
  background:var(--color-border);
  transition:transform var(--dur,220ms) var(--ease-out,ease);
}
.member img.avatar{display:block;object-fit:cover;}
/* Initials fallback for anyone added later without a photo. Same
   footprint, so the grid never jumps. */
.member div.avatar{
  display:grid;place-items:center;
  background:var(--color-primary);color:var(--color-on-primary);
  font-family:var(--heading);font-weight:900;font-size:44px;letter-spacing:-.02em;
}
.member.m2 div.avatar{background:var(--grass-ink);}
.member.m3 div.avatar{background:var(--tint-violet-fg);}

.member-body{
  display:flex;flex-direction:column;align-items:center;
  flex:1;padding:14px 20px 22px;
}
.member h3{font-size:var(--fs-2xl);}
.member .role{
  display:inline-block;margin-top:9px;
  background:var(--grass-50);border:1px solid var(--grass-border);
  color:var(--grass-text);border-radius:var(--r-pill);padding:4px 13px;
  font-family:var(--heading);font-weight:800;font-size:var(--fs-caption);
  text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;
}
/* The one-liner description and the email link that used to sit under
   the role were both removed on request. The card is now a portrait, a
   name and a role, and nothing else. Their rules are gone with them
   rather than left behind as dead weight.

   Removed with them: a broken selector fragment, `.member.m2 .member.m3`
   with no block, left over from stripping the per-card trait colours. An
   unterminated selector does not fail quietly in CSS. It swallowed the
   `.member a` rule that followed it, so the email link had been silently
   unstyled for as long as that fragment was there. */
@media(max-width:1024px){.team{grid-template-columns:repeat(2,1fr);}}
@media(max-width:640px){.team{grid-template-columns:1fr;}}

/* ---- events table ----------------------------------------------- */
/* A real <table>, not a grid of divs. These are rows of like data and a
   screen reader has to be able to walk them by column. Below 768px the
   header row is taken out of the paint and every cell announces its own
   label from data-label, because five columns cannot survive 390px. */
.table-wrap{
  margin-top:32px;overflow:hidden;
  background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--r-lg);
}
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;}
table.events{width:100%;border-collapse:collapse;font-family:var(--body);}
table.events caption{
  text-align:left;padding:22px 22px 0;
  font-family:var(--heading);font-weight:800;font-size:var(--fs-xl);
  color:var(--color-primary);
}
table.events caption .cap-note{
  display:block;margin-top:6px;
  font-family:var(--body);font-weight:400;font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);max-width:62ch;line-height:1.5;
}
table.events th,table.events td{
  padding:15px 22px;text-align:left;vertical-align:top;
  border-bottom:1px solid var(--color-border);
}
table.events thead th{
  background:var(--color-surface-sunken);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-label);
  text-transform:uppercase;letter-spacing:.05em;color:var(--grass-text);
  white-space:nowrap;
}
table.events tbody tr:last-child td{border-bottom:none;}
table.events tbody tr:hover{background:var(--color-surface-sunken);}
table.events td{
  font-size:var(--fs-sm);color:var(--color-on-surface-muted);line-height:1.5;
}
table.events .ev-name{
  font-family:var(--heading);font-weight:800;font-size:var(--fs-md);
  color:var(--color-primary);min-width:170px;
}
table.events .ev-when{font-weight:700;color:var(--color-on-surface);white-space:nowrap;}
table.events .ev-where{min-width:180px;}
table.events .ev-do{min-width:230px;}
.ev-tag{
  display:inline-block;margin-top:7px;white-space:nowrap;
  background:var(--grass-50);border:1px solid var(--grass-border);
  color:var(--grass-text);border-radius:var(--r-pill);padding:3px 11px;
  font-family:var(--heading);font-weight:800;font-size:var(--fs-caption);
}
.ev-tag.tba{background:var(--field);border-color:var(--color-border);color:var(--ink-3);}
table.events .btn{padding:10px 16px;font-size:var(--fs-label);}

@media(max-width:768px){
  .table-wrap{border-radius:var(--r);}
  table.events caption{padding:18px 18px 0;font-size:var(--fs-lg);}
  table.events thead{
    position:absolute;width:1px;height:1px;overflow:hidden;
    clip:rect(0 0 0 0);clip-path:inset(50%);
  }
  table.events tr{
    display:block;border-bottom:1px solid var(--color-border);padding:10px 0;
  }
  table.events tbody tr:last-child{border-bottom:none;}
  /* tbody th is included deliberately. The first cell of each row is a
     <th scope="row">, not a td, so a rule written only for td left the
     event NAME as the one cell on the page with no label and no grid,
     which is the cell that most needs both. */
  table.events td,
  table.events tbody th{
    display:grid;grid-template-columns:minmax(82px,30%) 1fr;gap:12px;
    border-bottom:none;padding:7px 18px;min-width:0;
    text-align:left;
  }
  table.events td::before,
  table.events tbody th::before{
    content:attr(data-label);
    font-family:var(--heading);font-weight:800;font-size:var(--fs-caption);
    text-transform:uppercase;letter-spacing:.04em;color:var(--grass-text);
  }
  table.events .ev-name,table.events .ev-where,table.events .ev-do{min-width:0;}
  table.events .ev-when{white-space:normal;}
}

/* ---- news list --------------------------------------------------- */
.news{display:flex;flex-direction:column;gap:18px;margin-top:34px;}
.news-item{
  display:grid;grid-template-columns:150px 1fr;gap:26px;
  background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--r-lg);padding:26px;
}
.news-date{
  font-family:var(--heading);font-weight:800;font-size:var(--fs-label);
  text-transform:uppercase;letter-spacing:.05em;color:var(--grass-text);padding-top:5px;
}
.news-item h3{font-size:var(--fs-2xl);}
.news-item p{
  font-family:var(--body);font-size:var(--fs-base);
  color:var(--color-on-surface-muted);line-height:var(--lh-body);
  margin-top:10px;text-wrap:pretty;
}
.news-item .more{
  display:inline-flex;align-items:center;gap:7px;min-height:var(--tap);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-sm);
  color:var(--grass-text);
}
.news-item .more:hover{text-decoration:underline;}
@media(max-width:768px){
  .news-item{grid-template-columns:1fr;gap:10px;padding:22px;}
  .news-date{padding-top:0;}
}

/* ---- volunteer roles ---------------------------------------------- */
.roles{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:34px;}
.role{
  display:flex;flex-direction:column;
  background:var(--color-surface-sunken);border:1px solid var(--color-border);
  border-radius:var(--r-lg);padding:24px 22px;
}
.role .ic{
  width:48px;height:48px;border-radius:14px;display:grid;place-items:center;
  background:var(--grass-50);color:var(--grass-text);
}
.role.r2 .ic{background:var(--navy-50);color:var(--color-primary);}
.role.r3 .ic{background:var(--tint-amber-bg);color:var(--tint-amber-fg);}
.role.r4 .ic{background:var(--tint-gold-bg);color:var(--tint-gold-fg);}
.role.r5 .ic{background:var(--tint-violet-bg);color:var(--tint-violet-fg);}
.role h3{font-size:var(--fs-xl);margin-top:16px;}
.role > p{
  font-family:var(--body);font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);line-height:var(--lh-body);margin-top:8px;
}
/* A description list, because "Time" and "every other Saturday" are a
   term and its definition, and that pairing should survive being read
   aloud out of visual order. */
.role dl{margin:16px 0 0;display:flex;flex-direction:column;gap:9px;}
.role dt{
  font-family:var(--heading);font-weight:800;font-size:var(--fs-caption);
  text-transform:uppercase;letter-spacing:.04em;color:var(--grass-text);
}
.role dd{
  margin:2px 0 0;font-family:var(--body);font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);line-height:1.45;
}
@media(max-width:1024px){.roles{grid-template-columns:repeat(2,1fr);}}
@media(max-width:640px){.roles{grid-template-columns:1fr;}}

/* ---- numbered steps ------------------------------------------------ */
.steps{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:34px;}
.step{
  background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--r-lg);padding:24px 22px;
}
.step .n{
  width:34px;height:34px;border-radius:50%;
  background:var(--color-primary);color:var(--color-on-primary);
  display:grid;place-items:center;
  font-family:var(--heading);font-weight:900;font-size:var(--fs-base);
}
.step h3{font-size:var(--fs-lg);margin-top:14px;}
.step p{
  font-family:var(--body);font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);margin-top:8px;line-height:1.5;
  /* Rule `line-length-control` wants 60 to 75 characters. With .steps
     collapsed to one column at 768px this paragraph had no cap and ran
     to 106 characters on a tablet, which is where the eye starts losing
     its place on the return sweep. */
  max-width:66ch;
}
@media(max-width:768px){.steps{grid-template-columns:1fr;}}

/* ---- callout -------------------------------------------------------- */
.callout{
  display:flex;gap:14px;align-items:flex-start;margin-top:28px;
  background:var(--grass-50);border:1px solid var(--grass-border);
  border-radius:var(--r-lg);padding:20px 22px;
}
.callout svg{color:var(--grass-text);flex:none;margin-top:3px;}
.callout p{
  font-family:var(--body);font-size:var(--fs-base);
  color:var(--color-on-surface-muted);line-height:var(--lh-body);
  /* A flex item defaults to min-width:auto, which floors it at the width of
     its longest unbreakable run. Paired with the overflow-wrap:anywhere on
     the links below, this is what stops a long email address setting the
     width of the whole callout at 320px. */
  min-width:0;
}
.callout strong{color:var(--grass-text);}
/* These sit mid-sentence. DESIGN.md sets a 44px floor on tap targets,
   and this is the one documented exception to it: a link inside a
   running paragraph cannot be 44px tall without wrecking the line
   spacing of the prose around it, which is its own readability cost.
   WCAG 2.5.8 carves out the same exception for links in text. They are
   padded to clear 24px, they are underlined so they do not rely on the
   green alone, and any link that is a primary action is a .btn
   elsewhere rather than an inline link here. */
.callout a,
table.events caption a,
.news-item p a{
  display:inline-block;padding:4px 1px;
  font-weight:700;color:var(--grass-text);text-decoration:underline;
  text-underline-offset:2px;
  /* overflow-wrap MUST stay on the <a>, not the parent. display:inline-block
     makes this an atomic inline box, so a wrap rule on the paragraph cannot
     reach inside it. Without this the 29-character email address is a single
     unbreakable 257px box, which set the min-content width of a flex item in
     .callout and pushed the document to 338px at a 320px viewport. Because
     body carries overflow-x:hidden as a backstop, the page did not scroll,
     it CLIPPED: the last 18px of the address was cut off at the screen edge
     on the one element whose entire job is to be read and tapped. */
  overflow-wrap:anywhere;
}

/* ---- event photography --------------------------------------- */
/* Real photographs of real Kwirkz events, from the Kick it for Kwirkz
   day at Municipal Park and The Hive of Hope Mills on 28 June 2026.

   Every img carries width and height matching the file, so the browser
   reserves the right box before the bytes land (rule `image-dimension`,
   and the same Core Web Vitals note as .hero-logo). Everything below
   the fold is loading="lazy". These are ~1.5MB in total, so shipping
   them eagerly would cost more than the whole rest of the site.

   Captions are real captions, not decoration: alt text describes the
   photograph for someone who cannot see it, the figcaption tells
   everyone what the moment was. They say different things on purpose. */
/* Symmetric margin. This used to be `34px 0 0`, with no bottom at all, so
   a figure that happened to be the last thing in its section sat flush on
   the next section's edge. On the volunteer page that put a photo hard
   against the top border of a white band: measured 0px. A figure should
   never touch what follows it, wherever this class is used. */
.shotline{
  margin:34px 0;
  border:1px solid var(--color-border);border-radius:var(--r-lg);
  overflow:hidden;background:var(--color-surface);
  box-shadow:var(--shadow-md);
}
.shotline img{
  width:100%;height:auto;display:block;
  aspect-ratio:1600/1082;object-fit:cover;
}
.shotline figcaption{
  padding:16px 22px;
  font-family:var(--body);font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);line-height:1.5;
}
.shotline figcaption strong{color:var(--color-on-surface);font-weight:700;}

.gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:18px;}
.shot{
  margin:0;display:flex;flex-direction:column;
  border:1px solid var(--color-border);border-radius:var(--r-lg);
  overflow:hidden;background:var(--color-surface);
}
.shot img{
  width:100%;height:auto;display:block;
  aspect-ratio:3/2;object-fit:cover;
}
.shot figcaption{
  padding:13px 16px;
  font-family:var(--body);font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);line-height:1.45;
}
/* The photo credit line under a gallery. Small, but never below the
   12px floor set in tokens.css. */
.photo-note{
  margin-top:14px;
  font-family:var(--body);font-size:var(--fs-caption);
  color:var(--color-on-surface-faint);
}
@media(max-width:900px){.gallery{grid-template-columns:repeat(2,1fr);}}
@media(max-width:600px){
  .gallery{grid-template-columns:1fr;}
  .shotline figcaption{padding:14px 18px;}
}

/* ---- contact methods ------------------------------------------ */
.contact-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:34px;}
.contact-card{
  display:flex;flex-direction:column;
  background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--r-lg);padding:24px 22px;
}
.contact-card .ic{
  width:48px;height:48px;border-radius:14px;display:grid;place-items:center;
  background:var(--grass-50);color:var(--grass-text);
}
.contact-card.c2 .ic{background:var(--navy-50);color:var(--color-primary);}
.contact-card.c3 .ic{background:var(--tint-amber-bg);color:var(--tint-amber-fg);}
.contact-card h3{font-size:var(--fs-xl);margin-top:16px;}
.contact-card p{
  font-family:var(--body);font-size:var(--fs-sm);
  color:var(--color-on-surface-muted);line-height:var(--lh-body);margin-top:8px;
  /* the email address is the longest unbreakable run on the page */
  overflow-wrap:anywhere;
}
.contact-card .btn{margin-top:auto;align-self:flex-start;}
.contact-card p + .btn{margin-top:16px;}
@media(max-width:900px){.contact-grid{grid-template-columns:1fr;}}

/* ---- map ------------------------------------------------------- */
/* OpenStreetMap, not Google Maps, and that is a deliberate choice.
   A Google Maps embed sets third-party cookies and reports the visit
   back before anyone has agreed to anything. This site tells families
   in the waitlist form that it never sells their data, and a tracking
   iframe on the contact page would quietly undercut that. OSM's embed
   needs no API key, sets no cookies and costs nothing.

   The frame carries an explicit title, because an untitled iframe is
   announced as just "frame" by a screen reader. The written address
   sits directly beneath it, so nobody has to be able to read a map to
   get the location. */
.map-wrap{
  margin-top:30px;overflow:hidden;
  background:var(--color-surface);
  border:1px solid var(--color-border);border-radius:var(--r-lg);
}
.map-frame{
  position:relative;width:100%;aspect-ratio:16/9;
  background:var(--field);border-bottom:1px solid var(--color-border);
}
.map-frame iframe{
  position:absolute;inset:0;width:100%;height:100%;
  border:0;display:block;
}
.map-meta{
  display:flex;flex-wrap:wrap;gap:20px;
  align-items:flex-start;justify-content:space-between;
  padding:20px 22px;
}
.map-addr{font-family:var(--body);font-size:var(--fs-base);color:var(--color-on-surface-muted);}
.map-addr strong{
  display:block;font-family:var(--heading);font-weight:800;
  font-size:var(--fs-lg);color:var(--color-primary);margin-bottom:6px;
}
.map-addr .note{
  display:block;margin-top:8px;
  font-size:var(--fs-sm);color:var(--color-on-surface-faint);max-width:52ch;
}
.map-actions{display:flex;flex-wrap:wrap;gap:10px;}
@media(max-width:600px){
  .map-frame{aspect-ratio:4/3;}
  .map-meta{flex-direction:column;gap:14px;}
  .map-actions{width:100%;}
  .map-actions .btn{flex:1;}
}

/* ---- motion preference --------------------------------------- */
@media(prefers-reduced-motion:reduce){
  *{animation-duration:.001ms !important;transition-duration:.001ms !important;}
  html{scroll-behavior:auto;}
}

/* ---- partner marquee --------------------------------------------- */
/* Two identical tracks laid side by side inside one flex row. The row
   translates left by exactly 50%, which lands the second track precisely
   where the first began, so the restart is invisible. The duplicate in
   the markup is what makes that possible; cloning in JS would work too
   but would leave the strip empty until the script ran.

   MOTION IS THE RISK HERE, not the layout. This is a special needs
   organisation and an endlessly moving strip is exactly the kind of
   thing that costs some visitors the page. Three escape hatches:
   it pauses on hover, it pauses on keyboard focus, and under
   prefers-reduced-motion it stops being a marquee at all. */
.marquee{
  position:relative;
  margin-top:34px;
  overflow:hidden;
  /* Fade both edges so logos enter and leave instead of being chopped
     off against a hard border. */
  -webkit-mask-image:linear-gradient(to right,transparent,#000 9%,#000 91%,transparent);
  mask-image:linear-gradient(to right,transparent,#000 9%,#000 91%,transparent);
  display:flex;width:100%;
}
.partner-track{
  list-style:none;margin:0;padding:0;
  display:flex;align-items:center;gap:22px;
  padding-right:22px;
  flex:none;
  animation:partner-scroll 85s linear infinite;
}
@keyframes partner-scroll{
  from{transform:translateX(0);}
  to{transform:translateX(-100%);}
}
/* Both tracks must pause together or the two halves drift apart and the
   seam opens up, so the trigger is on the container. */
.marquee:hover .partner-track,
.marquee:focus-within .partner-track{animation-play-state:paused;}

/* The global reduce block sets animation-duration:.001ms !important on
   everything, which is the right instinct for a one-shot transition and
   exactly the wrong outcome here. This animation is iteration-count
   infinite, so a near-zero duration does not stop it, it spins the
   keyframes about a million times a second: the track looks frozen while
   the compositor burns battery behind it. Killing the NAME is what stops
   an animation. Duration carries the !important, name does not, so
   animation-name:none wins cleanly and no !important arms race is needed.

   The track is then parked at translateX(0) showing the first logos, and
   the rail becomes a normal horizontal scroller so every partner is still
   reachable, now under the visitor's own thumb rather than on a timer.
   That is the substitution WCAG 2.2.2 asks for: motion removed, content
   kept. */
@media(prefers-reduced-motion:reduce){
  .partner-track{animation-name:none;transform:none;}
  .marquee{
    overflow-x:auto;
    -webkit-overflow-scrolling:touch;
    scroll-snap-type:x proximity;
    /* The edge mask exists to fade logos in and out of a moving rail.
       Once the rail is hand-scrolled it just hides the first and last
       tile behind a gradient, so it comes off. */
    -webkit-mask-image:none;mask-image:none;
  }
  .marquee .partner{scroll-snap-align:start;}
}

/* Light tiles. The logos are the sponsors' own artwork in their own
   colours now, lifted from the thank-you reel and white-balanced so the
   card each one was sitting on becomes true white rather than the grey
   blue the video had baked into it. Colour marks need a light ground,
   and a plain surface with a hairline keeps the frame quiet so twenty
   two different logos do not fight each other. */
.partner{
  flex:none;
  display:grid;place-items:center;
  height:132px;
  min-width:196px;
  padding:0 30px;
  background:var(--color-surface);
  border:1px solid var(--color-border);
  border-radius:var(--r-lg);
}
/* Height is the constant, width follows the artwork. max-width catches
   the widest wordmarks and object-fit:contain scales them rather than
   squashing them. */
.partner img{
  height:72px;width:auto;
  max-width:250px;
  object-fit:contain;
}

@media(max-width:640px){
  .partner{height:104px;min-width:152px;padding:0 22px;}
  .partner img{height:56px;max-width:186px;}
  .partner-track{gap:14px;padding-right:14px;animation-duration:65s;}
}

/* Rule `reduced-motion`. The global reduce block at the end of this file
   collapses every animation to 0.001ms, which for a marquee is worse
   than useless: it would snap the strip to its end position instantly
   and leave a half empty row. So this rule does not just stop the
   animation, it dismantles the marquee. The strip becomes a normal
   wrapping grid of logos, the duplicate track is removed from the paint
   entirely so nothing is shown twice, and the edge fade goes with it. */
@media(prefers-reduced-motion:reduce){
  .marquee{
    overflow:visible;flex-wrap:wrap;justify-content:center;
    -webkit-mask-image:none;mask-image:none;
    padding-left:var(--gutter-safe-l);padding-right:var(--gutter-safe-r);
  }
  .partner-track{
    animation:none;
    flex-wrap:wrap;justify-content:center;
    padding-right:0;
  }
  .partner-track[aria-hidden="true"]{display:none;}
}

/* ---- events table: actions and attachments ------------------------ */
/* Every row now offers both a way in and a way to help, so the last
   column holds two buttons rather than one. They stack, because side by
   side they would force the column wide enough to squeeze the "What
   happens" text into a ribbon. align-items:stretch so the pair are the
   same width as each other and read as a set. */
.ev-act{white-space:normal;}
.ev-act .btn{
  display:flex;width:100%;
  padding:9px 14px;font-size:var(--fs-label);
}
.ev-act .btn + .btn{margin-top:8px;}
@media(max-width:768px){
  /* In the stacked card layout the label column is already carrying the
     row, so the buttons sit side by side again where there is room. */
  .ev-act{display:flex;flex-wrap:wrap;gap:8px;}
  .ev-act .btn{width:auto;flex:1 1 140px;}
  .ev-act .btn + .btn{margin-top:0;}
}

/* The attached flyer. Reads as a document link, not another button, so
   it never competes with Register and Volunteer. */
.ev-doc{
  display:inline-flex;align-items:center;gap:7px;
  margin-top:10px;min-height:var(--tap);
  font-family:var(--heading);font-weight:800;font-size:var(--fs-label);
  color:var(--grass-text);
}
.ev-doc:hover{text-decoration:underline;}
.ev-doc svg{flex:none;}

/* Optical sizing, one logo. Everything in the strip is matched by height,
   which is right for a mark plus a wordmark but wrong for a pure wordmark:
   Paxton Bedsole is letters and two rules and nothing else, at a 3.6:1
   ratio, so filling the same 48px made its type read far larger than any
   other partner's. Sized down until it matches the rest by eye rather
   than by measurement. */
.partner img[src*="paxton-bedsole"]{height:48px;}
@media(max-width:640px){
  .partner img[src*="paxton-bedsole"]{height:38px;}
}

/* The product name leads, the pill states the status underneath. One
   badge carrying both was doing two jobs and reading as neither.
   margin-bottom, not the section's usual heading spacing, because the
   pill has to sit tight under the name it belongs to. */
/* Larger than a normal section heading. This one names the product the
   whole section is about, so it outranks the h2s around it. */
/* h2.soon-head, not .soon-head: .band h2 sets a font-size and carries an
   element in its selector, so a bare class ties nothing and loses. */
h2.soon-head{
  font-size:clamp(32px,5.2vw,54px);
  max-width:20ch;
  margin:0 0 16px;
}
/* .soon-head declares its own cap, so the wide variant has to be said
   again here or the two tie on specificity and source order decides. */
h2.soon-head.h2-wide{max-width:32ch;}


/* ---- centred section header --------------------------------------- */
/* Used by the partners section, where the strip below runs the full
   width of the screen and a left-aligned header sitting over the middle
   of it read as though it belonged to something else.

   text-align on the wrapper, then the two caps that are normally set
   from the left edge are re-centred: .band h2 caps at 20ch and .blurb at
   62ch, and a capped block still sits hard left unless its auto margins
   are put back. */
.wrap.centered{text-align:center;}
.wrap.centered .band-copy{
  display:block;
  margin-left:auto;margin-right:auto;
}
.wrap.centered h2,
.wrap.centered .blurb{margin-left:auto;margin-right:auto;}

/* ---- video in a shotline figure ------------------------------------ */
/* Reuses the photo figure so a video sits in the page the same way a
   photograph does: same frame, same caption treatment, same rounded
   corners. aspect-ratio holds the box before the poster loads, so the
   caption never jumps up the page and back down. */
.video-shot video{
  display:block;width:100%;height:auto;
  aspect-ratio:16/9;
  background:var(--navy);
  object-fit:cover;
}

/* Section headings cap at 20ch so a long one breaks into a readable
   stack rather than a single wide line. This one is 29 characters and
   wants 304px against that 292px cap, so it broke after "two" for the
   sake of twelve pixels, in a column with 768px going spare.

   32ch, not none and not nowrap. The cap is in ch, so it scales with the
   fluid heading size and holds one line at every desktop width, while a
   narrow phone still wraps the heading normally instead of pushing the
   page sideways. */
.band h2.h2-wide{max-width:32ch;}
/* Same utility for page hero titles, which cap at 24ch rather than 20ch
   but wrap for the same reason. Kept on one selector list so there is
   one place to change the widened cap. */
.page-hero h1.h2-wide{max-width:36ch;}

/* The mission statement beside the pull quote. Centred and bold on
   request: it is the thesis of the whole organisation rather than
   running copy, so it is set as a statement instead of a paragraph.
   Weight 700 rather than 800, because DM Sans at this size goes muddy
   at the heavier weight and this text has to stay easy to read. */
.blurb.mission-statement{
  text-align:center;
  font-weight:700;
  color:var(--color-on-surface);
  text-wrap:balance;
}
