
@font-face { font-family: 'Space Grotesk'; font-style: normal; font-weight: 500; font-display: swap; src: url('/assets/space-grotesk-500.woff2') format('woff2'); }
@font-face { font-family: 'Space Grotesk'; font-style: normal; font-weight: 700; font-display: swap; src: url('/assets/space-grotesk-700.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 300; font-display: swap; src: url('/assets/fraunces-300.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: italic; font-weight: 300; font-display: swap; src: url('/assets/fraunces-300-italic.woff2') format('woff2'); }
@font-face { font-family: 'Fraunces'; font-style: normal; font-weight: 600; font-display: swap; src: url('/assets/fraunces-600.woff2') format('woff2'); }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 400; font-display: swap; src: url('/assets/archivo-400.woff2') format('woff2'); }
@font-face { font-family: 'Archivo'; font-style: normal; font-weight: 600; font-display: swap; src: url('/assets/archivo-600.woff2') format('woff2'); }

:root {
  --bg: #fdfcf9; --bg-soft: #f5f2ea; --bg-warm: #faf3e9;
  --ink: #17141f;
  /* DT-P40. Secondary text was #5b5566: legal at 7:1 and still faint in
     practice, because almost nothing on this site uses it at body size. It
     carries labels, captions, table headings and axis text at 0.72–0.9rem on a
     warm cream ground, and small warm-grey type reads lighter than its number
     says. One token, several hundred places. */
  --ink-soft: #453f4f;
  --violet: #6d28d9; --magenta: #d61f74; --amber: #e56910;
  --violet-soft: #ede7fb; --magenta-soft: #fbe7f1;
  --night: #08070d; --night-2: #17141f;
  --light: #f2efe8; --light-soft: #c4c0cc;   /* DT-P40: secondary text on the dark bands, same complaint, same fix */
  --line: #dcd5c8; --line-dark: #35313f;   /* DT-P40: a rule you cannot see is not a rule */
  --grad: linear-gradient(100deg, #6d28d9 0%, #d61f74 55%, #e56910 100%);
  --cinema: #08070d; --cinema-text: #f2efe8; --cinema-soft: #c4c0cc; --stagelight: #e9a862; --stagelight-deep: #9a5b16;
  /* DT-P132: what the masthead actually measures — 0.9rem of padding above and
     below its content, plus its rule. It is a number, and a number written in
     one file and relied on in another is how a seam opens. So the cinema hero
     subtracts THIS token rather than a figure of its own, and a browser test
     asserts the hero's bottom edge lands on the fold at five window sizes on
     both cinema pages. Change the masthead's padding or type and that test
     fails naming this line, which is the only thing that keeps a measurement
     honest. */
  --masthead-h: 4.25rem;
  /* DT-P79: ONE hover amber, for every button that answers with amber — the
     amber one deepening on a light card, the amber one deepening on a dark
     card, the white one on a dark card, and the room's Join. Philip asked for
     the "Set it up" hover to be "an orange consistent with other buttons",
     and consistent means literally the same value, from one place, so a
     future adjustment moves all of them at once and cannot move only three. */
  --hover-amber: #d68f3a;

  /* ---- the semantic layer (DT-P149) ------------------------------------
     The names above say what a colour IS. These say what it is FOR, and they
     are aliases: one source of truth, no hex repeated. Nothing renders
     differently for declaring them.

     They exist because a colour system that is never measured is a claim, and
     "we follow WCAG" without code behind it is a defect. The platform's
     contrast gate checks a fixed semantic vocabulary — --text on --surface,
     a label on --action, a focus ring, a border — and this product had none
     of those names, so the gate read the stylesheet, found no pairing it
     recognised, and had been passing on NOTHING since the day we adopted it.

     Every line here is DESCRIPTIVE: it names the token the product actually
     uses in that role, read off the components, not the one that would score
     best. --border-strong is --line because --line is what a form field's
     border is, and that pairing fails (see DT-P149). Choosing --ink-soft
     instead would have bought a green tick and blinded the gate again. */
  --text: var(--ink);
  --text-muted: var(--ink-soft);
  --text-inverse: #191106;          /* the label on the amber button, below */
  --surface: var(--bg);
  --surface-sunken: var(--bg-soft);
  --surface-raised: #ffffff;        /* a card, a panel, a field */
  --action: var(--stagelight);
  --action-hover: var(--hover-amber);
  --focus-ring: var(--stagelight-deep);
  /* T-52. NOT an alias: --line is #dcd5c8, a 1.42:1 hairline against the page,
     and WCAG 2.2 1.4.11 wants 3:1 for the boundary that IDENTIFIES a control.
     A field whose edge you cannot see is a field you cannot find. This is the
     warm grey that clears it — 3.44:1 on white, 3.35:1 on the page, 3.08:1 on
     the sunken ground — and it is used ONLY where a border is the affordance:
     fields, outline buttons, the file picker. Dividers, cards, panels and
     rules keep --line, which is decoration and exempt. */
  --border-strong: #948977;
  /* Deliberately ABSENT, so the gate reports them as unchecked rather than
     being handed something that is not true:
       --action-quiet            the amber action has no quiet ground.
       --colour-positive/caution/critical-500   there is no status palette.
     And the vocabulary has ONE --action, so the studio's violet cannot be a
     second one; the violet pairings are measured in test/web.js instead. */

  --serif: 'Fraunces', Georgia, serif;
  --display: 'Space Grotesk', system-ui, sans-serif;
  --body: 'Archivo', system-ui, -apple-system, sans-serif;

  /* ---- the motion protocol (DT-P76) ------------------------------------
     Philip: "Smooth, calm, thoughtful should be a fixed design feature."

     So motion is a token, not a decision each component makes for itself.
     ONE curve and THREE durations, and nothing anywhere writes its own
     number. A product where every transition was chosen separately is a
     product that moves at eight different speeds, and the reason that reads
     as cheap is not any single one of them — it is the disagreement.

     The curve starts quickly and lands slowly, which is what "calm" means in
     motion: the movement is over before you have finished registering it, and
     the last third eases rather than stops dead. There is no bounce and no
     overshoot. Those say playful; this product is somebody's livelihood.

     --tap    a control answering a finger. Must stay well under ~150ms or it
              feels like the interface is thinking about it. This one does not
              get slower with the others: a press is the one moment where
              instant IS the premium feeling.
     --ease-t the default. Hover, colour, opacity, a card arriving.
     --enter  something appearing that was not there — the fold, a page.

     DT-P78: Philip asked for these to be slower — "calm, premium, smooth,
     even if that makes it a little slower". He is right, and the reason is
     worth writing down: fast reads as *responsive* on a control you are
     touching and as *abrupt* on a whole page you are reading. 200ms is a
     perfectly good hover and was a poor page. So the two came apart —
     --ease-t moved a little, --enter moved a lot, --tap did not move at all —
     rather than everything sliding to one compromise that suits nothing. */
  --curve: cubic-bezier(0.22, 0.61, 0.36, 1);
  --tap: 90ms;
  --ease-t: 260ms;
  --enter: 480ms;
}

/* ---- moving between pages (DT-P76) -------------------------------------
   Philip: "The same smooth, calm, thoughtful transition should happen when
   transitioning through a button, such as the academy button and selecting a
   lesson."

   Every page of this product is rendered on the server, so a button leading
   somewhere has always been a hard cut: white flash, new page. This is a
   cross-document view transition — the browser holds the old page, paints the
   new one, and eases between them. Two declarations, no script, no change to
   the content security policy, and no framework: the reason the whole product
   can be given this at once is precisely that nothing here is a single-page
   application.

   A browser that has not learned it ignores both rules and navigates the way
   it does today. Nothing depends on it working. */
   DT-P78: slower, and OVERLAPPED. The leaving page fades over most of the
   arriving page's entrance rather than clearing out first, so there is never
   a moment of empty canvas between the two — that gap is what made the first
   version read as a quick blink rather than a dissolve. The arriving page
   also rises a little further (10px, not 6) over a longer time: a small
   movement over a long duration is what "calm" looks like, where the same
   duration with no movement just looks slow.

   This is the transition between rail tabs as well. Every tab in the studio
   is a link to another page, so a tab change IS a navigation and gets this
   for free — there is no separate tab animation, and there should not be one. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: pageout var(--enter) var(--curve) both; }
::view-transition-new(root) { animation: pagein var(--enter) var(--curve) both; }
@keyframes pageout { to { opacity: 0; } }
@keyframes pagein { from { opacity: 0; transform: translateY(10px); } }

/* One escape, for everybody who has asked their machine for less movement.
   It has to name the view-transition pseudo-elements explicitly: they are
   generated outside the document, so a rule about .thing never reaches them.
   Colour changes survive — motion is feedback, and taking away the feedback
   is not the same courtesy as taking away the movement. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; overflow-x: clip; font-family: var(--body); background: var(--bg); color: var(--ink); line-height: 1.65; -webkit-font-smoothing: antialiased; }
h1, h2, h3 { font-family: var(--serif); font-weight: 600; line-height: 1.06; letter-spacing: -0.01em; text-wrap: balance; }
h1 { font-size: clamp(2.8rem, 7vw, 5.2rem); margin: 0 0 1.1rem; }
h2 { font-size: clamp(1.9rem, 4vw, 2.9rem); margin: 0 0 0.75rem; font-weight: 300; }
h3 { font-size: 1.3rem; margin: 0 0 0.3rem; }
p, li, figcaption, blockquote { text-wrap: pretty; }
p { margin: 0.8rem 0; }
a { color: var(--stagelight-deep); text-decoration-thickness: 1px; text-underline-offset: 3px; }
code { background: var(--bg-soft); border: 1px solid var(--line); padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.92em; }
main { max-width: 47rem; margin: 0 auto; padding: clamp(2rem, 5vw, 3.25rem) 1.35rem 4.5rem; }

/* ---- the reading pages (DT-P49) ---------------------------------------
   /account, /apply, /privacy and their neighbours are documents, not posters,
   and they had been inheriting the marketing register wholesale: a 5.2rem
   headline over an eighty-character measure, section headings sitting a
   third of a line clear of the paragraph above them, and controls arriving
   with no more space around them than a sentence.

   Nothing here touches the hero or the bands. It is scoped to main, which is
   exactly the set of pages a person reads rather than looks at. */
main h1 { font-size: clamp(2.1rem, 4.4vw, 3.1rem); line-height: 1.1; margin: 0 0 0.35rem; }
/* A section heading owes far more space to what came before it than to what
   comes after: that gap is the only thing telling a reader a new subject has
   started. */
main h2 { font-size: clamp(1.35rem, 2.6vw, 1.8rem); font-weight: 400; margin: 2.9rem 0 0.55rem; }
main h3 { font-size: 1.06rem; margin: 1.8rem 0 0.3rem; }
main p { margin: 0.75rem 0; max-width: 64ch; line-height: 1.68; }
/* DT-P50: a two-line lede that leaves one word alone on the first line reads
   as a mistake. balance splits it evenly instead; pretty alone only protects
   the last line, which is not where this breaks. */
/* DT-P50: a lede is one sentence and belongs on as few lines as the column
   allows. It takes the full column rather than a narrower measure, and balance
   splits whatever is left over evenly instead of stranding a word — which is
   what "it moved on to two lines" actually looked like. */
main > p:first-of-type { font-size: 1.05rem; color: var(--ink-soft); max-width: none; text-wrap: balance; }
main > p:first-of-type + p.soft { margin-top: 0.35rem; }
/* DT-P53: NOT balanced. Balancing splits this into two even lines whatever the
   measure, which is how it came to read "…Report a concern. No" / "account
   needed…" — a break mid-clause with a capital letter left dangling. Greedy
   wrapping fills the first line and pushes the unbreakable final sentence
   whole onto the second, which is the break a reader expects. */
main footer p { max-width: none; text-wrap: normal; }
/* DT-P53: a line that must not be split. The safeguarding footer used to break
   as "…Report a concern. No" / "account needed…", which reads as a mistake and
   leaves a capital letter dangling at the end of a line. Holding the final
   sentence together moves the break to the sentence boundary, where a reader
   expects one, at every width — text-wrap: balance could not be relied on to
   choose it. */
.nb { white-space: nowrap; }

/* DT-P62: a teacher's own page in the directory.
   Built to be finished WITHOUT a photograph. The grid gains its picture column
   only when there is a picture — .tbio--photo — rather than reserving a space
   and filling it with a grey circle. A teacher who has decided not to put
   their face on the internet has usually thought about it harder than we
   have, and should not be presented as an incomplete profile for it. */
.tbio { display: grid; gap: clamp(1.6rem, 4vw, 3rem); align-items: center; }
.tbio--photo { grid-template-columns: minmax(0, 19rem) minmax(0, 1fr); }
/* DT-P162: the hero. Dark, because the photograph is the thing on this page and
   a cream page makes every portrait look like a document scan — and because the
   card the family just clicked was dark behind the same face, so the two
   screens rhyme. It is a band rather than a full-height hero: this is a page to
   READ, and a family who has to scroll past a cinema frame to reach the first
   sentence about a teacher has been sold to rather than informed. */
/* DT-P163. Philip: "can we make the fill colour on the teacher's individual
   page more vibrant and brand consistent?"
   It was --night, flat: the right answer for a numbers band and a dull one
   behind a person. The register this product was built to (D-024, fourth
   revision) asks for "slick, clean, vibrant, more colourful" and declares ONE
   energy gradient — violet through magenta to amber — which has sat in :root
   as --grad, used by nothing, since the cinema rewrite. So this is that
   gradient at the weight a photograph can stand in front of: violet where the
   picture sits, a magenta warmth low and right, falling to the product's own
   night at the edges. No new hue is introduced; all three are the brand's.
   The brightest point is held at #2a1150 rather than being allowed to rise —
   white is 16.22:1 on it, the location line 9.08:1, and the chip edge clears
   the 3:1 a boundary needs. */
.tbio__hero {
  background:
    radial-gradient(85% 120% at 14% 12%, #2a1150 0%, rgba(42,17,80,0) 62%),
    radial-gradient(70% 95% at 86% 92%, rgba(214,31,116,0.32) 0%, rgba(214,31,116,0) 62%),
    linear-gradient(168deg, #160d28 0%, #0c0914 58%, var(--night) 100%);
  color: var(--light);
  padding-top: clamp(1.8rem, 4vw, 2.8rem); padding-bottom: clamp(2rem, 4vw, 3rem);
}
/* One measure for the whole page. The site's bands run to 82rem, which is right
   for a grid of cards and wrong for a person: at that width the name sat in the
   left half of a black field and the biography stopped two hundred pixels short
   of its own sidebar. */
.tbio__hero .band__inner, .band__inner.tbio__body { max-width: 72rem; }
.tbio__hero h1 { color: #ffffff; }
.tbio__hero .tbio__back a { color: var(--stagelight); }
.tbio__where { margin: 0.1rem 0 0; color: var(--light-soft); font-size: 1rem; }
.tbio__hero .tcard__styles { margin: 1rem 0 0; }
/* DT-P151. The photograph, whole. This rule asked for a square and CROPPED to
   get one — aspect-ratio 1/1 with object-fit: cover — so the first real
   portrait in the directory arrived and the teacher's own page cut her off at
   the chin, while the card beside it (DT-P145) showed all of her. The page a
   family reads to decide who teaches their child is the wrong place to trim
   somebody's face to fit a shape we chose.
   Now the box takes the picture's OWN proportions: width fills the column,
   height follows, and there is no crop to be had because there is nothing to
   crop to. The cap stops a very tall photograph running past the words beside
   it — at a 15rem column a 2:3 portrait is 22.5rem, comfortably inside it. */
/* DT-P162. THE FRAME IS UNIFORM EVEN WHEN THE PICTURE IS NOT.
   Philip: "why is Miss Nay's photo thinner than Miss Martin's? I noticed their
   size isn't uniform."
   Because of the rule above this one. width:100% with height:auto and a
   max-height means a portrait taller than the cap has its HEIGHT clamped, and
   the browser then narrows the WIDTH to keep the proportions — so a 9:16 phone
   photograph came out a third narrower than a 4:5 one, and nothing said why.
   The answer is not to go back to cropping (DT-P151 removed that for good
   reason: it took the top off a teacher's head). It is that the frame should be
   the constant and the picture should sit inside it. Every teacher now gets the
   same 4:5 box; a photograph that does not fill it is centred on a ground with
   some depth in it, exactly as the cards do it, rather than pillarboxed in
   black like a mistake. */
.tbio__p { aspect-ratio: 4 / 5; border-radius: 18px; }
.tbio__m h1 { margin: 0 0 0.6rem; font-size: clamp(1.8rem, 4vw, 2.6rem); }
.tbio__f { display: grid; gap: 0.85rem; margin: 0; }
.tbio__f dt { font-family: var(--display); font-size: 0.74rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); }
.tbio__f dd { margin: 0.2rem 0 0; font-size: 0.95rem; line-height: 1.5; }
/* DT-P176: the provenance note under a fact. Quiet, and always present where
   the fact is the teacher's own answer rather than something we established. */
.tbio__fn { display: block; margin-top: 0.1rem; font-size: 0.76rem; line-height: 1.4; color: var(--ink-soft); }
.tbio__w { margin: 1rem 0 0; }
.tbio__w p { margin: 0 0 0.9rem; max-width: 68ch; line-height: 1.75; }
.tbio__a { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1.6rem 0 0; }
.tbio__a .btn { margin: 0; }
.tbio__back { margin: 0 0 1.2rem; font-size: 0.88rem; }
/* The ghost button on the dark hero, borrowed from the cinema hero rather than
   invented: a white pill on black shouts as loudly as the amber one, and these
   two are not equal choices. */
.tbio__hero .btn--ghost { background: transparent; color: var(--cinema-text); border: 1.5px solid rgba(242,239,232,0.45); }
/* DT-P170: white label on hover, as on the cinema heroes. Same button, same
   dark ground, same reason — the border lights, the word does not dim. */
@media (hover: hover) { .tbio__hero .btn--ghost:hover { border-color: var(--stagelight); color: var(--cinema-text); background: rgba(233,168,98,0.12); transform: translateY(-2px); } }
.tbio__hero .btn--ghost:focus-visible { color: var(--cinema-text); }
.tbio__hero .btn--ghost:active { transform: translateY(0); }
/* The body: an article with its facts beside it. The measure is the text's, not
   the container's — 82rem of biography is unreadable — and the aside holds the
   things a family checks WHILE reading rather than after it. */
.tbio__body { display: grid; gap: clamp(1.8rem, 4vw, 3rem); align-items: start; padding-top: clamp(1.4rem, 3vw, 2.4rem); }
@media (min-width: 62rem) { .tbio__body { grid-template-columns: minmax(0, 1fr) minmax(0, 20rem); } }
.tbio__main { min-width: 0; }
.tbio__h { margin: 0 0 0.4rem; font-size: clamp(1.3rem, 2.4vw, 1.7rem); }
.tbio__side { display: grid; gap: 1rem; min-width: 0; }
@media (min-width: 62rem) { .tbio__side { position: sticky; top: calc(var(--masthead-h) + 1.2rem); } }
/* On a phone the column becomes a stack, and a stack has an order. Both cards
   sitting after the enquiry form put "what ages does she teach" BELOW the form
   that asks what you would like to know — so the aside stops being a box
   (display: contents) and its two cards take their own places: the facts go
   above the biography, where they are read, and the way in stays at the foot,
   where somebody who has just finished reading is standing. */
@media (max-width: 61.99rem) {
  .tbio__side { display: contents; }
  .tqual { order: -2; }
  .tbio__facts { order: -1; }
  .tbio__main { order: 0; }
  .tbio__go { order: 1; }
}
.tbio__card { border: 1px solid var(--line); border-radius: 16px; padding: 1.2rem 1.3rem 1.4rem; background: var(--bg-soft); }
.tbio__card--go { background: var(--bg-warm); }
.tbio__card--go p { margin: 0 0 1rem; font-size: 0.92rem; line-height: 1.6; color: var(--ink-soft); }
.tbio__card--go .btn { display: block; text-align: center; }
.tbio__ch { margin: 0 0 0.7rem; font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--display); color: var(--ink-soft); }
.tbio__ask { margin: 0.9rem 0 0 !important; font-size: 0.88rem; text-align: center; }
/* DT-P175: the qualifications card. Two groups, because the difference between
   them is the whole honesty of the thing: a list that ran awarded and
   in-progress together in one column would read as one list of achievements.
   The group headings are the same small caps as the card heading one size down,
   the year is quiet, and the "in progress, not yet awarded" line sits under its
   own row rather than once at the foot — a disclaimer a reader can scroll past
   is a disclaimer for the reader who already agreed with us. */
.tqual__h { margin: 1rem 0 0.45rem; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; font-family: var(--display); color: var(--ink-soft); }
.tqual__h:first-of-type { margin-top: 0; }
.tqual__l { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; }
.tqual__l li { font-size: 0.94rem; line-height: 1.5; }
.tqual__y { color: var(--ink-soft); font-size: 0.86rem; white-space: nowrap; }
.tqual__p { display: block; margin-top: 0.15rem; font-size: 0.82rem; line-height: 1.45; color: var(--ink-soft); }
.tqual__s { margin: 0.75rem 0 0; font-size: 0.84rem; line-height: 1.5; color: var(--ink-soft); }
.tqual__n { margin: 1rem 0 0; font-size: 0.78rem; line-height: 1.55; color: var(--ink-soft); }
/* DT-P176: the per-row mark. Two states in one list, so the difference has to
   be readable at a glance and must not be carried by colour alone — the seen
   one is prefixed with a tick and both are spelled out in words. Quiet by
   design: the qualification is the fact, and the mark is a note about where
   the fact came from. */
.tqual__m { display: block; margin-top: 0.15rem; font-size: 0.78rem; line-height: 1.4; color: var(--ink-soft); }
.tqual__m--seen { color: var(--ink); font-weight: 600; }
.tqual__m--seen::before { content: '✓'; margin-right: 0.3rem; }
@media (max-width: 44rem) {
  .tbio--photo { grid-template-columns: 1fr; }
  .tbio__p { max-width: 13rem; }
}
/* The enquiry, ruled off from the bio: it is a different act from reading. */
.tenq { margin: 2.6rem 0 0; padding: 1.8rem 0 0; border-top: 1px solid var(--line); max-width: 42rem; }
.tenq h2 { margin: 0 0 0.5rem; }

/* DT-P64: verified reviews on a teacher's page.
   The badge carries a NUMBER — how many classes that household attended — and
   it is the number, not the word "verified", that does the work. Anybody can
   print a tick; only this product can say the family was in the room eleven
   times. So the count is set at the same weight as the stars rather than
   trailing off in grey after them. */
.tvr { margin: 2.6rem 0 0; padding: 1.8rem 0 0; border-top: 1px solid var(--line); }
.tvr h2 { margin: 0 0 0.35rem; }
.tvr__s { margin: 0 0 1.4rem; font-size: 0.95rem; color: var(--ink-soft); }
.tvr__l { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.tvr__l li { margin: 0; padding: 1.15rem 1.25rem; border: 1px solid var(--line); border-radius: 14px; background: #fff; }
.tvr__h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem 0.7rem; margin: 0 0 0.7rem; }
.tvr__r { color: var(--stagelight-deep); letter-spacing: 0.08em; font-size: 1rem; }
.tvr__v { font-family: var(--display); font-size: 0.74rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-soft); }
.tvr__w { margin: 0; line-height: 1.65; }
.tvr__b { margin: 0.7rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }

/* The review form itself: one page, one job, nothing else on it. A person who
   followed a link out of an email has already agreed to spend a minute, and
   every extra control is a chance to spend it somewhere else. */
.rvw { max-width: 40rem; }
.rvw__k { font-family: var(--display); font-size: 0.76rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-soft); margin: 0 0 0.5rem; }
.rvw .pick { font-size: 0.98rem; }
.rvw__st { color: var(--stagelight-deep); letter-spacing: 0.09em; }
.roster__n { color: inherit; text-decoration: none; border-bottom: 1px solid var(--line-dark); }
@media (hover: hover) { .roster__n:hover { color: var(--stagelight-deep); } }

/* DT-P62: the photograph in the studio, beside its own controls. */
.sphoto { display: flex; gap: 1rem; align-items: flex-start; }
.sphoto__i { width: 7rem; height: 7rem; border-radius: 14px; object-fit: cover; flex: 0 0 auto; background: var(--bg-soft); }
/* A file input is a control like any other, and was the one that still looked
   like 1997 next to fields this product had already made uniform. */
/* DT-P73: every file input in the product, not only the ones inside a studio
   form. The room's music picker and the store's upload are not .sform, so they
   were falling back to the browser's own control — "Choose File" jammed hard
   against "no file selected" with no space between a button and the sentence
   describing it. */
input[type="file"] { padding: 0.55rem 0.75rem; font-size: 0.85rem; cursor: pointer; line-height: 1.6; }
/* Both spellings. Safari only learned ::file-selector-button in 16.4 and still
   answers to the -webkit- name. They cannot be combined into one selector
   list: a browser that does not understand one of them drops the whole rule.

   THE GAP IS A BORDER, NOT A MARGIN. Twice now a margin-right on this
   pseudo-element has failed to arrive in Safari while measuring correctly in
   Chromium, which is how "fixed" and "still too close" were both true at the
   same time. Margins on the file button are honoured inconsistently across
   engines; a border is part of the box and every engine draws it. So the
   separation is a 1.15rem transparent right border, with background-clip:
   padding-box so the button's white fill stops at the padding edge and the
   field's own background shows through the gap, and the pill outline drawn as
   an inset box-shadow because the border is now doing a different job.

   The margin stays as well. It costs nothing where it works and this is the
   third attempt at four pixels of empty space. */
input[type="file"]::file-selector-button {
  margin-right: 0.35rem; min-height: 34px; padding: 0 0.95rem; border-radius: 999px;
  border: 1px solid transparent; border-right-width: 1.15rem; background-clip: padding-box;
  box-shadow: inset 0 0 0 1px var(--border-strong);
  background-color: #fff; font-family: var(--display); font-size: 0.8rem;
  font-weight: 500; color: var(--ink); cursor: pointer;
  transition: background-color var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve), transform var(--tap) var(--curve);
}
input[type="file"]::-webkit-file-upload-button {
  margin-right: 0.35rem; min-height: 34px; padding: 0 0.95rem; border-radius: 999px;
  border: 1px solid transparent; border-right-width: 1.15rem; background-clip: padding-box;
  box-shadow: inset 0 0 0 1px var(--border-strong);
  background-color: #fff; font-family: var(--display); font-size: 0.8rem;
  font-weight: 500; color: var(--ink); cursor: pointer;
  transition: background-color var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve), transform var(--tap) var(--curve);
}
/* Rule 2 and rule 3 of the button protocol apply to this one too: it is a
   button, whoever draws it. */
@media (hover: hover) { input[type="file"]::file-selector-button:hover { background-color: var(--bg-soft); box-shadow: inset 0 0 0 1px var(--ink-soft); } }
@media (hover: hover) { input[type="file"]::-webkit-file-upload-button:hover { background-color: var(--bg-soft); box-shadow: inset 0 0 0 1px var(--ink-soft); } }
input[type="file"]::file-selector-button:active { transform: translateY(1px); }
input[type="file"]::-webkit-file-upload-button:active { transform: translateY(1px); }

/* DT-P54: a choice a person makes with one tap. Radios and checkboxes on the
   long public forms are boxes you can hit, not 13px dots with a label floating
   beside them — the account-kind question is the single most consequential
   thing anybody answers here and it should not look like fine print. */
.pick {
  display: flex; align-items: flex-start; gap: 0.75rem;
  margin: 0.6rem 0 0; padding: 0.85rem 1rem;
  border: 1px solid var(--line); border-radius: 12px;
  font-weight: 400; font-size: 1rem; line-height: 1.55; cursor: pointer;
}
@media (hover: hover) { .pick:hover { border-color: var(--stagelight-deep); } }
.pick:has(> input:checked) { border-color: var(--stagelight-deep); background: rgba(216,150,74,0.07); }
.pick > input { flex: 0 0 auto; margin: 0.25rem 0 0; }
.form--long > .pick + .pick { margin-top: 0.5rem; }
.form--long > .pick + p.soft, .form--long > .pick + label { margin-top: 1.1rem; }
.form--long > blockquote.signable + .pick { margin-top: 1.2rem; }
main ul, main ol { margin: 0.9rem 0; padding-left: 1.15rem; }
main li { margin: 0.5rem 0; line-height: 1.6; }
main li::marker { color: var(--ink-soft); }
/* Controls are a change of gear, not another sentence. */
main .actions { margin-top: 1.9rem; }
main form + h2 { margin-top: 2.6rem; }
main blockquote { margin: 1.1rem 0; }

/* the one gradient that carries every accent */
.accent, .hero h1 em, h2 em { font-family: var(--serif); font-style: italic; color: var(--stagelight-deep); }

/* ---- masthead ------------------------------------------------------- */
/* D-18: in the iPhone app the page runs under the camera and the clock, because
   the app's web view is the whole screen (viewport-fit=cover, declared in every
   head). The bar keeps its contents below that strip; in a browser the inset
   is 0 and nothing changes. The first-screen arithmetic below subtracts the
   same inset, so the fold still lands where DT-P167 put it. */
.masthead { background: rgba(255,255,255,0.88); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 10; padding-top: env(safe-area-inset-top, 0px); }
/* DT-P171: and above the narrow breakpoint the masthead is exactly what
   --masthead-h says it is, rather than nearly.
   The first screen is sized by subtracting that token from the window, and the
   token was an estimate of a height the contents decided: 67.984px on a wide
   window, 68.58px at 768px. Both are wrong, in opposite directions, and the
   0.016px shortfall is what put a row of cream at the bottom of Philip's
   screen. From 48rem up the bar is a single nowrap row — the same row at every
   width — so fixing its height changes nothing anyone can see and makes the
   arithmetic beneath it exact. Below 48rem it is left alone: the bar can wrap
   there, and a wrapped bar inside a fixed height would lose a line. */
@media (min-width: 48rem) {
  .masthead { height: calc(var(--masthead-h) + env(safe-area-inset-top, 0px)); box-sizing: border-box; }
  .masthead__inner { height: 100%; }
}
.masthead__inner { max-width: 82rem; margin: 0 auto; padding: 0.9rem 1.6rem; display: flex; flex-wrap: nowrap; gap: 0.5rem 1.7rem; align-items: center; }
.masthead__links { display: flex; flex-wrap: nowrap; gap: 1.7rem; align-items: baseline; }
.masthead__drop { display: none; position: relative; }
@media (max-width: 52rem) {
  .masthead__links { display: none; }
  .masthead__drop { display: block; }
}
.masthead__drop summary { list-style: none; cursor: pointer; font-family: var(--body); font-weight: 600; font-size: 0.85rem; letter-spacing: 0.04em; color: var(--ink-soft); padding: 0.45rem 1.15rem; border: 1px solid var(--line); border-radius: 999px; user-select: none; }
.masthead__drop summary::-webkit-details-marker { display: none; }
.masthead__drop[open] summary { color: var(--stagelight-deep); border-color: var(--stagelight-deep); }
.masthead__panel { position: absolute; right: 0; top: calc(100% + 0.7rem); background: #ffffff; border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 24px 54px -22px rgba(23,20,31,0.4); padding: 1rem 1.3rem; display: flex; flex-direction: column; align-items: flex-start; gap: 0.9rem; min-width: 13.5rem; z-index: 40; }
.masthead__panel .masthead__gate { align-self: stretch; text-align: center; }
body:has(.hero--cinema) .masthead__drop summary { color: var(--cinema-soft); border-color: rgba(242,239,232,0.3); }
body:has(.hero--cinema) .masthead__drop[open] summary { color: var(--stagelight); border-color: var(--stagelight); }
body:has(.hero--cinema) .masthead__panel { background: #14111c; border-color: rgba(242,239,232,0.16); }
.masthead__name { font-family: var(--display); font-weight: 700; font-size: 1.3rem; letter-spacing: -0.02em; color: var(--ink); text-decoration: none; margin-right: auto; }
.masthead__name b { font-weight: 700; color: var(--stagelight-deep); }
.masthead__inner a:not(.masthead__name) { text-decoration: none; color: var(--ink-soft); font-size: 0.85rem; font-weight: 600; letter-spacing: 0.02em; padding-bottom: 2px; border-bottom: 2px solid transparent; transition: color var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve); }
@media (hover: hover) { .masthead__inner a:not(.masthead__name):hover { color: var(--stagelight-deep); border-bottom-color: var(--stagelight-deep); } }
.masthead__inner a.masthead__gate { border: 1px solid var(--line); border-radius: 999px; padding: 0.4rem 1rem; border-bottom-width: 1px; transition: border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve); }
@media (hover: hover) { .masthead__inner a.masthead__gate:hover { border-color: var(--stagelight-deep); } }
.masthead__gate i { font-family: var(--serif); font-style: italic; font-weight: 300; text-transform: none; letter-spacing: 0.02em; margin-right: 0.35rem; }
body:has(.hero--cinema) .masthead__inner a.masthead__gate { border-color: rgba(242,239,232,0.25); }
@media (hover: hover) { body:has(.hero--cinema) .masthead__inner a.masthead__gate:hover { border-color: var(--stagelight); } }

/* ---- hero: light, vivid, moving -------------------------------------- */
.hero { position: relative; background: var(--bg); overflow: hidden; }
.hero::before { content: ''; position: absolute; width: 52rem; height: 52rem; border-radius: 50%; background: radial-gradient(circle, rgba(233,168,98,0.14), transparent 62%); top: -18rem; left: -14rem; animation: drift 18s ease-in-out infinite alternate; pointer-events: none; }
.hero::after { content: ''; position: absolute; width: 46rem; height: 46rem; border-radius: 50%; background: radial-gradient(circle, rgba(233,168,98,0.1), transparent 60%); bottom: -16rem; right: -10rem; animation: drift 22s ease-in-out infinite alternate-reverse; pointer-events: none; }
@keyframes drift { from { transform: translate(0,0) scale(1); } to { transform: translate(6rem,4rem) scale(1.18); } }
.hero__inner { position: relative; z-index: 1; max-width: 82rem; margin: 0 auto; padding: clamp(3.5rem, 8vw, 6.5rem) 1.6rem clamp(3rem, 7vw, 5.5rem); display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: center; }
@media (min-width: 60rem) { .hero__inner { grid-template-columns: 1.05fr 0.95fr; } }
.hero__copy > * { animation: enter 0.9s cubic-bezier(.2,.7,.2,1) both; }
.hero__copy > :nth-child(2) { animation-delay: 0.1s; } .hero__copy > :nth-child(3) { animation-delay: 0.2s; }
.hero__copy > :nth-child(4) { animation-delay: 0.3s; } .hero__copy > :nth-child(5) { animation-delay: 0.4s; }
@keyframes enter { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
.hero__kicker { display: inline-block; font-family: var(--body); font-size: 0.78rem; letter-spacing: 0.3em; text-transform: uppercase; font-weight: 600; color: var(--stagelight-deep); }
.hero h1 { color: var(--ink); }
.hero__lede { max-width: 48ch; font-size: clamp(1.05rem, 1.5vw, 1.2rem); color: var(--ink-soft); }
.hero__cta { margin-top: 1.6rem; display: flex; flex-wrap: wrap; gap: 0.9rem; align-items: center; }
.hero__note { color: var(--ink-soft); font-size: 0.95rem; }

/* the photo cluster: colour photography in a gradient ring, slowly alive */
.hero__art { position: relative; min-height: 22rem; }
.hero__art .frame { position: relative; border-radius: 24px; overflow: hidden; box-shadow: 0 30px 70px -30px rgba(8,7,13,0.55); }
.hero__art .frame::before { content: ''; position: absolute; inset: 0; border-radius: 24px; border: 1px solid rgba(233,168,98,0.65); z-index: 2; pointer-events: none; }
.hero__art .frame img { display: block; width: 100%; height: clamp(22rem, 44vw, 30rem); object-fit: cover; transform-origin: 60% 30%; animation: kenburns 26s ease-in-out infinite alternate; }
.hero__art .frame img.hero__alt { position: absolute; inset: 0; height: 100%; opacity: 0; animation: kenburns 26s ease-in-out infinite alternate, heroswap 22s ease-in-out infinite; transform-origin: 40% 45%; }
@keyframes kenburns { from { transform: scale(1.02); } to { transform: scale(1.14) translate(-1.5%, -1%); } }
@keyframes heroswap { 0%, 38% { opacity: 0; } 50%, 88% { opacity: 1; } 100% { opacity: 0; } }
.hero__art .chip { position: absolute; z-index: 3; left: -1rem; bottom: 1.6rem; background: #ffffff; border: 1px solid var(--line); border-radius: 14px; padding: 0.7rem 1.1rem; box-shadow: 0 18px 40px -18px rgba(25,21,39,0.35); font-weight: 600; font-size: 0.9rem; animation: chipfloat 6s ease-in-out infinite alternate; }
.hero__art .chip b { display: block; font-family: var(--serif); font-weight: 600; font-size: 1.3rem; color: var(--stagelight-deep); }
@keyframes chipfloat { from { transform: translateY(0); } to { transform: translateY(-10px); } }

/* ---- the gateway: two doors, one house ------------------------------- */
.gateway { display: flex; flex-direction: column; min-height: 100svh; background: var(--cinema); color: var(--cinema-text); }
.gateway__doors { display: flex; flex-direction: column; flex: 1; }
@media (min-width: 54rem) { .gateway__doors { flex-direction: row; } }
.gateway__safety { margin: 0; padding: 0.8rem 1.6rem; text-align: center; font-size: 0.85rem; color: var(--cinema-soft); border-top: 1px solid rgba(242,239,232,0.12); }
.gateway__safety a { color: var(--stagelight); }
.gateway__signin { margin: 0; padding: 0.9rem 1.6rem 0.2rem; text-align: center; font-size: 0.9rem; color: var(--cinema-soft); border-top: 1px solid rgba(242,239,232,0.12); }
.gateway__signin a { color: var(--stagelight); }
.gateway__foot { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.4rem 2rem; padding: 0.8rem 1.6rem; border-top: 1px solid rgba(242,239,232,0.12); font-size: 0.85rem; color: var(--cinema-soft); }
.gateway__foot p { margin: 0; padding: 0; border: 0; text-align: left; }
.gateway__foot a { color: var(--stagelight); }
.gateway__legal { white-space: nowrap; }
@media (max-width: 40rem) { .gateway__foot { flex-direction: column; align-items: flex-start; } }
.gateway__door { position: relative; flex: 1; min-height: 50svh; display: flex; align-items: flex-end; overflow: hidden; text-decoration: none; color: var(--cinema-text); transition: flex 750ms cubic-bezier(.25,.8,.25,1); container-type: inline-size; }
@media (min-width: 54rem) { .gateway__door:focus-visible { flex: 1.55; } @media (hover: hover) { .gateway__door:hover { flex: 1.55; } } }
.gateway__door + .gateway__door { border-top: 1px solid rgba(242,239,232,0.18); }
@media (min-width: 54rem) { .gateway__door + .gateway__door { border-top: 0; border-left: 1px solid rgba(242,239,232,0.18); } }
.gateway__door img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 30%; opacity: 0.85; filter: contrast(1.04) saturate(1.05) brightness(0.96); transform: scale(1.06); transition: transform 900ms cubic-bezier(.2,.7,.2,1), opacity 600ms ease; }
@media (hover: hover) { .gateway__door:hover img { transform: scale(1.12); opacity: 1; } }
.gateway__doors .gateway__door:first-child img { object-position: center 22%; }
.gateway__doors .gateway__door:last-child img { object-position: center 78%; }
.gateway__door .scrim { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(8,7,13,0.5) 0%, rgba(8,7,13,0.05) 40%, rgba(8,7,13,0.92) 100%); }
@media (max-width: 54rem) { .gateway__door .scrim { background: linear-gradient(180deg, rgba(8,7,13,0.4) 0%, rgba(8,7,13,0.08) 34%, rgba(8,7,13,0.96) 100%); } }
.gateway__label { position: relative; z-index: 1; padding: clamp(1.8rem, 4vw, 3.5rem); padding-bottom: clamp(2.6rem, 6vh, 4.5rem); width: 100%; }
body:has(.notice) .gateway__label { padding-bottom: clamp(8rem, 16vh, 11rem); }
/* DT-P73: the gateway on a phone.
   Two faults, both only visible at 390px. The page ended in a band of the
   site's off-white below the doors, because .gateway is min-height 100svh and
   two stacked doors plus the notice and the safety line are TALLER than that —
   so the body showed through underneath. And the privacy notice's padding was
   applied to both labels, which on a stacked layout pushed the first door's
   wordmark up out of the dark foot of its scrim and into the bright middle of
   the photograph, where it could not be read. Stacked, the notice only covers
   the LAST door, so only that one needs the room. */
body:has(.gateway) { background: var(--cinema); }
@media (max-width: 54rem) {
  body:has(.notice) .gateway__doors .gateway__door:first-child .gateway__label { padding-bottom: clamp(2.6rem, 6vh, 4.5rem); }
  body:has(.notice) .gateway__doors .gateway__door:last-child .gateway__label { padding-bottom: clamp(7rem, 20vh, 10rem); }
  /* And a deeper foot on the scrim, because a phone reads this outdoors. */
  .gateway__door .scrim { background: linear-gradient(180deg, rgba(8,7,13,0.45) 0%, rgba(8,7,13,0.12) 28%, rgba(8,7,13,0.99) 100%); }
}
/* DT-P167 trimmed this with the rule it mirrors: it clears the cookie notice,
   and 18vh cleared it twice over. */
body:has(.notice) .hero--cinema .titles { padding-bottom: clamp(7rem, 13vh, 9.5rem); }
body:has(.notice) .feature__header .titles { padding-bottom: clamp(8rem, 16vh, 11rem); }
/* while the notice stands, every page ends with room to scroll clear of it */
/* DT-P41: the notice used to be position:fixed with 8rem of body padding
   reserved underneath it, and the two numbers disagreed. 8rem is 128px; the
   notice is about 107px on a wide screen and more when the sentence wraps, so
   scrolling to the bottom of a page revealed whichever part of the reserved
   strip the notice did not cover — a band of bare page above the banner. No
   single number could have been right: too small and the last line of content
   hides behind the notice, too large and the gap appears.

   So the notice stops floating and takes real space instead. Sticky rather
   than fixed means it occupies its own height in the flow — nothing to reserve
   and nothing to guess — while still sitting against the bottom of the window
   as you scroll. The flex column with an auto top margin is what keeps it
   there on a page shorter than the window, where a sticky element would
   otherwise sit wherever the content happened to end. */
body:has(.notice) { padding-bottom: 0; min-height: 100vh; display: flex; flex-direction: column; }
/* order:1 rather than a markup change. The notice is emitted by each site
   module inside the page body, but the safety footer is appended by the shell
   AFTER it, so on the long marketing pages the notice is not the last element
   and an auto top margin pinned it halfway up. Flex order puts it last
   visually wherever it was written, without asking every caller to move it. */
body:has(.notice) > .notice { order: 1; margin-top: auto; }
body:has(.notice) .totop { bottom: 8.5rem; }
@media (max-width: 40rem) { body:has(.notice) .totop { bottom: 13.5rem; } }
.gateway__label > * { animation: titleup 1.4s cubic-bezier(.16,.6,.2,1) both; }
.gateway__label > :nth-child(2) { animation-delay: 0.35s; }
.gateway__label > :nth-child(3) { animation-delay: 0.7s; }
.gateway__label > :nth-child(4) { animation-delay: 1s; }
.gateway__kicker { display: block; color: #ffffff; font-size: clamp(0.56rem, 2.5vw, 0.78rem); letter-spacing: 0.28em; text-transform: uppercase; font-weight: 600; margin-bottom: 0.8rem; white-space: nowrap; text-shadow: 0 1px 2px rgba(8,7,13,0.95), 0 2px 8px rgba(8,7,13,0.85), 0 4px 22px rgba(8,7,13,0.7); }
.gateway__label h2 { font-family: var(--serif); font-weight: 300; font-size: clamp(1.6rem, min(4.5vw, 9.2cqw), 4rem); color: #ffffff; margin: 0 0 0.5rem; white-space: nowrap; text-shadow: 0 2px 22px rgba(8,7,13,0.65); }
.gateway__label h2 b { font-weight: 300; font-style: italic; color: var(--stagelight); }
.gateway__label p { color: var(--cinema-text); max-width: 34ch; min-height: 3.3em; margin: 0.3rem 0 1.1rem; }
/* equal rules under both doors: the same fixed width either side, so the
   two enter lines read as one level pair whatever their word lengths */
.gateway__enter { display: inline-block; width: 15rem; max-width: 100%; font-family: var(--body); font-size: 0.85rem; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase; color: var(--stagelight); border-bottom: 1px solid var(--stagelight); padding-bottom: 6px; transition: letter-spacing 300ms ease; }
@media (hover: hover) { .gateway__door:hover .gateway__enter { letter-spacing: 0.24em; } }
.gateway__mark { position: absolute; z-index: 2; top: calc(clamp(1.2rem, 3vh, 2.2rem) + env(safe-area-inset-top, 0px)); left: 50%; transform: translateX(-50%); font-family: var(--serif); font-weight: 300; font-style: italic; font-size: 1.5rem; color: var(--cinema-text); letter-spacing: 0.04em; pointer-events: none; }

/* ---- the screens: the working product, framed ------------------------- */
.screens { display: grid; gap: 2.5rem; margin-top: 2.5rem; }
@media (min-width: 60rem) { .screens { grid-template-columns: 1.3fr 1fr; align-items: start; } }
.screen { margin: 0; }
.screen__frame { background: #ffffff; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; box-shadow: 0 30px 70px -34px rgba(8,7,13,0.5); }
.screen__bar { display: flex; gap: 6px; padding: 0.65rem 0.9rem; border-bottom: 1px solid var(--line); background: var(--bg-soft); }
.screen__bar i { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.screen__bar i:first-child { background: var(--stagelight); }
.screen__frame img { display: block; width: 100%; }
.screen figcaption { color: var(--ink-soft); font-size: 0.85rem; margin-top: 0.7rem; letter-spacing: 0.04em; }

/* ---- the cinematic hero (House Lights Down — the homepage overture) ---- */
/* DT-P132. Philip: a very small white bleed across the bottom of the page when
   it first opens. It was there, on both cinema pages, and it grew with the
   window: 4px at 900 tall, 18px at 1080, 108px at 1200.

   Two causes, one shape. The hero was sized as a FRACTION of the window
   (92svh) while the masthead above it is a fixed 68px, and a fraction and a
   fixed number never agree — they happened to cancel at about 850px tall and
   parted company either side of it. Then the min(…, 64rem) cap took over on a
   tall screen and opened the gap wide. What showed through was the off-white
   band beneath, which reads as a rendering fault rather than a hint that there
   is more page.

   So the hero now takes the room that is actually left — the window less the
   masthead — and the cap is gone, because a hero that stops short of the fold
   always shows whatever is under it, and "house lights down" means the screen
   is full. The scroll cue at the foot of the hero now sits exactly on the
   fold, which is where a cue belongs. */
/* DT-P167. Philip: "can we make a small adjustment so that the scrolling
   banner is visible on the landing screen and not below it?"
   The hero asked for the whole screen under the masthead, so the ribbon — the
   one piece of the page that MOVES on its own — sat just below the fold where
   a first-time visitor never saw it. The first screen now holds both: the
   hero gives up exactly the ribbon's height, so the two together are one
   screenful and nothing else creeps up with them.
   --ribbon-h is declared rather than measured because the drill that guards
   this (test/fold-drill.mjs) reads the rendered box and would catch the two
   drifting apart. Pages without a ribbon are unchanged: the variable is
   redeclared to zero on them below. */
/* DT-P171. The white line at the foot of the first screen, found by measuring
   Philip's own screenshot rather than the page: its last row of pixels is
   #fdfcf9 — the body's cream — one device pixel tall, under a row of the
   ribbon's border.
   The arithmetic is 0.016px out. --masthead-h says 4.25rem, which is 68px, but
   the masthead is sized by its contents and measures 67.984px, so the first
   screen is subtracting a shade more than the masthead takes and stops
   0.016px short of the window. The fold drill has a 1px tolerance and called
   that "not a bleed" — true on a 1× screen, wrong on the 2× one he is looking
   at, where a boundary 0.016px above the bottom still rounds to a whole light
   device pixel. Both sites show it because both do the same subtraction: on
   /teachers the cream appears under the ribbon, on /dancers under the hero.
   So the first screen now claims ONE PIXEL MORE than the sum says it needs.
   The estimate can drift either way with type and zoom; an overshoot means the
   drift is absorbed by a pixel of near-black below the fold instead of a pixel
   of cream above it. The cost is the ribbon's bottom hairline sitting just
   under the fold, which nobody can see; the cream, which everybody could, is
   gone. */
.hero--cinema { position: relative; background: var(--cinema); color: var(--cinema-text); overflow: hidden; min-height: calc(100svh - var(--masthead-h) - env(safe-area-inset-top, 0px) - var(--ribbon-h, 0px) + 1px); display: flex; align-items: flex-end; }
.hero--cinema::before, .hero--cinema::after { display: none; }
.hero--cinema .backdrop { position: absolute; inset: 0; }
.hero--cinema .backdrop img { width: 100%; height: 100%; object-fit: cover; object-position: center 30%; opacity: 0.92; filter: contrast(1.06) saturate(1.08) brightness(0.94); transform-origin: 55% 32%; animation: pushin 34s ease-out both; }
@keyframes pushin { from { transform: scale(1.14); } to { transform: scale(1.02); } }
.hero--cinema .vignette { position: absolute; inset: 0; background:
  linear-gradient(180deg, rgba(8,7,13,0.72) 0%, rgba(8,7,13,0.1) 26%, rgba(8,7,13,0.08) 52%, rgba(8,7,13,0.9) 86%, #08070d 100%),
  radial-gradient(120% 90% at 50% 40%, transparent 55%, rgba(8,7,13,0.55) 100%); }
/* DT-P167: the padding is what actually decides whether the ribbon is seen.
   The hero's MIN-height gave up the ribbon's height, but on a 900px-tall
   desktop the content was taller than that anyway — 16vh of air above the
   kicker and 9vh below the note — so the hero overhung and the ribbon stayed
   under the fold. The air comes down to 9vh and 6vh, which on a tall screen
   still reads as a stage and on a short one is the difference between seeing
   the ribbon move and never knowing it was there. */
.hero--cinema .titles { position: relative; z-index: 1; width: 100%; max-width: 82rem; margin: 0 auto; padding: clamp(3.5rem, 8.5vh, 8rem) 1.6rem clamp(2.25rem, 5.5vh, 4.5rem); display: grid; }
.hero--cinema .titles > * { animation: titleup 1.6s cubic-bezier(.16,.6,.2,1) both; }
.hero--cinema .titles > :nth-child(1) { animation-delay: 0.3s; }
.hero--cinema .titles > :nth-child(2) { animation-delay: 0.9s; }
.hero--cinema .titles > :nth-child(3) { animation-delay: 1.7s; }
.hero--cinema .titles > :nth-child(4) { animation-delay: 2.3s; }
.hero--cinema .titles > :nth-child(5) { animation-delay: 2.7s; }
@keyframes titleup { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }
.hero--cinema .hero__kicker { border: 0; background: none; padding: 0; color: var(--stagelight); font-size: 0.8rem; letter-spacing: 0.34em; text-transform: uppercase; font-weight: 600; }
/* DT-P90: the mark, on the one dark ground this site has.
   The logo is drawn on near-black, so it belongs where the page is already
   near-black rather than pasted as a black square onto cream. mix-blend-mode
   screen was the first attempt and it FAILED on the live page: the hero's
   own stacking context isolated the blend, so the mark rendered as a grey
   rectangle over the photograph — worse than doing nothing, and visible only
   once it was deployed rather than in any test.

   So the transparency is baked into the file instead. Alpha is derived from
   the image's own luminance, which is arithmetically what 'screen over black'
   does — the same result, computed once at build time, composited correctly
   on any ground by every engine, and depending on no blend mode at all.
   620px PNG-8 with alpha: 38KB, against 946KB for the original.

   DT-P94, Philip: the mark had been dropped INTO the title block, so it
   pushed the headline and the call to action down and the hero's own line —
   "Run your dance school from one place" — fell below the fold on a laptop.
   A logo is not a paragraph. It now sits as a LAYER: out of .titles, anchored
   to the hero itself, over the photograph, with the titles standing exactly
   where they stood before it existed.

   TOP-RIGHT, not top-left: the first attempt put it top-left and it landed on
   the kicker and the first line of the headline, which is a different way of
   ruining the same composition. The titles occupy the left two-thirds at every
   width; the upper right is stage and shadow. Its right edge lines up with the
   text column's rather than the viewport's, so mark and headline read as one
   composition rather than two things that happen to share a photograph. */
.hero__mark { position: absolute; z-index: 1; top: clamp(1.2rem, 5vh, 3.5rem); right: max(1.2rem, calc((100% - 82rem) / 2 + 0.2rem)); width: min(15rem, 34vw); height: auto; pointer-events: none; }
@media (max-width: 40rem) { .hero__mark { width: min(10rem, 38vw); top: 0.9rem; right: 0.9rem; } }
/* A viewer who has asked for less motion is not asking for less brand, so
   nothing here animates in the first place — the mark is static by design. */
.hero--cinema h1 { font-family: var(--serif); font-weight: 300; letter-spacing: -0.01em; line-height: 1.02; font-size: clamp(3rem, 8.5vw, 7rem); max-width: 16ch; margin: 1rem 0 1.1rem; color: #ffffff; text-shadow: 0 2px 40px rgba(8,7,13,0.6); }
.hero--cinema h1 em { font-family: var(--serif); font-style: italic; font-weight: 300; background: none; -webkit-text-fill-color: currentColor; color: var(--stagelight); }
/* ---- DT-P164: the follow spot ----------------------------------------
   Philip: "on the landing page for teachers, and the parallel landing page
   for students, can we apply the same light effect moving across the hero
   text once it has settled as we have on the hero text for arcgabriel.com?"

   The same technique, and the notes that make it work are ArcGabriel's own,
   kept because they were paid for in mistakes:

   BACKGROUND-CLIP: TEXT. The letters are painted by a gradient three times
   the width of the words, positioned off their right edge and slid to the
   left. Moving the window left moves the visible feature RIGHT, which is
   what sends the light across the words.

   WIDTH: FIT-CONTENT, so the gradient is sized to the letters rather than to
   the column. The light then crosses exactly the words and leaves at the last
   glyph rather than somewhere out in the margin.

   THE DESCENDERS. Once text is painted by a background, the paint is bounded
   by the element's box and WebKit enforces that strictly: without the padding
   the tail of the 'g' in "dancing" is sliced off by a flat horizontal cut.
   The padding is cancelled by an equal negative margin so nothing moves.

   WHAT IS DIFFERENT HERE, and it is the whole of the adaptation. ArcGabriel
   sweeps dark ink on pale paper, so its band is a bright gold-to-white
   highlight. This headline is already white on a near-black stage — there is
   nothing brighter than white to light it with. So the band is the product's
   own stagelight amber instead: a warm light crossing white letters, which on
   this page is not a metaphor. The core is 49.6% to 50.4% of a 300% gradient,
   about a third of a glyph at any instant, with everything either side
   returning to white within a further 3%. The rule to keep if this is edited
   is ArcGabriel's: widen the band and the headline starts to disappear.

   THE WAIT. Philip asked for the light AFTER the text settles. The headline
   is the second child of .titles, so it enters on a 1.6s curve at 0.9s and is
   at rest at 2.5s. The sweep opens its travel off the leading edge, where
   there is nothing to light up, so the first fifth looks like nothing
   happening — it therefore starts at 2.1s and the light is on the first
   letters at about the moment the words stop moving.

   Reduced motion: no animation, and the frozen position is the far end of the
   gradient, which is plain white. A viewer who asked for less motion sees
   exactly the headline that shipped before this existed. */
/* DT-P168. "How do we make the light as sharp and bright as arcgabriel.com?"
   asked a third time, so I went and measured theirs instead of reading it.
   Two things were different, and neither was the colour.
   ONE: THEIR BAND IS ENORMOUS. Its .ln element is 298px — one LINE — and the
   band spans 44.5% to 55.5% of a gradient three times that, which is 98px:
   a THIRD of the line, glowing, with a 7px white core inside it. Mine was
   4.2% of a 1115px element that held both lines at once: a 140px event on
   words nearly four times as wide, which is 12% where theirs is 33%. I had
   been narrowing it on the word "tighter" and making it smaller and quieter
   with every pass. The band is now their geometry exactly, on a per-LINE
   element that is a third the width, staggered by their 180ms so the second
   line catches the light a moment after the first.
   TWO: A DARK PAGE NEEDS BLOOM. Theirs is dark ink on pale paper, so a gold
   band is LIGHTER than the letters and reads as light without any help. Ours
   is near-white letters on a black stage, where nothing is brighter than
   white — which is why two passes of recolouring could not make it brighter.
   What actually makes a light look bright on a dark stage is halation: the
   spill around the hot spot. So there is a second layer behind the letters,
   the same text, painted by a gradient that is TRANSPARENT everywhere except
   the band, blurred. Where the core passes, the letters glow into the black
   around them. That is the mechanism, and it is the only one available on a
   dark ground. */
.hero--cinema h1 .ln {
  display: block; width: fit-content; position: relative; isolation: isolate;
  padding-bottom: 0.34em; margin-bottom: -0.34em;
}
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .hero--cinema h1 .ln {
    /* DT-P170: tighter, and measured rather than judged. DT-P168 copied
       ArcGabriel's PERCENTAGES, which is the wrong unit to copy: their line is
       298px, ours is 619px and 790px at laptop width, so the same 11% of a
       300% gradient that gives them a 98px band gave us one of 204px and
       261px. The light was not too wide as a fraction; it was twice as wide as
       the thing Philip was comparing it to, in pixels, on the screen.
       The flanks are now pulled in to 0.54 of their spread, which puts the
       band at 110px and 140px — their scale of event, on our scale of line —
       while the WHITE CORE is left exactly where it was at 49.6/50/50.4 and so
       gets WIDER in pixels than theirs, not narrower. That is the reference's
       rule used in the direction it permits: widening the pale flanks makes
       the headline disappear, a hotter core does not. Short fall-off, hard
       centre. */
    background-image: linear-gradient(96deg,
      #f2efe8 0%, #f2efe8 45.7%,
      #8a6a2c 47.05%, #c08f34 47.96%, #e9a862 48.71%,
      #f8dcae 49.36%, #fffaf0 49.6%, #ffffff 50%, #fffaf0 50.4%,
      #f8dcae 50.64%, #e9a862 51.29%, #c08f34 52.04%, #8a6a2c 52.95%,
      #f2efe8 54.3%, #f2efe8 100%);
    background-size: 300% 100%;
    background-position: 100% 0;
    background-repeat: no-repeat;
    -webkit-background-clip: text; background-clip: text;
    color: transparent; -webkit-text-fill-color: transparent;
    /* DT-P171: slower. 3200ms to 4600ms — the same light, given half again as
       long to cross the line, so the eye follows it rather than catching it.
       The start is unchanged at 2100ms, which is where the titles stop moving;
       only the travel is stretched. The second line's stagger stretches with
       it, 180ms to 260ms, so the two lines stay in the same relationship. */
    /* arcwright-allow: literal-duration-on-control — a marketing set piece, which AW-109 exempts by name: a headline being lit is not a control answering a finger */
    animation: arcsheen 4600ms linear 2100ms forwards;
  }
  /* The bloom. Same text, same travel, but its gradient is transparent
     everywhere the band is not — so nothing glows until the light arrives.
     A REAL element carrying aria-hidden, not generated content: Chrome puts
     ::before text into the accessibility tree, and a headline read out twice
     is a worse defect than a dull one. */
  .hero--cinema h1 .ln .glow {
    position: absolute; left: 0; top: 0; z-index: -1; pointer-events: none;
    font-style: normal; white-space: nowrap;
    /* DT-P169: the same room below the baseline the visible line gets. Once
       text is painted by a background WebKit bounds the paint by the element's
       box, and a blurred copy that is clipped flat shows the cut as a straight
       edge through the glow — the reference's descender note applies to this
       layer exactly as it does to the letters above it. */
    padding-bottom: 0.34em;
    /* DT-P170: the bloom is pulled in by the same 0.54, and the blur with it.
       A halo sized for the old band around the new one would put back in soft
       light exactly what was taken out in hard, and the line would read wider
       than before rather than tighter. */
    background-image: linear-gradient(96deg,
      rgba(255,255,255,0) 0%, rgba(255,255,255,0) 47.59%,
      rgba(233,168,98,0.35) 48.71%, rgba(255,240,214,0.75) 49.36%,
      rgba(255,255,255,0.95) 50%, rgba(255,240,214,0.75) 50.64%,
      rgba(233,168,98,0.35) 51.29%, rgba(255,255,255,0) 52.41%,
      rgba(255,255,255,0) 100%);
    background-size: 300% 100%;
    background-position: 100% 0;
    background-repeat: no-repeat;
    -webkit-background-clip: text; background-clip: text;
    color: transparent; -webkit-text-fill-color: transparent;
    filter: blur(9px) saturate(1.12);
    /* arcwright-allow: literal-duration-on-control — the same set piece; this layer must travel with the one above it */
    animation: arcsheen 4600ms linear 2100ms forwards;
  }
  .hero--cinema h1 .ln:nth-child(2), .hero--cinema h1 .ln:nth-child(2) .glow { animation-delay: 2360ms; }
  @keyframes arcsheen { to { background-position: 0% 0; } }
}
@media (prefers-reduced-motion: reduce) {
  .hero--cinema h1 .ln, .hero--cinema h1 .ln .glow { animation: none; }
  .hero--cinema h1 .ln .glow { display: none; }
}
.hero--cinema .hero__lede { color: var(--cinema-text); max-width: 52ch; font-size: clamp(1rem, 1.4vw, 1.15rem); text-shadow: 0 1px 20px rgba(8,7,13,0.7); }
.hero--cinema .hero__cta { margin-top: 1.7rem; }
/* T-52's exception, and it matters: on the cinema hero the ground is near-black,
   where the amber FILL is already 9.6:1 and --stagelight-deep would be 2.7:1 —
   so the boundary that helps on cream would be the weakest edge on the page.
   The border returns to the fill colour and the fill does the identifying. */
.hero--cinema .btn { background: var(--stagelight); border-color: var(--stagelight); color: var(--text-inverse); box-shadow: 0 18px 44px -18px rgba(233,168,98,0.55); }
@media (hover: hover) { .hero--cinema .btn:hover { box-shadow: 0 24px 54px -18px rgba(233,168,98,0.65); } }
/* DT-P167. Philip: "'I have a code' and 'see the studio' buttons are a bit
   aggressive when clicked. Can we make the click actions smoother and a little
   slower. The colour of the click button also looks out of kilter with the
   site's branding."
   Both complaints were one defect. This rule set the ghost button transparent
   on the cinema hero and its hover set only the border and the text — so
   BACKGROUND fell through to the generic .btn--ghost:hover, which is #f3ead9,
   a cream mixed for an off-white page. On a near-black stage a cream pill
   arrives like a light being switched on in a theatre: that is the colour out
   of kilter, and it is also most of what read as aggressive, because the
   flash happened in 90ms.
   Now the hover stays on the stage: the amber the page already uses, at a
   weight that lifts the button off the backdrop without becoming a slab.
   DT-P170: the LABEL stays white. Taking the text to amber as well was one
   change too many — the word is the thing you are reading, and dimming it to
   a mid-tone at the moment of pointing at it makes the button look like it is
   fading rather than answering. Philip: "when the button is hovered over or
   selected, can we keep the text white." The amber does the lighting from the
   border and the wash; the label is the one part that holds still, and it
   holds at the brightest contrast the control has. */
.hero--cinema .btn--ghost { background: rgba(242,239,232,0.06); color: var(--cinema-text); border: 1.5px solid rgba(242,239,232,0.46); }
@media (hover: hover) {
  .hero--cinema .btn--ghost:hover {
    border-color: var(--stagelight); color: var(--cinema-text);
    background: rgba(233,168,98,0.14); box-shadow: 0 18px 38px -22px rgba(233,168,98,0.5);
  }
}
/* "or selected" — focus and the held press are the same promise. */
.hero--cinema .btn--ghost:focus-visible, .hero--cinema .btn--ghost:active { color: var(--cinema-text); }
/* And the press itself, which is the other half of "smoother and a little
   slower". Every other control in this product answers a finger in --tap,
   90ms, and that is right for a control you are touching on a form. These two
   are the opening gesture of a cinematic hero, and Philip is right that a
   90ms snap is the wrong register for it: the lift and the colour now settle
   over --ease-t instead. Still a declared token, not a number invented here,
   and scoped to the hero so no form button becomes sluggish. */
.hero--cinema .btn, .hero--cinema .btn--ghost {
  transition: transform var(--ease-t) var(--curve), background-color var(--ease-t) var(--curve),
    border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve),
    box-shadow var(--ease-t) var(--curve);
}
.hero--cinema .btn:active, .hero--cinema .btn--ghost:active { transform: translateY(0) scale(0.985); }
@media (prefers-reduced-motion: reduce) {
  .hero--cinema .btn:active, .hero--cinema .btn--ghost:active { transform: none; }
}
.hero--cinema .hero__note { color: var(--cinema-soft); }
/* DT-P170. The scroll cue is gone, from the markup as well as from here.
   It was a hairline falling down the foot of the hero to say there is more
   below. DT-P167 hid it where the ribbon shows, on the reasoning that two
   hints at one edge is one hint and one blemish — which left it standing on
   /dancers, and Philip saw it again: "both sites have a small white line at
   the bottom of the page that shouldn't be there." He is right, and the
   narrower fix was the wrong one. A cue that has to be explained as a cue is
   not reading as a cue; it is reading as a mark on the screen. Both heroes
   are cut off by the fold on purpose, which already says the page continues,
   so nothing replaces it. */

/* the masthead dims the house lights when the overture is on the bill */
body:has(.hero--cinema) .masthead { background: rgba(8,7,13,0.72); backdrop-filter: blur(12px); border-bottom-color: rgba(242,239,232,0.12); }
body:has(.hero--cinema) .masthead__name { color: var(--cinema-text); }
body:has(.hero--cinema) .masthead__name b { background: none; -webkit-text-fill-color: currentColor; color: var(--stagelight); }
body:has(.hero--cinema) .masthead__inner a:not(.masthead__name) { color: var(--cinema-soft); }
@media (hover: hover) { body:has(.hero--cinema) .masthead__inner a:not(.masthead__name):hover { color: var(--stagelight); border-bottom-color: var(--stagelight); } }


/* ---- the ribbon ------------------------------------------------------- */
/* The ribbon's own height, and the page that has one says so. A page with no
   ribbon leaves --ribbon-h at its 0px fallback and its hero still takes the
   whole first screen. */
/* The ribbon's height is DECLARED and then enforced, rather than left to fall
   out of a clamped font size: at 768px the type stepped down, the ribbon came
   out 4px shorter than the hero had given up for it, and a 4px band of page
   showed under the fold. The variable and the box now agree by construction.
   Below 48rem it stays at its 0px fallback: on a phone the first screen is a
   different composition, and buying a marquee with a third of the headline's
   air is a bad trade. The ribbon is still there, one flick down. */
@media (min-width: 48rem) { body:has(.tilt .marquee) { --ribbon-h: 3rem; } }
/* D-18 (Philip, from the iPhone app, 15 Oct 2026): "the scrolling carousel is
   too low, and therefore doesn't show when you first land". The trade above —
   a flick down on a phone — held while the hero fit the screen. On a phone the
   hero's titles are taller than the screen, so the ribbon was not one flick
   down, it was two. Below 48rem the ribbon now sits between the bar and the
   hero: the one piece of the page that moves is on the first screen, and the
   hero below it is the same composition it was. The body is already a column
   when the privacy notice is up; this makes it one whenever a ribbon is. */
@media (max-width: 47.99rem) {
  body:has(.tilt .marquee) { display: flex; flex-direction: column; }
  body:has(.tilt .marquee) > .masthead { order: -2; }
  body:has(.tilt .marquee) > .tilt { order: -1; }
}
.tilt { overflow: clip; margin: 0; position: relative; z-index: 2; }
.marquee { background: var(--cinema); color: var(--cinema-soft); overflow: hidden; border-top: 1px solid rgba(242,239,232,0.1); border-bottom: 1px solid rgba(242,239,232,0.1); }
@media (min-width: 48rem) { .marquee { height: var(--ribbon-h); display: flex; align-items: center; } }
.marquee__track { display: flex; width: max-content; animation: ribbon 28s linear infinite; }
.marquee span { font-family: var(--body); font-weight: 600; text-transform: uppercase; font-size: clamp(0.8rem, 1.4vw, 0.95rem); letter-spacing: 0.22em; padding: 0.7rem 1.2rem 0.6rem 0; white-space: nowrap; }
@keyframes ribbon { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ---- buttons ---------------------------------------------------------- */
.btn, button {
  display: inline-block; padding: calc(0.9rem - 1px) calc(1.9rem - 1px); border-radius: 999px;
  /* T-52. The amber fill is 2.00:1 against the cream page, and WCAG 2.2 1.4.11
     asks 3:1 of whatever IDENTIFIES a control. The 9.11:1 label does that, so
     the fill was defensible — but a boundary settles it: --stagelight-deep is
     5.27:1 against the page, so the button's edge is identifiable under any
     reading of the clause, and the brand colour it is filled with does not
     change. The padding above absorbs the border, so the box is the same size
     it was to the pixel. */
  border: 1px solid var(--stagelight-deep);
  background: var(--stagelight); color: var(--text-inverse); font-family: var(--body); font-size: 0.92rem; font-weight: 600;
  letter-spacing: 0.02em; text-decoration: none; cursor: pointer;
  transition: transform var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve);
  box-shadow: 0 16px 34px -16px rgba(233,168,98,0.7);
}
/* T-20 / AW-108 rule 2 and rule 5. This hover lifted and deepened its shadow
   and left the colour exactly where it was. On a touch device there is no
   hover at all, so movement is the half that is never felt — the colour is the
   half that has to move. --hover-amber is the product's own deeper amber,
   already worn by the operator console's go button and the room's join button,
   so this is the palette agreeing with itself rather than a new colour. It
   deepens: relative luminance 0.462 -> 0.343. */
@media (hover: hover) { .btn:hover, button:hover { background: var(--hover-amber); color: var(--text-inverse); transform: translateY(-2px); box-shadow: 0 22px 44px -14px rgba(233,168,98,0.75); } }
.btn:active, button:active { transform: translateY(0); }
.btn--ghost { background: #ffffff; color: var(--ink); border: 1.5px solid var(--border-strong); box-shadow: none; }
/* Rule 5: --bg-soft on an off-white page is barely a step. */
@media (hover: hover) { .btn--ghost:hover { border-color: var(--stagelight-deep); color: var(--stagelight-deep); background: #f3ead9; box-shadow: none; transform: translateY(-2px); } }
.btn--ghost:active { transform: translateY(0); }
/* DT-P74: motion is a courtesy, not a requirement. */
@media (prefers-reduced-motion: reduce) {
  .btn, button, .btn:active, button:active { transform: none; } @media (hover: hover) { .btn:hover, button:hover { transform: none; } }
}

/* ---- one shape, one row (DT-P39) ---------------------------------------
   Philip: "Every page should have the right button spacing, formatting and
   placement — they should be uniform."
   The buttons were already one style; what was ragged was everything AROUND
   them. Every mutation on this site is its own POST form, and a <form> is a
   block, so two buttons that belong side by side arrived stacked, each with
   whatever margin its neighbour happened to give it. So the fix is not a new
   button — it is a rule that says a form wrapping a single button IS a
   button, and one row class that owns the spacing between them.
   The studio solved this months ago with .sbtn/.srow; this brings the same
   answer to the pages a family and an applicant see. Heights match the
   studio's 44px so a control is the same object everywhere in the product. */
.btn, button { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; line-height: 1; text-align: center; }
/* DT-P217: and a button marked hidden is HIDDEN.
   The line above sets a display, and a display on an element selector beats
   the one [hidden] carries — so every button in this product that starts
   hidden was shown anyway, everywhere except a phone. A phone was excepted
   because somebody hit this on a phone and fixed it inside that media query,
   one storey down from the cause.
   Philip: "there is a button that says turn the sound on. But I cannot figure
   out its functionality. It looks like a redundant button." It is not
   redundant: it is the prompt for a browser that will not play a class until
   somebody asks, and it is supposed to appear only then. "Switch camera" was
   showing on laptops with one camera for the same reason. */
.btn[hidden], button[hidden] { display: none; }
/* DT-P225, and this is the THIRD time this trap has been sprung, so it stops
   being patched one selector at a time.

   [hidden] is an attribute selector, so ANY rule that sets a display on a
   class or an element outranks it and the thing marked hidden is shown anyway.
   It cost us the room's "turn the sound on" button (DT-P217), the room buttons
   on a phone before that, and yesterday the Day field on the sell form: moving
   it inside .sform__pair put it under a rule giving .sform__pair > div a
   display, which outranks [hidden], so a teacher choosing "just once, on a
   date" was still shown a Day menu she could not use. Philip, on seeing it:
   "when they can't select the day, they're not confused about why not."

   !important is the right tool here and not a shortcut. hidden is not styling,
   it is a statement that the element is not part of the page, and there is no
   case in this product where a rule about layout should be allowed to overrule
   it. Anything that genuinely needs to show is not hidden.

   The progressive-disclosure script disables hidden fields as well, so the
   data was never wrong -- but a control somebody can see and cannot touch is
   its own defect, and this is the rule that ends it. */
[hidden] { display: none !important; }

/* DT-021. The honeypot. Positioned off-screen rather than display:none or
   hidden, deliberately: a field the browser considers invisible is a field some
   automation skips, and the point is that a script fills it. It carries a real
   label and aria-hidden, so a screen reader passes over it rather than reading
   a nonsense question to somebody enrolling their child. */
.roomrec { display: inline-flex; align-items: center; gap: .4rem; font-weight: 600; color: #b3261e; }
.roomrec::before { content: ''; width: .6rem; height: .6rem; border-radius: 50%; background: currentColor; }
.roomnotice { margin: 0 0 .5rem; padding: .5rem .75rem; border-radius: .5rem; background: #fdecea; color: #5f1a14; font-weight: 600; }
@media (prefers-reduced-motion: no-preference) { .roomrec::before { animation: roomrecpulse 2s ease-in-out infinite; } }
@keyframes roomrecpulse { 50% { opacity: .35; } }
.sr-trap { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
/* DT-P49: scoped to main, and that scoping is the fix for a bug this rule
   caused. A :has() selector takes the specificity of its argument, so this one
   outranked the studio's own rule and undid its display:contents there,
   turning each single-button form back into an
   inline box that sits on the text baseline. Two buttons side by side then
   aligned by baseline instead of by box, and the one in a form sat lower than
   the one that was a link. The studio and the console have their own control
   systems; this one belongs to the reading pages and now stays in them. */
main form:has(> button:only-child) { display: inline-block; margin: 0; }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 1.5rem 0 0; }
.actions > form:has(> button:only-child) { display: contents; }
/* In a list — a dancer, an invoice, a booking — the button sits ON the line
   it belongs to rather than starting a new one. */
main li > form:has(> button:only-child) { margin-left: 0.5rem; vertical-align: middle; }
main li button, .actions--tight button, .btn--s { min-height: 36px; padding: calc(0.45rem - 1px) calc(1.1rem - 1px); font-size: 0.85rem; }

/* ---- bands ------------------------------------------------------------ */
.band { padding: clamp(3.5rem, 9vw, 6.5rem) 1.6rem; position: relative; overflow: hidden; }
.band__inner { max-width: 82rem; margin: 0 auto; position: relative; }
.band--stage { background: var(--night); color: var(--light); }
.band--stage h2 { color: #ffffff; }
.band--stage p { color: var(--light); }
.band--stage .soft, .band--stage .meta { color: var(--light-soft); }
.band--stage a:not(.btn) { color: var(--stagelight); }
.band--beige { background: var(--night); color: var(--light); }
.band--soft { background: var(--bg-soft); }
.band--slim { padding-top: 1.6rem; padding-bottom: 1.6rem; }
#apply { scroll-margin-top: 3.5rem; }
.band__kicker { font-family: var(--body); font-size: 0.78rem; letter-spacing: 0.24em; text-transform: uppercase; color: var(--stagelight-deep); font-weight: 600; }
.band--stage .band__kicker, .band--beige .band__kicker { color: var(--stagelight); }
.band__lede { max-width: 56ch; font-size: 1.1rem; color: var(--ink-soft); }
.band--stage .band__lede { color: var(--light); }

/* the ghost word behind a band */
.ghost { position: absolute; top: -0.12em; right: -0.05em; font-family: var(--serif); font-weight: 300; text-transform: uppercase; font-size: clamp(5rem, 18vw, 13rem); line-height: 1; letter-spacing: -0.03em; color: transparent; -webkit-text-stroke: 1px var(--line); pointer-events: none; user-select: none; }
.band--stage .ghost, .band--beige .ghost { -webkit-text-stroke: 1px var(--line-dark); }
/* DT-P94, Philip: "'The Bill' and the line that cuts across it needs
   improving. 'The Bill' doesn't make sense."

   He is right twice. THE BILL is theatre jargon — the programme, the running
   order — and it is the kind of word that delights the person who chose it
   and puzzles everybody else. On a page whose whole job is a parent finding a
   dance teacher, the largest word on the screen should not need explaining.
   It reads "Teachers" now, which is what the section is.

   And the rule beneath the heading ran straight through the middle of it, so
   the watermark looked less like a design decision than like two elements
   that had collided. This variant is smaller, sits higher and further out,
   and drops behind the content — clear of the rule, quiet enough to be a
   texture rather than a competitor. */
.ghost--tucked { top: -0.34em; right: -0.02em; font-size: clamp(3.5rem, 11vw, 8rem); z-index: 0; opacity: 0.55; }
@media (max-width: 48rem) { .ghost--tucked { display: none; } }
.band__inner > :not(.ghost) { position: relative; z-index: 1; }
.band--glow::before { content: ''; position: absolute; width: 44rem; height: 44rem; border-radius: 50%; background: radial-gradient(circle, rgba(233,168,98,0.12), transparent 65%); top: -14rem; right: -12rem; animation: drift 16s ease-in-out infinite alternate; pointer-events: none; }

/* ---- the ledger: the programme of what works -------------------------- */
/* the programme of what works, set like programme notes: an aligned grid —
   every horizontal rule runs level across the page, the act numeral sits on
   the rule's right shoulder like a folio mark */
.ledger { list-style: none; padding: 0; margin: 3rem 0 0; position: relative; display: grid; gap: 0 clamp(3rem, 6vw, 5.5rem); }
@media (min-width: 54rem) { .ledger { grid-template-columns: 1fr 1fr; } }
.ledger li { padding: 1.3rem 0 2rem; border-top: 1px solid var(--ink); position: relative; }
.ledger .no { position: absolute; top: 1.35rem; right: 0; font-family: var(--body); font-size: 0.72rem; letter-spacing: 0.28em; font-weight: 600; color: var(--stagelight-deep); }
.ledger h3 { color: var(--ink); margin: 0; padding-right: 5rem; font-size: clamp(1.35rem, 2.4vw, 1.7rem); font-weight: 600; }
.ledger h3::after { content: ''; display: block; width: 2.2rem; height: 2px; background: var(--stagelight); margin-top: 0.55rem; transition: width 300ms cubic-bezier(.2,.7,.2,1); }
@media (hover: hover) { .ledger li:hover h3::after { width: 4.4rem; } }
.ledger p { margin: 0.6rem 0 0; color: var(--ink-soft); max-width: 46ch; font-size: 0.98rem; }

/* ---- the filmstrip: photography in motion ----------------------------- */
.filmstrip { background: var(--bg); overflow: hidden; padding: clamp(1rem, 2.5vw, 2rem) 0; }
.filmstrip__track { display: flex; gap: clamp(0.9rem, 1.6vw, 1.4rem); width: max-content; animation: strip 48s linear infinite; }
@media (hover: hover) { .filmstrip:hover .filmstrip__track { animation-play-state: paused; } }
.filmstrip img { height: clamp(13rem, 30vw, 20rem); width: auto; display: block; border-radius: 18px; box-shadow: 0 22px 44px -24px rgba(8,7,13,0.5); transition: transform 500ms cubic-bezier(.2,.7,.2,1); }
@media (hover: hover) { .filmstrip img:hover { transform: scale(1.05) rotate(-1deg); } }
@keyframes strip { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ---- the honest numbers ----------------------------------------------- */
.numbers { display: grid; gap: 1.6rem 2rem; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); margin-top: 2.5rem; }
.numbers > div { border-left: 1px solid var(--line-dark); padding-left: 1.3rem; }
.numbers .fig { font-family: var(--serif); font-weight: 300; font-size: clamp(3rem, 7.5vw, 5rem); line-height: 1; margin: 0; letter-spacing: -0.02em; color: var(--stagelight); }
.numbers .cap { text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.78rem; font-weight: 600; color: var(--ink-soft); margin-top: 0.4rem; }
.band--stage .numbers .cap, .band--beige .numbers .cap { color: var(--light-soft); }

/* ---- splits ----------------------------------------------------------- */
.split { display: grid; gap: 2.5rem; grid-template-columns: 1fr; align-items: center; margin-top: 2.5rem; }
.split figure { margin: 0; position: relative; }
.split figure::after { content: ''; position: absolute; inset: 0; border-radius: 20px; border: 1px solid var(--stagelight); opacity: 0.85; transform: translate(14px, 14px); z-index: 0; transition: transform 400ms cubic-bezier(.2,.7,.2,1); }
@media (hover: hover) { .split figure:hover::after { transform: translate(8px, 8px); } }
.split img { position: relative; z-index: 1; width: 100%; max-height: min(76svh, 36rem); display: block; border-radius: 20px; object-fit: cover; box-shadow: 0 24px 54px -26px rgba(25,21,39,0.5); transition: transform 700ms cubic-bezier(.2,.7,.2,1); }
@media (hover: hover) { .split figure:hover img { transform: scale(1.02); } }
@media (min-width: 54rem) { .split { grid-template-columns: 1.1fr 1fr; } .split--flip > :first-child { order: 2; } }

/* ---- the price: the poster -------------------------------------------- */
.price { background: var(--cinema); color: var(--cinema-text); padding: clamp(2.5rem, 6vw, 4.5rem); position: relative; overflow: hidden; margin-top: 2.5rem; border-radius: 28px; box-shadow: 0 34px 80px -34px rgba(8,7,13,0.7); }
.price img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.42; }
.price__scrim { position: absolute; inset: 0; background: linear-gradient(100deg, rgba(8,7,13,0.92) 30%, rgba(8,7,13,0.35) 100%); }
.price__inner { position: relative; max-width: 38rem; }
.price .band__kicker { color: var(--stagelight); }
.price__figure { font-family: var(--serif); font-weight: 300; font-size: clamp(4rem, 12vw, 7.5rem); letter-spacing: -0.02em; color: #ffffff; line-height: 0.95; margin: 0.4rem 0; }
.price__figure small { display: block; margin-top: 0.7rem; font-size: 0.16em; letter-spacing: 0.2em; font-family: var(--body); font-weight: 600; text-transform: uppercase; }
.price p { color: #ffffff; }

/* ---- forms ------------------------------------------------------------ */
label { display: block; margin: 1rem 0 0.3rem; font-weight: 600; font-size: 0.92rem; }
.band--stage label { color: var(--light); }
/* DT-P50: the field rule has to say what a field is. Written as a bare element
   selector it caught checkboxes and radios too — so the enrolment consent tick rendered as
   a 26rem bordered box with a tiny mark floating in the middle of it, and the
   words that belonged beside it sat after a block the width of the form. The
   studio had already patched around this once for its own checkboxes, which
   was the clue nobody followed up. */
input:not([type="checkbox"]):not([type="radio"]), select, textarea { width: 100%; max-width: 26rem; padding: 0.8rem 1rem; border: 1.5px solid var(--border-strong); border-radius: 12px; font: inherit; background: #ffffff; color: var(--ink); transition: border-color var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve); }
input[type="checkbox"], input[type="radio"] {
  width: 1.15rem; height: 1.15rem; min-height: 0; max-width: none; flex: 0 0 auto;
  margin: 0.15rem 0 0; padding: 0; accent-color: var(--stagelight-deep); cursor: pointer;
}
/* A tick and its sentence are one thing, so they sit on one line and wrap
   together — and the sentence is a sentence, not a field label, so it does not
   inherit the label weight. */
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) {
  display: flex; align-items: flex-start; gap: 0.65rem;
  font-weight: 400; font-size: 1rem; line-height: 1.6; max-width: 64ch;
  margin: 1.3rem 0 0; cursor: pointer;
}
main label:has(> input[type="checkbox"]) + button { margin-top: 1.7rem; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--stagelight-deep); box-shadow: 0 0 0 3px rgba(233,168,98,0.28); }
.band--stage input, .band--stage select, .band--stage textarea { background: var(--night-2); border-color: var(--line-dark); color: var(--light); }
button { margin-top: 1.2rem; }
.soft { color: var(--ink-soft); }
.warn { color: var(--stagelight-deep); font-weight: 600; }
/* DT-P116. A confirmation has to LOOK like one.
   The enquiry form's success message was rendered as <p class="ok"> — a class
   this stylesheet has never defined — so it came out as ordinary body text,
   sitting between the paragraph that invites you to ask and an empty form.
   Philip sent a real question and read the confirmation as more explanatory
   copy, because that is exactly what it looked like. A change that happened
   and cannot be seen to have happened is the same defect as one that did not
   happen (AW-047). */
.said {
  margin: 0 0 1.2rem; padding: 1rem 1.2rem;
  border: 1.5px solid var(--stagelight-deep); border-left-width: 5px; border-radius: 12px;
  background: rgba(233, 168, 98, 0.12); color: var(--ink);
}
.said b { display: block; font-size: 1.05rem; margin-bottom: 0.3rem; }
.said p { margin: 0.5rem 0 0; }
.said p:first-child { margin-top: 0; }

/* ---- long forms (the application, teacher onboarding) ------------------
   A form someone fills in for ten minutes needs a rhythm, not a stack: one
   spacing step between a label and its field, a bigger one between fields,
   and a bigger one again between sections, so the eye can tell a new
   question from a new subject. Sections are ruled off rather than merely
   spaced, because a heading that only has margin still reads as belonging
   to the field above it. */
/* One column, one width, one rhythm: the form and the prose that explains it
   sit in the same measure so the page does not change shape halfway down. */
.page-form, .form--long { max-width: 42rem; }
.page-form > h1 { margin: 0 0 0.6rem; font-size: clamp(2rem, 4.4vw, 3rem); }
.page-form > h1 + .soft { margin: 0 0 0.4rem; }

/* Section headings, in the form and after it, are the same heading: ruled off
   rather than merely spaced, because a heading that only has margin still
   reads as belonging to the field above it. */
.page-form h2, .form--long h2 {
  margin: 2.9rem 0 0.2rem; padding-top: 1.7rem; border-top: 1px solid var(--line);
  font-size: clamp(1.25rem, 2.2vw, 1.55rem); font-weight: 600; line-height: 1.25;
}
.page-form > p, .form--long > p { margin: 0.5rem 0 0; max-width: 54ch; }
.form--long label { margin: 1.5rem 0 0.4rem; }
/* A tick box is a control in this form, so it is the width of the controls.
   The 64ch readability cap on a bare tick-and-sentence is right in prose and
   wrong here, where it left the box stopping short of the fields above it. */
.form--long > .pick { max-width: 100%; }
.form--long h2 + label, .form--long > p + label { margin-top: 1.2rem; }
/* Every field in one form is one width. The base rule caps a field at 26rem
   and its :not() selectors outrank a plain .form--long input, so the inputs
   stayed at 26rem while the selects and textareas beside them stretched to the
   42rem column — two field widths in one form, which is exactly what the
   uniformity brief forbids (AW-104). Matching the specificity fixes it, and
   the column's own max-width is what keeps a line readable. */
.form--long input:not([type="checkbox"]):not([type="radio"]),
.form--long select,
.form--long textarea { max-width: 100%; }
.form--long textarea { min-height: 8.5rem; line-height: 1.6; resize: vertical; }
/* The submit is its own line, always. A button is inline-block, so a form whose
   last field is a text input put the button BESIDE it — which read as a search
   box rather than the end of a form, and only ever happened on the forms that
   happen to end in an input rather than a textarea. Block with a fitted width
   gives it the line without stretching it across the page (AW-104). */
.form--long button { display: block; width: fit-content; margin-top: 2rem; }
.form--long button + .soft { margin: 0.8rem 0 0; font-size: 0.9rem; }
/* Why the form did not send, said where the click happened. */
.formerror {
  margin: 2rem 0 0; padding: 0.9rem 1.1rem; border-radius: 12px;
  border: 1px solid var(--stagelight-deep); background: var(--bg-warm);
  color: var(--ink); font-size: 0.92rem; line-height: 1.55; max-width: 100%;
}
.formerror + button { margin-top: 1rem; }
/* A password you can choose to read. The button sits inside the field's own
   footprint so the row still reads as one control. */
.reveal { position: relative; display: block; max-width: 26rem; }
.reveal input { padding-right: 5rem; }
.reveal__toggle {
  position: absolute; top: 50%; right: 0.45rem; transform: translateY(-50%);
  margin: 0; padding: 0.35rem 0.85rem; border-radius: 999px;
  background: var(--bg-soft); color: var(--ink-soft); border: 1px solid var(--line);
  font-family: var(--body); font-size: 0.78rem; font-weight: 600; letter-spacing: 0.04em;
  box-shadow: none; cursor: pointer;
}
@media (hover: hover) { .reveal__toggle:hover { transform: translateY(-50%); background: #ffffff; color: var(--stagelight-deep); border-color: var(--stagelight-deep); box-shadow: none; } }
.reveal__toggle:active { transform: translateY(-50%); }
.operator-way {
  margin: 1.6rem 0 0.4rem; padding: 1rem 1.25rem; border: 1px solid var(--line);
  border-left: 3px solid var(--stagelight); border-radius: 12px; background: var(--bg-soft);
  font-size: 0.92rem; line-height: 1.6; max-width: 56ch;
}
.formworking {
  margin: 0.9rem 0 0; font-size: 0.9rem; line-height: 1.55; color: var(--ink-soft); max-width: 54ch;
}
.formworking::before {
  content: ''; display: inline-block; width: 0.7rem; height: 0.7rem; margin-right: 0.5rem;
  border: 2px solid var(--stagelight); border-top-color: transparent; border-radius: 50%;
  vertical-align: baseline; animation: spin 800ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .formworking::before { animation: none; } }
button:disabled { opacity: 0.65; cursor: progress; transform: none; box-shadow: none; }
@media (hover: hover) { button:disabled:hover { transform: none; box-shadow: none; } }

/* A discreet note under a field: present for the person who needs it, quiet
   for the person who does not. */
.hint { margin: 0.45rem 0 0; font-size: 0.85rem; line-height: 1.6; color: var(--ink-soft); max-width: 60ch; }
.hint b, .hint strong { color: var(--ink); font-weight: 600; }
.form--long .hint + label { margin-top: 1.2rem; }
/* A note under a file input needs more air than one under a text box: the
   dashed field already reads as a block, so a note tight against it looks
   like part of the control rather than a remark about it. */
.form--long input[type="file"] + .hint { margin-top: 0.85rem; padding-left: 0.1rem; }

/* ---- choosers (a searchable list, a tick list) ------------------------- */
.chooser { display: block; }
.chooser__search { margin-bottom: 0.5rem; }
.ticks {
  border: 1.5px solid var(--line); border-radius: 12px; background: #ffffff;
  padding: 0.3rem 1rem 1rem; max-height: 20rem; overflow-y: auto;
}
.ticks__group { border: 0; margin: 0; padding: 0.9rem 0 0; }
.ticks__group legend {
  padding: 0; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--ink-soft);
}
.tick {
  display: flex; align-items: baseline; gap: 0.6rem; margin: 0.35rem 0 0;
  font-weight: 400; font-size: 0.95rem; cursor: pointer;
}
.tick input { width: auto; max-width: none; margin: 0; flex: none; accent-color: var(--stagelight-deep); transform: translateY(1px); }
@media (hover: hover) { .tick:hover span { color: var(--stagelight-deep); } }
.ticks__summary { margin: 0.6rem 0 0; font-size: 0.85rem; line-height: 1.5; color: var(--ink); max-width: 52ch; }
.ticks__summary--empty { color: var(--ink-soft); }
/* DT-P108: one tick on its own, outside a .ticks box — the "I also teach
   online" answer sits directly under the towns it qualifies. */
.tick--standalone { margin: 0.55rem 0 0; font-size: 1rem; }

/* A declared-insurance marker in the directory: quiet, factual, and gone the
   day the cover it describes expires. */
.badge {
  display: inline-block; margin: 0.5rem 0 0; padding: 0.3rem 0.85rem;
  border: 1px solid var(--line); border-radius: 999px; background: var(--bg-soft);
  font-size: 0.78rem; font-weight: 600; letter-spacing: 0.02em; color: var(--ink-soft);
}
.badge::before { content: '✓'; margin-right: 0.4rem; color: var(--stagelight-deep); font-weight: 700; }
/* DT-P162: the same marker on the dark hero. --stagelight-deep is a brown made
   to be read on cream and disappears on black, so the tick takes the light
   amber instead — the same token the dark bands already use for their links. */
.badge--dark { border-color: rgba(242,239,232,0.46); background: rgba(242,239,232,0.08); color: var(--light); }
.badge--dark::before { color: var(--stagelight); }

/* A file input should read as an action, not as a broken text box. */
.form--long input[type="file"] {
  padding: 0.7rem 0.9rem; background: var(--bg-soft); border-style: dashed; cursor: pointer;
  font-size: 0.9rem;
}
/* The dark variant on the long public forms. It keeps the border-as-gap: a
   plain "border: 0" here would have quietly undone the fix above on exactly
   the forms a stranger meets first. */
.form--long input[type="file"]::file-selector-button {
  margin-right: 0.3rem; padding: 0.45rem 1rem; border: 0 solid transparent; border-right-width: 1.15rem;
  border-radius: 999px; background-clip: padding-box; box-shadow: none;
  background-color: var(--ink); color: #ffffff; font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer;
}
.form--long input[type="file"]::-webkit-file-upload-button {
  margin-right: 0.3rem; padding: 0.45rem 1rem; border: 0 solid transparent; border-right-width: 1.15rem;
  border-radius: 999px; background-clip: padding-box; box-shadow: none;
  background-color: var(--ink); color: #ffffff; font: inherit; font-weight: 600; font-size: 0.85rem; cursor: pointer;
}

/* The signable text: quoted, and shown whole. Someone signing this is entitled
   to see all of it without a scrollbar hiding the last clause. */
.signable {
  margin: 0.9rem 0 0.4rem; padding: 1.3rem 1.5rem; max-width: 100%;
  background: var(--bg-soft); border: 1px solid var(--line);
  border-left: 3px solid var(--stagelight); border-radius: 12px;
  font-size: 0.95rem; line-height: 1.8; color: var(--ink-soft);
}
.band--stage .signable { background: var(--night-2); border-color: var(--line-dark); color: var(--light-soft); }

/* ---- directory -------------------------------------------------------- */
.filters { display: flex; flex-wrap: wrap; gap: 1rem 1.4rem; align-items: end; margin-top: 1.5rem; margin-bottom: 3rem; }
.filters label { margin: 0; }
.filters input, .filters select { max-width: 11rem; }
.filters button { margin-top: 0; }
/* ---- the site footer (DT-P130) ---------------------------------------
   The safeguarding line stays first and stays a sentence: somebody may need it
   in a hurry, and a hurry does not read columns. Everything else is what a
   person looks for at the bottom of a site before trusting it with a child. */
.sitefoot { display: block; }
.sitefoot__urgent { margin: 0 0 2rem; font-size: 1rem; }
.sitefoot__cols {
  display: grid; gap: 1.6rem 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  padding: 1.6rem 0 0; border-top: 1px solid rgba(255,255,255,0.14);
}
.sitefoot__cols h2 {
  margin: 0 0 0.35rem; font-family: var(--display); font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--stagelight);
}
.sitefoot__cols p { margin: 0 0 0.6rem; font-size: 0.85rem; color: var(--cinema-soft); max-width: 26ch; }
.sitefoot__cols ul { list-style: none; margin: 0; padding: 0; }
.sitefoot__cols li { margin: 0 0 0.3rem; }
.sitefoot__cols a { color: var(--cinema-text); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve); }
@media (hover: hover) { .sitefoot__cols a:hover { color: var(--stagelight); border-bottom-color: var(--stagelight); } }
.sitefoot__rule {
  margin: 1.8rem 0 0; padding: 1.1rem 0 0; border-top: 1px solid rgba(255,255,255,0.14);
  font-size: 0.8rem; color: var(--cinema-soft); max-width: 70ch;
}

/* ---- the directory, as cards (DT-P126) -------------------------------
   Philip: cards rather than horizontals, premium, clean, succinct — and two
   formats, because roughly half of any directory has no photograph yet and a
   grid where every other tile is an empty box is worse than a list.

   So the second format is not a placeholder, it is a DESIGN: the mark on a
   deep ground, sized and centred deliberately, so a teacher who has not
   uploaded a photograph looks like a member of something rather than like a
   missing file. Both formats share one silhouette — the same 4:5 media, the
   same body, the same rhythm — so a row of them reads as one set. */
.tcards {
  list-style: none; padding: 0; margin: 2.2rem 0 0;
  display: grid; gap: 1.6rem;
  grid-template-columns: repeat(auto-fill, minmax(18.5rem, 1fr));
}
.tcard {
  display: flex; flex-direction: column; overflow: hidden;
  background: #ffffff; border: 1px solid var(--line); border-radius: 18px;
  box-shadow: 0 1px 2px rgba(24,18,10,0.04);
  transition: transform var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve);
}
@media (hover: hover) { .tcard:hover { transform: translateY(-3px); border-color: var(--stagelight); box-shadow: 0 22px 44px -26px rgba(24,18,10,0.4); } }
.tcard:focus-within { border-color: var(--stagelight-deep); }
/* DT-P145: the tile holds the photograph; the photograph does not size the tile.
 *
 * aspect-ratio is a PREFERENCE, not a constraint. The image sat in this tile as
 * a grid item, and a grid item's natural size is allowed to grow its track — so
 * the first real portrait photograph uploaded to the directory rendered a
 * 258x516 tile instead of 258x258, buried the teacher's name, and made her card
 * twice the height of everybody else's. Every square image had passed, which is
 * the only kind anyone had tried.
 *
 * Absolutely positioned, so the photograph is out of flow and cannot influence
 * the box at all. overflow: hidden then makes the rounded corner honest.
 *
 * CONTAIN, not cover: a teacher's headshot is shown whole. Cropping a portrait
 * to a square is how you take the top off somebody's head, and a directory whose
 * whole argument is that a family is choosing a PERSON should not do that. The
 * ground behind is the same dark the crest uses, so a tall photograph sits on it
 * deliberately rather than looking like a mistake.
 *
 * The crest is excluded: it is a mark, sized and centred by the rules below,
 * and it was never the thing that broke.
 */
/* DT-P162: the same ground as the crest and as the bio frame. Flat black made
   a portrait look pillarboxed — like a video in the wrong shape — where a
   ground with some depth in it reads as a mount. The frame stays uniform; only
   what sits behind an un-square photograph changes. */
.tcard__media { position: relative; aspect-ratio: 1 / 1; overflow: hidden; background: radial-gradient(120% 95% at 50% 38%, #2e2436 0%, var(--night-2) 55%, #0e0b12 100%); display: grid; place-items: center; }
/* DT-P163. THE FRAME IS ALWAYS FULL.
   A square box with a contained portrait leaves bars, and a row of cards whose
   photographs are different widths does not read as a row. Cropping would make
   them identical and would take the top off somebody's head (DT-P151). So the
   picture fills its own frame: the same file, blurred and darkened, covers the
   box behind the real one, which still sits whole on top. Edge to edge, the
   same size as its neighbour, nothing trimmed.
   The backdrop is inset past the edges because a blur samples beyond what it
   can reach and would otherwise fade at the corners. It is aria-hidden and has
   no alt: it carries no information the photograph above it does not. */
/* DT-P165. Philip, looking at the tiles beside the newly violet hero: "for
   aesthetics and consistency, do you think it worth applying the same soft
   background in brand colours on the all teachers photo cards?"
   Yes, and the version that was NOT worth shipping is worth recording: brand
   colours ALONE, with the blurred photograph dropped. It looked the most
   deliberately branded of the three and it was the wrong answer — an identical
   violet behind every face makes a directory of PEOPLE look like a set of
   placeholders, and at forty teachers it would flatten the grid into
   wallpaper. So the gradient goes UNDER the blur rather than instead of it:
   the same three hues as the hero, each tile still carrying the light of its
   own picture. The grid is consistent in temperature rather than identical in
   colour, which is the honest most a photograph-led page can be.
   The blur is darkened and dropped to 72% so the colour beneath it reads. */
.pframe {
  position: relative; overflow: hidden;
  background:
    radial-gradient(85% 120% at 22% 14%, #2a1150 0%, rgba(42,17,80,0) 64%),
    radial-gradient(70% 95% at 82% 92%, rgba(214,31,116,0.34) 0%, rgba(214,31,116,0) 62%),
    var(--night);
}
.pframe__bg {
  position: absolute; inset: -10%; width: 120%; height: 120%; object-fit: cover; display: block;
  filter: blur(26px) saturate(1.05) brightness(0.42); opacity: 0.72; transform: scale(1.06);
}
.pframe__i { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.tcard__media:not(.tcard__crest) > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
/* The no-photograph format. A ground with some depth in it, and the mark at a
   size that reads as a crest rather than as a logo somebody forgot to remove. */
.tcard__crest { background: radial-gradient(120% 95% at 50% 42%, #2e2436 0%, var(--night-2) 52%, #0e0b12 100%); }
/* The mark is a pale emblem drawn for a dark ground, and it carries a strapline
   in small caps — too small and it is a smudge, so it is given most of the tile
   rather than tucked into a corner. */
.tcard__crest img { width: 78%; max-width: 15rem; height: auto; object-fit: contain; opacity: 0.95; }
.tcard__body { flex: 1; display: flex; flex-direction: column; gap: 0.55rem; padding: 1.15rem 1.25rem 1.3rem; }
.tcard__name { font-family: var(--serif); font-weight: 400; font-size: 1.4rem; line-height: 1.15; margin: 0; letter-spacing: -0.01em; }
.tcard__name a { color: var(--ink); text-decoration: none; }
@media (hover: hover) { .tcard__name a:hover { color: var(--stagelight-deep); } }
.tcard__where { margin: 0; font-size: 0.85rem; color: var(--ink-soft); }
.tcard__styles { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.1rem 0 0; padding: 0; list-style: none; }
.tcard__style {
  font-size: 0.74rem; letter-spacing: 0.02em; padding: 0.24rem 0.6rem;
  border: 1px solid var(--line); border-radius: 999px; color: var(--ink-soft); background: var(--bg-soft);
}
.tcard__style--more { border-style: dashed; }
/* DT-P162: the same chip on the dark hero. Not a new colour — the page's own
   light on the page's own night, at a weight that reads as a label rather than
   as a button somebody forgot to style. */
/* 0.38 rather than a rounder number: at 0.32 the edge was 2.55:1 against the
   night and the gate's own rule for a boundary is 3:1. A chip is a label rather
   than a control, so nothing forces it — but a pairing the system permits is a
   pairing someone will ship on something that IS a control. DT-P163 raised it
   again from 0.38 to 0.46: the hero is no longer flat black, and 0.38 fell to
   2.94:1 against the violet the chips now sit on. 0.46 is 3.67:1 there and
   4.53:1 on the night, so it holds wherever the chip is used. */
.tcard__style--dark { border-color: rgba(242,239,232,0.46); color: var(--light); background: rgba(242,239,232,0.08); }
/* Three lines. A directory where one teacher's paragraph pushes their card
   twice the height of the one beside it is not a grid, it is a stack. */
.tcard__bio {
  margin: 0.15rem 0 0; color: var(--ink-soft); font-size: 0.9rem; line-height: 1.55;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
/* DT-P162. Side by side until they do not fit, which on a card this narrow is
   most of the time: "Teacher declares public liability insurance" beside a
   button wrapped to three lines and dragged the button out of line with every
   other card in the grid. The badge takes its own row and the button keeps its
   place at the foot, so a row of cards still ends on a row of buttons. */
.tcard__foot { margin-top: auto; padding-top: 0.9rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.6rem 0.75rem; }
.tcard__foot .badge { margin: 0; flex: 1 1 100%; }
.tcard__go {
  display: inline-block; text-decoration: none; background: var(--stagelight); color: var(--text-inverse);
  padding: calc(0.55rem - 1px) calc(1.15rem - 1px); border: 1px solid var(--stagelight-deep); border-radius: 999px; font-weight: 600; font-size: 0.82rem; white-space: nowrap;
  box-shadow: 0 12px 26px -14px rgba(233,168,98,0.7);
  transition: transform var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve), background var(--ease-t) var(--curve);
}
@media (hover: hover) { .tcard__go:hover { transform: translateY(-2px); background: var(--stagelight-deep); color: #fff; box-shadow: 0 18px 36px -14px rgba(233,168,98,0.8); } }
.tcard__go:active { transform: translateY(0); }
@media (max-width: 30rem) { .tcards { grid-template-columns: 1fr; gap: 1.2rem; } }

.roster { list-style: none; padding: 0; margin: 2rem 0 0; border-top: 1px solid var(--line); }
.roster li { display: grid; grid-template-columns: 1fr auto; gap: 0.4rem 2rem; align-items: center; padding: 1.6rem 0.6rem; border-bottom: 1px solid var(--line); margin: 0; border-radius: 12px; transition: background var(--ease-t) var(--curve), padding-left var(--ease-t) var(--curve); }
@media (hover: hover) { .roster li:hover { background: var(--bg-soft); padding-left: 1.4rem; } }
.roster h3 { font-size: clamp(1.5rem, 3.4vw, 2.2rem); margin: 0; color: var(--ink); }
.roster .meta { color: var(--ink-soft); font-size: 0.92rem; margin: 0.15rem 0 0; }
.roster .bio { color: var(--ink-soft); max-width: 60ch; margin: 0.4rem 0 0; }
.roster .join { display: inline-block; text-decoration: none; background: var(--stagelight); color: var(--text-inverse); padding: calc(0.65rem - 1px) calc(1.4rem - 1px); border: 1px solid var(--stagelight-deep); border-radius: 999px; font-weight: 600; font-size: 0.85rem; letter-spacing: 0.02em; transition: transform var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve); white-space: nowrap; box-shadow: 0 12px 26px -14px rgba(233,168,98,0.7); }
@media (hover: hover) { .roster .join:hover { transform: translateY(-2px); box-shadow: 0 18px 36px -14px rgba(233,168,98,0.8); } }
@media (max-width: 40rem) { .roster li { grid-template-columns: 1fr; } }

/* ---- the bill: promoted teachers on the family landing ---------------- */
.bill { list-style: none; padding: 0; margin: 2.5rem 0 1rem; display: grid; gap: 2rem 3rem; }
@media (min-width: 54rem) { .bill { grid-template-columns: 1fr 1fr; } }
.bill li { border-top: 1px solid var(--ink); padding-top: 1.1rem; }
.bill__name { display: inline-block; font-family: var(--serif); font-weight: 300; font-size: clamp(1.9rem, 4vw, 2.9rem); line-height: 1.1; color: var(--ink); text-decoration: none; transition: color var(--ease-t) var(--curve); }
@media (hover: hover) { .bill__name:hover { color: var(--stagelight-deep); font-style: italic; } }
.bill .meta { color: var(--ink-soft); font-size: 0.88rem; letter-spacing: 0.06em; text-transform: uppercase; margin: 0.3rem 0 0; }

/* legacy card classes (operator console, safeguarding routes) kept usable */
.cards { list-style: none; padding: 0; margin: 2rem 0 0; border-top: 1px solid var(--line); display: block; }
.card { display: block; padding: 1.5rem 0.6rem; border-bottom: 1px solid var(--line); transition: background var(--ease-t) var(--curve), padding-left var(--ease-t) var(--curve); }
@media (hover: hover) { .card:hover { background: var(--bg-soft); padding-left: 1.4rem; } }
.card h3 { font-size: clamp(1.3rem, 3vw, 1.8rem); margin: 0; }
.card .meta { color: var(--ink-soft); font-size: 0.92rem; margin: 0.15rem 0 0; display: inline-block; margin-right: 1.2rem; }
.card .join { display: inline-block; margin-top: 0.8rem; text-decoration: none; background: var(--stagelight); color: var(--text-inverse); padding: calc(0.6rem - 1px) calc(1.4rem - 1px); border: 1px solid var(--stagelight-deep); border-radius: 999px; font-weight: 600; font-size: 0.85rem; }

/* ---- the feature: a story set like a magazine piece ------------------- */
.feature__header { position: relative; background: var(--cinema); color: var(--cinema-text); overflow: hidden; min-height: min(66svh, 42rem); display: flex; align-items: flex-end; }
.feature__header img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 35%; opacity: 0.8; filter: contrast(1.05) saturate(1.05) brightness(0.95); transform-origin: 50% 40%; animation: pushin 30s ease-out both; }
.feature__header .vignette { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(8,7,13,0.6) 0%, rgba(8,7,13,0.05) 30%, rgba(8,7,13,0.88) 82%, #08070d 100%); }
.feature__header .titles { position: relative; z-index: 1; width: 100%; max-width: 82rem; margin: 0 auto; padding: clamp(5rem, 12vh, 8rem) 1.6rem clamp(2.6rem, 6vh, 4rem); }
.feature__header .titles > * { animation: titleup 1.4s cubic-bezier(.16,.6,.2,1) both; }
.feature__header .titles > :nth-child(2) { animation-delay: 0.5s; }
.feature__header .titles > :nth-child(3) { animation-delay: 1s; }
.feature__kicker { display: block; color: var(--stagelight); font-size: 0.78rem; letter-spacing: 0.32em; text-transform: uppercase; font-weight: 600; margin-bottom: 1rem; }
.feature__header h1 { font-family: var(--serif); font-weight: 300; font-size: clamp(2.6rem, 6.5vw, 5.2rem); line-height: 1.04; max-width: 18ch; margin: 0 0 0.9rem; color: #ffffff; }
.feature__standfirst { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--cinema-text); max-width: 46ch; font-family: var(--serif); font-style: italic; font-weight: 300; }
.feature__byline { color: var(--cinema-soft); font-size: 0.9rem; letter-spacing: 0.06em; }
.feature__body { max-width: 44rem; margin: 0 auto; padding: clamp(3rem, 7vw, 5rem) 1.4rem 3rem; font-size: 1.08rem; line-height: 1.85; }
.feature__body > p:first-of-type::first-letter { font-family: var(--serif); font-weight: 300; font-size: 4.4em; line-height: 0.8; float: left; padding: 0.08em 0.12em 0 0; color: var(--stagelight-deep); }
.feature__body p { margin: 1.15rem 0; }
.feature__rule { border: 0; width: 4rem; height: 2px; background: var(--stagelight); margin: 2.6rem auto; }
.pullquote { font-family: var(--serif); font-style: italic; font-weight: 300; font-size: clamp(1.5rem, 3vw, 2.1rem); line-height: 1.35; color: var(--ink); border: 0; margin: 2.8rem 0; padding: 0 0 0 1.4rem; border-left: 2px solid var(--stagelight); }
.feature__figure { margin: 2.8rem -1.4rem; }
@media (min-width: 60rem) { .feature__figure { margin: 3rem -6rem; } }
.feature__figure img { width: 100%; display: block; border-radius: 16px; box-shadow: 0 26px 60px -30px rgba(8,7,13,0.55); }
.feature__figure figcaption { text-align: center; color: var(--ink-soft); font-size: 0.85rem; margin-top: 0.7rem; letter-spacing: 0.04em; }
.feature__close { text-align: center; padding: 0 1.4rem clamp(3rem, 7vw, 5rem); }

/* ---- the essay (legacy) ----------------------------------------------- */
.essay p { margin: 1rem 0; }
.essay .byline { color: var(--ink-soft); font-style: italic; }

/* ---- footer ----------------------------------------------------------- */
footer { border-top: 1px solid var(--line); margin-top: 4rem; padding-top: 1.8rem; color: var(--ink-soft); font-size: 0.9rem; line-height: 1.75; }
.band--stage footer, footer.on-stage { border-color: var(--line-dark); color: var(--light-soft); }
footer.on-stage { margin-top: 0; }

/* ---- the live room --------------------------------------------------- */
/* DT-P60: the class room, built for a phone first.
   A dancer attending from anywhere is holding a phone in portrait, propped
   against a wall, standing far enough back to be seen head to toe. Everything
   below follows from that: tall tiles rather than the 4:3 letterbox a webcam
   suggests, controls big enough for a thumb, and a canvas that is the screen
   rather than the prose column. */
.mainroom { max-width: 78rem; }
.roomtitle { font-size: clamp(1.15rem, 3.5vw, 1.6rem); margin: 0 0 0.35rem; }
.roomstatus { margin: 0 0 0.75rem; font-size: 0.9rem; color: var(--ink-soft); }
.room { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0.6rem; }
/* 3:4 — a standing person, not a talking head. A 4:3 frame of a dancer either
   crops them at the knees or shows a room with a small figure in the middle. */
.room video { width: 100%; aspect-ratio: 3 / 4; background: var(--night-2); border-radius: 12px; object-fit: cover; display: block; }
.room__mute { outline: 2px dashed var(--line); outline-offset: -6px; opacity: 0.55; }
/* On a wide screen a class is watched in landscape, and a single other person
   should fill the frame rather than sit in a column. */
@media (min-width: 48rem) {
  .room { grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); }
  .room video { aspect-ratio: 4 / 3; }
}
/* One tile: it is a 1-to-1, so give it the room. */
.room video:only-child { max-height: 68vh; width: auto; margin: 0 auto; }

/* DT-P66: side by side, each as large as the room allows.
   Philip: "I'd like them side by side, so that the teacher can see themselves
   on a full screen and the student on a full screen, and the same the other
   way round." An auto-fit grid gave two tiles two columns but let their height
   fall out of the aspect ratio, so on a laptop two people sat in a band across
   the middle with the rest of the screen empty. The room now takes the height
   that is left and the tiles divide it, which is what makes each one big.

   The count comes from the script as data-n, because CSS cannot count children
   without writing a quantity-query ladder for every possible number — and the
   script already knows. Nothing here is an inline style: style-src 'self'
   refuses one silently (AW-056). */
@media (min-width: 48rem) {
  .room {
    display: flex; flex-wrap: wrap; align-content: stretch;
    height: max(22rem, calc(100vh - 19rem));
    height: max(22rem, calc(100dvh - 19rem));
  }
  .room > video { flex: 1 1 auto; height: 100%; width: auto; min-width: 0; aspect-ratio: 4 / 3; max-height: 100%; }
  /* Two people: half the width each, the full height. The pair a class
     actually is, most of the time. */
  .room[data-n="2"] > video { flex: 1 1 calc(50% - 0.3rem); height: 100%; }
  /* Three or four: two rows of two, so nobody is a strip. */
  .room[data-n="3"] > video, .room[data-n="4"] > video { flex: 1 1 calc(50% - 0.3rem); height: calc(50% - 0.3rem); }
  /* Five or six: three across, two down — the most the mesh carries. */
  .room[data-n="5"] > video, .room[data-n="6"] > video { flex: 1 1 calc(33.333% - 0.4rem); height: calc(50% - 0.3rem); }
  .room[data-n="1"] > video { flex: 1 1 100%; height: 100%; width: 100%; }
}

/* Maximised. Deliberately NOT the Fullscreen API: that takes the browser's own
   chrome away, and a dancer who cannot find the way out of a full-screen video
   in the middle of a class is worse off than one whose teacher is a bit
   smaller. The other tiles stay in the DOM — display:none stops the picture,
   never the sound — so the room is still a room, and one tap brings it back.
   object-fit: contain rather than cover: at this size a cropped head is worse
   than a letterbox, and feet are the point. */
.room--max { position: relative; }
.room--max > video { display: none; }
.room--max > video.room__big {
  display: block; flex: 1 1 100%;
  width: 100%; height: 100%; max-height: 100%;
  aspect-ratio: auto; object-fit: contain; background: var(--night-2);
}
/* DT-P120. THE MIRROR.
   The button toggled room--mirror on the tile grid, the local tile carried
   room__me, and NOTHING IN THIS STYLESHEET HAS EVER READ EITHER OF THEM. So
   the mirror has never worked, on any device — and it looked as though it did,
   because the button relabelled itself and remembered the choice. AW-010: a
   promise with no code behind it, wearing the appearance of the thing.

   Only your OWN tile turns (DT-P60): a dancer watching their teacher
   demonstrate a step to the right must be shown it to the right. */
.room--mirror > video.room__me { transform: scaleX(-1); }
.room > video { cursor: zoom-in; }
.room > video.room__big { cursor: zoom-out; }
.room > video:focus-visible { outline: 3px solid var(--stagelight-deep); outline-offset: 2px; }
/* DT-P120. A room is a surface, not a document.
   The page could be pinched smaller and smaller until the class was a stamp in
   the middle of a white field, because the default minimum scale lets a page
   shrink to a quarter. The viewport meta on this page stops that (see app.js);
   this stops the two touch behaviours that come with it — the rubber-band drag
   at the edges, and double-tap-to-zoom landing on a video somebody meant to
   tap. Pinch to zoom IN still works: a room is not a reason to take that away
   from anybody who needs it. */
.room { touch-action: manipulation; overscroll-behavior: contain; }

/* A phone held sideways is a person who has decided to watch properly. */
@media (max-width: 48rem) and (orientation: landscape) {
  .room video { aspect-ratio: 16 / 9; max-height: 64vh; }
  .roomtitle, .roomstatus, .roomhelp { display: none; }
}

/* DT-P60: controls a thumb can hit. 44px is the floor everywhere in this
   product; here they also wrap into rows rather than scrolling, because a
   control you have to scroll to reach during a class is a control you do not
   use. On a phone the bar sticks to the bottom of the screen. */
.roombar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0.9rem 0 0; }
.roombtn {
  min-height: 44px; margin: 0; padding: 0 1rem; border-radius: 999px;
  border: 1px solid var(--border-strong); background: #fff; box-shadow: none;
  font-family: var(--display); font-size: 0.85rem; font-weight: 500; color: var(--ink);
  cursor: pointer; white-space: nowrap;
}
/* DT-P74, rule 2: a change of colour AND a lift. And rule 5: the substance
   moves too. This kept a white background — identical to its resting state —
   so the only answer to a pointer was a thin border going violet, which in a
   room lit by a video feed is close to no answer at all. */
@media (hover: hover) { .roombtn:hover { border-color: var(--violet); color: var(--violet); background: var(--violet-soft); transform: translateY(-1px); box-shadow: none; } }
.roombtn:active { transform: translateY(1px); }
.roombtn[disabled] { opacity: 0.5; cursor: not-allowed; }
@media (hover: hover) { .roombtn[disabled]:hover { transform: none; border-color: var(--border-strong); color: var(--ink); background: #fff; } }
.roombtn[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--light); }
@media (hover: hover) { .roombtn[aria-pressed="true"]:hover { background: var(--night); border-color: var(--night); color: var(--light); } }
/* Rule 4 again, and this one was live: --go had no hover of its own, so it
   inherited .roombtn:hover and the amber "Join" button turned white under the
   pointer — the identical defect to the amber "Open your room", in the one
   place where a teacher is in a hurry and cannot afford to doubt the button. */
.roombtn--go { background: var(--stagelight); border-color: var(--stagelight-deep); color: var(--night); font-weight: 600; }
@media (hover: hover) { .roombtn--go:hover { background: var(--hover-amber); border-color: var(--hover-amber); color: var(--night); } }
.roombtn--quiet { color: var(--ink-soft); }
.roombtn:disabled { opacity: 0.55; cursor: progress; }
.roomhelp { margin: 0.8rem 0 0; font-size: 0.85rem; color: var(--ink-soft); }
.roomhelp summary { cursor: pointer; }
.roomhelp p { margin: 0.5rem 0 0; max-width: 62ch; font-size: 0.85rem; line-height: 1.6; }

/* DT-P70: the music bar. Its own block under the controls rather than more
   buttons in the same row: a teacher reaches for the mute button in a hurry
   and for the music deliberately, and putting them on one line is how somebody
   pauses the class instead of the track. Hidden entirely for dancers — it is
   the teacher's stereo, not a shared one. */
.roommusic { margin: 1rem 0 0; padding: 0.9rem 1.1rem; border: 1px solid var(--line); border-radius: 14px; background: #fff; max-width: 40rem; }
.roommusic__k { margin: 0 0 0.5rem; font-family: var(--display); font-size: 0.74rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-soft); }
.roommusic__pick { display: block; font-size: 0.88rem; font-weight: 600; margin: 0 0 0.5rem; }
.roommusic__pick input { margin-top: 0.4rem; }
.roommusic__now { margin: 0; font-size: 0.88rem; font-variant-numeric: tabular-nums; }
.roombar--music { margin-top: 0.7rem; }
.roommusic__vol { display: inline-flex; align-items: center; gap: 0.55rem; font-size: 0.82rem; font-weight: 500; margin: 0; }
.roommusic__vol input { width: 8rem; max-width: 8rem; padding: 0; }
.roommusic__n { margin: 0.8rem 0 0; font-size: 0.79rem; line-height: 1.6; color: var(--ink-soft); max-width: 64ch; }
.roommusic__n + .roommusic__n { margin-top: 0.6rem; }
/* DT-P217: finished business, folded. A deleted class cannot leave the
   Services page — families who bought it are still owed their sessions — but
   it should not sit in the middle of what she sells either. */
.sfold { margin: 0.9rem 0 0; border-top: 1px solid var(--line); padding-top: 0.7rem; }
.sfold > summary { cursor: pointer; font-family: var(--display); font-size: 0.82rem; font-weight: 600; color: var(--ink-soft); letter-spacing: 0.02em; list-style: none; }
.sfold > summary::-webkit-details-marker { display: none; }
.sfold > summary::before { content: '+'; display: inline-block; width: 1.1rem; font-weight: 700; }
.sfold[open] > summary::before { content: '−'; }
@media (hover: hover) { .sfold > summary:hover { color: var(--violet); } }

/* DT-P217: the queue, visible. A feature nobody can see is a feature nobody
   has — this one existed from the day the music did and showed only as "2 of
   5" at the end of a sentence. */
.roomq { list-style: none; margin: 0.6rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; max-height: 11rem; overflow-y: auto; }
.roomq__i { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.5rem; border-radius: 8px; font-size: 0.84rem; background: var(--paper-2, #f6f4ef); }
.roomq__i--now { background: var(--violet-soft); font-weight: 600; }
.roomq__t { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roomq__x { flex: none; min-height: 28px; width: 28px; padding: 0; border: 1px solid var(--line); border-radius: 999px; background: #fff; color: var(--ink-soft); font-size: 0.9rem; line-height: 1; cursor: pointer; }
@media (hover: hover) { .roomq__x:hover { border-color: var(--violet); color: var(--violet); } }

/* The extra audio elements carrying a teacher's music have nothing to show. */
.room audio { display: none; }
@media (max-width: 48rem) {
  .roommusic { margin-bottom: 0.6rem; }
  .roommusic__vol { flex: 1 1 100%; }
  .roommusic__vol input { width: 100%; max-width: none; }
}
@media (max-width: 48rem) {
  /* DT-P60: everybody in the class fits on the screen at once.
     Stacked 3:4 tiles are correct for ONE person and wrong for two: a dancer
     had to scroll to see their teacher, during a class, on a phone propped
     against a wall — which is to say they could not see them. So on a phone
     the room is a fixed column the height of what is left of the screen, and
     the tiles share it however many there are. dvh rather than vh because the
     browser's own bar slides away and vh does not notice. */
  .room {
    display: flex; flex-direction: column; gap: 0.45rem;
    height: max(20rem, calc(100vh - 15.5rem));
    height: max(20rem, calc(100dvh - 15.5rem));
  }
  .room > video { flex: 1 1 0; min-height: 0; height: auto; width: 100%; aspect-ratio: auto; }
  .room video:only-child { max-height: none; }
  /* DT-P66: maximise works the same on a phone, where it matters more — one
     person filling a small screen is the difference between seeing footwork
     and guessing at it. */
  .room--max > video.room__big { flex: 1 1 100%; height: 100%; }
  .roombar {
    position: sticky; bottom: 0; z-index: 5;
    margin: 0.6rem -1rem 0; padding: 0.55rem 1rem calc(0.55rem + env(safe-area-inset-bottom, 0px));
    background: rgba(253,252,249,0.94); backdrop-filter: blur(12px); border-top: 1px solid var(--line);
    gap: 0.4rem; justify-content: center;
  }
  .roombtn { flex: 1 1 auto; justify-content: center; display: inline-flex; align-items: center; padding: 0 0.7rem; font-size: 0.8rem; }
  /* A display on a class selector beats [hidden]'s, so the sound prompt and the
     camera switch — both of which start hidden — appeared on a phone and
     nowhere else. Caught by looking at a render rather than by reading the
     rule, which is the only way this kind of fault is ever caught. */
  .roombtn[hidden] { display: none; }
  .roomhelp { display: none; }   /* the mirror explains itself by being a toggle */
}

/* ---- choreography: reveals that never hide the page ------------------- */
@supports (animation-timeline: view()) {
  .band__inner > *, .ledger li, .bill li, .screens .screen, .split figure, .split > div, .roster li, .feature__figure, .pullquote {
    animation: rise both; animation-timeline: view(); animation-range: entry 0% entry 65%;
  }
  @keyframes rise { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: none; } }
}

/* ---- signed-in lists: actions stand in a level row, compact ------------ */
main ul { list-style: none; padding-left: 0; }
main ul li { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.8rem; margin: 0 0 1.1rem; padding: 0.8rem 0; border-bottom: 1px solid var(--line); }
main li form { margin: 0; }
/* single-button actions stand in one level cluster on the row's right */
main li > form:has(> button:only-child) { margin-left: 0.7rem; }
main li > form:has(> button:only-child):first-of-type { margin-left: auto; }
/* a form with fields takes its own full row beneath */
main li > form:has(input, select) { flex-basis: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
main li > form:has(input, select) label { margin: 0; }
main li button { margin: 0; padding: 0.45rem 1.15rem; font-size: 0.8rem; }
main li input[type="date"] { display: inline-block; width: auto; max-width: 11rem; margin-right: 0.6rem; padding: 0.45rem 0.8rem; vertical-align: middle; }
main li label { display: inline-block; margin: 0.3rem 0.7rem 0.2rem 0; font-size: 0.85rem; }

/* ---- back to the top --------------------------------------------------- */
.totop { position: fixed; right: 1.2rem; bottom: 1.2rem; z-index: 20; width: 2.9rem; height: 2.9rem; border-radius: 50%; background: var(--cinema); color: var(--stagelight); display: flex; align-items: center; justify-content: center; text-decoration: none; font-size: 1.05rem; border: 1px solid rgba(233,168,98,0.5); box-shadow: 0 12px 30px -12px rgba(8,7,13,0.6); transition: transform var(--ease-t) var(--curve); }
@media (hover: hover) { .totop:hover { transform: translateY(-3px); color: var(--stagelight); } }
@supports (animation-timeline: scroll()) {
  .totop { animation: topfade linear both; animation-timeline: scroll(); animation-range: 120px 480px; }
  @keyframes topfade { from { opacity: 0; pointer-events: none; } to { opacity: 1; pointer-events: auto; } }
}

/* ---- the privacy notice ------------------------------------------------ */
.notice { position: sticky; bottom: 0; z-index: 30; background: var(--cinema); color: var(--cinema-text); border-top: 1px solid rgba(233,168,98,0.4); box-shadow: 0 -14px 40px -20px rgba(8,7,13,0.7); }
.notice__inner { max-width: 110rem; margin: 0 auto; padding: 1rem 1.6rem; display: flex; flex-wrap: wrap; gap: 0.8rem 2.5rem; align-items: center; justify-content: space-between; }
.notice p { margin: 0; flex: 1 1 24rem; max-width: none; font-size: 0.9rem; color: var(--cinema-text); }
.notice b { font-family: var(--serif); font-style: italic; font-weight: 300; color: var(--stagelight); font-size: 1.02rem; letter-spacing: 0.01em; }
.notice a { color: var(--stagelight); }
.notice form { margin: 0; flex: 0 0 auto; }
@media (min-width: 64rem) { .notice__inner { gap: 0.6rem 1.2rem; } .notice p { font-size: 0.76rem; } .notice b { font-size: 0.88rem; } .notice button { padding: 0.45rem 1rem; font-size: 0.78rem; } }
.notice button { margin: 0; padding: 0.6rem 1.5rem; font-size: 0.85rem; }

/* ---- the spacing pass (Philip's third edit): air around every button,
   air between words and pictures ---------------------------------------- */
.hero__cta { margin-top: 2.1rem; margin-bottom: 0.5rem; }
.hero--cinema .hero__cta { margin-top: 2rem; margin-bottom: 0.6rem; }
h2 { margin-bottom: 1rem; }
.band__lede { margin-bottom: 2.4rem; }
.band .btn { margin-top: 1rem; }
.hero__cta .btn, .notice button { margin-top: 0; }
button { margin-top: 1.4rem; }
.filters button { margin-top: 0; }
.split { gap: clamp(2.8rem, 5.5vw, 4.5rem); margin-top: 3rem; }
.split > div > .band__kicker { display: inline-block; margin-bottom: 0.3rem; }
.split figure { margin: 0.4rem 0; }
.split + .split { margin-top: clamp(3.5rem, 7vw, 5.5rem); }
.screens { gap: 3rem; margin-top: 3rem; }
.screen figcaption { margin-top: 0.9rem; }
.bill { margin: 3rem 0 1.6rem; }
.ledger { margin-top: 3rem; }
.numbers { margin-top: 3rem; }
.price { margin-top: 3rem; }
.price__inner p { max-width: 52ch; }
.feature__close p { margin: 1.2rem 0; }
.feature__close .btn { margin-top: 0.8rem; }
.hero__art { margin-top: 0.5rem; }
form label:first-of-type { margin-top: 1.4rem; }
input + button, select + button, textarea + button { margin-left: 1.2rem; }
.filters button { margin-left: 0.4rem; }

/* ---- page-to-page: a phased crossfade, never an abrupt cut ------------ */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: vt-out 400ms ease both; }
::view-transition-new(root) { animation: vt-in 700ms 120ms cubic-bezier(.2,.7,.2,1) both; }
@keyframes vt-out { to { opacity: 0; } }
@keyframes vt-in { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none !important; }
}

/* ---- the Stripe guide (DT-P191) ---------------------------------------
   One step per screen. The drawings are grey frames with the labels written
   in, never a copy of Stripe's page: a ring in the deep amber marks the one
   thing to press, a dashed magenta edge marks the thing to avoid, and a
   faded row marks the thing to leave alone. Nothing in a drawing is a
   control, so nothing in one takes focus or a hover state. */
.sg-toc { display: grid; gap: 0.45rem; margin: 1.6rem 0 0.5rem; padding: 1rem 1.15rem; border: 1px solid var(--line); border-radius: 14px; background: var(--surface-raised); }
.sg-toc a { display: flex; justify-content: space-between; gap: 1rem; min-height: 44px; align-items: center; font-weight: 600; text-decoration: none; border-bottom: 1px solid var(--line); }
.sg-toc a:last-child { border-bottom: 0; }
.sg-toc a span { font-weight: 400; color: var(--ink-soft); font-size: 0.9rem; white-space: nowrap; }
.sg-need { padding: 0.4rem 1.15rem 0.6rem; border-radius: 14px; background: var(--bg-warm); border: 1px solid var(--line); }
.sg-step { margin: 2.4rem 0 0; padding-top: 1.6rem; border-top: 1px solid var(--line); scroll-margin-top: 5rem; }
.sg-step h3 { display: flex; align-items: center; gap: 0.7rem; font-size: 1.2rem; margin: 0 0 1rem; }
.sg-step__n { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 2.1rem; height: 2.1rem; border-radius: 50%; background: var(--night-2); color: var(--light); font-family: var(--display); font-size: 1rem; }
.sg-step__type, .sg-step__avoid { margin: 1rem 0 0; padding: 0.75rem 1rem; border-radius: 12px; }
.sg-step__type { background: var(--surface-sunken); border-left: 4px solid var(--stagelight-deep); }
.sg-step__avoid { background: var(--magenta-soft); border-left: 4px solid var(--magenta); }
.sg-step__type > b, .sg-step__avoid > b { display: block; font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 0.2rem; }
.sg-step__avoid ul { margin: 0.3rem 0; }
/* main ul li is a flex row of controls elsewhere (the lists of records); these
   are sentences, so they read as a plain list. Later in the file than that
   rule would be ideal, but equal-specificity plus two classes wins anyway. */
main .sg-need ul, main .sg-step ul { list-style: disc; padding-left: 1.15rem; }
main .sg-need ul li, main .sg-step ul li { display: list-item; margin: 0.45rem 0; padding: 0; border-bottom: 0; }
.sg-copy { font-weight: 600; font-size: 0.95rem; background: var(--surface-raised); border: 1px solid var(--line); border-radius: 8px; padding: 0.55rem 0.75rem; user-select: all; overflow-wrap: anywhere; }

.sg-shot { margin: 0 0 1rem; border: 1px solid var(--border-strong); border-radius: 12px; overflow: hidden; background: var(--surface-raised); box-shadow: 0 10px 30px -18px rgba(23,20,31,0.35); }
.sg-shot__bar { display: flex; align-items: center; gap: 0.7rem; padding: 0.5rem 0.75rem; background: #ece8e0; border-bottom: 1px solid var(--line); }
.sg-shot__dots { flex: none; width: 2.6rem; height: 0.7rem; background: radial-gradient(circle at 0.35rem 50%, #c9c2b6 0.3rem, transparent 0.32rem), radial-gradient(circle at 1.3rem 50%, #c9c2b6 0.3rem, transparent 0.32rem), radial-gradient(circle at 2.25rem 50%, #c9c2b6 0.3rem, transparent 0.32rem); }
.sg-shot__url { flex: 1; min-width: 0; padding: 0.2rem 0.65rem; border-radius: 999px; background: var(--surface-raised); font-size: 0.8rem; color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sg-shot__body { display: grid; gap: 0.55rem; padding: 1rem 1.1rem 1.15rem; font-size: 0.9rem; }
.sg-shot__cap { padding: 0.6rem 1.1rem 0.75rem; border-top: 1px solid var(--line); background: var(--surface-sunken); font-size: 0.88rem; line-height: 1.5; }
.sg-shot__fine { display: block; color: var(--ink-soft); font-size: 0.78rem; margin-top: 0.15rem; }
.sg-h { font-family: var(--display); font-weight: 700; font-size: 1.02rem; }
.sg-t { color: var(--ink); }
.sg-t--dim { color: var(--ink-soft); font-size: 0.82rem; }
.sg-f { display: grid; gap: 0.2rem; }
.sg-f__l { font-size: 0.78rem; font-weight: 600; color: var(--ink-soft); }
.sg-f__v { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; min-height: 2.3rem; padding: 0.35rem 0.7rem; border: 1px solid var(--border-strong); border-radius: 8px; background: var(--surface-raised); overflow-wrap: anywhere; }
.sg-b { white-space: nowrap; flex: none; display: inline-flex; align-self: start; justify-self: start; align-items: center; padding: 0.4rem 0.9rem; border-radius: 8px; background: var(--night-2); color: var(--light); font-weight: 600; font-size: 0.85rem; }
.sg-c { display: flex; align-items: center; gap: 0.55rem; }
.sg-c__box { flex: none; width: 1rem; height: 1rem; border: 1.5px solid var(--border-strong); border-radius: 4px; background: var(--surface-raised); }
.sg-c__box--on { background: var(--night-2); border-color: var(--night-2); box-shadow: inset 0 0 0 3px var(--surface-raised); }
.sg-o { display: grid; gap: 0.1rem; padding: 0.6rem 0.8rem; border: 1px solid var(--border-strong); border-radius: 10px; }
.sg-o span { color: var(--ink-soft); font-size: 0.82rem; }
.sg-banner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.55rem 0.8rem; border-radius: 8px; background: var(--bg-warm); }
/* The marks. Colour is never the only signal: "do" is a thick ring, "avoid" a
   dashed edge with a struck label, "leave" a faded row. */
.sg-f--do .sg-f__v, .sg-b--do, .sg-o--do, .sg-c--do .sg-c__box, .sg-banner--do { outline: 3px solid var(--stagelight-deep); outline-offset: 2px; }
.sg-o--do { background: #fbf1e3; }
.sg-o--avoid, .sg-banner--avoid { border: 2px dashed var(--magenta); background: var(--magenta-soft); }
.sg-o--avoid b { text-decoration: line-through; text-decoration-thickness: 2px; }
.sg-f--leave, .sg-c--leave { opacity: 0.62; }

.sg-check { display: grid; gap: 0.6rem; margin: 1rem 0 0.5rem; }
.sg-check button { justify-self: start; }
.sg-verdict { margin: 1rem 0 0; padding: 0.9rem 1.1rem; border-radius: 12px; border-left: 5px solid; scroll-margin-top: 5rem; }
.sg-verdict p { margin: 0.3rem 0 0; }
.sg-verdict--ok { background: #fbf1e3; border-color: var(--stagelight-deep); }
.sg-verdict--no { background: var(--magenta-soft); border-color: var(--magenta); }
.sg-q { border-bottom: 1px solid var(--line); }
.sg-q summary { min-height: 44px; display: flex; align-items: center; font-weight: 600; cursor: pointer; }
.sg-q p { margin-top: 0; }

/* ---- motion respects the visitor (AW-059) ---------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition: none !important; animation: none !important; }
}

/* ---- the operator console ---------------------------------------------
   The plainest surface in the product, and deliberately so. Density is the
   courtesy here: one person at nine in the morning who wants a number.
   Three panels across, because an operator screen that leaves half a desktop
   empty makes somebody scroll to compare two numbers that would have fitted
   side by side.

   DT-P39: Philip asked for this to look like the same product as the teacher
   panel, and he was right that it did not. The console had grown its own
   accent (amber), its own corner radius, its own tab shape and its own button
   — three months of small decisions, each reasonable, adding up to a second
   design system. So the SHAPES now come from the studio: the same pill rail,
   the same 16px cards, the same violet action, the same table rules and the
   same display face on labels.

   What did NOT change is the density. Consistency was the ask; matching the
   studio's airy spacing would have cost this screen the thing it is for. Same
   vocabulary, spoken faster. */
.opwrap { max-width: 1120px; margin: 0 auto; padding: 1.5rem 1.25rem 5rem; }
.opwrap * { min-width: 0; }
.opwrap h1 { font-family: var(--display); font-size: clamp(1.5rem, 3vw, 1.95rem); font-weight: 600; letter-spacing: -0.02em; margin: 0 0 1rem; }
.opwrap h2 { font-family: var(--display); font-size: 1rem; font-weight: 600; margin: 0 0 0.5rem; letter-spacing: -0.01em; }
.opwrap h3 { font-family: var(--display); font-size: 0.9rem; font-weight: 600; margin: 1.5rem 0 0.25rem; letter-spacing: -0.01em; }

.opwho {
  display: flex; flex-wrap: wrap; gap: 0.75rem 1.5rem; align-items: center; justify-content: space-between;
  padding: 0.75rem 0 1rem; font-size: 0.85rem; color: var(--ink-soft);
}
.opwho b { color: var(--ink); font-weight: 600; }

/* The tab row: the studio's rail, at the console's pace. Sticky, because an
   operator scrolling a long table should not have to come back up to move on;
   it scrolls sideways inside itself rather than wrapping into two rows that
   shift the page down (AW-075's spirit). */
.optabs { position: sticky; top: 3.6rem; z-index: 8; display: flex; gap: 0.15rem;
  margin: 0 -1.25rem 1.4rem; padding: 0.5rem 1.25rem; overflow-x: auto; scrollbar-width: none;
  background: rgba(253,252,249,0.9); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); }
.optabs::-webkit-scrollbar { display: none; }
.optabs a {
  flex: 0 0 auto;   /* DT-P59: the strip scrolls; the tabs keep their words */
  display: inline-flex; align-items: center; height: 2.2rem; padding: 0 0.85rem; border-radius: 999px;
  text-decoration: none; color: var(--ink-soft); font-family: var(--display); font-size: 0.86rem; font-weight: 500;
  white-space: nowrap; transition: background var(--ease-t) var(--curve), color var(--ease-t) var(--curve);
}
@media (hover: hover) { .optabs a:hover { background: var(--bg-soft); color: var(--ink); } }
.optabs__here { background: var(--ink) !important; color: var(--light) !important; }
.optabs a { gap: 0.4rem; }
.optabs__n { display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem;
  padding: 0 0.28rem; border-radius: 999px; background: var(--magenta); color: #fff; font-size: 0.68rem; font-weight: 700;
  font-variant-numeric: tabular-nums; }
.optabs__here .optabs__n { background: var(--stagelight); color: var(--night); }

/* Figures are read by comparing them, so they get tabular numerals and a
   serif at display size. Proportional digits make a person compare shapes
   instead of values. */
.opgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 1rem; margin: 0 0 1.5rem; }
.opstat { background: #ffffff; border: 1px solid var(--line); border-radius: 14px; padding: 0.9rem 1rem; }
.opstat b {
  display: block; font-family: var(--serif); font-weight: 600; font-size: 1.62rem; line-height: 1.15;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.opstat span { display: block; margin-top: 0.15rem; color: var(--ink-soft); font-family: var(--display); font-size: 0.79rem; line-height: 1.35; }
.opstat i { display: block; margin-top: 0.25rem; color: var(--ink-soft); font-size: 0.72rem; font-style: normal; }

.opcard { background: #ffffff; border: 1px solid var(--line); border-radius: 16px; padding: 1.15rem 1.25rem; margin: 0 0 1.1rem; }
.opcard--warn { border-color: var(--violet); box-shadow: inset 3px 0 0 var(--violet); }
.opcard--calm { background: var(--bg-soft); }
.opcols { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 1rem; align-items: start; }
.opnote { margin: 0 0 0.75rem; color: var(--ink-soft); font-size: 0.85rem; line-height: 1.55; max-width: 68ch; }
.opempty { margin: 0; color: var(--ink-soft); font-size: 0.88rem; }

/* A wide table is the one thing on this site permitted to scroll sideways,
   in its own box, never the page (AW-075). */
.optablewrap { overflow-x: auto; margin: 0 -0.25rem; }
.optable { width: 100%; border-collapse: collapse; font-size: 0.86rem; font-variant-numeric: tabular-nums; }
.optable th {
  text-align: left; font-family: var(--display); font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase;
  font-weight: 700; color: var(--ink); padding: 0.4rem 0.75rem 0.55rem; border-bottom: 1.5px solid #ab9f89;
  white-space: nowrap;
}
.optable td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #cfc7b6; vertical-align: top; line-height: 1.45; }
@media (hover: hover) { .optable tbody tr:hover { background: var(--bg-soft); } }
.optable .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.optable .nowrap { white-space: nowrap; }
.optable a { font-weight: 600; }
.opsub { display: block; color: var(--ink-soft); font-size: 0.79rem; margin-top: 0.1rem; }

.oppill {
  display: inline-block; padding: 0.1rem 0.55rem; border-radius: 999px; background: var(--bg-soft);
  border: 1px solid var(--line); color: var(--ink-soft); font-family: var(--display); font-size: 0.72rem; font-weight: 500;
  letter-spacing: 0.02em; white-space: nowrap;
}
.oppill--good { color: #1f5c1f; border-color: #b7dcb7; background: #eef7ee; }
.oppill--warn { color: #6d28d9; border-color: #cdb8f5; background: var(--violet-soft); }
.oppill--bad { color: #8a2b2b; border-color: #eec6c2; background: #fbeceb; }

/* Buttons: one height, one spacing step, one row. The brief's complaint was
   buttons that sit at different heights and different distances for no reason
   a person can see, so the row states both once. */
.opcontrols { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-top: 0.75rem; }
.opcontrols > * { margin: 0; }
.opform { display: inline-flex; margin: 0; }
.opwrap .opbtn { text-decoration: none; }
.opwrap button, .opwrap .opbtn {
  margin: 0; min-height: 44px; padding: 0 1.25rem; white-space: nowrap; border-radius: 999px; border: 1px solid transparent;
  background: var(--violet); color: #ffffff; font-family: var(--display); font-size: 0.86rem; font-weight: 500;
  letter-spacing: 0.01em; box-shadow: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
  transition: background var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve), transform var(--tap) var(--curve);
}
/* DT-P80: the button protocol reaches the console. These rules were written
   before DT-P74 to DT-P79 and broke three of them at once — no lift on hover
   (rule 2), a quiet button whose hover changed only its border while the
   substance stayed white (rule 5), and hand-written durations rather than the
   motion tokens. Density is what makes this screen different from the studio;
   how a button answers a pointer is not. */
@media (hover: hover) { .opwrap button:hover, .opwrap .opbtn:hover { background: #5b21b6; color: #ffffff; transform: translateY(-1px); box-shadow: none; } }
.opwrap button:active, .opwrap .opbtn:active { transform: translateY(1px); }
.opwrap button.op-quiet, .opwrap .opbtn--quiet { background: #ffffff; color: var(--ink); border-color: var(--line); }
@media (hover: hover) { .opwrap button.op-quiet:hover, .opwrap .opbtn--quiet:hover { background: #ebe4d5; border-color: var(--ink); color: var(--ink); } }
.opwrap button.op-go { background: var(--stagelight); color: var(--night); }
@media (hover: hover) { .opwrap button.op-go:hover { background: var(--hover-amber); color: var(--night); } }
.opwrap button.op-warn { background: #8a2b2b; color: #ffffff; }
@media (hover: hover) { .opwrap button.op-warn:hover { background: #601d1d; color: #ffffff; } }
.opwrap button:disabled { opacity: 0.6; cursor: progress; transform: none; }
@media (hover: hover) { .opwrap button:disabled:hover { transform: none; } }

/* Every single-line control lands on exactly the declared touch target, so an
   input and a select beside each other are the same height rather than meeting
   in the middle at an arbitrary number. */
.opwrap input, .opwrap select, .opwrap textarea {
  min-height: 44px; padding: 0.55rem 0.75rem; border-radius: 12px; border: 1px solid var(--line);
  background: #ffffff; font-size: 0.88rem; margin: 0;
}
.opwrap textarea { min-height: 6rem; width: 100%; max-width: 100%; line-height: 1.5; }
.opwrap input[type="search"], .opwrap input[type="date"] { max-width: 100%; }
.opwrap label { margin: 0 0 0.3rem; font-family: var(--display); font-size: 0.78rem; font-weight: 600; color: var(--ink-soft); }
.opwrap input:focus, .opwrap select:focus, .opwrap textarea:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px var(--violet-soft); }

/* A filter bar is a row of controls, aligned on their bottoms so the labels
   above them do not push them out of line. */
.opfilters { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; margin-bottom: 1rem; }
.opfilters > div { display: flex; flex-direction: column; }
.opfilters input, .opfilters select { min-width: 9rem; }

/* DT-P58: the support thread. Same bubbles as the studio's, on the console's
   own scale, so a conversation reads as a conversation on both sides. */
.opchat { list-style: none; margin: 0 0 1.1rem; padding: 0; display: flex; flex-direction: column; gap: 0.65rem; max-height: 32rem; overflow-y: auto; }
.opchat__m { max-width: 46rem; padding: 0.7rem 0.95rem; border-radius: 14px; border: 1px solid var(--line); background: #ffffff; }
.opchat__m--me { align-self: flex-end; background: rgba(109,40,217,0.07); border-color: rgba(109,40,217,0.22); }
.opchat__b { margin: 0; font-size: 0.9rem; line-height: 1.6; white-space: pre-wrap; }
.opchat__d { margin: 0.35rem 0 0; font-size: 0.75rem; color: var(--ink-soft); }
/* A form that is a block of fields rather than one control in a row. */
.opform--block { display: flex; flex-direction: column; align-items: flex-start; gap: 0; }
.opform--block textarea { margin-bottom: 0.9rem; }
.optable tr.op-new td { font-weight: 600; }
.optable .opnum { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* DT-P59: on a phone the console's tables keep a readable shape and their own
   box scrolls, exactly as the studio's do (AW-075). */
.optablewrap { overflow-x: auto; }
.optable { min-width: 34rem; }

.opgood, .opbad { padding: 0.75rem 1rem; border-radius: 12px; font-size: 0.9rem; margin: 0 0 1rem; }
.opgood { background: #eef7ee; border: 1px solid #b7dcb7; color: #1f5c1f; }
.opgood::before { content: '✓'; margin-right: 0.5rem; font-weight: 700; }
.opbad { background: #fbeceb; border: 1px solid #eec6c2; color: #8a2b2b; }
.opbad::before { content: '⚠'; margin-right: 0.5rem; font-weight: 700; }

/* A bar per month. Server-rendered, no library, no script: an operator screen
   that needs JavaScript to show a number is a screen that sometimes does not. */
/* A percentage height needs a parent with a definite one. align-items:flex-end
   would collapse each column to its own content, and the bar's percentage
   would then resolve against nothing and render as a hairline. The columns
   stretch to the full height instead and the bar is pushed down by an auto
   margin. */
.opbars { display: flex; align-items: flex-end; gap: 0.4rem; margin: 0.75rem 0 0.25rem; }
.opbars div { flex: 1 1 0; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; gap: 0.3rem; }
.opbars i { display: block; width: 100%; background: var(--violet); border-radius: 4px 4px 0 0; }
/* AW-056: this product sets style-src 'self', so it writes no style
   attributes at all. A bar height therefore cannot be an inline style, and
   the first attempt at one was refused by the browser silently and rendered
   nothing. The heights are a fixed ladder of classes instead, declared here
   where the policy permits them, and the renderer quantises to the nearest
   rung. Twenty-four rungs is finer than the eye reads a bar chart. */
.opbar--0 { height: 0.15rem; }
.opbar--1 { height: 0.41rem; }
.opbar--2 { height: 0.67rem; }
.opbar--3 { height: 0.93rem; }
.opbar--4 { height: 1.19rem; }
.opbar--5 { height: 1.45rem; }
.opbar--6 { height: 1.71rem; }
.opbar--7 { height: 1.97rem; }
.opbar--8 { height: 2.23rem; }
.opbar--9 { height: 2.49rem; }
.opbar--10 { height: 2.75rem; }
.opbar--11 { height: 3.01rem; }
.opbar--12 { height: 3.27rem; }
.opbar--13 { height: 3.54rem; }
.opbar--14 { height: 3.80rem; }
.opbar--15 { height: 4.06rem; }
.opbar--16 { height: 4.32rem; }
.opbar--17 { height: 4.58rem; }
.opbar--18 { height: 4.84rem; }
.opbar--19 { height: 5.10rem; }
.opbar--20 { height: 5.36rem; }
.opbar--21 { height: 5.62rem; }
.opbar--22 { height: 5.88rem; }
.opbar--23 { height: 6.14rem; }
.opbar--24 { height: 6.40rem; }
.opbars span { font-size: 0.68rem; color: var(--ink); letter-spacing: 0.02em; }   /* DT-P40: an axis label is a label */
.opbarnote { display: flex; justify-content: space-between; font-size: 0.72rem; color: var(--ink-soft); margin: 0; }

.opdl { display: grid; grid-template-columns: minmax(9rem, auto) 1fr; gap: 0.4rem 1rem; margin: 0; font-size: 0.88rem; }
.opdl dt { font-family: var(--display); font-size: 0.8rem; color: var(--ink-soft); }
.opdl dd { margin: 0; }
.opquote {
  margin: 0.5rem 0 0; padding: 0.75rem 1rem; background: var(--bg-soft); border-left: 3px solid var(--violet);
  border-radius: 0 12px 12px 0; font-size: 0.88rem; line-height: 1.6; white-space: pre-wrap;
}
/* ---- DT-P80: classes the screens have been asking for and never had ------
   .opform--stack, .opmeta, .opdone, .opwarn and .opask were written in
   screens.js and declared NOWHERE. A CSS class that does not exist fails in
   total silence — no error, no warning, the element simply keeps whatever it
   inherited — and here that meant the Offers form inherited
   .opform's own inline-flex, and laid six fields and a button out in a
   single row running off the side of the card. That is exactly the shape of
   AW-056's silent style-attribute refusal, and it now has a test: every class
   an operator screen uses must be declared in this stylesheet.

   The lesson is worth more than the fix. A misspelt or missing class is the
   only kind of front-end mistake that produces no diagnostic anywhere, in any
   browser, ever — so it has to be caught by something that reads both files. */
.opform--stack { display: flex; flex-direction: column; align-items: stretch; gap: 0.35rem; max-width: 46rem; }
/* DIRECT children only. As a descendant selector this also caught the labels
   inside .opchoice and .oppick, and the 0.85rem it adds pushed the second
   radio pill down by fourteen pixels while the first stretched — two controls
   in one row at two different heights, which is the exact defect AW-107 is
   about, reintroduced by a selector that was one character too greedy. */
.opform--stack > label { margin-top: 0.85rem; }
.opform--stack > label:first-child { margin-top: 0; }
.opform--stack input, .opform--stack select, .opform--stack textarea { width: 100%; }
.opform--stack > button { align-self: flex-start; margin-top: 1.3rem; }
/* A row of fields that genuinely belong side by side, which is different from
   a stack that lost its class. Aligned at the TOP, so the labels sit on one
   line: bottom alignment lines up the controls and lets the labels drift,
   which reads as the wonkier of the two. Every control here is 44px anyway,
   so the bottoms line up regardless. */
.opform--row { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; align-items: flex-start; margin-top: 0.85rem; }
.opform--row > div { display: flex; flex-direction: column; flex: 1 1 8rem; }
.opform--row > div > label { margin: 0 0 0.3rem; }
.opform--row input, .opform--row select { min-width: 0; width: 100%; }

.opmeta { display: block; color: var(--ink-soft); font-size: 0.82rem; line-height: 1.5; }
.opdone { margin: 0; color: #1f5c1f; font-size: 0.9rem; font-weight: 600; }
.opwarn { margin: 0; color: #8a2b2b; font-size: 0.9rem; font-weight: 600; }
.opask { display: flex; flex-direction: column; align-items: stretch; gap: 0.35rem; max-width: 34rem; }
.opask label { margin-top: 0.85rem; }
.opask label:first-child { margin-top: 0; }

/* ---- DT-P80: the diary, the notice composer and the delete gate ---------- */
.opday { margin: 1.4rem 0 0.5rem; padding-bottom: 0.4rem; border-bottom: 1.5px solid #ab9f89;
  font-family: var(--display); font-size: 0.82rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; }
.opday:first-child { margin-top: 0; }
.opmeet { display: grid; grid-template-columns: 5.5rem minmax(0, 1fr) auto; gap: 0.5rem 1rem; align-items: center;
  padding: 0.85rem 0; border-bottom: 1px solid #cfc7b6; }
.opmeet:last-child { border-bottom: none; }
.opmeet__t { font-family: var(--display); font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.opmeet__w { min-width: 0; }
.opmeet__w b { display: block; font-size: 0.92rem; }
.opmeet__a { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: flex-end; }
@media (max-width: 44rem) {
  .opmeet { grid-template-columns: 1fr; }
  .opmeet__a { justify-content: flex-start; }
}

/* The recipient picker. A scrolling list of checkboxes rather than a
   multi-select: a native multi-select needs a modifier key nobody remembers,
   and an operator who ticks the wrong teacher here sends a stranger a letter. */
.oppick { max-height: 18rem; overflow-y: auto; border: 1px solid var(--line); border-radius: 12px; padding: 0.35rem 0.65rem; background: #fff; }
.oppick label { display: flex; align-items: center; gap: 0.6rem; margin: 0; padding: 0.45rem 0.1rem;
  font-family: var(--body); font-size: 0.88rem; font-weight: 400; color: var(--ink); border-bottom: 1px solid var(--bg-soft); cursor: pointer; }
.oppick label:last-child { border-bottom: none; }
.oppick input[type="checkbox"], .oppick input[type="radio"] { min-height: 0; width: 1.05rem; height: 1.05rem; flex: 0 0 auto; margin: 0; padding: 0; }
.oppick span { color: var(--ink-soft); font-size: 0.8rem; }
.opchoice { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0.2rem 0 0.4rem; }
.opchoice label { display: inline-flex; align-items: center; gap: 0.45rem; margin: 0 !important; padding: 0.5rem 0.85rem; min-height: 44px;
  border: 1px solid var(--line); border-radius: 999px; background: #fff; cursor: pointer;
  font-family: var(--display); font-size: 0.83rem; font-weight: 500; color: var(--ink); }
.opchoice input { min-height: 0; width: 1rem; height: 1rem; margin: 0; padding: 0; }

/* The delete gate is the one screen in this console that is allowed to shout.
   Everything else is deliberately flat; this is the single action that cannot
   be undone, and a page that looks like every other page is a page somebody
   reads at the speed of every other page. */
.opdanger { border: 1.5px solid #8a2b2b; border-radius: 16px; background: #fdf6f5; padding: 1.15rem 1.25rem; margin: 0 0 1.1rem; }
.opdanger h2 { color: #8a2b2b; }
.opdanger ul { margin: 0.6rem 0 0; padding-left: 1.1rem; }
.opdanger li { margin: 0.25rem 0; font-size: 0.9rem; }
.opdanger .opform--stack { margin-top: 1.1rem; }

@media (max-width: 40rem) {
  .opwho { font-size: 0.8rem; }
  .opwrap { padding: 1rem 0.9rem 4rem; }
  /* DT-P59: the tab strip bleeds to the edge by pulling against the wrapper's
     padding, so when that padding changes the bleed has to change with it.
     Left at 1.25rem against a 0.9rem wrapper it was 11px wider than the phone
     and scrolled the whole page sideways — AW-075's exact prohibition, caused
     by two numbers that have to agree and were only stated once. */
  .optabs { margin-left: -0.9rem; margin-right: -0.9rem; padding-left: 0.9rem; padding-right: 0.9rem; }
}

/* ---- canvas ---------------------------------------------------------- */
.studio { max-width: 96rem; margin: 0 auto; padding: 0 1.6rem 4rem; }
.studio * { min-width: 0; }              /* grid children may shrink: the one line that stops a long word widening the page */

/* ---- the rail: horizontal, sticky under the masthead ------------------ */
.srail { position: sticky; top: 3.6rem; z-index: 8; margin: 0 -1.6rem 1.4rem; padding: 0 1.6rem;
  background: rgba(253,252,249,0.86); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); }
.srail__in { display: flex; align-items: center; gap: 1.2rem; height: 3.5rem; }
/* DT-P71: the name shrinks, and CLIPS rather than overflowing. .studio * sets
   min-width: 0 so every flex child may shrink below its content — which is
   what stops a long word widening the page — and with nowrap and no overflow
   rule the text simply spilled out of its box and ran into the first tab
   ("Miss Eleanor VanceToday"). The same family as the DT-P57 tab clipping, one
   element to the left. It keeps a floor so it never shrinks to nothing. */
.srail__who { flex: 0 1 auto; min-width: 4rem; max-width: 14rem; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--display); font-weight: 600; font-size: 0.95rem; letter-spacing: -0.01em; white-space: nowrap; }
/* DT-P57: the tabs keep their own width and the STRIP scrolls. Without
   flex: 0 0 auto on each tab, flex shrank every one of them until the words
   ran into each other and the first was clipped mid-letter — which is what a
   phone showed. min-width: 0 on the strip is what lets it be narrower than its
   contents so the overflow is real scrolling rather than a wider page. */
.srail__tabs { display: flex; gap: 0.15rem; flex: 1 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
/* DT-P198: five items now, not fifteen, and a menu that opens below its
   heading cannot live in a strip that clips what overflows it. So the strip
   no longer scrolls; on a narrow screen it wraps onto a second line instead. */
.srail__tabs { overflow: visible; flex-wrap: wrap; align-items: center; }
.srail__tabs::-webkit-scrollbar { display: none; }
.srail__t { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0.4rem; height: 2.2rem; padding: 0 0.85rem; border-radius: 999px;
  font-family: var(--display); font-size: 0.86rem; font-weight: 500; color: var(--ink-soft); text-decoration: none; white-space: nowrap; transition: background var(--ease-t) var(--curve), color var(--ease-t) var(--curve); }
@media (hover: hover) { .srail__t:hover { background: var(--bg-soft); color: var(--ink); } }
.srail__t.is-on { background: var(--ink); color: var(--light); }
.srail__n { display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem; padding: 0 0.28rem;
  border-radius: 999px; background: var(--magenta); color: #fff; font-size: 0.68rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.srail__t.is-on .srail__n { background: var(--stagelight); color: var(--night); }
/* DT-P198: the groups. A heading drawn exactly like a tab, a small chevron
   that turns when it is open, and a panel under it holding that group's
   screens. The panel is a card, not a floating list of words. */
.smenu { position: relative; flex: 0 0 auto; }
.smenu__s { list-style: none; cursor: pointer; user-select: none; }
.smenu__s::-webkit-details-marker { display: none; }
.smenu__c { width: 0.42rem; height: 0.42rem; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform var(--ease-t) var(--curve); }
.smenu[open] .smenu__c { transform: translateY(1px) rotate(-135deg); }
.smenu[open] > .smenu__s:not(.is-on) { background: var(--bg-soft); color: var(--ink); }
.smenu__p { position: absolute; top: calc(100% + 0.4rem); left: 0; z-index: 20; min-width: 14rem; display: flex; flex-direction: column; gap: 0.1rem;
  padding: 0.45rem; border-radius: 14px; background: var(--surface-raised); border: 1px solid var(--line); box-shadow: 0 18px 40px -20px rgba(23,20,31,0.45); }
.smenu__i { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; min-height: 44px; padding: 0 0.8rem; border-radius: 10px;
  font-family: var(--display); font-size: 0.9rem; color: var(--ink); text-decoration: none; white-space: nowrap; transition: background var(--ease-t) var(--curve); }
@media (hover: hover) { .smenu__i:hover { background: var(--bg-soft); } }
.smenu__i.is-on { background: var(--ink); color: var(--light); }
.smenu__i.is-on .srail__n { background: var(--stagelight); color: var(--night); }
@media (max-width: 44rem) {
  .srail__in { height: auto; min-height: 3.5rem; padding: 0.4rem 0; }
  .smenu__p { position: fixed; left: 1rem; right: 1rem; top: auto; }
}

/* Rules 2 and 3 of the button protocol reach the rail too. This one is a link
   drawn as a button, and a link drawn as a button owes the same feedback as a
   button — otherwise the only control that is on every screen is the one
   control that never answers a pointer. One rule, not two: a control whose
   look and whose motion are declared in separate places is a control where
   somebody eventually edits one and not the other. */
.srail__room { flex: 0 0 auto; height: 2.2rem; display: inline-flex; align-items: center; padding: 0 0.95rem; border-radius: 999px;
  border: 1px solid var(--border-strong); font-family: var(--display); font-size: 0.83rem; color: var(--ink); text-decoration: none; white-space: nowrap;
  transition: border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve), background var(--ease-t) var(--curve), transform var(--tap) var(--curve); }
@media (hover: hover) { .srail__room:hover { border-color: var(--violet); color: var(--violet); background: var(--bg-soft); transform: translateY(-1px); } }
.srail__room:active { transform: translateY(1px); }
@media (max-width: 44rem) {
  .srail__who { display: none; }
  /* DT-P57: on a phone the rail is the tabs and nothing else. The sign-out
     control could not shrink below its own words, so it pushed the page 16px
     wider than the screen — a horizontal scroll on every portal screen, which
     AW-075 forbids outright. Signing out lives on Your account, which is where
     a person looks for it anyway. */
  /* .srail scopes these so a later rule of equal weight cannot undo them. */
  .srail .srail__end { display: none; }
  /* DT-P57: and the teacher's room link, for the same reason — it squeezed the
     tabs until they clipped. The room is one tap away on Today, which is the
     screen a teacher lands on. */
  .srail .srail__room { display: none; }
}

/* ---- the greeting ----------------------------------------------------- */
.shello { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 1.6rem; align-items: center;
  background: linear-gradient(118deg, #3b1178 0%, #6d28d9 42%, #b81a63 100%); color: #fff;
  border-radius: 20px; padding: 1.7rem 1.9rem; margin-bottom: 1.1rem; position: relative; overflow: hidden; }
/* A single soft light, top right. Enough to stop a flat rectangle reading as a
   block of colour, not so much that it becomes a gradient demo. */
.shello::after { content: ''; position: absolute; width: 26rem; height: 26rem; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.16), transparent 68%); top: -13rem; right: -6rem; pointer-events: none; }
.shello > * { position: relative; z-index: 1; }
.shello__d { margin: 0 0 0.45rem; font-family: var(--display); font-size: 0.76rem; letter-spacing: 0.09em; text-transform: uppercase; color: rgba(255,255,255,0.92); }
.shello__h { margin: 0; font-family: var(--serif); font-size: clamp(1.5rem, 3.1vw, 2.15rem); line-height: 1.1; font-weight: 600; letter-spacing: -0.02em; }
.shello__s { margin: 0.5rem 0 0; font-size: 0.97rem; line-height: 1.55; color: rgba(255,255,255,0.96); max-width: 48ch; }
.shello__f { margin: 0.7rem 0 0; padding-top: 0.7rem; border-top: 1px solid rgba(255,255,255,0.2); font-size: 0.86rem; line-height: 1.55; color: rgba(255,255,255,0.9); max-width: 52ch; }
.sday { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; max-height: 11rem; overflow-y: auto; }
.sday--none { display: flex; align-items: center; justify-content: center; border: 1px dashed rgba(255,255,255,0.3); border-radius: 12px; padding: 1rem; }
.sday--none p { margin: 0; color: rgba(255,255,255,0.94); font-size: 0.88rem; }
.sday__i { display: grid; grid-template-columns: 3.2rem minmax(0,1fr) auto auto; gap: 0.7rem; align-items: center;
  padding: 0.5rem 0.7rem; border-radius: 10px; background: rgba(255,255,255,0.14); font-size: 0.87rem; }
.sday__i.is-done { opacity: 0.62; }   /* DT-P40: past, not erased */
.sday__i.is-live { background: rgba(255,214,153,0.26); box-shadow: inset 0 0 0 1px rgba(255,214,153,0.55); }
.sday__t { font-family: var(--display); font-variant-numeric: tabular-nums; color: #ffd699; font-weight: 600; }
.sday__n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sday__m { color: rgba(255,255,255,0.9); font-size: 0.79rem; white-space: nowrap; }
.sday__go { color: #fff; text-decoration: none; border-bottom: 1px solid #ffd699; font-size: 0.79rem; white-space: nowrap; }
@media (max-width: 62rem) { .shello { grid-template-columns: 1fr; } }
/* DT-P52: the family's greeting has no second panel to put beside it — the
   studio's right-hand column holds today's classes and a parent has no
   equivalent. One track rather than a two-track grid with a hole in it. */
.shello--solo { grid-template-columns: minmax(0, 1fr); }
.shello--solo .shello__s { max-width: 68ch; }

/* DT-P52: the rail's right-hand control. The studio's is a link to the room;
   the portal's is a form, so it is a button that has to LOOK like that link
   rather than like a submit button — same height, same pill, same quiet
   border, and none of the global button margin. */
.srail__end { display: flex; margin: 0; }
.srail__out { flex: 0 0 auto; height: 2.2rem; min-height: 0; display: inline-flex; align-items: center; padding: 0 0.95rem;
  margin: 0; border-radius: 999px; border: 1px solid var(--border-strong); background: transparent; box-shadow: none;
  font-family: var(--display); font-size: 0.82rem; font-weight: 500; color: var(--ink-soft); cursor: pointer; white-space: nowrap; }
@media (hover: hover) { .srail__out:hover { background: transparent; border-color: var(--violet); color: var(--violet); transform: none; box-shadow: none; } }

/* ---- "How to use this page" (DT-P61) ----------------------------------
   Closed by default, because a person who knows the screen should not have to
   look past an explanation of it every day. Open, it is a card like any other
   card — help that looks like a foreign object is help nobody trusts.
   A <details> needs no script: it opens on click and on Enter, it is announced
   as a disclosure, and it cannot break. */
.phelp { margin: 0 0 1.1rem; border: 1px solid var(--line); border-radius: 16px; background: #fff; overflow: hidden; }
.phelp__s { display: flex; align-items: center; gap: 0.6rem; padding: 0.85rem 1.25rem; cursor: pointer;
  font-family: var(--display); font-size: 0.88rem; font-weight: 600; color: var(--ink); list-style: none; }
.phelp__s::-webkit-details-marker { display: none; }
.phelp__s::after { content: '+'; margin-left: auto; font-size: 1.1rem; font-weight: 400; color: var(--ink-soft); }
.phelp[open] .phelp__s::after { content: '–'; }
.phelp[open] .phelp__s { border-bottom: 1px solid var(--line); }
@media (hover: hover) { .phelp__s:hover { color: var(--violet); } }
.phelp__i { display: inline-flex; align-items: center; justify-content: center; width: 1.35rem; height: 1.35rem;
  border-radius: 999px; background: var(--violet); color: #fff; font-size: 0.8rem; font-weight: 700; flex: 0 0 auto; }
/* DT-P76: "how to use this page" opens rather than appears. A <details> cannot
   be transitioned open without script — the browser flips display outright —
   but the body of it can ease in once it exists, and that is what the eye
   follows. The chevron and the border change on the summary carry the rest.
   The animation is on the body, not the whole element, so the summary itself
   never moves under the pointer that just clicked it. */
.phelp__b { padding: 1.1rem 1.25rem 1.25rem; }
.phelp[open] .phelp__b { animation: disclose var(--enter) var(--curve) both; }
.phelp__s { transition: color var(--ease-t) var(--curve), background var(--ease-t) var(--curve); }
.phelp__s::after { transition: transform var(--ease-t) var(--curve); }
@keyframes disclose { from { opacity: 0; transform: translateY(-6px); } }
.phelp__l { margin: 0; font-size: 0.95rem; line-height: 1.6; color: var(--ink); max-width: 68ch; }
.phelp__u { margin: 0.85rem 0 0; padding: 0; list-style: none; }
.phelp__u li { position: relative; padding: 0.4rem 0 0.4rem 1.35rem; font-size: 0.89rem; line-height: 1.6; color: var(--ink); max-width: 72ch; }
.phelp__u li::before { content: '·'; position: absolute; left: 0.4rem; top: 0.3rem; color: var(--violet); font-weight: 700; font-size: 1.1rem; }
.phelp__u li + li { border-top: 1px solid var(--bg-soft); }
.phelp__n { margin: 1rem 0 0; padding: 0.8rem 1rem; border-radius: 12px; background: var(--bg-soft);
  font-size: 0.87rem; line-height: 1.6; color: var(--ink); max-width: 72ch; }
/* DT-P173: the same list, NUMBERED, where the order is the instruction.
   "Open an account, make the link, copy it across" read as three bullets is
   three things to do in any order; read as 1, 2, 3 it is a route. Same rule
   as everywhere else on this screen — the shape of the thing carries part of
   the meaning, so it has to be the right shape. */
ol.phelp__u { counter-reset: phelp; }
ol.phelp__u li { padding-left: 1.8rem; }
ol.phelp__u li::before { counter-increment: phelp; content: counter(phelp); left: 0.15rem; top: 0.45rem;
  font-size: 0.78rem; font-variant-numeric: tabular-nums; }
/* A link inside the help, visibly a link: this one leaves the product for a
   payments provider, and a destination somebody cannot see is the shape of a
   phishing message (DT-P29's rule, applied to our own page). The addresses
   are written out rather than hidden behind "click here" for the same
   reason. */
.phelp__a { color: var(--violet); text-decoration: underline; text-underline-offset: 2px; word-break: break-word; }
@media (hover: hover) { .phelp__a:hover { color: var(--stagelight-deep); } }

/* DT-P63: the Academy's lesson page. */
.sless__v { display: block; width: 100%; border-radius: 12px; border: 1px solid var(--line); background: var(--night); margin: 0 0 0.2rem; }
.sless__h { margin: 1.5rem 0 0.35rem; font-family: var(--display); font-size: 0.95rem; font-weight: 600; }
.scard__b > .sless__h:first-child { margin-top: 0; }
.sless__p { margin: 0; font-size: 0.95rem; line-height: 1.68; max-width: 66ch; color: var(--ink); }

/* ---- the grid: twelve columns across, cards spanning them -------------- */
/* DT-P56: cards that sit side by side are the same height.
   Philip: "make the containers the same size so that they look consistent."
   align-items: start sized every card to its own content, so a row of two was
   a row of two different rectangles — which reads as a mistake even when the
   content genuinely differs in length. Stretch makes the ROW the unit: the
   cards match, and the shorter one simply has more room at the bottom, which
   is what a designed grid looks like. The card is a column so its body can
   take up the slack rather than leaving a gap under the border. */
.sgrid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 1.1rem; align-items: stretch; }
.scard { grid-column: span 12; background: #fff; border: 1px solid var(--line); border-radius: 16px; overflow: hidden;
  display: flex; flex-direction: column; }
.scard__b { flex: 1 1 auto; }
.scard--3 { grid-column: span 3; } .scard--4 { grid-column: span 4; } .scard--5 { grid-column: span 5; }
.scard--6 { grid-column: span 6; } .scard--7 { grid-column: span 7; } .scard--8 { grid-column: span 8; } .scard--12 { grid-column: span 12; }
.scard--stage { background: linear-gradient(160deg, #2f0f63 0%, #5b21b6 100%); border-color: transparent; color: #fff; }
.scard--stage .scard__h { border-bottom-color: rgba(255,255,255,0.18); }
.scard--stage h2 { color: #fff; }
/* DT-P73: EVERY kind of prose, not a list of the three that existed when this
   was written. A .sclear paragraph on the new store card came out near-black
   on the purple and could not be read at all. A tone that has to be told about
   each new text class is a tone that will keep doing this, so the rule now
   names the element and the exceptions are the ones that carry their own
   colour on purpose. */
.scard--stage .shint, .scard--stage .sroom__p, .scard--stage .sclear,
.scard--stage p, .scard--stage li, .scard--stage dd { color: rgba(255,255,255,0.94); }
.scard--stage dt, .scard--stage .sstat__l { color: rgba(255,255,255,0.8); }
.scard--stage a { color: #ffd699; }
.scard--stage .sfeat li::before { color: #ffd699; }
.scard--stage .swas { color: rgba(255,255,255,0.78); }
/* DT-P160. .sclear carries no margin, which is right when a card holds one
   paragraph and wrong the moment it holds two: the timezone notice ran its
   explanation and its "2 bookings would move" straight into each other as one
   grey slab, and the sentence a teacher most needs to see disappeared into the
   one above it. Scoped to this tone rather than made global, because every
   other .sclear in the product is already spaced by whatever sits around it. */
.scard--stage .sclear + .sclear { margin-top: 0.7rem; }
.scard--stage .scode code { background: rgba(255,255,255,0.16); color: #ffd699; }
.scard__h { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem 0.8rem; border-bottom: 1px solid var(--line); }
.scard__h h2 { margin: 0; font-family: var(--display); font-size: 0.93rem; font-weight: 600; letter-spacing: -0.01em; }
.scard__a { font-size: 0.8rem; color: var(--violet); text-decoration: none; white-space: nowrap; }
.scard__b { padding: 1.3rem 1.25rem; }
@media (max-width: 74rem) {
  .scard--3, .scard--4, .scard--5 { grid-column: span 6; }
  .scard--7, .scard--8 { grid-column: span 12; }
}
@media (max-width: 48rem) { .scard { grid-column: span 12 !important; } }

/* ---- figures ---------------------------------------------------------- */
/* DT-P57: two figures fit side by side on a phone and one does not read as a
   dashboard. 9.5rem plus a 1.6rem gutter needed 329px of a 318px card, so
   every stat block became a single column of numbers. */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(7.25rem, 1fr)); gap: 0.9rem 1.4rem; }
@media (max-width: 30rem) { .stats { gap: 0.85rem 1rem; } }
/* A figure row sitting under prose inside a card, rather than being the whole
   card. It needs a rule above it and room to breathe: without them the numbers
   read as a continuation of the sentence, which is what made the Grow listing
   card look like a paragraph that had fallen over. */
.stats--inset { margin: 1.3rem 0 0; padding: 1.2rem 0 0; border-top: 1px solid var(--line); }
.stats--inset + .srow { margin-top: 1.3rem; }
.sstat__v { margin: 0; font-family: var(--display); font-size: 1.55rem; font-weight: 600; letter-spacing: -0.025em; font-variant-numeric: tabular-nums; line-height: 1.15; }
.sstat__l { margin: 0.1rem 0 0; font-size: 0.79rem; color: var(--ink-soft); }
/* DT-P40: this was ink-soft at 0.8, which lands around 4.3:1 — a note nobody
   reads is a note not worth rendering. The opacity is gone; the size already
   says it is secondary. */
.sstat__n { margin: 0.12rem 0 0; font-size: 0.73rem; color: var(--ink-soft); }
.sstat--warn .sstat__v { color: var(--amber); }

.spill { display: inline-flex; align-items: center; height: 1.35rem; padding: 0 0.5rem; border-radius: 999px; background: var(--bg-soft);
  font-family: var(--display); font-size: 0.72rem; font-weight: 500; color: var(--ink-soft); white-space: nowrap; }
.spill--good { background: rgba(109,40,217,0.1); color: var(--violet); }
.spill--warn { background: rgba(229,105,16,0.13); color: #a2470a; }

/* ---- the first-run guide (DT-P154) ------------------------------------- */
/* A numbered list that turns into ticks as a teacher works through it. On the
   dark card it is the first thing she meets; on Academy the same list sits on
   the ordinary ground, because there it is reference rather than a prompt. */
/* Capped, because on a wide screen the status sat a hand's width from the step
   it belonged to and read as a different column of the page. A measure is what
   makes a list of five things scannable; full bleed is what makes it a table. */
.sguide { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.5rem; max-width: 54rem; }
.sguide__c { margin: 0.9rem 0 0; font-size: 0.85rem; }
.sguide__i { display: grid; grid-template-columns: 2rem 1fr auto; align-items: start; gap: 0.2rem 0.9rem;
  padding: 0.85rem 1rem; border-radius: 14px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.14); }
.sguide__n { display: inline-flex; align-items: center; justify-content: center; width: 1.9rem; height: 1.9rem;
  border-radius: 50%; background: var(--stagelight); color: var(--night); font-family: var(--display);
  font-size: 0.85rem; font-weight: 600; }
.sguide__i.is-done .sguide__n { background: rgba(255,255,255,0.22); color: #fff; }
.sguide__a { font-family: var(--display); font-weight: 600; font-size: 0.96rem; text-decoration: none; }
@media (hover: hover) { .sguide__a:hover { text-decoration: underline; } }
.sguide__w { margin: 0.2rem 0 0; font-size: 0.86rem; line-height: 1.5; max-width: 62ch; }
/* The status word is the quietest thing in the row on purpose: the teacher is
   here to read what to do next, not to be scored. */
.sguide__s { font-family: var(--display); font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.72; white-space: nowrap; }
.sguide__i.is-done .sguide__a { opacity: 0.75; }
.sguide__f { margin: 1rem 0 0; font-size: 0.85rem; line-height: 1.55; max-width: 68ch; }
.sguide__x { margin: 0.9rem 0 0; }
/* On Academy the card is not the dark one, so the row needs the light palette's
   own edges rather than the white-on-violet ones above. */
.scard--guide:not(.scard--stage) .sguide__i { background: var(--bg-soft); border-color: var(--line); }
.scard--guide:not(.scard--stage) .sguide__a { color: var(--ink); }
.scard--guide:not(.scard--stage) .sguide__w { color: var(--ink-soft); }
/* And the DONE badge with it. The rule above receded it with translucent white,
   which is right on the violet card and invisible on this one — a completed step
   with a blank disc where its number was, seen in a screenshot before anybody
   met it. Receding on a light ground means going quiet in ink, not in white. */
.scard--guide:not(.scard--stage) .sguide__i.is-done .sguide__n { background: var(--ink-soft); color: var(--bg); }
/* DT-P157: the one step to do next. Louder than the rest and quieter than an
   alert — it is a signpost, not a warning, and a teacher who would rather pick
   her own order must not feel told off for it. The left edge carries it, so
   nothing moves and no row changes size when the next step changes. */
.sguide__i.is-next { border-left: 3px solid var(--stagelight); }
/* DT-P158: where a face-to-face class is, on the family's own list. Its own
   line under the title rather than another column, because a hall address is
   long and a table that grows a column is a table that stops fitting a phone. */
.swhere { display: block; margin-top: 0.15rem; font-size: 0.82rem; color: var(--ink-soft); }
/* DT-P158b: the register's ticks. A list with bullets beside its checkboxes
   reads as two lists of markers and neither is the one you press. */
.sreg { list-style: none; margin: 1rem 0 0; padding: 0; display: grid; gap: 0.15rem; }
.sreg__l { display: flex; align-items: center; gap: 0.7rem; padding: 0.55rem 0.6rem; border-radius: 10px; cursor: pointer; min-height: 44px; }
.sreg__l input { width: 1.15rem; height: 1.15rem; flex: none; }
/* DT-P159: the timezone-move notice. It renders on the violet card, where the
   light theme's dim class is close to invisible — and the OLD time is the half
   a teacher checks against her memory, so it has to be readable while still
   reading as the past. Its own class, so nothing else that dims moves.
   (No backticks in here: this stylesheet is a JS template literal, and one
   stray backtick ends the string and takes the rest of the file with it.) */
.szm { margin-top: 1rem; }
.szm__was { color: rgba(255,255,255,0.62); }
.scard--guide .szm__was, .scard:not(.scard--stage) .szm__was { color: var(--ink-soft); }
.sheld__w { font-size: 0.78rem; letter-spacing: 0.04em; text-transform: uppercase; opacity: 0.75; }
/* DT-P182: the free times a family picks from. A day is a heading and a wrapped
   row of times, because the shape of the answer is "which day, then what time"
   and a flat list of eighty buttons makes the reader do the grouping. The
   buttons are ordinary .sbtn, so they inherit the lift, press and hover the
   button protocol requires and nothing here restates a colour. */
.slots { display: grid; gap: 1.1rem; margin: 0.6rem 0 0.2rem; }
.slotday__h { margin: 0 0 0.4rem; font-size: 0.95rem; font-weight: 600; }
.slotday__t { display: flex; flex-wrap: wrap; gap: 0.4rem; }
/* DT-P185: a thumb, not a pin.
   The lesson drill measured these at 65x34 on a phone — correct, legible, and
   below the 44px every platform's guidance agrees on. A parent books at nine at
   night, one-handed, often moving, and a 34px target is one they miss and then
   book the wrong half hour. The button protocol's colour, lift and press are
   untouched; this sets a floor on the box they happen in. */
.slotday__t .sbtn { min-height: 44px; min-width: 44px; }
.sguide__i.is-next .sguide__s { opacity: 1; font-weight: 600; }
.scard--guide:not(.scard--stage) .sguide__i.is-next { border-left-color: var(--stagelight-deep); }
@media (max-width: 40rem) {
  .sguide__i { grid-template-columns: 1.9rem 1fr; }
  .sguide__s { grid-column: 2; justify-self: start; }
}

/* ---- lists ------------------------------------------------------------ */
.sneed, .smini, .sinbox, .snotes, .sfeat { list-style: none; margin: 0; padding: 0; }
.sneed__i { padding: 0.55rem 0 0.55rem 0.9rem; border-left: 3px solid var(--line); font-size: 0.88rem; }
.sneed__i + .sneed__i { border-top: 1px solid var(--line); }
.sneed__i--warn { border-left-color: var(--amber); }
.sneed__i--bad { border-left-color: var(--magenta); }
.sneed__i a { color: var(--ink); text-decoration: none; }
@media (hover: hover) { .sneed__i a:hover { text-decoration: underline; } }
/* A cap on the line length, because a card can be 60rem wide and a sentence
   that long is one nobody finishes (DT-P71). */
.sclear { margin: 0; font-size: 0.9rem; color: var(--ink-soft); line-height: 1.55; max-width: 68ch; }
/* A statement that is the point of its card rather than a note beside a form:
   it takes the width it is given. Still capped, because a line the width of a
   96rem canvas is unreadable however important it is. */
.sclear--wide { max-width: 96ch; }

/* DT-P73: the action row sits on the floor of its card.
   Philip: "can we make the button placement uniform so they look aligned."
   Cards in a row are already the same height — .sgrid stretches them — but the
   content stacked from the top, so two cards side by side put their buttons at
   two different heights depending on how much text was above. A card body that
   is a flex column with its last .srow pushed down by margin-top: auto lines
   every action in a row up, everywhere in both portals, without any screen
   having to know about it. */
.scard__b { display: flex; flex-direction: column; }
.scard__b > .srow:last-child { margin-top: auto; padding-top: 1rem; }
.scard--fit { align-self: start; }

/* DT-P75: an offer folded to one line. It keeps the dark card so it still
   reads as an offer rather than as a notice, but it is one row high: the name,
   the one line that says what it is, and the way back in. */
.soffer { display: flex; align-items: center; justify-content: space-between; gap: 1.2rem; flex-wrap: wrap; }
.soffer--folded .scard__b { padding: 1rem 1.25rem; }
.soffer__m { min-width: 0; flex: 1 1 16rem; }
.soffer__t { margin: 0; font-family: var(--display); font-size: 0.95rem; font-weight: 600; color: #fff; }
.soffer__s { margin: 0.2rem 0 0; font-size: 0.85rem; line-height: 1.5; color: rgba(255,255,255,0.9); }
.soffer form { display: contents; }
/* DT-P76: folding is a round trip to the server, so there is no element to
   animate from one state to the other — the page that comes back is a
   different page. What CAN be smooth is the arrival, and that turns out to be
   the part a person actually watches: the strip eases up into place rather
   than being there already when the paint lands, and the reopened card does
   the same a little more slowly, because it is bigger and a large thing
   moving at a small thing's speed reads as hurried.

   These are entry animations, not transitions, and they are declared here
   rather than on .scard generally: a page where every card slid in would be a
   page that takes half a second to settle every time somebody clicks
   anything. Movement is for the thing that changed. */
/* DT-P80: an offer this teacher has not met before. A word and a warm edge —
   not a colour that demands to be dismissed, because there is nothing to
   dismiss: it clears itself the moment the page has been opened. */
.soffer__new { display: inline-block; margin-left: 0.6rem; padding: 0.1rem 0.5rem; border-radius: 999px;
  background: var(--stagelight); color: var(--night); font-family: var(--display); font-size: 0.66rem;
  font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; vertical-align: middle; }
.soffer--fresh { box-shadow: inset 0 0 0 2px var(--stagelight); }
.soffer--folded { animation: offerfold var(--enter) var(--curve) both; }
.soffer--open { animation: offeropen calc(var(--enter) + 80ms) var(--curve) both; }
@keyframes offerfold { from { opacity: 0; transform: translateY(-8px) scaleY(0.94); transform-origin: top; } }
@keyframes offeropen { from { opacity: 0; transform: translateY(10px); } }

.smini li { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 0.2rem 0.7rem; padding: 0.6rem 0; font-size: 0.87rem; align-items: center; }
.smini li + li { border-top: 1px solid var(--line); }
.smini li a { display: contents; color: var(--ink); text-decoration: none; }
.smini b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.smini__b { grid-column: 1; font-size: 0.79rem; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* DT-P172e: where a face-to-face class is, on the card a parent glances at.
   It sits in the second column on the first row — beside the class, not under
   it — because an address wrapped under a title reads as a footnote and this
   is the thing somebody is about to drive to. It wraps rather than truncates:
   half an address is worse than none. */
.smini__w { grid-row: 1; grid-column: 2; justify-self: end; text-align: right; max-width: 14rem;
  font-size: 0.79rem; line-height: 1.4; color: var(--ink-soft); }
.smini__n { grid-row: 1 / span 2; grid-column: 2; display: inline-flex; align-items: center; justify-content: center; min-width: 1.4rem; height: 1.4rem;
  border-radius: 999px; background: var(--magenta); color: #fff; font-size: 0.72rem; font-weight: 700; font-variant-numeric: tabular-nums; }

/* DT-P62: the teacher, with their face if they have added one. Philip asked
   for the photograph to appear in the family portal beside their name, and the
   reason is the same one that put it on the bio: a parent is judging a person,
   and a name alone gives them nothing to judge. A teacher who has NOT added
   one gets their initials rather than a grey silhouette — a picture of a
   person who is not there reads as an unfinished profile, which is exactly
   what a teacher who thought about it and declined does not deserve. */
.steach { list-style: none; margin: 0; padding: 0; }
.steach li { display: flex; align-items: center; gap: 0.85rem; padding: 0.7rem 0; }
.steach li + li { border-top: 1px solid var(--line); }
.steach__p { width: 3rem; height: 3rem; flex: 0 0 auto; border-radius: 50%; object-fit: cover; background: var(--bg-soft); }
.steach__p--none { display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--display); font-size: 0.95rem; font-weight: 600; letter-spacing: 0.02em;
  color: var(--ink-soft); border: 1px solid var(--line); }
.steach__m { display: grid; gap: 0.15rem; min-width: 0; }
.steach__m b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.steach__m .smini__b { grid-column: auto; }

.sinbox { max-height: 32rem; overflow-y: auto; }
.sinbox__i + .sinbox__i { border-top: 1px solid var(--line); }
.sinbox__i a { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 0.15rem 0.7rem; padding: 0.7rem 0.2rem; text-decoration: none; color: var(--ink); }
@media (hover: hover) { .sinbox__i:hover a { background: var(--bg-soft); } }
.sinbox__i.is-new .sinbox__n { font-weight: 700; }
.sinbox__n { font-size: 0.89rem; display: flex; align-items: center; gap: 0.4rem; }
.sinbox__c { display: inline-flex; align-items: center; justify-content: center; min-width: 1.15rem; height: 1.15rem; padding: 0 0.25rem;
  border-radius: 999px; background: var(--magenta); color: #fff; font-size: 0.68rem; font-variant-numeric: tabular-nums; }
.sinbox__b { grid-column: 1; font-size: 0.79rem; color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sinbox__d { grid-row: 1; grid-column: 2; font-size: 0.74rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; }

.snotes li { padding: 0.55rem 0; font-size: 0.87rem; line-height: 1.55; }
.snotes li + li { border-top: 1px solid var(--line); }
.snotes__d { font-family: var(--display); font-size: 0.74rem; color: var(--ink-soft); margin-right: 0.45rem; font-variant-numeric: tabular-nums; }
.sfeat li { padding: 0.4rem 0 0.4rem 1.1rem; font-size: 0.88rem; line-height: 1.5; position: relative; }
.sfeat li::before { content: '·'; position: absolute; left: 0.25rem; color: var(--stagelight); font-weight: 700; }

/* ---- chat ------------------------------------------------------------- */
.schat { list-style: none; margin: 0 0 1.1rem; padding: 0.2rem; display: flex; flex-direction: column; gap: 0.55rem; max-height: 30rem; overflow-y: auto; }
.schat__m { max-width: 82%; }
.schat__m--me { align-self: flex-end; }
.schat__b { margin: 0; padding: 0.6rem 0.85rem; border-radius: 14px; background: var(--bg-soft); font-size: 0.9rem; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
.schat__m--me .schat__b { background: var(--violet-soft); }
.schat__d { margin: 0.2rem 0 0; font-size: 0.72rem; color: var(--ink-soft); }
.schat__m--me .schat__d { text-align: right; }

/* ---- tables: they scroll, the page does not (AW-075) ------------------- */
.stable-wrap { overflow-x: auto; margin: 0 -1.25rem; padding: 0 1.25rem; }
/* DT-P52: a sentence introducing a table needs air under it, or the heading
   row reads as the second line of the paragraph. */
/* DT-P78: and the table stands clear of the sentence introducing it. Once the
   rows had room, 0.85rem left the column headings crowding the paragraph
   above them — fixing the inside of a table exposes the space around it. */
.scard__b > p + .stable-wrap { margin-top: 1.6rem; }
/* DT-P52: one rhythm inside a card body, so a screen assembled from smaller
   pieces than the studio's does not need a spacing decision per piece. */
.scard__b > p + .sempty, .scard__b > p + .smini, .scard__b > p + ol, .scard__b > p + ul { margin-top: 0.6rem; }
.scard__b > * + .srow { margin-top: 0.9rem; }
.scard__b > p + .sform, .scard__b > p + form { margin-top: 1.2rem; }
/* Anything that ends a card's prose or list, followed by a form, is a change of
   register: the reading stops and the typing starts. It gets a full step. */
.scard__b > .sempty + .sform, .scard__b > .smini + .sform, .scard__b > ul + .sform, .scard__b > .stable-wrap + .sform { margin-top: 1.4rem; }
.scard__b > .shint + .sform { margin-top: 1.2rem; }
.scard__b > .stable-wrap + .shint { margin-top: 0.85rem; }
/* DT-P57: a table keeps a readable shape and its BOX scrolls (AW-075). Left to
   shrink, a five-column table on a phone wraps every heading onto three lines
   and clips the controls in the last column, which is worse than a swipe. */
.stable { width: 100%; border-collapse: collapse; font-size: 0.86rem;  min-width: 32rem; }
/* DT-P39: the column headings and the rules between rows were both a shade
   away from invisible. A table is read by running the eye along a row, and
   that needs a line the eye can actually find. */
/* DT-P78: a row has room to breathe. 0.6rem above and below a line of text is
   the spacing of a dense data grid, and this is not one — these tables hold a
   handful of people, with a 44px button sitting in the last column. At that
   height the button filled its cell edge to edge and the row read as cramped
   even when nothing was actually touching. The heading sits further off its
   rule for the same reason: a column heading pressed against the line beneath
   it is the thing that makes a table look like a spreadsheet. */
.stable th { text-align: left; font-family: var(--display); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--ink); padding: 0 1.1rem 0.8rem 0; border-bottom: 1.5px solid #ab9f89; white-space: nowrap; }
.stable td { padding: 1rem 1.1rem 1rem 0; border-bottom: 1px solid #cfc7b6; vertical-align: middle; }
/* The last column is usually the action, and it is right-aligned by the button
   itself rather than by the cell, so it needs no right padding at all — the
   0 keeps the button flush with the card's own edge rather than floating a
   ragged distance from it. */
.stable th:last-child, .stable td:last-child { padding-right: 0; }
.stable tr:last-child td { border-bottom: none; }
/* :not(.sbtn) is the fix for the Message button reading as a black pill with no
   text on it — this rule was repainting the button's own label ink-on-violet.
   A button is never "a link in a cell" for styling purposes. */
.stable td a:not(.sbtn) { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--line-dark); }
.stable td .srow { margin-top: 0; }
.stable .snum { font-variant-numeric: tabular-nums; white-space: nowrap; }
.stable td form { display: inline; }
.sdim { color: var(--ink-soft); }

/* ---- controls -----------------------------------------------------------
   One system, and it is the answer to a specific complaint: buttons that were
   different heights, different colours, and in one case black text on a black
   pill. Three kinds and no more —

     .sbtn        the action this card is for            (violet, solid)
     .sbtn--q     a secondary or destructive action      (quiet, outlined)
     .sbtn--go    walk-into-the-room                     (warm, solid)

   plus .sbtn--s, which changes only the size for a table row. Everything else
   — height, radius, weight, transition, focus ring — is shared, so a new
   button cannot be a new design by accident.

   The colour moved off near-black deliberately (Philip: "this is an energised
   dance teacher, black won't land"). Violet is the brand's own, and it is
   also what makes .sbtn legible: a solid dark pill inheriting ink-coloured
   text was the black-on-black bug below.
------------------------------------------------------------------------- */
.sbtn { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
  /* DT-P49: the site-wide button carries margin-top: 1.2rem, for the marketing
     forms where a button follows a field. In a row of controls that margin is
     an eleven-pixel drop on whichever control happens to be a <button> rather
     than a link — which is exactly the misalignment under Your agreement. The
     studio's row owns its own spacing, so the inherited margin goes. */
  margin: 0;
  /* DT-P56: min-height too, not just height. The site-wide button rule sets
     min-height: 44px, so a <button> ignored the small size while an <a> beside
     it took it — two controls in one row at two different heights, which is
     precisely the unevenness being reported. */
  height: 44px; min-height: 44px; padding: 0 1.25rem; border: 1px solid transparent; border-radius: 999px;
  background: var(--violet); color: #fff !important; font-family: var(--display); font-size: 0.87rem; font-weight: 500;
  line-height: 1; white-space: nowrap; cursor: pointer; text-decoration: none;
  /* DT-P49: the site-wide button carries an amber glow and lifts two pixels on
     hover. Both are right on a marketing page and wrong here, and neither was
     being reset — so a studio button that happened to be a <button> looked
     different from one that happened to be a link, which is the whole thing
     this system exists to prevent. */
  box-shadow: none;
  /* DT-P76: the motion protocol's tokens, not this button's own numbers. The
     colour eases at --ease-t and the press answers at --tap, because a
     control that takes 200ms to go down under a finger feels broken while a
     colour that changes in 90ms feels like a flicker. Same curve for both. */
  transition: background var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve),
              color var(--ease-t) var(--curve), transform var(--tap) var(--curve); }
/* ── The button protocol (DT-P74) ─────────────────────────────────────────
   Philip: "I want you to document a design protocol which ensures that all
   buttons are correctly coloured in both text and substance, have motion when
   hovered over and have motion when clicked."

   Four rules, and every variant below obeys all four. The whole point is that
   a NEW variant cannot be added without answering all four questions, because
   the last two defects came from variants that answered one or two.

   1. SUBSTANCE AND TEXT ARE DECLARED TOGETHER. A rule that sets a background
      and leaves the colour to be inherited is how a white button ended up on a
      purple card with white text on it. Every variant states both.
   2. HOVER MOVES AND CHANGES. A shift of background AND a one-pixel lift. One
      alone is not enough: colour alone is invisible to somebody who is not
      looking straight at it, and movement alone is invisible on a trackpad
      that never hovers.
   3. CLICK PRESSES. translateY(1px) with no shadow, so the thing you pressed
      visibly goes down under the finger.
   4. A TONE OVERRIDES THE PALETTE, NEVER THE IDENTITY. On a dark card a
      variant keeps being what it is — a --go is still the amber one, a --q is
      still the quiet one — it simply switches to the colours that read on that
      background. Overriding .sbtn flat is what turned the amber "Open your
      room" white, and it is forbidden by rule 1 anyway.
   5. A HOVER DEEPENS. Always, everywhere, on every ground: the substance goes
      DARKER under the pointer, and far enough to be a state rather than a
      tint.

      Rule 2 said hover must change. It did not say by how much or in which
      direction, and that gap produced the white "Set it up" fading to a cream
      barely distinguishable from itself — technically a change, and it read
      as the button going pale rather than the button responding.

      My first answer to that made direction depend on the ground: deepen on a
      light card, brighten on a dark one. Philip's answer was better and is
      the one written here. A single direction is what "consistent" means, and
      consistency across a product beats a locally clever contrast argument —
      a person learns "pressing toward it makes it darker" once and it is then
      true of every control they ever meet. It also has the property my
      version did not: it is a fact a test can check, because relative
      luminance is a number and "toward more contrast" is not.

      Text follows the substance. A white button that deepens to near-black
      takes light text with it (rule 1), which is why these are stated in
      pairs.

      A hover that only alters a border while the substance stays put is the
      same defect wearing a different hat, and is why the room button now
      takes a tint instead of staying white.

   Motion respects prefers-reduced-motion at the bottom of this block. */
@media (hover: hover) { .sbtn:hover { background: #5b21b6; color: #fff !important; transform: translateY(-1px); box-shadow: none; } }
.sbtn:active { transform: translateY(1px); }
.sbtn:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.sbtn[disabled], .sbtn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
@media (hover: hover) { .sbtn[disabled]:hover, .sbtn[aria-disabled="true"]:hover { transform: none; } }

/* The quiet one is quiet at rest, not on hover: --bg-soft was so close to the
   card it read as a border change with a shrug attached. */
.sbtn--q { background: transparent; color: var(--ink) !important; border-color: var(--line); }
@media (hover: hover) { .sbtn--q:hover { background: #ebe4d5; color: var(--ink) !important; border-color: var(--ink); transform: translateY(-1px); } }

/* Rule 5: the amber DEEPENS. It used to lighten to #f2bd80, which on a white
   ground is the button receding — the same washed-out reading as the cream,
   one card along. One token, so every amber hover in the product is the same
   amber and stays that way. */
.sbtn--go { background: var(--stagelight); border-color: var(--stagelight-deep); color: var(--night) !important; }
@media (hover: hover) { .sbtn--go:hover { background: var(--hover-amber); color: var(--night) !important; transform: translateY(-1px); } }

.sbtn--s { height: 34px; min-height: 34px; padding: 0 0.9rem; font-size: 0.79rem; }
/* DT-P81: the destructive tone. Quiet at rest — a delete button that shouts
   from a card you are only reading is a button somebody eventually presses to
   make it stop shouting — and unmistakably red the moment you point at it.
   Rule 5 still holds: it deepens. */
.sbtn--x { background: transparent; color: #8a2b2b !important; border-color: #e3c4c1; }
@media (hover: hover) { .sbtn--x:hover { background: #8a2b2b; color: #fff !important; border-color: #8a2b2b; transform: translateY(-1px); } }
/* A row that sits away from the row above it, for the one action that is not
   like the others. */
.srow--end { justify-content: flex-end; margin-top: 1.2rem; padding-top: 0.9rem; border-top: 1px solid var(--line); }

/* On a dark card. Each variant keeps its identity and changes only its
   palette (rule 4), which is why these are three rules and not one. */
/* Rule 5 on a dark card is the same as everywhere else: DEEPEN. And DT-P79:
   it deepens to the SAME amber every other button deepens to.

   I reached for the product's ink here first — the pair used by a selected
   rail tab and a pressed room button — reasoning that it was already the
   product's word for "active". Philip's correction is the better one: the
   button a person is pointing at should answer in the product's ONE hover
   colour, whatever the button was at rest. Ink is what "selected" means, not
   what "you are about to press this" means, and using it for both makes the
   two states argue. Every amber hover in the product now comes from one
   token, including this one. */
.scard--stage .sbtn { background: #fff; border-color: #fff; color: var(--night) !important; }
@media (hover: hover) { .scard--stage .sbtn:hover { background: var(--hover-amber); border-color: var(--hover-amber); color: var(--night) !important; transform: translateY(-1px); } }
.scard--stage .sbtn--go { background: var(--stagelight); border-color: var(--stagelight); color: var(--night) !important; }
@media (hover: hover) { .scard--stage .sbtn--go:hover { background: var(--hover-amber); border-color: var(--hover-amber); color: var(--night) !important; } }
.scard--stage .sbtn--q { background: transparent; border-color: rgba(255,255,255,0.55); color: #fff !important; }
@media (hover: hover) { .scard--stage .sbtn--q:hover { background: rgba(8,7,13,0.42); border-color: #fff; color: #fff !important; } }
.scard--stage .sbtn:focus-visible { outline: 2px solid #ffd699; outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .sbtn, .sbtn:active { transform: none; transition: background var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve); } @media (hover: hover) { .sbtn:hover { transform: none; transition: background var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve), color var(--ease-t) var(--curve); } }
  .srail__room, .srail__room:active { transform: none; } @media (hover: hover) { .srail__room:hover { transform: none; } }
  /* An offer that arrives without easing in is still an offer. */
  .soffer--folded, .soffer--open, .phelp[open] .phelp__b { animation: none; }
}
/* A row of actions sits on one step, whether or not each is wrapped in its own
   form — which is what threw the Sign out button out of line. */
.srow { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-top: 0.9rem; }
.srow form { display: contents; }
.scard__b > .sbtn { margin-top: 0.9rem; }
/* The dark-card variants live in the button protocol above, together, so a
   change to one is made beside the others. Two places declaring the same
   thing is how the amber "Open your room" quietly turned white. */

/* DT-P53: the studio form's spacing.
   This used to be a flex column with one 0.3rem gap between everything and an
   extra 0.55rem above each label. Two problems, and Philip named both of them
   on Your studio: a label sat almost as close to the field ABOVE it as to its
   own field below, so nothing grouped; and a form with eight fields ran as one
   undifferentiated block of controls.
   A form is read as label-plus-field PAIRS. The pair holds together (0.4rem
   from label to its own field) and the pairs stand apart (1.35rem from a field
   to the next label) — a better than three-to-one ratio, which is what makes a
   form scannable without a single rule or box. The gap is 0 and every step is
   an explicit margin, because a flex gap cannot tell the two distances apart. */
/* DT-P56: ONE spacing scale for every form in this product.
   Philip: "Text, buttons and fields need to be formatted, spaced correctly and
   uniform." The steps are named here once and nothing overrides them:

     0.4rem   inside a pair — a label to its own field, a hint to the field it
              describes. Close enough that they read as one thing.
     1.35rem  between pairs. Three times the inside step, which is what makes a
              form scannable without a single rule or box.
     1.7rem   before the submit. The questions stop; the action begins.

   A hint may sit either between a label and its field (explaining the field
   below) or after a field (explaining the field above). Both happen, so both
   have a rule — a form where one of them collapses to nothing is what Philip
   was looking at on Add a child. */
.sform { display: flex; flex-direction: column; gap: 0; }
.sform label { font-family: var(--display); font-size: 0.79rem; font-weight: 500; color: var(--ink-soft);
  margin: 1.35rem 0 0.4rem; }
.sform > label:first-child, .sform__pair label { margin-top: 0; }
/* DT-P73: :not() on the tick and the radio, and it is AW-107 in the mirror.
   That rule says an override of a base field rule must repeat the base rule's
   own :not() selectors. This is the same fault from the other side: a SCOPED
   field rule that forgot them, so width: 100% and min-height: 44px reached the
   checkbox inside a .pick, blew it up into a 44px-tall box and squeezed its
   sentence into a one-word-per-line column outside the border. It shipped
   because the notice board was never looked at after it was built — which is
   the whole of AW-107's other half. */
.sform input:not([type="checkbox"]):not([type="radio"]), .sform select, .sform textarea { width: 100%; min-height: 44px; padding: 0.6rem 0.8rem;
  border: 1px solid var(--border-strong); border-radius: 10px; background: var(--bg); font: inherit; font-size: 0.9rem; color: var(--ink); }
/* A select left native is a different height and a different shape from the
   inputs beside it, which is exactly the unevenness being complained about.
   The chevron is drawn with two gradients rather than an image, because the
   policy is default-src 'none' and a background image would be refused
   silently (AW-056) — the same trap as the inline style attribute. */
.sform select {
  -webkit-appearance: none; appearance: none; padding-right: 2.2rem; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%),
                    linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
  background-position: right 1.15rem center, right 0.8rem center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}
.sform textarea { min-height: 5.5rem; resize: vertical; line-height: 1.55; }
.sform input:focus, .sform select:focus, .sform textarea:focus { outline: 2px solid var(--violet); outline-offset: 1px; border-color: transparent; }
/* A hint BEFORE its field: part of the pair above it, and the field follows at
   the inside step. */
.sform > label + .shint { margin: 0 0 0.4rem; }
.sform > .shint + input, .sform > .shint + select, .sform > .shint + textarea, .sform > .shint + .sform__pair { margin-top: 0; }
/* A hint AFTER a field explains the field above, so it stays with it and the
   next pair still begins a clear step later. */
.sform > input + .shint, .sform > select + .shint, .sform > textarea + .shint, .sform > .sform__pair + .shint { margin: 0.45rem 0 0; }
.sform > .shint + label { margin-top: 1.15rem; }
/* DT-P223: a time row. It was a bare <fieldset> with no rule of its own, so it
   carried the browser's default border and inset legend in the middle of a
   form where nothing else has one. Now it is a quiet block, and the repeat
   choice is two visible radios rather than a drop-down opening on an answer
   the teacher never gave. The radios are NOT pre-checked: a row means nothing
   until she says which kind it is, and a default that answers for her is how
   the date field stayed off her screen in the first place. */
.sform__rowset { border: 0; margin: 1.35rem 0 0; padding: 0.9rem 0 0; border-top: 1px solid var(--border); }
.sform__rowset:first-of-type { margin-top: 1rem; }
.sform__leg { padding: 0; font-family: var(--display); font-size: 0.79rem; font-weight: 600; color: var(--ink); }
.srepeat { display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; margin: 0.55rem 0 0.2rem; }
.srepeat__opt { display: flex; align-items: center; gap: 0.5rem; margin: 0; cursor: pointer;
  font-family: var(--display); font-size: 0.86rem; font-weight: 500; color: var(--ink); }
.srepeat__opt input[type="radio"] { width: 1.05rem; height: 1.05rem; min-height: 0; margin: 0; accent-color: var(--violet); cursor: pointer; }
/* A tick and its sentence inside a studio form: a control on its own line,
   at reading size rather than at the small muted weight .sform label gives a
   field caption. It is a thing you agree to, not a label for a box below it. */
.sform > .pick { width: 100%; max-width: 100%; margin: 1.35rem 0 0; font-size: 0.92rem; font-weight: 400; color: var(--ink); line-height: 1.55; }
.sform > .pick + .shint { margin: 0.5rem 0 0; }
.sform > .pick + label { margin-top: 1.35rem; }
.sform button { margin-top: 1.7rem; align-self: flex-start; }
/* DT-P71: whatever comes AFTER a form inside a card needs the same air the
   form's own last field gets. .phelp carries margin: 0 0 1.1rem — no top
   margin at all, because it was written to sit at the top of a page — so on
   the Money screen the Save button and the "How to get a payment link" panel
   were touching. One rule for anything that follows a form rather than a fix
   for that one panel, because the next block somebody puts after a form will
   land in exactly the same place (AW-104). */
.scard__b > .sform + * { margin-top: 1.6rem; }
.sform__pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem 0.9rem; margin-top: 1.35rem; align-items: start; }
/* DT-P226: a field inside a pair that is not half of a pair. The starting date
   for a weekly time explains itself in a sentence, and a sentence squeezed into
   one column of a two-column grid is the letterbox problem again. */
.sform__pair > .sform__wide { grid-column: 1 / -1; }
.sform > .sform__pair:first-child { margin-top: 0; }
.sform__pair > div { display: flex; flex-direction: column; }
.sform--row button { margin-top: 0.9rem; }
@media (max-width: 34rem) { .sform__pair { grid-template-columns: 1fr; } }

/* DT-P56: a card's action row. Every card ends its actions in ONE row on one
   step, whatever mix of links, buttons and forms it is made of — two rows of
   one button each, at two different distances, is what a card looks like when
   nobody decided. A form INSIDE a row contributes its button to the row
   (display: contents) and must not add the form's own submit margin. */
.srow > .sform button, .srow form button { margin-top: 0; }
/* Figures, then the sentence about them. */
.scard__b > .stats + .shint, .scard__b > .stats + p { margin-top: 0.9rem; }
.scard__b > .stats + .sform, .scard__b > .stats + form { margin-top: 1.2rem; }
.scheck { display: flex; align-items: center; gap: 0.6rem; margin-top: 1.35rem; font-size: 0.87rem; color: var(--ink); }
.scheck input { width: auto; min-height: 0; }
.svisually { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

.shint { margin: 0.7rem 0 0; font-size: 0.79rem; line-height: 1.55; color: var(--ink-soft); }
.shours { list-style: none; margin: 0 0 1rem; padding: 0; }
.shours li { display: grid; grid-template-columns: 5.5rem minmax(0,1fr); gap: 0.5rem; padding: 0.45rem 0; align-items: start; }
.shours li + li { border-top: 1px solid var(--line); }
.shours__d { font-family: var(--display); font-size: 0.78rem; font-weight: 600; color: var(--ink-soft); padding-top: 0.15rem; }
.shours__w { display: flex; flex-wrap: wrap; gap: 0.35rem; }
/* DT-P136: white-space and max-width. The pill was written for "17:00–20:00"
   and DT-P134 started putting "18:00–19:00 · School run" in it, which broke
   across two lines inside a 999px radius — so a closure rendered as an oval
   the height of two lines, and "all day" rendered as a circle. The label now
   stays on one line and the pill shrinks to the column instead of growing
   down. Found by photographing the screen rather than by reading the markup,
   which is the only way this class of fault is ever found. */
.shours__i { display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.2rem 0.3rem 0.2rem 0.55rem; border-radius: 999px;
  white-space: nowrap; max-width: 100%; min-width: 0;
  font-size: 0.76rem; font-variant-numeric: tabular-nums; background: rgba(109,40,217,0.1); color: var(--violet); }
.shours__i--blocked { background: var(--bg-soft); color: var(--ink-soft); }
.shours__i form { display: contents; }
.shours__x { border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 0.95rem; line-height: 1; padding: 0 0.2rem; opacity: 0.8; }
@media (hover: hover) { .shours__x:hover { opacity: 1; } }
/* DT-P136: the remove × carries the product-wide 44px minimum tap target, and
   inside a 12px pill that made the pill 72px tall — a closure rendered as a
   circle. The minimum exists for fingers, so it is kept for fingers and
   dropped for a mouse. On a phone the card is full width, so a 44px-tall pill
   is a pill rather than a circle anyway. Measured in a browser, not reasoned
   about: the first two fixes I tried were aimed at text wrapping, which was
   never the cause. */
@media (pointer: fine) { .shours__x { min-height: 0; } }
.swk__b--hours rect { fill: rgba(109,40,217,0.07); }
.swk__b--hours { pointer-events: none; }
.swk__k--hours::before { background: rgba(109,40,217,0.14); }
.scell__s { display: block; font-size: 0.75rem; color: var(--ink-soft); margin-top: 0.12rem; }
/* DT-P227: a sentence rather than a caption, and it needs room. Philip: "the
   distance between that statement and the button is very close. We need to
   create more space so that we've got an aesthetically correct structure." */
.scell__s--note { margin-top: 0.5rem; line-height: 1.5; max-width: 34rem; }
.stable tr.is-off { opacity: 0.75; }   /* DT-P40: archived is still readable */
.sdone { margin: 0; padding: 0.75rem 0.95rem; border-radius: 10px; background: rgba(109,40,217,0.08);
  border-left: 3px solid var(--violet); font-size: 0.88rem; line-height: 1.55; color: var(--ink); }
/* DT-P144: it had no bottom margin, so on the photograph card the confirmation
   sat hard against the picture it was confirming — the two read as one crowded
   object rather than as a notice about the thing below it. Only when something
   FOLLOWS it: in several places this notice is alone in its own card, and a
   trailing margin there would be a gap under nothing. */
.sdone:not(:last-child) { margin-bottom: 1.1rem; }
/* DT-P185: the same notice, in the colour it is when it is a warning rather
   than a confirmation. It is the warm accent the rest of the product already
   uses for "look at this", not a new red: nothing here has gone wrong, she has
   simply not finished telling us when she teaches. */
.sdone--warn { background: rgba(233,168,98,0.14); border-left-color: var(--action); }
/* DT-P175d: the qualifications card. Each row is a fact, its standing in
   words, and the form that changes it — so the row has to breathe and its
   standing note has to WRAP. .smini__b truncates to one line with an ellipsis,
   which is right for "3 signatures" and wrong for "the certificate is in a
   different name": the reason a claim is not on her page is the one line on
   this screen that must never be cut off. */
.squals__l li { display: block; padding: 1rem 0; }
.squals__l .smini__b { display: block; grid-column: auto; white-space: normal; overflow: visible; margin-top: 0.15rem; max-width: 60ch; }
.squals__l .spill { margin-top: 0.4rem; }
.squals__f { margin-top: 0.7rem; }
.squals__sub { margin: 1.4rem 0 0; font-size: 0.95rem; }
/* DT-P177: the experience switch, set apart from the list above it because it
   governs a different line on her page and a control that looks like another
   row is a control somebody presses by accident. */
/* DT-P180: the setup chip. A quiet strip under the rail, not a banner: it is a
   signpost for somebody mid-task, and it removes itself when the last step is
   done. Full width and tappable on a phone, because the person most likely to
   be half way through setting up is holding one. */
.ssetup { display: flex; align-items: center; gap: 0.6rem; margin: 0.9rem auto 0.9rem; max-width: var(--swide, 78rem);
  padding: 0.6rem 0.9rem; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-soft);
  color: var(--ink); text-decoration: none; font-size: 0.86rem; width: calc(100% - 2rem); }
/* DT-P180: tick lists for ages and languages. A grid that reflows rather than a
   native multi-select, which on a phone hides every choice behind a tap and is
   the reason these two fields were free text in the first place. */
.spick { margin: 1.1rem 0 0; padding: 0; border: 0; }
.spick legend { padding: 0; font-size: 0.86rem; font-weight: 600; }
.spick__g { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.35rem 0.9rem; margin: 0.5rem 0 0; }
.spick__e { display: block; margin: 0.75rem 0 0.25rem; font-size: 0.82rem; color: var(--ink-soft); }

/* The save button and the proof it worked, on one line. */
.ssaved { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin-top: 1.1rem; }
.ssaved__w { margin: 0 !important; }

/* DT-P180: the chosen photograph, before it is sent. Same 4:5 frame the bio
   uses, so what she sees here is the shape families get. */
.sprev { margin: 0.85rem 0 0; display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 0.85rem; align-items: start; }
.sprev__i { width: 96px; aspect-ratio: 4 / 5; object-fit: cover; border-radius: 12px; border: 1px solid var(--line); background: var(--bg-soft); }
.sprev__i:not([src]) { display: none; }
.sprev__s { margin: 0 !important; }

.ssetup__n { flex: none; padding: 0.1rem 0.55rem; border-radius: 999px; background: var(--violet); color: #fff; font-size: 0.78rem; }
.ssetup span { color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (hover: hover) { .ssetup:hover { border-color: var(--violet); } }

.squals__years { margin: 1.2rem 0 0; padding: 0.95rem 1.1rem; border: 1px solid var(--line); border-radius: 12px; background: var(--bg-soft); }
.squals__years p { margin: 0 0 0.6rem; }
.sempty { margin: 0; font-size: 0.87rem; line-height: 1.55; color: var(--ink-soft); }
.sback { margin: 1rem 0 0; font-size: 0.84rem; }
.sback a { color: var(--violet); text-decoration: none; }
.sroom__p { margin: 0 0 0.9rem; font-size: 0.9rem; line-height: 1.6; }

/* ---- the dancer's record ---------------------------------------------- */
.srec { display: flex; align-items: flex-end; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap; }
.srec__k { margin: 0; font-family: var(--display); font-size: 0.72rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-soft); }
.srec__n { margin: 0.2rem 0 0; font-family: var(--serif); font-size: clamp(1.4rem, 2.6vw, 1.9rem); font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
.srec__m { margin: 0.3rem 0 0; font-size: 0.84rem; color: var(--ink-soft); }
.srec__a { display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; }
.srec__a form { display: contents; }
.svisually { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* DT-P65: a labelled fact list inside a card — an application, read rather
   than clicked through. Label above value in a wrapping grid, so a long email
   address takes the room it needs instead of pushing a two-column table wide
   enough to scroll. */
.sfacts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.85rem 1.3rem; margin: 0 0 1rem; }
.sfacts dt { font-family: var(--display); font-size: 0.71rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-soft); }
.sfacts dd { margin: 0.2rem 0 0; font-size: 0.9rem; line-height: 1.5; overflow-wrap: anywhere; }

/* DT-P68: the shop, on both sides.
   A thing for sale is a picture and some words, so the row is picture-then-
   words and the picture is a fixed square — a catalogue whose images are all
   different heights reads as a jumble however good each photograph is. An item
   with no picture gets an empty square rather than a collapsed row, because
   the alignment of everything below it is what makes the list scannable. */
.sshop, .fshop { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.fshop { grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); }
.sshop__i, .fshop__i { display: flex; gap: 1rem; align-items: flex-start; padding: 0.9rem 0; margin: 0; }
.sshop__i + .sshop__i { border-top: 1px solid var(--line); }
/* Cards of different text lengths still line their controls up: the card
   stretches to the row height, the words take what they need, and the order
   form is pushed to the bottom by margin-top: auto. Three "Order it" buttons
   at three different heights is the tell of a grid nobody looked at. */
.fshop__i { padding: 1rem; border: 1px solid var(--line); border-radius: 14px; background: #fff; align-items: stretch; }
.fshop__m { display: flex; flex-direction: column; }
.fshop__f { margin-top: auto; }
.sshop__i--off { opacity: 0.55; }
.sshop__p, .fshop__p { width: 5.5rem; height: 5.5rem; flex: 0 0 auto; border-radius: 12px; object-fit: cover; background: var(--bg-soft); }
.fshop__p { width: 6.5rem; height: 6.5rem; }
.sshop__p--none, .fshop__p--none { display: block; border: 1px dashed var(--line); }
.sshop__m, .fshop__m { min-width: 0; flex: 1 1 auto; }
.sshop__t, .fshop__t { margin: 0; font-size: 0.95rem; }
.fshop__pr { margin: 0.15rem 0 0; font-family: var(--display); font-size: 1.05rem; font-weight: 600; }
.sshop__d, .fshop__d { margin: 0.35rem 0 0; font-size: 0.86rem; line-height: 1.55; color: var(--ink-soft); }
.sshop__f { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin: 0; }
.sshop__f input[type="file"] { max-width: 14rem; font-size: 0.78rem; padding: 0.4rem 0.55rem; }
@media (max-width: 40rem) {
  /* DT-P73: on a phone the picture goes above the words rather than beside
     them. Side by side at 390px leaves the text about 200px wide, which turns
     "Collect at Tuesday's class" into three lines and a description into a
     column. The picture also gets to be bigger, which is the point of it. */
  .sshop__i, .fshop__i { flex-direction: column; }
  .sshop__p, .fshop__p { width: 100%; height: auto; aspect-ratio: 4 / 3; }
  .fshop { grid-template-columns: 1fr; }
  .sshop__f input[type="file"] { max-width: 100%; }
}
/* The order form inside a card: label, control, label, control, tight. */
.fshop__f { padding-top: 0.8rem; display: grid; gap: 0.25rem; }
.fshop__f label { margin: 0.5rem 0 0; font-size: 0.8rem; font-weight: 600; }
.fshop__f input, .fshop__f select { width: 100%; max-width: 100%; padding: 0.5rem 0.7rem; font-size: 0.88rem; }
.fshop__f button { margin-top: 0.9rem; }

/* DT-P69: the notice board. A notice is read in one pass — when, what, where,
   then the detail — so the date leads and is the smallest thing on the card.
   Ordering it the other way makes a board of identical bold titles. */
.sevents { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.9rem; }
.sevent { margin: 0; padding: 0.95rem 1.1rem; border: 1px solid var(--line); border-radius: 14px; background: #fff; }
.sevent--past { opacity: 0.6; }
.sevent__w { margin: 0; font-family: var(--display); font-size: 0.76rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-soft); }
.sevent__t { margin: 0.25rem 0 0; font-size: 1.02rem; }
.sevent__p { margin: 0.2rem 0 0; font-size: 0.88rem; color: var(--ink-soft); }
.sevent__b { margin: 0.55rem 0 0; font-size: 0.9rem; line-height: 1.6; max-width: 62ch; white-space: pre-wrap; }

.sjoin { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 1.2rem; }
.sjoin__k { margin: 0; font-family: var(--display); font-size: 0.74rem; letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-soft); }
.sjoin__v { margin: 0.3rem 0 0; font-size: 1rem; }
.sjoin__v code { background: var(--bg-soft); padding: 0.2rem 0.5rem; border-radius: 6px; font-size: 0.92rem; }

.sprice { margin: 1rem 0 0.3rem; font-size: 1.05rem; }
.swas { text-decoration: line-through; opacity: 0.8; }   /* DT-P40: the old price is the point of showing it */
.scode { margin: 0.9rem 0 0.8rem; font-size: 0.95rem; }
.scode code { background: rgba(233,168,98,0.16); color: var(--stagelight); padding: 0.3rem 0.6rem; border-radius: 8px; letter-spacing: 0.06em; }

/* ---- diagrams --------------------------------------------------------- */
.schart { width: 100%; }
.schart svg { width: 100%; height: auto; display: block; }
.sbar { fill: var(--violet); }
/* ---- charts: the drawing is SVG, every word is HTML (DT-P51) -----------
   The rule this section exists to encode: a chart label is an HTML element
   beside the drawing, never a <text> inside it. Labels set as SVG text
   rendered as asked in Chrome and came out pale enough to be unreadable in
   Safari, because SVG text goes through a painter that gets neither the stem
   darkening nor the gamma-corrected antialiasing HTML text gets. No amount of
   darkening the fill fixes a problem that is not about colour.

   Everything below is therefore ordinary text in the page's own type scale,
   laid over the drawing with CSS grid so the two agree without either knowing
   the other's units.
------------------------------------------------------------------------- */
.schart { width: 100%; }
.schart__plot { display: block; width: 100%; height: 9.5rem; }
.sax { display: grid; font-family: var(--display); font-size: 0.78rem; font-weight: 600; color: var(--ink); }
.sax--top { justify-content: end; margin-bottom: 0.2rem; font-variant-numeric: tabular-nums; }
.sax--x { grid-auto-flow: column; grid-auto-columns: 1fr; margin-top: 0.35rem; text-align: center; }

/* ---- the week ---------------------------------------------------------- */
.swk { --swk-rows: 20rem; }
/* DT-P199: the video button before its time — visibly there, visibly not yet. */
.sbtn.is-off { opacity: 0.5; pointer-events: none; }
.sanchor { scroll-margin-top: 7rem; }
.scard__b > .stats + .stable-wrap { margin-top: 1.3rem; }
.srow--under { margin-top: 0.55rem; }
/* DT-P227: and further still when a line of explanation sits above it. */
.scell__s--note + .srow--under { margin-top: 0.85rem; }
/* DT-P199: receipts folded away, opened when wanted. */
.sfold > summary { cursor: pointer; font-family: var(--display); font-weight: 600; min-height: 44px; display: flex; align-items: center; }
.sfold[open] > summary { margin-bottom: 0.6rem; }
/* DT-P203: buttons in one row line up. A form gives its submit button room
   above it; beside a link in the same row that pushed the button lower than
   the link. Lining up their bottoms keeps the room and the alignment. */
.sform .srow { align-items: flex-end; }

/* DT-P203: a table in a narrow card fits the card rather than scrolling. */
.scard--4 .stable { min-width: 0; }

/* DT-P203: tables on a phone. Each row becomes a block: the first cell as its
   heading, every other value on its own line with its column name beside it,
   and the buttons underneath. Nothing scrolls sideways, and no button is left
   off the edge of the screen. */
@media (max-width: 34rem) {
  .stable-wrap { overflow: visible; margin: 0; padding: 0; }
  .stable { min-width: 0; }
  .stable, .stable tbody, .stable tr, .stable td { display: block; width: 100%; }
  .stable thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .stable tr { padding: 0.85rem 0; border-bottom: 1px solid #cfc7b6; }
  .stable tr:first-child { padding-top: 0.2rem; }
  .stable td { border: 0; padding: 0.2rem 0; text-align: left; }
  .stable td:empty { display: none; }
  .stable td[data-label] { position: relative; padding-left: 7.5rem; min-height: 1.6rem; }
  .stable td[data-label]::before { content: attr(data-label); position: absolute; left: 0; top: 0.42rem; width: 6.8rem;
    font-family: var(--display); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); }
  .stable td:first-child, .stable td[data-label]:first-child { display: block; padding: 0 0 0.35rem; min-height: 0; }
  .stable td:first-child::before { content: none; }
  .stable td .srow { margin-top: 0.35rem; }
  .stable .sopt { white-space: normal; }
}

/* DT-P202: one class, its prices one under another. */
.sopt { display: block; white-space: nowrap; }
.sopt + .sopt { margin-top: 0.25rem; }
/* DT-P201: the week calendar can be pressed, and says what a block is. */
.swk__a { cursor: pointer; }
.swk__a:focus-visible rect { stroke: var(--ink); stroke-width: 2; }
@media (hover: hover) { .swk__a:hover rect { filter: brightness(1.12); } }
.swkp { width: min(26rem, calc(100vw - 2rem)); max-height: calc(100vh - 2rem); overflow: auto; margin: auto; padding: 1.2rem 1.25rem 1.25rem;
  border: 1px solid var(--line); border-radius: 16px; background: #fff; color: var(--ink); box-shadow: 0 18px 50px rgba(8,7,13,0.28); }
.swkp:target:not(:popover-open) { display: block; position: fixed; inset: 0; z-index: 50; }
.swkp::backdrop { background: rgba(8,7,13,0.38); }
.swkp__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.swkp__h { margin: 0; font-family: var(--display); font-size: 1.05rem; font-weight: 600; }
.swkp__s { margin: 0.2rem 0 0; font-size: 0.84rem; color: var(--ink-soft); }
.swkp__f { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 1rem; margin: 1rem 0 0; font-size: 0.9rem; }
.swkp__f dt { font-family: var(--display); font-size: 0.74rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); align-self: center; }
.swkp__f dd { margin: 0; }
.swkp__l { margin: 1rem 0 0.3rem; font-family: var(--display); font-size: 0.74rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); }
.swkp__p { margin: 0; padding: 0 0 0 1.1rem; font-size: 0.92rem; line-height: 1.6; }
.swkp__none { margin: 0; font-size: 0.9rem; color: var(--ink-soft); }
.swkp__a { margin-top: 1.1rem; flex-wrap: wrap; }
/* DT-P200: the guide on Today, minimised to one line and opened again. */
.sguide__tg { margin: 0; }
.sguide__c--fold { margin: 0; }
.scard--folded .scard__h { border-bottom: 0; padding-bottom: 0.3rem; }
.scard--folded .scard__b { padding-top: 0.2rem; }
/* DT-P197: a heading inside a long form, so the sell form reads in parts. */
.sform__h { margin: 1.4rem 0 0.2rem; font-family: var(--display); font-size: 0.95rem; }
/* DT-P225: a sub-heading inside a section. The pack of sessions is an optional
   thing within "What it costs", not a second section, and it needed a line of
   explanation ABOVE its fields rather than a summary underneath them -- Philip:
   "these warnings have to be given up front so that the fields are
   pre-positioned and it avoids confusion." */
.sform__h4 { margin: 1.5rem 0 0.2rem; font-family: var(--display); font-size: 0.86rem; font-weight: 600; color: var(--ink); }
.sform > .sform__h4 + .shint { margin: 0 0 0.5rem; }
/* DT-P192: moving the week. The steps sit on one line with the dates between
   them; the date box wraps under them on a phone. */
.swkn { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem 1.25rem; margin: 0 0 1rem; }
.swkn__steps { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; }
.swkn__range { font-family: var(--display); font-size: 1rem; min-width: 8.5rem; text-align: center; scroll-margin-top: 6rem; }
.swkn__jump { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0; }
.swkn__jump label { margin: 0; font-size: 0.85rem; color: var(--ink-soft); }
.swkn__jump input[type="date"] { width: auto; max-width: 11.5rem; min-height: 44px; }
.swk__head, .swk__body { display: grid; grid-template-columns: 2.6rem minmax(0, 1fr); }
.swk__days { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
.swk__d { text-align: center; font-family: var(--display); font-size: 0.8rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink); padding-bottom: 0.45rem; }
.swk__d.is-now { color: var(--violet); }
.swk__hours { display: grid; grid-auto-rows: 1fr; height: var(--swk-rows); }
/* The hour belongs to the line at the TOP of its row, so it is lifted half a
   line to sit on it rather than under it. */
.swk__hr { position: relative; }
.swk__hr b { position: absolute; top: -0.62em; right: 0.5rem; font-family: var(--display);
  font-size: 0.78rem; font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.swk__plot { display: block; width: 100%; height: var(--swk-rows); }
.swk__g { stroke: #cfc7b6; stroke-width: 1; }
.swk__g--m { stroke: #ab9f89; }
.swk__c { stroke: #cfc7b6; stroke-width: 1; }
.swk__b rect { rx: 3; }
.swk__b--full rect { fill: var(--violet); }
.swk__b--open rect { fill: rgba(109,40,217,0.14); stroke: var(--violet); stroke-width: 1; }
.swk__b--admin rect { fill: rgba(23,20,31,0.08); stroke: var(--ink-soft); stroke-width: 1; stroke-dasharray: 3 2; }
/* DT-P144: a meeting is committed time that nobody books a place at, so it is
   neither a class nor an absence and gets the third brand colour rather than a
   green or a blue that appears nowhere else in this product. Amber against the
   violet reads as "also yours, different thing". */
.swk__b--meeting rect { fill: rgba(229,105,16,0.16); stroke: var(--amber); stroke-width: 1; }
.swk__b--hours rect { fill: rgba(109,40,217,0.07); }
.swk__b--hours { pointer-events: none; }
.swk__key { display: flex; flex-wrap: wrap; gap: 0.9rem; margin-top: 0.7rem; }
.swk__k { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; color: var(--ink); }
.swk__k::before { content: ''; width: 0.7rem; height: 0.7rem; border-radius: 2px; }
.swk__k--full::before { background: var(--violet); }
.swk__k--open::before { background: rgba(109,40,217,0.14); box-shadow: inset 0 0 0 1px var(--violet); }
.swk__k--hours::before { background: rgba(109,40,217,0.14); }
.swk__k--admin::before { background: rgba(23,20,31,0.08); box-shadow: inset 0 0 0 1px var(--ink-soft); }
.swk__k--meeting::before { background: rgba(229,105,16,0.16); box-shadow: inset 0 0 0 1px var(--amber); }

/* ---- your people: rows, not a picture of rows -------------------------- */
.sfun { list-style: none; margin: 0; padding: 0; }
.sfun__r { display: grid; grid-template-columns: minmax(6.5rem, auto) minmax(0, 1fr) 2.5rem;
  align-items: center; gap: 0.75rem; padding: 0.4rem 0; }
.sfun__r + .sfun__r { border-top: 1px solid #ded7ca; }
.sfun__l { font-size: 0.88rem; color: var(--ink); }
.sfun__t { display: block; height: 1.15rem; }
.sfun__b { display: block; height: 100%; border-radius: 4px; }
.sfun__b--0 { background: var(--violet); }
.sfun__b--1 { background: #8b4fd6; }
.sfun__b--2 { background: var(--magenta); }
.sfun__b--3 { background: var(--amber); }
.sfun__v { font-family: var(--display); font-size: 0.88rem; font-weight: 600; color: var(--ink);
  text-align: right; font-variant-numeric: tabular-nums; }
/* A width is a class from a fixed ladder, because a style attribute is refused
   outright under this product's policy (AW-056). */
.sfun__w-1 { width: 5%; }
.sfun__w-2 { width: 10%; }
.sfun__w-3 { width: 15%; }
.sfun__w-4 { width: 20%; }
.sfun__w-5 { width: 25%; }
.sfun__w-6 { width: 30%; }
.sfun__w-7 { width: 35%; }
.sfun__w-8 { width: 40%; }
.sfun__w-9 { width: 45%; }
.sfun__w-10 { width: 50%; }
.sfun__w-11 { width: 55%; }
.sfun__w-12 { width: 60%; }
.sfun__w-13 { width: 65%; }
.sfun__w-14 { width: 70%; }
.sfun__w-15 { width: 75%; }
.sfun__w-16 { width: 80%; }
.sfun__w-17 { width: 85%; }
.sfun__w-18 { width: 90%; }
.sfun__w-19 { width: 95%; }
.sfun__w-20 { width: 100%; }
.sblock { fill: var(--violet); opacity: 0.85; }
.sgrid { stroke: var(--line); stroke-width: 1; }
/* DT-P51: every rule that used to colour an SVG <text> is gone. Labels are
   HTML now (see .sax, .swk__d, .sfun__l above) because Safari paints SVG text
   through a painter that gets neither stem darkening nor gamma-corrected
   antialiasing, so small display-face words look washed out no matter what
   fill they carry. Do not put a label inside a drawing again. */
.sring { display: flex; flex-direction: column; align-items: center; text-align: center; }
.sring__d { position: relative; display: flex; align-items: center; justify-content: center; }
.sring svg { width: 8.5rem; height: 8.5rem; display: block; }
.sring__t { stroke: var(--bg-soft); }
.sring__v { stroke: var(--violet); }
.sring__n {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: var(--display); font-size: 1.35rem; font-weight: 600; color: var(--ink);
}
.sring__c { margin: 0.5rem 0 0; font-family: var(--display); font-size: 0.85rem; font-weight: 500; }
.sring__s { margin: 0.15rem 0 0; font-size: 0.78rem; color: var(--ink-soft); }

/* D-22: the recordings screens, and the label saying which portal this is.
   A list of lessons drawn like the inbox: one row each, the title and the day
   on the first line, what is in it under that, the state at the right. */
.srail__portal { flex: 0 0 auto; display: inline-flex; align-items: center; height: 1.35rem; padding: 0 0.6rem; border-radius: 999px;
  background: var(--ink); color: var(--light); font-family: var(--display); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; }
@media (max-width: 44rem) {
  .srail__in { flex-wrap: wrap; }
  .srail__portal { order: -1; }
  .srail__tabs { flex-basis: 100%; }
}
.scard__b > .sback { margin: 0 0 0.9rem; }
.srecs { list-style: none; margin: 1rem 0 0; padding: 0; }
.srecs__i + .srecs__i { border-top: 1px solid var(--line); }
.srecs__i a { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.2rem 0.9rem; padding: 0.85rem 0.2rem; text-decoration: none; color: var(--ink); }
@media (hover: hover) { .srecs__i:hover a { background: var(--bg-soft); } }
.srecs__t { grid-column: 1; font-family: var(--display); font-size: 0.97rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srecs__d { grid-column: 2; grid-row: 1; font-size: 0.78rem; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
.srecs__m { grid-column: 1; font-size: 0.82rem; color: var(--ink-soft); line-height: 1.5; }
.srecs__s { grid-column: 2; grid-row: 2; justify-self: end; align-self: start; }
.splayer { display: block; width: 100%; max-height: 70vh; margin: 1rem 0 0; border-radius: 12px; background: #000; }
.sremark { display: block; width: 100%; margin: 0.6rem 0 0; }
.tlink { display: inline-flex; align-items: center; min-height: 28px; padding: 0 0.55rem; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-soft);
  font-family: var(--display); font-size: 0.78rem; font-variant-numeric: tabular-nums; color: var(--violet); cursor: pointer; }
.snotes form { margin-top: 0.4rem; }
.snotes li > p { margin: 0; }
.scard__b > .sform + .snotes, .scard__b > .sform + .sempty { margin-top: 1.4rem; }
.scard__b > .sdone + video, .scard__b > .shint + video { margin-top: 1rem; }
.scard__b > .sfeat + .sdone, .scard__b > .sfeat + .sform, .scard__b > .sfeat + .srow { margin-top: 1rem; }
.scard__b > .srow--end { margin-top: 1.6rem; }
.scard__b > .sdone + .sform, .scard__b > .sdone + .srow { margin-top: 1rem; }

/* D-24. Two faults Philip found at a narrow window.
   1. The rail stuck at 3.6rem while the masthead above it is 4.25rem (plus the
      phone's safe area in the app), so the top of the rail, and the first row
      of its tabs, slid under the masthead. Above the breakpoint the masthead is
      exactly --masthead-h tall, so the rail now sticks exactly below it. Below
      it the masthead's height is whatever its contents make it, and a rail that
      wraps onto two or three rows is too tall to pin anyway, so it scrolls with
      the page and nothing can hide behind anything.
   2. The setup bar had no space below it, so it touched the help bar. */
.srail { top: calc(var(--masthead-h) + env(safe-area-inset-top, 0px)); }
@media (max-width: 47.99rem) {
  .srail { position: static; }
}

/* D-25: the class page. The top card was a stack of loose paragraphs with the
   status, the title text and the action floating apart. It is now a header row
   (status and the line families see) over one action row: the explanation on
   the left, the button on the right, on a single baseline. */
.scourse { display: flex; flex-direction: column; gap: 1rem; }
.scourse__top { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1rem; }
.scourse__blurb { margin: 0; font-size: 1.02rem; line-height: 1.5; color: var(--ink); }
.scourse__act { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.8rem 1.5rem;
  padding-top: 1rem; border-top: 1px solid var(--line); margin: 0; }
.scourse__act .shint { margin: 0; flex: 1 1 22rem; max-width: 62ch; }
.scourse__act .sbtn, .scourse__act button { margin: 0; flex: 0 0 auto; }
/* D-25: a form that is the last thing in a card fills it, so its action row can
   sit on the floor of the card. DT-P73 did this for a .srow that is a direct
   child of the card body; every form nests its row one level deeper, which is
   why Add the time, Save the price and Save sat at three different heights. */
.scard__b > .sform:last-child { flex: 1 1 auto; }
.sform > .srow:last-child { margin-top: auto; padding-top: 1.2rem; }
.sform > .srow:last-child .sbtn, .sform > .srow:last-child button { margin-top: 0; }
.scard__b > .sform > .sform__pair:first-child, .scard__b > .sform > input[type="hidden"] + .sform__pair { margin-top: 0; }
.scard__b > .shours:first-child, .scard__b > .stable-wrap:first-child { margin-top: 0; }
.sguide__m { margin: 0.45rem 0 0; font-size: 0.86rem; line-height: 1.5; max-width: 62ch; }
.sguide__i.is-next .sguide__m { font-weight: 500; }
@media (max-width: 47.99rem) { .ssetup { align-items: flex-start; border-radius: 18px; } .ssetup span { white-space: normal; overflow: visible; } }

/* D-29: the family's "Your classes" rows. One row per class: what it is, the
   three numbers together, and the action on the row's right. A table cell was
   holding the name, the notice and the button, which is what put the button
   under a sentence with the numbers stranded at the far edge. */
.fcls { list-style: none; margin: 0.9rem 0 0; padding: 0; display: grid; gap: 0.75rem; }
.fcls__i { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 0.9rem 2rem;
  padding: 1rem 1.15rem; border: 1px solid var(--line); border-radius: 14px; background: var(--paper, #fff); }
.fcls__t { margin: 0; font-family: var(--display); font-weight: 600; font-size: 1.02rem; color: var(--ink); }
.fcls__m { margin: 0.15rem 0 0; font-size: 0.82rem; color: var(--ink-soft); }
.fcls__note { margin: 0.6rem 0 0; font-size: 0.8rem; line-height: 1.5; color: var(--ink-soft); max-width: 40rem; padding: 0.5rem 0.75rem; border-radius: 10px; background: var(--wash, rgba(0,0,0,0.04)); }
.fcls__n { display: flex; gap: 1.6rem; margin: 0; }
.fcls__n div { text-align: center; min-width: 3.4rem; }
.fcls__n dt { font-size: 0.7rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-soft); font-family: var(--display); white-space: nowrap; }
.fcls__n dd { margin: 0.1rem 0 0; font-family: var(--display); font-size: 1.35rem; font-weight: 600; color: var(--ink); }
.fcls__wait { flex-basis: 100%; }
.fcls__w, .fcls__x { font-size: 0.72rem; color: var(--ink-soft); text-transform: none; letter-spacing: 0; }
.fcls__a { display: flex; justify-content: flex-end; align-items: center; min-width: 8.5rem; }
.fcls__a .shint { text-align: right; max-width: 14rem; margin: 0; }
@media (max-width: 47.99rem) {
  .fcls__i { grid-template-columns: 1fr; gap: 0.8rem; }
  .fcls__n { justify-content: flex-start; }
  .fcls__n div { text-align: left; }
  .fcls__a { justify-content: flex-start; min-width: 0; }
  .fcls__a .shint { text-align: left; max-width: none; }
}

/* D-30: the store form's "who can buy it" group, and the small listing form on
   each item. */
.sform__set { margin: 1.35rem 0 0; padding: 0; border: 0; min-inline-size: 0; }
.sform__set > legend { padding: 0; font-weight: 600; font-size: 0.92rem; margin-bottom: 0.2rem; }
.sform__set > .pick { margin-top: 0.6rem; width: 100%; max-width: 100%; }
.sshop__f select { width: auto; max-width: 11rem; font-size: 0.8rem; padding: 0.4rem 0.55rem; }
.sshop__f input.sshop__pp, .sshop__m .sshop__f input.sshop__pp { width: 7rem; max-width: 7rem; min-height: 0; font-size: 0.8rem; padding: 0.4rem 0.6rem; border-radius: 10px; }
.sshop__f select { min-height: 0; border-radius: 10px; }
.sshop__m > .sshop__f { margin-top: 0.6rem; }

/* D-31: where an item is sold. */
.sshop__where { margin: 0.9rem 0 0; }
.sshop__where > summary { cursor: pointer; color: var(--violet); text-decoration: underline; font-size: 0.85rem; font-weight: 600; min-height: 36px; display: flex; align-items: center; }
.sshop__where--row { flex: 1 1 100%; margin: 0.3rem 0 0; }
.sshop__cs { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.4rem 0.8rem; margin-top: 0.5rem; }
.sshop__cs .sshop__c, .sform .sshop__cs .pick { margin: 0; padding: 0.35rem 0.6rem; font-size: 0.85rem; width: auto; max-width: none; }

/* ---- the Store: hero ------------------------------------------------- */
.shop__hero {
  background:
    radial-gradient(85% 120% at 14% 12%, #2a1150 0%, rgba(42,17,80,0) 62%),
    radial-gradient(70% 95% at 86% 92%, rgba(214,31,116,0.32) 0%, rgba(214,31,116,0) 62%),
    linear-gradient(168deg, #160d28 0%, #0c0914 58%, var(--night) 100%);
  color: var(--light);
  padding-top: clamp(2.6rem, 6vw, 4.4rem); padding-bottom: clamp(2.4rem, 5vw, 3.6rem);
}
.shop__hero .band__inner { max-width: 72rem; }
.shop__hero h1 { color: #ffffff; margin: 0.4rem 0 0; font-size: clamp(2.1rem, 5.4vw, 3.6rem); line-height: 1.05; max-width: 18ch; }
.shop__kicker { color: var(--stagelight); }
.shop__lede { margin: 1.1rem 0 0; max-width: 58ch; color: var(--light); font-size: 1.05rem; line-height: 1.65; }
.shop__body .band__inner { max-width: 72rem; }
.shop__back { margin: 0 0 1.4rem; font-size: 0.92rem; }
.shop__fine { margin: 2.4rem 0 0; font-size: 0.85rem; color: var(--ink-soft); max-width: 70ch; }

/* ---- the category menu: links, so it works with nothing but a browser -- */
.shopmenu { margin: 0 0 1.8rem; }
.shopmenu ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.55rem; }
.shopmenu li { margin: 0; }
.shopmenu__a {
  display: inline-flex; align-items: center; gap: 0.5rem; min-height: 44px; padding: 0.35rem 1.1rem;
  border: 1.5px solid var(--border-strong); border-radius: 999px; background: #ffffff; color: var(--ink);
  font-weight: 600; font-size: 0.9rem; text-decoration: none;
  transition: border-color var(--ease-t) var(--curve), background var(--ease-t) var(--curve), color var(--ease-t) var(--curve);
}
.shopmenu__a span { font-weight: 500; font-size: 0.78rem; color: var(--ink-soft); }
@media (hover: hover) { .shopmenu__a:hover { border-color: var(--stagelight-deep); background: #f3ead9; } }
.shopmenu__a.is-on { background: var(--ink); border-color: var(--ink); color: #ffffff; }
.shopmenu__a.is-on span { color: var(--light-soft); }
.shopmenu__a:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* ---- the product card: one card, three places ------------------------- */
.pcards {
  list-style: none; padding: 0; margin: 0;
  display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
}
.pcard {
  display: flex; flex-direction: column; overflow: hidden; margin: 0;
  background: #ffffff; border: 1px solid var(--line); border-radius: 18px;
  box-shadow: 0 1px 2px rgba(24,18,10,0.04);
  transition: transform var(--ease-t) var(--curve), box-shadow var(--ease-t) var(--curve), border-color var(--ease-t) var(--curve);
}
@media (hover: hover) { .pcard:hover { transform: translateY(-3px); border-color: var(--stagelight); box-shadow: 0 22px 44px -26px rgba(24,18,10,0.4); } }
.pcard:focus-within { border-color: var(--stagelight-deep); }
.pcard__link { display: block; text-decoration: none; }
.pcard__media, .prod__media {
  position: relative; aspect-ratio: 1 / 1; overflow: hidden; background: var(--bg-soft);
  display: grid; place-items: center;
}
.pcard__media > img, .prod__media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.pcard__media--none, .prod__media--none { background: radial-gradient(120% 95% at 50% 42%, #2e2436 0%, var(--night-2) 52%, #0e0b12 100%); }
.pcard__media--none > img, .prod__media--none > img { position: static; width: 62%; height: auto; max-width: 13rem; object-fit: contain; opacity: 0.95; }
.pcard__body { flex: 1; display: flex; flex-direction: column; gap: 0.3rem; padding: 1.05rem 1.2rem 1.25rem; }
.pcard__cat { margin: 0; font-family: var(--display); font-size: 0.7rem; font-weight: 600; letter-spacing: 0.13em; text-transform: uppercase; color: var(--stagelight-deep); }
.pcard__low { margin-left: 0.4rem; letter-spacing: 0.02em; text-transform: none; font-size: 0.74rem; color: var(--magenta); }
.pcard__name { margin: 0.1rem 0 0; font-family: var(--serif); font-weight: 400; font-size: 1.3rem; line-height: 1.2; letter-spacing: -0.01em; }
.pcard__name a { color: var(--ink); text-decoration: none; }
@media (hover: hover) { .pcard__name a:hover { color: var(--stagelight-deep); } }
.pcard__by { margin: 0; font-size: 0.85rem; color: var(--ink-soft); }
.pcard__price { margin: auto 0 0; padding-top: 0.7rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.6rem; }
.pcard__price b { font-size: 1.15rem; color: var(--ink); }
.pcard__price span { font-size: 0.8rem; color: var(--ink-soft); }
@media (max-width: 30rem) { .pcards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.9rem; } .pcard__body { padding: 0.8rem 0.85rem 1rem; } .pcard__name { font-size: 1.05rem; } .pcard__media--none > img { width: 70%; } }

.shop__empty { padding: 3rem 1.6rem; text-align: center; border: 1.5px dashed var(--border-strong); border-radius: 18px; background: #ffffff; }
.shop__empty h2 { margin: 0 0 0.5rem; }
.shop__empty p { margin: 0 auto; max-width: 44ch; color: var(--ink-soft); }
.shop__sellers { margin-top: 3rem; }
.shop__sellers h2 { font-size: 1.1rem; margin: 0 0 0.9rem; }
.shop__chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.shop__chips li { margin: 0; }
.shop__chips a {
  display: inline-block; padding: 0.4rem 0.95rem; border: 1px solid var(--line); border-radius: 999px;
  background: var(--bg-soft); color: var(--ink); font-size: 0.88rem; text-decoration: none;
}
.shop__chips a span { color: var(--ink-soft); font-size: 0.78rem; margin-left: 0.25rem; }
@media (hover: hover) { .shop__chips a:hover { border-color: var(--stagelight-deep); background: #f3ead9; } }

/* ---- a teacher's storefront: her bio first ---------------------------- */
.shopfront__bio { margin: 1.1rem 0 0; max-width: 62ch; }
.shopfront__bio p { margin: 0 0 0.8rem; color: var(--light); font-size: 1.02rem; line-height: 1.7; }
.shopfront__hero .tbio__a { margin-top: 1.4rem; }

/* ---- the band on the landing page ------------------------------------- */
.shopband { background: var(--bg); }
.shopband__head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 1rem 2rem; margin-bottom: 1.8rem; }
.shopband__head h2 { margin: 0.2rem 0 0; }
.shopband__lede { margin: 0.7rem 0 0; max-width: 54ch; color: var(--ink-soft); }
.shopband__track {
  list-style: none; margin: 0; padding: 0.3rem 0.2rem 1.2rem; display: flex; gap: 1.4rem;
  overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-left: 0.2rem; overscroll-behavior-x: contain;
  scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent;
}
.shopband__track:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 4px; border-radius: 12px; }
.shopband__track > .pcard { flex: 0 0 min(17rem, 74vw); scroll-snap-align: start; }
.shopband__ctl { display: flex; align-items: center; justify-content: center; gap: 0.7rem; margin-top: 0.4rem; }
.shopband__ctl[hidden] { display: none; }
.shopband__b {
  min-width: 44px; min-height: 44px; padding: 0 1rem; border-radius: 999px; line-height: 1;
  background: #ffffff; color: var(--ink); border: 1.5px solid var(--border-strong); box-shadow: none; font-size: 1.05rem;
}
.shopband__b--text { font-size: 0.85rem; min-width: 5rem; }
@media (hover: hover) { .shopband__b:hover { background: #f3ead9; border-color: var(--stagelight-deep); color: var(--ink); box-shadow: none; transform: none; } }

/* ---- a product ---------------------------------------------------------- */
.prod { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(1.6rem, 4vw, 3.4rem); align-items: start; }
.prod__pic { position: sticky; top: calc(var(--masthead-h) + 1rem); }
.prod__media { border: 1px solid var(--line); border-radius: 22px; }
.prod__main h1 { margin: 0.25rem 0 0; font-size: clamp(1.8rem, 4vw, 2.6rem); line-height: 1.1; }
.prod__by { margin: 0.5rem 0 0; color: var(--ink-soft); }
.prod__price { margin: 1.1rem 0 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2rem 0.8rem; }
.prod__price b { font-size: 2rem; font-family: var(--serif); font-weight: 400; color: var(--ink); }
.prod__price span { font-size: 0.92rem; color: var(--ink-soft); }
.prod__low { margin: 0.5rem 0 0; font-weight: 600; color: var(--magenta); font-size: 0.9rem; }
.prod__desc { margin-top: 1.2rem; max-width: 58ch; }
.prod__desc p { margin: 0 0 0.8rem; line-height: 1.7; color: var(--ink-soft); }
.prodbuy.form--long { max-width: none; margin-top: 1.8rem; padding: clamp(1.2rem, 3vw, 1.8rem); background: #ffffff; border: 1px solid var(--line); border-radius: 18px; }
.prodbuy h2, .prodbuy.form--long h2 { margin: 0; padding: 0; border: 0; font-size: 1.25rem; }
.prodbuy label { margin-top: 1.1rem; }
.prodbuy #b_qty { max-width: 8rem; }
.prodbuy input:not([type="checkbox"]):not([type="radio"]) { max-width: 100%; }
.prodbuy [aria-invalid="true"] { border-color: var(--magenta); box-shadow: 0 0 0 3px rgba(214,31,116,0.16); }
.prodbuy__err { margin: 1rem 0 0; padding: 0.8rem 1rem; border-radius: 12px; border-left: 4px solid var(--magenta); background: var(--magenta-soft); color: var(--ink); font-weight: 600; }
.prodbuy__addr { margin: 1.4rem 0 0; padding: 0; border: 0; min-inline-size: 0; }
.prodbuy__addr legend { padding: 0; font-weight: 700; font-size: 0.98rem; }
.prodbuy__addr legend span { font-weight: 500; font-size: 0.82rem; color: var(--ink-soft); margin-left: 0.4rem; }
.prodbuy__opt { font-weight: 500; color: var(--ink-soft); font-size: 0.82rem; }
.prodbuy__pair { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 0 0.9rem; }
.prodbuy__ship { margin: 0.5rem 0 0; font-size: 0.84rem; line-height: 1.55; color: var(--ink-soft); }
.prodbuy__note { margin: 1.3rem 0 0; padding: 0.8rem 1rem; border-radius: 12px; background: var(--bg-soft); color: var(--ink-soft); font-size: 0.92rem; }
.prodbuy .pick { margin-top: 1.4rem; }
.prodbuy button[type="submit"] { width: 100%; margin-top: 1.4rem; min-height: 52px; font-size: 1rem; }
.prodbuy button[type="submit"]:active { transform: translateY(1px); }
@media (prefers-reduced-motion: reduce) { .prodbuy button[type="submit"]:active { transform: none; } }
.prodbuy .soft { margin: 1rem 0 0; font-size: 0.84rem; line-height: 1.6; }
@media (max-width: 56rem) { .prod { grid-template-columns: minmax(0, 1fr); } .prod__pic { position: static; } .prod__media { aspect-ratio: 4 / 3; } }
@media (max-width: 30rem) { .prodbuy__pair { grid-template-columns: minmax(0, 1fr); } }

/* ---- an order, and the small print -------------------------------------- */
.shop__order { max-width: 40rem; }
.shop__order h1 { margin: 0.4rem 0 0.6rem; font-size: clamp(1.7rem, 4vw, 2.3rem); }
.shop__state {
  display: inline-block; margin: 0; padding: 0.25rem 0.8rem; border-radius: 999px; background: var(--bg-soft); border: 1px solid var(--line);
  font-family: var(--display); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-soft);
}
.shop__state--good { background: var(--violet-soft); border-color: var(--violet); color: var(--violet); }
.shopsum { margin: 1.6rem 0 0; padding: 0.4rem 1.2rem; background: #ffffff; border: 1px solid var(--line); border-radius: 16px; }
.shopsum div { display: flex; justify-content: space-between; gap: 1rem; padding: 0.8rem 0; border-bottom: 1px solid var(--line); }
.shopsum div:last-child { border-bottom: 0; }
.shopsum dt { margin: 0; color: var(--ink-soft); }
.shopsum dd { margin: 0; font-weight: 600; white-space: nowrap; }
.shopsum__t dt, .shopsum__t dd { color: var(--ink); font-size: 1.05rem; font-weight: 700; }
.shop__addr { margin: 1.2rem 0 0; line-height: 1.6; }
.shop__prose { max-width: 46rem; }
.shop__prose h1 { margin-bottom: 1.2rem; }
.shop__prose h2 { font-size: 1.15rem; margin: 1.8rem 0 0.4rem; }
.shop__prose p { line-height: 1.75; color: var(--ink-soft); }
