/* shared/caret.css */
/* caret — the shared FA disclosure marker (replaces the old "▾"/"▸" glyphs
   everywhere, Vicky 2026-07-23). Rotates with its details.

   Lifted out of admin/list_dropdown.css and into shared/ on 2026-08-03: the
   name field is a buyer-journey component that needs the same marker the
   admin menus use, and a component in shared/ cannot depend on a rule that
   only ships in the admin bundle. Nothing about the rule changed. */
/* --text-muted, not --text-faint (Vicky, 2026-09-02). --text-faint is the
   design system's HAIRLINE tone and is documented as not a text colour, so
   every disclosure marker in the product was drawn at a weight meant for a
   rule between two rows. A caret is the only thing on a summary that says the
   row opens, so it has to be as readable as the words beside it. */
.caret { align-items: center; color: var(--text-muted); display: inline-flex; font-size: .8em; margin-left: .3rem; transition: transform .15s; }
.caret svg, .caret i { display: block; }
/* Darkens with the row it belongs to: a marker that stays put while its
   summary lights up reads as part of the background rather than the control. */
summary:hover > .caret { color: var(--text); }
details[open] > summary > .caret { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .caret { transition: none; }
}

/* shared/powered_by.css */
/* ---- "Powered by Tito" ----
   ONE recipe, on every surface a buyer can reach (Vicky, 2026-08-21: "powered
   by Tito shouldn't be underlined", and "it should be the same size
   everywhere, not too prominent").

   It was three. The journey's footer drew it muted at .8rem; the Pro event
   page drew it in full page ink at the body size, a third larger and far
   darker; Classic drew it at the body size in link blue. All three underlined
   it, so the quietest sentence on the page was styled as the loudest link on
   it — on the event page, the most prominent link above the fold after the
   ticket names themselves.

   Keyed on the attribute rather than a class because that is what every
   surface already puts on this anchor: `data-src="style:powered_by"` is how
   Inspect finds it, so the credit cannot be styled here and not be the credit.
   Lives in shared/ and is loaded by BOTH bundles for the same reason caret.css
   is (styles.go) — a copy under styles/event/ would be a second recipe to keep
   in step, which is the drift this rule exists to end.

   Hover restores the underline: it is still a link, and nothing else marks it
   as one. */
[data-src="style:powered_by"] {
  color: var(--muted, currentColor);
  font-size: .75rem;
  opacity: .85;
  text-decoration: none;
}
[data-src="style:powered_by"]:hover { opacity: 1; text-decoration: underline; }
[data-src="style:powered_by"]:focus-visible { border-radius: .2rem; outline: 2px solid currentColor; outline-offset: 2px; }

/* shared/account_footer.css */
/* ---- The account footer (account_footer.go, PRO-4799) ----
   The account's own small print — an Impressum, credits, an unsubscribe line —
   with its own logo, at the foot of every buyer page. Shared by both bundles
   (styles.go) so the event page and the journey draw one footer, not two.

   With no background set it is not a band at all: it takes the page's own
   surface and the page's muted ink, and reads as the quiet last thing on the
   page. A background turns it into the full-width band DFA have, and then
   --footer-ink is always set too (footerColors picks one that reads).

   Full width and at the bottom of the window on a short page: the layouts
   that carry it (journey_shell.css, event_pro.css, event_classic.css) make
   the page a column and this margin-top: auto is what pushes it down. */
account-footer {
  background: var(--footer-bg, transparent);
  box-sizing: border-box;
  color: var(--footer-ink, var(--muted, currentColor));
  display: block;
  font-size: .8125rem;
  line-height: 1.6;
  margin-top: auto;
  padding: 2rem 1.25rem 1.5rem;
  text-align: start;
  width: 100%;
}
account-footer[data-band] { padding-block: 2.5rem 2rem; }
account-footer > footer-inner { display: block; margin: 0 auto; max-width: 640px; }
account-footer .footer-logo { display: inline-block; margin-bottom: 1rem; }
/* Where the logo goes against the words (footer_logo_position, PRO-4965). The
   template already put it first or last in the source; these only space it.
   Under the words it takes the gap above it rather than below. Beside them the
   two make a row that wraps, so on a phone the logo goes over or under the
   words again instead of squeezing them into a column a word wide. */
account-footer[data-logo="below"] .footer-logo { margin-bottom: 0; margin-top: .4rem; }
account-footer:is([data-logo="left"], [data-logo="right"]) > footer-inner { align-items: flex-start; column-gap: 2rem; display: flex; flex-wrap: wrap; }
account-footer:is([data-logo="left"], [data-logo="right"]) .footer-logo { flex: none; }
account-footer:is([data-logo="left"], [data-logo="right"]) footer-text { flex: 1 1 16rem; min-width: 0; }
/* The footer's own logo only: a picture or a grid in the words is sized by
   its own rules below. */
account-footer :is(a.footer-logo img, img.footer-logo) { display: block; height: auto; max-height: 3rem; max-width: 12rem; }
account-footer footer-text { display: block; }
account-footer footer-text :is(p, ul, ol) { margin: 0 0 .6rem; }
account-footer footer-text :is(ul, ol) { padding-inline-start: 1.25rem; }
/* Headings in the footer are the footer's own: small, and in its ink rather
   than the page's heading colour, which was chosen for the card. */
:root account-footer footer-text :is(h2, h3) { color: inherit; font-size: .9375rem; font-weight: 600; line-height: 1.4; margin: 0 0 .4rem; }
:root account-footer footer-text h3 { font-size: .8125rem; }
/* Links wear the footer's ink and an underline, never the page's link colour:
   that colour was picked to read on the card, and the band is not the card.
   :root lifts this over the journey's own `body.buyer-journey a`. */
:root account-footer a[href] { color: inherit; text-decoration: underline; }
:root account-footer a[href]:focus-visible { border-radius: .2rem; outline: 2px solid currentColor; outline-offset: 2px; }
/* Pictures and photo grids in the footer (account_footer.go's footerBlocks).
   The journey's own rules for these (static_page.css, photo_grid.css) do not
   reach the event page's bundle, and a file can sit in only one bundle, so
   the footer carries the few it needs, scoped to itself. A grid's columns
   are fixed shares of the row and never rewrap, as on a page. */
account-footer .sp-picture { margin: 0 0 1rem; }
account-footer .sp-picture img { display: block; height: auto; max-width: 100%; }
account-footer .sp-grid { margin: 0 0 1rem; }
account-footer .sp-grid ul { display: grid; gap: .5rem; grid-template-columns: repeat(3, minmax(0, 1fr)); list-style: none; margin: 0; padding: 0; }
account-footer .sp-grid ul[data-cols="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
account-footer .sp-grid ul[data-cols="1"] { grid-template-columns: minmax(0, 1fr); }
account-footer .sp-grid li { margin: 0; }
account-footer .sp-grid a { display: block; }
account-footer .sp-grid img { aspect-ratio: 1; display: block; height: auto; object-fit: cover; width: 100%; }
account-footer .sp-grid ul[data-shape="portrait"] img { aspect-ratio: 4 / 5; }
account-footer .sp-grid ul[data-shape="landscape"] img { aspect-ratio: 3 / 2; }
account-footer .sp-grid figcaption { margin-top: .5rem; }
/* The Frame tab's columns and social links (site_frame.go, PRO-5171), above
   the words in the same footer, on exactly the words' measure: the pages that
   line the words up with their card (journey_shell.css, event_pro.css) line
   these up the same way, so the columns, the icons and the small print start
   at one edge (Vicky, 2026-10-06). The columns and the icons wear the look's own footer
   element names (site_chrome.css lays them out); only the spacing is here. */
account-footer > footer-links { display: grid; gap: 2rem; margin: 0 auto 2rem; max-width: 640px; }
account-footer > footer-links:only-child { margin-bottom: 0; }
/* Under columns, the small print is as wide as what it says, so pointing at
   it in Customize outlines the words rather than an empty band to the edge. */
account-footer:has(> footer-links) footer-text { max-width: 100%; width: fit-content; }
:root account-footer site-footer-columns h2 { color: inherit; font-size: .9375rem; font-weight: 600; line-height: 1.4; margin: 0 0 .6rem; }
account-footer site-footer-columns ul { gap: .4rem; }

/* shared/session_picker.css */
/* The buyer's time picker: one radio row per time.

   It was a <select> until PRO-4549 gave a time its own description, and an
   <option> cannot hold a second line — so the picker that could only be
   drawn properly by the step wizard is drawn in the markup instead, and the
   zero-JS page reads the same words as everyone else. The portal's chosen
   group (portal_store.css) is the model: name, a quieter line under it, and
   a note on the right saying what is different about this one.

   PALETTE-FREE ON PURPOSE. This one component renders in two documents that
   share no tokens — the standalone event page's --custom-* set and the buyer
   journey's own — so every colour here is mixed out of currentColor and
   inherits whichever palette it lands in. That is also what makes it correct
   on a themed page, where the account picked the ink.

   Nothing here sets `display` on .session-picker: checkout-steps.js hides the
   whole fieldset with the `hidden` attribute while the wizard runs, and a
   display rule would outrank it. */
/* A group with ONE time on sale is stated, not asked: no fieldset, no legend,
   no row to press (theme_blocks.html, Vicky 2026-09-03). It sits under the
   group's own heading and reads as a fact about the ticket rather than as a
   control somebody has failed to notice, so it takes the quiet weight the
   row's second line takes and none of the row's furniture.

   Palette-free like everything else here: mixed out of currentColor so it is
   correct in both documents this component renders in, and on a themed page
   where the account picked the ink. */
.session-stated {
  color: color-mix(in srgb, currentColor 72%, transparent);
  margin: 0 0 .25rem;
}
.session-stated-desc {
  color: color-mix(in srgb, currentColor 72%, transparent);
  font-size: .92em;
  line-height: 1.45;
  margin: 0 0 1rem;
}
.session-stated:last-child { margin-bottom: 1rem; }
.session-picker {
  border: 0;
  margin: 0 0 1rem;
  /* A fieldset's default min-inline-size is min-content, which stops it
     shrinking inside the purchase column and pushes a phone page sideways.
     The house idiom for a chrome-less fieldset (swatch_choices.css). */
  min-inline-size: 0;
  padding: 0;
}
/* In an admin form it is one field among the others, so it takes the space
   above it that a field's label takes (label.css). Without it the legend sat
   hard under the box before it (Vicky, 2026-10-09, the New quote form). */
.session-picker[data-in-form] { margin-top: .8rem; }
/* Padding on a legend lands UNDER it rather than around it, so the gap to
   the first row is a margin. */
.session-picker > legend { margin: 0 0 .35rem; padding: 0; }
/* The same footnote on the zero-JS legend. Palette-free like the rest of this
   file, so it is mixed out of currentColor and stays right on a themed page. */
.session-picker > legend .step-heading-count {
  color: color-mix(in srgb, currentColor 62%, transparent);
  font-size: .8125em;
  font-weight: 400;
  white-space: nowrap;
}
/* The zone the times are told in, said once over the rows (PRO-5558), and
   copied as it is onto the step wizard's time step. Quiet, like the count
   beside the legend. */
.session-zone {
  color: color-mix(in srgb, currentColor 62%, transparent);
  font-size: .8125em;
  margin: 0 0 .5rem;
}
.session-row {
  align-items: flex-start;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  border-radius: 10px;
  cursor: pointer;
  display: flex;
  gap: .6rem;
  margin: 0 0 .4rem;
  padding: .65rem .8rem;
}
.session-row:hover:not([data-shut]) { border-color: color-mix(in srgb, currentColor 45%, transparent); }
.session-row:has(input:checked) {
  background: color-mix(in srgb, currentColor 6%, transparent);
  border-color: currentColor;
}
/* The row is the target, so the focus ring belongs on the row — the radio
   itself is a 13px box in the corner of it. */
.session-row:has(input:focus-visible) { outline: 2px solid currentColor; outline-offset: 2px; }
.session-row[data-shut] { cursor: default; opacity: .55; }
.session-row > input { flex: none; margin: .2rem 0 0; }
.session-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: .15rem;
  /* Without this a long description refuses to wrap and widens the row. */
  min-width: 0;
}
/* The attendee's own schedule page (ticket_schedule.html) draws the row as a
   div, because a row there can carry a waitlist button and a label may not
   hold a second control; the words are the radio's label, by for=. The
   base label rule (label.css) would make them a small grey field name. */
label.session-body { color: inherit; cursor: pointer; font-size: inherit; margin: 0; }
.session-when { font-weight: 600; }
.session-where { font-size: .875rem; opacity: .75; }
.session-desc { font-size: .875rem; line-height: 1.45; opacity: .75; }
.session-note {
  flex: none;
  font-size: .8125rem;
  opacity: .8;
}

/* Refused at checkout: the block says so where the answer is given, rather
   than at the top of the page (Vicky: status goes under the job). The same
   treatment a refused choice of goods gets, and palette-free for the same
   reason as the rest of this file — a themed page keeps its own ink. */
.session-picker.bad > legend { color: inherit; }
.session-picker.bad .session-row {
  border-color: color-mix(in srgb, currentColor 55%, transparent);
}

/* shared/addon_group.css */
/* A CHOICE of items a ticket comes with (PRO-4597): several things a buyer
   picks between rather than adds one by one.

   In shared/ rather than event/ since 2026-09-05, and for the same reason
   session_picker.css is: it renders in two documents with two palettes and is
   written to need neither. It used to draw under a ticket row on the event
   page; it draws on the OPTIONS page now, which is a buyer-journey document
   loading public.css, and an event-only sheet left it unstyled there.

   It is the session picker's shape with a different question, and that is
   deliberate — a buyer meeting both on one page should not have to learn two
   layouts for "pick from these". What differs is the heading: a session
   group's legend is Tito's sentence, and this one is the ORGANISER's own name
   for the choice, so it carries the weight a name deserves.

   Palette-free, mixed out of currentColor like session_picker.css, because
   this renders on a themed page where the account picked the ink. */

/* The row this sits in is a GRID of name, price and quantity — the shape an
   ordinary add-on needs and the wrong one for a block that owns its whole
   width. Reset to a plain block so the choice spans the column rather than
   being squeezed into the name's track. */
ticket-row[addon-group] {
  display: block;
  padding-bottom: .35rem;
}

.addon-group {
  border: 0;
  margin: 0 0 .75rem;
  /* A fieldset's default min-inline-size is min-content, which stops it
     shrinking inside the purchase column and pushes a phone page sideways.
     The house idiom for a chrome-less fieldset (swatch_choices.css). */
  min-inline-size: 0;
  padding: 0;
}

/* Padding on a legend lands UNDER it rather than around it, so the gap to the
   first row is a margin. */
.addon-group > legend {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin: 0 0 .4rem;
  padding: 0;
}

.addon-group-name { font-weight: 600; }

.addon-group-row {
  align-items: flex-start;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  border-radius: 10px;
  cursor: pointer;
  display: flex;
  gap: .6rem;
  margin-bottom: .4rem;
  padding: .65rem .8rem;
}
.addon-group-row:last-child { margin-bottom: 0; }
.addon-group-row:hover { border-color: color-mix(in srgb, currentColor 45%, transparent); }
.addon-group-row:has(input:checked) {
  background: color-mix(in srgb, currentColor 6%, transparent);
  border-color: currentColor;
}
.addon-group-row:has(input:focus-visible) { outline: 2px solid currentColor; outline-offset: 2px; }
/* The row is the label, so the box inside it must not eat the click twice.
   The top margin sits it on the name's line, as the session row's does. */
.addon-group-row > input { flex: none; margin: .2rem 0 0; }

.addon-group-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: .15rem;
  min-width: 0;
}
/* The session row's recipe (session_picker.css), so a workshop and a dinner
   asked on one page read at the same weight. */
.addon-group-item { font-weight: 600; overflow-wrap: anywhere; }
.addon-group-desc { font-size: .875rem; line-height: 1.45; opacity: .75; }
/* Pushed to the end rather than given a column: a free item's word and a
   priced one's amount are different lengths and neither should move the name. */
.addon-group-price {
  font-weight: 600;
  margin-left: auto;
  text-align: right;
  white-space: nowrap;
}
/* The tax note under the amount, right-aligned with it. Full ink: dimmed
   at this size it falls below AA contrast. */
.addon-group-price .tax-note { font-size: .75rem; font-weight: 400; }

/* Refused at checkout: the block says so where the answer is given, rather
   than at the top of the page (Vicky: status goes under the job). */
.addon-group.bad > legend { color: inherit; }
.addon-group.bad .addon-group-row {
  border-color: color-mix(in srgb, currentColor 55%, transparent);
}

/* shared/asked_elsewhere.css */
/* What page one carries for the BROWSER to drive, and must not show without
   it.

   Two routes ask a buyer what their tickets come with (Vicky, 2026-09-05).
   checkout-steps.js walks them in the page, instantly, which is the majority
   experience and the one worth keeping. Without JavaScript they are a server
   page between the tickets and the details (checkout_options.go).

   The wizard can only drive controls that are IN the page, so page one still
   renders them — and page one must not SHOW them, because a buyer with no
   JavaScript would then meet every question for every ticket type before
   choosing a ticket, which is the thing both routes exist to stop.

   So they are hidden by default and revealed by the class the inline script
   in <head> stamps on <html>.

   THE REVEAL MUST YIELD TO [hidden], and the :not() below is the whole of
   that. An author `display:` beats the UA's own [hidden] { display: none } —
   which is how a choice block shipped on 2026-09-05 marked hidden by the
   wizard and drawn on the screen anyway, and how this file reintroduced the
   same fault an hour after it was fixed. The wizard sets and clears [hidden]
   on these very elements as a buyer walks, so a reveal that ignores it takes
   control of what shows away from the module that is doing the showing.

   Once .js is on the document and the module has run, it owns what is on
   screen, exactly as it did before this file existed. */
/* The LIST ITEM goes with the block, or the gap stays where the block was: an
   empty <li> still takes the list's own spacing and its rule, so a page with
   four choices hidden showed four blank rows between the tickets. The block
   is a <li><ticket-row> on the modern skin and a bare <ticket-row> on the
   classic one, so both are named. */
li[data-asked-elsewhere],
ticket-row[addon-group] {
  display: none;
}

html.js li[data-asked-elsewhere]:has(> ticket-row[addon-group]:not([hidden])) {
  display: revert;
}

html.js ticket-row[addon-group]:not([hidden]) {
  display: block;
}

/* A TIME HIDES ONLY WHERE SOMETHING CAN BRING IT BACK (PRO-5157).

   The default hide above is safe for the questions beside it because those
   have somewhere else to be asked: without JavaScript, checkout_options.go
   serves them as a page between the tickets and the details, so hiding them
   on page one costs that buyer nothing.

   WHICH HALF OF THE SCHEDULE THAT IS TRUE OF IS data-name-leads, and it is
   the whole of the split below. An OPTIONS-shaped group (NameLeads, the
   workshop a buyer picks by its name) is asked on that page, PER SEAT
   (checkout_options.go, `if !group.NameLeads { continue }`), so its run keeps
   the unconditional hide: showing it on page one would put one order-wide box
   in front of the tickets, which answers for every seat at once and is the
   thing both routes exist to stop.

   A TIME-shaped group has nowhere else. The options page skips it, the server
   reads session_<id> straight off the checkout POST (selectedSessions,
   shop.go), and no page asks for a time on its own — so the picker is on page
   one for that buyer or it is nowhere. On the TIME-FIRST shape the run holds
   the ticket rows and their quantity boxes too, so the event page had nothing
   on it to choose and nothing on it to buy; on the shared shape the tickets
   stood and the times did not, and the checkout refused a time nobody had
   been offered.

   So a time's run hides under html.js and not before it. With the class on,
   the cascade is exactly what it was: this hides, the reveal below outranks
   it, and [hidden] takes it back, so the wizard still owns what is on screen.
   Without the class neither rule applies and the times read top to bottom,
   which is what they did before 2026-09-06.

   The exclusion rides in :where() so it weighs nothing. Written plainly it
   would put this rule on (0,4,2), which is the reveal's own weight, and the
   reveal would then be winning on SOURCE ORDER — true today and one moved
   rule away from being false. At (0,3,2) it loses to the reveal outright,
   which is the relationship the rule below is written against. */
html.js section.schedule-group:not([data-schedule-list]):where(:not([data-name-leads])) {
  display: none;
}

section.schedule-group[data-name-leads]:not([data-schedule-list]) {
  display: none;
}

html.js section.schedule-group:not([data-schedule-list]):not([hidden]) {
  display: revert;
}

/* AND NOT BEFORE THE WIZARD HAS DRAWN (PRO-4687).

   Vicky, walking the buyer journey: "there's also a brief flash of the
   workshops before the tickets are displayed, as the page loads."

   The reveal above is stamped by the inline script in <head>, so it happens
   at first paint. checkout-steps.js is a module and runs after it, so the
   workshops stood on screen for a frame before the wizard replaced them with
   its own panel.

   So the reveal waits for the module instead. The module adds .steps-drawn to
   the root element at its END, on every path it can leave by — the wizard
   engaging AND the wizard deciding not to — because the page has to come back
   either way.

   ONLY A RUN THAT CARRIES NO TICKETS. data-times-only is written by the
   server (theme_blocks.html) for a run holding times alone, which is the
   shape a buyer sees the flash in. A grouped run holds its own ticket list,
   and hiding one would hide a quantity input — a control a buyer is aiming
   at, and one that browser tests wait to see. Keeping the rule off those runs
   is what makes "the module never ran" a stale frame rather than a checkout
   with no tickets in it.

   The failsafe is in theme_blocks.html: if the module never runs at all, the
   page marks itself on load and the worst case is the flash we started with.
   Hiding a question for good is far worse than showing it early, and it would
   only ever happen on a broken asset, which is exactly when nobody is
   watching. */
html.js:not(.steps-drawn) form[data-checkout] section.schedule-group[data-times-only] {
  display: none;
}

/* shared/sr_only.css */
/* The 1px box only stays 1px if nothing pads it. A .sr-only BUTTON inherits
   button.css's padding, and with content-box that padding is added OUTSIDE
   the 1px — so every list's sr-only search submit measured 28×14 and read to
   the screen audit as a real tap target too small to hit (WCAG 2.2 SC 2.5.8).
   It was never a pointer target at all: it is clipped. Zeroing the box is
   what makes the element as small as it claims to be. */
.sr-only { border: 0; clip: rect(0 0 0 0); clip-path: inset(50%); height: 1px; overflow: hidden; padding: 0; position: absolute; white-space: nowrap; width: 1px; }

/* shared/qty_mark.css */
/* The ± on a quantity stepper, DRAWN rather than typed.

   The buttons used to carry "−" and "+" as text, centred by the flex box
   around them. That centres the LINE BOX, and a glyph does not sit in the
   middle of its own line box: where its ink lands depends on the font's
   ascent/descent split and on where that font puts the mathematical axis. So
   the mark came out off-centre, by a different amount in every skin and in
   both directions at once — measured on 2026-09-20, the Classic skin's − sat
   0.5px high and its + a full 1px high, while the same two glyphs on the
   default look sat 1.5px and 1px LOW, because that look resolves to Inter and
   Classic's to the system face. In a solid Classic circle it is plain to the
   eye (Paul).

   No nudge can fix that: the correction would have to differ per skin, per
   glyph, and per whatever font the visitor's device actually falls back to.
   Two rectangles have no metrics, so they are exactly centred everywhere, at
   every zoom, in every locale — and the mark stops changing shape when a skin
   asks for a weight the face has to synthesise.

   THE BUTTON CARRIES NO TEXT AT ALL NOW, and that is deliberate rather than
   tidiness. The first cut kept the glyph in the DOM at font-size 0, which
   read as free — until you ask what happens when anything puts a font-size
   back: a user stylesheet's `button { font-size: 16px !important }`, a later
   rule at higher specificity, an account's own CSS naming .qty. Then the
   glyph returns ON TOP of the bars and the button wears a doubled mark. The
   exposure is worst in the event bundle, where this sheet is concatenated
   FIRST and so loses every equal-specificity tie. Nothing is lost by
   removing the text: the accessible name has always been the translated
   aria-label stepper.js sets, which overrides text content anyway.

   Listed by the buyer bundle AND the standalone event bundle (styles.go),
   because a stepper draws on both and the recipe is the same on each. */
button.step {
  --step-arm: .6rem;   /* the bar's length: the old glyph's width at 1.1rem */
  --step-rule: .09rem; /* its thickness */
  position: relative;
}
/* Both marks are one horizontal bar; the plus adds the upright. Centred by
   inset + translate rather than by flex, so the two bars cross at the
   button's exact middle and neither depends on the other. */
button.step::before,
button.step::after {
  background: currentColor;
  content: "";
  inset-block-start: 50%;
  inset-inline-start: 50%;
  position: absolute;
  translate: -50% -50%;
}
button.step::before { block-size: var(--step-rule); inline-size: var(--step-arm); }
button.step[data-step="up"]::after { block-size: var(--step-arm); inline-size: var(--step-rule); }
/* Forced colours override background-color, which would leave a blank button
   with no mark at all. ButtonText is the one the surrounding button is
   guaranteed to be drawn against.
   The disabled pair is the half a drawn mark loses for free and typed text
   got: Windows High Contrast greys a disabled button's OWN text and knows
   nothing about a background we painted, so without this a stepper at its
   limit looks exactly like one that will still step. Colour, not opacity,
   which forced colours does not honour (admin/page_order.css says the same). */
@media (forced-colors: active) {
  button.step::before,
  button.step::after { background: ButtonText; }
  button.step:disabled::before,
  button.step:disabled::after { background: GrayText; }
}

/* shared/name_field.css */
.name-row { display: grid; gap: .8rem; grid-template-columns: 1fr 1fr; }
/* The ultimate name field. The input is the field; the reading beneath it is
   an EXTENSION OF THE BOX — one thin strip, flush under the input, same
   border, bottom corners rounded — that gains a labelled pair per part as
   the name is typed (Paul, 2026-08-29). It replaced a hairline-ruled block
   of one row per part, which put the same words on three lines and pushed
   the rest of the checkout down for every name with a middle one. Where the
   split was a guess the doubted value carries the runner-up inline, one tap
   away. The correction itself lives one step down behind the Edit control.

   Being this small is also why the strip shows from the first word again,
   where the block waited for a guess to justify itself — see name-field.js
   on what that trade did and did not settle. */
/* A row, so the control can sit at the TRAILING EDGE of the strip (Vicky,
   2026-08-19) rather than under it. The pairs reflow inside the dl while
   the control keeps its corner, so a long reading costs a second line of
   words and never a line of its own. */
.namefield-read {
  align-items: baseline;
  background: color-mix(in srgb, var(--fg, currentColor) 3%, transparent);
  border: 1px solid var(--control-line, var(--field-line));
  /* The input's own corner, so the two boxes are one shape — the buyer
     field wears the event design's radius, and the house step covers
     anywhere else this partial is rendered. */
  border-radius: 0 0 var(--bj-radius-sm, var(--r-md, 7px)) var(--bj-radius-sm, var(--r-md, 7px));
  border-top: 0;
  box-sizing: border-box;
  display: flex;
  font-size: var(--bj-support, .85rem);
  gap: .5rem;
  /* Over the input's own bottom border, so the two read as one control
     rather than as a box with a box under it. */
  margin: -1px 0 0;
  padding: .3rem .65rem;
  width: 100%;
}
/* [hidden] must still beat display:flex — it does on specificity (0,2,0
   over 0,1,0), which is why the restatement below is not enough on its own
   and this rule must never gain a second class. */
.namefield-read > dl { flex: 1; min-width: 0; }
/* Author display rules outrank the UA's [hidden] — restate it. */
.namefield-read[hidden], .namefield-details > summary[hidden], .namefield-words[hidden] { display: none; }
.namefield-strip { display: flex; flex-wrap: wrap; gap: .1rem .9rem; margin: 0; }
/* Label and value are one item, so the strip wraps between parts and never
   between a label and the word it labels. */
.namefield-pair { align-items: baseline; display: flex; gap: .35rem; min-width: 0; }
/* Muted, and then a step back toward the text: the strip's own tint moves
   the local background 3% toward the foreground, which takes plain --muted
   on this surface close enough to the AA floor that a palette rounding the
   other way could cross it. The label pays the tint back and still reads as
   the quiet half of the pair. */
.namefield-read dt { color: color-mix(in srgb, var(--muted, var(--text-muted)) 88%, var(--fg, currentColor)); font-weight: 400; }
.namefield-read dd { margin: 0; }

/* The one-tap alternative: quiet enough to ignore, plainly an offer. Dashed
   underline so it reads as "or this?" rather than as the current value. */
/* Dashed UNDERLINE, not a border: a border-bottom gives the button a taller
   box, which made the one row carrying an offer sit lower than its
   neighbours in the reading. text-decoration costs no height. */
button.namefield-alt, body.buyer-journey button.namefield-alt {
  background: none; border: 0; border-radius: var(--r-sm, 4px); box-shadow: none;
  color: var(--journey-link, var(--accent)); cursor: pointer; display: inline; font: inherit; line-height: inherit;
  margin: 0 0 0 .5rem; min-height: 0; padding: 0; text-decoration: underline dashed;
  text-decoration-color: color-mix(in srgb, var(--journey-link, var(--accent)) 55%, transparent);
  text-underline-offset: .25em; width: auto; }
button.namefield-alt:hover, body.buyer-journey button.namefield-alt:hover {
  background: var(--accent-weak, rgba(37,99,235,.09)); text-decoration-color: var(--journey-link, var(--accent)); }
.namefield-applied { align-items: center; color: var(--ok-ink); display: inline-flex; gap: .25rem; margin-inline-start: .5rem; }

/* A pencil and the word "Edit", top right of the block it changes, and
   only ever beside a reading we have already said we are unsure about
   (Vicky, 2026-08-19). The 2026-08-03 objection this still answers was an
   unlabelled pencil floating off to the right of rows it never claimed —
   the fix was a label, not a position, so the label is what stayed.

   Not underlined: it is an action on the block beside it, not a link out of
   the page, and the underline was what made it read as one. flex: none so
   the label never wraps into two lines against a long reading.

   WCAG 2.2 2.5.8 (target size, minimum): the 24px floor is on min-height,
   with the padding doing the width — a min-width would strand the tint away
   from the words on a locale whose "Edit" is one short word. */
button.namefield-edit, body.buyer-journey button.namefield-edit {
  align-items: center; background: none; border: 0; border-radius: var(--r-md, 7px); box-shadow: none;
  color: var(--journey-link, var(--accent)); cursor: pointer; display: inline-flex;
  flex: none; font: inherit; font-size: var(--bj-support, .85rem); gap: .3rem;
  line-height: 1.4; margin: 0; min-height: 24px; padding: .15rem .35rem;
  text-decoration: none; white-space: nowrap; width: auto; }
button.namefield-edit:hover, body.buyer-journey button.namefield-edit:hover {
  background: var(--accent-weak, rgba(37,99,235,.09)); }
.namefield-edit svg { flex: none; }
/* Visually hidden, still read out: the live region, and the sentence that
   describes the Edit control (see name-field.js — it is a description
   rather than the accessible name so WCAG 2.5.3 holds in every language). */
.namefield-live, .namefield-vh { block-size: 1px; clip-path: inset(50%); inline-size: 1px; overflow: hidden; position: absolute; }

/* namefield-more is the ADMIN fold — title, middle name, suffix and goes
   by, folded because almost nobody fills them in. Its own class rather
   than namefield-details: that one names the buyer field's reading
   machinery, and TestAdminAttendeeNameFields exists to keep the buyer
   field out of admin. Same look, so it rides the same rules below. */
.namefield-details, .namefield-more { margin: .45rem 0 0; }
/* WCAG 2.2 2.5.8 (target size, minimum): a disclosure summary IS a pointer
   target, and at .85rem this row measured 140×20px on a 390px screen — the
   admin screen audit caught it the day it shipped. min-height rather than
   padding, so the row grows to the floor without adding a gap above the
   fields it opens. Admin only: the buyer field's own summary sits in the
   journey layout and is audited separately. */
.namefield-more > summary { min-height: 24px; }
/* The disclosure marker is the house .caret (shared/caret.css) — the same
   one the admin menus use — not a text glyph (Vicky, 2026-08-03). */
.namefield-details > summary, .namefield-more > summary, .namefield-all > summary { align-items: center; color: var(--muted, var(--text-muted)); cursor: pointer; display: flex; font-size: var(--bj-support, .85rem); list-style: none; width: fit-content; }
.namefield-details > summary::-webkit-details-marker, .namefield-more > summary::-webkit-details-marker, .namefield-all > summary::-webkit-details-marker { display: none; }
.namefield-details[open] > summary, .namefield-more[open] > summary, .namefield-all[open] > summary { margin-bottom: .35rem; }
/* "View all name fields" is the way further down, not the next thing to
   read: it sits at the right edge, out of the reading order the words and
   the reading above it establish (Vicky, 2026-08-03). */
.namefield-all > summary { margin-left: auto; }
/* A tighter, deliberate rhythm inside the BUYER field. The page-wide label
   margin is set for standalone form rows; stacked two-up rows inside a
   disclosure read as drifting apart at that spacing (Vicky, 2026-08-03).
   The admin variant keeps the house rhythm — it IS a standalone form. */
.namefield-details .name-row label, .namefield-more .name-row label { margin-top: 0; }
.namefield-details .name-row + .name-row, .namefield-more .name-row + .name-row { margin-top: .55rem; }

/* Admin: six fields in a form, so they breathe like the rest of one. The
   pairs sit closer to each other than to the field above and below, so a
   row reads as one thing rather than two loose columns. */
.namefield-plain .name-row { gap: 1.1rem 1rem; }
.namefield-plain .name-row + .name-row { margin-top: 1.1rem; }
.namefield-plain .name-row label { margin-top: 0; margin-bottom: .3rem; }
.namefield-plain { margin-bottom: 1.4rem; }
.namefield-all { border-top: 1px solid var(--hairline, var(--line)); margin-top: .8rem; padding-top: .7rem; }
/* Only once the fields are actually showing: a rule between them and
   whatever the form asks for next, so the name stops being one continuous
   run of inputs with the email. */
.namefield-all[open] { border-bottom: 1px solid var(--hairline, var(--line)); padding-bottom: 1rem; }

/* Tap the words that make up the last name. Word chips are deliberately
   NOT buttons-sized — hairline outline, supporting-text weight — because a
   row of big buttons reads as the primary action of the page rather than a
   quiet correction. Still ≥1.5rem tall, for WCAG 2.2 target size. */
.namefield-words { margin: .1rem 0 .2rem; }
.namefield-taphint { color: var(--muted, var(--text-muted)); font-size: var(--bj-support, .85rem); margin: 0 0 .45rem; }
.namefield-wordrow { display: flex; flex-wrap: wrap; gap: .35rem; }
button.namefield-word, body.buyer-journey button.namefield-word {
  align-items: center; background: transparent; border: 1px solid var(--hairline, var(--line));
  border-radius: var(--r-md, 7px); box-shadow: none; color: var(--fg, var(--text)); cursor: pointer; display: inline-flex;
  font: inherit; font-size: var(--bj-support, .85rem); gap: .3rem; margin: 0; min-height: 1.75rem;
  padding: .15rem .55rem .15rem .4rem; transition: background .13s, border-color .13s, color .13s; width: auto; }
button.namefield-word:hover, body.buyer-journey button.namefield-word:hover {
  border-color: var(--control-line, var(--line-strong)); }
button.namefield-word[aria-pressed="true"], body.buyer-journey button.namefield-word[aria-pressed="true"] {
  background: var(--accent-weak, rgba(37,99,235,.09)); border-color: color-mix(in srgb, var(--journey-link, var(--accent)) 32%, transparent);
  color: var(--accent-ink, var(--journey-link, var(--accent))); font-weight: 550; }
/* Every chip carries a mark: a faint cross when the word is not part of the
   last name, the accent tick when it is. Blank space on one side read as an
   alignment bug rather than a state (Vicky, 2026-08-03), and rendering both
   at the same size keeps the row from shifting under the finger.  */
.namefield-tick { display: inline-flex; line-height: 0; transition: color .12s; }
.namefield-tick.is-off { color: var(--muted, var(--text-muted)); opacity: .5; }
.namefield-tick.is-on { color: var(--journey-link, var(--accent)); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .namefield-tick, button.namefield-word { transition: none; }
}
/* The greeting question ("Liebe Anna" / "Lieber Anna" / "Hallo Anna"). Its
   options are the yes/no answer cards (yesno_field.css), so all this file
   owns is the fieldset and its legend. */
.namefield-address { border: 0; margin: .9rem 0 0; min-width: 0; padding: 0; }
/* The legend reads as one more field label: the journey's label voice on a
   buyer page, and admin's own radio-field legend (form_voice.css) there. */
.namefield-address > legend { padding: 0; }
/* The display, padding and width reset the attendee card's section-heading
   legend (checkout_attendee.css reaches every legend inside the card). */
body.buyer-journey .namefield-address > legend { color: var(--fg); display: block; font-size: var(--bj-support); font-weight: 600; margin-bottom: .4rem; padding: 0; width: auto; }

/* shared/yesno_field.css */
/* yesno_field.css — the two answers of a yes/no question (yesno_field.html),
   and the one-of-three greeting question under a name (name_field.html),
   which wears the same cards so every one-of-a-few answer on a form looks
   alike (Bill, 2026-09-21).
   Both are cards rather than bare radios so the whole answer is the hit area,
   which is what makes this one tap on a phone; the native input stays visible
   because it is the thing that says "pick one of these", and because a
   checkmark drawn in CSS would disappear with the icon font.

   The pair sits on one row and wraps rather than shrinking: two answers never
   need more than half a line each, and a translation that does ("Ano"/"Ne" is
   short, "Ja, ich brauche eines" is not) is better on two rows than clipped.

   ONE component, two palettes — this partial renders in the admin AND on the
   buyer's own pages, and the buyer journey re-derives its colour from the
   ORGANIZER's palette (journey_tokens.css), where --accent is not the admin
   blue and --accent-weak is a wash of whatever the account chose. Styling
   against the admin's tokens alone put near-black on a grey tint with blue
   text on top of it. So the picked state is expressed once through local
   --yn-* and remapped below. */
/* max-width, because the admin's forms are far wider than two one-word
   answers: without it "Yes" and "No" each stretch to a third of the window
   and the pair reads as two buttons rather than one question's answers. The
   cap is generous enough for the longest translation at either end. */
.yesno { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 .2rem;
  /* 26rem off a buyer page (the admin's own forms), the column's full width
     on one: --bj-field is 100% there, because a box that stops short of the
     card's edge leaves a gutter beside it and the shell does not narrow to
     meet it. */
  max-width: var(--bj-field, 26rem);
  --yn-accent: var(--accent); --yn-line: var(--field-line); }
/* The side padding is the surrounding form's own text inset, so the radio
   starts where the words in the plain controls above it start — the same rule
   a choose-several answer follows, and for the same reason (check_group.css).
   .6rem is the admin's 1px + .6rem field; the buyer's .65rem is set below. */
.yesno .yn { align-items: center; border: 1px solid var(--yn-line); border-radius: var(--r-md, 7px);
  cursor: pointer; display: flex; flex: 1 1 8rem; gap: .45rem; margin: 0;
  padding: .5rem .6rem; }
.yesno .yn:hover { border-color: color-mix(in srgb, var(--yn-accent) 40%, var(--yn-line)); }
.yesno .yn > input { accent-color: var(--yn-accent); flex: none; margin: 0; }
.yesno .yn:has(input:checked) { border-color: var(--yn-accent); }
.yesno .yn:has(input:focus-visible) { outline: 2px solid var(--yn-accent); outline-offset: 2px; }

/* Admin: the accent ring plus the tint its choice cards already wear, so a
   picked answer reads the same as every other picked thing in there. Text on
   the tint is --accent-ink, never --accent (which misses AA on it in light —
   see colors.css and TestAccentOnTintIsInk). */
app-shell .yesno .yn:has(input:checked) { background: var(--accent-weak); }
app-shell .yesno .yn:has(input:checked) > span { color: var(--accent-ink); }

/* Buyer journey: the ring ALONE, following reg_tickets.css — no tint and no
   recoloured text. The palette is the organizer's, so we cannot know what a
   tint of it does to a word sitting on it; the ring carries the state at any
   hue, and the answer stays the page's own ink. */
body.buyer-journey .yesno { --yn-accent: var(--p-accent); --yn-line: var(--control-line); }
body.buyer-journey .yesno .yn { padding-inline: .65rem; }
/* The ring goes to ink with the dot. A picked card was carrying the
   account's colour on its edge AND in the middle, which is the same twice
   inside one answer; ink on the edge is a plain difference from the
   unpicked card's grey without the doubling, and it needs no second inset
   line to be seen. --yn-accent stays the account's colour on :focus-visible
   below, which is the one place here it says something. */
body.buyer-journey .yesno .yn:has(input:checked) { border-color: var(--fg); }
/* The dot: 15px, drawn, filled with the page's own ink — the same treatment
   a choose-several answer's tick takes (check_group.css), and for the same
   reason. journey_form.css sizes every buyer radio at 20px with
   accent-color, and at that size a saturated fill is a blob rather than a
   mark; it also spent the account's colour on the answer AND on the ring
   around it, twice inside one question. Ring keeps the colour, dot takes the
   ink. A ring with an inner dot, because that is what a radio looks like —
   the tick shape belongs to the checkbox. */
body.buyer-journey .yesno .yn > input[type=radio] {
  appearance: none; background: var(--journey-card, #fff); block-size: 15px;
  border: 1.5px solid var(--control-line); border-radius: 50%; cursor: pointer;
  inline-size: 15px; position: relative; }
body.buyer-journey .yesno .yn > input[type=radio]:hover {
  border-color: color-mix(in srgb, var(--fg) 45%, var(--control-line)); }
body.buyer-journey .yesno .yn > input[type=radio]:checked { border-color: var(--fg); }
body.buyer-journey .yesno .yn > input[type=radio]:checked::after {
  background: var(--fg); border-radius: 50%; content: ""; height: 7px; left: 50%;
  position: absolute; top: 50%; transform: translate(-50%, -50%); width: 7px; }

/* shared/phone_field.css */
/* A phone answer is two controls, one question: the country (its calling
   code) and the national number share a row. The select stays as narrow as
   its chosen option; the number input takes the room — it's the part being
   typed. min-width:0 lets both shrink inside narrow columns instead of
   overflowing them. */
/* The PAIR takes the form's measure, not each half of it: capping the two
   controls individually made one question twice as wide as every other field
   in the column (2026-08-21). The fallback keeps this component whole on the
   surfaces that have no --bj-field, where it stretches as it always did. */
.field-phone { display: flex; gap: .5rem; max-width: var(--bj-field, none); }
.field-phone select { flex: 0 1 auto; min-width: 0; max-width: 45%; }
.field-phone input { flex: 1 1 auto; min-width: 0; }
/* Inside the pair the halves are sized by the flex above, so neither may
   carry the column's cap of its own. */
body.buyer-journey form:not([data-checkout]) .field-phone :where(select, input) { max-width: none; }
body.buyer-journey form:not([data-checkout]) .field-phone select { max-width: 45%; }
/* With script the country is a combobox showing only the flag and the
   calling code (phone_field.html, select.js), so it takes a fixed,
   small width and the number gets everything else (Paul, 2026-09-24: the
   whole country name chopped the number box on a live widget). 7.75rem holds
   a flag, "+971" and the chevron, and the word for Country when nothing is
   picked yet. */
.field-phone > [rst-combo] { flex: 0 0 auto; width: 7.75rem; }

/* event/event_reset.css */
* { box-sizing: border-box; }
html,body { margin: 0; }
body { font-family: ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; }
button,input,select,textarea { font: inherit; }
/* The focus ring takes the ACCOUNT's own link colour (Vicky, 2026-08-21:
   "where's the bright yellow select highlight coming from?" — here, and it was
   a hardcoded #f5b700). One amber on every event page in the fleet, whatever
   the organizer's palette, which is why it read as something gone wrong rather
   than as a chosen colour: on this near-black card it was the loudest thing on
   the screen and it belonged to no part of the design.
   The link colour is the right source because it is already the account's
   "this is interactive" colour and already has to be legible on this card. The
   amber stays as the fallback for any surface that emits no palette at all.
   2px, not 3: three was compensating for a colour nothing else on the page
   agreed with. */
a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible {
  outline: 2px solid var(--custom-link-color,#f5b700);
  outline-offset: 2px;
}

/* event/event_page.css */
.event-banner { display: block; height: auto; object-fit: cover; width: 100%; }
.event-logo { display: block; object-fit: contain; }
.muted { color: var(--custom-text-color-muted); }
.grow { flex: 1; }
.price { font-variant-numeric: tabular-nums; }
.pill {
  border: 1px solid currentColor;
  border-radius: 99px;
  display: inline-block;
  font-size: .75rem;
  margin-left: .35rem;
  padding: .1rem .5rem;
  white-space: nowrap;
}

/* event/event_ticket_section.css */
/* ticket-section — one section of the purchase construct: a heading (or none)
   and the ticket-lists under it. Part of the public styling contract
   (docs/themes.md §The CSS contract).

   display: contents, not block, and that is the whole rule. The wrapper
   arrived to give a section a name a theme can select on, over markup that
   used to be flat; giving it a box would change the built-in page's layout
   for no reason, and this way the h2 and the lists inside stay exactly the
   children of the form they have always been. A theme that wants a real
   section box says so itself — a custom element has no default display, and
   that is the trade for the name being yours. */
ticket-section { display: contents; }

/* display: contents is still an author `display:`, and an author `display:`
   beats the UA's [hidden] rule — so a section the step wizard hides would
   stay on the page. Anything that gives one of these elements a box owes it
   this line (found 2026-08-10, when timed events showed their ticket rows
   under the date cards). */
ticket-section[hidden] { display: none; }

/* event/event_ticket_row.css */
/* ticket-row — one buyable line in the purchase construct, and part of the
   public styling contract (docs/themes.md §The CSS contract): name, price,
   quantity on one flex line. A custom element has no default display, so
   this rule is what makes the row a row; the Classic and Pro skins add
   their own rules, and each brings its own attribute states (`sold-out`,
   `addon`, `donation`). */
ticket-row { align-items: center; display: flex; gap: 1rem; }
/* The words column gives way before the price and the stepper do, and a word
   longer than it has room for breaks rather than pushing the row out. A
   ticket description holding a pasted link with a long token is one such
   word: without these it ran 385px past a 390px phone's row, on the event
   page and in the widget's sheet alike (Fable review, 2026-09-29;
   TestEmbedWidgetUI, "shows its header without moving the list"). */
ticket-row > .grow { min-width: 0; overflow-wrap: anywhere; }
/* A ticket's description keeps the organizer's own line breaks, as a
   package's details already do (portal_package.css): a list typed one item a
   line ran together into one paragraph (Smashing Conf, 2026-10-07). pre-line
   and not pre-wrap, so the spaces between words still collapse. Zero weight,
   so their own [data-ticket-description] rule wins. */
:where([data-ticket-description]) { white-space: pre-line; }
/* A formatted description's bullets (PRO-5788, ticket_description.go). The
   list sits inside a ticket row that is often itself a list item with its
   markers off, and list-style is inherited, so the disc is said here. Zero
   weight, like the line breaks above, so their own rule still wins. */
:where([data-ticket-description]) :where(ul) { display: block; list-style: disc outside; margin: .25em 0; padding-left: 1.25em; }
:where([data-ticket-description]) :where(li) { display: list-item; }
/* The money aligns RIGHT inside its own cell, and it is the tax caption that
   makes this load-bearing rather than a nicety (Vicky, 2026-08-10). The cell
   is pushed flush right by .price's auto margin, so its right EDGE already
   lines up down the list — but its WIDTH is set by its widest line, and once
   a second line is in there that line is the caption, not the price. Rates
   have names of different lengths ("VAT @ 23%", "+ Sales tax @ 88.75%"), so
   every row got a differently-sized cell with the price sitting at the left
   of it: three prices, three starting positions, none of them lined up. The
   captions were the only thing on the page that WAS aligned.
   Not on the donation row: its .price is a flex COLUMN (below) whose items
   are already flush right, so text-align has nothing to align — the currency
   mark and the amount box sit on their own line inside it. */
ticket-row:not([donation]) .price { text-align: right; }
/* The caption is a note ABOUT the price, never a second price, and until now
   it had no rule of its own at all — it took whatever .muted happened to
   give it, which on the Pro skin is .875rem, the same step the ticket NAME
   beside it is set in. .75rem is small print without being fine print —
   .72rem, the smallest step on this page, was a step too far (Vicky,
   2026-08-10) and it is a rate a buyer may want to read; the weight is spelled out
   because .price carries 600 in the shared sheet and the Pro skin's own
   reset is the only reason the caption isn't bold today.

   Specificity is load-bearing: the element is class="muted tax-note", and
   the Pro skin's `& .muted` ties with a lone `.tax-note` and wins on order.
   Naming the cell it sits in settles it for both skins at once. */
ticket-row .price .tax-note { font-size: .75rem; font-weight: 400; }
/* A LONG CAPTION WRAPS UNDER ITS PRICE; it never sets the row's widths
   (PRO-5510). A rate called "German VAT 19% (Clay Studio Workshops)" made a
   caption wider than the name beside it, and the name gave way instead: one
   word a line on a laptop, one LETTER a line on a phone. So the caption has
   a measure of its own, balanced over its lines, and a row that carries one
   keeps the rest of the line for its words. Where the form is too narrow for
   the three to share a line, the price and the stepper go under the name,
   together at the right. A flex wrap was tried first and left the stepper
   alone on a line of its own.

   One element of weight, so it wins over the plain row above and loses to a
   layout that lays the row out itself (the Cards' grid, design_layout.go). */
ticket-row .price .tax-note { display: inline-block; max-inline-size: 12rem; text-wrap: balance; }
ticket-row:where(:not([donation]):has(> .price .tax-note)) {
  display: grid;
  gap: .5rem 1rem;
  grid-template-areas: "name price qty";
  grid-template-columns: minmax(0, 1fr) auto auto;

  & > .grow { grid-area: name; }
  & > .price { grid-area: price; }
  & > .qty { grid-area: qty; }
}
@container not (min-width: 34rem) {
  ticket-row:where(:not([donation]):has(> .price .tax-note)) {
    grid-template-areas: "name name" "price qty";
    grid-template-columns: minmax(0, 1fr) auto;
  }
}
/* A donation row's tick is how a buyer says yes, since an amount box has no
   quantity to raise off zero. It takes the QUANTITY column — after the
   amount, centred on the width of a stepper's + (2rem, .qty button.step in
   event_qty.css) — so the control that turns a row on is in the same place
   down the whole list, whether it's a + or a tick (Vicky, 2026-08-09). The
   classic skin's buttons are smaller and it says so itself.

   Sized like every other tickbox on a buyer page (journey_form.css): the
   browser default is 13px, and this one is a control a sale can turn on. The
   event page carries no .buyer-journey class, so that rule can't reach here
   and the recipe is repeated rather than shared. */
ticket-row[donation] .qty { inline-size: 2rem; justify-content: center;
  /* Two edges have to land at once: the tick centred on the + above it, and
     the amount box's right edge on the right edge of the quantity boxes
     above it (Vicky, 2026-08-09). The tick's own cell is the + wide, so the
     only thing in the way is the row's 1rem gap — swapped here for the .35rem
     the quantity column uses between its own controls (event_qty.css), which
     is exactly the space a + sits behind. */
  margin-inline-start: calc(.35rem - 1rem); }
ticket-row[donation] .qty > input[type="checkbox"] { accent-color: var(--p-accent, currentColor);
  block-size: 1.25rem; inline-size: 1.25rem; margin: 0; }
/* Put the tick back on the BOX when the row also carries a tax note (Vicky,
   2026-08-23: "the checkbox alignment looks off here"). It was 10px low, and
   only on this one row shape. The row centres its cells; a donation's .price
   is a flex COLUMN, so once a note joins the amount box that cell is two lines
   tall and its centre is no longer the box's centre — while the tick, being
   one line, sat on the cell's centre. The two controls a buyer uses together
   stopped lining up, and every other row on the page was unaffected, which is
   what made it read as a wonky tickbox rather than a layout rule.

   A bottom margin rather than align-self plus a height: with align-items:
   center the margin is part of what gets centred, so the tick rises by half of
   it, and half of the note is exactly the error. It is also the only version
   that needs no guess at the input's height — that comes from the skin's own
   padding and differs between Pro and Classic, whereas the note's own metrics
   are set right here: .75rem at the page's 1.5 line-height, plus this column's
   .15rem gap. */
ticket-row[donation]:has(.tax-note) .qty { margin-block-end: calc(.75rem * 1.5 + .15rem); }
/* A rate on a donation is added to whatever the buyer types, so the row says
   so — and it says it UNDER the amount box, not beside it (Vicky,
   2026-08-10), which is where every other row already puts its tax note
   (the fixed-price branch of the same template uses <br>). The box has no
   price text to sit beside, only an input, so an inline note read as part of
   the control.

   The symbol and the box moved into their own .donation-amount row so this
   can be a plain flex COLUMN, and the inline style that used to sit on
   .price came with them. Wrapping was tried first and is a dead end worth
   recording: the note is wider than the box, and `flex-basis: 100%` on it
   resolves against the very width it is trying to set, so the column
   collapsed to the box's width and pushed the currency symbol onto a line of
   its own. A column has no such circularity.

   align-items keeps the promise the tick rule below depends on — the box's
   right edge stays on the right edge of the quantity boxes above it, even
   though the note is wider than the box and grows the column. */
ticket-row[donation] .price { align-items: flex-end; display: flex;
  flex-direction: column; gap: .15rem; }
ticket-row[donation] .price .donation-amount { align-items: center; display: flex; gap: .4rem; }
/* The amount box's hint is a FORMAT, not an amount (Vicky, 2026-08-09: "the
   placeholder really looks like it's already populated" — and it did, because
   the pro skin painted it in the full text colour at full opacity, so a
   suggested 7.00 was indistinguishable from a typed one). The suggestion is
   real text on the row now, so the hint can read as what it is; what it must
   never do is look like money already given. */
ticket-row[donation] .price input::placeholder {
  color: color-mix(in srgb, var(--custom-text-color, currentColor) 45%, var(--custom-text-background-color, transparent));
  opacity: 1;
}

/* event/event_ticket_waitlist.css */
/* ticket-waitlist — the way onto a fully booked ticket's waitlist, under its
   description (waitlist_public.go), and part of the public contract
   (docs/themes.md). It is the one thing a sold-out row still lets somebody
   do, so it is drawn as the ACTION it is: the outlined button a placed piece
   wears in this skin (event_pro.css, a.button:not(.primary)), at the row's
   smaller size, in the place a ticket on sale has its quantity stepper:
   it is what you do with this ticket instead of choosing how many.

   It used to be a bold link in the organizer's link colour straight under
   the description (PRO-5510). On the Clay Studio demo, whose own stylesheet
   gives `ticket-waitlist a` a wavy underline, that read as a misspelt word in
   the description rather than as something to press. A box carries no
   underline at all, so no organizer decoration can make it read as prose.

   The Classic skin sets none of these colours (its card is literal white on
   any palette); event_classic.css gives it Classic's own. */
ticket-waitlist { display: block; }
ticket-waitlist a {
  align-items: center;
  border: 1px solid var(--custom-outlined-button-color, var(--custom-control-border, currentColor));
  border-radius: var(--custom-button-radius, 6px);
  color: var(--custom-outlined-button-color, var(--custom-link-color, currentColor));
  display: inline-flex;
  font-size: .875rem;
  font-weight: 600;
  line-height: 1.4;
  padding: .45rem .9rem;
  text-decoration: none;
}
ticket-waitlist a:hover {
  border-color: var(--custom-button-background-color, currentColor);
  color: var(--custom-button-background-color, currentColor);
}

/* event/event_qty.css */
.qty { align-items: center; display: flex; gap: .35rem; }
.qty input { text-align: center; width: 3rem; }
.qty input.stepped { -moz-appearance: textfield; appearance: textfield; }
.qty input.stepped::-webkit-inner-spin-button,.qty input.stepped::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.qty button.step {
  align-items: center;
  border: 1px solid currentColor;
  /* A stepper is a button, so it wears the organizer's Button shape: round
     under Pill, square under Square (Vicky, 2026-10-09: "pills here
     shouldn't have square edges" — the steppers were the one button on the
     page still following the card's corner). The card's corner, capped at
     8px, is only the fallback for a page that sets no shape. */
  border-radius: var(--custom-button-radius, min(var(--custom-card-border-radius, 8px), 8px));
  cursor: pointer;
  display: inline-flex;
  height: 2rem;
  justify-content: center;
  margin: 0;
  padding: 0;
  /* Rapid +/- taps must never read as a double-tap-to-zoom gesture;
     manipulation keeps pinch-zoom, so accessibility zoom is unharmed. */
  touch-action: manipulation;
  width: 2rem;
}
.qty button.step:disabled { cursor: default; opacity: .32; }

/* event/event_fields.css */
.checkout-fields label {
  display: block;
  font-size: .875rem;
  font-weight: 600;
  margin: .8rem 0 .35rem;
}
/* A tickbox question is a row, box then words (check_label.css): the block
   above, coming later at the same weight, sat the box above its words with
   only a space after it. Its room above is more than a label's: a label's
   own leading pads the gap under a field, and a box has none, so the same
   .8rem read as cramped under a bordered list (Vicky, 2026-10-13). */
.checkout-fields label.check { align-items: center; display: flex; gap: .55rem; }
/* Not on a choose-several answer, which is a label.check too: its rows sit
   flush on their dividers (check_group.css). */
.checkout-fields label.check:not(.cg-opt) { margin-block-start: 1.25rem; }
.checkout-fields input:not([type=checkbox]),.checkout-fields select,.checkout-fields textarea {
  width: 100%;
}
.checkout-fields textarea { min-height: 7rem; }
.checkout-button {
  border: 0;
  /* The account's own button shape (design.go's ButtonRadiusCSS), with the
     old fixed 10px as the fallback for any surface that does not emit the
     token. Square and pill are choices from 2026-08-21; before that every
     button on every buyer page wore a number somebody typed once. */
  border-radius: var(--custom-button-radius, 10px);
  cursor: pointer;
  font-size: .95rem;
  font-weight: 600;
  padding: .7rem 1.2rem;
}
.checkout-button.busy {
  color: transparent!important;
  pointer-events: none;
  position: relative;
}
.checkout-button.busy:after {
  animation: spin .6s linear infinite;
  border: 2px solid var(--custom-button-text-color,#fff);
  border-radius: 50%;
  border-top-color: transparent;
  content: "";
  height: 1rem;
  inset: 0;
  margin: auto;
  position: absolute;
  width: 1rem;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
.schedule-group h3 { margin: 1.5rem 0 .25rem; }
.flow-switch { font-size: .875rem; }

/* The quote action under the checkout button: a link that happens to submit
   (2026-08-21). Centred, quiet, underlined only on hover — the same recipe
   the journey's own back links use, so a secondary action reads the same way
   wherever a buyer meets one.

   The gap above it is 1.6rem rather than the .9rem it shipped with (Vicky,
   2026-09-07: "it's quite easy to accidentally hit the generate quote link").
   At .9rem the link sits inside the button's own reach on a touch screen —
   a 14px target roughly a fingertip below a full-width primary — and the
   whole point of this control's dress is that pressing it is a decision, not
   a slip. */
.quote-instead {
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  display: block;
  font: inherit;
  font-size: .875rem;
  margin: 1.6rem auto 0;
  opacity: .8;
  padding: .25rem .5rem;
  text-decoration: none;
}
/* `display:block` above outranks the user agent's `[hidden] { display: none }`,
   so without this the attribute is set and nothing happens — which is exactly
   how the checkout wizard spent a fortnight standing this link "down" in full
   view of the buyer. Any rule that gives an element a display must say what
   hidden means for it. */
.quote-instead[hidden] { display: none; }
.quote-instead:hover { opacity: 1; text-decoration: underline; }
.quote-instead:focus-visible { border-radius: 4px; outline: 2px solid currentColor; outline-offset: 2px; }

/* event/event_summary.css */
/* The order summary drawer: the Total line is a real disclosure (caret, no
   default marker), the breakdown reads as a receipt — name left, tabular
   price right, hairline between the drawer and the list above it. */
.summary { border-top: 1px solid var(--divider-color,#e1e1e1); margin-top: .5rem; }
.summary summary { align-items: center; cursor: pointer; display: flex; gap: 1rem; padding: 1rem 0; }
.summary summary { font-weight: 600; list-style: none; }
.summary summary::-webkit-details-marker { display: none; }
.summary summary .price { font-weight: 600; }
.summary [data-breakdown] { padding: 0 0 .9rem 1.1rem; }
.summary [data-breakdown] .row {
  align-items: baseline;
  display: flex;
  font-size: .92rem;
  gap: 1rem;
  opacity: .75;
  padding: .12rem 0;
}
.summary [data-breakdown] .row .price { margin-left: auto; }
.summary [data-breakdown] .row.sub { font-size: .85rem; opacity: .6; padding: 0 0 .12rem 1rem; }

/* event/event_lang_switch.css */
.lang-switch { font-size: .85rem; position: absolute; right: 1rem; top: 1rem; z-index: 30; }
.lang-switch>summary {
  align-items: center;
  background: var(--custom-text-background-color,#fff);
  border: 1px solid var(--custom-control-border,#d1d1d1);
  border-radius: 99px;
  color: var(--custom-text-color,#252535);
  cursor: pointer;
  display: flex;
  gap: .35rem;
  list-style: none;
  padding: .35rem .8rem;
}
.lang-switch>summary::-webkit-details-marker { display: none; }
.lang-switch>summary>svg { flex: none; }
/* The house .caret (shared/caret.css, loaded ahead of this file) colours
   itself with --text-faint, which this document has no palette for — it is
   its own document with its own --custom-* tokens. So the caret inherits the
   summary's text colour, and the opacity is the dimming the glyph used to
   carry. margin-left goes because the summary sets its own rhythm with gap. */
.lang-switch>summary>.caret { margin-left: 0; opacity: .6; }
.lang-switch>nav {
  background: var(--custom-text-background-color,#fff);
  border: 1px solid var(--custom-control-border,#d1d1d1);
  border-radius: .5rem;
  box-shadow: 0 6px 20px rgb(0 0 0/.25);
  color: var(--custom-text-color,#252535);
  display: flex;
  flex-direction: column;
  margin-top: .35rem;
  min-width: 10rem;
  padding: .3rem;
  position: absolute;
  right: 0;
}
.lang-switch>nav :is(a,[aria-current]) {
  border-radius: .3rem;
  color: inherit;
  padding: .35rem .55rem;
  text-decoration: none;
  white-space: nowrap;
}
.lang-switch>nav a:hover { background: color-mix(in srgb,currentColor 8%,transparent); }
.lang-switch>nav [aria-current] { font-weight: 700; }

/* The line for a visitor whose chosen language this event does not offer. It
   sits under the switcher rather than beside it, because the switcher is
   absolutely positioned and this has to push the page down — a notice that
   overlapped the banner would be read as part of the artwork. Quiet by
   design: it explains, it does not ask for anything. */
.lang-unoffered {
  color: var(--text-muted, #6b6b76);
  font-size: .85rem;
  margin: 0 0 .75rem;
  padding-right: 11rem;
}

/* On a phone the switch stops floating and takes a line of its own, above the
   card. Same change, same reason, as the buyer journey's copy of this control
   (styles/public/lang_switch.css) — this document is its own bundle with its
   own tokens, so the rule is stated twice rather than shared.

   Absolute is right on a wide screen: the control is chrome about the page
   rather than part of it, and the corner it sits in is empty. On a phone that
   corner is NOT empty — stub-band opens the page with the organizer's own
   name in it — and an out-of-flow pill over the top reserves no width, so the
   name simply ran underneath. "Northwind Software Conferences" rendered as
   "Northwind Software C" on the first screen a buyer sees, 87px of it painted
   under the pill and clipped by the card (2026-08-28). Nothing in the band can
   shrink to avoid it, because nothing in the band knows it is there.

   .lang-unoffered's `padding-right: 11rem` above is the same collision
   answered once, for one line, with a number. In flow the width is reserved
   by definition and no caller has to know it. */
@media (max-width: 599px) {
  .lang-switch {
    margin: 0 1rem .55rem auto;
    /* relative, NOT static: the open panel is `position: absolute; right: 0`
       against this element, so taking the switch out of the overlay must not
       also take away the box that panel is anchored to — static would send it
       to the nearest positioned ancestor, which is the whole page shell. It
       still lays out exactly where a static box would; all it keeps is the
       containing block. */
    position: relative;
    width: max-content;
  }
  /* …and with the switch out of that corner, the notice under it has no
     overlay left to clear. */
  .lang-unoffered { padding-right: 0; }
}

/* The currency switch (layout.html currency-switch) wears these clothes too,
   and a page with both stacks the second pill under the first: they share
   the corner, and side by side would put two variable-width pills in a
   fight for it. In flow on a phone they simply follow each other.

   Wide screens only, and that is the whole point of the query rather than
   tidiness: on a phone the pills are `position: relative` (above), and `top`
   moves a relative box just as readily as an absolute one. Unqualified, the
   3.25rem shifted the second pill 44px clear of its own flow position and
   dropped it straight onto the panel above it — the same collision this rule
   exists to prevent, arriving from the other direction. */
@media (min-width: 600px) {
  .lang-switch ~ .lang-switch { top: 3.25rem; }
}

/* …and 3.25rem is exactly where the first switcher's open panel begins
   (`> nav` is absolute, margin-top .35rem under a pill that is ~2.1rem
   tall). Both pills carry z-index 30, so paint order fell to DOM order and
   the LATER one drew over the earlier one's open menu: at 1440px the open
   language nav ran y 94-232 with the currency pill sitting at y 91-125 on
   top of it, and the first entry in the list could not be clicked at all.
   Past langSearchMin languages the child it covered was the type-to-find
   field rather than a language.

   So an open switcher outranks every pill that follows it. Written as "the
   ones after an open one drop" rather than "the open one rises" because it
   holds when both are open too: the panel a person is actually reading is
   the earlier one, and raising both would put the currency pill back on top
   of it. 29 is still above everything else either bundle draws (venue.css
   and cal_menu.css are the next ones down, at 20).

   The switchers also share a `name` (layout.html), so opening one closes the
   other and two panels are not normally on screen at once. This is what the
   corner does when that is unavailable — an older browser with no exclusive
   accordion — and it is CSS, so it holds with scripting off, which is how
   these controls work in the first place. */
.lang-switch[open] ~ .lang-switch { z-index: 29; }

/* event/event_pay_by_transfer.css */
/* .transfer-instead — Pay by bank transfer, the second way on under the
   checkout button (transfer_start.go). Its [data-pay-by-transfer] is part of
   the public contract (docs/themes.md). It replaced the customer sign-in line (Doc, 2026-10-13).

   A way on, so a button the size and shape of the checkout button above it,
   in the outlined dress (the house's quieter button, as on a placed piece's
   second button): no fill, an edge, the text colour. It is a real choice
   beside the card, never a link under it. The gap above it is the stacked
   footer's own, so the pair read as two buttons and the quote link under
   them as the third, quieter way on.

   Every state, because the shared button rule paints its fill, edge and lift
   on :hover and nudges it on :active, with more specificity than one class
   has on its own. */
.transfer-instead,
.transfer-instead:hover,
.transfer-instead:active {
  background: transparent;
  border: 1px solid var(--custom-outlined-button-color, var(--custom-control-border, currentColor));
  border-radius: var(--custom-button-radius, 10px);
  box-shadow: none;
  color: var(--custom-outlined-button-color, inherit);
  cursor: pointer;
  display: block;
  font: inherit;
  font-size: .95rem;
  font-weight: 600;
  margin: .75rem 0 0;
  padding: .7rem 1.2rem;
  transform: none;
  width: 100%;
}
.transfer-instead:hover { border-color: var(--custom-button-background-color, currentColor); }
/* Its display above outranks the user agent's [hidden] (the quote link's
   trap, event_fields.css), so hidden says what it means here. */
.transfer-instead[hidden] { display: none; }
.transfer-instead:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
/* Pressed, it is spinner.js's busy button like the checkout button above, but
   nothing on this page drew its ring: event_fields.css draws only
   .checkout-button's, and the shared button_busy.css is not in this bundle.
   So the button went blank and disabled with nothing in its place (Doc,
   2026-10-14). The label goes by its FILL, not its colour, so `color` keeps
   the ink the words were in and the ring is drawn in it with currentColor —
   which also follows an account stylesheet that repaints the button. */
.transfer-instead.busy {
  -webkit-text-fill-color: transparent;
  pointer-events: none;
  position: relative;
}
.transfer-instead.busy::after {
  animation: spin .6s linear infinite;
  border: 2px solid currentColor;
  border-radius: 50%;
  border-top-color: transparent;
  content: "";
  height: 1rem;
  inset: 0;
  margin: auto;
  position: absolute;
  width: 1rem;
}

/* event/event_order_size.css */
/* order-size — "Each order needs 2 to 6 tickets", the event's limits on one
   order (order_size.go), first thing in the checkout button's band, so the
   band's padding places it in every layout. Part of the public contract
   (docs/themes.md). Said before the buyer chooses rather than
   only when they are refused, so it reads as the page's own small print:
   muted and a step smaller than the rows above it. The widget's copy is
   styles/public/order_size.css, against that page's palette. */
order-size {
  color: var(--custom-text-color-muted);
  display: block;
  font-size: .875rem;
  margin-block: 0 .75rem;
}
/* Shown only while the buyer is outside the limits (order-size.js), so the
   display above must not reopen a line the page has hidden. */
order-size[hidden] { display: none; }

/* event/event_signup.css */
/* signup-form — a free event's one ticket, asked for in place of the picker
   (signup_form.go, theme_blocks.html's "signup-form"). The ticket row above it
   is the list's own row; this file only gives the two wrappers a box and
   places "RSVP for multiple people".

   The boxes take .checkout-fields' label and input rules (event_fields.css)
   through the class on <signup-fields>, so a question reads the same here as
   anywhere else on the page.

   The link is centred and small under a full-width button, far enough below
   it that a thumb meant for Confirm does not land on it, in the organizer's
   link colour so it reads as the thing to press. */
signup-fields,
signup-form { display: block; }
/* THE QUESTIONS' TOKENS. A name, a yes/no and a phone number are drawn by the
   shared partials the details step uses, and their sheets (name_field.css,
   yesno_field.css, phone_field.css, borrowed in styles.go) are written
   against the buyer journey's tokens (journey_tokens.css). This standalone
   page has none of those, only its own --custom-* palette, and an undefined
   var() drops the whole declaration: the name's disclosure grew a second
   marker and the yes/no cards lost their boxes. So the form maps the one
   palette onto the other, here and nowhere else on the page. */
signup-form {
  --accent: var(--custom-link-color, currentColor);
  --accent-ink: var(--custom-button-text-color, #fff);
  --accent-weak: color-mix(in srgb, var(--custom-link-color, currentColor) 10%, transparent);
  --bj-field: 100%;
  --bj-radius-sm: min(var(--custom-card-border-radius, 8px), 8px);
  --bj-support: .875rem;
  --control-line: var(--custom-control-border);
  --fg: var(--custom-text-color);
  --field-line: var(--custom-control-border);
  --hairline: color-mix(in srgb, var(--custom-control-border) 65%, transparent);
  --journey-card: var(--custom-text-background-color, transparent);
  --journey-link: var(--custom-link-color, currentColor);
  --line: var(--custom-control-border);
  --muted: var(--custom-text-color-muted);
  --ok-ink: var(--custom-text-color);
  --p-accent: var(--custom-link-color, currentColor);
  --r-md: min(var(--custom-card-border-radius, 8px), 8px);
  --r-sm: min(var(--custom-card-border-radius, 6px), 6px);
}
signup-fields { margin-block-start: .5rem; }
/* This page's own field rules are for a box somebody types in: every label a
   block in bold with room above, every input but a tickbox full-width with a
   border and padding (event_fields.css, event_pro.css). A yes/no answer is
   neither. Its card is a label and its radio an input, so the radio stretched
   across the card and pushed "Yes" out past its edge. The card and its radio
   keep the yes/no's own shape. */
signup-fields .yesno .yn { font-weight: 400; margin: 0; }
signup-fields .yesno .yn > input {
  border: revert;
  box-shadow: none;
  padding: 0;
  width: auto;
}
/* A tickbox question and a choose-several answer are drawn the way the
   details step draws them (check_label.css, check_group.css). Those sheets
   are not borrowed: they reach for the corner scale, which event.css does
   not carry (radius_scale_test.go), so the few rules this form needs are
   here, on the tokens mapped above. Without them a box sat above its words
   with only a space after it (Vicky, 2026-10-13). */
signup-fields label.check.check-field { color: var(--muted); font-size: .85rem; }
signup-fields .field-desc { color: var(--muted); display: block; font-size: .8rem; font-weight: 400; }
signup-fields p.field-desc { margin: -.1rem 0 .3rem; }
signup-fields label .field-desc { margin: .15rem 0 0; }
signup-fields label.check:has(.field-desc) { align-items: flex-start; }
signup-fields label.check:has(.field-desc) > input { margin-top: .3rem; }
signup-fields .checkgroup {
  border: 1px solid var(--field-line);
  border-radius: var(--r-md);
  display: flex;
  flex-direction: column;
  margin: 0 0 .2rem;
  overflow: hidden;
}
signup-fields .checkgroup .cg-opt {
  align-items: center;
  border-top: 1px solid var(--line);
  cursor: pointer;
  display: flex;
  gap: .55rem;
  margin: 0;
  padding: .6rem;
}
signup-fields .checkgroup .cg-opt:first-child { border-top: 0; }
signup-fields .checkgroup .cg-opt:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
signup-fields .checkgroup .cg-opt > input { flex: none; margin: 0; }
signup-fields .checkgroup .cg-opt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The fold under a name ("How should we read this name?") is empty until a
   name is typed, and its top margin still opened a gap under the box: the
   name sat 7px further from the email than every other question from the
   next. The space goes on its summary instead, which exists only when shown. */
signup-fields .namefield-details { margin-block-start: 0; }
signup-fields .namefield-details > summary { margin-block-start: .45rem; }
.signup-several {
  font-size: .875rem;
  margin-block: 1.6rem 0;
  text-align: center;
}
.signup-several a { color: var(--custom-link-color, currentColor); font-weight: 600; }
.signup-several a:focus-visible { border-radius: 4px; outline: 2px solid currentColor; outline-offset: 2px; }

/* event/event_owner_notes.css */
/* The organizer's notes' lift on the standalone event page (owner_strips.css
   draws the notes). That file asks for the elevation token, --shadow-pop,
   which public.css carries and this bundle does not: the event page runs on
   the account's --custom-* palette and has no tokens of ours. Written out
   here, under event/, where a literal is the house rule rather than a slip,
   and the same pale shadow on any account palette, because the note is
   Tito's and never the event's. */
owner-notes > :is(.secret-preview, .owner-strip, .page-preview) {
  box-shadow: 0 8px 24px rgb(16 24 40 / .16), 0 2px 6px rgb(16 24 40 / .08);
}

/* event/event_stub_band.css */
/* ---- The stub band, on the event page ----
   The torn edge is the buyer journey's ONE unifying element (Vicky,
   2026-08-21), and the event page is screen one. It had no band at all, so a
   buyer met the signature for the first time on the screen AFTER the one they
   arrived on — which is the opposite of a signature.

   Its own file rather than a reach into styles/public/: event.html is a
   standalone document with its own bundle and none of the --bj-* tokens, so
   the rules are the same shape written against the --custom-* palette the
   page really has. Two copies of a small thing, and the alternative is the
   whole journey token set loading on a page that uses six of it.

   The band takes the CARD's colour, like its sibling: a stub is the same
   paper as the ticket, told apart by the perforation and nothing else. */
.pro-card > stub-band {
  align-items: center;
  background: var(--custom-text-background-color);
  display: flex;
  gap: 1rem;
  justify-content: space-between;
  padding: 1rem;
  position: relative;
}
/* The line under it: a hairline by default, a perforation when the torn edge
   is on. Inset from the corners so it never crosses the radius. */
.pro-card > stub-band::after {
  border-bottom: 1px solid var(--divider-color);
  bottom: -1px;
  content: "";
  left: 10px;
  position: absolute;
  right: 10px;
}
.pro-card > stub-band[torn]::after { border-bottom-style: dashed; border-bottom-width: 1.5px; }
/* A band with nothing in it (Bill, 2026-09-18): the tear under a banner
   with no logo and the account name switched off. No height of its own, so
   the perforation and the notches sit on the foot of the picture. */
.pro-card > stub-band[bare] { min-height: 0; padding-block: 0; }
/* Laid ON the picture's last pixels rather than hung a pixel below them:
   below, a light dash on the white card all but disappears, which is what it
   did under the hero (Bill, 2026-09-18). */
.pro-card > stub-band[bare]::after { bottom: 0; }
.pro-card > stub-band > .sb-l { min-width: 0; }
.pro-card > stub-band > .sb-l > img { display: block; max-height: 30px; max-width: 118px; object-fit: contain; }
/* A definite width on the slot, so a viewBox-only SVG — an image with no
   intrinsic size — has a containing block to resolve against instead of
   collapsing to 0x0 in a shrink-to-fit flex item. The whole reasoning is on
   the same rule in styles/public/stub_band.css; this is the second copy the
   file header explains. */
.pro-card > stub-band > .sb-l:has(> img) { width: 118px; }
/* The account's name, not the event's: the event's name is the h1 six
   millimetres below, and a band that repeated it would be the second
   masthead this page was trying not to have.

   The heading ink rather than the link colour (Vicky, 2026-08-24). It stopped
   being an anchor to the account's event listing, because a way out of the
   flow does not belong in the masthead of the page a buyer is choosing
   tickets on — and a name still painted in the organizer's link colour is a
   link that has been switched off rather than a mark of who is selling. */
.pro-card > stub-band > .sb-l > .sb-name {
  color: var(--custom-heading-color);
  font-size: .95rem;
  font-weight: 600;
}
/* Half discs flush with the card's outer edge, for the reason the journey's
   are (styles/public/stub_band.css): a full one straddling the edge paints
   the page colour OVER the card's shadow and reads as a sticker rather than
   a hole. Rendered only where the card is flat — Design.Notched decides, in
   Go, once, for both pages. */
.pro-card > stub-band > .sb-notch {
  background: var(--custom-background-color);
  block-size: 20px;
  bottom: -10px;
  inline-size: 10px;
  position: absolute;
  z-index: 2;
}
.pro-card > stub-band > .sb-notch.l { border-radius: 0 10px 10px 0; left: -1px; }
.pro-card > stub-band > .sb-notch.r { border-radius: 10px 0 0 10px; right: -1px; }
/* The band bleeds to the card's edges, so the content below cannot also pad
   above it — the page's own top padding belongs to the band now. */
.pro-card:has(> stub-band) > .pro-content { padding-top: 1.6rem; }
@media (max-width: 599px) {
  .pro-card > stub-band > .sb-notch { display: none; }
}
/* The card's own step up at 600px (event_pro.css's .pro-content), so the band
   and the content it sits over share one inset. */
@media (min-width: 600px) {
  .pro-card > stub-band { padding: 1.15rem 2rem 1.1rem; }
}

/* event/event_addons.css */
/* Attached add-ons — the rows nested under one ticket type. The enhancement
   gate: with JS a row starts hidden and addon-rows.js reveals it while its
   ticket is selected; without JS every row renders and checkout validates. */
.js [data-addon-for] { display: none; }
/* A revealed row says what it becomes, never `revert`: the reveal lands on
   the <li> in the Pro skin and on <ticket-row> itself in Classic, and
   reverting a custom element gives you `inline`, which collapses the row. */
.js li[data-addon-for].addon-on { display: list-item; }
.js ticket-row[addon].addon-on { display: flex; }
@media (prefers-reduced-motion: no-preference) {
  .js [data-addon-for].addon-on { animation: addon-in .2s ease; }
}
@keyframes addon-in {
  from { opacity: 0; transform: translateY(-4px); }
}
ticket-row[addon] {
  & .grow label { font-size: 1rem; }
  & .price { font-size: 1rem; }
}
.addon-pill { font-size: .75rem; opacity: .8; padding: .1rem .45rem; vertical-align: .1em; }
.addon-desc { font-size: .875rem; }
.ticket-includes { color: var(--custom-text-color-muted); font-size: .875rem; }
/* When this ticket happens, where it happens at a time of its own
   (PRO-4672). It reads as the row's own line rather than a section above the
   list, so it takes the same quiet treatment as what a ticket includes. */
.ticket-when { color: var(--custom-text-color-muted); font-size: .875rem; }

/* event/event_promo.css */
.promo-box { margin: .9rem 0 .2rem; }
.promo-entry summary {
  align-items: center;
  cursor: pointer;
  display: flex;
  font-size: .9rem;
  font-weight: 500;
  gap: .4rem;
  list-style: none;
  opacity: .75;
  padding: .45rem 0;
  width: fit-content;
}
.promo-entry summary::-webkit-details-marker { display: none; }
.promo-entry summary:hover,.promo-entry[open] summary { opacity: 1; }
.promo-caret { flex: none; transition: transform .15s ease; }
.promo-entry[open] .promo-caret,.summary[open] .promo-caret { transform: rotate(90deg); }
/* The companion line: advisory or required, it reads as a quiet note — never
   a validation error. A REQUIRED rule speaks before it blocks, so its resting
   state is this exact styling; only a refused checkout adds .bad below. */
.recommend-hint {
  align-items: center;
  border-left: 3px solid var(--divider-color);
  color: var(--custom-text-color-muted);
  display: flex;
  flex-wrap: wrap;
  font-size: .875rem;
  gap: .6rem;
  margin: .6rem 0;
  padding: .35rem 0 .35rem .7rem;
}
/* display:flex above would beat the UA's [hidden]{display:none} — equal
   specificity, and an author rule always wins over the UA sheet — so the
   line stayed on screen however correctly the server and the JS set the
   attribute. Every nudge on this page needs this (.promo-chip/.promo-entry
   below already learned it the same way). */
.recommend-hint[hidden] { display: none; }
/* The sentence takes the row's spare width so the badge sits beside its
   first line rather than alone above it, and the Add control drops below
   only when there is genuinely no room for it. */
.recommend-hint > [data-companion-words] { flex: 1 1 16rem; }
/* Refused: the order reached checkout short of a required companion. The
   page's own error colour rides the rule and the badge while the WORDS go to
   full text colour.
   That split is unchanged, but one of the two reasons recorded for it is not:
   "red text over an account-chosen background can't be held to AA" stopped
   being true on 2026-09-08, when the red started being measured against the
   card (eventDesign.DangerInk). What keeps the words black is the other
   reason, which was always the better one — a sentence a buyer has to read is
   not the place to spend a colour, and the edge beside role="alert" already
   carries the state. */
.recommend-hint.bad {
  border-left-color: var(--danger-ink, #c22f3d);
  color: var(--custom-text-color);
}
/* Decorative — the sentence beside it says everything. Drawn in CSS so no
   module has to build markup, and so it can't survive the state changing.
   The glyph is knocked out in the CARD's own colour rather than white, which
   is what makes it legible on a fill that is no longer a fixed red: the fill
   clears 4.5:1 against the card by construction, so glyph-on-fill is that
   same ratio and can never be the pale-red-on-pink a second literal would
   have become. On the default white card it is white, exactly as before. */
.recommend-hint.bad::before {
  background: var(--danger-ink, #c22f3d);
  border-radius: 50%;
  color: var(--custom-text-background-color, #fff);
  content: "!";
  flex: none;
  font-size: .72rem;
  font-weight: 700;
  height: 1.05rem;
  line-height: 1.05rem;
  text-align: center;
  width: 1.05rem;
}
/* The free-seat nudge's Add control, shared with the required companion's.
   A link, not a submit: it claims the seats with JS off by navigating
   (applyEarnParam / applyCompanionParam), and a submit sitting above the
   checkout button would steal the form's Enter key. */
.recommend-hint .earn-add {
  background: transparent;
  border: 1px solid currentColor;
  border-radius: 4px;
  color: inherit;
  cursor: pointer;
  font: inherit;
  font-size: .85rem;
  padding: .1rem .55rem;
  text-decoration: none;
  white-space: nowrap;
}
.promo-row { display: flex; flex-wrap: wrap; gap: .5rem; margin: .2rem 0 .3rem; }
.promo-row input {
  background: transparent;
  border: 1px solid var(--custom-control-border,#6b6b6c);
  border-radius: 6px;
  color: inherit;
  flex: 1;
  font-family: ui-monospace,"SF Mono",Menlo,monospace;
  letter-spacing: .04em;
  max-width: 24ch;
  min-width: 12ch;
  padding: .55rem .6rem;
}
/* A refused field's own boundary, which WCAG 1.4.11 wants at 3:1 against
   what is behind it. The token clears 4.5, so the 3 comes free. */
.promo-row input[aria-invalid=true] { border-color: var(--danger-ink, #c22f3d); }
/* In the rail the box has the total's width to itself, so the field takes
   all of it beside Apply, as a checkout's discount field does. */
.purchase-rail .promo-row input { max-width: none; }
button.promo-apply,.recommend-hint .earn-add {
  background: transparent;
  border: 1px solid var(--custom-control-border,#d2d2d3);
  border-radius: var(--button-radius, 999px);
  box-shadow: var(--custom-control-shadow);
  color: var(--custom-text-color);
  cursor: pointer;
  font-size: .85rem;
  font-weight: 600;
  padding: .42rem 1rem;
}
button.promo-apply:hover,.recommend-hint .earn-add:hover {
  background: color-mix(in srgb,currentColor 8%,transparent);
}
.promo-chip {
  align-items: baseline;
  border: 1px solid var(--custom-control-border,#d1d1d1);
  border-radius: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  padding: .6rem .8rem;
}
.promo-chip[hidden],.promo-entry[hidden] { display: none; }
.promo-chip .mono {
  font-family: ui-monospace,"SF Mono",Menlo,monospace;
  font-weight: 700;
  letter-spacing: .04em;
}
.promo-chip .promo-desc { opacity: .75; }
.promo-chip .promo-remove {
  color: inherit;
  font-size: .85rem;
  margin-left: auto;
  opacity: .75;
  text-underline-offset: 2px;
}
.promo-chip .promo-remove:hover { opacity: 1; }
/* Several codes (stacked codes, 2b-ii): chips stack with a little air, and
   past three the rest sit behind "+ N more" (Paul, 2026-10-04), a summary
   drawn like the entry's own and gone once it has been opened: the codes it
   held are the answer, and a label over them saying how many is noise. */
.promo-chip + .promo-chip,.promo-more,.promo-more > .promo-chip:first-of-type { margin-top: .4rem; }
.promo-more > summary {
  cursor: pointer;
  font-size: .9rem;
  font-weight: 500;
  list-style: none;
  opacity: .75;
  padding: .3rem 0;
  width: fit-content;
}
.promo-more > summary::-webkit-details-marker { display: none; }
.promo-more > summary:hover { opacity: 1; }
.promo-more[open] > summary { display: none; }
.promo-note { font-size: .85rem; margin: .1rem 0 .4rem; }
/* "That code isn't valid" — the one red WORDS on this page, and the reason
   the token has to be computed rather than flipped: this is body text, so it
   owes 4.5:1 against a card nobody here chose. */
.promo-note.bad { color: var(--danger-ink, #c22f3d); }

/* event/event_tiers.css */
/* Price tiers on a buyer's ticket row (tiers.go). Four elements, and the
   placement of each is the decision rather than the styling:

   <tier-chip> is the live tier's NAME, and it sits UNDER the price, in the
   price's own column. Not beside the ticket name — there it reads as a
   product variant and invites "what's the difference between Early bird
   Admission and Standard Admission?", which has no answer. Not touching the
   number either — a chip against a price reads as a discount badge. Under it,
   the chip is a caption on the number, which is what it is: the tier explains
   why the price is what it is today.

   .tier-nudge is the amber line saying what the price does next. It carries a
   FILL-less colour and the chip carries a fill, deliberately: two tinted
   things in one row compete, and the split gives each a job — grey says what
   this price is, amber says what is coming.

   The amber is a TOKEN now, and the fallback beside it is the literal this
   rule used to carry. It was literal because this page has no status palette
   — it is the organiser's own --custom-* colours end to end — and #8a5a12 is
   correct on the white card it was measured against (5.9:1). It is 2.88:1 on
   a near-black card and 1.5:1 on a mid-grey one, and the card is a control an
   organiser has (seen on a customer's dark palette, 2026-09-08). So the value
   comes from event.html now, picked by the side of the card and then measured
   against it (eventDesign.WarnInk); the fallback is what the Classic skin,
   which never declares the token and whose card is #fff whatever the palette,
   goes on painting.

   The CHIP's pair is still literal, and that is deliberate rather than a
   half-sweep: its ink is measured against its own FILL (6.0:1), so the two
   travel together onto any card and the chip is never illegible — only, on a
   dark palette, a light-grey pill louder than the price it captions. Making
   it card-derived means computing both halves, which changes the chip on
   every light event to fix the dark ones: a design call, not a contrast fix. */
.tier-nudge {
  color: var(--warn-ink, #8a5a12);
  display: inline-block;
  font-size: .8125rem;
  margin-block-start: .15rem;
}
/* How little of THIS price is left, and why the ticket cannot be bought.
   Both are quiet: they are the row explaining itself, not the row shouting. */
.tier-left, .tier-shut {
  color: var(--custom-text-color-muted);
  display: inline-block;
  font-size: .8125rem;
  margin-block-start: .15rem;
}
/* The chip's own line, so it sits under the price rather than beside it, and
   right-aligned to the same edge the money uses. inline-block keeps the fill
   hugging the word instead of running the width of the cell. */
tier-chip {
  background: #ecebe5;
  border-radius: 999px;
  color: #56575f;
  /* BLOCK, not inline-block, and this is the whole point of the element
     rather than a detail of it: inline-block leaves the chip on the price's
     own line, sitting against the number — which is the treatment that reads
     as a discount badge and was rejected for exactly that (locked
     2026-07-30). A block box breaks the line; width: fit-content keeps it
     hugging the word instead of running the width of the cell; the auto
     inline-start margin holds it to the same right edge the money uses. */
  display: block;
  font-size: .75rem;
  font-weight: 600;
  margin-block-start: .35rem;
  margin-inline-start: auto;
  padding: .1rem .45rem;
  white-space: nowrap;
  width: fit-content;
}

/* event/event_bands.css */
/* Volume prices on a buyer's ticket row (bands.go). Two lines, and each takes
   the treatment its tier counterpart next door already earned — a row can only
   ever carry tiers or bands, so the two never appear together and a buyer who
   meets both on different rows meets one grammar.

   .band-line is the ladder itself: "3 or more: €80 each · 5 or more: €65 each".
   Quiet, like .tier-left, because it is the row explaining its own price rather
   than the row shouting. It is server-rendered, so it is there with scripting
   off — which is the whole reason it is a line of words and not a table.

   .band-nudge is the amber line that appears only when the buyer is one ticket
   short of the next band. Amber for the same reason .tier-nudge is amber: grey
   says what this price IS, amber says what is one step away. Same token, same
   fallback, and the same reason for both — read event_tiers.css's note on the
   nudge, which this one follows exactly. */
.band-line {
  color: var(--custom-text-color-muted);
  display: inline-block;
  font-size: .8125rem;
  margin-block-start: .15rem;
}
/* The nudge takes NO <br> in the markup, because a <br> renders whether or not
   the thing after it does and would leave a blank line under every ladder that
   has nothing to say. A block box breaks its own line instead.

   Which is why the display lives behind :not([hidden]): a bare `display` beats
   the hidden attribute, and the module's only way of putting this line away is
   to set hidden. */
.band-nudge:not([hidden]) {
  color: var(--warn-ink, #8a5a12);
  display: block;
  font-size: .8125rem;
  margin-block-start: .15rem;
  width: fit-content;
}

/* THE PRICE AND THE STEPPER DO NOT MOVE. The row centres its three cells
   (ticket-row's own rule, part of the theme contract), so anything that changes
   the name column's height re-centres the two controls beside it — and the
   nudge appears and disappears as somebody steps through the quantities. Vicky
   caught it stepping 1 to 5: the row went 67px to 108px and the stepper dropped
   21px, twice, under her finger (2026-08-28).

   Pinning the cells to the TOP of a laddered row is what makes that impossible
   rather than merely smaller: the column grows downward, and no wrap at any
   width can move a control that is anchored to the top edge. A slot reserved
   for the nudge would have fixed the desktop case only — on a phone the
   sentence wraps to four lines, and reserving four blank lines under every
   laddered row is worse than the jump.

   Scoped to rows that HAVE a ladder, so no other row on the page changes. It
   also reads better where it applies: the price sits level with the ticket
   name, and the ladder is a note underneath it rather than something the price
   is centred against. */
ticket-row:has(.band-line) { align-items: start; }

/* event/event_time_price.css */
/* A ticket repriced by the time its buyer chose (time_price.go). Two elements
   in the price cell, and the pairing is the decision rather than the styling.

   THE OLD PRICE IS STRUCK **AND** MUTED, NOT ONE OR THE OTHER. That rule is
   admin/change_parts.css's .ch-was, which was the only strikethrough in the
   codebase until this one and had already learned it: muted alone reads as a
   second label, and a strike alone reads as loud as the live price beside it.
   THE MUTING is what holds the pair apart, and it is doing that job alone —
   so the strike takes currentColor, the same muted ink the number is already
   in. It shipped on --custom-control-border and that was a step lighter
   again: two mutings stacked, and the line read as a smudge behind the figure
   rather than a mark across it. Nothing here needs the line to be quieter
   than the number it crosses; it needs it to be visibly a strike.

   ONE LINE, and that is why this shape was chosen over a chip under the price
   (docs/time-price-mockup.html shape B, rejected). The pair appears and
   disappears as a buyer steps through the times, and event_bands.css records
   what a price cell that changes height does: Vicky stepped 1 to 5 on
   2026-08-28, the row went 67px to 108px and the stepper dropped 21px under
   her finger, twice. A cell that stays one line cannot do that, so there is
   nothing here to pin to the top and no row whose alignment changes.

   BOTH DIRECTIONS WEAR THIS, and a time that costs MORE is not painted as a
   warning. It is not a warning: it is what the ticket costs then, the same
   fact as a reduction pointing the other way, and the amber on this page is
   reserved for what a price is ABOUT to do (event_tiers.css). A buyer meets
   one grammar for "the time changed the price" rather than two that must be
   told apart at a glance.

   No colour of our own anywhere below. The event page is the organizer's
   --custom-* palette end to end, and both of these are already in it. */
.price-was {
  color: var(--custom-text-color-muted);
  margin-inline-end: .4rem;
  text-decoration: line-through;
}
/* The one thing in the cell carrying the card's full ink. The Pro skin mutes
   the whole .price cell (event_pro.css), which is right for a row quoting one
   number and wrong for a row quoting two: the live price has to be the louder
   of the pair or the strike is decoration. 600 rather than the skin's own
   weight for the same reason, and only here — an unrepriced row is untouched. */
.price-now {
  color: var(--custom-text-color);
  font-weight: 600;
}

/* event/event_steps.css */
/* Stepped timed booking (checkout-steps.js) — Date › Time › Tickets, with a
   path chooser in front when an event has more than one schedule group.

   SHARED by both skins, unlike most of this directory. The wizard's markup is
   built in JavaScript, so there is exactly one structure and it can only have
   one home; a second copy in the other skin would be a copy of something no
   template renders, which is the drift trap docs/themes.md warns about. What
   IS per-skin is the palette, and that arrives through the eight custom
   properties below: Classic sets them from its highlight colour and its fixed
   card, Pro from the account's --custom-* tokens. The defaults here are the
   neutral ones, so a skin that forgets to set them gets a legible wizard
   rather than an invisible one.

   Before 2026-08-10 these rules lived in event_pro.css, which is also why the
   wizard only ran on the account's own host: the module gated on
   .pro-form-footer, and the standardized root-domain page — the URL an
   organizer is most likely to hand out — served the flat select with every
   ticket under it. */
.checkout-steps {
  --step-accent: #2d40ea;
  --step-accent-ink: #fff;
  --step-danger: #c22f3d;
  --step-heading-ink: #0a1f44;
  --step-ink: #30303a;
  --step-ink-muted: #61616e;
  --step-line: #c8c8ce;
  --step-link: #2d40ea;
  --step-surface: #fff;

  margin: .4rem 0 1rem;
}
.step-progress { display: flex; gap: .4rem; list-style: none; margin: 0 0 1.3rem; padding: 0; }
.step-progress li { flex: 1; min-width: 0; }
.step-chip {
  background: color-mix(in srgb,var(--step-accent) 12%,var(--step-surface));
  border: 0;
  border-radius: 99px;
  color: var(--step-ink-muted);
  cursor: pointer;
  font-size: .78rem;
  font-weight: 600;
  margin: 0;
  overflow: hidden;
  padding: .4rem .55rem;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
  width: 100%;
}
.step-progress li.active .step-chip { background: var(--step-accent); color: var(--step-accent-ink); }
.step-progress li.done .step-chip {
  background: color-mix(in srgb,var(--step-accent) 24%,var(--step-surface));
  color: var(--step-ink);
}
.step-chip:disabled { cursor: default; opacity: .5; }
/* BOOKING STEPS WITH THE HEADING (Vicky, 2026-10-07): the Booking steps
   control (design_tokens.go's step_bar) writes data-step-bar="heading" on
   the booking form, and only then. Every rule here is scoped under it, so a
   page on "pills" (and every Classic page) matches none of them and its
   chips are drawn exactly as above, which is what an organizer's own CSS
   against .step-chip was written for. Nothing above is edited for it.

   The row of chips is hidden, never removed: checkout-steps.js reads how
   many steps there are and which is current off it. What the buyer reads
   instead is on the step heading's own line, which every step has, so
   nothing under it moves when a second step arrives: the next step's name
   on the first step, and a button back to the one before on every later
   step (stepLine in checkout-steps.js).

   The side sits BESIDE the <h3> in a .step-heading-line, never inside it,
   and that line is drawn on every step of this mode, holding a side or
   not. The heading's margins move onto the line, so the heading stands
   exactly where it does on a step with nothing beside it.

   The side can carry an organizer's group name, so on a phone it is held
   to half the line and cut short with an ellipsis rather than pushed past
   the card. Cut, not wrapped: a side that wrapped onto three lines made the
   line taller the moment a second step arrived, and moved the ticket list
   under the buyer's hand, which is what this mode exists to stop. The back
   button's full name is still its aria-label. */
[data-step-bar="heading"] .step-progress { display: none; }
[data-step-bar="heading"] .step-heading-line {
  align-items: baseline;
  column-gap: 1rem;
  display: flex;
  margin: .1rem 0 .9rem;
}
[data-step-bar="heading"] .step-heading-line > .step-heading {
  flex: 1 1 auto;
  margin: 0;
  min-width: 0;
}
[data-step-bar="heading"] .step-heading-side {
  color: var(--step-ink-muted);
  flex: 0 1 auto;
  font-size: .85rem;
  font-weight: 400;
  max-width: 50%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
[data-step-bar="heading"] button.step-heading-side {
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  line-height: inherit;
  margin: 0;
  padding: 0;
}
/* The ‹ out of the label, so the button is named by its words (and its
   aria-label) and never reads the glyph aloud. */
[data-step-bar="heading"] .step-heading-back::before { content: "\2039"; margin-inline-end: .25em; }
[data-step-bar="heading"] button.step-heading-side:hover { color: var(--step-ink); }
[data-step-bar="heading"] button.step-heading-side:focus-visible {
  outline: 2px solid var(--step-heading-ink);
  outline-offset: 2px;
}
.step-heading {
  color: var(--step-heading-ink);
  font-size: 1.2rem;
  font-weight: 600;
  margin: .1rem 0 .9rem;
}
.step-heading:focus { outline: none; }
/* How many they may take, beside the heading rather than in it (Vicky,
   2026-09-04). It reads as the heading's footnote, so it takes the muted ink
   and drops back to body weight even where it sits inside a bold group name. */
.step-heading-count {
  color: var(--step-ink-muted);
  font-size: .8125rem;
  font-weight: 400;
  white-space: nowrap;
}
/* WHY THE BUTTON DIDN'T TAKE YOU ON. Written by checkout-steps.js above the
   first question a buyer has not answered when they press Continue, and gone
   again the moment the step redraws.

   The words take the ordinary text colour and the RULE carries the red, which
   is the same call event_promo.css records for its own refusal — read the
   note there, including the half of it that no longer holds.
   The red is the palette's, like every other colour in this file: the wizard
   is shared by both skins and each sets its own --step-* afterwards, so a
   literal here would be the one colour in the block that could not follow the
   card it lands on. */
.step-needed {
  border-left: 3px solid var(--step-danger);
  color: var(--step-ink);
  font-size: .9rem;
  margin: 0 0 .75rem;
  padding-left: .7rem;
}
.step-back {
  background: none;
  border: 0;
  color: var(--step-link);
  cursor: pointer;
  font-size: .9rem;
  font-weight: 600;
  margin: 0 0 .9rem;
  padding-block: .1rem;
  padding-inline: 1.15em 0;
  position: relative;
}
/* THE ARROW IS DRAWN HERE AND OUT OF FLOW, never typed into the label the
   module sets. A text decoration is painted across everything in flow inside
   the box whatever that descendant sets — the trap venue.css records for its
   separators — so an arrow sitting in the button's own text took the hover
   underline with it, under the glyph and under the space after it. Out of
   flow it cannot, and the padding it sits in is outside the line box the
   underline is drawn across, so the line starts at the first word. */
.step-back::before { content: "\2190"; inset-inline-start: 0; position: absolute; }
.step-back:hover { text-decoration: underline; }
/* Two ways back on one step: "← Choose tickets" and a times group's own
   "← Change date". Both are inline buttons, so with nothing between them they
   ran together as one line of words (Vicky, 2026-10-11). */
.step-back:not([hidden]) + .step-back { margin-inline-start: 1.25rem; }
/* The offering's own name over its rows, on the one step that can hold more
   than one group's cards. Drawn only when there IS more than one — over a
   single group it would repeat the heading directly above it — so this is
   always a divider between two sets of cards and never a title over the
   first.

   A LABEL, not a second heading. .step-heading is 1.2rem and asks the
   question; this only says which set the next few cards are, so it is told
   apart by SIZE and a hairline rather than by colour. It keeps the heading's
   own ink because it is an organizer's words rather than chrome, and the
   hairline earns its place: at 1rem/600 against .step-choice-main's
   1.02rem/600 the words alone read as another card.

   :first-of-type is the first h4 in the panel, which is safe because the
   step heading above it is an h3 and everything else the wizard draws is a
   button (checkout-steps.js).

   NOT uppercased and not tracked, though every other small label in the
   admin is. The words here are the ORGANIZER's own offering name in
   whatever language they wrote it, and text-transform on arbitrary content
   is a bug in Turkish, ugly in German and meaningless in Japanese — where
   letter-spacing then breaks the run as well.

   No dark block and no light-dark(): every colour on this screen arrives
   through the eight custom properties at the top of this file, which Classic
   sets from its highlight and Pro from the account's own tokens. A literal
   here would be the one thing on the screen that ignored the skin. */
.step-group-name {
  border-bottom: 1px solid var(--step-line);
  color: var(--step-heading-ink);
  font-size: 1rem;
  font-weight: 600;
  margin: 1.5rem 0 .8rem;
  padding-bottom: .3rem;
}
/* The first group opens directly under the back link, which already carries
   the gap. */
.step-group-name:first-of-type { margin-top: .2rem; }
/* One ticket's block, when an offering is asked once per ticket (PRO-4599).
   The list of cards under it is the same list every other seat gets, so the
   only thing telling two blocks apart is this head — which is why it carries
   both halves of the question: which ticket, and what it currently holds.

   No border, no card, no tint. Three or six of these run down one screen, and
   a box around each would put a frame inside a frame around every choice; the
   space above the head is what separates them, the same way the group heading
   separates offerings. */
.step-seat { margin: 0 0 1.1rem; }
.step-seat-head {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  gap: .2rem .6rem;
  justify-content: space-between;
  margin: 0 0 .4rem;
}
.step-seat-name {
  color: var(--step-heading-ink);
  font-size: .9rem;
  font-weight: 600;
}
/* What this seat holds, or that it holds nothing. Quiet until it is answered
   — an unanswered seat is the normal state on arrival, and six of them
   shouting would read as six errors. */
.step-seat-state {
  color: var(--step-ink-muted);
  font-size: .85rem;
}
.step-seat-state.set { color: var(--step-accent); font-weight: 600; }
.step-choice {
  align-items: stretch;
  background: var(--step-surface);
  border: 1.5px solid var(--step-line);
  border-radius: 12px;
  color: var(--step-ink);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: .2rem;
  margin: 0 0 .55rem;
  padding: .8rem 1rem;
  text-align: left;
  transition: opacity .32s ease,transform .32s ease,border-color .12s,background .12s;
  width: 100%;
}
.step-choice:hover:not(:disabled) { border-color: var(--step-accent); }
.step-choice[aria-current] {
  background: color-mix(in srgb,var(--step-accent) 8%,var(--step-surface));
  border-color: var(--step-accent);
}
.step-choice:disabled { cursor: default; opacity: .55; }
.step-choice-top {
  align-items: center;
  display: flex;
  gap: .5rem;
  justify-content: space-between;
}
.step-choice-main { font-size: 1.02rem; font-weight: 600; }
.step-choice-badge {
  background: color-mix(in srgb,var(--step-accent) 16%,var(--step-surface));
  border-radius: 99px;
  color: var(--step-ink);
  flex: none;
  font-size: .75rem;
  font-weight: 600;
  padding: .1rem .5rem;
}
.step-choice:disabled .step-choice-badge { opacity: .65; }
.step-choice-sub { color: var(--step-ink-muted); font-size: .85rem; }
/* The time's own description — prose, so it gets reading line-height where
   the detail line above it gets none. Only rendered when there is one. */
.step-choice-desc {
  color: var(--step-ink-muted);
  font-size: .85rem;
  line-height: 1.45;
  margin-top: .1rem;
}
.step-heading,.step-heading-side,.step-back { transition: opacity .32s ease,transform .32s ease; }
.step-panel.leaving { pointer-events: none; }
.step-panel.leaving .step-heading,.step-panel.leaving .step-heading-side,.step-panel.leaving .step-back,.step-panel.leaving .step-choice:not(.chosen) {
  opacity: 0;
  transform: translateY(-14px);
}
.step-panel.leaving .step-choice.chosen { transform: translateY(-6px); }
.step-choice.chosen { position: relative; }
.step-choice.chosen::after {
  animation: spin .6s linear infinite;
  border: 2px solid var(--step-accent);
  border-radius: 50%;
  border-top-color: transparent;
  bottom: .6rem;
  content: "";
  height: 1rem;
  position: absolute;
  right: .6rem;
  width: 1rem;
}

/* event/venue.css */
/* The location block (event.html "event-venue-map"): one card holding the
   map and the street address, which is itself what opens the directions
   (2026-09-18). <event-venue>,
   <event-map> and <venue-links> are all part of the public CSS contract
   (docs/themes.md). */
/* The CLIP moved onto the map (2026-08-21). It was on this card, to hold the
   map's square corners inside the card's round ones — and the moment the
   directions became a menu, that clip started cutting the menu's panel off,
   because the panel is positioned outside the flow and this is its nearest
   clipping ancestor. Nothing on the review instance showed it: a map needs a
   maps key, and the review instance has none, so the one screen that could
   have caught it never drew a map. Found by reading, not by looking.
   Clipping <event-map> instead does the same job for the same reason and
   leaves the card free to let a panel out. */
event-venue {
  border: 1px solid var(--divider-color);
  border-radius: 10px;
  display: block;
  margin: 1rem 0 1.5rem;
}
/* No map, no card. The border is there to hold the map and its words
   together; around two lines of text on their own it reads as something
   missing rather than something tidy (Vicky, 2026-08-10). The server sends the
   words up to the summary line instead when there is no map to keep them here
   (venueBlock.HasMap) — this rule is what covers the case it CANNOT know
   about: venue-map.js removing the map in the browser, which would
   otherwise leave a bordered box round nothing. */
event-venue:not(:has(event-map)) {
  border: 0;
  border-radius: 0;
  margin: 1rem 0;
}
event-venue:not(:has(event-map)) venue-facts { padding: 0; }
/* 9px, not 10: the card's corner less its own 1px border, which is the
   radius of the hole the map actually sits in. Only the TOP corners — the map
   is the first thing in the card and the words sit under it. */
event-map { border-radius: 9px 9px 0 0; display: block; overflow: hidden; }
/* A map venue-map.js draws itself (Apple's, OpenStreetMap's) goes in a
   canvas it creates at the moment it draws, on the iframe's proportions, so
   switching between maps doesn't move the page. Before that there is no
   canvas at all, which is why a page with no script, or a map that failed
   and had nothing to give way to, reads as a page with no map rather than an
   empty rectangle above the address. */
event-map [data-map-canvas],
event-map iframe {
  aspect-ratio: 3 / 2;
  border: 0;
  display: block;
  width: 100%;
}
/* Letterbox only where there is room for one. 3:2 is the phone shape: at
   375px the old 5:2 map was ~150px tall, too squat to place anything in, and
   a map you can't read is worse than the address on its own. */
@media (min-width: 34rem) {
  event-map [data-map-canvas],
  event-map iframe { aspect-ratio: 5 / 2; }
}
/* The type is set HERE, on the words half, not on the <p> inside it. Since
   2026-09-18 the address is either a paragraph or the directions menu's own
   summary depending on whether it has anywhere to open, and those two must
   be the same size — the reader cannot see which one they got. */
venue-facts {
  display: block;
  font-size: .9rem;
  padding: .75rem .9rem;
}
venue-facts p {
  color: var(--custom-text-color-muted);
  margin: 0;
}
/* The other home: no map, so the words sit with the venue's name on the
   summary line. No padding of its own — it is one more line in the meta
   stack. The 1.3em indent that used to be here was clearing a LOCATION PIN,
   and the pin went when the date and venue became one line (2026-08-20). What
   was left was an address stepped in from the line above it and a directions
   line stepped in again from that — which is the "clumsy/unfinished" Vicky
   read on 2026-08-21. Flush left, like every other line in the stack.

   And the same SIZE as the line above it, which is the line it continues:
   .pro-summary's .95rem here, Classic's 16px paragraph below. The address
   used to read a size smaller than the date it followed, which is the meta
   stack stepping down for no reason (Doc, 2026-09-18). */
.pro-meta venue-facts { font-size: .95rem; padding: 0; }
/* Classic's meta line is a <p>, so the facts follow it rather than sit
   inside; close the gap the paragraph would otherwise leave. */
.classic-info p:has(+ venue-facts) { margin-bottom: 0; }
.classic-info > div > venue-facts { font-size: 1rem; padding: .2rem 0 0; }
/* The directions line: the verb once, then the services. The separators are
   TEXT between the links, not a ::before on them — a pseudo-element inside an
   <a> takes the link's underline (text-decoration draws over descendants
   whatever they set) and sits inside its hit area, so the dots read as part of
   the link and click like it. */
venue-links {
  color: var(--custom-text-color-muted);
  display: block;
  font-size: .88rem;
  margin-top: .35rem;
}
venue-facts p:not(:empty) + venue-links { margin-top: .5rem; }
/* The address IS the control (2026-09-18), so this element is no longer a
   line UNDER the address — it is the address. It gives up the smaller type
   and the gap that separated the two, and takes the words half's own size
   instead. */
venue-links[data-address] {
  align-items: flex-start;
  display: flex;
  font-size: inherit;
  gap: .35em;
  margin-top: 0;
}

/* ---- Directions, as one disclosure ----
   The verb-and-three-services sentence became a menu on 2026-08-21: three
   underlined targets directly above the ticket list competed with the one
   thing the page is for. The <details> mechanics are the ticket's
   "Add to calendar" (public/cal_menu.css) — this bundle does not carry that
   file, so the few rules are repeated rather than the file being moved into a
   bundle the buyer journey would then load twice.

   The summary is a LINK, not a pill: it belongs to the meta stack, and a
   filled control there would put the weight back that the menu exists to
   take away. */
.dir-menu { display: inline-block; position: relative; }
.dir-menu > summary { align-items: center; color: var(--custom-link-color); cursor: pointer;
  display: inline-flex; gap: .15em; list-style: none; }
.dir-menu > summary::-webkit-details-marker { display: none; }
.dir-menu > summary:hover { text-decoration: underline; }
.dir-menu > summary:focus-visible { border-radius: .25rem; outline: 2px solid var(--custom-link-color); outline-offset: 2px; }
.dir-menu .caret { transition: transform .12s ease; }
.dir-menu[open] .caret { transform: rotate(180deg); }
.dir-menu > nav { background: var(--custom-text-background-color); border: 1px solid var(--custom-control-border);
  border-radius: .5rem; box-shadow: 0 6px 20px -8px color-mix(in srgb, var(--custom-text-color) 30%, transparent);
  display: flex; flex-direction: column; left: 0; margin-top: .35rem; min-width: 11rem;
  padding: .3rem; position: absolute; z-index: 20; }
/* The menu beside the address holds BUTTONS too since 2026-09-26 (each one
   switches the map in place), dressed exactly as the links beside them. */
.dir-menu > nav :is(a, button) { background: none; border: 0; border-radius: .3rem; color: var(--custom-text-color); cursor: pointer;
  font: inherit; font-size: .85rem; padding: .4rem .55rem; text-align: start; text-decoration: none; white-space: nowrap; }
.dir-menu > nav :is(a, button):hover { background: color-mix(in srgb, currentColor 8%, transparent); }
.dir-menu > nav button:focus-visible { outline: 2px solid var(--custom-link-color); outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) { .dir-menu .caret { transition: none; } }
/* ---- ...and the address, which is a LINK since 2026-09-25 ----
   Pressing it opens the service that drew the map (Paul: it has to read as a
   link, so it has to BE one), and the three dots beside it hold the menu of
   the others. The link and the dots are one row (the flex on
   venue-links[data-address] above): the address takes the room and the dots
   sit at its end.

   The link is INLINE text inside that row, and that is about wrapping: a
   street address is long enough to take two lines on a phone, and inline the
   arrow follows the last word wherever the last word lands, the hit area is
   the text and not the rectangle around it, and the focus ring draws round
   each line rather than round the gap at the end of the first.

   It keeps the address's own colour rather than taking the link colour. The
   menu existed to stop this corner competing with the ticket list (Vicky,
   2026-08-21), and a whole postal address in link blue would put back more
   weight than the two words it replaced. The external-link arrow (below), the
   pointer, the hover underline and the focus ring are what say it presses.
   Inside the map card the link becomes a block to hold the icons at its
   edges; the inline form is the one on the summary line. */
venue-links[data-address] .dir-go {
  color: inherit;
  text-decoration: none;
}
venue-links[data-address] .dir-go:hover { text-decoration: underline; }
venue-links[data-address] .dir-go:focus-visible {
  border-radius: .25rem;
  outline: 2px solid var(--custom-link-color);
  outline-offset: 2px;
}
/* The three dots: a 24px target (WCAG 2.2 SC 2.5.8) holding a 1em glyph, in
   the muted text colour, so it reads as the quieter of the two things on the
   row. At least one line tall and centred in it, with the row aligned to its
   top, so the dots sit on the address's first line (a baseline row put an
   icon-only box's bottom edge on the text's baseline, half a line high). The panel it opens hangs from its END edge, since the dots sit at the
   end of the row. */
.dir-more { flex: none; }
.dir-more > summary {
  align-items: center;
  border-radius: .3rem;
  color: var(--custom-text-color-muted);
  display: inline-flex;
  justify-content: center;
  min-block-size: max(24px, 1lh);
  min-inline-size: 24px;
}
.dir-more > summary:hover { background: color-mix(in srgb, currentColor 10%, transparent); text-decoration: none; }
.dir-more > nav {
  inset-inline-end: 0;
  left: auto;
}
/* ...and the last word of the address rides with the arrow. Without this the
   line breaks between them and leaves the marker alone under its own address
   (Doc, 2026-09-18) — a break the browser is entitled to take, because the
   chevron is an atomic inline box. venue.go's menuLabel is the split that
   feeds it, and says why a no-break space cannot do the same job. */
.dir-tail { white-space: nowrap; }
/* ---- The pin and the arrow (Paul, 2026-09-25) ----
   An address in the page's text colour does not look pressable, and the
   chevron that followed it said "there is more" rather than "this goes
   somewhere". So the address wears an external-link arrow, in the link
   colour, as the one thing on it that says press me; and inside the map card
   a pin in front, saying what the line is.

   Outside the card the arrow stays inline after the last word (the nowrap
   tail above keeps them together) and there is no pin: up on the summary
   line the address continues the venue's name, and a pin there is the
   step-in Vicky read as unfinished on 2026-08-21. */
venue-links[data-address] .pin { display: none; }
venue-links[data-address] .ext {
  color: var(--custom-link-color);
  display: inline-block;
  font-size: .85em;
  margin-inline-start: .3em;
  vertical-align: -.05em;
}
/* In the card the two icons go to the EDGES: pin at the start, arrow at the
   far end of the row, both held to the FIRST line when the address wraps.
   Absolute inside the link rather than a flex row, so the address itself
   stays one run of text that wraps under its own first word. The link is a
   block here, so the hit area is the whole row out to the arrow, which is
   what an arrow at the edge promises. 1lh is the link's own line height,
   so each icon is centred on the first line whatever the type. */
event-venue venue-links[data-address] .dir-go {
  display: block;
  flex: 1 1 auto;
  padding-inline: 1.5em;
  position: relative;
}
event-venue venue-links[data-address] :is(.pin, .ext) {
  display: block;
  height: 1em;
  inset-block-start: calc((1lh - 1em) / 2);
  margin: 0;
  position: absolute;
}
event-venue venue-links[data-address] :is(.pin, .ext) svg { display: block; }
event-venue venue-links[data-address] .pin {
  color: var(--custom-text-color-muted);
  font-size: 1em;
  inset-inline-start: 0;
}
event-venue venue-links[data-address] .ext {
  font-size: 1em;
  inset-inline-end: 0;
}
/* IN THE CARD, THE PANEL OPENS UPWARD, over the map (Vicky, 2026-08-24: the
   menu "is constrained in the box"). The clip that cut it off this time is the
   PAGE card's, one box further out than the one moved off <event-venue> on
   2026-08-21: .pro-card carries `overflow: clip` to hold the banner and the
   theme's band inside its rounded corners, and where the location is the last
   block on the page (page_arrangement.go — an organizer can put it there, and
   the map is a tall thing to have last) the panel's third service falls past
   the card's bottom edge and is cut.

   Moving THAT clip is not the same trade as last time. It holds a stack of
   full-bleed elements — banner, stub band, and a manifest theme's 8px colour
   band — against a radius an account can set as high as 40px, and no
   arrangement of per-element radii reproduces it: the element under the band
   would need a partial corner, which border-radius cannot describe. So the
   card keeps its clip and the panel stops needing to leave.

   Upward is not a workaround here, it is the better placement anyway: a map
   is 227px tall on a 375px phone and taller everywhere else, so a ~111px
   panel always has room above the summary, and it lands on the map it is
   about instead of over the ticket list — which is the same reason this
   became a menu in the first place. Scoped to <event-venue>, so the OTHER
   home of these words (up on the summary line, where there is no map because
   venueBlock.HasMap sent them there) still opens downward rather than over
   the event's own name. */
event-venue .dir-menu > nav { bottom: 100%; margin: 0 0 .35rem; }

/* event/event_calendar.css */
/* <event-calendar>: Add to calendar beside the date on the event page, when
   the organizer offers it before anybody books (PRO-5531, the
   event-calendar partial in event.html).

   The menu is the same zero-JS disclosure as Get directions (venue.css's
   .dir-menu), and these rules repeat that recipe rather than share its
   selectors: venue_menu_test.go reads .dir-menu's rules by name to keep the
   directions panel inside the page card, and a shared selector would hide
   them from it. The summary is a LINK, not a pill, for the reason given
   there: a filled control beside the date would compete with the one that
   sells the ticket.

   On its own line straight under the date, at the size of the line it
   follows: .pro-summary's .95rem on Pro, Classic's 16px paragraph there. */
event-calendar {
  display: block;
  font-size: .95rem;
  margin-top: .35rem;
}
.classic-info p:has(+ event-calendar) { margin-bottom: 0; }
.classic-info > div > event-calendar { font-size: 1rem; margin-top: .2rem; }
.cal-menu { display: inline-block; position: relative; }
.cal-menu > summary { align-items: center; color: var(--custom-link-color); cursor: pointer;
  display: inline-flex; gap: .15em; list-style: none; }
.cal-menu > summary::-webkit-details-marker { display: none; }
.cal-menu > summary:hover { text-decoration: underline; }
.cal-menu > summary:focus-visible { border-radius: .25rem; outline: 2px solid var(--custom-link-color); outline-offset: 2px; }
.cal-menu .caret { transition: transform .12s ease; }
.cal-menu[open] .caret { transform: rotate(180deg); }
.cal-menu > nav { background: var(--custom-text-background-color); border: 1px solid var(--custom-control-border);
  border-radius: .5rem; box-shadow: 0 6px 20px -8px color-mix(in srgb, var(--custom-text-color) 30%, transparent);
  display: flex; flex-direction: column; left: 0; margin-top: .35rem; min-width: 11rem;
  padding: .3rem; position: absolute; z-index: 20; }
.cal-menu > nav a { border-radius: .3rem; color: var(--custom-text-color); font-size: .85rem;
  padding: .4rem .55rem; text-decoration: none; white-space: nowrap; }
.cal-menu > nav a:hover { background: color-mix(in srgb, currentColor 8%, transparent); }
.cal-menu > nav a:focus-visible { outline: 2px solid var(--custom-link-color); outline-offset: -2px; }
@media (prefers-reduced-motion: reduce) { .cal-menu .caret { transition: none; } }

/* event/event_columns.css */
/* The purchase construct in two columns: the list you are choosing from on
   the left, what it comes to on the right (Vicky, 2026-08-22 — a buyer screen
   earns a second column if it has a live updating amount, and this total moves
   on every stepper press). Below the threshold these are three plain blocks in
   source order and the page is exactly what it was, which is why the mobile
   case needs no rules at all here.

   A CONTAINER query, not a media query, and the two jobs are deliberately on
   two different elements: a container query cannot style its own container, and
   `container-type` carries `contain: layout style inline-size` with it — not
   something to hand to an element whose own subtree is being shown and hidden a
   step at a time by the wizard. So the FORM is the container and the block
   inside it is the grid.

   Measuring the form rather than the viewport is also what makes the widget
   sheet and a themed page come out right for free: each asks how much room it
   actually has, and a narrow embed simply never crosses the threshold.

   EVERY RULE BELOW IS GATED ON data-priced, which eventFlowCore withholds from
   an event where nothing — no ticket, no good, no add-on, no donation — has a
   price (Vicky, 2026-08-23). The rule that earns a screen a second column asks
   for an amount, and a free event has none: its total prints the word "Free"
   and never moves again, so the rail would hold a Continue button the footer
   already keeps in view and nothing else. Such an event keeps the single
   column and the 640px card, which is what it had before this file existed.

   The gate is an EVENT-level answer, not a live one. A rail that appeared the
   moment somebody picked a paid ticket would slide the ticket list sideways
   under them mid-press; the page is one shape from the first paint. (The two
   checkout screens ask the same question of the ORDER, in railData — by then
   the selection is made and cannot move.)

   The attribute stays on the container query rather than replacing it: a free
   event is one column at every width, and a paid one still has to be wide
   enough. Both conditions, or the widget sheet gets a 352px rail in a 460px
   box. */
form[data-checkout] { container-type: inline-size; }

@container (min-width: 900px) {
  /* 22rem for the rail, which is the number that falls out of holding the
     LIST still: a 1020px card less its 2rem padding is 956px, and 956 minus
     this gap minus 352 leaves 576 — the exact width the ticket list has at
     640px today. So the row a buyer already knows does not stretch by a single
     pixel to pay for the second column (Vicky, 2026-08-23, asking whether the
     left column needed to be that wide; at 20rem it was 608px, and the gap
     between a ticket name and its price went from ~330px to ~360px).
     The rail wanted the 32px anyway: at 320px its longest line, the group
     discount's "Free tickets — 1 × Supporter", wrapped onto two. */
  .purchase-cols[data-priced] {
    align-items: start;
    display: grid;
    gap: 1.75rem;
    grid-template-columns: minmax(0, 1fr) 22rem;
  }
  /* A grid item's min-width is auto, so one long unbroken ticket name would
     push the column past its track and take the rail off the card with it. */
  [data-priced] > .purchase-main { min-width: 0; }

  /* A hairline, not a second card. The construct already sits inside
     .pro-card, and a filled-and-shadowed box in there would be a card inside a
     card — the boxes-in-boxes shape Vicky has taken out of the checkout twice.
     The border is the same --divider-color the ticket rows rule themselves
     with, so the rail reads as part of the same object.
     The radius never exceeds the card's own, the way event_qty.css already
     sizes a control inside it: a square-cornered card must not contain a
     rounded panel. */
  /* THE ACTION SITS UNDER THE THING IT ACTS ON, and the two travel together.
     The rail is ONE sticky group — summary, then button — rather than two
     stickies pinned to opposite ends of a stretched column.

     It was the two-ended version first, on the reading that the button should
     be pinned bottom-right on every screen (Vicky, 2026-08-24). Seeing it
     built, she called the result "very isolated and unanchored", and the
     screenshot says why: the summary is HIDDEN until something is selected,
     so an event page at rest has an empty rail with a lone button pushed to
     the foot of a track whose height comes entirely from the ticket list. It
     floats in the middle of the page beside the third row, attached to
     nothing.

     Pinning it to the top of an empty rail instead only trades one fault for
     the worse one: the button would then LEAP from top to bottom the moment a
     first ticket was picked, which is the flying-around she named as the
     thing to avoid. Keeping the pair together means the only movement is the
     button stepping down by the height of the summary that appeared above
     it — the natural consequence of content arriving, not a jump.

     `align-items: start` is what lets the rail hug its contents again; with
     `stretch` the column takes the list's full height and there is empty
     track under the group. The checkout's own steps still pin their action to
     the foot of their column, because there the summary must come BEFORE the
     form (the total has to be above a wallet button that charges on a tap)
     and the action after, so those two can never be one group.

     Nothing here may become a scroll container. A sticky child of one stops
     sticking silently, which is the trap #1552 already pinned for this
     screen. */
  [data-priced] > .purchase-rail {
    border: 1px solid var(--divider-color);
    border-radius: min(var(--custom-card-border-radius, 12px), 12px);
    display: flex;
    flex-direction: column;
    position: sticky;
    /* Below a sandbox's sticky strip, when there is one (owner_strips.css). */
    top: calc(1.25rem + var(--sandbox-strip-height, 0px));
  }
  [data-priced] > .purchase-rail > .summary { padding: 1.25rem; }
  /* The promotion box sits between the total and the button (Paul,
     2026-10-04), so it takes the total's inset: its caret lines up under
     the total's. */
  [data-priced] > .purchase-rail > .promo-box { margin: 0; padding: 0 1.25rem .5rem; }
  /* The admission message (checkout_admission.css) sits in the same place
     between the total and the button, so it takes the same inset. Without
     it the words, the orders a buyer is holding and Cancel ran flush
     against the panel's edge (PRO-5513). */
  [data-priced] > .purchase-rail > checkout-admission { padding: 0 1.25rem .5rem; }

  /* The breakdown ARRIVES OPEN, showing what you are getting (Vicky,
     2026-08-24, reversing the call recorded here). It was built this way once
     before and taken out again on the grounds that six money fences went red
     for it; that was a cost argument, not a design one, and she has now
     answered the design question. The fences moved instead.

     What did NOT change is whose state it is. The server picks the INITIAL
     state and checkout-summary.js still never touches `open` — a buyer who
     shuts the fold keeps it shut through every quantity change, which is the
     contract that module has held since it was written. */

  /* A timed event's wizard hides BOTH of the rail's children until the Tickets
     step, and a reserved-but-empty third of the card beside the date cards is
     just a hole. The column goes with it.
     Reserving it was the first attempt, on the grounds that the list must not
     shift sideways when the total arrives. That reasoning does not apply here:
     the rail is only ever empty during the wizard, where the ticket list has
     not been rendered yet, so there is nothing on screen to reflow. Everywhere
     else the footer is showing, which means the rail has content from the
     first paint and never collapses at all.

     The two selectors ask the same question from opposite ends, and the
     parent's has to be written as :not(:has(…)) rather than the :has(:has(…))
     it reads like — :has() may not be nested inside :has(), so that form is
     invalid and drops silently, leaving the track behind while the rail
     correctly disappears (which is exactly how this was found). */
  [data-priced] > .purchase-rail:not(:has(> :not([hidden]))) { display: none; }
  .purchase-cols[data-priced]:not(:has(> .purchase-rail > :not([hidden]))) {
    grid-template-columns: minmax(0, 1fr);
  }

  /* AN EMPTY CART HAS NOTHING TO FOLD. The box is on screen from the first
     paint now, so before anything is chosen its disclosure would be a caret
     over an empty drawer — checkout-summary.js marks that state and the
     control stands down to a plain line of text until there is a breakdown
     under it. */
  [data-priced] .purchase-rail .summary[data-empty] > summary {
    cursor: default; list-style: none; pointer-events: none;
  }
  /* The caret DIMS rather than vanishing (Vicky, 2026-09-08: "before the
     order has a value, there's a gap before Total where a caret later
     appears. Could this be filled?").

     Its box was always held so that the caret arriving would not shove the
     word Total sideways — but held EMPTY, which reads as a missing thing
     rather than a waiting one. Same call as the step chips beside it: show
     the real control in its inert state instead of reserving a blank.

     It stays unpressable — the rule above takes the pointer events and the
     cursor — so this is only about what the space says while there is
     nothing to fold. */
  [data-priced] .purchase-rail .summary[data-empty] .promo-caret { opacity: .3; }

  /* The disclosure is the first thing in the rail now, so the rule that
     separated it from the ticket list above it would draw across the top of
     the panel, a hairline inside a hairline.

     BOTH HALVES, and only the margin was taken the first time (Vicky,
     2026-08-25: "is there a darker/thicker top line here?"). The border
     survived, landing exactly against the panel's own — two 1px lines of the
     same colour stacked, so the top edge came out 2px while the other three
     stayed 1px, which reads as a heavier line rather than as a doubled one.
     The comment above described the fault and the fix only did half of it. */
  [data-priced] .purchase-rail .summary { border-top: 0; margin-top: 0; }
  [data-priced] .purchase-rail .summary summary { padding-top: 0; }

  /* The Pro footer's negative margins exist to bleed the button band to the
     CARD's edges (event_pro.css). The rail is not the card, so in here they
     would drag the button out through the panel's border on three sides. */
  /* The button lives INSIDE the box, under the total, with a hairline between
     them — one object rather than a panel and a loose button. Every margin
     goes, because the space is the box's padding now and a margin here would
     push the button out through the border it is supposed to sit inside. */
  [data-priced] .purchase-rail .pro-form-footer {
    border-top: 1px solid var(--divider-color);
    margin: 0;
    padding: 1rem 1.25rem;
  }
}

/* shared/checkout_admission.css */
checkout-admission {
  display: grid;
  font-size: .875rem;
  gap: .25rem;
  inline-size: 100%;
  min-inline-size: 0;
}
checkout-admission[hidden], checkout-admission [hidden] {
  display: none !important;
}
body checkout-admission p {
  font-size: inherit;
  line-height: 1.4;
  margin: 0;
  overflow-wrap: anywhere;
}
checkout-admission admission-actions {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
}
checkout-admission [data-admission-countdown] {
  flex: 1 1 auto;
}
body checkout-admission button[type=button] {
  background: transparent;
  border: 1px solid var(--hairline);
  border-radius: var(--bj-radius-sm, .3rem);
  color: var(--fg);
  font-size: inherit;
  font-weight: 400;
  inline-size: auto;
  margin: 0;
  min-block-size: 2rem;
  padding: .25rem .5rem;
}
/* The buyer's own unfinished orders, when those are the wait
   (checkout_admission_own.go): in the panel, on the refused details step and
   on the error page. One row to an order inside a hairline box, so the list
   reads as the orders and not as a run of loose links (Vicky, 2026-10-10):
   the order itself in the text colour, its reference after it, and its
   Cancel at the row's end. The event page sets no --fg or --hairline, so
   each falls back to the event page's text colour. */
ul[data-admission-orders] {
  border: 1px solid var(--hairline, color-mix(in srgb, var(--fg, var(--custom-text-color)) 14%, transparent));
  border-radius: var(--bj-radius-sm, .375rem);
  display: grid;
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
}
checkout-admission ul[data-admission-orders] {
  margin: .25rem 0 0;
}
/* Every row is the same shape, whatever the panel's width: the order on
   the first line, its reference under it, and Cancel at the row's end,
   centred on the two (Vicky, 2026-10-10: a reference that wrapped on one
   row and not the next read as random). */
ul[data-admission-orders] li {
  align-items: center;
  display: flex;
  gap: .75rem;
  padding: .5rem .75rem;
}
ul[data-admission-orders] li > a:first-child {
  display: grid;
  flex: 1 1 auto;
  gap: .25rem;
  justify-items: start;
  min-inline-size: 0;
}
ul[data-admission-orders] li + li {
  border-block-start: 1px solid var(--hairline, color-mix(in srgb, var(--fg, var(--custom-text-color)) 14%, transparent));
}
ul[data-admission-orders] a {
  color: var(--fg, var(--custom-text-color));
  text-decoration: none;
}
ul[data-admission-orders] a:hover, ul[data-admission-orders] a:focus-visible {
  text-decoration: underline;
}
/* Cancel in the theme's link colour, visited or not, so it reads as the
   row's one action. */
ul[data-admission-orders] a.admission-order-cancel {
  color: var(--custom-link-color, var(--accent));
  flex: none;
  white-space: nowrap;
}
/* Each order's reference in the house dress for a code (ref_chip.css),
   which is the journey bundle's and so absent from the event page this
   message also shows on. The same declarations, kept in step with that
   file: monospace, letterspaced, in a chip off the text colour. */
ul[data-admission-orders] .ticket {
  background: color-mix(in srgb, var(--fg, var(--custom-text-color)) 6%, transparent);
  border-radius: var(--bj-radius-sm, 4px);
  color: var(--fg, var(--custom-text-color));
  display: inline-block;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: .84em;
  font-weight: 600;
  letter-spacing: .09em;
  padding: .18rem .42rem .18rem .5rem;
  white-space: nowrap;
}
checkout-bar:has(checkout-admission:not([hidden])) {
  gap: .5rem;
}
@media (max-width: 599px) {
  checkout-bar:has(checkout-admission:not([hidden])) {
    padding-block: .5rem calc(.5rem + var(--bj-pad));
  }
}

/* shared/owner_strips.css */
/* owner strips — the full-bleed lines a signed-in organiser meets above an
   event's own public page, and (with one deliberate exception below) nobody
   else ever does. Two of them today:

   .secret-preview   the page opens for you and answers not-found for
                     everyone else (event_secret.go). Without it, a page that
                     loads fine is the thing most likely to be read as
                     working when it isn't.
   .owner-strip      paid tickets are being kept off the page because no
                     payment door is open (shop.go's paidHidden). It reports
                     an ABSENCE, so there is nothing on the page beside it to
                     notice — which is why it moved up here from a tinted
                     note under the description (Vicky, 2026-08-12).

   .sandbox-strip    this whole page is a copy of the account, for testing,
                     and nothing on it is charged for real (sandbox.go). THE
                     ONE STRIP IN THIS FILE EVERYBODY SEES rather than only a
                     signed-in organiser — a sandbox address is openable by
                     anyone holding the link, on purpose, so the strip is the
                     only thing telling whoever is looking that the checkout
                     in front of them is not real.

   LITERAL COLOURS, and that is the whole point of the file. Two reasons:

   1. The event bundle carries no shared tokens at all (styles.go — event.html
      is its own document on the account's own --custom-* palette), so
      var(--warn) and friends do not exist here. Written against them the
      strip rendered as unstyled bold text.
   2. Even if they did, this is a message from Tito ABOUT the page rather than
      part of the page, and an organiser palette could tint it into invisibility
      or into looking like the event's own design. Sitting deliberately outside
      the account's colours is what says "this is not your event page talking".

   The pair is fixed and measures 15.0:1, so it clears AA on any account
   palette, and the strips are full-bleed above everything so neither can be
   mistaken for a banner the organiser chose. Both can be up at once — a
   secret event with no payments connected stacks them, caveat first.

   A THEMED PAGE LOADS NONE OF THIS. docs/themes.md § The CSS contract says so
   outright, and it is a promise to theme authors rather than an oversight —
   so the sandbox strip injected into a themed document carries the same
   declarations inline, as sandboxStripDecls in sandbox.go.
   TestSandboxStripStyleMatchesItsStripsRule compares the two declaration sets
   and fails if either side gains, loses or changes one. */
/* owner-notes — where they sit: A TOAST IN THE BOTTOM-RIGHT CORNER, narrow
   (Vicky, 2026-10-06). Two shapes came first and both got missed: a
   near-black band across the top, which on a dark browser read as part of the
   browser, and a white card above the event, which read as part of the page.
   A third, the event's own width centred at the foot, sat on top of the form
   it was talking about. In the corner it stays in view however far the
   organizer scrolls and keeps clear of the event's column on a wide screen.
   On a phone there is no corner to keep to, so it takes the width less the
   gutters.

   One container for all of them, so two notes stack in a column rather than
   landing on top of each other. The body keeps room under its content for
   them (padding below), so the page's own foot, and a Confirm at the bottom
   of a short page, can always be scrolled clear of the notes. */
owner-notes {
  bottom: 1rem;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  position: fixed;
  right: 1rem;
  width: min(18rem, calc(100% - 2rem));
  z-index: 20;
}
body:has(owner-notes) { padding-bottom: 6rem; }
.secret-preview,
.owner-strip,
.page-preview {
  /* LIGHT, WITH A SHADED BAND ON TOP (Vicky, 2026-10-13, replacing the
     darker left edge of 2026-10-06, which read as dated). A pale cobalt card
     with a lift under it: it floats clear of the page, and no event design
     looks like it. The band is .note-label below.

     Still literal colours, for both reasons the top of this file gives.
     #1e2a52 on #eef2fd measures 13.2:1; the cobalt label on it 6.9:1. Both
     hold on any account palette, light or dark. Left-aligned: sentences. */
  background: #eef2fd;
  border: 1px solid #c7d3f5;
  border-radius: 10px;
  overflow: hidden;
  /* The lift that floats it off the page: the elevation token where the
     page has one (public.css), and event_owner_notes.css on the standalone
     event page, which carries no tokens. */
  box-shadow: var(--shadow-pop, none);
  box-sizing: border-box;
  color: #1e2a52;
  font: 400 13px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  margin: 0;
  padding: .6rem .85rem;
  text-align: start;
}

/* "Only your team sees this", small, on a shaded band across the top of
   the note (Vicky, 2026-10-13). It names the NOTE as the private thing: the page is exactly what everybody sees. The negative
   margins take it to the card's edges from inside the card's padding, so the
   note's own text needs no wrapper. #1a45c4 on #dfe6fb measures 5.9:1. */
.owner-strip .note-label {
  align-items: center;
  background: #dfe6fb;
  border-bottom: 1px solid #c7d3f5;
  color: #1a45c4;
  display: flex;
  font-size: 12px;
  font-weight: 400;
  gap: .35rem;
  margin: -.6rem -.85rem .5rem;
  padding: .35rem .85rem;
}
.owner-strip .note-label svg { flex: none; }

/* The sandbox strip wears the admin's sandbox bar as it looks in dark mode
   (Vicky, 2026-09-09, PRO-4743): amber ink on an amber-tinted near-black, with
   the bar's amber rule under it. One look for "you are in a sandbox" on both
   sides of the product, and the dark version rather than the light one because
   it holds up on any account palette, light or dark.

   Written out for the same reason as everything above. #34291f is the bar's
   --warn-weak (amber-400 at 16%) laid over --slate-900, #796045 is its border
   (amber-400 35% into --slate-700), and #f2a33c is --amber-400 itself. Amber
   on that fill measures 6.8:1, which clears AA for this 13px text.

   STICKY, because it used to scroll away with the page and then turn up again
   somewhere different on the next step. It stays at the top of the window the
   whole way through a purchase, the way the admin bar stays above the shell.
   The z-index clears the checkout's own sticky foot (checkout_bar.css, 3). */
.sandbox-strip {
  background: #34291f;
  border-bottom: 1px solid #796045;
  color: #f2a33c;
  font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  margin: 0;
  padding: .45rem .9rem;
  position: sticky;
  text-align: center;
  top: 0;
  z-index: 10;
}

/* How far a sticky rail has to stand off the top so the strip does not sit on
   it: the strip's one line (13px × 1.4), its padding and its rule, rounded up.
   The rails read it as var(--sandbox-strip-height, 0px) (checkout_cols.css,
   event_columns.css), so a live page, which has no strip, pins them where it
   always did. The rails only exist at widths where the sentence fits on one
   line, which is what lets this be a number rather than a measurement.

   scroll-padding does the same job for keyboard focus and in-page anchors:
   something tabbed to must never land underneath the strip (WCAG 2.2, 2.4.11). */
body:has(> .sandbox-strip) { --sandbox-strip-height: 34px; }
html:has(> body > .sandbox-strip) { scroll-padding-top: 34px; }

/* The secret note is a deeper cobalt than the rest (Vicky, 2026-08-28): it
   is the one that carries no problem at all, and stacked with another note
   the two are told apart before either is read. With the side rule gone
   (2026-10-13) it is the outline that is deeper. */
.secret-preview { border-color: #9fb3ea; }

/* .page-preview is the third strip: an organiser looking at one of their own
   static pages from the admin (static_page_public.go). It is the only one that
   carries a link and a state, because it is the only one whose two states LOOK
   identical — a draft and a live page render the same, and whether anyone else
   can open it is the one thing a preview cannot show.

   Its link takes the note's own ink rather than the account's link colour:
   the pair is fixed so no palette can weaken it, and an account's blue on
   cobalt would be the one thing on it the organizer's colours could argue
   with. Underlined, so it still reads as a link without a hue. */
.page-preview {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: .4rem .9rem;
  justify-content: flex-start;
}

/* a[href], not a bare `a`: the buyer journey paints every link
   --journey-link from `body.buyer-journey a`, which outranks a lone class and
   would leave this one the account's link colour on our own card — the exact
   trap head_edit.css already records. The attribute buys the specificity
   without borrowing the journey's own class, which this file cannot depend on:
   the event page is a different document and has none. */
.page-preview a[href],
.owner-strip a[href] {
  color: #1a45c4;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The owner strip carries a link only when an audience's own switch is what
   hid the paid rows (PRO-5185): it goes to that audience, where the fix is.
   Same ink as the page preview's, for the same reason. */
.page-preview a[href]:hover,
.owner-strip a[href]:hover { text-decoration-thickness: 2px; }

/* Amber for a draft, green once it is live — said before the sentence is read.
   Never the only carrier: the words say the same thing. */
.page-preview .strip-dot {
  background: #f2a33c;
  border-radius: 50%;
  flex: none;
  height: 7px;
  width: 7px;
}

.page-preview[data-state="live"] .strip-dot { background: #43c489; }

/* The live order note (layout.html, event.html, live_order_note.go): a
   member about to pay for a REAL order. The notes' own cobalt, like every
   other note here (Vicky, 2026-10-13: one family, not a colour of its own). */

/* shared/studio_signpost.css */
/* studio-signpost — the dashed line that stands where an organizer's own words
   WOULD go, on a slot nobody has written in yet (Vicky, 2026-08-29: "Signpost
   on"). It answers the one question an always-present-but-empty slot raises:
   how do you know it's there?

   IT ONLY EVER RENDERS IN THE CUSTOMIZE CANVAS. shop.go sets StudioSignpost
   from previewState, which is set on the preview engine's shadow servers and
   nowhere else, so a buyer can never meet this on a live page and no email can
   carry it. That is a property of the render, not of this file — but it is the
   reason the file may look like admin chrome while living in a buyer bundle.

   IT EDITS NOTHING. Clicking it takes the organizer to the row in the Words
   sidebar that does (studio.js); it is a pointer, not a control, which is why
   it carries no border-radius flourish and no hover fill that would suggest
   something happens here.

   LITERAL COLOURS, for exactly the reason owner_strips.css spells out: this
   file is listed by the event bundle, which is a standalone document carrying
   none of the shared tokens, so var(--accent) would render as nothing. The
   cobalt is --cobalt-600 written out (#1a45c4), the same hand-copy the secret
   strip keeps. If the brand blue moves, this line moves with it by hand. */
studio-signpost {
  align-items: center;
  background: rgb(26 69 196 / .04);
  border: 1px dashed rgb(26 69 196 / .45);
  border-radius: 8px;
  color: #1a45c4;
  cursor: pointer;
  display: flex;
  font: 500 13px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  justify-content: center;
  margin-top: 1.75rem;
  min-height: 3.25rem;
  padding: .8rem 1rem;
  text-align: center;
}

studio-signpost:hover { background: rgb(26 69 196 / .09); }

/* A visible focus ring on its own terms. The canvas is an iframe inside the
   studio and an organizer can tab into it, so this has to be reachable and
   obviously reachable — and the page it sits on may be any account palette, so
   the ring is drawn rather than inherited. */
studio-signpost:focus-visible {
  outline: 2px solid #1a45c4;
  outline-offset: 2px;
}

/* shared/notes_block.css */
/* event-notes — the element every piece of Content an organizer has placed on
   a page renders in (content_placements.go). It was two fixed slots, one under
   the tickets and one on the order page (GitHub #1674); those are gone
   (PRO-4659) and the element is now whatever an organizer put where. It is a
   documented part of the buyer journey's CSS contract (docs/themes.md), which
   is why every rule here is written against the element and never against a
   wrapper class.

   ONE ELEMENT NOW. There were two, event-notes and order-notes, one per fixed
   slot, and this file existed to keep their rules from drifting apart. A piece
   is placed rather than pinned, so one element carries every one of them, and
   which piece it is is its data-src rather than a second tag name.

   NOTHING HERE MAY NAME A PALETTE, because the three surfaces it lands on do
   not share one. Pro paints from --custom-text-color on the account's own
   colours; Classic is a white card with literal ink (#30303a) and no --custom-*
   at all; the order page is the buyer journey's own chrome. A var() would
   render as nothing on at least one of them. Everything below therefore
   inherits, or derives from currentColor — which is the reader's ink on every
   one of them by construction.

   The words came from the composer, so the marks that can land in here are
   exactly the composer's: two heading levels, bold, italic, links, and the two
   kinds of list (message_marks.go). Nothing else is possible, so nothing else
   is styled. */
/* Sized and spaced as PAGE CONTENT, which is what it now is.

   Every property this rule used to carry said the same thing: a small footnote
   appended under the tickets, set at .95rem, held to 60ch, and fenced off from
   the page by a rule above it. That was true of the two fixed slots
   (GitHub #1674) and it stopped being true when they went (PRO-4659). A piece
   an organizer chose to put under the event name is not a footnote, and reading
   it at nine tenths of the size of the paragraph beside it, in a narrower
   column, with a hairline over it, is the page disagreeing with them
   (Vicky, 2026-09-09).

   So the size, the measure and the leading are the description's — 72ch is the
   number .pro-description holds itself to, and the two now sit at the same
   width whichever order they are in — and the space is a margin the page's own
   blocks already use rather than padding under a rule.

   One number rather than one per surface, because it only ever binds on one of
   them. Classic draws a placed piece INSIDE the tickets card, which is narrower
   than 72ch at any body size, so the cap is never what decides the measure
   there. */
event-notes {
  display: block;
  margin: 1.5rem 0 2rem;
  max-width: 72ch;
}

/* The place a piece sits at on a buyer screen with no arrangement
   (content_anchors.go): the booking form, the payment page, the order page and
   the ticket. A custom element is inline by default, and an inline box holding
   block children is the shape the studio's bar cannot measure. Nothing else
   on it, so an empty one draws nothing. */
content-place { display: block; }

event-notes p { margin: 0 0 .75rem; }

/* h1 and h2 are what the composer's two heading buttons produce here, the same
   pair a static page emits (static_page_render.go). They are sized as headings
   INSIDE a block, not as the page's own — the event's name is the h1 a reader
   has already met at the top, and a second one at that scale under the tickets
   would read as a second page starting.

   CLAIMED TWICE, and this is the one thing in the file that is not simply a
   rule. Every frame this block lands in sizes its own headings from a CLASS —
   `.pro-card h1`, `.classic-card`'s card, `body.buyer-journey h1` — and a class
   outranks any bare element selector however it is written. Unclaimed, the
   organizer's "When you get here" came out at the page's own title size: 36px
   light on the Pro event page, 1.6rem on the order page, against body text.
   Naming the three frames is what makes one set of declarations hold on all of
   them.

   It is a SELECTOR LIST and not a second copy of the rule, so the numbers move
   once. And the bare form is still first, because a THEMED page has none of
   those classes and is the case the plain rule is for. */
event-notes h1,
event-notes h2,
:is(.pro-card, .classic-card, body.buyer-journey) event-notes h1,
:is(.pro-card, .classic-card, body.buyer-journey) event-notes h2 {
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1.35;
  margin: 1.25rem 0 .5rem;
}

event-notes h2,
:is(.pro-card, .classic-card, body.buyer-journey) event-notes h2 { font-size: 1rem; }

event-notes ol,
event-notes ul { margin: 0 0 .75rem; padding-left: 1.25rem; }

event-notes li { margin: 0 0 .3rem; }

/* The block owns the space above and below itself — the rule and the margin
   are its own — so its first and last children may not add to it. Without this
   an organizer whose block opens with a heading gets a gap that reads as a
   mistake in the page rather than as a choice they made.

   LAST IN THE FILE, and claimed at both weights for the same reason the
   headings are. The heading rule above is the more specific of the two at every
   weight and would win wherever these were written, so being last is what
   settles it. */
event-notes > :first-child,
:is(.pro-card, .classic-card, body.buyer-journey) event-notes > :first-child { margin-top: 0; }
event-notes > :last-child,
:is(.pro-card, .classic-card, body.buyer-journey) event-notes > :last-child { margin-bottom: 0; }

/* Underlined, always. A link inside a paragraph of somebody else's prose is
   the one place colour alone cannot carry it — the surrounding ink is the
   organizer's and the link's colour is the organizer's too, so on a palette
   where those are close the link would be invisible. The Pro skin additionally
   gives it --custom-link-color (event_pro.css), beside <event-description> and
   <venue-links>, which are the other two elements in this contract. */
/* Not a button, though: a piece's button is an a.button now
   (writeStaticButton), dressed as one by the skin it lands on, and an
   underline would make it a link wearing a button's box. */
event-notes a:not(.button) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

event-notes a:not(.button):hover { text-decoration-thickness: 2px; }

/* shared/button_row.css */
/* A row of buttons in a piece of Content: the buttons on consecutive lines,
   drawn as one p.sp-button-row (static_page_render.go), stacked or side by
   side as the row's first button says (data-layout="side"). One button alone
   is a row of one.

   Layout only, so it can be one file on both bundles: the event page's skins
   (event_pro.css, event_classic.css) and the journey's link-button
   (journey_shell.css) dress each button, and this decides where they sit.
   p.sp-button-row rather than the bare class, so a side-by-side row outranks
   the skins' full-width rule, which loads after this on the event page.

   Side by side, each button grows from 12rem, so two or three share a line on
   a laptop and wrap to one a line on a phone rather than squeezing their
   words. */
p.sp-button-row { display: flex; flex-direction: column; gap: .6rem; }
p.sp-button-row > a.button { margin: 0; }
p.sp-button-row[data-layout="side"] { flex-flow: row wrap; }
p.sp-button-row[data-layout="side"] > a.button { flex: 1 1 12rem; width: auto; }

/* shared/site_chrome.css */
/* site-header / site-footer — a look's own header and footer (site_chrome.go),
   drawn above and below every public page on the account's own hostname.

   DELIBERATELY PLAIN. These rules lay the parts out and nothing more: no
   colour of their own, no font, no background. What the header LOOKS like is
   the account's own stylesheet's job, written against these element names, so
   anything decided here is something an organizer would have to undo first.
   Colour and type come from the page they sit on.

   In shared/ because it belongs to both bundles: the public one and the event
   page's own (styles.go). */
site-header { align-items: center; display: flex; gap: 1.5rem; padding: .75rem 1.25rem; position: relative; }
site-header .site-logo { flex-shrink: 0; margin-inline-end: auto; }
:is(site-header, site-footer) .site-logo img { display: block; height: auto; max-height: 3rem; max-width: min(18rem, 60vw); }
site-header > nav { flex: 1; }
site-header nav ul { align-items: center; display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; justify-content: flex-end; list-style: none; margin: 0; padding: 0; }
site-action { flex-shrink: 0; }
/* The links take the page's text colour, as the social icons below do. With
   nothing said they were the browser's own #0000ee, which on a dark page is
   barely there (Vicky, 2026-10-08, on a header read from a dark website).
   Inherit is the page's colour, not one of ours, and :where() weighs nothing,
   so an account's own stylesheet still has the last word. */
:where(site-header, site-footer) a { color: inherit; }
/* The page's own switchers are never in the header: it is the organizer's
   content and nothing else (Vicky, 2026-10-06). With a header on the page
   they leave the corner, which is the header's, and sit in flow under it, at
   the right of the page's own top area: the shape a phone already gives
   them (lang_switch.css), at every width. relative, not static, so the open
   panel still hangs from its own pill. */
body:has(> site-header) .lang-switch { margin: 0 var(--bj-pad, 1.25rem) .55rem auto; position: relative; right: auto; top: auto; width: max-content; }
body:has(> site-header) .lang-switch ~ .lang-switch { top: auto; }
site-action a { border: 1px solid currentColor; border-radius: 999px; display: inline-block; padding: .45rem 1rem; text-decoration: none; }

/* The phone menu. A <details>, so it opens with no script; closed, only its
   summary is on the page. Hidden above the breakpoint, where the links sit in
   the row itself, and display:none takes the second copy out of the
   accessibility tree too.

   position: relative and the z-index are on the BUTTON, and that is what the
   panel below hangs from. Hung off the header instead, the panel was only
   under the button while the header happened to be positioned — and an
   account stylesheet has every reason to position it, or to stop positioning
   it on a phone, which sent the panel to the page's own bottom corner:
   748px below the button on a real account, off the screen. The z-index
   clears the language switcher's own panel (public/lang_switch.css), which
   can be open at the same time.

   The panel is right-aligned to the button and capped at the window, so a
   long label grows it leftward rather than off the screen; the links wrap
   inside it, because a word with no break in it would do the growing. */
details.site-menu { display: none; position: relative; z-index: 40; }
details.site-menu > summary { cursor: pointer; display: grid; list-style: none; padding: .5rem; place-items: center; }
details.site-menu > summary::-webkit-details-marker { display: none; }
details.site-menu > nav { background: Canvas; border-radius: .75rem; box-shadow: var(--shadow-menu); color: CanvasText; inset-inline: auto 0; max-width: calc(100vw - 2.5rem); min-width: max(12rem, 40vw); overflow-wrap: anywhere; padding: 1rem 1.25rem; position: absolute; top: calc(100% + .5rem); }
details.site-menu > nav ul { align-items: stretch; flex-direction: column; gap: .75rem; }

site-footer { display: block; padding: 3rem max(1.25rem, (100% - 76rem) / 2) 1.5rem; }
site-footer-head { align-items: center; display: flex; flex-wrap: wrap; gap: 1rem 2rem; justify-content: space-between; margin-bottom: 2.5rem; }
.site-social { display: flex; flex-wrap: wrap; gap: 1rem; list-style: none; margin: 0; padding: 0; }
.site-social a { color: inherit; display: block; }
.site-social svg { display: block; height: 1.5rem; width: 1.5rem; }
site-footer-columns { display: grid; gap: 2rem; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); }
site-footer-columns h2 { font-size: 1.05rem; margin: 0 0 .75rem; }
site-footer-columns ul { display: grid; gap: .6rem; list-style: none; margin: 0; padding: 0; }
.site-image { display: block; height: auto; margin-top: 3rem; max-width: 100%; }
.site-note { font-size: .875rem; margin: 2.5rem 0 0; }

/* Words a screen reader needs and a sighted reader does not. Its own class
   rather than .sr-only, because the event bundle does not carry that sheet. */
.site-sr { border: 0; clip-path: inset(50%); height: 1px; overflow: hidden; padding: 0; position: absolute; white-space: nowrap; width: 1px; }

/* The buyer shell insets the page (journey_shell.css), and the header and
   footer are the site around it, so they reach the window's edges the way the
   owner strips do. A strip above the header hands its gap on to the header.
   The page qualifiers sit inside :where() so each of these weighs no more than
   a bare element name: an account's own stylesheet moves the header with
   `site-header { margin: ... }` rather than by out-shouting us. */
body.buyer-journey:not([data-embed-sheet]):has(> site-header) { padding-top: 0; }
:where(body.buyer-journey:not([data-embed-sheet])) > site-header { margin: 0 -1.25rem 3rem; }
body.buyer-journey:not([data-embed-sheet]) > .sandbox-strip:has(+ site-header) { margin-bottom: 0; }
body.buyer-journey:not([data-embed-sheet]):has(> site-footer) { padding-bottom: 0; }
:where(body.buyer-journey:not([data-embed-sheet])) > site-footer { margin: 3rem -1.25rem 0; }

@media (max-width: 899px) {
  site-header > nav, site-header > site-action { display: none; }
  details.site-menu { display: block; }
}
@media (max-width: 599px) {
  site-header { flex-wrap: wrap; row-gap: .5rem; }
  site-header .site-logo img { max-width: min(12rem, 45vw); }
  :where(body.buyer-journey:not([data-embed-sheet])) > :is(site-header, site-footer) { margin: 0; }
}
@media print {
  site-header, site-footer { display: none; }
}
/* The event page's note about an unoffered language leaves room for the
   switcher in the corner (event_lang_switch.css). With a header the switcher
   is in flow under it, so the note takes the full width. */
body:has(> site-header) > .lang-unoffered { padding-right: 0; }

/* shared/event_hero.css */
/* ---- The hero (PRO-4797) ----
   The banner drawn as a background, with the event's title over it as live
   text and the logo on it. It takes the stub band's place at the top of the
   card, on the event page and on every buyer page after it (hero.go).

   Under shared/ because two documents draw it: the standalone event page and
   the journey layout. It reads its own two tokens, --custom-hero-ink and
   --custom-hero-background, and the People artwork reads
   --custom-heading-color, so its drawing re-tints with the palette. Both
   documents declare all three on :root, so one file serves both instead of
   a second copy drifting from the first.

   The stub band follows it: the hero takes the band's banner half, not its
   name or its tear.

   The picture is an <img> under the text rather than a CSS background: an
   account's URL never goes inside a url(), and object-position is how the
   focus control moves it. The colour behind it is what shows while it loads
   and where it never does, which is why the studio measures it against the
   title colour. */
event-hero {
  background: var(--custom-hero-background, #27272a);
  box-sizing: border-box;
  color: var(--custom-hero-ink, #ffffff);
  display: grid;
  /* One column that can shrink below its content, so a long title wraps
     inside the banner instead of widening the track past it.

     Eight rows, three bands (Vicky, 2026-09-28): top, middle and bottom
     each have a row for the logo and a row under it for the title, with
     the two stretchy rows between the bands taking whatever height is
     spare. Each of the two sits in its own band, and one sharing a band
     with the other is stacked under the logo rather than drawn over it,
     so the logo and the title can be put anywhere without ever covering
     each other. An empty row has no height. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto auto minmax(0, 1fr) auto auto;
  min-height: 11rem;
  overflow: hidden;
  padding: 1.25rem 1.5rem;
  position: relative;
}
/* The 3:1 shape every banner has, held by a spacer rather than by
   aspect-ratio: with aspect-ratio, the min-height below is carried across the
   ratio into a minimum WIDTH, and on a phone the hero came out wider than the
   screen. A percentage padding resolves against the width, so this can only
   ever make the hero taller, never wider. */
event-hero::before {
  content: "";
  grid-column: 1;
  grid-row: 1 / -1;
  padding-top: 33.333%;
}
event-hero > .eh-pic {
  block-size: 100%;
  inline-size: 100%;
  inset: 0;
  object-fit: cover;
  object-position: center center;
  position: absolute;
}
event-hero[data-focus="top"] > .eh-pic { object-position: center top; }
event-hero[data-focus="bottom"] > .eh-pic { object-position: center bottom; }
/* The logo has a place of its own, top left unless the organizer moved it
   (data-logo, below). */
event-hero > .eh-logo {
  align-self: start;
  display: block;
  grid-column: 1;
  grid-row: 1;
  justify-self: start;
  max-height: 44px;
  max-width: 160px;
  object-fit: contain;
  position: relative;
}
/* Regular weight, not bold: the DFA title this was built from is set light
   and large, and a heavy face over a photograph reads as a caption shouting.
   The size never drops under 1.75rem, which keeps it LARGE text for WCAG and
   is what the studio's 3:1 check assumes.

   Qualified by where it is because on the event page the title is the h1,
   and .pro-card h1 (event_pro.css, a later sheet) sets the heading colour,
   size and weight for every other h1 on that card. */
:is(.pro-card, main) > event-hero > .eh-title {
  align-self: end;
  color: inherit;
  font-family: var(--theme-font-heading, inherit);
  font-size: clamp(1.75rem, 5cqi, 2.4rem);
  font-weight: 400;
  grid-column: 1;
  grid-row: 8;
  justify-self: end;
  line-height: 1.12;
  margin: 0;
  max-width: 85%;
  position: relative;
  text-align: end;
  text-wrap: balance;
}
event-hero[upper] > .eh-title { text-transform: uppercase; }
/* Nine presets, each a band and a side (Bill, 2026-09-18). The default
   above is bottom right; these move one axis or both. "center" alone is the
   middle of both. */
event-hero[data-place^="top"] > .eh-title { align-self: start; grid-row: 2; }
event-hero[data-place^="center"] > .eh-title { align-self: center; grid-row: 5; }
event-hero[data-place$="left"] > .eh-title { justify-self: start; text-align: start; }
event-hero:is([data-place$="-center"], [data-place="center"]) > .eh-title { justify-self: center; text-align: center; }
/* The logo's own nine, the same way, one row above the title's in each band.
   It no longer moves to make room for the title (Vicky, 2026-09-28: "what
   if I wanted logo top middle and title bottom middle?"). The bands do that
   now. "under" never reaches here: that logo is in the band below. */
event-hero[data-logo^="center"] > .eh-logo { grid-row: 4; }
event-hero[data-logo^="bottom"] > .eh-logo { grid-row: 7; }
event-hero[data-logo$="right"] > .eh-logo { justify-self: end; }
event-hero:is([data-logo$="-center"], [data-logo="center"]) > .eh-logo { justify-self: center; }
/* Sharing a band, the logo stands a little clear of the title under it. */
event-hero:is([data-place^="top"][data-logo^="top"], [data-place^="center"][data-logo^="center"], [data-place^="bottom"][data-logo^="bottom"]) > .eh-logo { margin-bottom: 0.75rem; }

/* Where it sits. On the event page it is the card's first child, full
   bleed, like the banner it replaces. In the journey the card pads its
   content, so the hero bleeds out to the card's edges exactly as the stub
   band does (styles/public/stub_band.css). */
.pro-card > event-hero { border-radius: var(--custom-card-border-radius, 0) var(--custom-card-border-radius, 0) 0 0; }
body.buyer-journey > main > event-hero {
  border-radius: var(--journey-radius) var(--journey-radius) 0 0;
  margin: 0 calc(var(--bj-pad) * -1) var(--bj-section);
}
@media (max-width: 599px) {
  body.buyer-journey > main > event-hero { border-radius: 0; }
}

/* The stub band under the hero (Bill, 2026-09-18). It meets the picture
   square: the hero owns the card's top corners. With no name in it the band
   is `bare` and has no height of its own (stub_band.css, for every header),
   so the perforation and the notches run along the foot of the picture. */
event-hero + stub-band { border-radius: 0; }
body.buyer-journey > main > event-hero:has(+ stub-band) { margin-bottom: 0; }

/* The People artwork (Homage, 2026-10-07): the account overview's line
   drawing (dashboard_page.css, greet-band), drawn as a MASK in the heading
   colour so it re-tints with the palette, and faded out behind the middle
   where the event's name sits. Here rather than with the other pieces in
   design_banner.go because it is a file: a relative url() in this sheet
   resolves under /static/ on every host, which one in the page's head
   would not. ::after because ::before is the hero's 3:1 spacer above. */
event-hero[data-art="people"]::after {
  background: var(--custom-heading-color);
  content: "";
  inset: 0;
  mask: url(people-pattern.png) 0 0 / 380px, radial-gradient(ellipse 30% 46% at 50% 50%, transparent 45%, #000 85%);
  mask-composite: intersect;
  opacity: .14;
  pointer-events: none;
  position: absolute;
  z-index: -1;
}
/* Beside the words (the Split header) the name is not in the hero, so the
   fade behind the middle framed nothing and read as a hole in the drawing
   (Vicky, 2026-10-09). The pattern runs edge to edge there instead. */
event-split > event-hero[data-art="people"]::after { mask: url(people-pattern.png) 0 0 / 380px; }
/* Ruled (Ledger) keeps its lines to the lower 45%, under the name. Beside the
   words there is no name in it, and the rest was a flat black slab (the
   11-theme pass, 2026-10-09), so beside the words it is a whole ledger page:
   the rules full height and a step stronger, and a margin rule down the left
   in the link colour. The ground is still the organizer's. */
event-split > event-hero[data-art="ruled"] {
  background:
    linear-gradient(90deg, transparent 3rem, color-mix(in srgb, var(--custom-link-color) 80%, transparent) 3rem calc(3rem + 1px), transparent calc(3rem + 1px) calc(3rem + 4px), color-mix(in srgb, var(--custom-link-color) 80%, transparent) calc(3rem + 4px) calc(3rem + 5px), transparent calc(3rem + 5px)),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--custom-hero-background), #fff 22%) 0 1px, transparent 1px 1.75rem),
    var(--custom-hero-background);
}
/* ti.to's offset bar under the name follows the name's side. */
:is(.pro-card, main) > event-hero[data-art="people"]:is([data-place$="-center"], [data-place="center"]) > .eh-title::after { margin-inline: auto; }
:is(.pro-card, main) > event-hero[data-art="people"][data-place$="right"] > .eh-title::after { margin-inline-start: auto; }
@media (prefers-contrast: more) {
  event-hero[data-art="people"]::after { display: none; }
}

/* shared/customer_signed_in.css */
/* customer-signed-in — "Signed in as Example School", the member's own
   address, and "Your account" (customer_signed_in.go), and part of the
   public contract (docs/themes.md).

   In shared/ because it is drawn in two documents: at the top of the "Your
   order" rail on the details step and the payment page (the journey bundle),
   and on the event page in the checkout row, where <customer-sign-in> would
   otherwise be (the event bundle). So the address wears each page's own
   .muted rather than a colour named here. The link wears the journey's own
   link colour in the rail, and on the event page the sign-in line's recipe:
   the organizer's link colour, with Classic's blue set in event_classic.css.

   Small and quiet everywhere: it is a statement of who is buying, not a
   question, and nothing on the screen should be read before it except the
   things it sits beside. */
customer-signed-in {
  display: block;
  font-size: .875rem;
  /* A school's address can be one long word: it breaks rather than pushing
     the rail wider than its column. */
  overflow-wrap: anywhere;
}
/* The sentence on its own line; the address and the link share the next. */
customer-signed-in > span:first-child { display: block; }
customer-signed-in a { font-weight: 600; }
customer-signed-in a:focus-visible { border-radius: 4px; outline: 2px solid currentColor; outline-offset: 2px; }

/* In the rail: the first thing in it, at the size of the rows under it, with
   the gap the heading leaves under itself so the two read as separate. */
order-rail > customer-signed-in { font-size: var(--bj-support, .875rem); margin-block-end: 1rem; }

/* On the event page: where the sign-in line sits, on that line's terms
   (event_customer_sign_in.css): centred under the button, a fingertip away
   from it, and 1rem under the quote link when there is one. */
[data-purchase-footer] customer-signed-in { margin-block-start: 1.6rem; text-align: center; }
[data-purchase-footer] customer-signed-in a { color: var(--custom-link-color, currentColor); }
.quote-instead + customer-signed-in { margin-block-start: 1rem; }

/* shared/embed_sheet.css */
/* The widget's purchase sheet: what a buyer page looks like when it is being
   rendered inside the iframe on the organiser's own website, lifted over
   their page (embed_journey.go, layout.html's embed-sheet-top/bottom).

   THE SHEET'S TOP EDGE IS THE SAME ON EVERY STEP, and that is the point of
   nearly every rule here (Vicky, 2026-08-10: "it needs to be static in the
   centre of the page", after an earlier attempt where the modal jumped
   around). Tickets, details, payment and confirmation are four very
   different heights, so a box CENTRED on its content re-centres on each one
   and hops up the screen under the buyer's pointer.

   It used to be held still by a fixed height, and that drew a blank band
   under every step shorter than 640px (Paul, 2026-09-24: "a phantom eyebrow
   and chin"). So the height follows the content now, and what is held still
   is the TOP — where Back, the step's title and the close live, the three
   things a pointer goes back to:

     - a fixed WIDTH, and a TOP pinned where a 640px box centred in the
       window would start (--sheet-top), whatever is inside;
     - a height that is the content's, up to the room left below that top;
     - one scrolling region, <sheet-scroll>, between a header and a footer
       that are outside it, which scrolls only when the room runs out.

   Change any of those three and the sheet starts moving again.
   TestEmbedSheetDoesNotMoveBetweenSteps holds the top, the left and the
   width still across three steps.

   IT IS IN shared/ BECAUSE THE SHEET SPANS TWO DOCUMENTS, and that is the
   one thing to know before editing it. The first step renders the event's
   own purchase form, which is styled by the EVENT bundle (styles/event/, on
   the account's --custom-* palette) in a document of its own — exactly as
   event.html is. Every step after it is an ordinary buyer page in the
   layout.html shell, on the public bundle's --journey-* palette. Two
   bundles, one chrome, out of one file, so the box cannot drift between the
   step that sells and the step that asks; styles.go lists it as a trailing
   group on both.

   That is why every colour here resolves through --sheet-*: each chain names
   the journey token first, the event page's equivalent second, and Classic's
   own literal last (Classic sets neither vocabulary — its card is #fff and
   its rules are hardcoded, so the last leg is that card, copied from
   event_classic.css). The legs are the same colour by construction —
   --journey-card and --custom-text-background-color are both
   Design.CardBackground, and so on down the list. A token that existed on
   only one side would be a sheet that changed colour on step two.

   THE SCRIM DOES NOT DIM THE ORGANISER'S PAGE, and cannot. A cross-origin
   iframe never composites onto the page behind it — the browser paints the
   frame's own canvas backdrop (#450, #732) — so what is behind the sheet is
   OUR backdrop, and the scrim is what makes it read as a page dimmed under a
   modal rather than as a flat colour. Nothing here may reintroduce
   transparency on the frame; that road was walked and written down twice. */

/* The frame's canvas is PAINTED, never left to the browser — the same
   lesson as #450/#732, arriving one document further in. What is behind the
   sheet is our own canvas (a cross-origin iframe does not composite onto the
   page behind it), and an unpainted canvas is filled by the browser from
   whatever it decides the context is: viewed here it came out near-black
   under a light design, which turns the scrim below from a dim into a void.
   So the account's own page colour goes on deliberately, and the scrim dims
   THAT. --bg carries it in both documents — layout.html's shell sets it from
   the journey palette, event.html's head sets it from the account theme —
   which is why this one is not a --sheet-* chain like the rest. */
body[data-embed-sheet] { background: var(--bg, #fff); min-height: 0; padding: 0; }

/* Every chrome rule below is written `body[data-embed-sheet] .es-…`, and the
   prefix is load-bearing rather than tidy scoping. On the later steps this
   file is read inside the public bundle, where `body.buyer-journey footer`
   and `body.buyer-journey a` outrank a bare class — measured, and what it
   cost was a footer that spread across the sheet on the step that sells and
   huddled in the middle on the step that asks, with the close control
   turning link-blue on the way. Same box, same chrome, both documents: the
   prefix is what makes the second half of that true. */

/* The page inside the sheet gives up the buyer journey's card: the sheet IS
   the card, and a card inside a card at 460px is two borders and two lots of
   padding around a form that has neither to spare. Only the layout.html
   steps have a <main> to undo — the first step's own document has none.

   `sheet-scroll > main`, not `body > main`, and the difference was 64px of
   dead space under every confirmed order (Vicky, 2026-08-10: "quite a lot of
   white space at the bottom"). The chrome wraps the page — scrim, sheet,
   scroll region — so <main> is two levels down and a child selector on body
   matched nothing at all, silently: the rule was there, it was correct, and
   it applied to no element on any step. The journey's own rule is
   `body.buyer-journey > main` and lands a class-and-two-types, so this has
   to carry one more than that to win rather than merely tie. */
body[data-embed-sheet] sheet-scroll > main {
  background: none; border-radius: 0; box-shadow: none; margin: 0;
  max-width: none; min-height: 0; padding: 0;
}

/* The page's own credit line is emptied in sheet mode (layout.html: the
   sheet's foot carries the Tito line already), and an emptied element still
   brings its 48px top margin with it — 48px of nothing under the last thing
   a buyer read. Zero-height and no words means no gap either.

   :not(:has(*)), not :empty. The template leaves a newline between the
   comment and the credit it no longer draws, and a text node — even one of
   whitespace — is enough to make :empty false. So the rule matched nothing
   and the 48px stayed, as the blank band under the details step's button
   (Paul, 2026-09-24). "Has no element in it" is what "emptied" means here. */
body[data-embed-sheet] sheet-scroll > main > footer:not(:has(*)) { margin: 0; }

/* THE SHEET'S SIZE IS DECLARED ONCE, HERE, AND THE LOADER READS THE SAME
   WIDTHS. Since the lift, the organiser's page is dimmed by a scrim in
   THEIR document and our iframe is sized to the card — which means the
   loader (internal/embedjs/geometry.js) has to know how wide the sheet is.
   They agree because TestSheetSizeAgreesWithTheLoader parses both and
   compares w, full, wide and wide-at; a number changed here alone fails
   that test. Change them together. --tito-sheet-h is this stylesheet's alone,
   for the sheet opened straight (not framed): the loader's card has its own
   height (the push, geometry.js).
     --tito-sheet-w: 460px  --tito-sheet-h: 640px  --tito-sheet-full: 520px
   (h = the height the TOP is placed for, no longer the sheet's height;
   full = at and below this viewport width the sheet IS the screen).
     --tito-sheet-wide: 1020px  --tito-sheet-wide-at: 1100px
   (at and above wide-at, a purchase sheet is as wide as the event page's own
   card, so the steps with two columns on the full page have them here). */
:root {
  --tito-sheet-full: 520px;
  --tito-sheet-h: 640px;
  --tito-sheet-w: 460px;
  --tito-sheet-wide: 1020px;
  --tito-sheet-wide-at: 1100px;
}

/* The scrim inside the frame. Since the lift there is nothing behind the
   sheet for it to dim — the frame is the sheet's own rectangle now, and the
   dimming the visitor sees is the loader's, drawn on the organiser's page
   where their content actually is. It stays for the one case that still has
   a surround: the sheet opened on our own host, outside any widget, which is
   how it is reviewed and how a `?embed=` link behaves if somebody shares
   one. */
embed-scrim {
  background: color-mix(in srgb, #000 55%, transparent);
  inset: 0; position: fixed; z-index: 0;
}

embed-sheet {
  /* The two token vocabularies this chrome can find itself inside, and
     Classic's literals last. See the note at the top of this file. */
  --sheet-card: var(--journey-card, var(--custom-text-background-color, #fff));
  --sheet-fg: var(--fg, var(--custom-text-color, #30303a));
  --sheet-heading: var(--journey-heading, var(--custom-heading-color, #252535));
  --sheet-line: var(--line, var(--custom-control-border, #e1e1e1));
  --sheet-muted: var(--muted, var(--custom-text-color-muted, #65656d));
  --sheet-radius: var(--journey-radius, var(--custom-card-border-radius, 16px));

  /* The top edge, pinned: where a --tito-sheet-h box centred in the window
     would start, and never closer to the edge than 1.5rem. The sheet opened
     straight only; framed, the loader places the card. */
  --sheet-edge: 1.5rem;
  --sheet-top: max(var(--sheet-edge), calc((100dvh - var(--tito-sheet-h)) / 2));

  background: var(--sheet-card);
  border-radius: var(--sheet-radius);
  bottom: auto;
  color: var(--sheet-fg);
  display: flex;
  flex-direction: column;
  /* The content's height, up to the room below the pinned top: see the note
     at the top of this file. */
  height: auto;
  left: 0;
  margin: 0 auto;
  max-height: calc(100dvh - var(--sheet-top) - var(--sheet-edge));
  overflow: hidden;
  position: fixed;
  right: 0;
  top: var(--sheet-top);
  /* No drop shadow, and that is a decision rather than an omission: the
     sheet always sits on the scrim above, 55% black in either scheme, where
     a black shadow paints nothing anybody can see. The elevation tokens are
     the shared bundle's and do not reach the event document at all, so
     asking for one here would be a shadow that appeared on step two and not
     on step one — the exact drift this file exists to prevent. */
  width: min(var(--tito-sheet-w), 100%);
  z-index: 1;
}

/* The organizer's own step-bar colours (PRO-5305, docs/themes.md), declared
   here as initial, the guaranteed-invalid value, so every
   var(--custom-step-header-…, …) below keeps its own fallback until their
   stylesheet sets one. Zero weight, so their :root rule wins wherever it
   loads. Here and not on the page's own :root, whose bytes the look goldens
   record. This file rides both bundles, so it covers the widget's box too. */
:where(:root) {
  --custom-step-header-background: initial;
  --custom-step-header-color: initial;
}

/* THE HEADER IS THE STEP'S OWN LINE: Back, what this step is ("Your order",
   "Your details"), and the close (Paul, 2026-09-24). The title used to be a
   page-sized h1 at the top of the scrolling body, which spent the first
   screen of a 460px box saying which step it was and scrolled away with the
   first flick; in the header it says so for the whole step in one line.

   The close sits tight in the corner, as far from the top edge as from the
   right one: the header's block padding and its end padding are the same
   number, and the header is exactly one close control tall inside them.
   What sits beside it is centred on the same line. */
body[data-embed-sheet] .es-head {
  --es-corner: .6rem;
  align-items: center;
  /* The organizer's own band, if their stylesheet sets one (PRO-5304,
     docs/themes.md): --custom-step-header-background paints it and
     --custom-step-header-color colours its title, Back and Close. Unset,
     the header is what it always was. */
  background: var(--custom-step-header-background, transparent);
  border-bottom: 1px solid var(--sheet-line);
  display: flex;
  flex: none;
  gap: .5rem;
  min-height: 2.25rem;
  padding: var(--es-corner) var(--es-corner) var(--es-corner) 1.15rem;
}

/* THE EVENT'S NAME DOES NOT APPEAR INSIDE THE WIDGET, on any step. The
   journey's steps each open with it as an eyebrow, which is right on our own
   site — a buyer arriving from an email needs telling what they are paying
   for — and wrong in a box pasted onto the event's own page, where they read
   it a second ago (Vicky, 2026-08-10).
   Matched by data-src rather than by class: the SAME eyebrow carries the
   order reference on the order page ("Order R7X2QK"), which is not the event
   name and is worth keeping. The attribute is what tells them apart, and it
   is already there for the Customize studio's source map. */
body[data-embed-sheet] .bj-eyebrow[data-src="content:event.name"] { display: none; }

/* Every link in a step is the sheet's own text colour, underlined (Vicky,
   2026-09-29: "Change" and "Edit order" were the only colour in the widget,
   in the account's link blue, while the recap's Change was already text).
   :where keeps this under any component that dresses its own links, and a
   link styled as a button keeps its button, as does a whole row that is a
   link (change_list.css, on the confirmation). The event page's link colour
   is the event page's; in the sheet it read as a leftover. */
body[data-embed-sheet].buyer-journey :where(sheet-scroll a:not(.button, change-list > a)) {
  color: var(--sheet-fg);
  text-decoration-line: underline;
}
/* The way back keeps its own dress (back_link.css: the arrow, underlined
   only under the pointer) and takes the same colour. */
body[data-embed-sheet].buyer-journey a.back-link { color: var(--sheet-fg); }

/* With the event's name gone, the chosen times' recap would open the sheet
   on a bare rule and a gap (row.css gives .rows both, to sit under a
   heading this step does not draw here). The step opens on the recap
   instead, and the space down to the summary and on to the first question
   is the sheet's step rather than a page's section (Vicky, 2026-09-29: the
   name field was below the fold of a desktop sheet). */
body[data-embed-sheet] .bj-eyebrow[data-src="content:event.name"] + .rows { border-top: 0; margin-top: 0; }
body[data-embed-sheet] main > .rows + checkout-cols { margin-block-start: 1.25rem; }
body[data-embed-sheet] order-rail { margin-block-end: 1.5rem; }
/* The details form's first heading already has the summary's gap above
   it; its own section margin on top of that was a band of nothing between
   the total and the first thing to type. */
body[data-embed-sheet] #checkout-details > h2[data-src="copy:public.checkout.purchaser"] { margin-block-start: 0; }

/* The step's title. An h1 — it is the heading of what the sheet is showing,
   and the body no longer draws one of its own — at the size of chrome rather
   than of a page. flex:1 is what keeps the close at the right edge whether
   or not a Back sits in front of it. */
body[data-embed-sheet] .es-title {
  color: var(--custom-step-header-color, var(--sheet-heading));
  flex: 1;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: normal;
  line-height: 1.3;
  margin: 0;
  min-width: 0;
  /* It WRAPS rather than being cut off (WCAG 2.2, 1.4.4; Fable review): at
     200% text size a longer translation of "Complete your payment" beside
     Back is more than one line in 460px, and cutting it short there is words the
     reader can no longer see. At ordinary sizes every title fits on one. */
  overflow-wrap: anywhere;
}

/* Back and the title are one group: a hairline between them, so "‹ Back"
   and "Your details" read as a way out and a name rather than as one
   phrase. */
body[data-embed-sheet] .es-back + .es-title {
  border-left: 1px solid var(--sheet-line);
  padding-left: .6rem;
}

body[data-embed-sheet] .es-back {
  align-items: center;
  color: var(--custom-step-header-color, var(--sheet-muted));
  display: inline-flex;
  font-size: .9rem;
  gap: .25rem;
  text-decoration: none;
}

/* The same Back as a button (layout.html, EmbedBackForm): nothing of a
   button's own dress, so the two read as one control. */
body[data-embed-sheet] button.es-back {
  background: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  font-size: .9rem;
  margin: 0;
  padding: 0;
}

body[data-embed-sheet] .es-back:hover { color: var(--custom-step-header-color, var(--sheet-fg)); }

/* The recap's Change in the sheet, where it submits the header's Back form
   (chosen_schedule.html): a link's dress on a button, in the sheet's own
   text colour. */
body[data-embed-sheet] .schedule-change {
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font: inherit;
  margin: 0;
  padding: 0;
  text-decoration: underline;
}
body[data-embed-sheet] .es-back:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

/* Back and Change while the spam check runs (checkout-admission.js puts
   .busy on the one pressed): the ring replaces the words. Said here on
   purpose, because the colours above outrank button.busy and only the
   journey shell's rule happened to win by one point (PRO-5145). */
body[data-embed-sheet] button.es-back.busy,
body[data-embed-sheet] button.schedule-change.busy { color: transparent; }

/* The spam check's panel for the header's Back form, drawn between the
   header and the scroll region (layout.html): inset as they are. */
body[data-embed-sheet] embed-sheet > checkout-admission {
  border-bottom: 1px solid var(--sheet-line);
  padding: .6rem 1.15rem;
}

/* margin-left:auto so it sits right on a step with neither a Back nor a
   title (the order page draws its own heading). 36px, over the 24px target
   floor (WCAG 2.2, 2.5.8), and exactly the header's inner height, which is
   what puts it the same distance from the top edge as from the right. */
body[data-embed-sheet] .es-close {
  align-items: center;
  border-radius: 999px;
  color: var(--custom-step-header-color, var(--sheet-muted));
  display: flex;
  flex: none;
  height: 2.25rem;
  justify-content: center;
  margin-left: auto;
  text-decoration: none;
  width: 2.25rem;
}

body[data-embed-sheet] .es-close:hover { background: color-mix(in srgb, var(--custom-step-header-color, var(--sheet-fg)) 8%, transparent); color: var(--custom-step-header-color, var(--sheet-fg)); }

/* Drawn INSIDE the control, in the control's own colour: an indicator that
   contrasts by construction against whatever it sits on, and one that a
   years-old `:focus { outline: none }` on the page cannot reach — this is
   our own document, but the rule is the same one #743's button follows and
   it costs nothing to keep them alike. */
body[data-embed-sheet] .es-close:focus-visible { outline: 2px solid currentColor; outline-offset: -3px; }

/* The order review (embed_review.html), the modal's first screen once the
   box out on the organiser's page has done the asking. A receipt: each line
   is a name on the left and a price on the right, the total under a rule,
   and the button. Sheet-only vocabulary, so it lives here rather than in
   either bundle's own components. */
/* ADD-ON ROWS ARE ALWAYS VISIBLE ON THE REVIEW, and the rule they undo is
   the reason they have to be. On the event page an add-on hides until its
   ticket's stepper goes above zero (`.js [data-addon-for]`, event_addons.css)
   — addon-rows.js reveals it, keyed to that input. The review has no
   steppers for tickets: the choosing happened out in the box, and the rows
   it offers were filtered to the chosen types SERVER-side. So the script has
   no input to watch, would never reveal anything, and the heading sat over
   an empty space. Shown from the start here, which is also what the rows
   mean here. */
body[data-embed-sheet] .js [data-addon-for],
body[data-embed-sheet] li[data-addon-for] { display: list-item; }

body[data-embed-sheet] .review-lines { margin: 0 0 1rem; }

/* The same dress as the summary the next steps draw (order-rail,
   checkout_cols.css): recap rows at the supporting size, and the total the
   one loud line, its label on the amount's baseline. Two steps of one
   purchase drawing the order two ways read as two different orders. */
body[data-embed-sheet] .review-line {
  border-bottom: 1px solid var(--sheet-line);
  display: flex;
  font-size: var(--bj-support, .875rem);
  gap: 1rem;
  justify-content: space-between;
  padding: .5rem 0;
}

body[data-embed-sheet] .review-line dt { min-width: 0; }
body[data-embed-sheet] .review-line dd { flex: none; font-variant-numeric: tabular-nums; margin: 0; }
body[data-embed-sheet] .review-qty { color: var(--sheet-muted); }

body[data-embed-sheet] .review-total {
  align-items: baseline;
  display: flex;
  gap: 1rem;
  justify-content: space-between;
  margin: 0 0 1.25rem;
}

body[data-embed-sheet] .review-total > span { font-weight: 600; }
body[data-embed-sheet] .review-total strong { font-size: var(--bj-display, 1.35rem); font-variant-numeric: tabular-nums; }

/* The bar under the total keeps its own gap above it (sheet_bar.css), and
   the two gaps stacked were a band of nothing between the total and the
   button it is about. */
body[data-embed-sheet] .review-total:has(+ checkout-bar) { margin-bottom: 0; }

/* The purchase form's own footer bleeds to the edges of whatever holds it,
   and what holds it here is <sheet-scroll> rather than the event page's
   card — so the bleed is retuned to this padding instead of that one. The
   bottom is squared off: on the page the footer is the last thing before the
   card ends, and in the sheet there is a real footer under it. */
body[data-embed-sheet] .pro-form-footer {
  margin: 1rem -1.15rem 0;
  padding: 1rem 1.15rem;
}

/* THE ONE SCROLLING REGION. Sized by its content (flex 0 1 auto: it never
   grows past what it holds, and shrinks to scroll when the sheet runs out of
   room), which is what lets the sheet be as tall as its step and no taller.

   --bj-pad is the journey's own "how far to the card's edge", which the
   page's full-bleed pieces (the checkout's pinned foot, the stub band)
   measure their bleed with. Inside the sheet the edge is THIS padding, so
   saying so once here is what makes every one of them meet the sheet's
   edges instead of overshooting them.

   scroll-padding keeps a field tabbed to clear of the pinned foot at the
   bottom of this region (WCAG 2.2, 2.4.11): the browser scrolls it into the
   visible part rather than behind the button. */
sheet-scroll {
  --bj-pad: 1.15rem;
  /* Measured by the wide-sheet rules below: how much room the STEP has, which
     is what decides one column or two, in a frame or out of one. */
  container: sheet / inline-size;
  display: block;
  flex: 0 1 auto;
  min-height: 0;
  /* The sheet is over somebody else's page: a wheel that reaches the end of
     this list must not start scrolling their article underneath. */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 1.15rem;
  scroll-padding-block: 1rem 8rem;
}

/* THE CHECKOUT'S PINNED FOOT, AS THE SHEET'S. On a page it bleeds to the
   card and pins to the window (checkout_bar.css); in here the "window" is
   <sheet-scroll>, and the page's arithmetic left a band of nothing under the
   button and pinned the bar a padding above the region's real bottom, so the
   form scrolled through a gap below it and a heading could be caught half
   under its top edge (Paul, 2026-09-24). In the sheet it sits flush on the
   region's bottom edge, the same height stuck or not, with the same padding
   under the button as over the count: the bleed (bottom and margin, from
   checkout_bar.css, now measured in the sheet's own --bj-pad) already puts
   its edge on the region's edge, so the extra padding the page adds to cover
   a window's bottom margin is a blank band here, and goes. The bar's whole
   dress in the sheet, and its place at the foot of every step, are
   sheet_bar.css's. */

/* THE WIDE SHEET (Paul, 2026-09-24: "the widget should follow suit"). Where
   the window has room the sheet is as wide as the event page's own card
   (--tito-sheet-wide; the loader sizes the frame to the same number), and
   each step lays itself out the way it does on the full page:

   - A step with two columns there has them here. The details and payment
     steps put their order beside the form once their CARD is 900px wide
     (checkout_cols.css), and that card is <main>; on a page <main> is the
     body's child and the container, and in here it is the scroll region's,
     so it has to be named the container again. The first step's own form
     already is one (event_columns.css).
   - A step with one column keeps the journey's 640px measure, centred, as
     it is on the full page — the order review, the confirmation — rather
     than a receipt stretched to a thousand pixels.

   Keyed on the room the step has (the `sheet` container), not on the
   window, so the same rules are right in the frame and opened straight.

   <main> is a container on EVERY step in here, not only the ones with a
   rail, and that is load-bearing (Fable review). checkout_cols.css asks an
   UNNAMED container query, which answers from the nearest container of any
   kind. With <main> a container only when it had a rail, a rail-less step
   (a free order's details) found sheet-scroll instead, 1000px wide, and
   split into two columns with nothing to put in the second. Now it always
   finds <main>, which on a rail-less step is held to 640px below and so
   never reaches the 900px the second column needs. */
body[data-embed-sheet] sheet-scroll > main { container-type: inline-size; }

@container sheet (min-width: 700px) {
  /* main named as well as *: the rule at the top of this file that takes
     the journey's card off <main> is more specific than a bare *. */
  body[data-embed-sheet] sheet-scroll > *,
  body[data-embed-sheet] sheet-scroll > main { margin-inline: auto; max-width: 640px; }
  body[data-embed-sheet] sheet-scroll > :is(main:has(order-rail), form:has(.purchase-cols[data-priced]), form:has(review-extras)) { max-width: none; }
}

/* The order review's two columns (embed_review.html): the extras on the
   left, the order and its total on the right — the event page's own shape,
   and its numbers (a 22rem rail at a 1.75rem gap, event_columns.css), so the
   summary is the same width on the step before and the step after. Placed by
   grid areas rather than moved in the markup, so what is read and tabbed
   through is in the same order at every width. One column, the three parts
   are plain blocks in that order. */
review-cols, review-extras, review-foot { display: block; }

@container sheet (min-width: 900px) {
  review-cols:has(> review-extras) {
    align-items: start;
    column-gap: 1.75rem;
    display: grid;
    grid-template-areas: "extras lines" "extras foot";
    grid-template-columns: minmax(0, 1fr) 22rem;
    grid-template-rows: auto 1fr;
  }
  review-cols:has(> review-extras) > .review-lines { grid-area: lines; }
  /* min-width:0 or one long product name pushes the rail off the sheet. */
  review-cols > review-extras { grid-area: extras; min-width: 0; }
  review-cols:has(> review-extras) > review-foot { grid-area: foot; }
  /* The heading opens the column rather than floating a heading's margin
     below the top of the order beside it. */
  review-extras > .review-more:first-child { margin-top: 0; }
}

/* In two columns the order and its button are one pinned group
   (checkout_cols.css), held to the window's height less its stand-off and
   the room the button hangs in under it. In
   here the "window" is the scroll region, shorter than the frame by the
   sheet's own header and footer — so it is held to THAT, which
   embed-sheet.js measures onto --sheet-scroll-h, framed. Opened straight, or
   without the script, it is the room the window leaves the region (the
   sheet's pinned top and edge, less about 8rem of chrome): a fixed number,
   where one measured off a content-sized sheet shrank on every report
   (Fable review). At worst a long order scrolls a little early. The
   foot's padding rule above is the one-column dress, and a rail has none.
   Keyed on the step having a rail, for the reason <main> is always a
   container above: a rail-less step is one column whatever the sheet's
   width, and keeps its one-column foot. */
@container sheet (min-width: 900px) {
  body[data-embed-sheet] main:has(order-rail) checkout-side { max-block-size: calc(var(--sheet-scroll-h, 100dvh - var(--sheet-top) - var(--sheet-edge) - 8rem) - 2.5rem - var(--checkout-bar-room, 0px)); top: 1.25rem; }
}

body[data-embed-sheet] .es-foot {
  align-items: center;
  border-top: 1px solid var(--sheet-line);
  display: flex;
  flex: none;
  font-size: .8rem;
  gap: .75rem;
  justify-content: space-between;
  /* A line height of its own, in rem: the two bundles' body leading differ,
     and the foot came out 42.6px on the review and 43.2px on the details
     step, which put the action bar above it at two heights (sheet_bar.css).
     The lifted ticket box's foot states the same number (public/embed.css). */
  line-height: 1.25rem;
  /* The foot of the SHEET, not of the step: where the sheet is taller than
     the step (a phone, where the sheet is the screen), the spare room goes
     above this line rather than under it, so the card still ends at its
     footer. Sized to its content, the sheet has no spare room and this does
     nothing. */
  margin-top: auto;
  padding: .7rem 1.15rem;
}

body[data-embed-sheet] .es-out { color: var(--sheet-muted); text-decoration-color: var(--sheet-line); }
body[data-embed-sheet] .es-secured { align-items: center; color: var(--sheet-muted); display: flex; gap: .3rem; }
body[data-embed-sheet] .es-secured a { color: inherit; text-decoration: none; }
body[data-embed-sheet] .es-secured svg { flex: none; }

/* INSIDE THE WIDGET'S FRAME THE SHEET FILLS IT, and the two rules below are
   why this is not the phone rule underneath.

   Since the lift, the loader sizes our iframe to the sheet exactly — so in
   here the viewport IS the sheet, 460px wide, which is under the phone
   breakpoint. Left to the media query the sheet would go full-screen and
   square off its corners on a 27-inch monitor, and the frame around it would
   still be a rounded 460px box. The outer screen is cross-origin and cannot
   be read from in here at all, so what the sheet keys off is the one thing
   it does know: that it is framed (embed-sheet.js sets the attribute). The
   loader owns the size and the corners, because the loader can see the
   screen; the sheet just fills what it is given.

   The inner scrim goes with it: there is nothing behind the sheet in here
   any more, and the dimming the visitor sees is the loader's, over the
   organiser's own page. */
/* Framed, the sheet starts at the frame's top and is as tall as its content
   but never shorter than the frame (the loader sizes the frame to the height
   embed-sheet.js reports, so the two meet), and never taller: past that, the
   scroll region scrolls. On a phone the sheet is the screen and the step is
   usually shorter than it; the spare room then sits between the step and the
   footer (.es-foot), so the card still ends at its footer. */
:root[data-framed] embed-sheet {
  height: auto;
  max-height: 100%;
  min-height: 100%;
  top: 0;
  width: 100%;
}
:root[data-framed] embed-scrim { display: none; }

/* On a phone the sheet is the screen: no corners to round, no margin to
   centre in, and every pixel of width spent on the form. Standalone only —
   framed, the loader has already made the frame the screen and the rules
   above have made the sheet fill it. */
@media (max-width: 520px) {
  :root:not([data-framed]) embed-sheet { border-radius: 0; max-height: 100%; min-height: 100%; top: 0; width: 100%; }
  :root:not([data-framed]) embed-scrim { display: none; }
}

/* And where the window has room, the wide sheet: the loader's SHEET_WIDE at
   its SHEET_WIDE_AT, for the sheet opened straight. Framed, the loader has
   already sized the frame and the sheet fills it. The literal is the token
   above, because a custom property cannot appear in a media condition. */
@media (min-width: 1100px) {
  :root:not([data-framed]) embed-sheet { width: min(var(--tito-sheet-wide), 100% - 3rem); }
}

/* THE STEPS SLIDE (Paul, 2026-09-26, "Slide in view", chosen in the motion
   lab). Moving from one step to the next is a real navigation inside the
   frame, a new document each time, so the motion is a cross-document view
   transition: the browser holds a picture of the step being left and
   animates it into the step arriving. The header, the action bar (or, in two
   columns, the order and its button) and the footer are named, so each is
   its own layer and holds still; only the step's body, the scroll region,
   moves. Forward it goes out to the left and the next comes in from the
   right; Back is the mirror. Which way is decided by the ARRIVING document,
   whose head sets data-dir="back" on pagereveal (layout.html,
   embed-sheet-head, which says how it knows).

   The slide is clipped to the body, so a step never draws over the header
   or the footer on its way across.

   THE OPT-IN IS NOT IN THIS FILE, and must never be. This file is in the
   public and event bundles, which every buyer page loads, and the
   view-transition at-rule cannot be scoped by a selector: in
   here it turned on a cross-fade for every same-origin navigation on every
   buyer page, the Door included, and a page mid-transition swallows the
   pointer (the Door's payment test timed out on it). So the opt-in is in the
   sheet's own head (layout.html, embed-sheet-head), inside the same media
   query, and only the sheet's documents carry it. Without it nothing below
   ever runs: the names and the animations only act during a transition.

   ALL OF IT IS INSIDE THE MEDIA QUERY, names included. With reduced motion
   asked for, or a browser without cross-document view transitions (Firefox
   today), there is no transition at all and the next step is simply there,
   which is how the sheet always behaved.

   Two things this does not cover. The box on the organiser's page to the
   first step of the sheet is the loader swapping one frame for another
   (embed.js), not a navigation, so no view transition can span it. And a
   step whose document named two elements the same (two action bars showing
   at once) has its transition skipped by the browser: the step changes
   instantly, as it would with reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  body[data-embed-sheet] .es-head { view-transition-name: sheet-head; }
  body[data-embed-sheet] .es-foot { view-transition-name: sheet-foot; }
  body[data-embed-sheet] sheet-scroll { view-transition-name: sheet-body; }
  body[data-embed-sheet] sheet-scroll checkout-bar { view-transition-name: sheet-bar; }

  /* checkout-side is display: contents in one column, where it has no box
     to name; in two it is the pinned order and its button. */
  @container sheet (min-width: 900px) {
    body[data-embed-sheet] main:has(order-rail) checkout-side { view-transition-name: sheet-side; }
  }

  ::view-transition-group(*) {
    animation-duration: 250ms;
    animation-timing-function: var(--sheet-slide-ease);
  }
  ::view-transition-image-pair(sheet-body) { overflow: clip; }
  ::view-transition-old(sheet-body) { animation: 250ms var(--sheet-slide-ease) both sheet-out-left; }
  ::view-transition-new(sheet-body) { animation: 250ms var(--sheet-slide-ease) both sheet-in-right; }
  :root[data-dir="back"]::view-transition-old(sheet-body) { animation-name: sheet-out-right; }
  :root[data-dir="back"]::view-transition-new(sheet-body) { animation-name: sheet-in-left; }
}

:root { --sheet-slide-ease: cubic-bezier(.32, .72, 0, 1); }

@keyframes sheet-out-left { to { transform: translateX(-100%); } }
@keyframes sheet-in-right { from { transform: translateX(100%); } }
@keyframes sheet-out-right { to { transform: translateX(100%); } }
@keyframes sheet-in-left { from { transform: translateX(-100%); } }

/* shared/sheet_bar.css */
/* ---- The widget's action bar ----
   ONE PLACE FOR THE BUTTON THAT MOVES THE BUYER ON, on every step of the
   widget (Paul, 2026-09-25: "shouldn't the widget on mobile consistently place
   the continue button in that fixed area at the bottom so that it doesn't jump
   its position from screen to screen?"). It is the details step's own pinned
   foot (<checkout-bar>, public/checkout_bar.css), made the sheet's: the order
   review, the options, the add-on assignment, the free claim and the payment
   step wrap their one primary action in the same element, and the lifted
   ticket box dresses its Get tickets from the same numbers (public/embed.css).

   Two properties, and both have to hold for the button not to move:

   - THE SAME BAR. Rule, fill, padding and the button's own metrics are
     stated here once, for every document the sheet is drawn in. The review
     is on the EVENT bundle, which never loads checkout_bar.css, and its
     button is the event page's .checkout-button, a line-height and a border
     away from the journey's; left to each bundle the button came out 45.2px
     tall on one step and 45.7px on the next.
   - THE SAME PLACE. Pinned at the foot of the scroll region, directly on the
     sheet's footer, whether the step is taller than the screen or not. Sticky
     alone only pins a bar the content has pushed off the bottom; on a phone,
     where the sheet is the screen, a short step left its button partway down
     with a blank band under it, and the next, longer step had it at the
     bottom. So the step FILLS the region: every element between the region
     and the bar is a column that grows, and a spacer in front of the bar
     takes whatever room is left over. Where the sheet is sized to its step
     (a desktop, where the loader sizes the frame to what the step reports)
     there is no room left over and nothing moves.

   The fill is CSS, so it is part of the zero-JS baseline like the pin. The
   one script that has to know about it is the one that measures the step:
   a step that fills its frame measures as the frame and could never report
   its way back down (embed-sheet.js, natural). It switches the fill off for
   the length of one synchronous read with data-sheet-measuring on the root,
   which is why every rule that stretches anything is keyed on its absence.

   Two columns (a sheet at least 900px wide, the details and payment steps'
   order rail, the review's extras beside its total) are a different layout:
   the button stands under the order in the right-hand column, as it does on
   the full page (checkout_cols.css), and none of this applies there. */
:root {
  --sheet-bar-lead: 1.25rem;
  --sheet-bar-pad: .9rem;
}

/* The bar: a rule across the sheet, the card's own fill behind it so rows
   scroll out of sight under it, and the same padding above and below what it
   holds. It bleeds to the sheet's edges through the scroll region's padding
   (--bj-pad, embed_sheet.css), and sticks flush on the region's bottom edge
   for the same reason: sticky holds the MARGIN box (checkout_bar.css has the
   working-out). */
body[data-embed-sheet] sheet-scroll checkout-bar {
  align-items: center;
  background: var(--sheet-card);
  border-top: 1px solid var(--sheet-line);
  bottom: calc(var(--bj-pad) * -1);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin: var(--sheet-bar-lead) calc(var(--bj-pad) * -1) calc(var(--bj-pad) * -1);
  padding: var(--sheet-bar-pad) var(--bj-pad);
  position: sticky;
  z-index: 3;
}

/* The button, the same box in both bundles: the journey's primary dress on
   one side (checkout_bar.css) and the event page's .checkout-button on the
   other agree on the type and the padding and disagree on the line-height and
   the border. Stated here, they agree on all four. Colour stays each
   bundle's: both are the account's own button colour.
   A copy of checkout_bar.css's dress, and the lifted box's Get tickets is a
   third (public/embed.css): TestTheCheckoutBarsButtonIsDressedLikeEveryOther
   Primary holds all three to the journey's primary, and
   TestTheWidgetsActionBarIsOneBar holds the leading.
   The checkout button ([data-checkout-button]) takes the same four at one
   class of weight, in the second rule, so the organizer's rule naming it wins
   (PRO-5274); every other button in the bar keeps the first. */
body[data-embed-sheet] sheet-scroll checkout-bar button[type=submit]:not(:where([data-checkout-button])),
:where(body[data-embed-sheet] sheet-scroll checkout-bar) [data-checkout-button] {
  border: 1px solid transparent;
  font-size: .95rem;
  inline-size: 100%;
  line-height: 1.4;
  margin: 0;
  padding: .7rem 1.2rem;
}

/* The fill, in one column. The region (embed_sheet.css) grows to the sheet;
   the step's outermost element is at least as tall as the region; each
   element on the way down to the bar is a column that grows; and the bar is
   last, after a spacer that takes the room left over. The spacer is nothing
   tall when there is no room to spare and the bar keeps its own gap above
   it, so a long step ends exactly as it did.

   The sheet and the region are outside the query because a container query
   cannot style the container. The sheet takes a HEIGHT where it already
   fills something (the frame; the screen, opened straight on a phone),
   because a percentage can only resolve against a height that is definite,
   and min/max alone are not. Where the sheet is sized to its content (opened
   straight on a desktop) neither rule has room to use. */
:root[data-framed]:not([data-sheet-measuring]) embed-sheet:has(checkout-bar) { height: 100%; }
@media (max-width: 520px) {
  :root:not([data-framed]):not([data-sheet-measuring]) embed-sheet:has(checkout-bar) { height: 100%; }
}
:root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll:has(checkout-bar) { flex: 1 1 auto; }

@container sheet (max-width: 899.98px) {
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll > :has(checkout-bar) {
    min-block-size: 100%;
  }
  /* checkout-side is `display: contents` in one column (checkout_cols.css):
     it has no box to be a column, and its bar is already checkout-cols'. A
     hidden element stays hidden: `display` here would outrank [hidden]. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) {
    display: flex;
    flex: 1 0 auto;
    flex-direction: column;
  }
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(> checkout-bar)::after {
    content: "";
    flex: 1 0 0;
    order: 98;
  }
  /* Last, after the spacer, whatever order its container gave it (the
     details step's checkout_cols.css puts it third). */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll checkout-bar { order: 99; }

  /* THE COST OF THE COLUMNS, PAID BACK (Fable review). A block's vertical
     margins collapse into the larger of the two; a column's children keep
     both, so every gap in a column that used to be a block widened by the
     smaller margin: 16px over the review's add-ons, 28px down the add-on
     assignment, 12px over the email check. Each rule below drops the margin
     the collapse used to swallow, so the step is laid out exactly as it is
     with the fill off. TestWidgetActionBarHoldsStillUI compares the two
     layouts, element by element, on the steps the test shop reaches.

     What comes before a form that opens with a heading or a fieldset: that
     first thing brings its own, larger, top margin. Only such a form, and
     not any form (Fable review): a form that opens with a button brings no
     margin, and dropping what is over it leaves the button flush under it.

     The thing before the form is NAMED, not "anything": a :has() looking at
     a next sibling on a subject that could be any element makes Chromium
     restyle the whole document on every DOM change in it, and WebKit do the
     same for every `hidden` written anywhere. On VAP's details step that
     was most of what a keystroke in a country search cost
     (TestNoHasRuleRestylesTheWholePage). */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > :is(p, h1, h2, h3, div, section, header, callout, ul, ol, dl, table, details, nav):has(+ form:not([hidden]) > :is(h2, fieldset, .bj-field):first-of-type) { margin-block-end: 0; }
  /* A line of help over rows or a callout: the rows' or the callout's gap
     wins. The eyebrow over the payment step's callout is the same pair. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > p:has(+ :is(.rows, callout)) { margin-block-end: 0; }
  /* The callout over the payment step's amount: the callout's gap wins. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > callout + p { margin-block-start: 0; }
  /* A page's heading and the line under it: the heading's gap wins. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > h1 + p { margin-block-start: 0; }
  /* The payment options and the bank transfer request (PRO-4999): a line
     over a section heading, where the heading's gap wins; a section heading
     and the line under it, where the heading's gap wins again; and two lines
     in a row, the same margin on both sides of the pair. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > p:has(+ h2) { margin-block-end: 0; }
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > h2 + p { margin-block-start: 0; }
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > p + p { margin-block-start: 0; }
  /* A section heading over its rows: the rows' gap wins. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll :has(checkout-bar):not(checkout-side, [hidden]) > h2:has(+ .rows) { margin-block-end: 0; }
  /* The review's order over its add-ons: the add-ons' heading's gap wins. */
  :root:not([data-sheet-measuring]) body[data-embed-sheet] sheet-scroll review-cols > .review-lines:has(+ review-extras) { margin-block-end: 0; }
}

/* Two columns: the bar is the button under the order in the right-hand
   column (checkout_cols.css for the details, options and payment steps, embed_sheet.css
   for the review), so it gives up the rule, the fill, the bleed and the pin. */
@container sheet (min-width: 900px) {
  body[data-embed-sheet] main:has(order-rail) checkout-bar,
  body[data-embed-sheet] review-cols:has(> review-extras) checkout-bar {
    background: none;
    border-top: 0;
    margin: 1rem 0 0;
    padding: 0;
    position: static;
  }
  /* The details, options and payment steps hang theirs under the summary
     (checkout_cols.css), where it can be hung; this only has to outrank the
     sheet's own pin above. */
  @supports (anchor-name: --a) {
    body[data-embed-sheet] main:has(order-rail) checkout-cols > checkout-bar { bottom: auto; position: absolute; }
    /* And pinned rather than hung once script has measured the summary
       (checkout_cols.css, PRO-5135): the widget is where it was seen. */
    body[data-embed-sheet] main:has(order-rail) checkout-cols[data-side-measured] > checkout-bar {
      margin-block-start: calc(var(--checkout-side-h) + 1rem);
      position: sticky;
    }
  }
}

/* The same in the sheet, where the bar's own rule is more specific
   (checkout_bar.css says why it must stay hidden). */
body[data-embed-sheet] sheet-scroll checkout-bar[hidden] { display: none; }

/* shared/sandbox_pill.css */
/* sandbox-pill — the sandbox, said inside a widget frame: the ticket box and
   every step of its sheet (layout.html's "sandbox-pill" has the why). A full
   page says it with the strip over the whole document (owner_strips.css);
   inside a frame that band never met the card's rounded corners cleanly and
   ran under the × (Paul, 2026-09-26). This is one small word leading the
   header row instead, opposite the close.

   It IS the admin's Sandbox badge (shared/badge.css, .badge-test): same size,
   weight, padding and radius, and the same amber pair, so "sandbox" looks the
   same on both sides of the product. Written out rather than read from
   --warn-weak and --warn-ink for the reason owner_strips.css gives: the sheet's
   first step is the EVENT bundle's document, which carries no shared tokens.

   OPAQUE, where the badge's fill is a 13% tint. A tint takes its colour from
   whatever is under it, and under this is the account's own card, which an
   organiser can paint anything; laid over white, the badge's own tint is
   #fcf5df, so that is the fill, fixed. The pairs measure 5.9:1 (light,
   --amber-800 on the tint) and 6.8:1 (dark, --amber-400 on #34291f, the
   strip's own fill), both AA for this 11.5px text on any card.

   light-dark() follows the frame's pinned scheme (the account's card, never
   the visitor's device: layout.html and event.html set it). */
.sandbox-pill {
  background: light-dark(#fcf5df, #34291f);
  border-radius: 999px;
  color: light-dark(#8a5200, #f2a33c);
  flex: none;
  font: 550 11.5px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  letter-spacing: normal;
  margin: 0;
  padding: .16rem .6rem;
  white-space: nowrap;
  width: fit-content;
}

/* The sentence every full page shows, for a screen reader only: the
   sr-only recipe, written here because the event bundle does not carry
   sr_only.css. */
.sandbox-pill-say,
[data-sandbox-outside] .sandbox-pill {
  clip: rect(0 0 0 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

/* shared/embed_leave.css */
/* The widget sheet's "leave without finishing?" (layout.html, embed-sheet-top;
   embed-sheet.js opens it). A small card in the middle of the sheet, over a
   dimmed step, with the question centred over its two answers.

   Shared, beside embed_sheet.css, for the same reason the sheet's chrome is:
   the sheet's first step is on the event bundle and every later one on the
   public bundle, and the question must look the same on both. So the colours
   are the sheet's own (--sheet-*, embed_sheet.css), and the button's fill is
   whichever of the two bundles' button colours this document has. */
body[data-embed-sheet] .es-leave {
  background: var(--sheet-card);
  /* The edge, and not only the shadow: --shadow-modal lives in colors.css,
     which the event bundle (the sheet's first step) does not carry, so there
     the shadow drops and a borderless card would sit flat on the dim (Fable
     review). A literal fallback is refused by TestElevationShadowsUseTokens. */
  border: 1px solid var(--sheet-line);
  /* The sheet's corners on screen are the loader's, not the theme's
     (embedjs/geometry.js: 16px, 12px on a phone), so the card inside it
     follows those. A theme's card radius of 2px here read as an alert. */
  border-radius: 14px;
  box-shadow: var(--shadow-modal);
  color: var(--sheet-fg);
  inline-size: min(20rem, calc(100% - 2rem));
  padding: 1.75rem 1.5rem 1.25rem;
  text-align: center;
}

body[data-embed-sheet] .es-leave::backdrop { background: rgb(0 0 0 / .4); }

/* The question is the whole of the card, so it reads as its heading: centred,
   a step up in size and weight (Vicky, 2026-09-29: "looks undesigned, e.g.
   left aligned text"). */
body[data-embed-sheet] .es-leave p {
  color: var(--sheet-heading);
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.4;
  margin: 0 0 1.5rem;
  text-wrap: balance;
}

/* Stacked, full width: "Back to my order" is the one filled button, and
   "Cancel my order" sits under it as quiet text, so the answer an accidental
   press wants is the one that looks like the answer. Each names which way
   it goes (Vicky, 2026-09-29: "Keep going" read as either). Stacked at every width, so a language's
   longer words never wrap one button and not the other. */
body[data-embed-sheet] .es-leave-acts {
  display: flex;
  flex-direction: column;
  gap: .25rem;
}

body[data-embed-sheet] .es-leave-acts button {
  background: transparent;
  /* The edge, and not only the shadow: --shadow-modal lives in colors.css,
     which the event bundle (the sheet's first step) does not carry, so there
     the shadow drops and a borderless card would sit flat on the dim (Fable
     review). A literal fallback is refused by TestElevationShadowsUseTokens. */
  border: 1px solid var(--sheet-line);
  border-radius: var(--button-radius, var(--custom-button-radius, 8px));
  color: var(--sheet-muted);
  cursor: pointer;
  font: inherit;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.4;
  margin: 0;
  min-block-size: 2.75rem;
  padding: .7rem 1rem;
}

body[data-embed-sheet] .es-leave-acts button:hover { color: var(--sheet-fg); }

body[data-embed-sheet] .es-leave-acts [data-embed-stay],
body[data-embed-sheet] .es-leave-acts [data-embed-stay]:hover {
  background: var(--accent, var(--custom-button-background-color, #30303a));
  color: var(--accent-fg, var(--custom-button-text-color, #fff));
}

body[data-embed-sheet] .es-leave-acts button:focus-visible {
  outline: 2px solid var(--sheet-fg);
  outline-offset: 2px;
}

/* In, not snapped on: the card rises a little and fades, the dim fades
   under it. Only on the way in; "Back to my order" puts it away at once. */
@media (prefers-reduced-motion: no-preference) {
  body[data-embed-sheet] .es-leave[open] { animation: es-leave-in 180ms cubic-bezier(.32, .72, 0, 1); }
  body[data-embed-sheet] .es-leave[open]::backdrop { animation: es-leave-dim 180ms ease-out; }
}

@keyframes es-leave-in { from { opacity: 0; transform: translateY(.5rem) scale(.97); } }
@keyframes es-leave-dim { from { opacity: 0; } }

/* shared/widget_header.css */
/* <widget-header>: what an organiser chose to show above a widget's ticket
   list (widget_header.go, Paul 2026-09-29): the widget's own banner, the
   event's name, and a meta block with the date and the venue.

   ONE FILE FOR BOTH DOCUMENTS the list is drawn in, which is why it lives in
   shared/ and rides embedSheetStyles on both bundles: the box on the
   organiser's page (embed.html, public bundle) and the sheet's first step
   (embed_sheet.html, event bundle). So it reads only the palette names both
   documents declare (docs/themes.md, "The palette and type are variables"),
   which is also what makes it follow the organiser's colours and faces.

   THE BANNER BLEEDS to the edges of whatever holds it, and all three holders
   pad by the same 1.15rem: the inline card (tito-embed), the lifted box's
   scrolling body, and <sheet-scroll>. Inline it is the first thing in the
   card, so it takes the card's top corners, a pixel in for the card's
   border. Lifted and in the sheet it sits under the step's own header and is
   square.

   NO LAYOUT SHIFT: the <img> carries the rendition's own width and height,
   so `height: auto` gives it its box before a byte arrives, and the loader
   sizes the frame on the organiser's page once. A pasted https URL (an API
   caller's, or an imported event banner) has no size we know, and takes its
   own shape when it arrives: guessing 3:1 cropped the ends off VAP's 4:1
   artwork on the canary, which is worse than one move on load. The max
   height is for a tall picture in a wide inline box, which would otherwise
   push the tickets out of view; cover crops it rather than squashing it. */
widget-header {
  display: block;
  margin: 0 0 1.25rem;
}

widget-header:has(> .wh-title, > widget-meta) {
  border-bottom: 1px solid var(--hairline, var(--divider-color, color-mix(in srgb, currentColor 15%, transparent)));
  padding-bottom: 1rem;
}

widget-header > .wh-banner {
  background: color-mix(in srgb, currentColor 6%, transparent);
  display: block;
  height: auto;
  margin: -1.15rem -1.15rem 1rem;
  max-height: 14rem;
  max-width: none;
  object-fit: cover;
  width: calc(100% + 2.3rem);
}

widget-header > .wh-banner:last-child { margin-bottom: 0; }

/* Inline, the card's own corners. Only the list shape: a button-shape widget
   shows its list lifted, under the header. */
tito-embed:not([data-shape="button"]) > .embed-body > widget-header > .wh-banner {
  border-radius: calc(var(--journey-radius, 12px) - 1px) calc(var(--journey-radius, 12px) - 1px) 0 0;
}

/* In a sandbox the pill leads the inline card (public/embed.css), so the
   banner starts under it rather than over it. */
:root:not([data-sandbox-outside]) tito-embed:not([data-shape="button"]):has(> embed-head > .sandbox-pill) > .embed-body > widget-header > .wh-banner {
  border-radius: 0;
  margin-top: 0;
}

/* The sheet holds a one-column step to 640px on a wide window; the header
   spans the step it heads. */
body[data-embed-sheet] sheet-scroll > widget-header { max-width: none; }

/* The heading: the organiser's heading face and colour, at a size between
   the step's own title and the event page's. Every property named, because
   both bundles have opinions about an h2 (journey_headings.css,
   journey_classic.css) and this file trails both. */
widget-header > h2.wh-title {
  border: 0;
  color: var(--custom-heading-color, inherit);
  font-family: var(--theme-font-heading, inherit);
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: normal;
  line-height: 1.25;
  margin: 0 0 .6rem;
  overflow-wrap: anywhere;
  padding: 0;
}

widget-header > h2.wh-title:last-child { margin-bottom: 0; }

widget-header > h2.wh-title::after,
widget-header > h2.wh-title::before { content: none; }

/* Date and venue: small, muted, each line led by its icon, and wrapping
   under its own words rather than under the icon on a phone. */
widget-meta {
  color: var(--custom-text-color-muted, inherit);
  display: grid;
  font-size: .9rem;
  gap: .4rem;
  line-height: 1.4;
}

widget-meta > p {
  align-items: flex-start;
  display: flex;
  gap: .5rem;
  margin: 0;
  min-width: 0;
}

widget-meta > p > :is(svg, .pin) {
  display: block;
  flex: none;
  height: 1em;
  margin-top: .2em;
  width: 1em;
}

widget-meta .pin > svg { display: block; height: 100%; width: 100%; }

widget-meta > p > span {
  display: grid;
  gap: .1rem;
  min-width: 0;
  overflow-wrap: anywhere;
}

widget-meta .wh-venue { color: var(--custom-text-color, inherit); }

widget-meta a.wh-address {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: .15em;
}

widget-meta a.wh-address:hover { color: var(--custom-text-color, inherit); }

widget-meta .wh-address .ext {
  display: inline-block;
  height: .8em;
  margin-left: .25em;
  vertical-align: -.05em;
  width: .8em;
}

widget-meta .wh-address .ext > svg { display: block; height: 100%; width: 100%; }

/* shared/touch.css */
/* touch.css — the two rules that stop a phone zooming while somebody is
   trying to use the page. Both are FLOORS over whatever a component asked
   for, which is why this file is last in every bundle rather than sitting
   with the rest of shared/.

   ---- Double tap ----
   A phone browser holds a tap for a moment in case a second one follows,
   and reads the pair as zoom-in. On a control that is genuinely tapped
   twice in a row — a quantity stepper, a pager, a tab — that lands as the
   page jumping under your thumb rather than as a zoom anybody asked for.
   touch-action: manipulation drops the double-tap gesture and keeps
   panning and PINCH zoom, which is the one WCAG 2.2 AA cares about
   (1.4.4 Resize text): this is not user-scalable=no wearing a hat.

   It is scoped to the things a person taps, not to <body>, because
   double-tapping the words of a page to zoom in on them is a real way to
   read a page on a phone and a control is not words. shared/qty.css and
   styles/event/event_qty.css got here first for the +/- steppers; their
   rules now say the same thing this one does.

   type=range is left out on purpose — the slider's own drag is a touch
   gesture, and the value it wants for touch-action is the UA's, not ours.

   ---- Focus ----
   iOS Safari zooms the page in whenever it focuses a field computing under
   16px, and does not zoom back out afterwards. Most fields in the product
   are under that line: the admin shell runs at 13.5px by design
   (styles/admin/app_shell.css), and the standalone event page's inputs take
   the browser's own ~13px default because that bundle has no form_fields.css.

   So: a 16px floor, on a coarse pointer only. A mouse never triggers the
   zoom, so nothing about the desktop design moves — and max() leaves a
   field that is already bigger exactly where it was.

   !important because a floor has to outrank every component that sizes its
   own field (the list bar's search, the export panel's filename box, the
   tag form), and those are all authored with more specificity than an
   element selector can carry. */

a, area, button, label, select, summary, textarea,
input:not([type="range"]), [role="button"], [role="tab"] {
  touch-action: manipulation;
}

@media (pointer: coarse) {
  input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"],
    [type="submit"], [type="button"], [type="reset"], [type="image"],
    [type="range"], [type="color"]),
  textarea, select { font-size: max(16px, 1em) !important; }
}

