/* 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}
.barin{max-width:780px;margin:0 auto}
.barrow{display:flex;align-items:center;gap:12px;min-height:56px}
/* 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}
.menu.open{display:block}
.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{display:inline-flex;align-items:center;background:var(--neon);color:#08130A;font-weight:800;
  text-transform:uppercase;letter-spacing:.06em;font-size:max(12px, calc(var(--sec-scale,1) * 14px));padding:15px 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}
}
.announce .bnbtn{display:inline-flex;align-items:center;min-height:44px;padding:0 20px;
  border-radius:10px;background:var(--neon);color:#08130A;font-weight:800;text-decoration:none;
  white-space:nowrap;font-size:calc(var(--sec-scale,1) * 1em)}
.announce .bnbtn:hover,.announce .bnbtn:focus-visible{background:var(--tx);outline:none}
@media (min-width:1000px){.announce{padding:14px 20px}}

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

.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:calc(var(--sec-scale,1) * 15px);padding:16px 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)}

.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}
.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 document is a big button, because the point of this section
   is that a family prints it and brings it in. */
.doclist{list-style:none;margin:0;padding:0;display:grid;gap:14px}
.doclist .btn{margin-top:0}
.docnote{color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14px));margin-top:6px}

/* 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 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 (Bobbie, 2026-09-23): one box per workshop, flyer on
   top, tap to open the details. One column on phones, a grid from tablet up.
   Native <details>, the same open and close look as the dance style list. */
.wklist-wrap{margin-top:8px}
.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 12px}
.wklist{display:grid;grid-template-columns:1fr;gap:12px;align-items:start}
@media (min-width:700px){.wklist{grid-template-columns:repeat(2,1fr)}}
@media (min-width:1100px){.wklist{grid-template-columns:repeat(3,1fr)}}
.wkbox{background:var(--surf);border:1px solid var(--line);border-radius:14px;overflow:hidden}
.wkbox summary{list-style:none;cursor:pointer;display:flex;flex-direction:column;min-height:44px;position:relative}
.wkbox summary::-webkit-details-marker{display:none}
.wkbox summary:focus-visible{outline:2px solid var(--neon);outline-offset:-2px}
.wkbox .wkflyer{display:block;width:100%;height:auto;object-fit:contain;
  background:var(--surf2);border-bottom:1px solid var(--line)}
/* Sized by its own width and height attributes (the file's real pixels), so
   the box is reserved before the lazy image loads. Only a flyer whose size
   the build could not read gets this fixed shape instead. */
.wkbox .wkflyer-box{aspect-ratio:4/5}
.wkhead{display:flex;flex-direction:column;gap:4px;padding:14px 52px 16px 16px}
.wktitle{font-family:'Archivo',sans-serif;font-weight:800;text-transform:uppercase;letter-spacing:.06em;
  font-size:calc(var(--sec-scale,1) * 16px);line-height:1.25;color:var(--sec-color,var(--tx))}
.wkdates{color:var(--neon);font-weight:800;font-size:calc(var(--sec-scale,1) * 15px)}
.wkfact{color:var(--sec-color,var(--tx2));font-size:max(12px, calc(var(--sec-scale,1) * 14.5px))}
.wkbox summary .sp{position:absolute;right:16px;bottom:16px;color:var(--neon);font-family:'Archivo',sans-serif;
  font-size:24px;font-weight:700;line-height:1}
.wkbox summary .sp::after{content:'+'}
.wkbox[open] summary .sp::after{content:'\2212'}
.wkbox[open] summary{border-bottom:1px solid var(--line)}
.wkbody{padding:14px 16px 18px;color:var(--sec-color,var(--tx2));font-size:calc(var(--sec-scale,1) * 15.5px)}
.wkbody p{margin:0 0 10px;white-space:pre-line}
.wkbody .btn{margin-top:6px}
