/* 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. */
.caret { align-items: center; color: var(--text-faint); display: inline-flex; font-size: .8em; margin-left: .3rem; transition: transform .15s; }
.caret svg, .caret i { display: block; }
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; }

/* 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 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 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_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;
  /* The card's corner, capped at 8px — the same rule every other button on a
     buyer page follows (journey_shell.css). Fully round until 2026-08-21,
     which read as two pills beside a squared-off card once the default radius
     came down to 5. Capped rather than followed all the way up because a
     16px corner on a 32px button is a pill again. */
  border-radius: min(var(--custom-card-border-radius, 8px), 8px);
  cursor: pointer;
  display: inline-flex;
  font-size: 1.1rem;
  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;
}
.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; }
.schedule-group select { max-width: 100%; padding: .45rem; }
.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. */
.quote-instead {
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  display: block;
  font: inherit;
  font-size: .875rem;
  margin: .9rem auto 0;
  opacity: .8;
  padding: .25rem .5rem;
  text-decoration: 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_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; }
.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; }
/* 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; }

/* 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 (the promo note's) rides the rule and the badge
   while the WORDS go to full text colour — red text over an account-chosen
   background can't be held to AA, a red edge plus role="alert" can. */
.recommend-hint.bad {
  border-left-color: #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. */
.recommend-hint.bad::before {
  background: #c22f3d;
  border-radius: 50%;
  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;
}
.promo-row input[aria-invalid=true] { border-color: #c22f3d; }
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; }
.promo-note { font-size: .85rem; margin: .1rem 0 .4rem; }
.promo-note.bad { color: #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 two status colours are literal rather than tokens because this page has
   no status palette: it is the organiser's own --custom-* colours end to end,
   and .promo-note.bad already sets its red the same way. Both clear WCAG AA
   on the light card (5.9:1 amber, 6.0:1 chip ink on chip fill). */
.tier-nudge {
  color: #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 literal
   colour, same contrast (5.9:1 on the light card) — this page has no status
   palette, it is the organiser's own --custom-* colours end to end. */
.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: #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_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-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; }
.step-heading {
  color: var(--step-heading-ink);
  font-size: 1.2rem;
  font-weight: 600;
  margin: .1rem 0 .9rem;
}
.step-heading:focus { outline: none; }
.step-back {
  background: none;
  border: 0;
  color: var(--step-link);
  cursor: pointer;
  font-size: .9rem;
  font-weight: 600;
  margin: 0 0 .9rem;
  padding: .1rem 0;
}
.step-back:hover { text-decoration: underline; }
.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; }
.step-heading,.step-back { transition: opacity .32s ease,transform .32s ease; }
.step-panel.leaving { pointer-events: none; }
.step-panel.leaving .step-heading,.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, the street address and the directions links. <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: apple-map.js removing its own mount 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; }
/* The Apple provider's mount. It starts DISPLAY:NONE and apple-map.js reveals
   it in the one instant before MapKit measures it — because every other path
   through that module (no JS at all, a blocked library, a token Apple
   refuses, an address nothing can geocode) must leave a page identical to one
   with no map provider, and a sized-at-parse-time box would instead leave an
   empty rectangle above the address. Same proportions as the iframe once
   shown, so switching providers doesn't move the page. */
event-map [data-apple-map] {
  aspect-ratio: 3 / 2;
  display: none;
  width: 100%;
}
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-apple-map],
  event-map iframe { aspect-ratio: 5 / 2; }
}
venue-facts {
  display: block;
  padding: .75rem .9rem;
}
venue-facts p {
  color: var(--custom-text-color-muted);
  font-size: .9rem;
  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. */
.pro-meta venue-facts { padding: 0; }
.pro-meta venue-facts p { font-size: 1em; }
/* 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 { 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; }

/* ---- 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; }
.dir-menu > nav a { border-radius: .3rem; color: var(--custom-text-color); font-size: .85rem;
  padding: .4rem .55rem; text-decoration: none; white-space: nowrap; }
.dir-menu > nav a:hover { background: color-mix(in srgb, currentColor 8%, transparent); }
@media (prefers-reduced-motion: reduce) { .dir-menu .caret { transition: none; } }
/* 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_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;
    top: 1.25rem;
  }
  [data-priced] > .purchase-rail > .summary { padding: 1.25rem; }

  /* 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;
  }
  [data-priced] .purchase-rail .summary[data-empty] .promo-caret { visibility: hidden; }

  /* 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/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. */
.secret-preview,
.owner-strip,
.page-preview,
.sandbox-strip {
  /* Near-black, deliberately not a hue: most of these report something the
     reader has to weigh (paid tickets are being held back, this page is a
     draft, this shop is a copy that takes no real money), and a coloured band
     would either look like part of the design or like a warning about the
     wrong thing. */
  background: #1b1e27;
  color: #f8f9fb;
  font: 550 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  letter-spacing: .01em;
  margin: 0;
  padding: .65rem 1rem;
  text-align: center;
}

/* The secret strip takes the brand cobalt instead (Vicky, 2026-08-28): it is
   the one of the three that carries no problem at all — the event is secret
   because somebody chose that, and the strip exists so a page that loads for
   you is not read as a page that loads for everybody. Louder than the
   near-black on a white event page, and unmistakably ours.

   #1a45c4 is --cobalt-600 written out, for the reason the whole file exists:
   the event bundle carries no shared tokens, so var(--accent) would render as
   nothing here. Against #f8f9fb it measures 7.4:1, so it clears AA for this
   13px/550 text on any account palette, the same guarantee the pair above
   makes. If the brand blue ever moves, this line moves with it by hand. */
.secret-preview { background: #1a45c4; }

/* .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 strip's own ink rather than an accent: the pair above is
   fixed at 15.0:1 precisely so no palette can weaken it, and a blue link
   inside it would be the one thing on the strip an account'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: center;
}

/* 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 blue on a near-black bar — 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] {
  color: #f8f9fb;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.page-preview 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; }

/* 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 RECTANGLE 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 sized by its content re-centres on each one
   and hops up the screen under the buyer's pointer. What stops it:

     - a fixed WIDTH and HEIGHT, not max-height and not content-driven;
     - `inset: 0; margin: auto`, which centres a fixed-size fixed-position
       box in both axes and depends on nothing about what is inside it;
     - one scrolling region, <sheet-scroll>, between a header and a footer
       that are outside it.

   Change any of those three and the sheet starts moving again.

   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. */
body[data-embed-sheet] sheet-scroll > main > footer:empty { margin: 0; }

/* THE SHEET'S SIZE IS DECLARED ONCE, HERE, AND THE LOADER READS THE SAME
   NUMBERS. Since the lift, the organiser's page is dimmed by a scrim in
   THEIR document and our iframe is sized to the sheet and nothing more —
   which means embed.js has to know how big the sheet is, and two files now
   have an opinion about it. They agree because
   TestSheetSizeAgreesWithTheLoader parses both and compares; a number
   changed here alone fails that test rather than leaving a modal with a
   transparent margin around it. Change them together.
     --tito-sheet-w: 460px  --tito-sheet-h: 640px  --tito-sheet-full: 520px
   (full = at and below this viewport width the sheet IS the screen). */
:root {
  --tito-sheet-full: 520px;
  --tito-sheet-h: 640px;
  --tito-sheet-w: 460px;
}

/* 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));

  background: var(--sheet-card);
  border-radius: var(--sheet-radius);
  color: var(--sheet-fg);
  display: flex;
  flex-direction: column;
  /* Fixed, not max-content: see the note at the top of this file. */
  height: min(var(--tito-sheet-h), 100%);
  inset: 0;
  margin: auto;
  overflow: hidden;
  position: fixed;
  /* 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;
}

body[data-embed-sheet] .es-head {
  align-items: center;
  border-bottom: 1px solid var(--sheet-line);
  display: flex;
  flex: none;
  gap: .75rem;
  /* Back on the left where there is one, close on the right always — the
     event's name has gone from between them (layout.html says why). */
  justify-content: space-between;
  padding: .85rem 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; }

body[data-embed-sheet] .es-title {
  color: var(--sheet-heading);
  font-weight: 600;
  /* One line, ever: the header is chrome, and an event with a long name must
     not be allowed to push the tickets down the sheet. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

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

body[data-embed-sheet] .es-back:hover { color: var(--sheet-fg); }
body[data-embed-sheet] .es-back:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }

/* margin-left:auto rather than the header's justification: the close is the
   only thing in the header on most steps, and space-between with one child
   puts it at the START. Pushed from the element itself, it sits right
   whether or not a back link is beside it. */
body[data-embed-sheet] .es-close {
  align-items: center;
  border-radius: 999px;
  color: var(--sheet-muted);
  display: flex;
  flex: none;
  height: 32px;
  justify-content: center;
  margin-left: auto;
  text-decoration: none;
  width: 32px;
}

body[data-embed-sheet] .es-close:hover { background: color-mix(in srgb, var(--sheet-fg) 8%, transparent); 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; }

body[data-embed-sheet] .review-line {
  border-bottom: 1px solid var(--sheet-line);
  display: flex;
  gap: 1rem;
  justify-content: space-between;
  padding: .7rem 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 {
  display: flex;
  gap: 1rem;
  justify-content: space-between;
  margin: 0 0 1.25rem;
}

body[data-embed-sheet] .review-total strong { font-size: 1.15rem; }

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

sheet-scroll {
  display: block;
  flex: 1;
  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;
}

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;
  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. */
:root[data-framed] embed-sheet { height: 100%; 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; height: 100%; width: 100%; }
  :root:not([data-framed]) embed-scrim { display: none; }
}

