/* The Studio LDG public site.
   Black and neon green, which is where the current site already lives.
   Mobile first, 44px tap target floor, then a desktop pass. */

@font-face{font-family:'Anton';src:url(/fonts/anton.woff2) format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Archivo';src:url(/fonts/archivo.woff2) format('woff2');font-weight:100 900;font-display:swap}

:root{
  --ink:#0C0E0B; --surf:#151813; --surf2:#1D211A; --line:#2C3128;
  --neon:#25D11B; --neon-dim:#178211;
  --tx:#F7F7F5; --tx2:#B9BDB3; --tx3:#8A8F84;
}
*{box-sizing:border-box}
:focus-visible{outline:2px solid var(--neon);outline-offset:2px;border-radius:4px}
/* Deliberately NOT smooth. Bobbie, 2026-08-12: "get started can it jump not
   scroll? it takes too long to scroll". This page runs 66 classes deep, so a
   smooth scroll to the bottom is seconds of watching it slide by. */
html{scroll-behavior:auto}
/* The sticky bar is roughly 100px tall with its safe-area padding. Anything
   an anchor can land on gets cleared of it, or the heading you jumped to
   sits hidden behind the bar and the reader arrives mid paragraph. */
/* Clear of the sticky bar, at BOTH sizes. The bar is taller on desktop (it
   gains the nav row), so one fixed offset cannot serve both: at 108px a
   desktop heading landed 13px under a 121px bar, which reads as the link
   having gone to the previous section. */
section,[id]{scroll-margin-top:calc(env(safe-area-inset-top,0px) + 108px)}
/* 150px here was sized for a desktop bar that wrapped onto two rows. The bar
   is one row and 59px now, so that figure left a jumped-to heading floating
   ninety pixels below the bar. Keep this in step with .bar's real height:
   measure it, do not estimate it. */
@media (min-width:1100px){section,[id]{scroll-margin-top:calc(env(safe-area-inset-top,0px) + 76px)}}
body{margin:0;background:var(--ink);color:var(--tx);
  font-family:'Archivo',system-ui,sans-serif;font-size:17px;line-height:1.6;-webkit-font-smoothing:antialiased}
img{max-width:100%}
p{margin:0}
.wrap{max-width:780px;margin:0 auto;padding:0 18px 90px}
@media (min-width:1000px){
  .wrap,.barin{max-width:1060px}
  /* Prose stays at a readable measure even though the page is wider. */
  .sub,.card p,.fq p,.gen .body,.note,.count{max-width:72ch}
}
.skip{position:absolute;left:-9999px;min-height:48px;display:inline-flex;align-items:center}
.skip:focus{left:18px;top:8px;z-index:60;background:var(--neon);color:#08130A;
  padding:0 18px;border-radius:8px;display:inline-flex;align-items:center;min-height:48px}

/* Phone and email are the two things a parent taps. Inline text links put
   them at 19px, well under the 44px floor, on a site whose visitors are
   overwhelmingly one handed on a phone. They get to be real targets. */
.contactline{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}
.contactline a{display:inline-flex;align-items:center;min-height:48px;padding:0 18px;
  border:1.5px solid var(--neon-dim);border-radius:10px;color:var(--neon);text-decoration:none;
  font-weight:800;font-size:max(12px, calc(var(--sec-scale,1) * 15px))}
.contactline a:hover,.contactline a:focus-visible{background:var(--neon);color:#08130A;outline:none}
footer .flogo{height:64px;width:auto;display:block;margin-bottom:10px}
footer .ftag{font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;color:var(--neon);
  font-size:13px;letter-spacing:.1em;margin-bottom:14px}
footer .flinks{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
footer .flinks a{display:inline-flex;align-items:center;min-height:48px;padding:0 14px;
  border:1px solid var(--line);border-radius:10px;text-decoration:none}

/* The bar. Name left, one Menu button right, links stacked full width behind
   it where a thumb can reach them. Desktop shows seven links inline instead.
   The top padding clears an iPhone Dynamic Island; env() adds the real inset
   wherever the browser reports one. */
/* The bar steps out of the way while you are reading DOWN the page, and comes
   back the moment you scroll up. Sticky alone meant it sat across the top of
   whichever photo you had just scrolled to, which is what Bobbie kept seeing.
   It never hides while the menu is open or while focus is inside it, so it
   cannot vanish out from under a keyboard or a screen reader. */
.bar{transition:transform .28s ease}
.bar.tucked{transform:translateY(-100%)}
@media (prefers-reduced-motion:reduce){.bar{transition:none}}
.bar{position:sticky;top:0;z-index:30;background:rgba(12,14,11,.97);backdrop-filter:blur(10px);
  border-bottom:1px solid var(--line);padding:calc(env(safe-area-inset-top,0px) + 22px) 18px 0}
/* Bobbie, 2026-09-24: "on mobile you can't scroll the entire menu. it gets
   cut off by the floater and constant get started button." The open panel
   used to just grow as tall as its own content, inside .bar's ordinary
   document flow, and rely on the PAGE scrolling it into view. Two things
   broke that: the sticky "Get started" pill and the Back to top button are
   both position:fixed with z-index:40, above the bar's own z-index:30, so
   they sat on top of whatever the panel's bottom rows scrolled to; and
   because the panel starts BELOW the logo row rather than at the very top of
   the screen, simply capping the PANEL's own height to 100dvh still let its
   box run past the bottom of the viewport by exactly that header's height,
   proven by measuring it in a real browser before this fix (top:78px,
   height capped at the full 667px viewport, bottom landing at 745px: 78px
   of the last row sat off screen no matter how far the panel scrolled).
   Flexbox fixes both at once: while the menu is open (body.menu-open, set by
   site.js), .bar itself is capped to the viewport height (100dvh, with the
   100vh fallback for a browser that does not know dvh yet) and becomes a
   column flex container; the logo row keeps its own natural height
   (flex-shrink:0 on .barrow, unchanged below) and the menu panel is given
   exactly what is left (flex:1, min-height:0, its own overflow-y below), so
   it can never run past the screen and always has real scrollable room for
   every row, regardless of the exact header height on any device. */
body.menu-open .bar{max-height:100vh;max-height:100dvh;display:flex;flex-direction:column;overflow:hidden}
/* width:100% is load bearing, not decoration. .barin's own margin:0 auto
   (below, for centering under its max-width on a wide screen) is an AUTO
   margin, and an auto margin on a flex item's cross axis wins over
   align-items:stretch by spec: without an explicit width the item shrinks
   to fit its widest row's own content instead of filling the bar, which
   measured as a menu barely a third of the screen's width in a real
   browser before this line was added. width:100% gives it a definite size
   to stretch to; the auto margins then only do their original job of
   centering that (now full width, capped at max-width:780px) box. */
body.menu-open .barin{display:flex;flex-direction:column;min-height:0;flex:1;width:100%}
.barin{max-width:780px;margin:0 auto}
.barrow{display:flex;align-items:center;gap:12px;min-height:56px;flex-shrink:0}
/* flex-shrink:0 is load bearing. The bar is a flex row, and without this the
   logo is the item that gives way: it compressed to ZERO at 1000px and at
   every width from 1400px up, so the studio's own mark was missing from its
   own header on most desktop monitors, while the links and pills all looked
   fine. An adversarial review caught it on 2026-08-13; the width sweep that
   missed it checked overflow, wrapping and truncation but never asserted the
   brand was still on screen. Any future check of this bar measures the logo
   too. */
.brand{display:inline-flex;align-items:center;min-height:48px;text-decoration:none;flex-shrink:0}
.brand img{height:38px;width:auto;display:block}
@media (min-width:820px){ .brand img{height:46px} }
/* Kim, through Bobbie, 2026-09-23: "a 3 line hamburger navigation in the
   left corner that shows all of the sections/pages". Top left, three lines,
   at every width, and its panel lists every section and page. The row of
   links still shows where a wide screen has room for it. */
.menubtn{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;
  width:48px;min-height:48px;background:transparent;border:1.5px solid var(--neon-dim);
  border-radius:10px;padding:0;cursor:pointer}
.menubtn .bars{display:inline-flex;flex-direction:column;gap:4px}
.menubtn .bars i{display:block;width:20px;height:2px;background:var(--neon)}
.menu{display:none;padding:6px 0 14px}
/* The scroll container itself: given exactly the remaining flex space by
   .bar's column layout above once body.menu-open is set, min-height:0 so a
   flex item is actually allowed to shrink below its content's height (the
   flexbox default is min-height:auto, which is the one-line change that
   silently defeats this whole fix if it is ever dropped), and its own
   overflow-y so IT scrolls rather than the page. overscroll-behavior:contain
   stops a scroll that reaches the panel's own top or bottom from spilling
   onto the page behind it, so the whole page cannot creep while someone is
   just trying to read the last menu row. */
.menu.open{display:block;min-height:0;overflow-y:auto;overscroll-behavior:contain}
/* The floaters are hidden outright while the menu is open (below, body.
   menu-open .sticky-cta/.totop), so this bottom clearance is a second line
   of defense, not the only one, and only needed on a phone: at 700px and up
   the two floaters sit far enough apart from a panel that never gets tall
   enough to reach them for it to matter, and 112px of dead space under a
   wide, roomy menu has no job to do there. */
@media (max-width:699px){
  .menu.open{padding-bottom:calc(112px + env(safe-area-inset-bottom,0px))}
}
.menu a{display:flex;align-items:center;min-height:56px;color:var(--tx);text-decoration:none;
  font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;font-size:15px;letter-spacing:.08em;
  border-top:1px solid var(--line)}
.menu a:hover,.menu a:focus-visible{color:var(--neon);outline:none}
.menu .grp{font-size:10px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;
  color:var(--tx3);font-family:'Archivo',sans-serif;padding:16px 0 4px}
.desknav{display:none}
@media (min-width:1100px){
  /* NO WRAPPING. This used to be flex-wrap:wrap, so eight links broke onto a
     second row and the sticky bar grew to 121px against mobile's 79px. A
     sticky header covers whatever it is over, and at that height it ate the
     top of the hero photo on scroll while mobile looked fine. Bobbie,
     2026-08-12: "it doesn't in mobile but does in desktop". One row is the
     fix; the bar is a navigation strip, not a second hero. */
  .desknav{display:flex;margin-left:auto;gap:0;flex-wrap:nowrap;white-space:nowrap}
  /* The three-line menu button (2026-09-23) takes 60px of the row, which was
     already full: below 1280px the pills ellipsized. The row of links now
     shows only where it fits whole; narrower screens use the menu, which
     has every one of them. */
  .desknav a:not(.navlogin){display:none}
  .desknav a{color:var(--tx2);text-decoration:none;font-size:12px;font-weight:800;letter-spacing:.06em;
    text-transform:uppercase;padding:0 5px;border-radius:999px;display:inline-flex;align-items:center;min-height:44px}
  .desknav a:hover,.desknav a:focus-visible{color:var(--ink);background:var(--neon);outline:none}
  /* The inner width matched the prose column until 1000px, which left the
     links fighting for 780px on a 900px screen. Give the bar the room the
     window already has. */
  .barin{max-width:1060px}
  .bar{padding-top:10px}
  .barrow{min-height:48px}
}
/* The roomier spacing is GONE, deliberately. It made sense when the bar held
   eight links. With ten items it needed 1172px of content inside a .barin that
   is capped at 1060px, which is impossible at any viewport, so the bar simply
   squeezed the logo out of existence on every wide screen. The cap is what
   governs here, not the window: a wider monitor buys the bar nothing. */

/* The bar links that leave this site, so they read as doors rather than more
   sections. Bordered like the Menu button on purpose: that shape already means
   "this does something" on this page.

   The desktop bar must stay ONE row, and the margin below is why min-width:0
   appears TWICE. min-width:0 on the pill alone does nothing: .desknav is
   itself a flex item with the default min-width:auto, so the row refused to
   shrink and overflowed instead of letting the pills ellipsize. An earlier
   version of this comment claimed the pills were "allowed to shrink first",
   which a reviewer disproved by forcing the pressure and watching the row
   overflow at full pill width.

   It matters in one real case: before Archivo loads, or if that woff2 ever
   404s, the fallback font makes this row about 37px wider, which at 1100 to
   1134px is past the viewport. With both declarations the pills give way and
   the bar stays put; without them a horizontal scrollbar appears until the
   font arrives. */
.desknav{min-width:0}
.desknav .navlogin{margin-left:6px;border:1.5px solid var(--neon-dim);border-radius:999px;
  color:var(--neon);padding:0 12px;min-width:0;overflow:hidden;text-overflow:ellipsis}
.desknav .navlogin:hover,.desknav .navlogin:focus-visible{background:var(--neon);color:#08130A}

/* In the thumb menu it is a full-width row like the rest, but tinted, because
   a parent opening the menu to sign in should find it without reading. */
.menu .menulogin{color:var(--neon)}

section{padding:44px 0 4px;border-bottom:1px solid var(--line)}
section:last-of-type{border-bottom:0}
/* ------------------------------------------------------------ two faces
   Anton is a heavy CONDENSED display face. Above about 24px it is the whole
   look of the site; below that its counters close up, the strokes bleed
   together, and neon on black makes it worse. Bobbie, 2026-09-14, on the
   core values labels: "the font is hard to read. can we get more space
   between letters? or use a different font?" Both, in the end:
     - Anton keeps the display sizes (hero, section headings, the ticker,
       the big numbers) and every one of those gains a little tracking.
     - Anything that used to set Anton under about 22px is now Archivo 800,
       uppercase and tracked, which is the label style the page already used
       for pills, badges, buttons and the desktop nav. One system, two faces,
       and the small one is the readable one. */
/* --sec-scale and --sec-color: a block's own textSize and textColor choice
   (siteContent.ts), set inline on its data-block wrapper by
   sectionStyleAttrs() in build.mjs. Both fall back to a no-op: an untouched
   block's --sec-scale is unset so calc() multiplies by 1, and its --sec-color
   is unset so color falls back to whichever token that element already used.
   The two are custom properties, so they inherit down through the whole
   wrapper without every one of these rules needing its own scope check. */
/* A floor under Small (REFUTE_font_R2 finding 6, widened by REFUTE_font_R3
   finding 4): Small's 0.85 multiplier crushed .band .a, .note and .btn to
   10.2px/11.9px/11.9px at 375px, which nobody should have to read in a gym.
   REFUTE_font_R3 rendered every appears-when-filled block, filled, at
   375px/Small, and found five MORE selectors this feature scales that also
   land under 12px and had no floor: .num span and .stat span (the numbers
   strip's labels, 10.625px, the smallest on the whole site), .quote .by
   (11.05px), .gen .gnote (11.9px), .valhead (11.9px, shared by the tuition
   table's "What is included" and Mission's "Our core values"), and .docnote
   (11.9px). Every one of these has a Normal size of 12px or more, so
   max(12px, calc(...)) floors Small without moving Normal at all: at scale 1
   the calc() side already clears 12px and wins.
   www/tests/font-controls-small-floor.test.mjs parses this file for every
   selector whose font-size reads --sec-scale and measures each one for real,
   so a NEW scaled selector under 12px at Small fails that test on its own,
   without anyone having to remember to add it here by hand.
   .p .per and .announce .bndet cannot use a flat 12px floor the same way:
   their OWN Normal size (11.16px and 11.78px, both PRE-EXISTING and nothing
   to do with this feature) already sits under 12px, so a 12px floor there
   would make Small render them BIGGER than Normal, which nobody chose and
   the default-parity test would rightly reject. Floored instead to their
   own Normal size, so Small can never shrink them further, and left short
   of 12px; ask Bobbie whether the Normal baseline itself is worth raising,
   which is a different and older question than this feature. */
h2{margin:0 0 6px;font-family:'Anton',sans-serif;text-transform:uppercase;font-size:calc(var(--sec-scale,1) * clamp(24px,5.4vw,34px));line-height:1.05;letter-spacing:.06em;color:var(--sec-color,var(--tx))}
.sub{color:var(--sec-color,var(--tx2));margin-top:8px;font-size:calc(var(--sec-scale,1) * 1em)}
.card{background:var(--surf);border:1px solid var(--line);border-radius:14px;padding:22px;margin:16px 0}
.card .hd{color:var(--sec-color,var(--tx));font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;font-size:calc(var(--sec-scale,1) * 16px);line-height:1.25;letter-spacing:.08em}
/* Split in two on purpose, and the split is the whole fix.
   `.card p` (0,1,1) ALREADY outranked .lasthd, .note, .docnote, .mission and
   the schedule's own .count/.empty on COLOR before this feature touched
   anything: all six sit inside a .card, so their own color declarations were
   always dead weight and .card p's tx2 was what actually rendered. That is
   baseline behaviour, not a bug this feature gets to fix, and it still
   carries the pick correctly: whenever --sec-color IS set, every one of
   those six reads the SAME custom property .card p reads, so the value is
   identical no matter which rule wins. So the color half keeps .card p's
   original (0,1,1) exactly as it was.
   FONT-SIZE is different: baseline's .card p never set one at all, so those
   six always rendered at their OWN size. Adding a font-size straight onto
   `.card p` (REFUTE_font_R1's finding) made it win at (0,1,1) where it had
   never competed before, silently taking .lasthd from 38/24px, .mission from
   20px, .note and .docnote from 14px, and the schedule's own .count/.empty
   from 13.5/15px, all down to the inherited 17px. `:where(p)` drops the
   element's contribution, landing this at (0,1,0), a tie with those six that
   normal source order (they are declared later in this file) resolves in
   their favour, exactly as baseline's total absence of a rule here did.
   tests/font-controls-default-parity.test.mjs (www/tests) renders the real
   built page against the pre-feature stylesheet and fails if either half of
   this cascade ever drifts again. */
.card p{color:var(--sec-color,var(--tx2));margin-top:12px}
.card :where(p){font-size:calc(var(--sec-scale,1) * 1em)}
.card p:first-child{margin-top:0}
/* Size follows her pick; color deliberately does not. See DECISIONS/the R2
   report for the contrast math: .btn's own color is fixed #08130A text on a
   var(--neon) fill, and Bright IS var(--neon), so a color-following .btn
   would print neon-on-neon the moment she chose the one color that matches
   her own button. min-height stays a fixed 48px floor either way (CLAUDE.md's
   44px tap target rule): the box only ever grows for a larger label, never
   shrinks below it.
   --btn-scale is the block's Button size (Bobbie, 2026-09-24: "i want to be
   able to manage the size of the buttons"), written by sectionStyleAttrs()
   in build.mjs only for Small or Large. Unset it multiplies by 1, so Medium
   computes to exactly the numbers these rules had before it existed. It
   scales the words, the padding and the letter spacing, never min-height,
   so Small stays a full-size tap target. */
.btn{display:inline-flex;align-items:center;background:var(--neon);color:#08130A;font-weight:800;
  text-transform:uppercase;letter-spacing:calc(var(--btn-scale,1) * .06em);font-size:max(12px, calc(var(--sec-scale,1) * var(--btn-scale,1) * 14px));padding:calc(var(--btn-scale,1) * 15px) calc(var(--btn-scale,1) * 20px);border-radius:10px;
  margin-top:16px;min-height:48px;text-decoration:none}
.btn.ghost{background:transparent;color:var(--neon);border:1.5px solid var(--neon-dim)}

/* The opening. Deliberately the largest thing on the page: the first screen
   is the only screen you are guaranteed to get. */
/* The announcement banner. Named .announce, NOT .banner: the competition
   headline has used .banner since the first build, and a bare .banner rule
   here silently restyled it into a green bordered box. Caught by the offline
   half of the end-to-end test, not by looking. Sits above everything, and only exists when Kim
   has written one: there is no empty state to style, because an empty banner
   is no banner. Neon on ink so it reads as "this is happening now" rather
   than as another section of the page, and it takes the full width of the
   column so it cannot be mistaken for a card. */
/* The announcement ticker. It always scrolls, which Bobbie chose knowing the
   cost, and it carries a REAL pause button: WCAG 2.2.2 Pause Stop Hide is
   Level A, and pausing on hover does nothing for a parent on a phone or for
   anyone using a keyboard. */
.announce{margin:14px 0 4px;border:1.5px solid var(--neon-dim);border-radius:14px;
  background:linear-gradient(180deg,rgba(37,209,27,.14),rgba(37,209,27,.05));
  padding:12px 14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.announce .ticker{flex:1 1 220px;overflow:hidden;min-width:0}
.announce .track{display:flex;width:max-content;gap:3rem;animation:tick 26s linear infinite}
.announce .track.paused{animation-play-state:paused}
.announce .bnmsg{margin:0;white-space:nowrap;font-family:'Anton',sans-serif;text-transform:uppercase;
  color:var(--sec-color,var(--neon));font-size:calc(var(--sec-scale,1) * clamp(19px,4.6vw,26px));line-height:1.35;letter-spacing:.06em}
.announce .bndet{color:var(--sec-color,var(--tx));text-transform:none;font-family:inherit;font-size:max(11.78px, .62em);
  letter-spacing:.02em}
.announce .bnside{display:flex;align-items:center;gap:8px;flex:0 0 auto}
.bnpause{display:inline-flex;align-items:center;gap:7px;min-height:44px;padding:0 14px;
  border-radius:999px;border:1.5px solid var(--neon-dim);background:transparent;color:var(--neon);
  font-weight:800;font-size:13px;cursor:pointer}
.bnpause:hover,.bnpause:focus-visible{background:var(--neon);color:#08130A;border-color:var(--neon)}
.pauseglyph{width:10px;height:12px;border-left:3px solid currentColor;border-right:3px solid currentColor}
.bnpause[aria-pressed="true"] .pauseglyph{width:0;height:0;border:6px solid transparent;
  border-left:10px solid currentColor;border-right:0}
@keyframes tick{from{transform:translateX(0)}to{transform:translateX(calc(-50% - 1.5rem))}}
/* Their device asked for less motion. That is their call, not the page's: it
   stops, and the duplicate copy that exists only to hide the loop seam goes. */
@media (prefers-reduced-motion:reduce){
  .announce .track{animation:none;width:auto}
  .announce .track .bnmsg[aria-hidden="true"]{display:none}
  .announce .bnmsg{white-space:normal}
}
/* The banner's button follows the banner's Button size (--btn-scale, see
   .btn): smaller words and less side padding for Small, never under the
   44px min-height. It has no letter spacing of its own to reduce. */
.announce .bnbtn{display:inline-flex;align-items:center;min-height:44px;padding:0 calc(var(--btn-scale,1) * 20px);
  border-radius:10px;background:var(--neon);color:#08130A;font-weight:800;text-decoration:none;
  white-space:nowrap;font-size:max(12px, calc(var(--sec-scale,1) * var(--btn-scale,1) * 1em))}
.announce .bnbtn:hover,.announce .bnbtn:focus-visible{background:var(--tx);outline:none}
@media (min-width:1000px){.announce{padding:14px 20px}}

/* The home page popup (www/lib/popup.mjs, www/src/popup.js). Kim adds and
   removes it in the Hub; it is `hidden` until its script opens it, so a
   visitor without scripts never meets a box they cannot close. It covers the
   page as a fixed layer, which moves nothing underneath it, and sits above
   every floating button (.sticky-cta and .totop are 40, the skip link 60).
   The panel scrolls inside itself, so a small phone can always reach Close.
   `.popup[data-block]` rather than `.popup` because the Hub's preview
   outlines every [data-block] as position:relative, and this has to stay a
   fixed layer there too. */
.popup[data-block]{position:fixed;inset:0;z-index:100;display:flex;align-items:center;justify-content:center;
  padding:16px;touch-action:pinch-zoom}
/* The dark backdrop, said twice so the Hub preview's own hover tint on
   every [data-block] (a pale wash, drawn later in the page) never replaces
   it while the popup is being edited there. */
.popup[data-block],.popup[data-block]:hover{background:rgba(5,6,5,.8)}
.popup[hidden]{display:none}
.popup-panel{position:relative;width:min(100%,520px);max-height:90vh;max-height:90dvh;overflow:auto;
  overscroll-behavior:contain;touch-action:pan-y pinch-zoom;background:var(--surf);border:1.5px solid var(--neon-dim);border-radius:16px;
  padding:0 20px 22px;box-shadow:0 18px 50px rgba(0,0,0,.6);color:var(--tx)}
.popup-panel:focus{outline:none}
.popup-top{position:sticky;top:0;z-index:1;display:flex;justify-content:flex-end;padding:12px 0 8px;
  background:var(--surf)}
.popup-close{min-height:44px;min-width:44px;padding:0 16px;border-radius:999px;border:1.5px solid var(--neon-dim);
  background:var(--ink);color:var(--tx);font:inherit;font-weight:800;font-size:15px;cursor:pointer}
.popup-close:hover,.popup-close:focus-visible{background:var(--neon);color:#08130A;border-color:var(--neon);outline:none}
.popup-fig{margin:0 0 14px}
.popup-fig img{display:block;width:100%;height:auto;max-height:50vh;max-height:50dvh;object-fit:contain;border-radius:10px}
.popup-hd{margin:0 0 8px;font-family:'Anton',sans-serif;text-transform:uppercase;letter-spacing:.05em;line-height:1.1;
  overflow-wrap:anywhere;
  font-size:calc(var(--sec-scale,1) * clamp(24px,6vw,32px));color:var(--sec-color,var(--tx))}
.popup-msg{margin:0;white-space:pre-line;overflow-wrap:anywhere;font-size:max(12px, calc(var(--sec-scale,1) * 17px));line-height:1.5;
  color:var(--sec-color,var(--tx2))}
.popup-hd:empty,.popup-msg:empty{display:none}
.popup .popup-btn{margin-top:18px}
/* .btn's own display would otherwise beat [hidden] on the empty button
   placeholder an Off or button-less popup carries for the Hub preview. */
.popup .popup-btn[hidden]{display:none}
@media (prefers-reduced-motion:no-preference){
  .popup:not([hidden]) .popup-panel{animation:popin .22s ease-out}
}
@keyframes popin{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}

/* Every price in the studio's own tuition sheet is per month. Without this
   the table read as $50 and a parent could reasonably think per class. */
/* Schedule rows carry a stripe in their own style's hue, so the table reads
   as a set of different dances rather than 66 grey lines. Hue comes from the
   row; saturation and lightness are fixed here, which keeps every color on
   the page inside one palette. */
/* The day of the week as a heading across the table, not a column repeated on
   every one of 66 rows. Bobbie: "it says, like, Wednesday, larger and not
   listed every time." Sticky under the sticky column header so you always know
   which day you are looking at while scrolling. */
/* Back to top. Bottom LEFT, because the Get started pill already owns the
   bottom right and a floating control that covers another floating control is
   worse than no floating control. Appears only once there is something to go
   back up from. */
.totop{position:fixed;left:14px;bottom:16px;z-index:40;display:inline-flex;
  align-items:center;gap:6px;min-height:44px;padding:0 16px;border-radius:999px;
  background:var(--tx);border:1.5px solid var(--tx);color:#08130A;
  text-decoration:none;font-weight:800;font-size:15px;
  box-shadow:0 6px 20px rgba(0,0,0,.55)}
.totop[hidden]{display:none}
.totop:hover,.totop:focus-visible{background:var(--neon);color:#08130A;border-color:var(--neon)}

/* One block per day, because the day headings used to be rows inside a single
   sixty-six row table and a parent scrolling saw one unbroken wall. Bobbie:
   "it is too long of a giant list right now". Four short lists read as four
   lists; the same rows in one table read as a phone book. */
/* The access pill explains itself. Bobbie: "when you hover over a class that
   says invite explain what that is in a small hover pop up."

   It is a BUTTON, not a span with a title attribute, for two reasons a parent
   would feel: a native title never appears on a phone, where most of this site
   is read, and it cannot be reached by keyboard. As a button it opens on tap,
   on hover and on focus, and the sentence itself is real text in the HTML, so
   a screen reader and a crawler both get it whether it is open or not. */
/* The address, with the two things a parent standing on the fence needs: that
   they can just turn up, and when. The hours are the studio's own words from
   the contact page, kept term-time so they do not read as a year-round promise. */
/* One spot, several photos, rotating. The first slide is a real img in the
   HTML so the first paint is a photo and not a blank box that fills in later,
   and every other slide is in the HTML too so a crawler sees all of them. */
/* The five sections that had a box in the editor and no home on the page.
   Every one of them renders only when Kim has put something real in it, so
   none of these rules ever paints an empty frame. */
.wrapnarrow{max-width:var(--wrap,1100px);margin:0 auto;padding:0 var(--pad,18px)}
/* Registration. The one section with a deadline, so it reads as a notice
   rather than as more page. It removes itself after the last date. */
.reg{margin-top:22px;border-color:var(--neon-dim)}
.reg h2{color:var(--neon)}
.reggroup{margin-top:18px;border-left:3px solid var(--neon);padding-left:14px}
.reghd{margin:0 0 4px;font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;letter-spacing:.08em;
  color:var(--tx);font-size:clamp(15px,3.8vw,17px)}
.reglead{margin:8px 0 4px;color:var(--tx2);font-size:14.5px}
.regday{display:flex;flex-wrap:wrap;gap:4px 12px;align-items:baseline;padding:7px 0;
  border-bottom:1px solid var(--line)}
.regday:last-of-type{border-bottom:0}
.regwho{color:var(--neon);font-weight:800;font-size:14.5px;min-width:96px}
.regwhen{color:var(--tx);font-size:15.5px}
.reg .btn{margin-top:18px}
.numstrip{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px}
.num{flex:1 1 130px;background:var(--surf2);border:1px solid var(--line);border-radius:12px;
  padding:16px 14px;text-align:center}
.num b{display:block;font-family:'Anton',sans-serif;text-transform:uppercase;letter-spacing:.06em;
  color:var(--sec-color,var(--neon));font-size:calc(var(--sec-scale,1) * clamp(23px,6vw,32px));line-height:1}
.num span{display:block;color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 12.5px));margin-top:7px}

.quotes{position:relative;padding-bottom:34px}
.quotes .quote[hidden]{display:none}
.quotes .dots{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:center;gap:8px}
figure.quote{margin:0}
blockquote.q{margin:0}

.lastcard{margin-top:26px;text-align:center}
.lasthd{font-family:'Anton',sans-serif;text-transform:uppercase;letter-spacing:.05em;color:var(--sec-color,var(--tx));
  font-size:calc(var(--sec-scale,1) * clamp(24px,6.4vw,38px));line-height:1.05;margin:0 0 10px}

.promise{margin-top:18px;color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * clamp(16px,4.1vw,19px));line-height:1.45;
  border-left:3px solid var(--neon);padding-left:14px;max-width:52ch}

/* An editable photo spot with nothing to show yet: no photo published, and no
   built-in picture either (a staff card with no photo, the values picture, a
   dance style the site has no photo for, an empty photo strip or slideshow).
   Kept in the HTML, invisible, so the Hub's preview has a real element to
   stand her first pick in the moment she chooses one; a family visiting the
   live site never sees it, because only the sandboxed preview script (which
   the live build never runs) ever removes the `hidden` attribute that goes
   with it. Bobbie, 2026-09-15: "Why can't it show the photos etc in the
   preview?"

   Keyed on BOTH attributes, not `[data-photo-empty]` alone: the preview
   script only ever removes `hidden` (see sitePreview.ts's applyPhotos), it
   never removes `data-photo-empty` itself, so a rule keyed on the marker
   attribute alone never let the un-hidden element show. Refute round 1,
   2026-09-17: proven with the real built page in Chromium, a photo url
   posted exactly as PreviewPanel posts it, the wrapper's `hidden` attribute
   confirmed removed and the box still zero height because of this rule. */
[data-photo-empty][hidden]{display:none}
[data-until][hidden]{display:none}

.shots{position:relative;margin-top:22px;border-radius:14px;overflow:hidden;background:var(--surf2)}
.shots .slide{margin:0}
.shots .slide[hidden]{display:none}
.shots .slide img{display:block;width:100%;height:auto}
.shots figcaption{padding:10px 14px;color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 14px)}
.shots .dots{position:absolute;left:0;right:0;bottom:10px;display:flex;justify-content:center;gap:8px}
/* 44px of tap target around a 10px dot, because a dot you cannot hit is not a
   control. The visible dot is drawn by the inner box shadow. */
.shots .dot{width:44px;height:44px;padding:0;border:0;background:transparent;cursor:pointer;
  border-radius:999px;box-shadow:inset 0 0 0 5px rgba(255,255,255,.55);
  background-clip:content-box;box-sizing:border-box;
  border:17px solid transparent}
.shots .dot[aria-current="true"]{box-shadow:inset 0 0 0 5px var(--neon)}

.vidwrap{margin-top:22px;border-radius:14px;overflow:hidden;background:#000;aspect-ratio:16/9}
.vid{display:block;width:100%;height:100%;border:0}

.addr{margin-top:18px;border-left:3px solid var(--neon);padding-left:14px}
.addr p{margin:0 0 4px}
.addr .addrname{color:var(--sec-color,var(--tx));font-weight:800}
.addr .hours{color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14.5px))}
footer .fhours{color:var(--tx2);font-size:14px;margin-top:2px}

.pillwrap{position:relative;display:inline-block}
button.pill{font:inherit;cursor:help;border:1px solid currentColor;background:transparent}
.pilltip{position:absolute;left:0;top:calc(100% + 6px);z-index:30;width:max-content;max-width:min(78vw,300px);
  padding:10px 12px;border-radius:10px;background:var(--surf2);border:1px solid var(--neon-dim);
  color:var(--tx);font-size:13.5px;line-height:1.4;font-weight:500;text-transform:none;letter-spacing:0;
  box-shadow:0 10px 30px rgba(0,0,0,.6);
  opacity:0;visibility:hidden;transition:opacity .12s}
.pillwrap:hover .pilltip,.pillwrap:focus-within .pilltip,.pilltip.open{opacity:1;visibility:visible}
@media (prefers-reduced-motion:reduce){.pilltip{transition:none}}

.dayblock{margin-top:26px}
.dayblock:first-child{margin-top:8px}
.dayblock[hidden]{display:none}
.dayname{margin:0 0 10px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
  font-family:'Anton',sans-serif;text-transform:uppercase;letter-spacing:.07em;
  font-size:calc(var(--sec-scale,1) * clamp(24px,6.2vw,32px));line-height:1.05;color:var(--sec-color,var(--neon))}
/* Archivo by name. This used to say var(--font-body,inherit), and no such
   variable exists in this file, so "18 classes" inherited Anton at 13px. */
.dayname .daycount{font-family:'Archivo',sans-serif;font-size:max(12px, calc(var(--sec-scale,1) * 13px));font-weight:700;
  letter-spacing:.04em;color:var(--sec-color,var(--tx2));text-transform:none}

/* Workshops are the one thing on the page anybody can join without enrolling,
   so the line saying so is not fine print. */
.openall{color:var(--sec-color,var(--tx));font-weight:600;border-left:3px solid var(--neon);
  padding-left:12px;margin-top:14px;font-size:calc(var(--sec-scale,1) * 1em)}

/* "New to the studio" cards. Each takes its own hue so the section reads as a
   set of open doors rather than a wall of small print, and the question is the
   loud half because the question is what a nervous parent is scanning for. */
.adultgrid{display:grid;gap:10px;margin:16px 0}
@media (min-width:760px){.adultgrid{grid-template-columns:repeat(3,1fr)}}
.adultgrid .fq{border-left:4px solid hsl(var(--hue,140) 60% 50% / .9);
  background:hsl(var(--hue,140) 45% 50% / .08)}
.adultgrid .fq b{color:hsl(var(--hue,140) 75% 76%)}
#adults .startform{margin-top:18px}

.growth .fq{border-left:4px solid hsl(var(--hue,140) 60% 50% / .9);
  background:hsl(var(--hue,140) 45% 50% / .08)}
.growth .fq:hover{background:hsl(var(--hue,140) 45% 50% / .14);
  border-color:hsl(var(--hue,140) 70% 60%)}
/* The hue is a deliberate rainbow across these cards (spirit's rotating
   values, "New to the studio"'s growth-mindset colors), a different design
   from the rest of the site's, so it is kept even when she sets a text
   color: only the SIZE follows her choice here, the color stays the hue. */
.growth .fq b{color:hsl(var(--hue,140) 75% 76%);font-size:calc(var(--sec-scale,1) * 16px);line-height:1.3}
.growth h2{color:var(--sec-color,var(--neon))}
.growth .sub{color:var(--sec-color,var(--tx))}

.crow{border-left:5px solid hsl(var(--hue,140) 60% 50% / .95);
  background:hsl(var(--hue,140) 45% 50% / .07)}
.crow:hover{background:hsl(var(--hue,140) 45% 50% / .13)}
.crow td{border-bottom:1px solid rgba(255,255,255,.06)}
.crow td.nm{font-weight:700;color:var(--sec-color,var(--tx))}
.crow td.t{color:hsl(var(--hue,140) 70% 72%);font-weight:700;white-space:nowrap}
.crow .pill{background:hsl(var(--hue,140) 60% 50% / .22);
  border-color:hsl(var(--hue,140) 60% 60% / .55);color:hsl(var(--hue,140) 75% 80%)}
/* The style itself, as a word in its own color. The table is the one place a
   parent scans rather than reads, and a color they can learn is faster than
   a name they have to read twice. */
.crow .sty{display:inline-block;margin-left:8px;font-size:11px;font-weight:800;
  letter-spacing:.08em;text-transform:uppercase;color:hsl(var(--hue,140) 75% 74%);
  background:hsl(var(--hue,140) 60% 50% / .16);border-radius:999px;padding:3px 9px;
  white-space:nowrap}

/* "What is this kind of dance?" - the class name becomes a button that opens
   the studio's own description of that genre underneath. Styled as text with
   a small marker rather than as a button, because a table of 66 buttons reads
   as a wall of controls. */
.whatis{font:inherit;color:var(--sec-color,var(--tx));background:none;border:0;padding:0;margin:0;
  text-align:left;cursor:pointer;display:inline-flex;align-items:baseline;gap:6px;
  min-height:44px}
.whatis .q{display:inline-flex;align-items:center;justify-content:center;
  width:17px;height:17px;border-radius:50%;border:1px solid var(--neon-dim);
  color:var(--neon);font-size:11px;font-weight:800;flex:none}
.whatis:hover,.whatis:focus-visible{color:var(--neon)}
.whatis[aria-expanded="true"] .q{background:var(--neon);color:#08130A;border-color:var(--neon)}
.whatrow td{background:var(--surf2);color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14px));line-height:1.55;
  padding:12px 14px 16px}
.whatrow b{color:var(--neon);font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;
  font-size:12.5px;letter-spacing:.1em;margin-right:6px}

.noage{margin:10px 0 0;padding:12px 14px;border:1px solid var(--line);border-radius:10px;
  background:rgba(255,255,255,.03);color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14px));line-height:1.5}

.p .per{color:var(--sec-color,var(--tx3));font-weight:500;font-size:max(11.16px, .72em);margin-left:2px}

.hero{position:relative;overflow:hidden;border-radius:16px;margin:16px 0;background:var(--surf);
  border:1px solid var(--line)}
/* The photo box. It takes the photo's own shape (build.mjs writes it in as
   --hero-ratio, held between square and 16:9; 4:3 if it could not be read)
   instead of forcing a fixed 230 to 400px band. That band was fine on a
   phone, where it is close to 4:3 anyway, but on a desktop it stretched to
   the full 1060px column and showed 42% of the photo's height. The girls in
   the back row lost their heads. Bobbie, 2026-09-14: "the photo is getting
   cut off on the website".
   The height cap is the OLD band, kept on purpose: it is what keeps the
   buttons on the first screen of a tablet, and on a landscape phone it stops
   the photo from being the whole screen. Phones get a little more room below.
   WIDTH IS SET EXPLICITLY, and that is load bearing. With width:auto a
   max-height on an aspect-ratio box transfers into a max-WIDTH, so the box
   SHRANK instead of cropping: an adversarial pass measured a 322px photo
   tile in a 744px hero on a landscape phone, and a dark strip down the right
   edge on an iPad. A set width makes the cap crop the height, which is what
   object-fit:cover is for.
   The box also OWNS the drift, the twinkles and the spotlight (below), and it
   clips them, so none of that decoration can cross the headline. */
.hero .hphoto{position:relative;overflow:hidden;width:100%;aspect-ratio:var(--hero-ratio,4/3);
  max-height:clamp(230px,35vh,400px);background:var(--surf2)}
@media (max-width:640px){ .hero .hphoto{max-height:clamp(230px,44vh,420px)} }
.hero .hphoto>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  object-position:50% 34%;display:block}
.hero .htext{padding:26px 22px 30px}
.hero h1{margin:0;font-family:'Anton',sans-serif;text-transform:uppercase;color:var(--sec-color,var(--tx));
  font-size:calc(var(--sec-scale,1) * clamp(40px,11.6vw,78px));line-height:.95;letter-spacing:.035em}
/* .lite and .hsub b are the hero's own two-tone accent, a deliberate second
   color on top of the headline's own; they keep the neon accent regardless
   of her text color choice, the same way .growth .fq b keeps its hue. */
.hero h1 .lite{color:var(--neon)}
.hero .h2line{color:var(--sec-color,var(--tx2));font-family:'Anton',sans-serif;text-transform:uppercase;
  font-size:calc(var(--sec-scale,1) * clamp(20px,5.4vw,30px));line-height:1.18;max-width:22ch;
  margin-top:14px;letter-spacing:.07em}
.hero .hsub{color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * clamp(19px,5.1vw,27px));line-height:1.34;margin-top:20px;
  max-width:28ch;font-weight:500}
.hero .hsub b{color:var(--neon);font-weight:800}
.hero .cta{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px}
.hero .btn{font-size:max(12px, calc(var(--sec-scale,1) * var(--btn-scale,1) * 15px));padding:calc(var(--btn-scale,1) * 16px) calc(var(--btn-scale,1) * 22px);margin-top:0}
/* Once the column is 1060px wide the photo goes BESIDE the words rather than
   above them. Stacked, the only way to keep the headline on the first screen
   was to crop the photo to a letterbox; side by side, the photo box is close
   to square, a 4:3 photo shows its full height, and the headline is still
   the first thing on screen. The headline steps down a little so it can sit
   in half the width without every word breaking onto its own line. */
@media (min-width:1000px){
  .hero{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:stretch}
  .hero .hphoto{aspect-ratio:auto;max-height:none;min-height:460px;height:100%}
  .hero .htext{padding:36px 32px 38px;align-self:center}
  .hero h1{font-size:calc(var(--sec-scale,1) * clamp(52px,6.2vw,66px))}
  .hero .h2line{font-size:calc(var(--sec-scale,1) * 26px)}
  .hero .hsub{font-size:calc(var(--sec-scale,1) * 22px)}
}

.quote{border-left:3px solid var(--neon);padding:2px 0 2px 18px;margin-top:18px}
.quote .q{font-family:'Anton',sans-serif;text-transform:uppercase;color:var(--sec-color,var(--tx));
  font-size:calc(var(--sec-scale,1) * clamp(21px,5.3vw,31px));line-height:1.12;letter-spacing:.06em}
.quote .by{font-size:max(12px, calc(var(--sec-scale,1) * 13px));color:var(--sec-color,var(--neon));letter-spacing:.1em;text-transform:uppercase;font-weight:800;margin-top:12px}
/* The mission used to be one short line, and the big uppercase display face
   suited it. Kim's current mission, sent 2026-08-13, is a full two-sentence
   paragraph, and the same treatment turned it into eight lines of shouting
   capitals that nobody reads. Her own sheet sets it in sentence case, so this
   keeps the neon and the weight to mark it as the statement it is, and drops
   the uppercase and the display face that only worked at one line. */
.mission{color:var(--sec-color,var(--neon));font-size:calc(var(--sec-scale,1) * clamp(17px,2.6vw,20px));line-height:1.45;
  margin-top:8px;font-weight:700}

.bands{display:grid;grid-template-columns:1fr;gap:10px;margin-top:14px}
/* Age cards are buttons now: they set the schedule filter and take you there. */
.band{font:inherit;text-align:left;cursor:pointer;color:var(--sec-color,inherit);width:100%;display:block}
.band .go{display:inline-flex;align-items:center;min-height:34px;margin-top:10px;
  color:var(--neon);font-size:13px;font-weight:800;letter-spacing:.06em;text-transform:uppercase}
.band .go::after{content:'\2192';margin-left:7px;transition:transform .25s ease}
.band:hover .go::after,.band:focus-visible .go::after{transform:translateX(4px)}
.band:focus-visible{outline:2px solid var(--neon);outline-offset:3px}
.band{background:var(--surf2);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.band img{width:100%;height:170px;object-fit:cover;
  /* Default suits the landscape photos. A portrait one sets its own via the
     inline --focus that build.mjs emits, because 22% down a tall photo lands
     above the faces. */
  object-position:50% var(--focus,22%);
  margin-bottom:10px;border-radius:9px;display:block}
.band .n{font-family:'Archivo',sans-serif;font-weight:800;font-size:calc(var(--sec-scale,1) * 16.5px);text-transform:uppercase;letter-spacing:.07em}
.band .a{color:var(--sec-color,var(--neon));font-size:max(12px, calc(var(--sec-scale,1) * 12px));font-weight:800;letter-spacing:.1em;text-transform:uppercase}
.band p{color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 15px);margin-top:5px}

/* Bobbie's invitation line. It answers the access pills a parent meets over
   and over further down the page, so it gets to be as loud as they are. */
.invite{background:var(--surf);border:1px solid var(--neon-dim);border-left:4px solid var(--neon);
  border-radius:14px;padding:24px 22px 26px;margin-top:18px}
.invite .lead{font-family:'Anton',sans-serif;text-transform:uppercase;color:var(--sec-color,var(--tx));
  font-size:calc(var(--sec-scale,1) * clamp(26px,7vw,42px));line-height:1.02;letter-spacing:.045em}
.invite .body{color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * clamp(17px,4.3vw,22px));line-height:1.4;margin-top:16px;max-width:40ch}

.firstgrid{display:grid;grid-template-columns:1fr;gap:9px;margin-top:14px}
.fq{background:var(--surf2);border:1px solid var(--line);border-radius:12px;padding:15px 16px}
.fq b{display:block;color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * 16.5px);line-height:1.3}
.fq p{color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 15px);margin-top:6px}
/* An odd number of cards leaves the last one stranded in a two column
   grid, so it takes the full width instead. */
.firstgrid .fq:last-child:nth-child(odd){grid-column:1/-1}

.genlist{display:grid;grid-template-columns:1fr;gap:8px;margin-top:14px}
.gen{background:var(--surf2);border:1px solid var(--line);border-radius:12px;overflow:hidden}
/* Anton is a CONDENSED face, so uppercase at 18px set almost letter-to-letter
   and read as a block rather than as words. Bobbie flagged it twice, and
   tracking it wider helped but did not fix it. It is the label face now, like
   every other small uppercase line on the page. See the note above h2. */
.gen summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:56px;
  padding:10px 16px;font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;
  font-size:calc(var(--sec-scale,1) * 15px);letter-spacing:.07em;color:var(--sec-color,var(--tx))}
.gen summary::-webkit-details-marker{display:none}
.gen summary .sp{margin-left:auto;color:var(--neon);font-family:'Archivo',sans-serif;font-size:24px;font-weight:700;line-height:1}
.gen summary .sp::after{content:'+'}
.gen[open] summary .sp::after{content:'\2212'}
.gen[open] summary{border-bottom:1px solid var(--line)}
.gen .body{padding:14px 16px 16px;color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 15.5px)}
.gen .body img{width:100%;height:190px;object-fit:cover;object-position:50% 25%;
  margin-bottom:12px;border-radius:9px;display:block}
.gen .gnote{color:var(--sec-color,var(--tx3));font-size:max(12px, calc(var(--sec-scale,1) * 14px));margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}
.cta2{display:flex;flex-wrap:wrap;gap:10px;margin-top:6px}
.cta2 .btn{margin-top:10px}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.chips span,.chips a{text-decoration:none;font-size:14px;font-weight:700;background:var(--surf2);border:1px solid var(--neon-dim);
  border-radius:999px;padding:9px 14px;color:var(--neon)}
.badge{font-family:'Archivo',sans-serif;font-size:10px;font-weight:800;letter-spacing:.1em;
  text-transform:uppercase;color:var(--neon);border:1px solid var(--neon-dim);border-radius:999px;padding:3px 8px}

.filters{display:grid;gap:10px;margin-bottom:4px}
.frow{display:flex;flex-wrap:wrap;gap:7px;align-items:center}
.flab{font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--tx3);width:100%}
.chip{font-family:'Archivo',sans-serif;font-size:max(12px, calc(var(--sec-scale,1) * 13px));font-weight:700;background:var(--surf2);
  border:1px solid var(--line);border-radius:999px;color:var(--tx2);padding:0 14px;min-height:44px;cursor:pointer}
.chip[aria-pressed="true"]{background:var(--neon);border-color:var(--neon);color:#08130A}
.empty{color:var(--sec-color,var(--tx3));font-size:calc(var(--sec-scale,1) * 15px);padding:14px 8px}
.count{color:var(--sec-color,var(--tx3));font-size:max(12px, calc(var(--sec-scale,1) * 13.5px));margin-top:14px;font-weight:700;letter-spacing:.04em}
#sched td.d,#sched td.t{white-space:nowrap;color:var(--sec-color,var(--tx2))}
#sched td.tch{color:var(--sec-color,var(--tx3));font-size:max(12px, calc(var(--sec-scale,1) * 14px));white-space:nowrap}
#sched tbody tr:hover{background:var(--surf2)}
.tablewrap{max-height:min(70vh,640px);overflow:auto}
.tablewrap thead th{position:sticky;top:0;background:var(--surf);z-index:1}

/* DECISIONS 33 item 2 named the tuition table by name: its rows are the
   whole visible content of that block, not chrome, so the price label, the
   amount and "what is included" all follow the pick, same as any other
   block's body text. th has no color/scale wrap: the tuition table has no
   <thead> at all (its own rows are the only content), so th here is only
   ever the schedule's column labels. Those keep their own fixed small size
   on purpose, even though the schedule is now its own style-only block
   (classSchedule): a column label is chrome, and the guide lists it as an
   exception. */
table{border-collapse:collapse;width:100%;margin-top:12px;font-size:calc(var(--sec-scale,1) * 15.5px)}
th,td{text-align:left;padding:9px 8px;border-bottom:1px solid var(--line)}
th{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--tx3);font-weight:800}
td{color:var(--sec-color,var(--tx))}
td.p{text-align:right;font-variant-numeric:tabular-nums;color:var(--sec-color,var(--neon));font-weight:700;white-space:nowrap}
.tablewrap{overflow-x:auto}
.note{color:var(--sec-color,var(--tx3));font-size:max(12px, calc(var(--sec-scale,1) * 14px));margin-top:10px}
.pill{display:inline-block;font-size:10px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
  padding:3px 8px;border-radius:999px;background:#2A2410;color:#FFD84D;border:1px solid #5C4E12}

/* Competition. The one block allowed to shout. */
/* height:auto is load bearing, not tidiness. Photos carry width and height
   attributes so they reserve their space before loading (see jpegSize in
   build.mjs). An HTML height attribute becomes the USED height wherever author
   CSS sets width but not height, so without this the photo renders
   width:100% by attribute-height and stretches: the mission photo came out
   308x675 instead of 308x231 on a phone, faces and all. Any new rule that
   sets width on a photo needs a height with it. */
.comp img{width:100%;height:auto;border-radius:12px;border:1px solid var(--line);display:block}
.comp .banner{font-family:'Anton',sans-serif;text-transform:uppercase;color:var(--sec-color,var(--tx));
  font-size:calc(var(--sec-scale,1) * clamp(34px,9.4vw,62px));line-height:.94;margin-top:18px;letter-spacing:.04em}
/* .lite is the same deliberate two-tone accent as the hero's, kept regardless
   of her text color choice. */
.comp .banner .lite{color:var(--neon)}
.comp .bragsub{color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * clamp(16px,4.1vw,21px));font-weight:600;margin-top:12px}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:20px}
.stat{background:var(--ink);border:1px solid var(--neon-dim);border-radius:12px;padding:14px 13px;
  transition:transform .4s ease,border-color .4s ease}
.stat:hover{transform:translateY(-3px);border-color:var(--neon)}
.stat b{display:block;font-family:'Anton',sans-serif;font-weight:400;text-transform:uppercase;
  color:var(--sec-color,var(--neon));font-size:calc(var(--sec-scale,1) * clamp(23px,6vw,32px));line-height:1;letter-spacing:.06em}
.stat span{display:block;color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 12.5px));margin-top:7px}

.pgrid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
.pgrid figure{margin:0}
.pgrid img{width:100%;height:150px;object-fit:cover;object-position:50% 25%;
  border-radius:12px;border:1px solid var(--line);display:block}
.pgrid figcaption{color:var(--tx3);font-size:12px;margin-top:6px}
.card.prog>img{height:230px;object-fit:cover;object-position:50% 28%}
@media (min-width:640px){ .pgrid{grid-template-columns:1fr 1fr 1fr} .card.prog>img{height:300px} }

.card>img{width:100%;height:auto;border-radius:12px;border:1px solid var(--line);margin-bottom:16px;display:block}

/* One action that is always reachable. On a long page a parent should never
   have to hunt for how to start. Hidden once they reach the contact form,
   since offering to take them somewhere they already are is noise. */
.sticky-cta{position:fixed;left:50%;transform:translateX(-50%);bottom:calc(env(safe-area-inset-bottom,0px) + 14px);
  z-index:40;background:var(--neon);color:#08130A;font-weight:800;text-transform:uppercase;
  letter-spacing:.06em;font-size:14px;padding:0 26px;min-height:52px;border-radius:999px;
  display:inline-flex;align-items:center;text-decoration:none;
  box-shadow:0 10px 26px rgba(0,0,0,.55);transition:opacity .3s ease,transform .3s ease}
.sticky-cta[hidden]{display:none}
.sticky-cta.away{opacity:0;pointer-events:none;transform:translateX(-50%) translateY(14px)}

/* Belt and suspenders with the menu's own scroll fix above: while the menu is
   open (site.js sets this on <body>), both fixed floaters step out of the
   way entirely, so nothing can ever sit on top of the open panel regardless
   of scroll position or viewport quirks. */
body.menu-open .sticky-cta,body.menu-open .totop{display:none}

.startform{margin-top:18px;display:grid;gap:6px}
.startform label{color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14px));font-weight:700;margin-top:10px}
.startform .opt{color:var(--sec-color,var(--tx3));font-weight:400}
.startform input,.startform textarea{background:var(--ink);border:1px solid var(--line);
  border-radius:10px;color:var(--tx);font:inherit;font-size:16px;padding:13px 14px;min-height:48px}
.startform input:focus,.startform textarea:focus{outline:2px solid var(--neon);outline-offset:1px;border-color:var(--neon-dim)}
.startform textarea{min-height:88px;resize:vertical}
.startform .btn{justify-self:start;border:0;cursor:pointer}

footer{padding:34px 0 60px;color:var(--tx3);font-size:14px;line-height:1.7}
footer a{color:var(--tx2)}
/* <address> is italic by default; the footer address reads like the rest of the footer. */
footer address{font-style:normal}
footer .fname{color:var(--tx);font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;font-size:15px;letter-spacing:.08em}

/* ---------------------------------------------------------------- motion
   Everything here is decoration and must behave like it. Nothing may leave
   content hidden, nothing may move the layout, and the whole block is off
   under prefers-reduced-motion. */

/* Cards arrive in sequence rather than all at once. The stagger is what makes
   it read as designed instead of as a page still loading. */
.rise{opacity:0;transform:translateY(18px);
  transition:opacity .55s cubic-bezier(.22,.61,.36,1),transform .55s cubic-bezier(.22,.61,.36,1);
  transition-delay:calc(var(--i,0) * 70ms)}
.rise.in{opacity:1;transform:none}

/* Photos wipe open instead of appearing. Paired with the fade so a failed
   observer can never leave a photo clipped to nothing. */
.reveal img{clip-path:inset(0 0 12% 0);transform:scale(1.04);
  transition:clip-path .8s cubic-bezier(.22,.61,.36,1),transform .8s cubic-bezier(.22,.61,.36,1)}
.reveal.in img{clip-path:inset(0 0 0 0);transform:none}

/* The opening photo drifts, very slowly. 24s is long enough that you feel it
   rather than watch it. */
@keyframes drift{from{transform:scale(1.02)}to{transform:scale(1.11)}}
.hero .hphoto>img{animation:drift 24s ease-in-out infinite alternate}

.card,.band,.fq,.gen{transition:transform .35s ease,border-color .35s ease}
.band:hover,.fq:hover{transform:translateY(-3px);border-color:var(--neon-dim)}
.gen:hover{border-color:var(--neon-dim)}
.chips span,.chips a{transition:transform .25s ease,background .25s ease,color .25s ease}
.chips span:hover,.chips a:hover{transform:translateY(-2px);background:var(--neon);color:#08130A}
.btn{transition:transform .2s ease,filter .2s ease}
.btn:hover{transform:translateY(-2px);filter:brightness(1.08)}
.brand b{transition:opacity .3s ease}

/* The "cut out" look, without cutting anything out. Real background removal
   needs a matting tool this environment does not have, and a bad matte around
   hair looks worse than no matte at all. Instead the edges dissolve into the
   page, which on a black ground reads the same way and works on any photo. */
.melt{-webkit-mask-image:radial-gradient(120% 96% at 50% 42%,#000 52%,transparent 96%);
  mask-image:radial-gradient(120% 96% at 50% 42%,#000 52%,transparent 96%);border:0!important}
.melt.tall{-webkit-mask-image:linear-gradient(#000 62%,transparent 99%);
  mask-image:linear-gradient(#000 62%,transparent 99%)}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .rise,.reveal img{opacity:1!important;transform:none!important;transition:none!important;
    clip-path:none!important;transition-delay:0s!important}
  .hero .hphoto>img{animation:none}
  .card,.band,.fq,.gen,.chips span,.chips a,.btn{transition:none}
  .band:hover,.fq:hover,.chips span:hover,.chips a:hover,.btn:hover{transform:none}
}

/* ------------------------------------------------------- cutout dancers
   Real mattes, made with rembg. Only photos with genuine separation get one:
   a bad matte round a child's hair is worse than no matte. The silks shot was
   attempted and rejected because the matte removed the silks. */
.cut{position:relative;display:flex;justify-content:center;align-items:flex-end;
  min-height:300px;margin:10px 0 -10px}
.cut::before{content:'';position:absolute;inset:auto 0 8% 0;height:74%;
  background:radial-gradient(60% 60% at 50% 60%,rgba(37,209,27,.28),transparent 70%);
  filter:blur(6px);pointer-events:none}
.cut img{position:relative;max-height:340px;width:auto;filter:drop-shadow(0 18px 30px rgba(0,0,0,.6))}
@media (min-width:820px){
  .invitewrap{display:grid;grid-template-columns:1fr 300px;gap:18px;align-items:center}
  .cut{margin:0;min-height:360px}
  .cut img{max-height:400px}
}

/* Hero headline arrives a line at a time. */
.hero h1 .ln{display:inline-block;opacity:0;transform:translateY(.32em);
  transition:opacity .6s ease,transform .6s cubic-bezier(.22,.61,.36,1)}
.hero h1 .ln:nth-child(3){transition-delay:.14s}
.ready .hero h1 .ln{opacity:1;transform:none}

/* A slow spotlight over the opening photo, so the black is not flat. It
   lives on the photo box, not the hero, so it never crosses the headline:
   see the note on .hphoto. */
.hero .hphoto::after{content:'';position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(50% 40% at 22% 18%,rgba(37,209,27,.16),transparent 70%);
  animation:sweep 18s ease-in-out infinite alternate}
@keyframes sweep{from{transform:translate3d(-6%,0,0)}to{transform:translate3d(10%,4%,0)}}

/* Buttons catch the light on hover. */
.btn{position:relative;overflow:hidden}
.btn::after{content:'';position:absolute;top:0;left:-140%;width:60%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.35),transparent);
  transition:left .55s ease}
.btn:hover::after{left:140%}

/* A light sweep crosses a gallery photo once as it arrives, and again on
   hover. The sweep is a moving background on a pseudo element over the photo,
   never the photo itself, so a browser that ignores it loses nothing and no
   image is ever altered. Moving a background position rather than an element
   means there is nothing to clip, so the figure keeps its caption visible.
   The heights below track .pgrid img and have to move with it. */
.pgrid figure{position:relative}
.pgrid figure::after{content:'';position:absolute;top:0;left:0;right:0;height:150px;
  pointer-events:none;border-radius:12px;
  background-image:linear-gradient(100deg,transparent 42%,rgba(255,255,255,.26) 50%,transparent 58%);
  background-repeat:no-repeat;background-size:260% 100%;background-position:190% 0}
.pgrid figure.in::after{animation:shimmer 1.6s cubic-bezier(.22,.61,.36,1) .45s 1}
.pgrid figure:hover::after{animation:shimmer 1.15s cubic-bezier(.22,.61,.36,1) 1}
@keyframes shimmer{from{background-position:190% 0}to{background-position:-90% 0}}

/* A few slow twinkles over the opening photo. The layer belongs to the photo
   box, so a sparkle never lands on a word whatever shape the box takes, and
   it takes no pointer events, so it cannot get in the way of anything. */
.hero .hphoto::before{content:'';position:absolute;inset:0;pointer-events:none;z-index:1;
  background-repeat:no-repeat;
  background-image:
    radial-gradient(2px 2px at 13% 24%,rgba(255,255,255,.92),transparent 62%),
    radial-gradient(2px 2px at 79% 17%,rgba(37,209,27,.85),transparent 62%),
    radial-gradient(1.5px 1.5px at 46% 12%,rgba(255,255,255,.75),transparent 62%),
    radial-gradient(2px 2px at 88% 58%,rgba(255,255,255,.7),transparent 62%),
    radial-gradient(1.5px 1.5px at 27% 66%,rgba(37,209,27,.75),transparent 62%),
    radial-gradient(1.5px 1.5px at 62% 78%,rgba(255,255,255,.68),transparent 62%);
  animation:twinkle 7s ease-in-out infinite}
@keyframes twinkle{0%,100%{opacity:.22}50%{opacity:.85}}

@media (prefers-reduced-motion:reduce){
  /* Three that leaked, found by emulating the setting rather than by reading
     this block: the stat cards still lifted on hover, the age card arrow still
     slid, and the floating Get started pill still faded and moved. They are
     interaction transitions rather than motion that starts on its own, which
     is why they were missed, but the claim was that this block covers
     everything decorative, and it did not. */
  .stat{transition:none}
  .band .go::after{transition:none}
  .sticky-cta{transition:none}
  .hero h1 .ln{opacity:1!important;transform:none!important;transition:none}
  .hero .hphoto::after{animation:none}
  .hero .hphoto::before{display:none}
  .btn::after{display:none}
  .pgrid figure::after{display:none}
  .pgrid figure.in::after,.pgrid figure:hover::after{animation:none}
}

@media (min-width:640px){
  .bands{grid-template-columns:1fr 1fr}
  .genlist{grid-template-columns:1fr 1fr}
  .firstgrid{grid-template-columns:1fr 1fr}
  .pgrid img{height:190px}
  .pgrid figure::after{height:190px}
}

/* Kim's mission, vision and core values, 2026-08-13. Her sheet gives each
   value a name, a short line and a paragraph, and the short line is the part
   people actually remember, so it gets to look different from the body rather
   than being folded into it. */
.vision{color:var(--sec-color,var(--tx2));margin-top:12px;font-size:calc(var(--sec-scale,1) * 1em)}
.vision b{color:var(--sec-color,var(--tx))}
.valhead{font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;letter-spacing:.12em;
  font-size:max(12px, calc(var(--sec-scale,1) * 14px));margin:22px 0 6px;color:var(--sec-color,var(--tx))}
.values{list-style:none;margin:0;padding:0;display:grid;gap:14px}
.values li{border-left:2px solid var(--neon-dim);padding-left:14px}
.values b{font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;letter-spacing:.1em;
  font-size:calc(var(--sec-scale,1) * 14.5px);color:var(--sec-color,var(--neon));margin-right:8px}
.values i{color:var(--sec-color,var(--tx));font-style:italic;font-size:calc(var(--sec-scale,1) * 1em)}
.values p{color:var(--sec-color,var(--tx2));margin-top:5px;font-size:calc(var(--sec-scale,1) * 1em)}
.valclose{margin-top:18px;color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 1em)}
.valclose b{color:var(--sec-color,var(--tx))}
.weare{display:block;margin-top:6px;font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;
  font-size:14px;letter-spacing:.12em;color:var(--neon)}
/* What a family is paying for, off the same sheet as the prices. */
.included{list-style:none;margin:6px 0 0;padding:0;display:grid;gap:10px}
.included li{display:flex;flex-direction:column}
.included b{color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * 15px)}
.included span{color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 15px)}
@media (min-width:640px){ .included{grid-template-columns:1fr 1fr} }

/* The style name sits in its own span inside the summary so the editor's
   stamp wraps her words alone, not the badge beside them. min-width:0 lets a
   long name shrink inside the flex row instead of pushing the plus off it. */
.gen summary .gname{min-width:0}
/* Kim's own photo on a card: the built-in photos state their size so the page
   does not reflow, and a photo she uploaded arrives with no size, so every
   place one can land (.band img, .card.prog>img, .gen .body img,
   .card.person>img) has a fixed height and object-fit, and nothing moves. */
/* The workshop flyer is the marketing picture Kim posts, shown WHOLE. The
   dates, the times and the cost are printed on it, so it is never cropped the
   way a class photo is. */
.flyer{margin:0 0 16px}
.flyer img{display:block;width:100%;height:auto;border-radius:10px}
/* Her core values worksheet as a picture, under the list. Shown whole for the
   same reason as the flyer: it is words. */
.valuesphoto{margin:16px 0 0}
.valuesphoto img{display:block;width:100%;height:auto;border-radius:10px}
/* Meet the staff. A group photo across the top, then one card per person. It
   renders only once Kim has added someone, so an empty studio never shows. */
.staffgroup{margin:16px 0 0}
.staffgroup img{display:block;width:100%;height:auto;border-radius:14px}
/* How wide a whole picture may get (the editor's "Picture size"). Centered
   when narrower than the column. Nora, 2026-09-24: the core values poster at
   full width made the About page "so very long". */
.valuesphoto[data-size],.staffgroup[data-size]{margin-left:auto;margin-right:auto}
.valuesphoto[data-size=small],.staffgroup[data-size=small]{max-width:360px}
.valuesphoto[data-size=medium],.staffgroup[data-size=medium]{max-width:620px}
/* The photo at the bottom of Why LDG is a bare picture in its card, so it
   takes the same caps on the <img> itself (display:block, so auto margins
   center it). */
.card>img[data-size]{margin-left:auto;margin-right:auto}
.card>img[data-size=small]{max-width:360px}
.card>img[data-size=medium]{max-width:620px}
.staffgrid{display:grid;grid-template-columns:1fr;gap:10px;margin-top:14px}
@media (min-width:640px){.staffgrid{grid-template-columns:1fr 1fr}}
@media (min-width:1000px){.staffgrid{grid-template-columns:1fr 1fr 1fr}}
.card.person{margin:0}
.card.person>img{width:100%;height:260px;object-fit:cover;object-position:50% 25%;
  border-radius:10px;margin-bottom:12px;display:block}
/* Kept on its own neon, the staff card's own accent, same as .lite and the
   growth cards' hue: only the size follows her choice here. */
.card.person .role{color:var(--neon);font-weight:700;margin-top:4px;font-size:calc(var(--sec-scale,1) * 1em)}
/* Paperwork. Each form is one tidy row: a document icon, its name in plain
   case, a small Open. Bobbie, 2026-09-24, of the big stacked green buttons
   this replaces: "these buttons are too big. and I need more control over
   them... I do not like this layout". Every row is the same width whatever
   the name's length, and a long name wraps rather than widening. Kim picks the
   section's Layout (data-layout on the wrapper, build.mjs's documents(): List
   is one per row, Grid is two across from 640px up and still one on a phone)
   and its Button size (--btn-scale, the same control every button has),
   and never a height under the 44px tap floor. The section ends with room
   for the floating Get started button, so it can never sit on the last form.
   Rows are the same height unless a name needs a second line or a row has a
   note under it; in Grid, the two rows side by side always match. */
#paperwork{padding-bottom:calc(env(safe-area-inset-bottom,0px) + 84px)}
.doclist{list-style:none;margin:0;padding:0;display:grid;gap:12px;grid-template-columns:minmax(0,1fr)}
@media (min-width:640px){[data-layout="grid"] .doclist{grid-template-columns:repeat(2,minmax(0,1fr))}}
.docrow{position:relative;display:flex;flex-direction:column;justify-content:center;min-width:0;box-sizing:border-box;
  min-height:max(44px, calc(var(--btn-scale,1) * 52px));padding:calc(var(--btn-scale,1) * 8px) calc(var(--btn-scale,1) * 14px);
  background:var(--surf2);border:1px solid var(--line);border-radius:12px}
.docrow:hover,.docrow:focus-within{border-color:var(--neon);background:rgba(37,209,27,.08)}
/* The whole row is the tap target: the link's ::after covers the row, so a
   thumb anywhere on it opens the form, and the link itself still reads as
   just the form's name. */
.docitem{display:flex;align-items:center;gap:12px;min-height:28px;text-decoration:none;color:var(--sec-color,var(--tx))}
.docitem::after{content:'';position:absolute;inset:0;border-radius:12px}
/* The site's own focus ring (see the focus section below), drawn around the
   whole row rather than just the words. */
.docitem:focus-visible{outline:none}
.docitem:focus-visible::after{outline:3px solid var(--tx);outline-offset:3px;box-shadow:0 0 0 6px var(--neon-dim)}
.docicon{flex:0 0 auto;width:calc(var(--btn-scale,1) * 22px);height:calc(var(--btn-scale,1) * 22px);color:var(--neon)}
.doctitle{flex:1 1 auto;min-width:0;overflow-wrap:anywhere;text-transform:none;letter-spacing:0;font-weight:700;
  line-height:1.3;font-size:max(12px, calc(var(--sec-scale,1) * var(--btn-scale,1) * 16px))}
.docopen{flex:0 0 auto;color:var(--neon);font-weight:800;text-transform:uppercase;
  letter-spacing:calc(var(--btn-scale,1) * .06em);font-size:max(12px, calc(var(--btn-scale,1) * 13px))}
.docnote{color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14px));margin-top:6px}
.docrow .docnote{margin:2px 0 0 calc(var(--btn-scale,1) * 22px + 12px)}

/* A focus ring you can actually see, on every surface this page has.
   Ported from Codex's accessibility pass (PR #556) after the rest of that
   branch turned out to be already live.

   Proven by execution on hub-staging before this was written, and then
   re-measured by pixel diff in review, which corrected one of my claims:
   .contactline a painted 0.0% of its focus ring and .desknav a 2.5%, so those
   really were colour change alone. .btn was WEAKER rather than invisible: a
   same-colour neon outline separated from a neon fill by the 2px offset gap,
   painting 12.7%, which reads as button glow rather than as focus. Both are
   failures for anyone who cannot rely on colour, and both disappear against a
   photograph, but the honest word for .btn is ambiguous, not absent.

   A near-white outline carries the shape, and the dim neon halo outside it
   keeps that shape readable against white cards, black ground, neon fills and
   photographs alike. It is deliberately last in the file so it beats the
   outline:none in the hover rules above, which share its specificity. */
.brand:focus-visible,.menubtn:focus-visible,.menu a:focus-visible,.desknav a:focus-visible,
.btn:focus-visible,.band:focus-visible,.chip:focus-visible,.gen summary:focus-visible,
.contactline a:focus-visible,.announce .bnbtn:focus-visible,.bnpause:focus-visible,
.whatis:focus-visible,.pill:focus-visible,.skip:focus-visible,.sticky-cta:focus-visible,
footer a:focus-visible{
  outline:3px solid var(--tx);outline-offset:3px;box-shadow:0 0 0 6px var(--neon-dim)}

/* The sixteen genre accordions are the exception, and naming them in the list
   above was not enough. .gen carries overflow:hidden to clip its own rounded
   corners, and the summary fills the box edge to edge, so a ring drawn OUTSIDE
   the element is clipped away entirely. Measured in review: 0 of 17,640 ring
   pixels painted. Sixteen consecutive blind tab stops, on the very elements
   this rule claimed to cover.
   Drawn inside instead, and the halo goes with it, since a shadow outside the
   clip is invisible for the same reason. 3,264 pixels paint after the change. */
.gen summary:focus-visible{outline-offset:-6px;box-shadow:none}

/* The open hamburger menu's own links are the same shape of exception, caught
   in refute review 2026-09-24: .menu a fills the panel edge to edge with no
   side padding, and .menu.open now scrolls itself (its own overflow-y, added
   the same day for the mobile scroll fix above), which clips a ring drawn
   OUTSIDE the link on both the left and the right the moment the panel can
   scroll (setting only overflow-y on a box whose overflow-x is still
   'visible' computes overflow-x to 'auto' too, by spec). Same fix as .gen
   summary, inset instead of outside, rather than carving side padding and a
   matching negative margin into every menu row for a ring that already has
   an inset-drawing pattern to reuse. */
.menu a:focus-visible{outline-offset:-3px;box-shadow:none}

/* The floating Get started pill fades out over the contact form but stayed in
   the tab order at opacity 0, so a keyboard user reached a control that
   painted nothing at all. Hidden properly when it is away, which also takes it
   out of the tab order. */
/* visibility is delayed by the length of the fade, not flipped with it. Set
   plainly, it changed on the frame .away landed, so the 0.3s fade-out ran
   invisibly and the pill popped out instead of fading. Measured at 60ms:
   hidden already, opacity still 0.89. The 0s .3s delay lets the fade finish
   and then hides it, which is what takes it out of the tab order. */
.sticky-cta.away{visibility:hidden;transition:opacity .3s ease,transform .3s ease,visibility 0s .3s}
/* The gallery shows twelve and rotates the rest through them. Bobbie,
   2026-08-13: "it can be pages or slowly fade in and new ones replace them".
   The spares are still in the HTML for anyone, and anything, reading the page
   without JavaScript; they are simply not laid out. */
.pgrid figure.spare{display:none}
.pgrid img{transition:opacity .9s ease}
.pgrid img.fading{opacity:0}
/* Their device asked for less motion, so nothing swaps at all and the twelve
   stay put. An image that replaces itself is auto-updating content, and the
   honest answer to "less motion" is none rather than slower. */
@media (prefers-reduced-motion:reduce){
  .pgrid img{transition:none}
}
@media (min-width:1280px){
  .barin{max-width:1240px}
  .desknav a:not(.navlogin){display:inline-flex}
}

/* Upcoming workshops. One tile per workshop: the flyer BIG on top, then the
   title, the dates and ages as chips, the time and price, and Read more.

   Bobbie, 2026-09-24, on the live tiles: "It does not get your attention",
   "the photos for the work shops need to be double the size so it is easier
   to read the photos as that should have 99% of the info they need", "also
   the ages and dates need to pop out", and "maybe it is too black the
   background?". So:

   - The tiles sit on their own lighter band (.wklist-wrap), full width,
     and each tile is a lighter charcoal still, edged in green, so the grid
     reads as the thing to look at. Only this band changed color: the rest
     of the page keeps its own background.
     The tile is as light as it can be while EVERY text color Kim can pick
     for this section still reads at 4.5:1 or better on it (refute review,
     2026-09-24: a lighter #2C3328 tile left the Muted pick at 3.94:1).
     Measured on #232820: Muted (--tx3) 4.54:1, the default and Soft
     (--tx2) 7.9:1, white 14.0:1, Bright (--neon) 7.3:1. On the band:
     Muted 5.2:1. The green edge is 5.0:1 against the band and 4.4:1
     against the tile. www/tests/workshop-tile-contrast.test.mjs holds
     all of these to the line.
   - One column on a phone and on a tablet held upright, two from 1000px,
     where the page itself widens. The earlier grid put three across on a
     desktop and two on a 410px phone, which is exactly what kept every
     flyer a thumbnail. Two columns any narrower would squeeze a flyer
     back under double (an iPad mini at 768px drew it 1.77x, refute
     review 2026-09-24).
   - The flyer window is a 4:5 box (the shape of a phone-made flyer), up to
     560px tall: 439px tall on a 390px phone (was 170px) and 560px on a
     tablet or desktop (was 240px), more than double. Every flyer shows WHOLE, never
     cropped or stretched (object-fit:contain), and tapping it opens it full
     size (site.js). The fixed shape is what reserves the box before the
     lazy flyer arrives, so nothing below it moves when it loads.
   - The dates are a solid green chip and the ages a solid white one, both
     dark bold text: 9.2:1 and 17.7:1. */
:root{--wkband:#181D15;--wktile:#232820;--wkedge:#25A01C;--wkline:#3E4638}
.wklist-wrap{margin-top:26px;padding:26px 0 30px;background:var(--wkband);
  box-shadow:0 0 0 100vmax var(--wkband);clip-path:inset(0 -100vmax)}
.wklist-hd{font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;letter-spacing:.08em;
  font-size:calc(var(--sec-scale,1) * 16px);color:var(--sec-color,var(--tx));margin:0 0 14px}
.wklist{display:grid;grid-template-columns:minmax(0,1fr);gap:18px;align-items:start}
@media (min-width:1000px){.wklist{grid-template-columns:repeat(2,minmax(0,1fr));gap:22px}}
/* No overflow:hidden on the tile: the flyer clips its own top corners
   instead, so nothing inside a tile (a long title, an open description, the
   sticky flyer below) can ever be cut off by the tile's rounded edge. */
.wkbox{display:flex;flex-direction:column;min-width:0;background:var(--wktile);
  border:1.5px solid var(--wkedge);border-radius:16px;box-shadow:0 0 0 4px rgba(37,209,27,.06),0 14px 34px rgba(0,0,0,.4)}
/* The flyer is a link to itself: site.js opens it full size in a dialog,
   and with no script the link opens the picture on its own. */
.wkflyerlink{display:block;color:var(--tx2);text-decoration:none;border-radius:14.5px 14.5px 0 0;overflow:hidden}
.wkflyerlink:focus-visible{outline:3px solid var(--neon);outline-offset:-3px}
.wkflyerbox{position:relative;display:block;width:100%;aspect-ratio:4/5;max-height:560px;background:var(--wkband);overflow:hidden}
@supports not (aspect-ratio:4/5){.wkflyerbox{height:440px}}
/* width:100%;height:100% here, NOT auto: this is what reserves the box
   before the lazy flyer decodes. An "auto" size depends on the browser
   inferring an aspect ratio from the image's own width/height attributes
   ahead of decode, which measured as a real, un-reserved ZERO height in a
   real browser. The window's own shape needs no attribute and no
   inference, and object-fit:contain fits the real picture inside it once
   it arrives, whole, centered, never cropped or stretched. */
.wkbox .wkflyer{display:block;width:100%;height:100%;object-fit:contain}
/* The hint UNDER the flyer, never on it: a badge over the picture would
   cover the very dates and prices printed on the flyer. */
.wkzoom{display:flex;align-items:center;justify-content:center;gap:7px;min-height:36px;padding:4px 12px;
  background:var(--wkband);border-bottom:1px solid var(--wkline);font-size:13px;font-weight:700;letter-spacing:.04em}
.wkzoom svg{flex:0 0 auto}
.wkflyerlink:hover .wkzoom,.wkflyerlink:focus-visible .wkzoom{color:var(--neon)}
/* A workshop with no flyer uploaded still gets a real tile, brand color
   with its own title on it, rather than a blank hole where a picture should
   be: "the images must show" (Bobbie, 2026-09-24). Nothing here is invented
   copy, only the workshop's OWN title, already published. Shorter than a
   flyer window, since there is no picture to read. */
.wkflyerbox.wkflyer-none{aspect-ratio:auto;height:170px;border-radius:14.5px 14.5px 0 0;
  background:linear-gradient(160deg,rgba(37,209,27,.28),var(--wkband));
  display:flex;align-items:center;justify-content:center;border-bottom:1px solid var(--wkline)}
.wkflyer-none-title{font-family:'Anton',sans-serif;font-weight:400;text-transform:uppercase;
  letter-spacing:.04em;color:var(--neon);font-size:22px;line-height:1.15;text-align:center;
  padding:0 18px;display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;
  overflow-wrap:anywhere;hyphens:auto}
.wkhead{display:flex;flex-direction:column;gap:10px;padding:16px 18px 4px}
/* hyphens:auto (html lang="en" is set) so a single long word that still has
   to break carries a hyphen, and overflow-wrap:anywhere under it so even a
   title neither saves shows in full rather than running out of its tile. */
.wktitle{margin:0;font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
  font-size:calc(var(--sec-scale,1) * 18px);line-height:1.25;color:var(--sec-color,var(--tx));
  overflow-wrap:anywhere;hyphens:auto}
/* The chips keep their own colors whatever text color is picked for the
   section, the same as a button: dark words on a green or white fill, and
   a green text pick on a green chip would read as nothing. */
.wkchips{display:flex;flex-wrap:wrap;gap:8px;margin:0}
.wkchip{display:inline-flex;align-items:center;min-height:34px;padding:5px 14px;border-radius:999px;
  font-weight:800;line-height:1.2;letter-spacing:.02em;font-size:max(12px, calc(var(--sec-scale,1) * 15.5px))}
.wkchip-dates{background:var(--neon);color:#08130A}
.wkchip-ages{background:var(--tx);color:#08130A}
.wkchip-lab{font-size:max(12px, .78em);letter-spacing:.08em;text-transform:uppercase;margin-right:.1em}
.wkfacts{margin:2px 0 0;display:grid;gap:6px}
.wkfacts div{display:grid;grid-template-columns:3.6em minmax(0,1fr);gap:10px;align-items:baseline}
.wkfacts dt{font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--sec-color,var(--tx2));
  font-size:max(12px, calc(var(--sec-scale,1) * 12px))}
.wkfacts dd{margin:0;font-weight:600;color:var(--sec-color,var(--tx));font-size:calc(var(--sec-scale,1) * 16px);line-height:1.4}
/* Read more on the left, Sign up on the right. Open, Read more takes the
   whole row for her description and Sign up drops under it. */
.wkact{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:12px 18px 18px;margin-top:auto}
.wkmore{flex:1 1 auto;min-width:0}
.wkmore[open]{flex-basis:100%}
.wkmore summary{list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:10px;min-height:48px;
  padding:0 16px;border:1.5px solid var(--neon);border-radius:10px;color:var(--neon);font-weight:800;
  text-transform:uppercase;letter-spacing:.06em;font-size:max(12px, calc(var(--sec-scale,1) * 14px));
  scroll-margin:110px 0 110px}
.wkmore summary{position:relative}
.wkmore summary::-webkit-details-marker{display:none}
/* Read, never seen: which workshop a Read more or Sign up belongs to. */
.wkvh{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.wkmore summary:hover{background:rgba(37,209,27,.12)}
.wkmore summary:focus-visible{outline:2px solid var(--neon);outline-offset:2px}
.wkmore .sp::after{content:'+';font-size:22px;line-height:1}
.wkmore[open] .sp::after{content:'\2212'}
.wkmore-c{display:none}
.wkmore[open] .wkmore-c{display:inline}
.wkmore[open] .wkmore-o{display:none}
.wkact>.btn{margin-top:0;scroll-margin-bottom:110px}
/* Her description, the way she typed it (lib/workshopText.mjs). A readable
   measure; her blank lines as the biggest gaps and every other block
   (paragraph, list, schedule, prices) spaced from the next, so a
   description typed with no blank lines at all still has a rhythm;
   headings bold; the schedule on one green rule with each date row spaced
   from the next and the lines it owns smaller underneath; prices in two
   columns. The smaller lines never go under 12px at any text size. */
.wkbody{padding:16px 0 4px;max-width:68ch;color:var(--sec-color,var(--tx2));line-height:1.65;
  font-size:calc(var(--sec-scale,1) * 16px)}
.wkbody p{margin:0}
.wkbody .wkgrp+.wkgrp{margin-top:1.25em}
.wkbody .wkgrp>*+*{margin-top:.75em}
.wkbody .wkh{color:var(--sec-color,var(--tx));font-weight:800;line-height:1.35;font-size:1.06em}
.wkbody .wkh-lead{color:var(--sec-color,var(--neon));font-size:1.2em}
.wkbody .wkgrp>.wkh+*{margin-top:.3em}
.wkbody .wktag{color:var(--sec-color,var(--tx));font-weight:700}
.wkbody .wksched{display:grid;gap:.8em;padding:4px 0 4px 16px;border-left:3px solid var(--neon)}
.wkbody .wkgrp>.wksched{margin-top:1em}
.wkbody .wkgrp>.wksched+*{margin-top:1em}
.wkbody .wkrow-hd{color:var(--sec-color,var(--tx));font-weight:700;line-height:1.4}
.wkbody .wkwhen{color:var(--sec-color,var(--neon));font-weight:800}
.wkbody .wksub{font-size:max(12px, .88em);line-height:1.45;margin-top:3px}
.wkbody .wksublab{font-weight:700;color:var(--sec-color,var(--tx))}
.wkbody .wkul{list-style:none;margin:0;padding:0;display:grid;gap:6px}
.wkbody .wkul li{display:flex;gap:10px;align-items:baseline}
.wkbody .wkmark{flex:0 0 auto;min-width:1.2em;text-align:center;color:var(--sec-color,var(--neon));font-weight:800}
.wkbody .wkprices{margin:0;border:1px solid var(--wkline);border-radius:12px;background:rgba(12,14,11,.3)}
.wkbody .wkprices div{display:flex;flex-wrap:wrap;justify-content:space-between;gap:2px 16px;padding:9px 14px}
.wkbody .wkprices div+div{border-top:1px solid var(--wkline)}
.wkbody .wkprices dt{color:var(--sec-color,var(--tx))}
.wkbody .wkprices dd{margin:0 0 0 auto;color:var(--sec-color,var(--neon));font-weight:800;text-align:right}
/* An email or web address in her description is a real link, and a real
   44px target: padding grows the tap area, the equal negative margin keeps
   the line exactly where it was. */
.wkbody .wklink{display:inline-block;max-width:100%;padding:10px 0;margin:-10px 0;overflow-wrap:anywhere;
  color:var(--sec-color,var(--neon));font-weight:700;text-underline-offset:3px}
/* An OPEN tile takes the whole row rather than fighting its own column for
   the description, and pushes nothing sideways. Wide enough for two
   columns, it also lays out side by side: the flyer on the left, held in
   view while the description is read on the right. A browser without :has
   simply opens the tile in its own column. */
.wkbox:has(.wkmore[open]){grid-column:1/-1}
@media (min-width:1000px){
  .wkbox:has(.wkmore[open]){display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);
    grid-template-rows:auto 1fr;align-items:start}
  .wkbox:has(.wkmore[open])>.wkflyerlink,.wkbox:has(.wkmore[open])>.wkflyerbox{grid-column:1;grid-row:1/span 2;
    position:sticky;top:90px;border-radius:14.5px 0 12px 0}
  /* Held in view beside the words, so it must FIT in view: on a short
     laptop screen (1366x657) the full 560px window plus its Tap to enlarge
     bar ran 29px off the bottom (refute review, 2026-09-24). The window
     gives up height before the flyer is cut: object-fit:contain still
     draws it whole, just smaller. 90px top bar, 36px bar, 16px air. */
  .wkbox:has(.wkmore[open])>.wkflyerlink .wkflyerbox{max-height:min(560px, calc(100vh - 142px))}
  .wkbox:has(.wkmore[open])>.wkhead,.wkbox:has(.wkmore[open])>.wkact{grid-column:2}
}
/* The Get started pill and the Top button are fixed to the bottom of the
   screen, and they sat on top of an open workshop's last lines and its
   Sign up button (Bobbie, 2026-09-24, her screenshot). While a workshop
   tile is under them, site.js sets body.wk-reading and both step aside,
   the same way they step aside for the open menu. */
/* visibility too, not just opacity, and delayed the length of the fade the
   same way .sticky-cta.away is above: a pill at opacity 0 alone is still in
   the tab order, a control that paints nothing (refute review, 2026-09-24). */
body.wk-reading .sticky-cta{opacity:0;pointer-events:none;transform:translateX(-50%) translateY(14px);
  visibility:hidden;transition:opacity .3s ease,transform .3s ease,visibility 0s .3s}
body.wk-reading .totop{display:none}
/* The full size flyer (site.js). A dialog in the top layer, above every
   floating button; Escape, Close or a tap anywhere closes it. It never
   locks the page's scroll, so there is no lock to get stuck; site.js keeps
   the page from scrolling BEHIND it instead, and puts the page back where
   it was on close. touch-action lets a phone pinch in on the small print. */
.wklight{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;border:0;
  padding:calc(env(safe-area-inset-top,0px) + 68px) 12px calc(env(safe-area-inset-bottom,0px) + 12px);
  background:rgba(5,6,5,.97);color:var(--tx);touch-action:pinch-zoom;overscroll-behavior:contain;overflow:auto}
.wklight[open]{display:flex}
.wklight::backdrop{background:rgba(5,6,5,.6)}
/* margin:auto centers it and, unlike justify-content:center, never pushes
   a picture wider than the screen off its left edge where no scroll can
   reach it. */
.wklight-img{display:block;margin:auto;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;border-radius:8px}
/* On a phone the tile already shows the flyer the whole width of the
   screen, so the full size view fitted to the same screen was only about
   4% bigger (refute review, 2026-09-24). There it opens twice the screen's
   width instead (site.js sets the width, never past the picture's own),
   and a finger slides it around to read the small print. The Close button
   stays put above it. */
.wklight.wkpan{touch-action:auto}
.wklight.wkpan .wklight-img{flex:none;max-width:none;max-height:none;margin:0}
.wklight-close{position:fixed;top:calc(env(safe-area-inset-top,0px) + 12px);right:12px;min-height:44px;min-width:44px;z-index:1;
  padding:0 18px;border-radius:999px;border:1.5px solid var(--neon);background:var(--ink);color:var(--tx);
  font:inherit;font-weight:800;font-size:15px;cursor:pointer}
.wklight-close:hover,.wklight-close:focus-visible{background:var(--neon);color:#08130A;outline:2px solid var(--tx);outline-offset:2px}

/* The Facebook card in Contact (build.mjs facebookCard). */
/* .btn sets display, which would beat the hidden attribute: without this the feed
   button shows with no script to run it, and stays after the tap. */
.fbcard [hidden]{display:none}
/* A link and a button side by side wrap their words the same way. */
.fbcard .btn{justify-content:center;text-align:center}
.fbfeed iframe{display:block;border:0;width:100%;max-width:500px;height:700px;margin-top:16px;border-radius:10px;background:#fff}

/* The privacy policy page (build.mjs privacyPolicyHtml): room above each numbered heading. */
#privacy h2{margin-top:30px}
#privacy p{margin-top:10px}
