/* ============================================================================
   SMARTBOOKING — ORGANISATION ADMIN PORTAL
   ----------------------------------------------------------------------------
   Loaded *after* site.css by the two application layouts — _PortalLayout for the
   signed-in portal and _OnboardingLayout for registration and sign-in. Everything
   here is built on the tokens site.css already declares — no new colour, no new
   spacing scale, no new radius. If a value below is not a var(), that is a bug.

   WHY A SECOND FILE RATHER THAN A SECTION 21 IN site.css
   The landing page is production code and its stylesheet is frozen. A separate
   file means site.css is byte-identical, so the public page cannot be affected by
   anything in here — the strongest possible guarantee, and cheaper than reviewing
   a 130 KB diff every time the portal gains a component.

   The portal reuses site.css sections 1-19 in full: .btn, .card, .badge, .field,
   .input, .table, .stat, .empty, .alert. This file adds only what a left-rail
   shell needs and site.css has no vocabulary for.
   ============================================================================ */

/* ---------------------------------------------------------------- shell ---- */

.pt-shell {
  display: grid;
  grid-template-columns: 264px 1fr;
  min-height: 100vh;
  background: var(--surface-1);
}

.pt-main {
  display: flex;
  flex-direction: column;
  min-width: 0;   /* lets wide tables scroll inside their own box instead of
                     stretching the grid column and pushing the page sideways */
}

.pt-content {
  padding: var(--s-7) var(--s-7) var(--s-10);
  max-width: 1280px;
  width: 100%;
}

/* -------------------------------------------------------------- sidebar ---- */

.pt-side {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--s-5) var(--s-4);
  background: var(--surface-0);
  border-right: 1px solid var(--border);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.pt-side-brand {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  text-decoration: none;
  color: inherit;
}

.pt-side-brand .brand-mark { flex: 0 0 auto; }

.pt-side-org {
  font-size: 0.78rem;
  color: var(--ink-500);
  line-height: 1.3;
}

.pt-nav { display: flex; flex-direction: column; gap: var(--s-1); }

.pt-nav-group {
  padding: var(--s-4) var(--s-3) var(--s-1);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-400);
}

.pt-nav-link {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-md);
  color: var(--ink-700);
  text-decoration: none;
  font-weight: 500;
  line-height: 1.2;
  /* 44px minimum target, same rule the landing page follows */
  min-height: 44px;
}

.pt-nav-link:hover { background: var(--surface-2); color: var(--ink-900); }

.pt-nav-link .icon { flex: 0 0 auto; width: 20px; height: 20px; color: var(--ink-500); }

.pt-nav-link[aria-current="page"] {
  background: var(--brand-050);
  color: var(--brand-700);
  font-weight: 600;
}

.pt-nav-link[aria-current="page"] .icon { color: var(--brand-600); }

.pt-nav-sub { display: flex; flex-direction: column; gap: var(--s-1); padding-left: var(--s-8); }

.pt-nav-sub .pt-nav-link { min-height: 38px; padding-block: var(--s-2); font-size: 0.9rem; }

/* A placeholder is a roadmap item, not a broken link. It is visibly inert and says
   so in words — a disabled-looking thing that still navigates reads as a bug, and a
   link that 404s reads as a worse one. */
.pt-nav-link.is-soon {
  color: var(--ink-400);
  cursor: default;
  pointer-events: none;
}

.pt-nav-link.is-soon .icon { color: var(--ink-300); }

.pt-soon {
  margin-left: auto;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-400);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-full);
  padding: 2px var(--s-2);
}

/* --------------------------------------------------------------- topbar ---- */

.pt-top {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-7);
  background: var(--surface-0);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 20;
}

.pt-top-title { font-size: 1.05rem; font-weight: 650; margin: 0; }
.pt-top-actions { margin-left: auto; display: flex; align-items: center; gap: var(--s-3); }

/* ── THE BAR IS ONE ROW, AT EVERY DESKTOP WIDTH ──────────────────────────────
   .pt-top-account had no layout rule of its own, so it was a block box holding
   an inline-flex button, an inline-flex link and a <form>. The form is block,
   so "Sign out" sat on a SECOND LINE and the bar stood 109px tall instead of
   89px. That was true before "Switch role" existed; a fourth child just puts
   another control beside the fault.

   WHAT YIELDS, AND IN WHAT ORDER. The actions are the controls; the title is
   repeated by the page's own heading a few pixels below it. So the title
   shrinks first (flex-shrink: 100 against everything else's 1) and truncates,
   then the name and organisation truncate, and the buttons never move. Without
   min-width: 0 on the row and on each of those two, a flex item refuses to
   shrink below its content and the overflow lands on the page instead.

   Measured at 961px — the narrowest width that still has a sidebar rather than
   a drawer, so the bar has only 697px to work in. Four actions, a long page
   title, a 37-character name and a 62-character organisation: one 89px row and
   no horizontal scroll. */
.pt-top-actions { min-width: 0; }

.pt-top-account {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: 0 0 auto;
}

.pt-top-account form { margin: 0; }

.pt-top-title {
  min-width: 0;
  flex-shrink: 100;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pt-user { display: flex; align-items: center; gap: var(--s-3); min-width: 0; }

/* The <br/> between the name and the organisation still breaks; nowrap only
   stops each of those two wrapping onto a third and fourth line. */
.pt-user > span {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.pt-avatar {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: var(--r-full);
  background: var(--brand-100);
  color: var(--brand-700);
  font-weight: 700;
  font-size: 0.85rem;
}

.pt-user-name { font-size: 0.88rem; font-weight: 600; line-height: 1.2; }
.pt-user-role { font-size: 0.75rem; color: var(--ink-500); }

/* ----------------------------------------------------------- breadcrumb ---- */

.pt-crumbs {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2);
  font-size: 0.82rem; color: var(--ink-500);
  margin-bottom: var(--s-4);
}

.pt-crumbs a { color: var(--ink-600); text-decoration: none; }
.pt-crumbs a:hover { color: var(--brand-600); text-decoration: underline; }
.pt-crumbs [aria-current="page"] { color: var(--ink-800); font-weight: 600; }
.pt-crumbs-sep { color: var(--ink-300); }

/* ------------------------------------------------------------- headings ---- */

.pt-head {
  display: flex; align-items: flex-start; gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-6);
}

.pt-head h1 { margin: 0 0 var(--s-1); font-size: 1.6rem; line-height: 1.2; }
.pt-head p { margin: 0; color: var(--ink-600); }
.pt-head-actions { margin-left: auto; display: flex; gap: var(--s-3); flex-wrap: wrap; }

/* ------------------------------------------------------------ dashboard ---- */

.pt-grid { display: grid; gap: var(--s-5); }
.pt-grid-stats { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.pt-grid-split { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }


/* ── The stat tile's three lines, actually on three lines ────────────────────
   .stat is a site.css component and its parts are <span>s, but nothing in site.css
   gives them a display — so "7", "TOTAL EVENTS" and "2 drafts" rendered as one run
   of inline text: "7TOTAL EVENTS2 drafts". The margins .stat-label and .stat-note
   already carry were doing nothing, because vertical margins do not apply to an
   inline box. site.css:623 is the same fix made locally for .order-summary, which
   is the precedent for this being the missing declaration rather than a new idea.

   IN portal.css, so it reaches the four pages that use these tiles — the dashboard,
   the super-admin dashboard, the volunteer roster and the event volunteer overview —
   and cannot reach the booking journey, which loads site.css alone and has its own
   .stat-label treatments under .order-summary, .ref-panel and .confirm-sheet.

   The note's margin-top is dropped: with the label's margin-bottom below it there
   were two gaps stacking where the design wants one. */
.stat-value,
.stat-label,
.stat-note { display: block; }

.stat-note { margin-top: 0; }
/* A metric this dashboard cannot total yet, kept visually distinct from a real zero — which is
   the whole point of the distinction. A full-strength "0" is a measurement; a muted dash at the
   note's own weight is the absence of one, and the two cannot be confused at a glance.

   Colour is not the only signal: the character differs too, and each tile carries an aria-label
   saying the figure is not totalled here yet. A muted dash alone would mean nothing to somebody
   who cannot tell two greys apart. */
.stat-value-pending {
  color: var(--ink-400);
  font-weight: 400;
}


/* ------------------------------------------------------------------ tabs ---- */

/* The tab strip is horizontally scrollable on purpose: Phase 1 has one tab and the
   design anticipates fifteen. Wrapping them onto three rows is what makes an
   enterprise form look unfinished.

   SCROLLING WAS ALWAYS THE DESIGN; WHAT WAS MISSING WAS SAYING SO. On a phone the
   volunteer's four tabs measure 459px against about 343px of usable width, so the last
   one — "Check-in history" — was cut mid-word with no scrollbar a touch device ever
   draws. A clipped label with no affordance does not read as "scroll me", it reads as
   broken, and the strip is at its worst on exactly the page that needs it: land on
   Check-in history and the tab telling you where you are was the one off-screen.
   The fade below and portal-tabs.js answer those two things and nothing else. */
.pt-tabs {
  display: flex;
  gap: var(--s-1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--s-6);
  overflow-x: auto;
  scrollbar-width: thin;

  /* The swipe belongs to the strip. Without this, flicking past the last tab hands the
     gesture to the page and the whole layout slides sideways under the reader. */
  overscroll-behavior-x: contain;

  /* Keeps a tab scrolled into view clear of the fade rather than tucked beneath it. */
  scroll-padding-inline: var(--s-4);
}

/* Only for a reader who has not asked for less movement: this scrolls on page load. */
@media (prefers-reduced-motion: no-preference) {
  .pt-tabs { scroll-behavior: smooth; }
}

/* The affordance, applied by portal-tabs.js only on the side that actually has more to
   show — a permanent fade over a strip that fits is a lie about hidden content.

   A MASK RATHER THAN A GRADIENT OVERLAY, because a mask carries no colour. The strip
   sits on a different background in light and dark, and an overlay would have to know
   which; this fades whatever is behind it to transparent and cannot be wrong. Where
   mask-image is unsupported the fade is simply absent and the strip still scrolls. */
.pt-tabs.is-overflow-end {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
}

.pt-tabs.is-overflow-start {
  -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
}

.pt-tabs.is-overflow-start.is-overflow-end {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 2.5rem,
    #000 calc(100% - 2.5rem), transparent);
  mask-image: linear-gradient(to right, transparent, #000 2.5rem,
    #000 calc(100% - 2.5rem), transparent);
}

.pt-tab {
  flex: 0 0 auto;
  padding: var(--s-3) var(--s-4);
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--ink-600);
  font: inherit;
  font-weight: 550;
  text-decoration: none;
  white-space: nowrap;
  min-height: 44px;
}

.pt-tab:hover { color: var(--ink-900); }

.pt-tab[aria-current="page"] {
  color: var(--brand-700);
  border-bottom-color: var(--brand-600);
}

.pt-tab.is-soon { color: var(--ink-300); cursor: default; pointer-events: none; }

/* --------------------------------------------------------- link preview ---- */

/* The generated public link, shown beside the slug field. Read-only by design: the
   organiser controls the slug, and the link is what the slug produces. Showing it
   live is what makes "this cannot change after publishing" intelligible before they
   publish rather than after. */
.pt-linkpreview {
  display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap;
  margin-top: var(--s-2);
  padding: var(--s-2) var(--s-3);
  background: var(--surface-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm);
  font-size: 0.85rem;
  color: var(--ink-600);
}

.pt-linkpreview code {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  color: var(--ink-800);
  word-break: break-all;
}

/* The address as a link (issues #162, #108). Underlined, because a monospace
   string in a dashed box reads as "copy me", and the underline is what says
   "or tap me". The code keeps its own colour so the box looks the same as
   before until it is hovered. min-width: 0 so the long address still breaks
   inside the flex row instead of widening it. */
.pt-linkpreview a { min-width: 0; color: inherit; text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 3px; }
.pt-linkpreview a:hover code, .pt-linkpreview a:focus-visible code { color: var(--brand-600); }
.pt-linkpreview a:hover { text-decoration-color: var(--brand-600); }

/* The organisations sitting in a status, under its badge on the dashboard.

   A list rather than a comma-joined line: names run to four or five words here
   ("Hillside Cultural Society"), and separating those with commas leaves the
   reader working out where one organisation ends and the next begins. One per
   line answers "who is in this state" at a glance, which is the only question
   this panel is for — the count beside it and the View link own the rest. */
.pt-status-orgs {
  list-style: none;
  margin: var(--s-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--t-sm);
  color: var(--ink-700);
}

/* Long names wrap rather than widening the column and pushing the count and the
   View button out of the card. */
.pt-status-orgs li { overflow-wrap: anywhere; }

/* ------------------------------------------------------------ responsive ---- */

/* The drawer's own furniture, hidden until there is a drawer. Declared here rather
   than only inside the query below, so this file states the desktop layout without
   anybody having to read a media query to know that a portal sidebar has no close
   button in it. */
.pt-burger,
.pt-side-close,
.pt-side-account,
.pt-scrim { display: none; }

@media (max-width: 960px) {
  /* ── The sidebar becomes a drawer ──────────────────────────────────────────
     WHAT THIS REPLACED. These rules used to turn .pt-side into a horizontally
     scrolling strip — flex-direction: row, overflow-x: auto — and hid the group
     headings and the Events sub-menu outright. Measured at a 375px viewport it
     wanted 1651px and had 375: 1276px of navigation off-screen. "Dashboard"
     ended at 373px, so it was the only entry visible, and everything from
     "Events" to "Settings" could be reached only by dragging a strip most people
     never learn is draggable. The page scrolled sideways by 39px as well, which
     put "Sign out" past the right edge of the screen.

     THE SAME <aside>, THE SAME <nav>, THE SAME LINKS. Nothing is duplicated and
     nothing is re-rendered: the element that is the left column above this width
     is the panel below it. One navigation, one place to change it.

     It keeps flex-direction: column from the unconditional rule, because the
     drawer wants precisely the layout the sidebar already has. */
  .pt-shell { grid-template-columns: 1fr; }

  .pt-side {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    z-index: 60;
    width: min(86vw, 300px);

    /* dvh, with vh underneath for anything that does not know it: a mobile
       browser's collapsing address bar makes 100vh taller than the window, and
       the first thing to fall off the bottom is the account block. */
    height: 100vh;
    height: 100dvh;

    /* A menu longer than the phone scrolls inside the drawer. overscroll-behavior
       stops a flick that reaches the end from scrolling the page behind it. */
    overflow-y: auto;
    overscroll-behavior: contain;

    border-bottom: 0;
    box-shadow: var(--shadow-lg);

    /* CLOSED IS visibility: hidden, NOT A TRANSFORM ALONE. A panel pushed
       off-screen by a transform is still in the tab order and still read out: nine
       links and a sign-out button, invisible, ahead of the page. visibility takes
       it out of both. It is listed in the transition below so it flips late on the
       way out and the slide is still seen. */
    transform: translateX(-100%);
    visibility: hidden;
  }

  .pt-side.is-open { transform: none; visibility: visible; }

  /* The drawer's head: brand left, close right, reading as one bar. min-width: 0
     lets a long organisation name shrink instead of pushing the button out. */
  .pt-side-head { display: flex; align-items: center; gap: var(--s-3); }
  .pt-side-head .pt-side-brand { flex: 1 1 auto; min-width: 0; }
  .pt-side-close { display: inline-flex; margin-left: auto; }

  /* 44px, not the 40px .icon-btn gives every other bar in the product. This is the
     only control on the screen that reaches the navigation, and .pt-nav-link's own
     comment sets 44px as this product's minimum target — the button that opens the
     menu should not be smaller than the items inside it. */
  .pt-burger {
    display: inline-flex;
    width: 44px;
    height: 44px;
    margin-left: calc(var(--s-2) * -1);   /* optical: aligns the glyph, not the box */
  }

  /* The page title gives way rather than shoving the avatar off the screen — the
     same order of preference the public header states at its own narrow widths. */
  .pt-top-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ── The scrim ─────────────────────────────────────────────────────────────
     The same rgb(0 0 0 / 0.45) as .pt-dialog::backdrop, so an open drawer dims
     the page exactly as an open dialog does. */
  .pt-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 50;
    background: rgb(0 0 0 / 0.45);
    opacity: 0;
    visibility: hidden;
  }

  .pt-scrim.is-open { opacity: 1; visibility: visible; }

  /* The page behind the drawer does not scroll under it. Inside the query, so a
     desktop can never end up with overflow hidden on its body. */
  body.pt-nav-open { overflow: hidden; }

  /* ── The account block, at the foot of the drawer ───────────────────────────
     margin-top: auto holds it at the bottom when the menu is shorter than the
     screen; the border is the same hairline the rest of the portal divides with.
     Full-width buttons because the drawer is a column of stretched items. */
  .pt-side-account {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-2);
    margin-top: auto;
    padding-top: var(--s-4);
    border-top: 1px solid var(--border);
  }

  .pt-side-account-name {
    margin: 0 0 var(--s-1);
    font-size: var(--t-sm);
    font-weight: 600;
    color: var(--ink-700);
    overflow-wrap: anywhere;
  }

  /* NO form RULE HERE ANY MORE: the drawer's sign-out form moved into the account menu, which
     carries it at every width, and this block holds only links now. See _PortalSidebar.cshtml. */
  .pt-side-account .btn { width: 100%; }

  /* ── The top bar ───────────────────────────────────────────────────────────
     The bar's copy of the account actions comes off; the drawer's copy is what is
     displayed. Exactly one of the two, at every width. */
  .pt-top-account { display: none; }

  .pt-content { padding: var(--s-5) var(--s-4) var(--s-8); }
  .pt-top { padding-inline: var(--s-4); }
}

/* Motion is opt-in across this product, the same way .pt-dialog's entrance is: a
   reader who asked for less movement gets a drawer that is simply there. The
   global reduced-motion switch in site.css would neutralise a transition anyway —
   this states the intent rather than relying on an !important elsewhere. */
@media (prefers-reduced-motion: no-preference) and (max-width: 960px) {
  .pt-side {
    transition: transform var(--dur) var(--ease-out), visibility var(--dur);
  }

  .pt-scrim {
    transition: opacity var(--dur) var(--ease-out), visibility var(--dur);
  }
}

@media (max-width: 560px) {
  .pt-user-name, .pt-user-role { display: none; }
  .pt-head-actions { width: 100%; }

  /* ---------------------------------------------------- stacked table ----
     One block per row instead of a table that scrolls sideways.

     A three-column table inside the console's card needs 345px at a 375px
     viewport and has 261px — measured. .table-scroll then does what it is for
     and scrolls horizontally, which on this table means the organisation name
     is compressed into a 134px column four lines tall while the submitted date
     and the Review button sit off the right edge. The name is the one thing
     the row is about, so it is the wrong thing to lose.

     Opt-in via .pt-table-stack, not applied to .table, because the booking
     journey's tables are genuine numeric grids where the columns have to stay
     side by side to be compared. Every rule is inside this query, so the
     desktop layout is not touched.

     The honest cost: display:block removes the row and column relationships
     from the accessibility tree. That is why each cell carries its heading in
     data-label — the information stays, the grid does not. Against a table
     whose first column is unreadable and whose last is off-screen, this is the
     better trade at this width. */
  .pt-table-stack,
  .pt-table-stack tbody,
  .pt-table-stack tr,
  .pt-table-stack td {
    display: block;
    width: 100%;
  }

  /* The headings move into the cells below, so the row of them is redundant —
     hidden the way .sr-only does it rather than with display:none, which would
     also take it from a screen reader. */
  .pt-table-stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The border between rows becomes the divider between blocks. */
  .pt-table-stack tr {
    padding: var(--s-4) 0;
    border-bottom: 1px solid var(--border);
  }

  .pt-table-stack tr:last-child { border-bottom: 0; }
  .pt-table-stack tr:hover { background: none; }

  .pt-table-stack td {
    padding: 0 0 var(--s-2);
    border-bottom: 0;
  }

  .pt-table-stack td:last-child { padding-bottom: 0; }

  /* The column heading, carried down with its cell. Same treatment .table th
     already uses, so a stacked row reads like the table it came from. */
  .pt-table-stack td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: var(--t-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ink-500);
    font-weight: 700;
    margin-bottom: 2px;
  }

  /* The organisation name leads the block and needs no label above it. Full
     width, so a long name wraps as prose instead of down a narrow column. */
  .pt-cell-primary { font-size: var(--t-md); }

  /* A right-aligned figure has nothing to align against once stacked. */
  .pt-table-stack td.num { text-align: left; }

  /* THE ROW ACTIONS NEED NO OVERRIDE HERE. .vol-row-actions wraps at every
     width now, so there is no nowrap left for a phone-sized query to undo. */

  /* ------------------------------------------------ status breakdown ----
     Keeps its three columns where the queue table stacks.

     Stacking is right when every cell holds a sentence. Here the cells are a
     badge, one digit and a small button: turned into full-width blocks, the
     count and the View link each took a line of their own and no longer read as
     belonging to the status above them. The fix is not to stack this one.

     table-layout: fixed is what makes it hold: without it the first column is
     sized from its widest word, so a long organisation name widens column one
     and shoves the count and the button out of the card. Fixed layout means the
     two narrow columns take the widths set below, column one takes whatever is
     left, and the names wrap inside it — the primary row stays put however long
     a name is. */
  .pt-status-table { table-layout: fixed; width: 100%; }

  /* Top, not middle. .table td centres its content vertically, so beside a cell
     three names tall the count and the button drifted to the middle of the block
     instead of sitting on the badge's line. */
  .pt-status-table td {
    vertical-align: top;
    padding: var(--s-3) var(--s-2);
  }

  /* The card already provides the outer gutter. */
  .pt-status-table td:first-child { padding-left: 0; }
  .pt-status-table td:last-child { padding-right: 0; }

  /* Fixed so every row's count and button line up in the same place, whatever
     is above them. Sized to the content they actually hold — counts here are
     one or two digits, and the button says "View". */
  .pt-status-count { width: 2.5rem; }
  .pt-status-action { width: 4.5rem; }

  /* The button fills its column rather than sitting at an arbitrary width, which
     is what gives the third column one edge down the whole table. */
  .pt-status-action .btn { width: 100%; padding-inline: 0; }
}

/* ============================================================================
   ONBOARDING — registration, verification, sign-in
   ----------------------------------------------------------------------------
   Used by _OnboardingLayout. A narrow, centred column rather than the portal's
   left rail: nobody on these pages has an organisation yet, so there is nothing
   to navigate to and a sidebar would be an empty promise.
   ============================================================================ */

.ob-shell {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--surface-1);
}

.ob-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-6);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.ob-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  text-decoration: none;
  color: inherit;
}

.ob-top-actions { display: flex; align-items: center; gap: var(--s-3); }

.ob-main {
  flex: 1;
  width: 100%;
  max-width: 780px;
  margin-inline: auto;
  padding: var(--s-8) var(--s-5) var(--s-10);
}

/* The profile step has four sections of two-column rows; 780px squeezes it. */
.ob-main-wide { max-width: 1040px; }

/* ── ONE CARD, ONE PADDING ───────────────────────────────────────────────────
   WHAT WAS WRONG, MEASURED ON /register AT 375px. Every onboarding form is a
   <section class="card"> wrapping a <div class="card-pad">, and BOTH pad: the
   card 16px a side on a phone (site.css's 700px block) and the inner block 24px
   on top of it. So a 343px card gave its fields 261px and kept 82px — nearly a
   quarter of the screen — as two nested gutters. "e.g. Riverside Community
   Association" does not fit in what is left, which is the clipped placeholder
   that was reported.

   THE FIELDS WERE NEVER TOO WIDE. Nothing overflowed and nothing was cut off by
   the viewport: the inputs are width:auto in a grid column and already
   border-box, so they fill whatever they are given. They were given too little.
   Widening the input would have been the wrong fix — the container was the
   problem, so the container is what changes.

   :has rather than a new class, because the pairing is what is wrong and there
   are dozens of these cards. A card with no .card-pad — the empty states, the
   result panels — keeps its own padding untouched.

   SCOPED TO .ob-main ON PURPOSE. The portal's cards double up in exactly the
   same way and would also be better for this, but that is every screen behind
   sign-in; this is the onboarding column, which is the narrow one-form-per-page
   shell the reported page belongs to. */
.ob-main .card:has(> .card-pad) { padding: 0; }

.ob-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-5) var(--s-6);
  border-top: 1px solid var(--border);
  font-size: var(--t-sm);
  color: var(--ink-500);
}

.ob-head { margin-bottom: var(--s-6); }
.ob-head h1 { margin: 0 0 var(--s-2); }
.ob-head p { margin: 0; color: var(--ink-600); }

/* --------------------------------------------------------------- steps ---- */

.ob-steps {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  list-style: none;
  margin: 0 0 var(--s-6);
  padding: 0;
  overflow-x: auto;
}

.ob-step {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex: 0 0 auto;
  font-size: var(--t-sm);
  color: var(--ink-500);
}

/* The connector between steps. A pseudo-element on the FOLLOWING item rather
   than a border on each one, so the last step does not trail a line into
   nothing. */
.ob-step + .ob-step::before {
  content: "";
  width: 28px;
  height: 2px;
  background: var(--border);
  margin-right: var(--s-2);
}

.ob-step-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--surface);
  font-size: var(--t-sm);
  font-weight: 700;
  flex: 0 0 auto;
}

.ob-step-dot .icon { width: 16px; height: 16px; }

.ob-step.is-current { color: var(--ink-900); font-weight: 600; }
.ob-step.is-current .ob-step-dot { border-color: var(--brand-600); color: var(--brand-600); }

.ob-step.is-done .ob-step-dot {
  background: var(--brand-600);
  border-color: var(--brand-600);
  color: #fff;
}

/* -------------------------------------------------------- form sections ---- */

/* A titled block inside a long form. The profile step has four; without the
   grouping it is thirty fields in one column and nobody finishes it. */
.ob-section + .ob-section {
  margin-top: var(--s-6);
  padding-top: var(--s-6);
  border-top: 1px solid var(--border);
}

.ob-section-head { margin-bottom: var(--s-4); }
.ob-section-head h2 { margin: 0 0 var(--s-1); font-size: var(--t-md); }
.ob-section-head p { margin: 0; font-size: var(--t-sm); color: var(--ink-500); }

/* Two columns where there is room, one where there is not. auto-fit rather than
   a fixed count, so this needs no breakpoint of its own. */
.ob-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 var(--s-5);
}

.ob-grid-full { grid-column: 1 / -1; }

.ob-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  align-items: center;
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
}

.ob-actions-end { margin-left: auto; }

/* ----------------------------------------------------- result / outcome ---- */

/* Verification results and the submitted confirmation. Centred, with room for
   the illustration — these pages carry one message and should look like it. */
.ob-result { text-align: center; padding: var(--s-6) var(--s-4); }
.ob-result h1 { margin: var(--s-5) 0 var(--s-3); }
.ob-result p { margin: 0 auto var(--s-4); max-width: 52ch; color: var(--ink-600); }
.ob-result-art { display: block; margin-inline: auto; width: 100%; max-width: 220px; height: auto; }

.ob-result-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  justify-content: center;
  margin-top: var(--s-6);
}

/* ------------------------------------------------------- password meter ---- */

.ob-meter { display: block; margin-top: var(--s-2); }

.ob-meter-track {
  display: block;
  height: 6px;
  border-radius: var(--r-sm);
  background: var(--surface-3);
  overflow: hidden;
}

.ob-meter-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--r-sm);
  background: var(--danger-600);
  transition: width var(--dur) var(--ease-out), background-color var(--dur) var(--ease-out);
}

.ob-meter[data-score="1"] .ob-meter-fill { width: 25%; }
.ob-meter[data-score="2"] .ob-meter-fill { width: 50%;  background: var(--warn-600); }
.ob-meter[data-score="3"] .ob-meter-fill { width: 75%;  background: var(--warn-600); }
.ob-meter[data-score="4"] .ob-meter-fill { width: 100%; background: var(--ok-600); }

/* --------------------------------------------------- password visibility ---- */

/* The eye inside a password box. Sits beside the meter above because it belongs to
   the same four forms — sign-in, registration, activation and reset — and those all
   use _OnboardingLayout, which is the layout that loads this file.

   IN portal.css, NOT site.css. The .input it extends is a site.css component, but
   nothing outside these forms needs the extension, and site.css is shared with the
   public landing page whose styling is frozen. .ob-meter above made the same call.

   THE INPUT'S HEIGHT AND THE FORM'S LAYOUT ARE UNCHANGED. .pw-field is a plain
   block wrapping the input alone: it adds no margin, and the label, the meter and
   the validation message stay exactly where .field already puts them. The only
   thing that moves is the input's own right padding, so typed text stops before the
   button rather than running under it. */
.pw-field { position: relative; display: block; }

/* Room for the button. Scoped to .pw-field, so no other .input in the product is
   given padding it has no use for. */
.pw-field .input { padding-right: 52px; }

/* 44px, this product's minimum target — the same figure .pt-nav-link's own comment
   sets, and it fits: .input is 46px tall, which leaves a pixel either side.

   Centred by transform rather than by top/bottom: the box is 46px only until
   somebody's browser font makes it taller, and an offset measured from the middle
   survives that where a hard-coded top does not. */
.pw-reveal {
  position: absolute;
  top: 50%;
  right: 1px;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-500);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
}

.pw-reveal:hover { color: var(--ink-800); }

/* No focus rule of its own: site.css styles :focus-visible globally with a 2px
   brand outline, which is what every other control in this product shows. */

.pw-reveal .icon { width: 20px; height: 20px; }

/* ── Which eye ──────────────────────────────────────────────────────────────
   Both are rendered by the server, because _Icon is a partial and a script cannot
   call it; CSS then shows one. The alternative was building an SVG in JavaScript,
   which puts a copy of the icon set somewhere it can drift from the real one.

   Keyed off .is-shown on the button, the same class vocabulary portal-nav.js and
   welcome-v3.js use for a control that is currently doing something. */
.pw-reveal .pw-eye-off { display: none; }
.pw-reveal.is-shown .pw-eye { display: none; }
.pw-reveal.is-shown .pw-eye-off { display: inline-flex; }

.pw-eye,
.pw-eye-off { display: inline-flex; }

@media (max-width: 560px) {
  .ob-main { padding: var(--s-6) var(--s-4) var(--s-8); }
  .ob-step-label { display: none; }
  .ob-step + .ob-step::before { width: 16px; }
  .ob-top-hint { display: none; }

  /* THE LAST 3px, AND WHERE THEY CAME FROM. With the doubled card padding gone
     (see .ob-main .card:has above) a 375px screen gives the organisation-name
     field 261px of text, and "e.g. Riverside Community Association" at the
     product's 16px field size measures 264. Three pixels — one character's tail
     — still clipped.

     TAKEN FROM THE GUTTER, NOT FROM THE TYPE. Shrinking the font would make
     every field on the form harder to read to fix one placeholder, and the
     input's own 16px padding is what makes a tapped field comfortable. 24px of
     inner gutter is generous on a phone, so the inline half drops to 16 and the
     field gains all of it. Vertical padding is untouched: that is the rhythm
     between sections, and it is not what ran out.

     Inline only, and only under 560px — the desktop card keeps its 24px. */
  .ob-main .card-pad { padding-inline: var(--s-4); }

  /* THE SHELL'S OWN HEADER AND FOOTER, which are the two places an onboarding page
     can overflow sideways on a phone. The header's 32px side padding plus a 40px
     mark plus the wordmark plus a button is more than a 375px screen has; the
     footer sets three items in one non-wrapping row, and the third is now a build
     stamp long enough to read ("v1.4.0 · built 10 Sep 2026, 01:03 AM AEST").

     Both are fixed here rather than in the role chooser's own block because both
     belong to _OnboardingLayout and every page in it has the same problem. */
  .ob-top { padding: var(--s-3) var(--s-4); gap: var(--s-3); }
  .ob-brand { min-width: 0; gap: var(--s-2); }
  .ob-brand .brand-mark { width: 32px; height: 32px; }
  .ob-brand .brand-name { font-size: 1.1rem; }
  .ob-top-actions { flex: 0 0 auto; }

  .ob-foot { flex-wrap: wrap; justify-content: flex-start; gap: var(--s-2) var(--s-4); padding: var(--s-5) var(--s-4); }
}

/* ============================================================================
   CONFIRMATION DIALOG
   ----------------------------------------------------------------------------
   Rejecting and suspending an organisation both need a REASON, and that reason
   is emailed to the applicant word for word. window.confirm() cannot collect
   one — it returns a boolean — so these actions need a real dialog.

   <dialog> rather than a hand-built overlay: the browser provides focus
   trapping, Escape-to-close, inertness of the page behind it and the top layer
   for free, and a div-based modal gets at least one of those wrong.
   ============================================================================ */

.pt-dialog {
  width: min(520px, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--ink-900);
  box-shadow: var(--shadow-lg);
}

/* ------------------------------------------------- a dialog holding a form ----
   WIDER RATHER THAN TALLER, and the two numbers below are why the add-volunteer
   modal had a scrollbar on a desktop.

   The cap used to be min(88vh, 46rem). 46rem is 736px, so the dialog stopped at
   736px on EVERY screen however tall the window was — the viewport was never the
   constraint, this line was. The content measured about 1110px, so roughly 600px
   of an eight-field form sat below the fold on a 1080p display.

   88vh alone now, so a tall window shows the whole form and a short one still
   scrolls inside the body rather than off the top of the screen. The remaining
   height came out of the content, not out of the frame: the section descriptions
   went, four hints became one line each, and the field rhythm below is tighter.

   760px keeps it inside the 700-800px the design calls for, and .ob-grid-pairs
   gives two ~340px columns at that width — comfortable for a label and a 46px
   input, and collapsing to one column at 560px on its own. */
.pt-dialog-wide {
  width: min(760px, calc(100vw - 2rem));
  max-height: 88vh;

  /* THE HEAD AND FOOTER ARE PINNED AND ONLY THE BODY SCROLLS, and without these
     three declarations that is not what max-height does.

     A <dialog> is display:block. Capping its height while its child is a <form>
     taller than the cap does NOT make anything scroll — the form simply
     overflows. Measured: the dialog box ended at y=844 while the footer sat at
     y=1016, so Cancel and "Add volunteer" were outside the box entirely. The
     old rule set overflow-y on .pt-dialog-body, but nothing constrained that
     element's height, so it was never the thing that overflowed.

     The chain has to be unbroken: the dialog is the flex column, the form is a
     flex column inside it, and the body is the one item allowed to shrink.
     min-height:0 on both is what permits that — a flex item defaults to
     min-height:auto and refuses to shrink below its content, which is the
     reason a nested scroll area silently does nothing.

     THE display DECLARATION MOVED TO [open] BELOW, AND THAT IS A BUG FIX.
     See the rule after this one. */
  overflow: hidden;

  /* The containing block for the close button, which is positioned rather than
     placed in a flex row: .pt-dialog-head is shared with the super-admin's
     reason dialogs, which have no close button and must not gain a flex
     context they do not use. */
  position: relative;
}

/* ── THE FLEX COLUMN APPLIES ONLY WHILE THE DIALOG IS OPEN ──────────────────────
   A closed <dialog> is hidden by one UA rule and one only:

     dialog:not([open]) { display: none; }

   Any author rule that sets display on the same element beats it, because the UA
   sheet is the lowest-priority origin there is. `display: flex` sat on
   .pt-dialog-wide unconditionally, so the add-volunteer modal was laid out in
   normal flow on every page load — a 760px panel sitting under the roster,
   fully visible, with no open attribute anywhere. Measured on a closed dialog:
   display flex, 760 x 354, top 384. That is the panel "appearing over the bottom
   of the table" in the report; nothing had been clicked and no state was set.

   [open] is what the UA rule keys on, so scoping to it restores the hidden
   default exactly and leaves the scroll chain untouched when the dialog is up.
   showModal() sets the same attribute, so the modal path is unaffected.

   The other declarations stay unscoped on purpose: width, max-height and
   position describe a box that is not being painted, and splitting them would
   only make the open and closed rules harder to read together. */
.pt-dialog-wide[open] {
  display: flex;
  flex-direction: column;
}

.pt-dialog-wide > form {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.pt-dialog-wide .pt-dialog-head,
.pt-dialog-wide .pt-dialog-actions { flex: 0 0 auto; }

.pt-dialog-wide .pt-dialog-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

/* TIGHTER INSIDE A MODAL, AND ONLY INSIDE ONE. Every rule here is scoped to
   .pt-dialog-wide, because the registration profile form uses these same
   classes on a full page where the roomier rhythm is right — it scrolls anyway
   and thirty fields need the air. A modal is the opposite case: it is asking for
   one short thing and every 8px is a line closer to a scrollbar. */
.pt-dialog-wide .ob-section + .ob-section {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
}

.pt-dialog-wide .ob-section-head { margin-bottom: var(--s-2); }
.pt-dialog-wide .field { margin-bottom: var(--s-3); }

/* The body's own padding, one step in from the head and footer's. 24px top and
   bottom on a form this long is 48px of the height budget spent on air that the
   section rhythm already provides. */
.pt-dialog-wide .pt-dialog-body { padding: var(--s-4) var(--s-5); }

/* Two rows, not the shared 120px. .textarea's default height is right for a
   description on a page and is more than this dialog can spare; resize:vertical
   is inherited, so anybody with more to say can drag it. */
.pt-dialog-wide .textarea { min-height: 60px; }

/* The close button. Bare glyph, borderless, the same treatment .toast-close
   already uses for the same job — see site.css. Sized to the 24px WCAG 2.5.8
   target rather than to the character, which on its own is a 10px hit area. */
/* THE TWO CHECKBOX SECTIONS SIDE BY SIDE, which is where the last of the height
   came from. Verification and Portal access are one control and one line of
   help each — stacked, they cost two headings, two separators and two rows of a
   form that was already too tall, while the right half of both rows sat empty.

   Both headings are kept, so the four sections the design calls for are all
   still there and still named; only their arrangement changed. One column at
   560px, the same breakpoint .ob-grid-pairs collapses at. */
.vol-dialog-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-5);
}

@media (max-width: 560px) {
  .vol-dialog-pair { grid-template-columns: 1fr; gap: var(--s-4); }
}

.pt-dialog-close {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-500);
  font-size: var(--t-lg);
  line-height: 1;
  cursor: pointer;
}

.pt-dialog-close:hover { background: var(--surface-2); color: var(--ink-900); }

/* The head's own right padding has to clear the button, or a long heading runs
   underneath it. */
.pt-dialog-wide .pt-dialog-head { padding-right: calc(var(--s-5) + 32px); }


.pt-dialog::backdrop { background: rgb(0 0 0 / 0.45); }

.pt-dialog-head { padding: var(--s-5) var(--s-5) 0; }
.pt-dialog-head h2 { margin: 0 0 var(--s-2); font-size: var(--t-md); }
.pt-dialog-head p  { margin: 0; font-size: var(--t-sm); color: var(--ink-600); }

.pt-dialog-body { padding: var(--s-5); }

.pt-dialog-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}

/* Motion is opt-in across this product — the reveal animations on the public
   page are gated the same way — so a reader who asked for less movement gets a
   dialog that simply appears. */
@media (prefers-reduced-motion: no-preference) {
  .pt-dialog[open] { animation: pt-dialog-in 160ms var(--ease-out); }

  @keyframes pt-dialog-in {
    from { opacity: 0; transform: translateY(6px) scale(0.98); }
    to   { opacity: 1; transform: none; }
  }
}

/* --------------------------------------------------------------- detail ---- */

/* Label/value pairs on the organisation detail page. A description list,
   because that is what it is — and it reads correctly to a screen reader with
   no extra markup. */
.pt-detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s-5);
  margin: 0;
}

.pt-detail > div { min-width: 0; }

.pt-detail dt {
  font-size: var(--t-sm);
  color: var(--ink-500);
  margin-bottom: var(--s-1);
}

.pt-detail dd { margin: 0; font-weight: 600; overflow-wrap: anywhere; }

/* ------------------------------------------------------------- timeline ---- */

.pt-timeline { list-style: none; margin: 0; padding: 0; }

/* TIGHTER, AND THE TWO NUMBERS ARE DOING DIFFERENT JOBS. The bottom padding is
   the gap between one entry and the next — s-5 (24px) between three-line entries
   left the history reading as a list of distant events rather than a sequence, and
   an organisation's lifecycle is short enough to want to see at once. s-4 (16px) is
   still clearly a gap.

   The left padding is the space between the connector and the text, not between
   entries; s-6 (32px) was indenting every line a third of the way across a phone.
   s-5 clears the 10px dot with room to spare. */
.pt-timeline li {
  position: relative;
  padding: 0 0 var(--s-4) var(--s-5);
  border-left: 2px solid var(--border);
}

.pt-timeline li:last-child { border-left-color: transparent; padding-bottom: 0; }

.pt-timeline li::before {
  content: "";
  position: absolute;
  left: -6px;
  top: 4px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--brand-600);
}

.pt-timeline-when { font-size: var(--t-sm); color: var(--ink-500); }
.pt-timeline-what { font-weight: 600; }
.pt-timeline-why  { font-size: var(--t-sm); color: var(--ink-600); margin-top: var(--s-1); }

/* ------------------------------------------------- super-admin organisation ---- */

/* The organisation details page. Every rule here is spacing and type — the actions,
   the badges and the history entries are the ones that were already there.

   WHAT WAS ACTUALLY WRONG WITH THE PAGE, measured rather than guessed: each section
   was <section class="card"> wrapping <div class="card-pad">, and both carry
   padding: var(--s-5). 24px twice on all four sides, five times down the page, plus
   a .card-head with its own 16px margin below it. The content was never sparse; the
   chrome around it was doubled. Removing the inner wrapper is most of this change,
   and it needed no CSS at all. */

/* ── The header ─────────────────────────────────────────────────────────────
   Name and status on one line, type beneath. min-width: 0 so a long
   organisation name wraps instead of pushing the two actions off the row. */
.sa-head-id { min-width: 0; }

.sa-head-name {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-3);
}

.sa-head-name h1 { margin: 0; overflow-wrap: anywhere; }

/* ── The decision card ──────────────────────────────────────────────────────
   The status on the left, the actions on the right, one row on a desktop and
   two on a phone. space-between rather than a fixed split: with one button
   there is nothing to align a second column against. */
.sa-decision-row {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: space-between;
  gap: var(--s-4);
}

.sa-decision-state { margin: 0; min-width: 0; }

.sa-decision-status {
  margin: var(--s-1) 0 0;
  font-size: var(--t-md);
  font-weight: 650;
}

/* ── The fact grids ─────────────────────────────────────────────────────────
   TWO ALIGNED COLUMNS, which is what .pt-detail-compact could not give. That
   class is CSS multi-column — items flow down one column and into the next, so
   rows never line up across the gap and a tall item (the address, four lines)
   leaves the short column ragged at the bottom. It is still right for the five
   pages that use it, where the grouping IS top-to-bottom; here the request was
   a grid of pairs, and .ob-grid-pairs already draws one and already collapses
   to a single column at 560px.

   The dt/dd treatment is .pt-detail's, restated, because dropping .pt-detail
   from the class list dropped its label styling with it. */
/* ROWS TIGHTER THAN COLUMNS, which .pt-grid's single gap cannot express. 24px
   between two columns keeps them apart; 24px between a value and the next label
   stacked on a phone is airier than a label/value pair needs — it made the
   Organisation card 658px at 375px, measured. 16px rows, 24px columns. */
.sa-facts {
  margin: 0;
  gap: var(--s-4) var(--s-5);
}

.sa-facts > div { min-width: 0; }

.sa-facts dt {
  font-size: var(--t-sm);
  color: var(--ink-500);
  margin-bottom: var(--s-1);
}

.sa-facts dd {
  margin: 0;
  font-weight: 600;
  overflow-wrap: anywhere;   /* an address, a long email */
}


/* ── The super-admin events list ────────────────────────────────────────────
   Two rules, both spacing. The list's real problem was that it carried
   class="pt-table" inside class="pt-table-scroll" and NEITHER NAME HAS A RULE
   anywhere in this product — it rendered as a bare browser table. It now uses
   .table-scroll / .table / .pt-table-stack like every other table here, so the
   padding, the header caps, the separators, the hover and the phone layout all
   come from CSS that already existed. */

/* The organisation, named under the heading rather than inside a sentence. */
.sa-head-owner {
  margin: var(--s-1) 0 0;
  font-size: var(--t-md);
  font-weight: 650;
  color: var(--ink-800);
  overflow-wrap: anywhere;
}

/* The actions sit at the right of the row on a desktop and at the left of their
   own block on a phone. text-align was inline on the cell, which .pt-table-stack
   cannot override once the cell becomes a block — the button ended up pinned to
   the right of a full-width card with the label above it on the left. */

/* ── A warning button tone ───────────────────────────────────────────────────
   The button system is driven by --btn-bg / --btn-fg / --btn-border, so a tone
   is three custom properties and nothing else. This one was missing: the set
   had primary, secondary, ghost and danger, which left a state-changing
   administrative action with only "looks harmless" or "looks like deletion" to
   choose between.

   TINTED, NOT FILLED, and that is the point. .btn-danger is a solid red fill
   because it fronts an irreversible operation. Suspending an event is
   reversible and destroys nothing, so it gets the amber tint, an amber border
   and amber text — clearly a deliberate administrative act, clearly not a
   deletion. Every value is an existing --warn-* token, defined in both themes.

   IN portal.css RATHER THAN BESIDE .btn-danger IN site.css: the console is the
   only consumer, and site.css is shared with the public landing page. If a
   second surface ever wants this tone, moving it up is a two-line change. */
.btn-warn {
  --btn-bg: var(--warn-100);
  --btn-fg: var(--warn-700);
  --btn-border: var(--warn-600);
}

.btn-warn:hover:not(:disabled) {
  --btn-bg: var(--warn-600);
  --btn-fg: #fff;
  transform: translateY(-2px);
}

/* ── The availability dot ────────────────────────────────────────────────────
   ONE LINE PER ROW, FOUR POSSIBLE WORDS. The status cell showed a badge for the
   lifecycle status and, when suspended, a second line beneath it — two facts
   stacked, which is honest but reads as clutter down a column. A single dot and
   word says the one thing an administrator scans this column for: can anybody
   see this event, and if not, why.

   Active, Suspended, Draft, Cancelled. No new state is introduced: Suspended is
   the SuspendedUtc stamp and the other three are EventStatus, resolved to
   whichever answer applies first.

   COLOUR IS NEVER THE ONLY SIGNAL. The word is always there — the dot is a
   scanning aid for people who can use it, not the information itself. */
.sa-state {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  white-space: nowrap;
  font-weight: 600;
}

.sa-state::before {
  content: "";
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--ink-400);
}

.sa-state-active::before    { background: var(--ok-600); }
.sa-state-suspended::before { background: var(--warn-600); }
.sa-state-cancelled::before { background: var(--danger-600); }

/* Cancelled and suspended read muted: neither is a live event. */
.sa-state-suspended,
.sa-state-cancelled { color: var(--ink-700); }
/* ── The actions column ──────────────────────────────────────────────────────
   IT MUST STAY A TABLE CELL, and that is the whole of the bug this replaces.

   This rule set display: flex on the <td> — to stack two buttons back when
   Delete was still here. A <td> with display: flex STOPS BEING A TABLE CELL:
   it leaves the row's cell layout, so it no longer shares the row's height and
   its border-bottom draws on its own box at its own height. Measured at 1280px:
   the five data cells ran top 109 → bottom 201, the actions cell top 110 →
   bottom 179. That 22px difference is the "broken" row separator — a divider
   that stops short of the right-hand column and picks up again at the wrong
   height beside the button.

   Alignment inside a cell does not need flex. text-align works on a table cell,
   and .table td already sets vertical-align: middle, so the button centres in
   the row's full height with no extra rule. If a second action is ever added,
   the pattern is an inner <div class="vol-row-actions"> — which is exactly how
   the volunteer rosters do it, cell intact.

   A STABLE WIDTH, NOT A MINIMUM. width: 1% asked auto-layout for the narrowest
   the content allows, so the column resized with its own button — "Suspend
   event" and "Reactivate event" are different lengths, and a column that moves
   between rows is what made it look unaligned. An explicit width holds it
   still, and min-width keeps it there when the other columns are hungry. */
.sa-events-actions {
  text-align: right;
  white-space: nowrap;   /* the label never wraps inside the column */
}

.sa-events-actions-head {
  width: 190px;
  min-width: 190px;
  white-space: nowrap;
}

@media (max-width: 560px) {
  /* Stacked by .pt-table-stack, the cell is a full-width block with its label
     above it, so the fixed width is meaningless and right alignment would put
     the button opposite its own heading. Left, and free to wrap. */
  .sa-events-actions {
    text-align: left;
    white-space: normal;
  }
}

/* The date on one line. "24 Aug 2026" broke after the month in a column sized to
   its "STARTS" heading, so every dated row was a line taller than it needed to
   be. Released on a phone, where .pt-table-stack gives the cell the full width. */
.table.pt-table-stack td[data-label="Starts"] { white-space: nowrap; }

@media (max-width: 560px) {
  .table.pt-table-stack td[data-label="Starts"] { white-space: normal; }
}

@media (max-width: 560px) {
  .sa-events-actions { align-items: flex-start; }
}
/* ------------------------------------------------------- venue map picker ---- */

/* Google Maps draws its own controls and marker inside the frame. Everything here is
   the surrounding chrome — the search row and the frame itself — written in the design
   system's own tokens so the picker looks like part of the portal rather than like a
   widget dropped into it. */

.pt-mapsearch {
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
  margin-bottom: var(--s-2);
}

.pt-mapsearch .input { flex: 1 1 260px; min-width: 0; }

.pt-map {
  height: 340px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;

  /* The map stacks its own controls; without this they sit above the sticky topbar
     when the page scrolls. */
  z-index: 0;
}

/* ============================================================================
   PLATFORM AGREEMENT — document review
   ============================================================================

   A document-review surface, not a page of cards. The previous design put the
   agreement inside the same card treatment as a dashboard panel, which is why
   it read as a legal text dropped into an application rather than a part of it.

   Everything here uses the existing tokens — --surface-*, --border, --ink-*,
   --brand-* — so both themes are handled by the tokens rather than by any
   inversion. Scoped under .ag- and used only by /portal/agreement. */

/* ── Header ──────────────────────────────────────────────────────────────── */

.ag-head { align-items: flex-start; }
.ag-head-status { flex-shrink: 0; }

/* ── Metadata row ────────────────────────────────────────────────────────── */
/* Short facts, read across. auto-fit rather than a fixed count: the cells vary
   with state — a signed agreement shows a signing date where a pending one
   shows a deadline — and the row has to close up rather than leave a hole. */

.ag-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1px;
  margin: 0 0 var(--s-5);
  padding: 0;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}

.ag-meta-cell {
  background: var(--surface-1);
  padding: var(--s-3) var(--s-4);
}

.ag-meta dt {
  font-size: var(--t-sm);
  color: var(--ink-500);
  margin: 0 0 2px;
}

.ag-meta dd {
  margin: 0;
  font-weight: 650;
  font-size: var(--t-md);
}

.ag-meta-note {
  display: block;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink-500);
}

/* ── Two-column review ───────────────────────────────────────────────────── */

.ag-review {
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
  gap: var(--s-6);
  align-items: start;
}

.ag-aside { position: sticky; top: calc(var(--nav-h) + var(--s-4)); display: grid; gap: var(--s-3); }

/* <details open> rather than script. Desktop reads it as a heading with the
   marker suppressed; a phone gets a real disclosure it can collapse to get the
   document back. Nothing here needs JavaScript. */
.ag-nav {
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-4);
}

.ag-nav > summary {
  font-size: var(--t-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-500);
  cursor: default;
  list-style: none;
}

.ag-nav > summary::-webkit-details-marker { display: none; }

.ag-nav-list {
  list-style: none;
  margin: var(--s-3) 0 0;
  padding: 0;
  display: grid;
  gap: 1px;
}

/* No counter here any more. The heading carries its own number, because the
   administrator typed it into the document — a counter beside it printed the
   number twice, and could disagree with the text the moment a clause was
   renumbered by hand. */
.ag-nav-list a {
  display: block;
  padding: var(--s-2);
  border-radius: var(--r-sm);
  font-size: var(--t-sm);
  line-height: 1.35;
  color: var(--ink-700);
  text-decoration: none;
}

.ag-nav-list a:hover { background: var(--surface-2); color: var(--ink-900); }

.ag-nav-list a:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
}

/* Scroll-spy, added by agreement.js only. Its absence changes nothing. */
.ag-nav-list a[aria-current="true"] {
  background: var(--brand-100);
  color: var(--brand-700);
  font-weight: 650;
}


.ag-aside-actions { flex-wrap: wrap; }

/* ── The document ────────────────────────────────────────────────────────── */

.ag-sheet { display: grid; gap: var(--s-5); }

.ag-sheet-head { border-bottom: 1px solid var(--border); padding-bottom: var(--s-4); }
.ag-sheet-head h2 { margin: var(--s-2) 0 var(--s-1); font-size: var(--t-2xl, 1.5rem); }
.ag-sheet-head p { margin: 0; color: var(--ink-500); }

/* Reading measure. A legal clause set to the full width of a desktop card is a
   line nobody can track back to the start of. */
.ag-body { max-width: 68ch; }

.ag-clause {
  font-size: var(--t-lg);
  font-weight: 650;
  margin: var(--s-6) 0 var(--s-2);

  /* Anchors land below the sticky topbar rather than under it. */
  scroll-margin-top: calc(var(--nav-h) + var(--s-5));
}

.ag-clause:first-child { margin-top: 0; }

.ag-body p {
  margin: 0 0 var(--s-3);
  line-height: 1.7;
  color: var(--ink-700);
}

/* Lists inside the document. Same measure, colour and leading as a paragraph —
   a list is running text that happens to be enumerated, not a different voice.

   Written for the element as well as the class: .ag-list is what the plain-text
   renderer emits, and a bare ul/ol is what the rich-text editor produces. Both
   are the same list on the same page and must not look like two. */
.ag-list,
.ag-body ul,
.ag-body ol {
  margin: 0 0 var(--s-3);
  padding-left: var(--s-5);
  line-height: 1.7;
  color: var(--ink-700);
}

.ag-list li,
.ag-body li { margin: 0 0 var(--s-2); }

.ag-list li:last-child,
.ag-body li:last-child { margin-bottom: 0; }

/* The rest of what the toolbar can produce. Emphasis and underline need no rule
   — the browser's own is correct — so only the shapes with a house style are
   named here. */
.ag-body blockquote {
  margin: 0 0 var(--s-3);
  padding-left: var(--s-4);
  border-left: 3px solid var(--brand-200);
  color: var(--ink-600);
}

.ag-body a { color: var(--brand-600); }

.ag-sheet-foot {
  border-top: 1px solid var(--border);
  padding-top: var(--s-4);
  font-size: var(--t-sm);
  color: var(--ink-500);
}

/* ── The development notice ──────────────────────────────────────────────── */
/* Compact and informational. It states a standing property of the document,
   not an error to be cleared, so it does not borrow the warning treatment. */

.ag-notice {
  display: flex;
  gap: var(--s-3);
  align-items: flex-start;
  padding: var(--s-3) var(--s-4);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--brand-600);
  border-radius: var(--r-md);
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink-700);
}

.ag-notice-icon { color: var(--brand-600); flex-shrink: 0; display: flex; }
.ag-notice-icon .icon { width: 18px; height: 18px; }

/* ── The signing bar ─────────────────────────────────────────────────────── */
/* Sticky at the foot while reading, carrying the tick itself — a bar that only
   pointed at a control further down would be furniture. */

.ag-actions {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin-top: var(--s-5);
  padding: var(--s-4) var(--s-5);
  background: var(--surface-0);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
}

.ag-actions-confirm { margin: 0; flex: 1 1 320px; }

/* ── Responsive ──────────────────────────────────────────────────────────── */

@media (max-width: 1080px) {
  .ag-review { grid-template-columns: 210px minmax(0, 1fr); gap: var(--s-5); }
}

@media (max-width: 820px) {
  /* One column. The nav becomes a disclosure above the document, and unsticks —
     a sticky panel on a short viewport eats the reading area it exists to serve. */
  .ag-review { grid-template-columns: 1fr; }
  .ag-aside { position: static; }
  .ag-nav > summary { cursor: pointer; }
  .ag-nav-list { gap: 2px; }

  /* Two columns of clause links rather than eleven stacked rows. */
  .ag-nav-list { grid-template-columns: 1fr 1fr; }

  .ag-body { max-width: none; }

  /* The bar stops floating: on a phone it would cover the clause being read. */
  .ag-actions { position: static; box-shadow: none; }
}

@media (max-width: 520px) {
  .ag-nav-list { grid-template-columns: 1fr; }
  .ag-meta { grid-template-columns: 1fr 1fr; }
  .ag-actions { align-items: stretch; }
  .ag-actions .btn { width: 100%; }
}

/* Print: the document alone. */
@media print {
  .ag-aside, .ag-actions, .ag-meta { display: none !important; }
  .ag-sheet { border: 0; box-shadow: none; }
  .ag-review { display: block; }
  .ag-body { max-width: none; }
  .ag-clause { break-after: avoid; page-break-after: avoid; }
  .ag-body p { orphans: 3; widows: 3; }
}

/* ── Agreement editor (super admin) ──────────────────────────────────────── */
/* NOTHING HERE SIZES THE EDITOR, deliberately.

   It is the same .rte component the event description field uses — same
   data-editor attribute, same editor.js, same styling in site.css — and that
   component fills whatever container it is given. That is what keeps it inside
   this card at every width, and what stops the two editors drifting apart.

   The rules that were here set a max-width in ch and a min-height in vh. Between
   them that is what pushed the field past the card's edge and made it taller
   than the page. Anything added back here reintroduces one or the other. */

/* The preview is the organisation's own document partial, so it is inset rather
   than restyled — what is shown must be what they will see. */
.ag-preview { background: var(--surface-2); padding: var(--s-4); border-radius: var(--r-lg); }
.ag-preview .ag-sheet { margin: 0; }

.ag-notice-preview { border-left-color: var(--warn-600); }
.ag-notice-preview .ag-notice-icon { color: var(--warn-600); }

/* ── Agreement configuration (super admin) ───────────────────────────────────
   FULL WIDTH, and OUTSIDE the two-column split grid.

   It first sat inside that grid, which gave a document editor half a page:
   every clause body became a ~200px box with its own scrollbar. Making it span
   both columns fixed the width and broke the grid instead — a full-width item
   forces a row break, so Organisation and History were each left alone on a row
   with an empty column beside them.

   The section now lives after the grid closes. The two-column pairing above it
   is intact, and this is simply a block that fills the page. */

/* Every section on this page is now full width and evenly spaced, so this needs
   no special separation of its own -- the shared section margin does it. */
.ag-config { width: 100%; }

/* ── The empty column ────────────────────────────────────────────────────────
   A grid row stretches its items to the height of the tallest by default, so
   History — a short card — was being pulled to the full height of Organisation
   + Address + Administrators beside it, leaving a tall empty box.

   `start` lets each column be its own height. SCOPED TO THIS PAGE: pt-grid-split
   is shared with the portal dashboard, the super-admin dashboard and event
   details, and changing the stretch behaviour for all of them to fix one page
   would be a redesign nobody asked for. */
.ag-details-grid { align-items: start; }

/* ── Clause editor ───────────────────────────────────────────────────────────
   A form, not a word processor. Clauses are plain text by design, so this is
   text inputs — a rich-text control would reintroduce exactly the markup that
   design keeps out. */

.ag-editor {
  list-style: none;
  counter-reset: clause;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-4);
}

.ag-editor-clause {
  counter-increment: clause;
  display: grid;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--r-md);
}

/* The number comes from the counter, so it always matches the position the
   clause will be rendered at rather than one somebody typed. */
.ag-editor-clause::before {
  content: "Clause " counter(clause);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-500);
}

/* Heading and body are the two halves an administrator is actually separating,
   so the heading reads as a heading rather than as another field. */
.ag-editor-heading {
  font-weight: 650;
  font-size: var(--t-md);
}

/* Room to write. The previous height forced an inner scrollbar on almost every
   clause, which is the single worst thing you can do to someone editing prose:
   they cannot see the paragraph they are changing. */
.ag-editor-clause .textarea {
  min-height: 8.5rem;
  line-height: 1.6;
  resize: vertical;
}

/* ── Actions ─────────────────────────────────────────────────────────────────
   The two that change nothing an organisation can see sit together; publishing
   — the only one with an audience — sits apart. */

.ag-actions-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-top: var(--s-5);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}

/* ── Preview ─────────────────────────────────────────────────────────────────
   The organisation's own document partial, inset rather than restyled: what is
   shown here has to be what they will see. */
.ag-preview { background: var(--surface-2); padding: var(--s-5); border-radius: var(--r-lg); }
.ag-preview .ag-sheet { margin: 0; }

.ag-notice-preview { border-left-color: var(--warn-600); }
.ag-notice-preview .ag-notice-icon { color: var(--warn-600); }

@media (max-width: 720px) {
  .ag-editor-clause { padding: var(--s-4); }
  .ag-actions-bar { align-items: stretch; }
  .ag-actions-bar > .btn, .ag-actions-bar .row { width: 100%; }
  .ag-actions-bar .btn { flex: 1 1 auto; }
}

/* ── Compact field list ──────────────────────────────────────────────────────
   For a card holding many short facts inside a narrow column.

   .pt-detail already auto-fits columns at minmax(220px, 1fr) — but this card
   lives in one half of the split grid, roughly 450px wide, and two 220px tracks
   plus the gap need about 460px. It misses by a hair and collapses to a single
   stack, which is why eight short fields were running the height of the page.

   MULTI-COLUMN RATHER THAN GRID, deliberately. A grid fills row by row, so the
   pairs would read Address | Official email, Mobile | Contact person — related
   facts split across the fold. Columns flow top to bottom, so the address and
   the organisation's own contact details stay together on the left and the
   named contact person's details stay together on the right, which is the
   grouping the fields actually have. */

.pt-detail-compact {
  display: block;
  column-count: 2;
  column-gap: var(--s-6);
}

/* A field never splits across the column break — an address whose first line
   sits at the foot of one column and whose suburb starts the next is worse
   than no columns at all. */
.pt-detail-compact > div {
  break-inside: avoid;
  page-break-inside: avoid;
  margin: 0 0 var(--s-4);
}

.pt-detail-compact > div:last-child { margin-bottom: 0; }

/* Long values — an address, an email — wrap rather than widen the column. */
.pt-detail-compact dd { overflow-wrap: anywhere; }

/* One column on a phone. Two 180px columns in a 360px viewport is not a
   layout, it is two very narrow stacks. */
@media (max-width: 700px) {
  .pt-detail-compact { column-count: 1; }
}

/* ------------------------------------------------------------------ charts ---- */

/* Inline SVG bar charts, drawn by _BarChart.cshtml.
   
   NO CHART LIBRARY, and that is the project's constraint rather than a preference:
   "no npm, no CDN, no framework" is what keeps a strict CSP achievable and the
   pages fast on a hall's wifi. The same reasoning already produced the inline SVG
   illustration partials.
   
   Everything below is colour and type only — the geometry is in the partial,
   because a bar's width depends on the data and CSS cannot see it. */

.chart { margin: 0; }

/* Three columns: name, bar, value. A grid rather than flex so every row lines up
   down the same two edges however long a ticket type name is. */
.chart-rows { display: grid; gap: var(--s-2); margin: 0 0 var(--s-3); }

.chart-row {
  display: grid;
  grid-template-columns: minmax(0, 9rem) 1fr auto;
  align-items: center;
  gap: var(--s-3);
}

.chart-row dd { margin: 0; }

/* The unfilled remainder, so a short bar reads as a proportion of something rather
   than as a stub floating in space. */
.chart-track {
  display: block;
  height: 14px;
  border-radius: var(--r-xs);
  background: var(--surface-3);
  overflow: hidden;
}

.chart-bar {
  display: block;
  height: 100%;
  border-radius: var(--r-xs);
  background: var(--brand-500);
}

/* The tier tones the ticket chips already use, so a ticket type is the same colour
   in the table and in the chart beside it. Colour is never the only carrier: every
   bar is labelled and every value is printed as text. */
.chart-bar.tier-chip-1 { background: var(--brand-500); }
.chart-bar.tier-chip-2 { background: var(--gold-600); }
.chart-bar.tier-chip-3 { background: var(--ok-600); }
.chart-bar.tier-chip-4 { background: var(--info-600); }
.chart-bar.tier-chip-5 { background: var(--brand-400); }
.chart-bar.tier-chip-6 { background: var(--warn-600); }

.chart-label {
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-700);
  overflow-wrap: anywhere;
}

.chart-value {
  font-size: var(--t-sm);
  color: var(--ink-500);
  white-space: nowrap;
}

/* On a narrow panel the three columns become two rows: name and value share a line,
   the bar spans beneath them. Squeezing a 9rem label column into a 320px card is what
   turns every ticket type name into one word per line. */
 (max-width: 560px) {
  .chart-row { grid-template-columns: 1fr auto; }
  .chart-meter { grid-column: 1 / -1; order: 2; }
  .chart-value { order: 1; }
}

/* Under prefers-reduced-motion nothing here moves anyway — the chart is static by
   construction, which is one reason a hand-rolled SVG was the cheaper choice. */

/* A ticket type's heading row inside the sales table, and its variants beneath.
   
   The grouping is carrying meaning here, not decoration: the heading owns the
   stock figure and the rows beneath it own the sales, because several variants
   of one type draw on one pool. A flat list of them printed the same remaining
   count against every variant and read as several times the stock. */
.tier-group-row > th { background: var(--surface-2); }
.tier-group-row > td { background: var(--surface-2); font-variant-numeric: tabular-nums; }

/* Indented, with a rule, so the eye can see which heading a row belongs to and
   therefore which count governs it. Padding alone stops reading as containment
   once a row wraps. */
.tier-variant-row > td:first-child {
  padding-inline-start: var(--s-5);
  border-inline-start: 2px solid var(--border-strong);
}

.tier-variant-row .tier-variant-name { font-weight: 600; }

/* ── Finance overview (fin-*) ────────────────────────────────────────────────────────────────
   The money page's own layer: hero numbers, the money journey, the split table and the
   attention list. Colours come from the shared tokens so light and dark both hold. */

.fin-filter-row { display: flex; flex-wrap: wrap; gap: var(--s-4); align-items: end; }
.fin-filter-row .field { margin-bottom: 0; min-width: 150px; flex: 1 1 150px; }
.fin-filter-row .fin-filter-apply { flex: 0 0 auto; min-width: 0; }
.fin-filter-row [data-custom-date][hidden] { display: none; }

.fin-hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: var(--s-4);
  margin-top: var(--s-5);
}

.fin-stat { padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-1); }
.fin-stat-label { font-size: var(--t-sm); font-weight: 600; color: var(--ink-500); }
.fin-num {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.2rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-900);
}
.fin-num.is-ok { color: var(--ok); }
.fin-stat-sub { font-size: var(--t-xs); color: var(--ink-400); }
.fin-stat.is-lead { border-top: 3px solid var(--brand-600); }

.fin-plain { margin: var(--s-4) 0 0; font-size: var(--t-md); color: var(--ink-600); }
.fin-plain b { color: var(--ink-900); font-variant-numeric: tabular-nums; }

/* The journey: four stations and three arrows, folding vertical on narrow screens. */
.fin-journey { display: flex; align-items: stretch; gap: var(--s-3); }
.fin-step {
  flex: 1 1 0;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s-4);
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--surface-0);
}
.fin-step-label { font-size: var(--t-xs); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.fin-step-num { font-size: var(--t-xl); font-weight: 650; font-variant-numeric: tabular-nums; }
.fin-step-sub { font-size: var(--t-xs); color: var(--ink-400); }
.fin-step.is-info  { border-color: color-mix(in srgb, var(--info-600) 35%, transparent); }
.fin-step.is-info .fin-step-label  { color: var(--info-600); }
.fin-step.is-ok    { border-color: color-mix(in srgb, var(--ok-600) 40%, transparent); background: var(--ok-100); }
.fin-step.is-ok .fin-step-label    { color: var(--ok-700); }
.fin-step.is-warn  { border-color: color-mix(in srgb, var(--warn-600) 40%, transparent); background: var(--warn-100); }
.fin-step.is-warn .fin-step-label  { color: var(--warn-700); }
.fin-step.is-quiet .fin-step-label { color: var(--ink-400); }
.fin-step.is-quiet .fin-step-num   { color: var(--ink-400); }
.fin-arrow { align-self: center; color: var(--ink-300); font-size: 1.2rem; flex: 0 0 auto; }

@media (max-width: 720px) {
  .fin-journey { flex-direction: column; }
  .fin-arrow { transform: rotate(90deg); align-self: flex-start; margin-left: var(--s-5); }
}

.fin-note { margin: var(--s-4) 0 0; font-size: var(--t-sm); color: var(--ink-500); }
.fin-note b { color: var(--ink-800); font-variant-numeric: tabular-nums; }
.fin-note.is-warn-text { color: var(--warn-700); }

/* The split: a receipt, not a spreadsheet. */
.fin-breakdown { margin: 0; max-width: 460px; }
.fin-line { display: flex; justify-content: space-between; gap: var(--s-4); padding: var(--s-2) 0; }
.fin-line dt { margin: 0; color: var(--ink-600); }
.fin-line dd { margin: 0; font-variant-numeric: tabular-nums; font-weight: 600; }
.fin-line.is-minus dd { color: var(--ink-500); font-weight: 500; }
.fin-line.is-total { border-top: 2px solid var(--ink-900); margin-top: var(--s-1); padding-top: var(--s-3); }
.fin-line.is-total dt, .fin-line.is-total dd { font-weight: 750; color: var(--ink-900); font-size: var(--t-md); }
.fin-info { cursor: help; color: var(--info-600); font-style: normal; margin-left: 2px; }

/* Attention: green when quiet, a short list when not. */
.fin-allgood { display: flex; align-items: center; gap: var(--s-4); }
.fin-allgood-mark {
  width: 44px; height: 44px; flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: var(--r-full);
  background: var(--ok-100); color: var(--ok-700);
  font-size: 1.3rem; font-weight: 800;
}
.fin-issues { list-style: none; margin: var(--s-3) 0 0; padding: 0; display: grid; gap: var(--s-2); }
.fin-issues li { display: flex; align-items: center; gap: var(--s-3); }
.fin-issues a { margin-left: auto; white-space: nowrap; }

.fin-details summary { cursor: pointer; color: var(--ink-500); font-size: var(--t-sm); font-weight: 600; }
.fin-details summary:hover { color: var(--ink-800); }
.fin-num.is-bad { color: var(--danger); }
.fin-neg { color: var(--danger); }

/* ── Volunteer portal ────────────────────────────────────────────────────────
   The one place this feature adds CSS. Everything else it renders — cards,
   badges, stats, tables, the tab strip, the scanner and its verdict panels —
   comes from site.css and the blocks above, unchanged.

   READ ONE-HANDED, AT A DOOR, IN BAD LIGHT. That is what the rules below are
   for: figures large enough to read at arm's length, tap targets that do not
   need aiming, and tables that restack instead of scrolling sideways. */

.vol-event { display: grid; gap: var(--s-4); }
.vol-event-head { align-items: flex-start; gap: var(--s-3); }
.vol-event-badges { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* auto-fit rather than a fixed count: four figures sit in a row on a desktop
   and fall to two on a phone without a breakpoint deciding where. */
.vol-figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: var(--s-4);
}

/* A native <progress>, styled. It carries its own value and maximum, so the
   figure is announced without a duplicate aria-label to keep in step. */
.vol-progress {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 8px;
  border: 0;
  border-radius: 999px;
  background: var(--surface-2);
}

.vol-progress::-webkit-progress-bar { background: var(--surface-2); border-radius: 999px; }
.vol-progress::-webkit-progress-value { background: var(--brand-600); border-radius: 999px; }
.vol-progress::-moz-progress-bar { background: var(--brand-600); border-radius: 999px; }

.vol-event-actions { flex-wrap: wrap; }

/* The attendee filter. Search takes the room; the select and the buttons keep
   their own width so the row wraps predictably rather than squeezing. */
.vol-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}

.vol-filter-search { flex: 1 1 16rem; }
.vol-filter-actions { display: flex; gap: var(--s-2); }

@media (max-width: 640px) {
  /* TABLES BECOME CARDS. A four-column attendee list on a phone is a sideways
     scroll, and the page body must never scroll horizontally. Each row becomes
     a block and each cell prints its own heading from data-label — which is why
     every cell in these tables carries one. A cell that forgets it renders with
     no label rather than with the wrong one. */
  .vol-attendees thead { display: none; }

  .vol-attendees, .vol-attendees tbody, .vol-attendees tr, .vol-attendees td {
    display: block;
    width: auto;
  }

  .vol-attendees tr {
    padding: var(--s-3) 0;
    border-bottom: 1px solid var(--border);
  }

  .vol-attendees tr:last-child { border-bottom: 0; }

  .vol-attendees td {
    display: grid;
    grid-template-columns: 8rem 1fr;
    gap: var(--s-3);
    padding: var(--s-1) 0;
    border: 0;
  }

  .vol-attendees td::before {
    content: attr(data-label);
    font-size: var(--t-sm);
    color: var(--ink-500);
  }

  /* Full-width actions: a button a volunteer has to aim at is a button they
     miss with one thumb while holding a phone and a clipboard. */
  .vol-event-actions .btn { flex: 1 1 100%; }
}

/* ── Checkbox rows inside a form field ───────────────────────────────────────
   .checkline is site.css's checkbox row and needs nothing added. This is about
   what follows it.

   .hint and .field-error are <span>s, and margin-top on an INLINE box is ignored
   for layout — so a hint after a block-level label sits flush against it while
   the same hint after an <input> looks correctly spaced. That inconsistency is
   why the checkbox blocks read as cramped once the label stopped being a caption.

   Making them block is the whole fix: their own var(--s-2) margin-top then
   applies, matching the gap under every other field on the form. Scoped to a
   hint that directly follows a checkline, so nothing else on any page moves. */
.field > .checkline + .hint,
.field > .checkline + .hint + .field-error { display: block; }

/* ── Volunteer scanner, on a phone ────────────────────────────────────────────
   A volunteer works a door one-handed, standing up, with a queue in front of
   them. The camera is the primary control, so it leads the page and is sized to
   be the thing a thumb reaches — the pass-code box below it stays as the
   fallback for a camera that will not focus.

   SCOPED TO .vol-scan THROUGHOUT. The door page (/attendance/door) shares
   scan.js, the same ids and the same .scanner-view rules in site.css, and it is
   deliberately untouched: nothing here changes how that page looks or behaves.

   Nothing below is a new component. It resizes what site.css already defines. */

/* A square view rather than site.css's 3/4 portrait, capped against the
   viewport so the count and the Check in button stay on screen without a
   scroll on a short phone. A QR is square; a taller frame buys no accuracy and
   costs the volunteer the rest of the page. */
.vol-scan .scanner-view {
  aspect-ratio: 1 / 1;
  max-height: 60vh;
}

/* The reticle tightens with it — inset 18% of a square is a bigger target than
   18% of a portrait frame, and a guest holds the phone close. */
.vol-scan .scanner-reticle { inset: 12%; }

/* ── "or", between the QR form and the ticket-number form ────────────────────
   A rule either side of the word, drawn with two pseudo-elements in a flex row:
   the lines take whatever width is left, so nothing has to be measured and the
   separator works at 320px and at 1440px without a breakpoint.

   aria-hidden in the markup. Both forms already carry their own labelled button
   — "Check in" and "Verify ticket" — so a screen reader reading a bare "or"
   between them would add a word and no information. */
.scan-or {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin: var(--s-5) 0;
  font-size: var(--t-sm);
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-400);
}

.scan-or::before,
.scan-or::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--border);
}

/* THE COUNT, COMPACT. It is the second thing a volunteer looks at and it must
   not push the scanner off the top of the screen: one line, both figures, on
   every width down to the narrowest phone in use. */
.vol-scan .scan-counts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: baseline;
  justify-content: space-between;
  padding: var(--s-4) var(--s-5);
}

.vol-scan .scan-counts .figure-value { font-size: var(--t-2xl); line-height: 1.1; }

@media (max-width: 520px) {
  /* Tighter still, and the verdict loses its outer padding so the word a
     volunteer reads at arm's length gets the width instead. */
  .vol-scan .scan-counts { padding: var(--s-3) var(--s-4); }
  .vol-scan .scan-verdict { padding: var(--s-5) var(--s-4); }

  /* Thumb-sized, and wide enough to hit without looking. The button already
     carries .btn-lg .btn-block; this is the minimum target size on a phone. */
  .vol-scan #scan-form button[type="submit"] { min-height: 3rem; }
}

/* A phone held in landscape at a door has almost no height to spare, so the
   camera gives most of it back rather than filling the screen on its own. */
@media (max-height: 520px) and (orientation: landscape) {
  .vol-scan .scanner-view { max-height: 44vh; }
}

/* ── SCAN NEXT TICKET, inside the verdict panel ─────────────────────────────
   The verdict is coloured by outcome and the button sits on that colour, so it
   takes the panel's own ink rather than the primary purple: a green panel with a
   purple button reads as two unrelated things. Inherits colour, borders in it,
   fills on hover. Thumb-height, full width, clear of the text above it. */
.vol-scan .scan-verdict .scan-next {
  margin-top: var(--s-5);
  color: inherit;
  background: transparent;
  border: 2px solid currentColor;
  font-weight: 650;
}

.vol-scan .scan-verdict .scan-next:hover,
.vol-scan .scan-verdict .scan-next:focus-visible {
  background: rgba(255, 255, 255, 0.35);
}

/* ── QUICK SCAN, on the volunteer dashboard ─────────────────────────────────
   A card that is a link. The whole surface is the target, which is what a
   phone held in one hand at a door needs; the icon and the arrow are
   decorative and the text carries the meaning. Primary colour because it is
   the page's one action; everything else on the dashboard is reading. */
.vol-quickscan {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 84px;
  margin-bottom: var(--s-5);
  /* The same purple→blue sweep .btn-primary paints, over the same fallback,
     and white text in both themes for the same reason it gives: --brand-600
     is a pale lilac in dark mode, and white on lilac fails at arm's length. */
  background-color: #6c2bd9;
  background-image: var(--grad-brand);
  color: #ffffff;
  text-decoration: none;
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}

.vol-quickscan:hover,
.vol-quickscan:focus-visible {
  filter: brightness(1.08);
  color: #ffffff;
  text-decoration: none;
}

.vol-quickscan-icon,
.vol-quickscan-go { display: inline-flex; flex: 0 0 auto; }

.vol-quickscan-icon svg { width: 36px; height: 36px; }
.vol-quickscan-go svg { width: 24px; height: 24px; }

.vol-quickscan-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 auto; min-width: 0; }

.vol-quickscan-title {
  font-size: var(--t-xl);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  line-height: 1.1;
}

.vol-quickscan-sub { font-size: var(--t-sm); opacity: 0.9; }

@media (max-width: 520px) {
  .vol-quickscan { min-height: 96px; }
  .vol-quickscan-icon svg { width: 40px; height: 40px; }
}

/* --------------------------------------------------- two-column form rows ---- */

/* EXACTLY TWO COLUMNS, which .ob-grid's auto-fit cannot promise.
   
   .ob-grid is repeat(auto-fit, minmax(240px, 1fr)) — it fits as many columns as
   the container allows, so a four-field section on a wide card became one row of
   four and the intended pairing (name beside age, mobile beside email) was lost
   the moment somebody used a large monitor.
   
   The pattern is not new: .detail-facts and .review-facts in site.css already do
   this, for the same reason and with the same 560px collapse — their own comments
   say auto-fit was breaking a pair that had to stay a pair.
   
   A MODIFIER, not a replacement: it overrides the column track and nothing else,
   so .ob-grid's column gap, .ob-grid-full's span and .field's row rhythm all
   still apply. */
.ob-grid-pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (max-width: 560px) {
  /* One column, the same breakpoint the other fixed pairs collapse at. */
  .ob-grid-pairs { grid-template-columns: 1fr; }
}

/* Manage beside Remove, wrapping rather than overflowing. The two used to be a
   link and an inline form, which laid out on one line only because the form
   carried display:inline — and on a stacked row at 360px pushed the button past
   the card's edge. A flex row with a gap wraps instead, and keeps the 8px
   separation a mis-tap on Remove depends on. */
/* ------------------------------------------- the roster's row actions ----
   Three controls — Manage, Resend, Remove — on ONE line at desktop width.

   THEY WERE STACKING THREE HIGH, and the cause was measured rather than
   guessed. The buttons need about 338px side by side; auto table layout was
   giving the Actions column 227px, so flex-wrap: wrap put each one on a line of
   its own and every row became 157px tall.

   nowrap on the group is what asks for one line, and width: 1% on the cell is
   what gets it: for an auto-layout table that reads as "no wider than your
   content needs", which makes the column's min-content its real width and
   leaves the flexible text columns to give up the difference. The same
   technique .sa-events-actions uses two hundred lines up.

   Paired with the Contact cell below, because on its own nowrap pushed the
   table 27px past its container and .table-scroll started scrolling — which is
   the very thing that was clipping the volunteer's name off the left edge. */
.vol-row-actions {
  display: flex;

  /* WRAP, ALWAYS. This was nowrap with a media query undoing it below 1200px,
     which is backwards: the group holding one line is a nice-to-have and the
     table keeping its width is not. Held rigid the group does not shrink — it
     overflows its cell and lands on the column beside it, which is how "Manage"
     came to render as "anag" underneath the Portal badge.

     Allowed to wrap it stays inside the Actions column at every width and the
     row grows a line instead. Wrapping in the cell, never widening the table. */
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;

  /* A flex item will not shrink below its content unless told it may, and these
     are the three buttons. Without it the group has a min-content floor of its
     widest button and pushes the column open from the inside. */
  min-width: 0;
}

/* COMPACT, AND ONLY HERE. Three buttons at the ordinary .btn-sm rhythm measure
   307px together; at this padding and type size they measure 258px, which is
   what lets all three sit on one line inside a 279px column at 1440px. Scoped
   to this group so no other .btn-sm in the portal changes.

   Height is untouched — .btn-sm keeps its 36px box and its 44px touch target on
   a phone, because what was taken out is horizontal only. */
.vol-row-actions .btn {
  padding-left: 10px;
  padding-right: 10px;
  font-size: 0.78rem;
  white-space: nowrap;
}

/* ── ICON-ONLY BUTTONS ────────────────────────────────────────────────────────
   A SQUARE, SO THREE OF THEM FIT WHERE ONE WORD USED TO. The three row actions
   carried their labels as text and measured 306px together; as 36px squares
   with an 8px gap they measure 124px, and that difference is the whole reason
   the volunteer table can be a table at 1024px instead of stacking.

   NOTHING IS DISTINGUISHED BY SHAPE ALONE. Every one of these carries both
   aria-label and title in the markup — the first for a screen reader, the second
   for a hover — so the label did not disappear, it moved. Keyboard focus is the
   .btn outline, untouched.

   36px SQUARE, NOT SMALLER. .btn-sm is a 36px box already; this only stops the
   horizontal padding collapsing it into a rectangle around a 16px glyph, and the
   mobile rule below takes it to 44px where a thumb has to find it. */
.btn.icon-only {
  width: 36px;
  min-width: 36px;
  padding-left: 0;
  padding-right: 0;
  flex: 0 0 auto;
}

.btn.icon-only .icon { width: 18px; height: 18px; }

/* The stacked card gives the group the full width, and a thumb is not a mouse. */
@media (max-width: 560px) {
  .btn.icon-only { width: 44px; min-width: 44px; min-height: 44px; }
  .btn.icon-only .icon { width: 20px; height: 20px; }
}

/* ── THE MOBILE CARD, PAIRED ──────────────────────────────────────────────────
   .pt-table-stack gives every cell a line of its own, which for eight columns is
   a card ~850px tall — one volunteer per screen and a lot of scrolling to compare
   two. The four SHORT values are a number or a badge each and waste most of the
   line they are given, so they go two to a row:

     Anjana George
     +91 98765 43210
     anjana@gmail.com
     Event Support, Registration

     AGE        EVENTS
     28         5

     WWC        PORTAL
     ✓ Verified Active

     [eye] [mail] [x]

   ORDER IS SET HERE, NOT IN THE MARKUP. The DOM order is the table's — Volunteer,
   Contact, Age, Skills, Events, WWC, Portal, Actions — because that is the column
   order a desktop reads. Pairing Age with Events without moving Skills out from
   between them is what `order` is for, and it keeps ONE set of cells: no second
   markup, no duplicated data, and the labels each cell already prints stay
   attached to their own value.

   Scoped to .vol-table, so every other stacked table in the portal is untouched. */
@media (max-width: 560px) {
  .vol-table tr {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-2) var(--s-4);
    align-items: start;
  }

  /* The prose cells keep the full width and lead, in reading order. */
  .vol-table td:nth-child(1) { grid-column: 1 / -1; order: 1; }
  .vol-table td:nth-child(2) { grid-column: 1 / -1; order: 2; }
  .vol-table td:nth-child(4) { grid-column: 1 / -1; order: 3; }

  /* The four short ones, in pairs. */
  .vol-table td:nth-child(3) { order: 4; }
  .vol-table td:nth-child(5) { order: 5; }
  .vol-table td:nth-child(6) { order: 6; }
  .vol-table td:nth-child(7) { order: 7; }

  /* Actions last and full width, so three 44px targets sit in one row. */
  .vol-table td:nth-child(8) { grid-column: 1 / -1; order: 8; padding-top: var(--s-2); }

  /* The grid supplies the rhythm now; the stacked block's own bottom padding
     would double it. */
  .vol-table td { padding-bottom: 0; }

  /* THE SAME BADGE RULE THE TABLE GETS, because the half-column is narrower than
     the cell it replaced. .badge is nowrap by default, so "Invitation pending"
     measured 134px in a 123px grid column and hung 9px into the gutter. It has
     to wrap here for exactly the reason it has to wrap in the table. */
  .vol-table .badge { white-space: normal; overflow-wrap: break-word; }
}

/* ── THE PAGER ────────────────────────────────────────────────────────────────
   The counter and the controls on one line where there is room, stacked where
   there is not — the counter reads as a caption for the table above it, so it
   stays first in both directions.

   Built from .btn like every other control here, so focus rings, hover and the
   disabled state come from site.css rather than from a second opinion. */
.vol-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--border);
}

.vol-pager .hint { margin: 0; }

.vol-pager-controls {
  display: flex;
  align-items: center;
  gap: var(--s-1);
}

.vol-pager-numbers {
  display: flex;
  align-items: center;
  gap: var(--s-1);
}

/* A number is a square too, so the row of them reads as a set rather than as
   buttons of varying width. min-width rather than width: a three-digit page
   number on a long roster grows the box instead of spilling out of it. */
.btn.vol-page {
  min-width: 36px;
  padding-left: var(--s-2);
  padding-right: var(--s-2);
  background: transparent;
  border-color: transparent;
  color: var(--ink-600);
  font-variant-numeric: tabular-nums;
}

.btn.vol-page:hover:not(:disabled) { background: var(--surface-2); color: var(--ink-900); }

/* CURRENT IS FILLED, AND ALSO CARRIES aria-current — the same rule the status
   badges follow: never colour alone. */
.btn.vol-page.is-current {
  background: var(--brand-600);
  border-color: var(--brand-600);
  color: #fff;
  font-weight: 700;
}

.vol-pager-gap { padding: 0 var(--s-1); color: var(--ink-400); }

/* PREVIOUS POINTS BACK. One arrow icon in the set, turned rather than a second
   glyph added for a direction. */
[data-page-prev] .icon { transform: rotate(180deg); }

@media (max-width: 560px) {
  .vol-pager { justify-content: center; }
  .vol-pager .hint { width: 100%; text-align: center; }
}

/* THE ACTIONS CELL CARRIES NO SIZING OF ITS OWN, and both things it used to
   carry were removed for the same reason — a second opinion about one column.

   width: 1% was the auto-layout trick for "no wider than your content needs",
   replaced by the explicit share in the width table below. white-space: nowrap
   was there to hold the buttons on one line, and the group wraps by choice now;
   the cell holds nothing but that group, so the declaration governed nothing.

   The class stays because the markup and the tests name it, and because a
   column with no rule of its own is worth saying out loud. */
.vol-cell-actions { /* sizing lives in the width table below */ }

/* THE EMAIL BREAKS ONLY AS A LAST RESORT.

   This was overflow-wrap: anywhere, chosen to shrink the column's min-content
   so the table would stop overflowing. It worked and it was the wrong trade:
   `anywhere` tells auto table layout that Contact can be made arbitrarily
   narrow, so at 1024px it collapsed the column to 94px and broke
   admin@greenfieldcommunity.example across FIVE lines — measured, along with
   the 218px row that came with it.

   `break-word` breaks a long word only when it cannot fit on a line of its
   own, so a column with room shows the address whole and a cramped one breaks
   it once at the end rather than every twenty characters. What stops the
   column being cramped is the width table below, not this. */
.vol-cell-contact { overflow-wrap: break-word; }

/* --------------------------------------------- the roster's column widths ----
   PROPORTIONS, NOT A NEGOTIATION. Auto table layout distributes by content, and
   with eight columns that produced both failures at once — measured on the real
   page, not guessed:

     1920px (1774 available): every spare pixel went to the text columns. Age
       was 73px wide to hold "34" and Events 103px to hold "3", while the row
       carried three columns of visible slack.

     1024px (878 available): the reverse. Actions is nowrap and immovable at
       338px, so the squeeze fell on everything else — Contact 94px, the email
       five lines tall, the row 218px, and the table still 180px wider than its
       container with the buttons scrolled off the right.

   table-layout: fixed ends the argument: the widths below are honoured at every
   size, so no column bloats and none collapses. The shares are derived from what
   each column actually has to hold:

     Contact 24%  — 272px, the widest thing on the row. The measured natural
                    width of admin@greenfieldcommunity.example is 239px, so at
                    this share the address sits on ONE line with room to spare,
                    and break-word above never has to fire.
     Actions 29%  — 329px. The three buttons measure 77 + 137 + 77 plus two 8px
                    gaps = 306px, and the cell's padding takes the rest. Below
                    this they wrap and the row grows a second line of controls.
     Volunteer 12% and Skills 9% — the two that are meant to wrap. A long name
                    over two lines is the intended behaviour, not a fault.
     Age 4.5%, Events 5% — a number and nothing else.
     WWC 7.5%, Portal 9% — a badge each. Narrow enough that the longest wording
                    ("Invitation pending") takes two lines, which is the right
                    trade for a status: it costs nothing and buys the width
                    Contact and Actions need.

   They total exactly 100, so the table fills the content area with no gap and
   nothing left over.

   SCOPED ABOVE THE STACKING BREAKPOINT, and that is load-bearing rather than
   tidiness: .vol-table td:nth-child(n) outranks .pt-table-stack td on
   specificity, so left unscoped these percentages would survive into the
   stacked layout and fight the display: block that makes a phone readable. */
/* ── THIS TABLE STACKS AT 1200px, NOT AT 560px ─────────────────────────────────
   The shared .pt-table-stack breakpoint is 560px, and for a five-column table
   that is right. This one has EIGHT columns and a three-button action group, and
   the console gives it far less width than the viewport suggests: .pt-shell puts
   a 264px sidebar beside it down to 960px, and .pt-content and the card add 128px
   of padding on top.

   What that leaves, measured on the real shell:
     1440px window → 998px of table. Comfortable.
     1280px window → 888px. Tight but whole: email and actions each take two
                     lines, nothing is clipped.
     1024px window → 582px. SIX of the eight headings overflowed their own
                     columns and the buttons went to three lines.
      768px window → 638px (the sidebar has collapsed by here). No better.

   Below 1400px the table is the wrong shape for the space, so it becomes the
   stacked cards it already becomes on a phone — the same rows, the same
   data-label headings, no second markup and no duplicated data.

   The declarations are repeated from the 560px block rather than shared because
   CSS has no way to reuse a rule under a second query. They are kept identical
   on purpose: if one changes, both must. */
/* ── THE TABLE IS A TABLE FROM 561px UP ───────────────────────────────────────
   WHAT WAS HERE, AND WHY IT WENT. A @media (min-width: 561px) and
   (max-width: 1399px) block turned .vol-table into stacked blocks for every
   viewport under 1400px — which is most laptops. An organiser on a 1280px
   screen met one tall column per volunteer:

     Volunteer / CONTACT / phone / email / AGE / age / SKILLS / …

   reported, correctly, as "a very tall vertical block". The stacking itself was
   right for a phone and wrong everywhere else.

   THE MEASUREMENT THAT JUSTIFIED IT WAS TRUE AND IS NO LONGER. The table did
   need ~1000px, because it carried EVERY volunteer and three full-width text
   buttons per row (Manage + Resend access + Remove measured 306px of the row on
   their own). Two things changed:

     - the action group is now three icon buttons, ~120px instead of 306px;
     - the list shows five rows at a time, so the table is short enough to read
       rather than a wall that has to be scanned.

   Both were the owner's ruling, and together they are what lets the grid hold
   at 1024px. The widths below now apply from the shared stacking breakpoint up,
   so there is exactly one place the table stops being a table: 560px, where
   .pt-table-stack takes over and every cell prints its own data-label heading.
   Same rows, same markup, no duplicated data. */
@media (min-width: 561px) {
  .vol-table { table-layout: fixed; }

  /* ── A FLOOR, SO THE COLUMNS NEVER COLLIDE ────────────────────────────────
     The percentages below are proportions, and a proportion of too little is
     still too little. Measured: at a 1024px window the console leaves the table
     567px, which across eight columns is 71px each before padding — the WWC
     badge alone overflowed its cell by 19px and the action group fell to two
     rows.

     820px is the width at which every column holds its content, taken from the
     1280px case that measures clean. Below it the table keeps that width and
     .table-scroll — the wrapper this markup has always had — scrolls it inside
     the card. The PAGE never scrolls sideways; one region does, and no column,
     badge or action is lost to get there.

     This is the tablet answer, not the phone's: at 560px the table stops being
     a table and every row becomes the stacked card below. */
  .vol-table { min-width: 820px; }

  /* ── AND THE SCROLLER HAS TO BE THE CONTAINING BLOCK ──────────────────────
     .table-scroll is overflow-x: auto and nothing else. The Actions heading is
     a .visually-hidden span — position: absolute — so it resolves against the
     nearest POSITIONED ancestor, and there was none: it escaped the scroller
     entirely and sat at x=1041 in a 1009px viewport, taking the whole page
     33px sideways while the table itself was being clipped correctly.

     position: relative makes this wrapper that ancestor, so the span is clipped
     with everything else. No offsets are set, so nothing moves.

     Scoped to this table rather than to .table-scroll itself: every wide table
     with a hidden heading has the same latent leak, but .table-scroll is shared
     with screens this change has not looked at. */
  [data-volunteer-table] { position: relative; }

  /* THE SHARES ARE OF ~890-1000px, NOT OF THE VIEWPORT — which is the mistake
     the previous set was built on. The console is .pt-shell, a grid whose first
     column is a 264px sidebar, and .pt-content adds 40px of padding each side on
     top of the card's own 24px. At a 1440px window the table therefore has 998px
     to work with, and at 1280px about 888px — not the 1134px a full-width
     measurement suggests.

     Tuned against that real figure, every number below is a measured minimum:
       Volunteer 14%  — 140px at 998, so "David Mathew" sits on one line.
       Contact   23%  — 229px. The email's natural width is 239px, so it wraps
                        once at this size and not at all on a wider window. It is
                        the widest thing on the row and gets the largest share
                        after the actions.
       Age        5%, Events 5% — a number and its heading.
       WWC        7%  — 70px, enough for "Not verified" over two lines.
       Portal     9%  — 90px, enough for "Invitation pending" over two lines.
       Skills     9%  — 90px. Wraps to two or three lines, which is what a list
                        of skills is expected to do.
       Actions   28%  — 279px, against a compact button group measuring 258px.
                        The one column that must not be short: a button that does
                        not fit is a button that cannot be pressed. */
  /* HALF THE HORIZONTAL PADDING, AND IT IS THE SINGLE BIGGEST SAVING HERE.
     .table cells carry 16px each side. Across eight columns that is 256px of
     the 998px available — a quarter of the table spent on gaps between columns
     that already have gaps. Measured content minimums came to 1133px against
     998px available, and 128px of the 135px shortfall was this.

     Horizontal only: the vertical rhythm is what makes rows scannable and is
     left exactly as it was. */
  .vol-table th,
  .vol-table td {
    padding-left: 8px;
    padding-right: 8px;
  }

  .vol-table th:first-child,
  .vol-table td:first-child { padding-left: 0; }

  .vol-table th:last-child,
  .vol-table td:last-child { padding-right: 0; }

  /* EVERY SHARE CLEARS ITS OWN HEADING AS WELL AS ITS DATA. "EVENTS" is 52px of
     uppercase, letter-spaced type and its column held 40px, so the heading
     overflowed into the one beside it and the strip read "EVENTSWWC" — the
     values underneath were fine. A column is as wide as the WIDER of the two
     things stacked in it, and for the three narrow ones that is the label.

     Measured minimums, content plus the 16px of padding above (8px for the
     first and last, which drop their outer side):
       Volunteer 12%  — 112px usable against a 109px name.
       Contact   26%  — 243px against the email's 239px. One line.
       Age        5%  — 34px against a 32px "AGE".
       Skills   6.5%  — the one column that gives way, and the right one to:
                        a skills list is expected to wrap.
       Events     7%  — 54px against a 52px "EVENTS".
       WWC        7%  — 54px against "verified", its longest word at 52px.
       Portal     8%  — 64px against "Invitation" at 62px.
       Actions 28.5%  — 276px against a 272px button group. One line. */
  /* ── RE-CUT FOR ICON ACTIONS ────────────────────────────────────────────────
     The shares above were measured against a 276px action group of three text
     buttons. That group is now three 36px squares plus two 8px gaps — 124px —
     so Actions drops from 28.5% to 14% and the 14.5% goes where the old set was
     tightest: Volunteer and Contact, the two columns that hold real prose.

     At the narrowest width the grid now has to work at (1024px window → 582px
     of table) that is: Volunteer 116px, Contact 163px, Actions 81px. The action
     group wraps to two rows of squares there rather than overflowing, which is
     what .vol-row-actions' flex-wrap has always been for. */
  .vol-table th:nth-child(1), .vol-table td:nth-child(1) { width: 17%; }
  .vol-table th:nth-child(2), .vol-table td:nth-child(2) { width: 25%; }
  .vol-table th:nth-child(3), .vol-table td:nth-child(3) { width: 6%; }
  .vol-table th:nth-child(4), .vol-table td:nth-child(4) { width: 9%; }
  .vol-table th:nth-child(5), .vol-table td:nth-child(5) { width: 6%; }
  .vol-table th:nth-child(6), .vol-table td:nth-child(6) { width: 9%; }
  .vol-table th:nth-child(7), .vol-table td:nth-child(7) { width: 11%; }
  .vol-table th:nth-child(8), .vol-table td:nth-child(8) { width: 17%; }

  /* ── A BADGE WRAPS HERE RATHER THAN ESCAPING ──────────────────────────────
     .badge is white-space: nowrap everywhere else, and rightly — a status
     broken across two lines mid-word reads badly. In a fixed-layout table it
     means the longest wording simply leaves its column: measured at 1280px,
     "Invitation pending" ran 130px out of a 58px Portal cell and printed
     straight over the action icons beside it.

     Allowed to wrap, it takes two lines inside its own cell and the row grows
     a few pixels. Wrapping in the cell, never over the column beside it — the
     same trade the Contact column already makes with a long address. */
  /* THE PADDING COMES IN WITH IT. Wrapping alone was not enough: a badge's
     min-content width is its longest WORD plus its padding, so "Invitation" at
     12px plus 24px of gutter still measured 15px more than a 74px cell and
     printed over the icons beside it. At 8px each side it fits. Vertical
     padding and the type size are untouched, so the badge reads the same. */
  .vol-table .badge {
    white-space: normal;

    /* break-word, NOT anywhere: a word gives way only when it alone cannot fit,
       so "Invitation" stays whole wherever there is room and breaks rather than
       printing over the icons at the narrowest width this grid holds. */
    overflow-wrap: break-word;

    padding-left: var(--s-2);
    padding-right: var(--s-2);
  }

  /* 32px SQUARES INSIDE THE TABLE, 44px ON THE STACKED CARD. Three at 36px plus
     two 8px gaps is 124px, and the Actions cell offers about 116px at 1280px —
     which is why the group wrapped to a second row and made every row 80px
     taller than it needed to be. At 32px the trio is 112px and sits on one
     line. The phone keeps its 44px, where a thumb has to find it. */
  .vol-table .btn.icon-only { width: 32px; min-width: 32px; }
  .vol-table .btn.icon-only .icon { width: 16px; height: 16px; }

  /* THE ACTION GROUP ON ONE LINE, which is what the 15% above is for: three
     36px squares and two 8px gaps is 124px, and 15% of the narrowest table
     this grid has to hold (1024px window → 567px) is 85px — so at that width
     it wraps to two rows of icons rather than overflowing, and the row grows.
     Centred so a wrapped second row sits under the first rather than ragged. */
  .vol-table .vol-row-actions { justify-content: flex-start; }

  /* HEADINGS CENTRED WITH THEIR OWN DATA, not merely near it. .table th is
     left-aligned for prose columns, which left AGE and EVENTS sitting hard
     against the column before them — at 998px the Events and WWC headings ran
     together into "EVENTSWWC" while their values sat centred underneath.
     Aligning the pair together is what makes the grid readable. */
  .vol-table th:nth-child(3), .vol-table td:nth-child(3),
  .vol-table th:nth-child(5), .vol-table td:nth-child(5),
  .vol-table th:nth-child(6), .vol-table td:nth-child(6),
  .vol-table th:nth-child(7), .vol-table td:nth-child(7) { text-align: center; }

  /* .num right-aligns Events by default, which now fights the centring above. */
  .vol-table td.num, .vol-table th.num { text-align: center; }

  /* THE TWO STATUS BADGES MAY WRAP, and without this the whole width table
     above is undone by them.

     .badge is white-space: nowrap, which is right everywhere else it is used.
     Here it made "Invitation pending" an immovable 132px object in a 116px
     column: it did not wrap and it did not shrink, it simply hung 31px out of
     its cell and over the Manage button beside it. Caught by looking at the
     rendered row — every numeric check passed while the layout was visibly
     broken, because an overflowing inline-flex box changes no column width.

     Allowed to wrap it settles at 84px over two lines, inside the cell with
     room to spare, and the row does not grow: the Contact block beside it is
     already two lines tall.

     Scoped to these two columns rather than relaxed on .badge, which would
     reach every badge in the portal. */
  .vol-table td[data-label="WWC"] .badge,
  .vol-table td[data-label="Portal"] .badge { white-space: normal; }
}

/* NO BREAKPOINT FOR THE ACTIONS ANY MORE. A max-width: 1199px block used to
   undo a nowrap that .vol-row-actions no longer sets — the group wraps at every
   width now, so there is nothing left to undo and the query went with it. */

/* -------------------------------------------- the candidate being rostered ----
   The panel that answers "who am I about to put on this event" — age, WWC,
   portal access and how they stand against the event's minimum age.

   It replaces three warnings crammed into the <option> text, which is where
   those facts used to live because a <select> cannot carry a badge. Tinted and
   inset rather than bordered like a card: it belongs to the select above it, and
   a second card inside the add form would read as a second step.

   No fixed height. The eligibility line is one badge and one sentence, and its
   sentence is the longest thing here — letting the panel size itself is what
   keeps the wrap from becoming a gap at wider widths. */
.vol-candidate {
  margin-top: var(--s-4);
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.vol-candidate-name {
  margin: 0 0 var(--s-3);
  font-weight: 650;
  font-size: var(--t-md);
}

/* The same dt/dd grid as .pt-detail, at the narrower minimum a four-fact panel
   inside a form wants: 220px would give two columns where four fit. */
.pt-detail-tight {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s-4);
}

/* The eligibility cell holds a badge and a sentence, so it wraps where the other
   three do not. Line height comes back to normal because .pt-detail dd is set
   for a single short value. */
.pt-detail-tight dd { font-weight: 600; line-height: 1.5; }

/* ------------------------------------------------ the event setup stepper ----
   .ob-steps drawn as NAVIGATION rather than as a read-only indicator.

   The registration stepper it borrows from is a picture of progress: nothing in
   it is clickable, so its dot and label sit straight in the <li>. The event
   editor's steps are pages an organiser opens, so each pair is wrapped in an
   anchor — and these few rules are the whole difference. Scoped to
   .ob-steps-nav so registration's own stepper is untouched.

   REPLACED A TAB STRIP that carried three inert tabs badged "Soon". Tabs say
   "any order, all equal"; an event has a first thing and a last thing, and the
   strip never showed which of them was done. */
.pt-steps { margin-bottom: var(--s-6); }

.pt-steps-progress {
  margin: 0 0 var(--s-3);
  font-size: var(--t-sm);
  font-weight: 650;
  color: var(--ink-600);
}

/* The list's own bottom margin belongs to .pt-steps now, or the two stack. */
.pt-steps .ob-steps { margin-bottom: 0; }

.ob-steps-nav .ob-step-link {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  color: inherit;
  text-decoration: none;

  /* 44px of vertical target on a row of 28px circles. The label is part of the
     hit area, which is the point of wrapping both in one anchor. */
  min-height: 44px;
}

.ob-steps-nav a.ob-step-link:hover { color: var(--ink-900); }
.ob-steps-nav a.ob-step-link:hover .ob-step-dot { border-color: var(--ink-300); }

.ob-steps-nav .is-done a.ob-step-link:hover .ob-step-dot,
.ob-steps-nav .is-current a.ob-step-link:hover .ob-step-dot {
  border-color: var(--brand-700);
}

/* Not reachable until the event exists. Muted further than an upcoming step and
   given the default cursor, because the label is a <span> here and nothing else
   would say that it does not respond. */
.ob-steps-nav .is-locked { color: var(--ink-300); }
.ob-steps-nav .is-locked .ob-step-dot { color: var(--ink-300); border-color: var(--border); }
.ob-steps-nav .is-locked .ob-step-link { cursor: default; }

/* ------------------------------------------------------ step footer ----
   Back and Continue at the bottom of a step, so moving through the workflow
   does not depend on the strip at the top of the page.

   LINKS, NOT A COMBINED "save and continue" BUTTON. Each step page already
   saves the way it always has, and Ticket types deliberately stays put so
   several can be added in a row — folding movement into those handlers would be
   a behaviour change, not a layout one.

   Built from .ob-actions' shape: the same separator, top margin and flex row. */
.pt-step-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
}

/* Continue sits right whether or not Back is beside it. */
.pt-step-nav-end { margin-left: auto; }

@media (max-width: 560px) {
  /* Full width and stacked, so neither is a small target beside the other. The
     primary action comes FIRST in the visual order on a phone: it is the one
     being reached for, and thumbs start at the bottom. */
  .pt-step-nav { flex-direction: column-reverse; align-items: stretch; }
  .pt-step-nav .btn { width: 100%; }
  .pt-step-nav-end { margin-left: 0; }
}

/* THE LABELS STAY ON A PHONE, and this override is not optional.
   .ob-step-label is display:none below 560px — the onboarding stepper goes
   dots-only there, which is a reasonable choice for four steps somebody is
   walking through in one sitting and reading the heading of anyway.

   It is the wrong choice here. This strip is how an organiser answers "what is
   next" on an event they came back to, and five unlabelled circles answer it
   with nothing. Scoped to .ob-steps-nav, so registration keeps its dots. */
@media (max-width: 560px) {
  .ob-steps-nav .ob-step-label { display: inline; }
}

/* The Soon badge inside a step. .pt-soon carries margin-left:auto, which is right
   in a tab (it pushes the badge to the far edge of a wide tab) and wrong here:
   .ob-step-link is a flex row, so auto would shove the badge away from the label
   it belongs to and stretch the step across the strip. */
.ob-steps-nav .pt-soon { margin-left: 0; }

/* The count beside a step's label (issue #204): how many ticket types this event
   has, how many people are on it, how many seats have gone. It replaced the
   circled step number, so it deliberately does NOT look like one — a pill in the
   flow of the label rather than a numbered token at the head of it, or the two
   would read as a numbering that had simply moved. Hidden when the section is
   empty, in the view, so this never draws a zero. */
.pt-step-count {
  margin-left: var(--s-2);
  padding: 0 var(--s-2);
  border-radius: 999px;
  background: var(--surface-2, rgba(0, 0, 0, .06));
  font-size: var(--t-xs);
  font-weight: 650;
  font-variant-numeric: tabular-nums;
  color: var(--ink-600);
  line-height: 1.6;
}

/* On the step the reader is standing on, the count belongs to the accent, not to
   the muted rest of the strip. */
.ob-step.is-current .pt-step-count { color: var(--ink-900); }

/* The dot is now empty on every step but a finished one, so it needs its own
   size — it used to be held open by the digit inside it. */
.ob-steps-nav .ob-step-dot:empty { min-width: 1.5rem; min-height: 1.5rem; }

/* --------------------------------------------------- the volunteer picker ----
   A searchable list of radios, replacing a <select> whose option text had grown
   into one long unscannable line.

   WHY NOT A COMBO BOX: a native <select> cannot render two lines and cannot be
   searched; a scripted combobox has to reimplement focus management,
   aria-activedescendant, arrow keys, Escape and outside-click, which the venue
   field's own comment already argues against. A radio group gets keyboard
   navigation, labelling and form submission from the browser, and the filtering
   reuses the data-search pattern the attendee list established. */
.vol-picker {
  margin: 0 0 var(--s-5);
  padding: 0;
  border: 0;
}

.vol-picker > legend {
  padding: 0;
  margin-bottom: var(--s-2);
}

/* Scrolls itself rather than the page: fifty volunteers must not push the duty
   field and the button out of reach. Roughly five rows tall, so it is obvious
   there is more without the list dominating the form. */
.vol-picker-list {
  max-height: 20rem;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-0);
}

/* The whole row is the target, which is the point of wrapping the input and the
   text in one <label> — 56px on two lines of text, comfortably past the 44px
   minimum, and it stays a single tap on a phone. */
.vol-picker-option {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}

.vol-picker-option:last-child { border-bottom: 0; }
.vol-picker-option:hover { background: var(--surface-2); }

/* ── The "Selected events" list on the add-a-volunteer dialog ───────────────
   THE SAME SHAPE AS .vol-picker-list ABOVE, AND FOR THE SAME REASON: an
   organisation with thirty events must not push the invitation checkbox and the
   Add button off the bottom of a dialog. It borrows .checkline for each row —
   the wrapping checkbox row the rest of this form already uses — so nothing
   here styles a checkbox or a label, only the box they sit in. */
.vol-event-picker {
  max-height: 14rem;
  overflow-y: auto;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-0);
}

.vol-event-picker .checkline { margin-bottom: var(--s-2); }
.vol-event-picker .checkline:last-child { margin-bottom: 0; }

/* 24px is the WCAG 2.5.8 minimum, and the row is the real target anyway. The
   nudge down aligns it with the first line rather than the block's centre. */
.vol-picker-option input[type="radio"] {
  width: 24px;
  height: 24px;
  margin-top: 2px;
  accent-color: var(--brand-600);
  flex: 0 0 auto;
}

/* THE CHECKED ROW IS MARKED BY MORE THAN THE RADIO. :has is what makes that
   possible without script — and where it is unsupported the radio itself still
   shows the state, so nothing is lost. */
.vol-picker-option:has(input:checked) {
  background: var(--surface-2);
  box-shadow: inset 3px 0 0 var(--brand-600);
}

/* The keyboard's own position, which is not the same as the checked one while
   arrowing through a group. */
.vol-picker-option:has(input:focus-visible) {
  outline: 2px solid var(--brand-600);
  outline-offset: -2px;
}

.vol-picker-text { display: flex; flex-direction: column; min-width: 0; }

.vol-picker-name {
  font-weight: 650;
  color: var(--ink-900);
  overflow-wrap: anywhere;
}

/* The second line: everything that tells two people of the same name apart.
   Wraps rather than truncating — an address cut off mid-domain is the one thing
   that would defeat the purpose of showing it. */
.vol-picker-meta {
  font-size: var(--t-sm);
  color: var(--ink-500);
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  /* Taller rows and a shorter list: a phone shows three at a time and scrolls,
     which reads better than five cramped ones. */
  .vol-picker-list { max-height: 16rem; }
  .vol-picker-option { padding: var(--s-4); }
}

/* ------------------------------------------------------- section labels ----
   The eyebrow above each block of this page: Event summary, Assigned
   volunteers, Assign a volunteer.

   Built from .stat-label's treatment rather than the landing page's .eyebrow,
   which carries a gradient rule and brand colour — decoration this page was
   asked to avoid. These replace the card headings that used to sit INSIDE each
   card, so the page gained no headings; it lost two. */
.pt-section-label {
  margin: 0 0 var(--s-2);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-500);
}

/* ------------------------------------------------------- event summary ----
   WHAT WAS WRONG: four facts in .pt-detail's repeat(auto-fit, minmax(220px,
   1fr)). At this page's width that is three columns, so the fourth fact took a
   row to itself and left two thirds of it empty — the "unfinished" band.

   The name is a heading now rather than a fact, which leaves exactly three and
   fills the row. Nothing here is a new component: the card, the badge and the
   hint are all the portal's own. */
.vol-summary { margin-bottom: var(--s-5); }

.vol-summary-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
  padding-bottom: var(--s-4);
  border-bottom: 1px solid var(--border);
}

/* Dominant, as the brief asks — the largest type on the page after the h1, and
   the first thing read once the eye leaves the stepper. */
.vol-summary-name {
  margin: 0;
  font-size: var(--t-2xl);
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

/* Three facts, so three columns wherever 200px fits three times, and one below
   that. minmax(0, …) rather than auto so a long venue cannot push the column
   past the card. */
.vol-summary-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  gap: var(--s-3);
}

/* One tinted panel per fact, which is the "clearly separated groups" the brief
   asks for. One level of nesting inside the card and no more — the facts
   themselves carry no further boxes. */
.vol-fact {
  min-width: 0;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.vol-fact-label {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0 0 var(--s-2);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-500);
}

/* Sized down from the 24px default: at label scale a full-size glyph reads as
   an illustration rather than as punctuation. */
.vol-fact-label .icon { width: 15px; height: 15px; flex: 0 0 auto; }

.vol-fact-value {
  margin: 0;
  font-weight: 600;
  line-height: 1.45;
  overflow-wrap: anywhere;
}

/* "to", between the two ends of a range. Muted because it is joinery, not
   information — the two times are what is being read. */
.vol-fact-to { color: var(--ink-500); font-weight: 400; }

.vol-fact .hint { margin: var(--s-1) 0 0; }

/* THE ONE FACT THAT CHANGES WHO MAY BE ROSTERED. The other two are context; this
   one is a rule, and the accent edge is what says so. A single-sided border, so
   no radius on that side — a rounded corner against a straight edge reads as a
   rendering fault. */
.vol-fact.is-requirement {
  border-left: 3px solid var(--brand-600);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/* The overview cards sat 24px below the summary and 24px above the roster,
   which with a section label between them was more air than the page needed. */
/* ── The volunteer an address already belongs to ─────────────────────────────
   Shown by the event's add panel when the service refuses to create a second
   record. A sunken block rather than a nested card: it is evidence inside the
   form the coordinator is already in, not a second thing on the page.

   THE SAME FOUR FACTS THE ROSTER TABLE PRINTS, laid out as a definition list
   because that is what they are — a label and a value. No table: four pairs in
   a row is a table with one row, which stacks worse and reads no better. */
.vol-existing {
  margin: var(--s-4) 0;
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-2);
}

.vol-existing-name {
  margin: 0 0 var(--s-3);
  font-family: var(--font-display);
  font-size: var(--t-lg);
  font-weight: 600;
  color: var(--ink-900);
}

/* auto-fit rather than a fixed count, so this needs no breakpoint of its own:
   four across on a desktop card, two on a tablet, one on a phone. */
.vol-existing-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s-3) var(--s-4);
  margin: 0;
}

.vol-existing-facts dt {
  font-size: 0.66rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--ink-500);
  margin-bottom: var(--s-1);
}

/* overflow-wrap: break-word, NOT anywhere. An address is the widest value here
   and must be able to break, but "anywhere" breaks it character by character
   even when there is room — which is the fault this product already fixed once
   on the volunteer table. */
.vol-existing-facts dd {
  margin: 0;
  font-size: var(--t-sm);
  color: var(--ink-800);
  overflow-wrap: break-word;
}

.vol-existing-state {
  margin: 0 0 var(--s-4);
  font-weight: 600;
  color: var(--ok-700);
}

.vol-overview { margin-bottom: var(--s-5); }

/* THE STEPPER WRAPS AT EVERY WIDTH, not only on a phone — and this fixes a
   scrollbar visible on a 1366px desktop.

   .ob-steps is overflow-x: auto, and six labelled steps plus five connectors
   need more than the portal's content column once the sidebar has taken its
   270px. So the strip scrolled sideways and clipped "Schedule" at the right
   edge: a horizontal scrollbar under the navigation of a page whose whole brief
   was "this looks unfinished".

   The connectors go with it. A 28px rule between two items on different lines
   points at nothing, and they were only ever decoration — the numbers carry the
   sequence. */
.ob-steps-nav {
  flex-wrap: wrap;
  overflow-x: visible;
  gap: var(--s-2) var(--s-4);
}

.ob-steps-nav .ob-step + .ob-step::before { display: none; }

/* ---------------------------------------------- SmartCreate (Part 20) ----
   The AI event assistant's panel, above the create form. Every colour is a
   site.css token so both themes come for free; the one accent is the gold
   badge, because ✨ suggestions already speak that colour. The panel ships
   hidden and smart-create.js wakes it — see the partial for why. */
.sc-panel { margin-bottom: var(--s-5); }

.sc-head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.sc-title { margin: 0; font-size: var(--t-lg); }

.sc-badge {
  background: var(--gold-100);
  color: var(--gold-700);
}

.sc-tagline {
  margin: var(--s-1) 0 var(--s-4);
  color: var(--ink-500);
}

/* The conversation. Scrolls itself rather than the page, like .vol-picker-list:
   a long chat must not push the form out of reach — the form is the point. */
.sc-feed {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  max-height: 20rem;
  overflow-y: auto;
}

.sc-feed:not(:empty) { margin-bottom: var(--s-4); }

.sc-msg {
  margin: 0;
  display: grid;
  gap: var(--s-1);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-2);
  max-width: 60ch;
}

/* The assistant's turns carry the brand edge; the organiser's stay neutral. */
.sc-msg-assistant { border-left: 3px solid var(--brand-600); }

.sc-who {
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-500);
}

.sc-composer .textarea { resize: vertical; }

/* The marks a filled field wears, appended inside its label so they read as part
   of the field's name line. Green for "your words, applied"; gold for the
   assistant's own suggestion — the ✨ colour the panel badge already speaks.
   Removed the moment the organiser edits the field: an edited value is theirs. */
.sc-chip {
  margin-left: var(--s-2);
  padding: 1px var(--s-2);
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: none;
  white-space: nowrap;
}

.sc-chip-filled {
  background: var(--ok-100);
  color: var(--ok-700);
}

.sc-chip-suggested {
  background: var(--gold-100);
  color: var(--gold-700);
}

/* Readiness — application facts in three human states, no percentages. Tinted
   and inset like .vol-pending: it belongs to the conversation above it, and the
   left edge is the state at a glance — green ready, gold nearly, brand when
   several details remain, danger while something needs fixing first. */
.sc-ready {
  margin: var(--s-2) 0 var(--s-4);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border);
  border-left: 3px solid var(--brand-600);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.sc-ready.is-ok { border-left-color: var(--ok-600); }
.sc-ready.is-warn { border-left-color: var(--gold-500); }
.sc-ready.is-fix { border-left-color: var(--danger-600); }

.sc-ready-headline {
  margin: 0;
  font-weight: 650;
}

.sc-ready .hint { margin: var(--s-1) 0 0; }

.sc-ready-fix { color: var(--danger-600); }

/* ---------------------------------------------- SmartCreate change review ----
   The before-and-after card for a proposed modification: the one place a
   natural-language change becomes concrete, so the values carry the weight.
   Tinted and inset like .vol-pending; the brand edge says "awaiting your
   decision" the way the warning edge says "pending" elsewhere. */
.sc-diff {
  margin: var(--s-2) 0 var(--s-4);
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-left: 3px solid var(--brand-600);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.sc-diff-title {
  margin: 0 0 var(--s-3);
  font-weight: 650;
}

.sc-diff-row { margin: 0 0 var(--s-3); }

.sc-diff-label {
  margin: 0 0 var(--s-1);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-500);
}

.sc-diff-values {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2);
  overflow-wrap: anywhere;
}

.sc-diff-from { color: var(--ink-500); text-decoration: line-through; }
.sc-diff-arrow { color: var(--ink-400); }
.sc-diff-to { font-weight: 650; }

.sc-diff-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-4);
}

/* ---------------------------------------------------- Magic Paste (P5) ----
   A small disclosure under the composer, not another page: toggle, textarea,
   one Analyze action. The result reuses the .sc-diff review card. */
.sc-paste { margin-top: var(--s-4); }

.sc-paste-block {
  margin-top: var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.sc-paste-block .textarea { resize: vertical; }

/* The conflict decision: a real label wrapping a real checkbox, so the whole
   phrase is the target and the state is announced. */
.sc-paste-choice {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-2);
  font-size: var(--t-sm);
  font-weight: 600;
  cursor: pointer;
}

.sc-paste-choice input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--brand-600);
}

/* ------------------------------------------ previous-event picker (P7) ----
   Compact rows, not an event-management screen: name, one metadata line,
   whole row is the target (comfortably past 44px on two lines). */
.sc-reuse-list {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  max-height: 20rem;
  overflow-y: auto;
}

.sc-reuse-row {
  display: grid;
  gap: 2px;
  text-align: left;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-0);
  cursor: pointer;
  font: inherit;
  color: inherit;
}

.sc-reuse-row:hover { background: var(--surface-2); }

.sc-reuse-row:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: -2px;
}

.sc-reuse-name {
  font-weight: 650;
  overflow-wrap: anywhere;
}

.sc-reuse-meta {
  font-size: var(--t-sm);
  color: var(--ink-500);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------- voice input (P8) ----
   The microphone is a quiet secondary control beside Send; listening borrows
   the danger tone the way recording indicators everywhere do, with the state
   also carried in the words and aria-pressed — never colour alone. */
.sc-voice { white-space: nowrap; }

.sc-voice.is-listening {
  border-color: var(--danger-600);
  color: var(--danger-600);
}

/* ------------------------------------------------ the live event preview ----
   The EXISTING customer preview page, landed in a frame beside the form. A
   supporting convenience, not a third workflow: closed by default, one
   control, and closing it gives the form its full width back.

   The wrapper is built by script and is invisible layout while closed — the
   form flows exactly as before. Open, the form keeps the wider column (the
   work happens there) and the preview takes the narrower one. */
.sc-layout.is-preview-open {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 38%);
  gap: var(--s-5);
  align-items: start;
}

/* Sticky beside a long form, so scrolling the form keeps the customer view in
   sight without the preview ever covering a control. */
.sc-preview {
  position: sticky;
  top: var(--s-4);
  padding: var(--s-3) var(--s-4) var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

.sc-preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}

.sc-preview-title {
  margin: 0;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-500);
}

/* The frame is the real public page; the quiet border-radius keeps it reading
   as a view of something, not a developer iframe. */
.sc-preview-frame {
  display: block;
  width: 100%;
  height: min(72vh, 680px);
  border: 0;
  border-radius: var(--r-sm, 6px);
  background: var(--surface-0);
}

.sc-preview-note { margin: var(--s-2) 0 0; }

/* The one preview control: always reachable, words not glyph alone. */
.sc-preview-toggle {
  position: fixed;
  right: var(--s-5);
  bottom: var(--s-5);
  z-index: 40;
}

/* On narrow screens the preview is a full-screen sheet on demand — never a
   third scrolling section under the form. */
@media (max-width: 960px) {
  .sc-layout.is-preview-open { display: block; }

  .sc-preview {
    position: fixed;
    inset: 0;
    z-index: 60;
    border: 0;
    border-radius: 0;
    background: var(--surface-1);
    display: flex;
    flex-direction: column;
    padding: var(--s-3) var(--s-4);
  }

  .sc-preview-frame {
    flex: 1;
    height: auto;
    min-height: 0;
  }

  /* The page behind the sheet holds still. */
  body.sc-preview-showing { overflow: hidden; }

  .sc-preview-toggle { right: var(--s-4); bottom: var(--s-4); }
}

.sc-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

/* ══ 20b. THE EVENT IS LIVE ═══════════════════════════════════════════════════════════════
   Where publishing lands. One quiet celebration and the three things anybody wants next —
   see it as an attendee does, keep working on it, or leave. Tokens only. */

.ev-live {
  border-color: var(--accent-line);
  background: linear-gradient(180deg, var(--brand-050), var(--surface) 55%);
  text-align: center;
}

.ev-live-mark { margin: 0 0 var(--s-2); font-size: 2rem; line-height: 1; }

.ev-live-name {
  margin: var(--s-2) 0 var(--s-4);
  font-family: var(--font-display);
  font-size: 1.35rem;
}

.ev-live-facts {
  list-style: none;
  margin: 0 0 var(--s-5);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3) var(--s-5);
  font-size: .92rem;
  color: var(--ink-2);
}

.ev-live-facts li { display: flex; align-items: center; gap: .45em; }
.ev-live-facts .icon { width: 1.05em; height: 1.05em; color: var(--ink-3); }

.ev-live-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
}

.ev-live-actions .btn { min-height: 44px; }
.ev-live-actions .icon { width: 1.05em; height: 1.05em; }

.ev-live-link {
  margin: var(--s-4) 0 0;
  font-size: .82rem;
  color: var(--muted);
  /* A public address can be long and must never widen the page. */
  overflow-wrap: anywhere;
}

@media (max-width: 560px) {
  /* Full-width, stacked, thumb-sized — this is the screen somebody shows a friend. */
  .ev-live-actions { flex-direction: column; }
  .ev-live-actions .btn { justify-content: center; }
  .ev-live-facts { flex-direction: column; align-items: center; gap: var(--s-2); }
}

/* ══ 21. SMARTCREATE WORKSPACE ════════════════════════════════════════════════════════════
   The conversational event assistant (architecture Part 20). A full-screen surface that
   covers the ordinary Add event page: conversation on the left, the event assembling on the
   right, and the form it hands off to waiting underneath.

   EVERY COLOUR, SHADOW, RADIUS AND STEP OF SPACING IS A TOKEN from site.css, so this surface
   is the same product as the rest of the portal in both themes and inherits the dark palette
   without a single override. The only new paint is one soft wash of --brand-050 behind the
   header, which is what makes the surface read as somewhere else without becoming a second
   design system. */

.sw {
  position: fixed;
  inset: 0;
  /* Above the portal's own layers — the sidebar (20), the mobile drawer (60) and the
     SmartCreate preview sheet (60). This surface covers the application, deliberately. */
  z-index: 200;
  background:
    radial-gradient(75rem 26rem at 50% -14rem, var(--brand-050), transparent 72%),
    var(--page);
}

.sw-frame {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  /* minmax(0, 1fr), NOT the default auto column. An auto track refuses to shrink below its
     content's min-content width, so ONE unwrappable thing anywhere inside — a long word, a
     nowrap label in the header — silently widens the whole surface past the phone's screen
     and pushes the composer off the right edge. This is the cap that makes that impossible. */
  grid-template-columns: minmax(0, 1fr);
  /* THE PARENT IS THE MEASUREMENT. .sw is fixed with inset 0, which IS the viewport by
     definition — so a percentage here needs no viewport unit, and there is no vh/dvh/svh
     question to get wrong on a phone with a collapsing address bar. */
  height: 100%;
}

/* ── The bar ─────────────────────────────────────────────────────────────────────────────── */

.sw-top {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-3) var(--s-5);
  border-bottom: 1px solid var(--rule);
  /* Flex items refuse to shrink below min-content unless told they may. Without this the
     header sets a floor on the width of the entire surface. */
  min-width: 0;
}

.sw-leave {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  min-height: 44px;
  padding: 0 var(--s-3) 0 var(--s-2);
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink-2);
  font: inherit;
  font-size: .9rem;
  cursor: pointer;
}

.sw-leave:hover { background: var(--surface-sunk); color: var(--ink); }

/* Shrinks first, and truncates rather than pushing the progress pill off the screen. */
.sw-id { min-width: 0; flex: 0 1 auto; overflow: hidden; }

.sw-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sw-brand {
  margin: 0;
  /* Flex, because site.css draws every svg as a block. An icon beside text in normal flow
     stacks; the whole product puts its icons in flex containers for this reason. */
  display: flex;
  align-items: center;
  gap: .4em;
  min-width: 0;
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: -0.01em;
}

.sw-brand span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sw-sub {
  margin: 0;
  font-size: .78rem;
  color: var(--muted);
}

.sw-pill {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: .55em;
  min-height: 44px;
  padding: 0 var(--s-4);
  border: 1px solid var(--rule);
  border-radius: 99px;
  background: var(--surface);
  color: var(--ink-2);
  font: inherit;
  font-size: .85rem;
  cursor: pointer;
  box-shadow: var(--shadow-xs);
}

.sw-pill:hover { border-color: var(--accent-line); color: var(--ink); }

.sw-pill-count {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent-deep);
  background: var(--accent-soft);
  border-radius: 99px;
  padding: .1em .6em;
}

/* ── The two columns ─────────────────────────────────────────────────────────────────────── */

.sw-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 360px);
  gap: 0;
  min-height: 0;
}

.sw-convo {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  grid-template-columns: minmax(0, 1fr);
  min-height: 0;
  min-width: 0;
}

.sw-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-5) var(--s-5) var(--s-3);
  scroll-behavior: smooth;
  /* MESSAGES SIT AT THE BOTTOM, beside the composer, the way every messaging app arranges a
     short conversation — a first question stranded at the top of a tall empty column is the
     tell-tale of a chat window built as a document. The feed's auto top margin does it, and
     normal scrolling takes over as soon as there is more than a screenful. */
  display: flex;
  flex-direction: column;
}

@media (prefers-reduced-motion: reduce) {
  .sw-scroll { scroll-behavior: auto; }
}

.sw-feed {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  /* TIGHT WITHIN A TURN, looser between them — the rhythm of a messaging app rather than of
     a document. At 12px everywhere only four turns fitted a laptop screen and the whole
     column read as airy; 8px here plus the change-of-speaker margin below gives 8px inside a
     run and 20px where the speaker changes, which is where the eye actually wants the break. */
  gap: var(--s-2);
  /* The conversation is a column of speech, not a page of text: capped so a long reply never
     runs to a 1600px line, and centred so the composer below lines up under it. The auto TOP
     margin is what anchors a short conversation to the bottom of the scroller. */
  max-width: 46rem;
  width: 100%;
  margin: auto auto 0;
}

/* ── Messages: the WhatsApp rhythm, in the portal's own materials ───────────────────────── */

.sw-msg {
  max-width: 88%;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--radius-lg);
  font-size: .95rem;
  line-height: 1.55;
  /* Words arrive already broken to the bubble: a pasted address or a long venue name must
     wrap rather than widen the column. */
  overflow-wrap: anywhere;
}

.sw-msg p { margin: 0; }
.sw-msg p + p { margin-top: var(--s-2); }

.sw-msg-ai {
  justify-self: start;
  background: var(--surface);
  border: 1px solid var(--rule);
  /* The squared corner is the tail — the same trick every messaging app uses to say which
     side spoke, done with a radius instead of an SVG. */
  border-bottom-left-radius: var(--r-sm, 6px);
  box-shadow: var(--shadow-sm);
}

.sw-msg-you {
  justify-self: end;
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-bottom-right-radius: var(--r-sm, 6px);
  color: var(--ink);
}

/* A change of speaker gets air; a continuation does not. */
.sw-msg-you + .sw-msg-ai,
.sw-msg-ai + .sw-msg-you,
.sw-msg-you + .sw-note,
.sw-note + .sw-msg-ai { margin-top: var(--s-3); }

/* The assistant's name, ONCE, at the top of the conversation — an introduction rather than a
   badge on every sentence. */
.sw-who {
  justify-self: start;
  display: flex;
  align-items: center;
  gap: .4em;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--accent-deep);
}

/* WHAT THIS TURN IS DOING, when it is not simply getting on with it.
   An explanation and a recommendation are labelled; an acting turn is not, because the values
   appearing in the summary beside it already say so — and a badge on every reply is noise. */
.sw-mode {
  display: inline-block;
  margin-bottom: var(--s-2);
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: var(--surface-sunk);
  border: 1px solid var(--rule);
  border-radius: 99px;
  padding: .15em .6em;
}

/* A QUESTION, not just a sentence. The one line the organiser is being asked to answer is set
   in the display face — the same voice the public site uses — so it reads as a person asking
   rather than a field label. */
.sw-ask {
  display: block;
  font-family: var(--font-display);
  font-size: 1.2rem;
  line-height: 1.3;
  margin-top: var(--s-2);
  text-wrap: balance;
}

/* Earlier turns, compressed. The conversation stays calm by keeping context to one line. */
.sw-recap {
  justify-self: stretch;
  display: flex;
  align-items: baseline;
  gap: .6em;
  padding: var(--s-2) var(--s-3);
  border-left: 2px solid var(--rule);
  color: var(--muted);
  font-size: .82rem;
}

.sw-recap b { color: var(--ink-3); font-weight: 600; }

/* Something the application is telling the organiser, in its own voice rather than the
   assistant's — a refusal, an outage, or a note about this build. Never styled as a bubble:
   it is not something anybody said. */
.sw-note {
  justify-self: stretch;
  display: flex;
  gap: .6em;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius);
  background: var(--brand-050);
  color: var(--ink-2);
  font-size: .86rem;
}

.sw-note-warn {
  border-color: color-mix(in srgb, var(--warn) 40%, var(--rule));
  background: var(--warn-soft);
  color: var(--ink);
}

/* ── The dock: replies, composer, sources ───────────────────────────────────────────────── */

.sw-dock {
  padding: 0 var(--s-5) var(--s-4);
  /* Room for the home indicator on a phone, nothing anywhere else. */
  padding-bottom: calc(var(--s-4) + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to bottom, transparent, var(--page) 45%);
}

.sw-replies,
.sw-composer,
.sw-tools { max-width: 46rem; margin-inline: auto; }

.sw-replies {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
}

.sw-replies:empty { display: none; }

.sw-reply {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  min-height: 44px;
  padding: 0 var(--s-4);
  border: 1px solid var(--accent-line);
  border-radius: 99px;
  background: var(--surface);
  color: var(--accent-deep);
  font: inherit;
  font-size: .88rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: var(--shadow-xs);
  transition: transform .18s ease, box-shadow .18s ease, background-color .18s ease;
}

.sw-reply:hover {
  background: var(--accent-soft);
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}

.sw-reply:active { transform: none; }

/* A choice that is only an example must not look like a value. Prices and quantities are
   never applied from a suggestion — they are typed — so these carry no weight or colour. */
.sw-reply-eg {
  border-color: var(--rule);
  color: var(--ink-2);
  font-weight: 400;
}

.sw-composer {
  display: flex;
  align-items: flex-end;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-2) var(--s-2) var(--s-4);
  border: 1.5px solid var(--accent-line);
  border-radius: 26px;
  background: var(--surface);
  box-shadow: var(--shadow-md);
}

.sw-composer:focus-within {
  border-color: var(--accent);
  box-shadow: var(--shadow-md), 0 0 0 3px var(--accent-soft);
}

.sw-input {
  flex: 1 1 auto;
  min-width: 0;
  /* 16px is the floor iOS uses to decide whether to zoom the page on focus. Anything smaller
     zooms, and a zoomed page cannot be scrolled back reliably with a keyboard open. */
  font: inherit;
  font-size: 16px;
  line-height: 1.5;
  padding: var(--s-2) 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  resize: none;
  max-height: 8.5rem;
  overflow-y: auto;
}

.sw-input:focus { outline: none; }
.sw-input::placeholder { color: var(--muted); }

.sw-mic,
.sw-send {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: var(--surface);
  color: var(--ink-2);
  font-size: 1.05rem;
  display: grid;
  place-items: center;
  cursor: pointer;
}

.sw-mic:hover { border-color: var(--accent-line); color: var(--accent-deep); }

.sw-send {
  border-color: transparent;
  background: var(--accent);
  color: #fff;
  box-shadow: var(--shadow-brand);
}

.sw-send:disabled {
  background: var(--surface-sunk);
  border-color: var(--rule);
  color: var(--muted);
  box-shadow: none;
  cursor: default;
}

/* Listening. One state, said twice — colour and a slow pulse — because colour alone is not a
   state anybody can rely on. */
.sw-mic[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-deep);
  animation: sw-listen 1.6s ease-in-out infinite;
}

@keyframes sw-listen {
  50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 18%, transparent); }
}

.sw-tools {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-3);
}

.sw-tool {
  display: inline-flex;
  align-items: center;
  gap: .4em;
  min-height: 40px;
  padding: 0 var(--s-3);
  border: 0;
  border-radius: 99px;
  background: transparent;
  color: var(--ink-3);
  font: inherit;
  font-size: .82rem;
  cursor: pointer;
}

.sw-tool:hover { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-xs); }

/* ── The three doorways: paste, poster, a previous event ────────────────────────────────── */

.sw-block {
  max-width: 46rem;
  margin: var(--s-3) auto 0;
  padding: var(--s-4);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  display: grid;
  gap: var(--s-2);
}

.sw-block-label {
  margin: 0;
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink-2);
}

.sw-block-input {
  width: 100%;
  /* 16px, so focusing this on a phone cannot zoom the page. */
  font: inherit;
  font-size: 16px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--page);
  color: var(--ink);
  resize: vertical;
}

.sw-block-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.sw-block-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-3);
}

.sw-block-note { margin: 0; font-size: .76rem; color: var(--muted); }

.sw-previous { display: grid; gap: var(--s-2); }

.sw-previous-row {
  display: grid;
  gap: .15em;
  text-align: left;
  padding: var(--s-3);
  /* A real target on a phone. */
  min-height: 44px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--page);
  font: inherit;
  cursor: pointer;
}

.sw-previous-row:hover { border-color: var(--accent-line); background: var(--accent-soft); }

.sw-previous-name { font-weight: 600; font-size: .92rem; }
.sw-previous-meta { font-size: .78rem; color: var(--ink-3); }

/* ── The review card: ONE card, whatever the details came from ──────────────────────────── */
/* Sits in the conversation rather than beside it, because reviewing what SmartCreate found IS
   part of the conversation — and because a second column for it would not survive a phone. */

.sw-review {
  justify-self: stretch;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  padding: var(--s-4);
  display: grid;
  gap: var(--s-3);
}

.sw-review-head {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.1rem;
}

/* Small on every screen: the organiser needs to recognise what they uploaded, not study it. */
.sw-review-poster {
  width: 100%;
  max-width: 8rem;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
}

.sw-find {
  display: grid;
  gap: .2em;
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}

.sw-find:first-of-type { padding-top: 0; border-top: 0; }

.sw-find-label {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5em;
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The badge sits with its label rather than being pushed to the far edge here: in a narrow
   card the two belong together. */
.sw-find-label .sw-tag { margin-left: 0; text-transform: none; letter-spacing: .02em; }

.sw-find-value { margin: 0; font-size: .95rem; overflow-wrap: anywhere; }

.sw-find-pair {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5em;
  font-size: .95rem;
}

.sw-find-mine { color: var(--ink-3); text-decoration: line-through; }
.sw-find-arrow { color: var(--muted); }
.sw-find-theirs { font-weight: 600; }
.sw-find-why { margin: .15em 0 0; font-size: .78rem; color: var(--muted); }

.sw-find-conflict { border-left: 3px solid color-mix(in srgb, var(--warn) 55%, transparent); padding-left: var(--s-3); }

.sw-find-choose {
  display: flex;
  align-items: center;
  gap: .55em;
  /* A tick is a decision, so it gets a real target. */
  min-height: 44px;
  font-size: .86rem;
  color: var(--ink-2);
  cursor: pointer;
}

.sw-find-choose input { width: 1.1em; height: 1.1em; accent-color: var(--accent); flex: none; }

.sw-review-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule);
}

.sw-status {
  max-width: 46rem;
  margin: var(--s-3) auto 0;
  min-height: 1.4em;
  font-size: .84rem;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: .5em;
}

.sw-status:empty { display: none; }

.sw-status::before {
  content: "";
  width: .5em;
  height: .5em;
  border-radius: 50%;
  background: var(--accent);
  animation: sw-pulse 1.2s ease-in-out infinite;
}

.sw-status.is-quiet::before { animation: none; background: var(--muted); }

@keyframes sw-pulse { 50% { opacity: .2; } }

/* ── The event, assembling ──────────────────────────────────────────────────────────────── */

.sw-side {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  border-left: 1px solid var(--rule);
  background: var(--surface);
  min-height: 0;
}

.sw-side-scroll { overflow-y: auto; padding: var(--s-5) var(--s-4); }

.sw-side-head {
  margin: 0 0 var(--s-3);
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--muted);
}

.sw-card {
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  /* Sunk inside the white summary column rather than page-coloured: a panel within a panel
     reads as contained, which is what the summary is. */
  background: var(--surface-sunk);
  padding: var(--s-4);
}

.sw-empty { margin: 0; font-size: .86rem; color: var(--muted); line-height: 1.6; }

.sw-name {
  margin: 0 0 var(--s-3);
  font-family: var(--font-display);
  font-size: 1.2rem;
  line-height: 1.25;
}

.sw-facts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s-2); }

.sw-fact {
  display: flex;
  align-items: baseline;
  gap: .55em;
  font-size: .9rem;
}

.sw-fact-icon { flex: none; }
.sw-fact-value { min-width: 0; overflow-wrap: anywhere; }

/* A fact lands. 250ms, opacity and a few pixels — enough to notice, not enough to wait for. */
.sw-fact-in { animation: sw-land .25s ease-out; }

@keyframes sw-land {
  from { opacity: 0; transform: translateY(4px); }
}

@media (prefers-reduced-motion: reduce) {
  .sw-fact-in { animation: none; }
  .sw-mic[aria-pressed="true"] { animation: none; }
  .sw-status::before { animation: none; }
}

/* WHERE A VALUE CAME FROM, ALWAYS VISIBLE. The organiser's own words, the assistant's
   suggestion and a venue matched from their saved list are three different kinds of fact, and
   an organiser reviewing an event has to be able to tell them apart at a glance. */
.sw-tag {
  flex: none;
  margin-left: auto;
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .04em;
  border-radius: 99px;
  padding: .15em .5em;
  white-space: nowrap;
}

.sw-tag-suggested { background: var(--accent-soft); color: var(--accent-deep); }
.sw-tag-venue { background: var(--ok-soft); color: var(--ok); }
.sw-tag-missing { background: var(--warn-soft); color: var(--warn); }

.sw-fact-missing .sw-fact-value { color: var(--muted); }

.sw-tickets-block { margin-top: var(--s-5); }
.sw-tickets { display: grid; gap: var(--s-3); }

.sw-ticket {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-sunk);
  padding: var(--s-3) var(--s-4);
}

.sw-ticket-name { margin: 0; font-weight: 700; font-size: .92rem; }

.sw-ticket-meta {
  margin: .2em 0 0;
  font-size: .86rem;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

.sw-ticket-note { margin: .35em 0 0; font-size: .74rem; color: var(--muted); }
.sw-ticket-need { margin: .25em 0 0; font-size: .8rem; color: var(--warn); }
.sw-ticket-incomplete { border-color: color-mix(in srgb, var(--warn) 45%, var(--rule)); }

.sw-ready { margin: 0; }

.sw-meter {
  height: 6px;
  border-radius: 99px;
  background: var(--surface-sunk);
  overflow: hidden;
}

.sw-meter i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: 99px;
  background: linear-gradient(90deg, var(--brand-400), var(--brand-600));
  transition: width .3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .sw-meter i { transition: none; }
}

.sw-ready-line { margin: .5em 0 0; font-size: .8rem; color: var(--ink-3); }

.sw-side-foot {
  padding: var(--s-4);
  border-top: 1px solid var(--rule);
  display: grid;
  gap: var(--s-3);
}

.sw-finish { width: 100%; justify-content: center; }

/* NOT READY MUST NOT LOOK READY. Fading a violet gradient to 55% still reads as a live
   primary button — it keeps its colour and its lift, so it invites the click it is about to
   refuse. Dropping the gradient and the shadow entirely is what makes "there is something
   left to do" visible at a glance, and the readiness line right above it says what. */
.sw-finish:disabled {
  background: var(--surface-sunk);
  border: 1px solid var(--rule);
  color: var(--muted);
  box-shadow: none;
  opacity: 1;
  cursor: default;
}

.sw-foot-note { margin: 0; font-size: .74rem; color: var(--muted); text-align: center; }

/* ── Leaving ─────────────────────────────────────────────────────────────────────────────── */

.sw-exit {
  position: fixed;
  inset: 0;
  z-index: 210;
  display: grid;
  place-items: center;
  padding: var(--s-4);
  background: color-mix(in srgb, var(--ink-900) 55%, transparent);
}

.sw-exit-card {
  width: min(30rem, 100%);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-xl);
  padding: var(--s-5);
}

.sw-exit-title { margin: 0 0 var(--s-2); font-family: var(--font-display); font-size: 1.3rem; }
.sw-exit-copy { margin: 0 0 var(--s-4); color: var(--ink-2); font-size: .92rem; }

.sw-exit-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.sw-exit-actions .btn { flex: 1 1 auto; justify-content: center; }

/* ── The handoff banner, on the form underneath ─────────────────────────────────────────── */

.sw-handoff {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
  padding: var(--s-4);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: var(--brand-050);
  box-shadow: var(--shadow-xs);
}

.sw-handoff-mark { flex: none; font-size: 1.15rem; }
.sw-handoff p { margin: 0; font-size: .92rem; color: var(--ink-2); }
.sw-handoff p + p { margin-top: .3em; }
.sw-handoff strong { color: var(--ink); }

/* ── The design preview, Development only ───────────────────────────────────────────────── */

.sw-demo-flag {
  margin: .2em 0 0;
  align-self: start;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: .25em .8em;
  border-radius: 99px;
  background: var(--warn-soft);
  color: var(--warn);
  border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
  /* Never widens the header, however long the wording gets. */
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── The entry point, on the dashboard and the event list ───────────────────────────────── */

.sw-doors {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: var(--s-4);
  margin-bottom: var(--s-6);
}

.sw-door {
  display: grid;
  gap: var(--s-2);
  align-content: start;
  padding: var(--s-5);
  border: 1px solid var(--rule);
  border-radius: var(--radius-lg);
  background: var(--surface);
  text-decoration: none;
  color: inherit;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}

.sw-door:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--accent-line); }

@media (prefers-reduced-motion: reduce) {
  .sw-door, .sw-reply { transition: none; }
  .sw-door:hover, .sw-reply:hover { transform: none; }
}

/* THE RECOMMENDED PATH, AND IT LOOKS LIKE IT. One glow on the page, spent here. */
.sw-door-ai {
  border-color: var(--accent-line);
  background: linear-gradient(135deg, var(--brand-050), var(--surface) 62%);
  box-shadow: var(--shadow-brand);
}

.sw-door-ai:hover { box-shadow: var(--shadow-brand), var(--shadow-md); }

.sw-door-title { margin: 0; font-family: var(--font-display); font-size: 1.25rem; }
.sw-door-copy { margin: 0; font-size: .89rem; color: var(--ink-2); line-height: 1.55; }

.sw-door-go {
  margin-top: var(--s-2);
  font-size: .86rem;
  font-weight: 700;
  color: var(--accent-deep);
}

.sw-door-manual .sw-door-title { font-size: 1.1rem; }

.sw-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ── The application's icons, at conversation scale ─────────────────────────────────────── */
/* site.css draws the set at 24px on a 2px grid. These are the sizes that keep it beside
   14–16px text without the icon becoming the loudest thing in the row. */

.sw .icon { width: 1.05em; height: 1.05em; flex: 0 0 auto; }
.sw-brand .icon { width: 1.15em; height: 1.15em; color: var(--accent); }
.sw-who .icon { width: 1.1em; height: 1.1em; }
.sw-fact-icon { color: var(--ink-3); position: relative; top: .12em; }
.sw-fact-missing .sw-fact-icon { color: var(--muted); }
.sw-note .icon { position: relative; top: .15em; color: var(--accent-deep); }
.sw-note-warn .icon { color: var(--warn); }

.sw-ticket-name {
  display: flex;
  align-items: center;
  gap: .45em;
}

.sw-ticket-name .icon { color: var(--ink-3); }

/* ── The opening: one question, centred, before there is a conversation ─────────────────── */
/* A wide field with a single bubble in the bottom-left corner is a chat window with nothing
   in it. Until the organiser answers, the first question is the whole screen — and the moment
   they do, this class comes off and it becomes an ordinary conversation. */

/* THE OPENING IS ONE CLUSTER, centred as a group: question, then the choices, then the
   composer. Centring only the question leaves it floating half a screen above the chips that
   answer it — two halves of one thought, pulled apart by the scrolling area they happen to
   live in. Laying the column out as flex for this state alone puts them back together. */
.sw.is-opening .sw-convo {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.sw.is-opening .sw-scroll {
  flex: 0 1 auto;
  overflow: visible;
  padding-bottom: 0;
}

.sw.is-opening .sw-feed { margin-block: 0; }

/* A little air under the question, so the choices read as answers to it rather than as part
   of the same card. */
.sw.is-opening .sw-dock { flex: 0 0 auto; background: none; padding-top: var(--s-4); }

.sw.is-opening .sw-who { justify-self: center; }

.sw.is-opening .sw-msg-ai {
  justify-self: center;
  text-align: center;
  max-width: 34rem;
  /* No tail: this is a title, not a remark. */
  border-bottom-left-radius: var(--radius-lg);
}

.sw.is-opening .sw-ask {
  font-family: var(--font-display);
  font-size: 1.85rem;
  line-height: 1.18;
  margin-top: var(--s-3);
}

.sw.is-opening .sw-note { max-width: 34rem; justify-self: center; }
.sw.is-opening .sw-replies { justify-content: center; }

/* ── The inline panel, dormant on this route ────────────────────────────────────────────── */
/* Hidden, never removed: its script still owns the fill layer, the field chips and the live
   preview, and the preview is assembled around these nodes. See the workspace script. */

.sc-panel.sw-dormant { display: none !important; }

/* ── Phones and small tablets ───────────────────────────────────────────────────────────── */

@media (max-width: 960px) {
  .sw-doors { grid-template-columns: 1fr; }

  .sw-top { padding: var(--s-2) var(--s-3); gap: var(--s-2); }
  .sw-leave { padding: 0 var(--s-2); }
  .sw-leave span:last-child { display: none; }
  .sw-brand { font-size: .95rem; }
  .sw-pill-label { display: none; }

  .sw-body { grid-template-columns: minmax(0, 1fr); }

  .sw-scroll { padding: var(--s-4) var(--s-3) var(--s-2); }
  .sw-dock { padding-inline: var(--s-3); }
  .sw-msg { max-width: 94%; font-size: .94rem; }
  .sw-ask { font-size: 1.1rem; }
  .sw.is-opening .sw-ask { font-size: 1.4rem; }

  /* THE SUMMARY BECOMES A SHEET, not a squeezed column. Closed by default: on a phone the
     conversation is the product and the summary is something you check. */
  .sw-side {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Never taller than the screen it slides over, so its own footer stays reachable. */
    max-height: 82dvh;
    z-index: 205;
    border-left: 0;
    border-top: 1px solid var(--rule);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    box-shadow: var(--shadow-xl);
    transform: translateY(101%);
    /* VISIBILITY, NOT JUST A TRANSFORM. A sheet parked off the bottom edge is still in the tab
       order, so a keyboard reaches a summary and a "Review & continue" button nobody can see —
       visibility takes it out of the tab order and still animates. */
    visibility: hidden;
    transition: transform .25s ease, visibility .25s linear;
  }

  .sw-side[hidden] { display: grid; }

  .sw.is-sheet-open .sw-side { transform: translateY(0); visibility: visible; }

  .sw-side-foot { padding-bottom: calc(var(--s-4) + env(safe-area-inset-bottom, 0px)); }

  @media (prefers-reduced-motion: reduce) {
    .sw-side { transition: none; }
  }

  /* A grab handle, because a sheet that appears from the bottom edge should look draggable
     even where the only way to close it is the button. */
  .sw-side-scroll::before {
    content: "";
    display: block;
    width: 2.5rem;
    height: 4px;
    border-radius: 99px;
    background: var(--rule-strong);
    margin: calc(-1 * var(--s-2)) auto var(--s-4);
  }

  .sw-exit-actions { flex-direction: column; }
}

@media (max-width: 420px) {
  .sw-tool { font-size: .78rem; padding: 0 var(--s-2); }
  .sw-name { font-size: 1.1rem; }

  /* The two values of a conflict stack rather than squeezing onto one line, and the arrow
     would read as nonsense pointing sideways between stacked rows. */
  .sw-find-pair { display: grid; gap: .1em; }
  .sw-find-arrow { display: none; }

  .sw-review-actions .btn { flex: 1 1 auto; justify-content: center; }
}

/* ══ 22. THE PREPARED TICKET PLAN ═════════════════════════════════════════════════════════
   What SmartCreate set aside during the conversation, waiting on the ticket screen for a
   person to press Add. It borrows the same brand tint as the handoff banner and the published
   screen, so "this came from SmartCreate" reads the same everywhere in the journey — and it is
   deliberately quieter than the "Add a ticket type" card below it, because that card is the
   one that actually creates things. Tokens only. */

.tp-card {
  border-color: var(--accent-line);
  background: linear-gradient(180deg, var(--brand-050), var(--surface) 60%);
}

.tp-card .card-title { display: flex; align-items: center; gap: .45em; }
.tp-card .card-title .icon { width: 1.05em; height: 1.05em; color: var(--accent-deep); }

.tp-lead { margin: 0 0 var(--s-4); color: var(--ink-2); font-size: .95rem; }

/* Auto-fit rather than a fixed count: one prepared ticket should not sit in a third of the
   width with two empty columns beside it, and eight should not each be a full-width band. */
.tp-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: var(--s-3);
}

.tp-item {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: var(--shadow-xs);
}

.tp-name {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: .45em;
  font-weight: 600;
  /* A ticket name is the organiser's own words and can be long. It wraps here rather than
     stretching the grid track or being clipped. */
  overflow-wrap: anywhere;
}

.tp-mark { flex: none; font-size: .95em; line-height: 1; }

.tp-meta {
  margin: 0;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* A ticket the organiser never priced. Stated as what it still needs — the same thing the
   conversation would have asked for — and not dressed as a problem, because it isn't one. */
.tp-need { margin: 0; color: var(--warn); font-size: .9rem; }

.tp-note { margin: 0; color: var(--muted); font-size: .82rem; }

.tp-item-actions {
  margin-top: auto;
  padding-top: var(--s-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.tp-actions { margin-top: var(--s-4); }

@media (max-width: 480px) {
  /* Full-width targets on a phone: three buttons wrapping into ragged rows is harder to hit
     than three stacked ones. */
  .tp-item-actions { flex-direction: column; }
  .tp-item-actions .btn { justify-content: center; }
}

/* ── §21 addendum: prepared-ticket controls in the workspace ────────────────────────────────
   A prepared ticket can be finished, corrected or removed where it is shown, so an unpriced
   ticket is a thing to fix rather than a dead end in front of a disabled button. Small, quiet
   controls: the card is a summary, not a form. Tokens only. */

.sw-ticket-actions {
  margin-top: var(--s-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.sw-ticket-btn {
  appearance: none;
  min-height: 32px;
  padding: 0 var(--s-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--accent-deep);
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  cursor: pointer;
}

.sw-ticket-btn:hover { border-color: var(--accent-line); background: var(--brand-050); }
.sw-ticket-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.sw-ticket-btn-quiet { border-color: transparent; background: transparent; color: var(--ink-3); }
.sw-ticket-btn-quiet:hover { border-color: var(--rule); background: var(--surface); color: var(--ink); }

.sw-ticket-btn-primary { border-color: var(--accent); background: var(--accent); color: #fff; }
.sw-ticket-btn-primary:hover { background: var(--accent-deep); border-color: var(--accent-deep); }

.sw-ticket-edit {
  margin-top: var(--s-2);
  display: grid;
  gap: .35em;
}

.sw-ticket-edit-label { font-size: .74rem; color: var(--ink-3); }

.sw-ticket-edit-input {
  width: 100%;
  min-height: 36px;
  padding: 0 var(--s-2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 16px; /* the iOS zoom floor */
  font-variant-numeric: tabular-nums;
}

.sw-ticket-edit-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The whole plan against the whole venue, for as long as it is true. */
.sw-tickets-total {
  margin: var(--s-3) 0 0;
  font-size: .82rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.sw-tickets-total-over {
  color: var(--warn);
  font-weight: 600;
}

/* ── §21b SmartCreate workspace: guided answers ─────────────────────────────────────────────
   Chips that write directly, the widget slot (pickers, ticket card, image preview), the review
   card's rows and the finished state. Everything here is a shape; colours are the tokens. */

/* A chip the organiser has just tapped: filled, for the beat before the value lands. */
.sw-reply.is-picked {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.sw-reply[disabled]:not(.is-picked) { opacity: .45; cursor: default; }

.sw-widget { max-width: 46rem; margin: 0 auto var(--s-3); }
.sw-widget[hidden] { display: none; }

.sw-wbox {
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-md);
  padding: var(--s-4);
  display: grid;
  gap: var(--s-3);
}

.sw-wbox-head {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1rem;
}

.sw-wbox-note { margin: 0; font-size: .8rem; color: var(--ink-3); }

.sw-wbox-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
}

/* Two fields side by side where there is room; stacked on a phone. */
.sw-picker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s-3);
}

.sw-field { display: grid; gap: .35em; }
.sw-field-label { font-size: .78rem; color: var(--ink-3); }

.sw-field-input {
  width: 100%;
  min-height: 42px;
  padding: 0 var(--s-3);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 16px; /* the iOS zoom floor */
  font-variant-numeric: tabular-nums;
}

textarea.sw-field-input { min-height: 7rem; padding: var(--s-2) var(--s-3); resize: vertical; }

.sw-field-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.sw-presets { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.sw-preset { min-height: 36px; font-size: .82rem; }

.sw-thumb {
  width: 100%;
  max-width: 16rem;
  height: auto;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
}

/* The way back to a question, beside the detail it changes. Quiet until hovered. */
.sw-fact-change {
  margin-left: auto;
  padding: .15em .6em;
  border: 1px solid transparent;
  border-radius: 99px;
  background: transparent;
  color: var(--accent-deep);
  font: inherit;
  font-size: .74rem;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}

.sw-fact-change:hover, .sw-fact-change:focus-visible {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

.sw-name-change { margin: -.2em 0 var(--s-2); }

/* The review card's rows: label, value, and the way back. */
.sw-finish-row {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding-top: var(--s-2);
  border-top: 1px solid var(--rule);
}

.sw-finish-label { flex: 0 0 7rem; font-size: .78rem; color: var(--ink-3); }
.sw-finish-value { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.sw-finish-row-missing .sw-finish-value { color: var(--warn); font-weight: 600; }

.sw-finish-sub {
  margin: var(--s-2) 0 0;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.sw-finish-warn { margin: 0; color: var(--warn); font-weight: 600; font-size: .88rem; }

.sw-finish-problems {
  margin: 0;
  padding-left: 1.2em;
  color: var(--warn);
  font-size: .88rem;
  display: grid;
  gap: .25em;
}

/* Done: the addresses that matter, as buttons. */
.sw-done-line { margin: 0; }
.sw-done-links { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-2); }

@media (prefers-reduced-motion: reduce) {
  .sw-reply.is-picked { transition: none; }
}

/* The one door on the manual Add event page: the dashboard's door, alone, above the form. */
.sw-doors-single { grid-template-columns: 1fr; margin-bottom: var(--s-5); }

/* A block (paste / poster / previous) can be closed with its own control, not only by tapping
   the tool that opened it. */
.sw-block { position: relative; }

.sw-block-close {
  position: absolute;
  top: var(--s-2);
  right: var(--s-2);
  width: 32px;
  height: 32px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink-3);
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.sw-block-close:hover, .sw-block-close:focus-visible { color: var(--ink); border-color: var(--rule-strong); }

/* The Edit page's door is a button: same door, same look, no link underline or button chrome. */
button.sw-door { width: 100%; text-align: left; font: inherit; cursor: pointer; }

/* ── §22 Manager dashboard ──────────────────────────────────────────────────────────────────
   The organiser's workspace: greeting and one action, event cards, a quiet attention list and
   performance strip. Tokens only, so light and dark both hold. Compact by intent — a card is
   read in two seconds, not studied. */

.db-hero {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-5);
  margin-bottom: var(--s-6);
}

.db-greeting {
  margin: 0;
  font-size: var(--t-2xl);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.db-summary {
  margin: var(--s-1) 0 0;
  color: var(--ink-600);
  font-size: var(--t-md);
  max-width: 60ch;
}

.db-hero-actions { display: flex; flex-direction: column; align-items: flex-end; gap: var(--s-1); flex: none; }
.db-hero-or { font-size: var(--t-xs); color: var(--ink-500); }

.db-section { margin-bottom: var(--s-6); }

.db-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
}

.db-section-head h2 { margin: 0; font-size: var(--t-xl); }
.db-section-head-tight h2 { font-size: var(--t-lg); }

.db-group {
  margin: var(--s-5) 0 var(--s-3);
  font-family: var(--font-sans);
  font-size: var(--t-sm);
  font-weight: 650;
  color: var(--ink-600);
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.db-group-count {
  font-size: var(--t-xs);
  padding: 0 var(--s-2);
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-600);
}

.db-events {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-2);
}

/* Four to a row on a desk, then three, then two, then one on a phone — always whole cards. */
@media (max-width: 1180px) { .db-events { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 900px) { .db-events { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* The card is one row: a small image (or a tinted tile with the date) on the left, the facts and
   one action on the right. Read in two seconds, not studied. */
.db-ev {
  padding: var(--s-2);
  display: grid;
  grid-template-columns: 48px minmax(0, 1fr);
  gap: var(--s-2);
  align-items: stretch;
}

.db-ev:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }

.db-ev-thumb {
  position: relative;
  display: block;
  width: 48px;
  min-height: 48px;
  border-radius: var(--r-md);
  overflow: hidden;
  background: linear-gradient(135deg, var(--brand-100), var(--surface-2));
  text-decoration: none;
}

.db-ev-neutral .db-ev-thumb { background: linear-gradient(135deg, var(--surface-3), var(--surface-2)); }
.db-ev-danger .db-ev-thumb { background: linear-gradient(135deg, var(--danger-100), var(--surface-2)); }
.db-ev-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

.db-ev-date {
  position: absolute;
  inset: auto var(--s-1) var(--s-1) var(--s-1);
  padding: 2px 0;
  border-radius: var(--r-sm);
  background: var(--surface-0);
  border: 1px solid var(--border);
  text-align: center;
  line-height: 1;
}

.db-ev-date b { display: block; font-family: var(--font-display); font-size: var(--t-sm); font-weight: 600; }
.db-ev-date small { display: block; margin-top: 1px; font-size: 0.5rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-500); }

/* THE WEEKDAY SITS ABOVE THE DAY NUMBER, in the month's own treatment — same size, same
   letter-spacing, same muted ink — so the chip reads as one three-line stack rather than a
   label bolted to a date. It reuses the <small> rule above and only drops the leading margin
   that rule adds, because this one is first rather than under the number.

   NO HEIGHT IS ADDED TO THE CARD. The chip is absolutely positioned inside .db-ev-thumb, which
   stretches to the row; the chip grows from about 29px to 38px inside a box measuring 155px. */
.db-ev-date .db-ev-dow { margin-top: 0; color: var(--ink-600); font-weight: 700; }
.db-ev-date-empty { padding: var(--s-1) 0; color: var(--ink-400); }
.db-ev-date-empty svg { width: 16px; height: 16px; margin: 0 auto; }

.db-ev-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }

/* Status first, then the name on its own line so it is read whole, two lines at most. */
.db-ev-head { display: flex; flex-direction: column-reverse; align-items: flex-start; gap: 2px; }
.db-ev-name { margin: 0; font-size: var(--t-base); line-height: 1.2; min-width: 0; max-width: 100%; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.db-ev-name a { color: inherit; text-decoration: none; }
.db-ev-name a:hover { color: var(--brand-600); }
.db-ev-badges { display: flex; gap: var(--s-1); flex: none; }
.db-ev-badges .badge { padding: 0 var(--s-2); font-size: 0.62rem; }

.db-ev-line {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 1px var(--s-2);
  font-size: 0.72rem;
  color: var(--ink-500);
  min-width: 0;
}

.db-ev-line span { display: inline-flex; align-items: center; gap: 3px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.db-ev-line span:first-child { flex: none; }
.db-ev-line span + span { flex: 1 1 auto; }
.db-ev-line svg { width: 12px; height: 12px; flex: none; }

.db-ev-sales { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-2); align-items: center; margin-top: 1px; }
.db-progress { height: 4px; border-radius: var(--r-full); background: var(--surface-3); overflow: hidden; }
.db-progress i { display: block; height: 100%; border-radius: var(--r-full); background: var(--brand-600); }
.db-progress-hot i { background: var(--gold-500); }
.db-ev-sold { font-size: 0.72rem; color: var(--ink-600); font-variant-numeric: tabular-nums; white-space: nowrap; }
.db-ev-sold b { color: var(--ink-900); }

.db-ev-note { margin: 1px 0 0; font-size: 0.72rem; color: var(--ink-500); }
.db-ev-note-danger { color: var(--danger-600); font-weight: 600; }

.db-ev-foot {
  margin-top: auto;
  padding-top: 2px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}

.db-ev-rev { font-size: 0.7rem; color: var(--ink-500); display: flex; flex-direction: column; line-height: 1.15; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.db-ev-rev b { font-family: var(--font-display); font-size: var(--t-sm); color: var(--ink-900); font-variant-numeric: tabular-nums; }
.db-ev-rev small { font-size: 0.6rem; letter-spacing: 0.04em; text-transform: uppercase; }
.db-ev-foot .btn-sm { min-height: 28px; padding: 0 var(--s-2); font-size: 0.74rem; }

.db-past { margin-top: var(--s-5); }
.db-past > summary { cursor: pointer; font-size: var(--t-sm); font-weight: 650; color: var(--ink-600); margin-bottom: var(--s-3); display: flex; align-items: center; gap: var(--s-2); }
.db-past > .btn { margin-top: var(--s-3); }

/* The first-time manager's first screen. */
.db-first { text-align: center; padding: var(--s-8) var(--s-5); display: grid; justify-items: center; gap: var(--s-2); }
.db-first svg { width: 32px; height: 32px; color: var(--brand-600); }
.db-first h3 { margin: 0; }
.db-first p { margin: 0 0 var(--s-3); color: var(--ink-600); max-width: 46ch; }

/* THE CHARTS. Two, drawn inline as SVG from the read model: no library, one hue for the
   series (the brand), the theme's own ink for text, a recessive grid, the last point marked.
   Colour never carries a meaning on its own; every bar has its number beside it. */
.db-charts { display: grid; grid-template-columns: 1.5fr 1fr; gap: var(--s-3); margin-bottom: var(--s-3); }
.db-chart { padding: var(--s-4) var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); }
.db-chart-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); }
.db-chart-head h3 { margin: 0; font-family: var(--font-sans); font-size: var(--t-sm); font-weight: 650; }
.db-chart-total { font-family: var(--font-display); font-size: var(--t-xl); font-weight: 600; font-variant-numeric: tabular-nums; }
.db-chart-sub { margin: 0; font-size: var(--t-xs); color: var(--ink-500); }
.db-chart svg { width: 100%; height: auto; display: block; overflow: visible; }
.db-chart-axis { display: flex; justify-content: space-between; font-size: 0.66rem; color: var(--ink-400); font-variant-numeric: tabular-nums; }
.db-chart-grid { stroke: var(--border); stroke-width: 1; }
.db-chart-area { fill: var(--brand-600); fill-opacity: 0.18; }
.db-chart-line { fill: none; stroke: var(--brand-600); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.db-chart-dot { fill: var(--brand-600); stroke: var(--surface-0); stroke-width: 2; }
.db-chart-label { fill: var(--ink-400); font-size: 9px; font-family: var(--font-sans); }
.db-hbars { display: flex; flex-direction: column; gap: var(--s-2); }
.db-hbar { display: grid; grid-template-columns: minmax(0, 38%) minmax(0, 1fr) auto; gap: var(--s-2); align-items: center; font-size: var(--t-xs); }
.db-hbar-name { color: var(--ink-700); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-decoration: none; }
.db-hbar-name:hover { color: var(--brand-600); }
.db-hbar-track { height: 8px; border-radius: 4px; background: var(--surface-3); overflow: hidden; }
.db-hbar-track i { display: block; height: 100%; border-radius: 4px; background: var(--brand-600); }
.db-hbar-hot i { background: var(--gold-500); }
.db-hbar-val { color: var(--ink-600); font-variant-numeric: tabular-nums; white-space: nowrap; }
.db-hbar-val b { color: var(--ink-900); }

/* Attention and performance, side by side. */
.db-band { display: grid; grid-template-columns: 1.3fr 1fr; gap: var(--s-3); align-items: start; }

.db-attention { padding: var(--s-4) var(--s-5); }

.db-caught-up {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--ink-600);
}

.db-caught-up svg { width: 22px; height: 22px; color: var(--ok-600); flex: none; }

.db-att-list { list-style: none; margin: 0; padding: 0; }

.db-att {
  display: grid;
  grid-template-columns: 30px 1fr auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3) 0;
  border-top: 1px solid var(--border);
}

.db-att:first-child { border-top: 0; padding-top: var(--s-1); }

.db-att-mark {
  width: 30px;
  height: 30px;
  border-radius: var(--r-sm);
  display: grid;
  place-items: center;
  background: var(--surface-2);
  color: var(--ink-600);
}

.db-att-mark svg { width: 15px; height: 15px; }
.db-att-danger .db-att-mark { background: var(--danger-100); color: var(--danger-600); }
.db-att-warn .db-att-mark { background: var(--warn-100); color: var(--warn-600); }
.db-att-gold .db-att-mark { background: var(--gold-100); color: var(--gold-700); }
.db-att-ok .db-att-mark { background: var(--ok-100); color: var(--ok-600); }

.db-att-text { display: flex; flex-direction: column; min-width: 0; }
.db-att-text strong { font-size: var(--t-sm); }
.db-att-text span { font-size: var(--t-xs); color: var(--ink-500); }

.db-perf { padding: var(--s-4) var(--s-5); display: flex; flex-direction: column; gap: var(--s-3); }

.db-eyebrow {
  margin: 0;
  font-size: var(--t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--ink-500);
}

.db-perf-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: var(--s-2); }
.db-perf-grid div { padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface-2); }
.db-perf-grid span { display: block; font-size: 0.66rem; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 650; color: var(--ink-500); }
.db-perf-grid b { font-family: var(--font-display); font-size: var(--t-xl); font-weight: 600; font-variant-numeric: tabular-nums; }
.db-perf-context { margin: 0; font-size: var(--t-sm); color: var(--ink-600); }

.db-money {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--border);
}

.db-money b { display: block; font-family: var(--font-display); font-size: var(--t-lg); font-variant-numeric: tabular-nums; }

/* PHONE. Greeting, then Create, then the events as full-width cards, then attention, then
   performance — the order a manager on a phone needs, not the desktop squeezed. */
@media (max-width: 720px) {
  .db-hero { flex-direction: column; align-items: stretch; gap: var(--s-4); }
  .db-hero-actions { align-items: stretch; }
  .db-hero-actions .btn { width: 100%; min-height: 48px; }
  .db-hero-or { text-align: center; }
  .db-events { grid-template-columns: 1fr; }
  .db-ev { grid-template-columns: 56px minmax(0, 1fr); }
  .db-ev-line { flex-wrap: wrap; }
  .db-band { grid-template-columns: 1fr; }
  .db-charts { grid-template-columns: 1fr; }
  .db-att { grid-template-columns: 30px 1fr; }
  .db-att .btn { grid-column: 2; justify-self: start; }
}

/* ── §25 The guided event builder ────────────────────────────────────────────────────────────
   The classic form's fields as four questions. Tokens only; .card, .field, .input, .hint and
   .optional-mark are site.css. Script adds the step-by-step behaviour later; this holds up without it. */

.eb { max-width: 820px; display: grid; gap: var(--s-4); }

.eb-progress {
  list-style: none;
  margin: 0 0 var(--s-2);
  padding: 0;
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.eb-progress a {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 34px;
  padding: 0 var(--s-3) 0 var(--s-1);
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  color: var(--ink-600);
  font-size: var(--t-sm);
  font-weight: 600;
  text-decoration: none;
}

.eb-progress a b {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: var(--r-full);
  background: var(--surface-2);
  color: var(--ink-700);
  font-size: var(--t-xs);
}

.eb-progress a:hover { border-color: var(--border-strong); color: var(--ink-900); }
.eb-progress li.is-current a { border-color: var(--brand-600); color: var(--ink-900); }
.eb-progress li.is-current a b { background: var(--brand-600); color: #fff; }
.eb-progress li.is-done a b { background: var(--ok-100); color: var(--ok-600); }

.eb-step { padding: var(--s-6); display: grid; gap: var(--s-4); }
.eb-step .field { margin-bottom: 0; }
.eb-step-head { display: grid; gap: var(--s-1); }
.eb-step-head h2 { margin: 0; font-size: 1.45rem; line-height: 1.2; text-wrap: balance; }
.eb-lead { margin: 0; color: var(--ink-600); }

.eb .input-lg { min-height: 52px; font-size: 1.1rem; }
.eb-venue > .icon { top: 50px; }

/* The quiet tick under a field once it holds something sensible. Script shows it; without
   script it is simply never there. */
.eb-ok {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: var(--s-2);
  font-size: var(--t-xs);
  font-weight: 650;
  color: var(--ok-600);
}
.eb-ok .icon { width: 14px; height: 14px; }

/* One more thing, behind one tap: the end time. */
.eb-more > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 40px;
  padding: 0 var(--s-3);
  border-radius: var(--r-md);
  border: 1px dashed var(--border-strong);
  color: var(--ink-700);
  font-weight: 600;
  cursor: pointer;
}
.eb-more > summary::-webkit-details-marker { display: none; }
.eb-more > summary .icon { width: 16px; height: 16px; }
.eb-more > summary:hover { border-color: var(--brand-600); color: var(--brand-600); }
.eb-more[open] > summary { display: none; }

/* The optional groups: a card each, the summary is the whole header. */
.eb-step-details { padding: 0; background: none; border: 0; box-shadow: none; }
.eb-group { padding: 0; overflow: hidden; }
.eb-group + .eb-group { margin-top: var(--s-3); }

.eb-group > summary {
  list-style: none;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "title mark" "sub mark";
  align-items: center;
  gap: 2px var(--s-3);
  padding: var(--s-4) var(--s-5);
  cursor: pointer;
}
.eb-group > summary::-webkit-details-marker { display: none; }
.eb-group > summary:hover { background: var(--surface-2); }
.eb-group-title { grid-area: title; display: inline-flex; align-items: center; gap: var(--s-2); font-weight: 650; color: var(--ink-900); }
.eb-group-title .icon { width: 18px; height: 18px; color: var(--brand-600); }
.eb-group-sub { grid-area: sub; font-size: var(--t-sm); color: var(--ink-500); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.eb-group-sub code { font-size: var(--t-xs); }
.eb-group > summary > .optional-mark { grid-area: mark; padding: 2px var(--s-2); border-radius: var(--r-full); background: var(--surface-2); }
.eb-group[open] > summary { border-bottom: 1px solid var(--border); }
.eb-group-body { padding: var(--s-5); display: grid; gap: var(--s-4); }
.eb-group-body .field { margin-bottom: 0; }

/* One obvious next step. */
.eb-actions {
  max-width: 820px;
  margin-top: var(--s-5);
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
}
.eb-actions .btn-lg .icon { width: 18px; height: 18px; }
.eb-next { margin-left: auto; }
.eb-fallback { max-width: 820px; margin: var(--s-4) 0 0; }

@media (max-width: 720px) {
  .eb-step { padding: var(--s-5) var(--s-4); }
  .eb-step-head h2 { font-size: 1.3rem; }
  .eb-actions { flex-direction: column; align-items: stretch; }
  .eb-actions .btn-primary { min-height: 52px; order: -1; }
  .eb-next { margin-left: 0; text-align: center; }
  .eb-group > summary { padding: var(--s-4); }
  .eb-group-body { padding: var(--s-4); }
}

/* §25b — what the builder's script adds: one step on stage, the step bar, chips, the suggestion
   and the summary. Offstage keeps the step's size (a map initialised in a display:none box
   renders grey), so a hidden step is moved out of view rather than removed. */
.eb { position: relative; }
.eb-step.is-offstage {
  position: absolute;
  left: -200vw;
  top: 0;
  width: 100%;
  opacity: 0;
  pointer-events: none;
}

.eb-stepbar {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding-top: var(--s-2);
  border-top: 1px solid var(--border);
}
.eb-stepbar-label { margin: 0 auto 0 0; }
.eb-stepbar .btn-ghost + .eb-stepbar-label { margin-left: 0; }
.eb-continue { min-width: 150px; }
.eb-note { margin: 0; color: var(--warn-600); font-weight: 600; }

/* THE EVENT-TYPE CHIPS WERE STYLED HERE, AND THE MODULE THAT BUILT THEM IS GONE.
   event-builder.js turned the Event Type <select> into one chip per option and hid the select
   behind a visually-hidden class. That read well with ten values; issue #289 made it twenty-one, and
   the grid ran to six rows on a phone for a single answer. The field is the native select the
   markup always rendered, matching the Category control beside it -- so there is nothing left
   for these rules to style. Removed rather than left dead: a visually-hidden helper nobody
   applies is the kind of rule that gets reused by accident. */

.eb-suggest {
  margin: calc(-1 * var(--s-2)) 0 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-3);
  font-size: var(--t-sm);
  color: var(--ink-600);
}
.eb-suggest b { color: var(--ink-900); }

.eb-summary { padding: var(--s-5); display: grid; gap: var(--s-3); border-color: var(--ok-600); }
.eb-summary-head { margin: 0; display: flex; align-items: center; gap: var(--s-2); font-weight: 650; color: var(--ink-900); }
.eb-summary-mark { display: grid; place-items: center; width: 24px; height: 24px; border-radius: var(--r-full); background: var(--ok-100); color: var(--ok-600); font-size: var(--t-sm); }
.eb-summary-lines { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--s-2) var(--s-4); }
.eb-summary-lines dt { font-size: var(--t-xs); letter-spacing: 0.06em; text-transform: uppercase; font-weight: 650; color: var(--ink-500); padding-top: 3px; }
.eb-summary-lines dd { margin: 0; display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); min-width: 0; }
.eb-summary-value { font-weight: 600; color: var(--ink-900); overflow-wrap: anywhere; }
.eb-summary-empty { color: var(--ink-500); }
.eb-summary-edit { font-size: var(--t-sm); font-weight: 600; white-space: nowrap; }

@media (max-width: 720px) {
  .eb-stepbar { flex-wrap: wrap; }
  .eb-continue { flex: 1 1 100%; min-height: 52px; order: 3; }
  .eb-summary-lines { grid-template-columns: 1fr; gap: var(--s-1); }
  .eb-summary-lines dt { padding-top: var(--s-2); }
}

/* §25c — the live card beside the builder, and the actions that stay in reach on a phone. */
.eb-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-6); align-items: start; }
.eb-main { min-width: 0; }
.eb-preview { display: none; }

@media (min-width: 1100px) {
  .eb-layout { grid-template-columns: minmax(0, 1fr) 320px; }
  .eb-preview { display: block; position: sticky; top: calc(var(--nav-h, 64px) + var(--s-5)); }
}

.eb-eyebrow {
  margin: 0 0 var(--s-2);
  font-size: var(--t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 650;
  color: var(--ink-500);
}

.eb-preview-card { padding: 0; overflow: hidden; }
.eb-preview-art { position: relative; aspect-ratio: 5 / 2; background: var(--surface-2); color: var(--brand-600); }
.eb-preview-artwork, .eb-preview-artwork .cat-art { position: absolute; inset: 0; width: 100%; height: 100%; }
.eb-preview-artwork.event-banner { height: 100%; padding: 0; border: 0; display: block; }
.eb-preview-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.eb-preview-cat { position: absolute; left: var(--s-3); bottom: var(--s-3); background: var(--surface-0); }
.eb-preview-body { padding: var(--s-4) var(--s-5); display: grid; gap: var(--s-1); }
.eb-preview-org { margin: 0; font-size: var(--t-xs); letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-500); }
.eb-preview-title { margin: 0 0 var(--s-1); font-size: 1.2rem; line-height: 1.25; overflow-wrap: anywhere; }
.eb-preview-line { margin: 0; display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-sm); color: var(--ink-600); }
.eb-preview-line .icon { width: 15px; height: 15px; flex: none; color: var(--ink-500); }
.eb-preview-url { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--ink-500); overflow-wrap: anywhere; }
.eb-preview-url code { font-size: inherit; }
.eb-preview-foot { padding: var(--s-3) var(--s-5) var(--s-4); border-top: 1px solid var(--border); display: grid; gap: var(--s-2); justify-items: start; }
.eb-preview-foot .btn .icon { width: 14px; height: 14px; }

/* ON A PHONE THE NEXT ACTION STAYS IN REACH: the step bar and the final actions stick to the
   bottom of the screen, on the page's own ground so the form scrolls behind them. */
@media (max-width: 720px) {
  .eb-stepbar, .eb-actions {
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin-left: calc(-1 * var(--s-4));
    margin-right: calc(-1 * var(--s-4));
    padding: var(--s-3) var(--s-4) var(--s-4);
    background: var(--surface-0);
    border-top: 1px solid var(--border);
    box-shadow: 0 -10px 24px -16px rgb(0 0 0 / 0.6);
  }
  .eb-actions { margin-left: 0; margin-right: 0; border-radius: var(--r-lg) var(--r-lg) 0 0; }
}

/* §25d — on a phone the builder's own progress is enough; the six-step strip waits for a desk. */
@media (max-width: 720px) {
  .eb-page-steps { display: none; }
}

/* §25e — the SmartCreate mark on a button, and the compact door. */
.btn-ai { position: relative; box-shadow: var(--shadow-brand); }
.btn-ai .icon { color: var(--gold-200); filter: drop-shadow(0 0 4px rgb(255 255 255 / 0.35)); transition: transform var(--dur-fast) var(--ease-out); }
.btn-ai:hover { box-shadow: var(--shadow-brand), var(--shadow-md); }
.btn-ai:hover .icon { transform: rotate(12deg) scale(1.08); }
.sw-doors-compact { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; margin: 0; }
.sw-doors-compact .btn-sm .icon { width: 16px; height: 16px; }
.sw-doors-hint { font-size: var(--t-xs); color: var(--ink-500); text-align: right; max-width: 34ch; }

/* The two ways to create an event, wherever the choice is offered: AI first, then the form. */
.cc-choice { display: flex; gap: var(--s-3); flex-wrap: wrap; align-items: center; }
.cc-hint { margin: var(--s-2) 0 0; font-size: var(--t-xs); color: var(--ink-500); max-width: 52ch; }
.db-hero-actions .cc-hint { text-align: right; }
@media (max-width: 720px) {
  .cc-choice { flex-direction: column; align-items: stretch; }
  .cc-choice .btn { width: 100%; }
  .db-hero-actions .cc-hint { text-align: left; }
  .sw-doors-compact { align-items: flex-start; }
  .sw-doors-hint { text-align: left; }
}

/* ── §23 Events list ─────────────────────────────────────────────────────────────────────────
   The working list: a toolbar (search, status pills, count), a table whose headings sort, and
   a row of icon actions. Tokens only; the sold bar is the dashboard's own .db-progress. */

.ev-list { overflow: hidden; }

.ev-toolbar {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  flex-wrap: wrap;
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--border);
}

.ev-search { flex: 1 1 260px; max-width: 380px; margin: 0; }
.ev-search .input { min-height: 40px; }

.ev-pills { display: flex; gap: var(--s-1); flex-wrap: wrap; }

.ev-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 32px;
  padding: 0 var(--s-3);
  border-radius: var(--r-full);
  border: 1px solid var(--border);
  background: transparent;
  color: var(--ink-600);
  font: inherit;
  font-size: var(--t-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}

.ev-pill .num { font-size: var(--t-xs); padding: 0 6px; border-radius: var(--r-full); background: var(--surface-2); color: var(--ink-600); }
.ev-pill:hover { border-color: var(--border-strong); color: var(--ink-900); }
.ev-pill.is-on { background-image: var(--grad-brand); background-color: var(--brand-600); border-color: transparent; color: #fff; }
.ev-pill.is-on .num { background: rgb(255 255 255 / 0.2); color: inherit; }

.ev-count { margin: 0 0 0 auto; font-size: var(--t-sm); white-space: nowrap; }

/* Headings that sort: the whole heading is the button, the chevrons say "sortable" at rest
   and the active direction is written in aria-sort and drawn from it. */
.ev-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}

.ev-sort .icon { width: 14px; height: 14px; opacity: 0.45; }
.ev-sort:hover .icon, th[aria-sort="ascending"] .ev-sort .icon, th[aria-sort="descending"] .ev-sort .icon { opacity: 1; }
th[aria-sort="ascending"] .ev-sort, th[aria-sort="descending"] .ev-sort { color: var(--brand-600); }
th[aria-sort="descending"] .ev-sort .icon { transform: rotate(180deg); }

.ev-table td { padding: var(--s-3) var(--s-4); }
.ev-table th { white-space: nowrap; }

.ev-cell-event { min-width: 260px; }
.ev-cell-event > * { vertical-align: middle; }

.ev-thumb {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-right: var(--s-3);
  border-radius: var(--r-md);
  overflow: hidden;
  background: linear-gradient(135deg, var(--brand-100), var(--surface-2));
  color: var(--brand-600);
  flex: none;
}

.ev-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ev-thumb .icon { width: 18px; height: 18px; }

.ev-ident { display: inline-flex; flex-direction: column; gap: 2px; min-width: 0; max-width: calc(100% - 60px); }
.ev-name { font-weight: 650; color: var(--ink-900); text-decoration: none; line-height: 1.25; }
.ev-name:hover { color: var(--brand-600); text-decoration: underline; }
.ev-where { display: inline-flex; align-items: center; gap: 4px; font-size: var(--t-sm); color: var(--ink-500); }
.ev-where .icon { width: 13px; height: 13px; flex: none; }

.ev-cell-when { white-space: nowrap; }
.ev-when { display: block; font-variant-numeric: tabular-nums; }
.ev-cell-when .badge { margin-top: 4px; }

.ev-sales { display: grid; gap: 4px; min-width: 130px; max-width: 170px; text-decoration: none; color: inherit; }
.ev-sales-line { font-size: var(--t-sm); color: var(--ink-600); font-variant-numeric: tabular-nums; }
.ev-sales-line b { color: var(--ink-900); }
.ev-sales .db-progress { margin: 0; }
.ev-types { font-size: var(--t-xs); color: var(--ink-500); }
.ev-sales:hover .ev-types { color: var(--brand-600); text-decoration: underline; }

.ev-cell-money { white-space: nowrap; }
.ev-money { display: block; font-family: var(--font-display); font-size: var(--t-md); font-variant-numeric: tabular-nums; }
.ev-cell-money small { display: block; }

.ev-cell-created { white-space: nowrap; color: var(--ink-600); font-variant-numeric: tabular-nums; }

/* The actions: one row of 34px icon buttons, forms inline so a POST looks like its neighbours. */
.ev-cell-actions { width: 1%; white-space: nowrap; }
.ev-actions { display: inline-flex; align-items: center; gap: 2px; }
.ev-actions form { display: inline-flex; margin: 0; }
.ev-act { width: 34px; height: 34px; }
.ev-act .icon { width: 17px; height: 17px; }
.ev-act:hover { background: var(--surface-2); color: var(--brand-600); }
.ev-act-publish:hover { background: var(--ok-100); color: var(--ok-600); }
.ev-act-danger:hover { background: var(--danger-100); color: var(--danger-600); }
.ev-act.is-done { background: var(--ok-100); color: var(--ok-600); }

.ev-none { border-top: 1px solid var(--border); }

@media (max-width: 720px) {
  .ev-toolbar { padding: var(--s-3) var(--s-4); }
  .ev-search { max-width: none; flex-basis: 100%; }
  .ev-count { margin-left: 0; flex-basis: 100%; }
  .ev-cell-event { min-width: 220px; }
}

/* ── §24 The SmartCreate mark ─────────────────────────────────────────────────────────────────
   Every way into SmartCreate is the same button: the primary sweep, a gold sparkle, and one soft
   glow — the dashboard hero, the Events list, the Add event and Edit doors. Learnt once, it is
   recognised everywhere; a manager never has to read the label to know this is the AI. */
.btn-ai {
  position: relative;
  box-shadow: var(--shadow-brand);
}

.btn-ai .icon { color: var(--gold-200); filter: drop-shadow(0 0 4px rgb(255 255 255 / 0.35)); }
.btn-ai:hover { box-shadow: var(--shadow-brand), var(--shadow-md); }
.btn-ai:hover .icon { transform: rotate(12deg) scale(1.08); }
.btn-ai .icon { transition: transform var(--dur-fast) var(--ease-out); }

/* The door bar on the manual pages: one sentence and the button, side by side. */
.sc-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-4) var(--s-5);
  margin-bottom: var(--s-5);
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-lg);
  background: linear-gradient(135deg, var(--brand-050), var(--surface) 62%);
}

.sc-bar-copy { margin: 0; font-size: var(--t-sm); color: var(--ink-600); line-height: 1.5; max-width: 60ch; }
.sc-bar-copy strong { color: var(--ink-900); }
.sc-bar .btn { flex: none; }

@media (max-width: 720px) {
  .sc-bar { flex-direction: column; align-items: stretch; }
  .sc-bar .btn { width: 100%; }
}
/* ============================================================================
   ROLE CHOOSER — /account/role
   ----------------------------------------------------------------------------
   The page a login with more than one role lands on. It asks ONE question and
   the answer is a button, so it is built as a hero plus a card per choice
   rather than as a list of rows: a row that is one of two whole destinations
   reads like a setting, and this is not a setting.

   A CARD PER OPTION, NOT TWO. The brief describes the common pair —
   organisation and volunteer — but RolesForAsync can return a third (platform
   staff), so nothing here counts to two. The "current" treatment is a modifier,
   not a position.

   Lives beside .ob- because it is the same shell and the same sheet; every
   colour, radius, shadow and step of space is a token from site.css, so it
   themes itself and cannot drift from the rest of the product.
   ============================================================================ */

/* ------------------------------------------------------------------ hero ---- */

.rc-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(170px, 30%, 230px);
  align-items: center;
  gap: var(--s-5);
  margin-bottom: var(--s-7);
  padding: var(--s-6);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  background:
    radial-gradient(120% 140% at 100% 0%, var(--brand-100), transparent 58%),
    linear-gradient(135deg, var(--brand-050), var(--surface));
}

.rc-hero-text { min-width: 0; }

.rc-eyebrow {
  margin: 0 0 var(--s-2);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--brand-600);
}

/* --t-4xl is the global h1 and it is sized for a landing page; inside a 780px
   column beside an illustration it wraps to four lines. */
.rc-hero h1 { margin: 0 0 var(--s-3); font-size: var(--t-3xl); }

.rc-lede {
  margin: 0;
  max-width: 46ch;
  color: var(--ink-600);
  font-size: var(--t-md);
  line-height: 1.55;
}

/* Who is signed in, under the lede (issue #161). A quiet row rather than a
   badge: it is context, not an action. The address sits after the name on a
   wide screen and drops under it on a phone, and breaks anywhere rather than
   widening the hero — an email is one long token. */
.rc-signed-in {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  margin: var(--s-4) 0 0;
  color: var(--ink-600);
  font-size: var(--t-sm);
  line-height: 1.5;
}

.rc-signed-in > span:last-child { min-width: 0; overflow-wrap: anywhere; }
.rc-signed-in strong { color: var(--ink-900); font-weight: 650; }
.rc-signed-in-email { color: var(--ink-500); }
.rc-signed-in-email::before { content: "·"; margin: 0 var(--s-2); color: var(--ink-400); }

.rc-signed-in-icon {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  color: var(--brand-600);
}

.rc-signed-in-icon svg { width: 16px; height: 16px; }

@media (max-width: 560px) {
  .rc-signed-in-email { display: block; }
  .rc-signed-in-email::before { content: none; margin: 0; }
}

/* A HERO WITH NOTHING BESIDE IT. The organisation chooser carries no illustration:
   the complaint about that page was empty vertical space, and art would add the
   most of it while saying the least. Without this the grid still reserves the
   second column and the text sits in a 70% gutter. */
.rc-hero-plain { grid-template-columns: minmax(0, 1fr); }

.rc-hero-art { min-width: 0; }

/* _OnboardingArt caps itself at 220px and centres, which is right for the
   outcome pages it was built for. Here it fills the column it was given. */
.rc-hero-art .ob-result-art { max-width: 100%; }

/* ----------------------------------------------------------------- cards ---- */

.rc-choices { display: grid; gap: var(--s-4); }

.rc-card {
  margin: 0;
  padding: var(--s-5) var(--s-6);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur) var(--ease-out),
              box-shadow var(--dur) var(--ease-out);
}

.rc-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }

/* WHERE THEY ALREADY ARE. Border, tint and a lifted shadow — deliberately not a
   larger card or a different padding, because the cards sit one above the other
   and any size difference reads as importance rather than as state. */
.rc-card.is-current {
  border-color: var(--brand-200);
  box-shadow: var(--shadow-md);
  background: linear-gradient(180deg, var(--brand-050), var(--surface) 140px);
}

.rc-card-head {
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr);
  align-items: start;
  gap: var(--s-4);
}

.rc-card-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: var(--r-full);
  background: var(--info-100);
  color: var(--info-600);
}

.rc-card.is-current .rc-card-icon { background: var(--brand-100); color: var(--brand-700); }

.rc-card-title { min-width: 0; }

/* A flex row rather than an inline badge: .badge is inline-flex, and inline-flex
   on the baseline of a display-serif heading sits visibly low. */
.rc-card-title h2 {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin: 0 0 var(--s-2);
  font-size: var(--t-xl);
}

/* The badge is UI, not heading — it must not inherit the serif. */
.rc-card-title h2 .badge { font-family: var(--font-sans); letter-spacing: 0.005em; }

.rc-card-desc { margin: 0; color: var(--ink-600); line-height: 1.5; }

/* Which organisation. Indented to the title rather than the card edge, so it
   belongs to the heading above it instead of starting a new column. */
.rc-card-where {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-4) 0 0;
  padding-left: calc(52px + var(--s-4));
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--ink-700);
}

.rc-where-icon { display: inline-flex; flex: 0 0 auto; color: var(--ink-400); }
.rc-where-icon .icon { width: 18px; height: 18px; }

.rc-card-foot {
  margin-top: var(--s-5);
  padding-top: var(--s-5);
  border-top: 1px solid var(--border);
}

.rc-card-foot .btn { min-width: 168px; }

/* ── COMPACT CARDS, for a list rather than a pair of destinations ────────────
   The role chooser shows two or three whole portals and gives each a 168px
   button to match its weight. The organisation chooser can show any number of
   rows of one line each, and at that density the same button is the widest
   thing on the card by a long way. Everything else — the border, the radius,
   the shadow, the head, the badge — is shared, because the two pages are read
   one after the other by anybody who holds two roles across two parishes.

   44px of height is kept: this is tapped one-handed like everything else. */
.rc-choices-compact .rc-card { padding: var(--s-4) var(--s-5); }
.rc-choices-compact .rc-card-head { grid-template-columns: 44px minmax(0, 1fr); gap: var(--s-3); }
.rc-choices-compact .rc-card-icon { width: 44px; height: 44px; }
.rc-choices-compact .rc-card-icon .icon { width: 20px; height: 20px; }
.rc-choices-compact .rc-card-title h2 { font-size: var(--t-lg); }

.rc-choices-compact .rc-card-where { padding-left: calc(44px + var(--s-3)); }

.rc-choices-compact .rc-card-foot { margin-top: var(--s-4); padding-top: var(--s-4); }
.rc-choices-compact .rc-card-foot .btn { min-width: 0; padding-inline: var(--s-5); }

/* .btn-arrow's hover nudge is a transform, which does nothing to an inline box. */
.rc-card-foot .btn-arrow { display: inline-flex; }
.rc-card-foot .btn-arrow .icon { width: 18px; height: 18px; }

/* ------------------------------------------------------------------ note ---- */

.rc-note {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  margin: var(--s-6) 0 0;
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink-600);
}

.rc-note-icon { display: inline-flex; flex: 0 0 auto; margin-top: 1px; color: var(--ink-400); }
.rc-note-icon .icon { width: 18px; height: 18px; }

/* THE ACTION KEEPS ITS OWN WIDTH, and this one line is the whole clipping fix.
   .btn carries `overflow: hidden`, and a flex item whose overflow is not `visible`
   has an automatic minimum size of 0 instead of its content width — so the button
   was free to shrink below its label, and `white-space: nowrap` then cut the label
   off mid-word ("Register another organis…"). Refusing to shrink restores the
   content width as the floor, at every width, so the label cannot be clipped here
   again. The note's text is the flexible half and wraps as it always did. */
.rc-note .btn { flex: 0 0 auto; }

/* THE SENTENCE TAKES THE REST OF THE LINE AND WRAPS INSIDE ITSELF, so it stays
   beside the icon instead of breaking onto its own line once the note is allowed
   to wrap. `min-width: 0` is the half that is easy to miss: a flex item will not
   shrink below its longest word without it, which is enough to push the whole
   sentence onto the next line on a narrow phone. */
.rc-note-text { flex: 1 1 0; min-width: 0; }

/* ------------------------------------------------------------ narrow ---- */

/* THE ART GOES RATHER THAN SHRINKS. Below this the column cannot hold a heading
   and an illustration side by side, and a 120px version of it is a smudge. It
   carries no information, so removing it removes nothing. */
@media (max-width: 720px) {
  .rc-hero {
    grid-template-columns: minmax(0, 1fr);
    padding: var(--s-5);
    margin-bottom: var(--s-6);
  }

  .rc-hero-art { display: none; }
  .rc-hero h1 { font-size: var(--t-2xl); }
}

@media (max-width: 560px) {
  .rc-card { padding: var(--s-5) var(--s-4); border-radius: var(--r-lg); }
  .rc-card-head { grid-template-columns: 44px minmax(0, 1fr); gap: var(--s-3); }
  .rc-card-icon { width: 44px; height: 44px; }
  .rc-card-icon .icon { width: 20px; height: 20px; }
  .rc-card-title h2 { font-size: var(--t-lg); }

  /* The indent is wider than the text it would leave room for at this width. */
  .rc-card-where { padding-left: 0; }

  .rc-card-foot { margin-top: var(--s-4); padding-top: var(--s-4); }
  .rc-card-foot .btn { width: 100%; min-height: 48px; }

  /* THE COMPACT RULES ARE ONE CLASS MORE SPECIFIC THAN THE THREE ABOVE, so they
     win inside this query too and the phone would keep the desktop indent and
     the desktop padding. Restated here rather than weakened there: the compact
     block is what makes the organisation cards a list, and it should keep doing
     that at every width it applies to. */
  .rc-choices-compact .rc-card { padding: var(--s-4); }
  .rc-choices-compact .rc-card-where { padding-left: 0; }
  .rc-choices-compact .rc-card-foot .btn { min-width: 0; width: 100%; }

  /* THE NOTE STACKS RATHER THAN COMPETING FOR THE LINE. At 375px the sentence and
     a 200px action cannot share a row and still read: the text is squeezed to a
     column of two or three words while the button takes what is left. Wrapping
     lets the icon and the sentence keep the first line and drops the action onto
     its own, which is the order somebody reads it in anyway — what it is, then
     what to do about it.

     Full width and 48px to match .rc-card-foot .btn a few rules above: the cards
     on this page already answer "how does a button look on a phone here", and a
     second answer beside the first is how two conventions start. */
  .rc-note { padding: var(--s-4); flex-wrap: wrap; }

  .rc-note .btn {
    flex-basis: 100%;
    width: 100%;
    min-height: 48px;
    margin-top: var(--s-1);
  }
}

/* ── TICKET TYPE ROW ACTIONS ──────────────────────────────────────────────────
   THE SAME SHAPE .vol-row-actions ESTABLISHED, for the same reason and with the
   same trade. Edit / Withdraw / Delete were three inline forms separated by
   whitespace, so they laid out as inline blocks: at 1024px they wrapped onto
   three lines and took the Actions cell to 221px, and the Delete button ended up
   129px below the top of the row. On a phone it was 361px down — past the
   starter's guidance note, the price, the places and the on-sale badge.

   NOTHING IS HIDDEN AND NOTHING IS REMOVED. All three actions are still here and
   still post to the handlers they always did; they are laid out as one group
   instead of three floating forms, so Delete sits beside Edit where the row's
   own heading says the actions are.

   WRAP, NEVER NOWRAP. Held rigid the group would not shrink — it would overflow
   into the column beside it, which is exactly the defect .vol-row-actions
   documents. Allowed to wrap it stays inside the Actions column at every width
   and the row grows a line instead. */
.tt-row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;

  /* A flex item will not shrink below its content unless told it may, and these
     are the three buttons. Without it the group has a min-content floor of its
     widest button and pushes the column open from the inside. */
  min-width: 0;
}

/* The inline <form> wrappers are layout-invisible, so the buttons inside them
   are what the flex row must lay out. Without this each form is a block-level
   flex item and the buttons cannot sit beside each other. */
.tt-row-actions form {
  display: contents;
}

/* COMPACT, AND ONLY HERE — the same 10px/0.78rem the volunteer roster's group
   uses, so the two action columns in this portal read as one convention rather
   than two. Height is untouched: .btn-sm keeps its 36px box and its 44px touch
   target on a phone, because what was taken out is horizontal only. */
.tt-row-actions .btn {
  padding-left: 10px;
  padding-right: 10px;
  font-size: 0.78rem;
  white-space: nowrap;
}

/* THE REFUSAL SITS IN THE GROUP, not under it. When a type has sold, Delete is
   replaced by the sentence saying why — and that sentence has to occupy the same
   place the button did, or the row reflows depending on whether somebody has
   bought a ticket. */
.tt-row-actions .hint {
  margin: 0;
  align-self: center;
}

/* ── NO WIDTH IS FORCED ON THIS COLUMN, AND THAT WAS A FINDING ────────────────
   THE OBVIOUS FIX MADE IT WORSE, TWICE, SO IT IS WRITTEN DOWN HERE RATHER THAN
   REDISCOVERED. All three buttons measure 195px together, so a 227px floor on
   the cell looks like the way to keep them on one line. Measured at 1024px, that
   floor took the table from 582px to 616px inside a 582px container — so
   .table-scroll began scrolling sideways and Delete, the rightmost thing in the
   row, went off the right edge of the visible region. A floor that hides the
   button it exists to reveal is not a fix. A smaller 142px floor did the same
   thing for the same reason.

   WHAT WORKS IS THE ORDER, NOT THE WIDTH. Auto layout already gives this column
   about 159px — 127px of content — and Edit plus Delete measure 110px with their
   gap, so the pair the organiser needs fits without being asked. Withdraw is what
   wraps to a second line when there is no room, which costs nothing: it is the
   one action here with an alternative, and the markup puts it after Delete so it
   is the button that drops. See Tickets.cshtml, where that order is explained.

   So the table keeps its natural widths and the page never scrolls sideways. */

/* ── THE ACTIONS STAY AT THE TOP OF A TALL ROW ────────────────────────────────
   The starter's guidance note makes its row two or three times the height of
   every other row. With the cell vertically centred, the actions floated to the
   middle of that space and drifted away from the ticket name they belong to.
   Pinned to the top they line up with the row's first line at every height. */
.table .tt-cell-actions {
  vertical-align: top;
}

/* ── TICKET TYPE FIGURES: THE PRICE AND THE PLACES ────────────────────────────
   WHAT THE ORGANISER ACTUALLY SAW, measured at 768px on the real stylesheet:

     AUD
     $0.00        100

   Three separate things made that unreadable, and each is fixed below.

   1. THE AMOUNT BROKE IN HALF. MoneyDisplay.Exact returns "AUD $0.00" with an
      ordinary space, and this column resolves to 107px — 75px of content — so
      the line broke after the currency code. "AUD" alone above "$0.00" reads as
      a label over a number rather than as one amount.

   2. NOTHING SAID WHICH FIGURE WAS WHICH. The headings live in <thead>, and the
      starter's guidance note makes its row 247px tall, so on every row after the
      first the words "Price" and "Places" are a screen away from the numbers
      under them. A bare "100" beside a bare "$0.00" is the reported confusion.

   3. BOTH FLOATED IN THE MIDDLE OF THE ROW. Table cells are vertically centred,
      so in a 247px row the two figures sat ~100px below the heading and ~100px
      above the divider, touching nothing. That is the "disconnected" part.

   NOTHING HERE TOUCHES A VALUE. The price, the currency, the capacity and every
   calculation behind them are exactly as they were — this is where they sit and
   what is written above them. */

/* 1 · ONE AMOUNT, ONE LINE. Auto table layout widens the column to the figure's
      min-content width instead of breaking it, which is the right trade for
      money: a wrapped amount is misread, a slightly wider column is not. */
.tt-figure {
  white-space: nowrap;
}

/* 3 · THE FIGURES START WHERE THE ROW STARTS, so they line up with the ticket
      name beside them and with the heading above them rather than drifting to
      the middle of whatever height the row happens to be. Applied to the whole
      row's cells, not only these two, or the badge and the actions would keep
      floating while the numbers moved. */
.tt-table td {
  vertical-align: top;
}

/* 2 · THE HEADING COMES DOWN INTO THE CELL, between the stacking breakpoint and
      the width at which the console stops being cramped.

      BELOW 561px this is already solved and nothing is added: .pt-table-stack
      turns every row into a card and prints each cell's data-label above it.
      The rules below deliberately start one pixel above that, so the two never
      both apply and there is exactly one label.

      960px IS BORROWED, NOT INVENTED. It is where .pt-shell gives up its 264px
      sidebar — the established line in this stylesheet between "the console is
      squeezed" and "there is room". Above it the table is wide, the rows are
      shorter and the header row reads as the heading it is.

      SAME TYPOGRAPHY AS THE STACKED LABEL, because it is the same idea: the
      column heading, carried down with its cell. Copied from
      .pt-table-stack td[data-label]::before rather than invented beside it, so
      a phone and a tablet do not label the same number two different ways. */
@media (min-width: 561px) and (max-width: 960px) {
  .tt-table td.tt-figure::before {
    content: attr(data-label);
    display: block;
    font-size: var(--t-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--ink-500);
    font-weight: 700;
    margin-bottom: 2px;
  }
}

/* §26 — the pictures an event already has, as small squares.

   THE TILE IS THE SIZE THE THUMBNAIL IS, which is the whole point of the block.
   The gallery was drawn by fetching each 1200px photograph and letting the
   browser shrink it into a 180×120 box; these are 96px squares showing the
   400px square the upload already wrote. object-fit keeps a portrait
   photograph from being stretched into a square that lies about its framing.

   The banner keeps its own 16:9 shape (.pt-thumb-wide): it is the one picture
   whose composition an organiser is judging, so cropping it to a square here
   would misrepresent the thing they are checking. */
.pt-thumbs-group { margin-top: var(--s-4); }

.pt-thumbs-head {
  margin: 0 0 var(--s-2);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-500);
  font-weight: 700;
}

.pt-thumbs-count {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
  color: var(--ink-500);
}

.pt-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
}

.pt-thumb {
  position: relative;
  margin: 0;
  width: 96px;
}

.pt-thumb img {
  display: block;
  width: 96px;
  height: 96px;
  object-fit: cover;
  border-radius: var(--r-md);
  border: 1px solid var(--border);
  background: var(--surface-2);
}

.pt-thumb-wide,
.pt-thumb-wide img { width: 160px; }
.pt-thumb-wide img { height: 90px; }

/* The caption is the alt text, kept to one line: it identifies the picture
   without turning a row of tiles into a paragraph. */
.pt-thumb-cap {
  margin-top: 4px;
  font-size: var(--t-xs);
  color: var(--ink-500);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ON the picture, top right. A remove button under each tile made twelve
   pictures read as twelve stacked forms. Always visible rather than revealed on
   hover — a control that appears only on hover is a control a touch screen
   does not have. */
.pt-thumb-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  margin: 0;
}

.pt-thumb-x {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  color: #fff;
  background: rgba(24, 18, 43, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 999px;
}

.pt-thumb-x:hover,
.pt-thumb-x:focus-visible { background: var(--danger); }

/* ── §27 The account menu (issue #250) ─────────────────────────────────────────────────────────
   The top-right chip is a <details>; .pt-user is its summary, so the bar looks as it did until
   it is clicked. The panel is positioned off the chip's right edge, inside .pt-top's sticky
   stacking context, so it rides above the page content the bar already sits over.

   THE ACTIONS INSIDE IT KEEP THEIR CLASS. .pt-top-account is still the wrapper and the 960px rule
   above still hides it, which is what keeps exactly one copy of Switch role / Switch organisation
   / Sign out in the tree at every width. The rules here set only direction and alignment for it
   and NEVER display — a more specific display would beat the media query and put two copies on
   a phone. */
.pt-account { position: relative; }

.pt-account > summary {
  list-style: none;
  cursor: pointer;
  border-radius: var(--r-md);
  padding: var(--s-1) var(--s-2);
  margin: calc(-1 * var(--s-1)) calc(-1 * var(--s-2));
}

.pt-account > summary::-webkit-details-marker { display: none; }
.pt-account > summary:hover { background: var(--surface-2); }

.pt-account > summary:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
}

.pt-account-caret {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  color: var(--ink-500);
}

.pt-account-caret .icon { width: 16px; height: 16px; }
.pt-account[open] .pt-account-caret { transform: rotate(180deg); }

.pt-account-menu {
  position: absolute;
  top: calc(100% + var(--s-2));
  right: 0;
  width: min(320px, calc(100vw - 2 * var(--s-4)));
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-3);
  background: var(--surface-0);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  z-index: 30;
}

.pt-account-head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2);
}

.pt-account-avatar { width: 44px; height: 44px; font-size: 1rem; flex: 0 0 auto; }

.pt-account-who { min-width: 0; }
.pt-account-who p { margin: 0; overflow-wrap: anywhere; }
.pt-account-name { font-weight: 650; color: var(--ink-900); }
.pt-account-email { font-size: var(--t-sm); color: var(--ink-600); }
.pt-account-org { font-size: var(--t-sm); color: var(--ink-500); }

.pt-account-sep {
  border: 0;
  border-top: 1px solid var(--border);
  margin: var(--s-1) 0;
}

/* A column of full-width, left-aligned actions, the way a menu reads; .btn centres by default
   because it is built for a row. */
.pt-account-menu .btn { width: 100%; justify-content: flex-start; }
.pt-account-menu .pt-top-account { flex-direction: column; align-items: stretch; gap: var(--s-1); }

/* AN EMPTY WRAPPER STILL COSTS A GAP. Both switches are conditional — an administrator of one
   organisation holding one role gets neither — and since Sign out left this wrapper it can now
   be empty, at which point it is a zero-height flex item that the menu's own gap still spaces on
   both sides. :not(:has(*)) rather than :empty because Razor leaves whitespace inside the div and
   :empty counts that as content. It hides nothing that is rendered, so it cannot put a second
   copy of anything on a phone — the concern the rule above is written for. */
.pt-account-menu .pt-top-account:not(:has(*)) { display: none; }

/* THE SIGN-OUT FORM IS A BOX NOBODY WANTS, at either level. It used to sit inside
   .pt-top-account and now sits directly in the menu — display: contents makes its button a flex
   item of whichever one contains it, so the form's own block box stops adding a line and the
   menu's gap spaces the button like every sibling. The selector is no longer tied to the
   wrapper for that reason; it is still the only <form> in this menu. */
.pt-account-menu form { display: contents; }

@media (prefers-reduced-motion: no-preference) {
  .pt-account-caret { transition: transform var(--dur-fast) var(--ease-out); }
}

/* ── §28 The sidebar's organisation name is a door (issue #255) ────────────────────────────────
   .pt-side-brand is a <div> now, because the mark and the organisation name go to different
   places and anchors cannot nest. Its flex rules above are unchanged; these only make the two
   anchors inside it look exactly as the one anchor did, and give the organisation's name the
   one cue that it can be clicked. */
.pt-side-home { color: inherit; text-decoration: none; }
.pt-side-home:hover { color: inherit; }

/* THE MARK KEEPS ITS 40px BOX. As a direct child of the flex row the span WAS a flex item, so
   its width and height applied; wrapped in an inline anchor it is inline, an inline box ignores
   both, and the SVG grew to the column and pushed the name into three lines. The anchor is made
   the flex item instead and the span a block inside it, so the old rules mean what they meant. */
.pt-side-brand > .pt-side-home { display: flex; flex: 0 0 auto; }
.pt-side-brand > .pt-side-home .brand-mark { display: block; width: 40px; height: 40px; }
.pt-side-brand > .pt-side-home .brand-mark svg { display: block; width: 100%; height: 100%; }

.pt-side-brand-text { display: flex; flex-direction: column; min-width: 0; }

.pt-side-org-link { text-decoration: none; color: var(--ink-500); }

.pt-side-org-link:hover,
.pt-side-org-link:focus-visible { color: var(--brand-600); text-decoration: underline; }

.pt-account-org a { color: inherit; text-decoration: underline; text-decoration-color: var(--border-strong); }
.pt-account-org a:hover { color: var(--brand-600); }
/* ── The slug field, wrapping (issue #208) ──────────────────────────────────
   A textarea carrying .input, so it inherits every colour, border and focus
   ring the single-line field had. What is added here is only the shape: two
   rows of wrapped text instead of one row that scrolls sideways.

   .input's own min-height is 46px and .textarea's 120px — this field carries
   .input, so the tall rule never applies and the height comes from rows="2".

   overflow-wrap:anywhere is the point of the change. A slug is one long
   unbroken token with no spaces to break at, so without it the browser keeps
   it on one line and the wrap never happens.

   resize:none because there is nothing to drag out to: the value is capped at
   sixty characters, which two rows already hold at 320px. */
.pt-slug {
  min-height: 0;
  line-height: 1.5;
  overflow-wrap: anywhere;
  word-break: break-all;
  resize: none;
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: var(--t-sm);
}
/* ── Where you belong, and as what (issue #253) ─────────────────────────────
   A list of rows on the profile page: the organisation and the role on the
   left, the switch or the "Current" badge on the right.

   Not .table. A table implies columns worth scanning down, and there are two
   facts per row — on a phone the same markup simply stacks, which a table
   cannot do without the stacked-table rules elsewhere in this file. */
.pt-memberships {
  list-style: none;
  margin: var(--s-4) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.pt-membership {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-1);
}

/* The two facts are one block, so the row wraps between the names and the
   action rather than between the organisation and the role. */
.pt-membership-who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The hint under the organisation's name is the role; it carries the page's
   ordinary hint colour and needs no top margin inside this stack. */
.pt-membership-who .hint { margin-top: 0; }

/* The form is a wrapper around one button and must not become a block that
   pushes the button onto its own line. */
.pt-membership form { margin: 0; }

/* ── §29 Copy the public link (issue #292) ─────────────────────────────────────────────────────
   The button beside an event's public address, on the details page and the publishing step.
   It never shrinks, so on a phone the long address wraps inside its box and the button drops
   to its own line — the row grows downward, never sideways. is-done is the moment after a copy,
   in the same ok tones the Events list's copy icon uses. */
.pt-copy-link { flex: 0 0 auto; white-space: nowrap; gap: var(--s-1); }
.pt-copy-link .icon { width: 1em; height: 1em; }
.pt-copy-link.is-done { --btn-fg: var(--ok-600); --btn-bg: var(--ok-100); }

/* In the publishing step the address sits in a sentence rather than a flex box, so the button
   takes a little air on both sides and lines up with the text beside it. */
.ev-live-link .pt-copy-link { margin-left: var(--s-2); vertical-align: middle; }

/* ── §30 Date and time inputs on a phone (issue #290) ──────────────────────────────────────────
   On an iPhone the builder's "Date and start time" and "Ends" boxes ran past the card's right
   edge. Two things conspire. The step is a grid, and a grid item's min-width defaults to auto —
   it can never be narrower than its content's intrinsic width. And iOS WebKit gives a
   datetime-local control a wide intrinsic width of its own, which width:100% cannot shrink below;
   Chromium's control is narrow, which is why a desktop never showed it. So: let the grid items
   shrink, and take the platform sizing off the control. The value is also started at the left
   like every other field (iOS centres it) and kept at text height while empty (iOS collapses it),
   so the box stays the same size before and after a date is picked. */
.eb-step > .field,
.eb-step > .eb-more,
.eb-more > .field,
.eb-group-body > .field { min-width: 0; }

.input[type="datetime-local"],
.input[type="date"],
.input[type="time"] {
  display: block;
  min-width: 0;
  max-width: 100%;
  appearance: none;
  -webkit-appearance: none;
}

.input[type="datetime-local"]::-webkit-date-and-time-value,
.input[type="date"]::-webkit-date-and-time-value,
.input[type="time"]::-webkit-date-and-time-value {
  text-align: start;
  min-height: 1.5em;
}

/* ── §30 Insights (issues #332, #333) ────────────────────────────────────────────────────────
   Tickets and money as pictures. Tiles are site.css .stat; the bars are _SegmentBar, one strip
   divided into named parts with a legend that carries every figure as text. Every colour is a
   token, so both themes hold, and no part is told apart by colour alone. */
.in-pick { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3); margin: var(--s-4) 0 var(--s-3); }
.in-pick .label { margin: 0; }
.in-pick .input { max-width: 28rem; flex: 1 1 16rem; }
.in-scope { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2) var(--s-3); margin: 0 0 var(--s-4); font-size: var(--t-md); }
.in-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-4); }
.in-card-wide { grid-column: 1 / -1; }
.in-card .card-pad { display: flex; flex-direction: column; gap: var(--s-4); }
.in-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--s-3); }
.in-tiles .stat { padding: var(--s-3) var(--s-4); }
.in-tiles .stat-value { font-size: var(--t-xl); }
.in-empty { display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-3); padding: var(--s-5); }
.in-empty h3 { margin: 0; }
.in-empty p { margin: 0; color: var(--ink-600); max-width: 48ch; }
.in-table th.num, .in-table td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }

.segbar { margin: 0; display: flex; flex-direction: column; gap: var(--s-3); }
.segbar-track { display: flex; height: 14px; border-radius: 7px; background: var(--surface-3); overflow: hidden; }
.segbar-seg { display: block; height: 100%; min-width: 2px; }
.segbar-seg + .segbar-seg { border-left: 2px solid var(--surface-0); }
.segbar-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-5); font-size: var(--t-sm); }
.segbar-legend li { display: flex; align-items: center; gap: var(--s-2); }
.segbar-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 3px; flex: none; }
.segbar-name { color: var(--ink-600); }
.segbar-val { color: var(--ink-900); font-variant-numeric: tabular-nums; }
.seg-brand { background: var(--brand-600); }
.seg-ok { background: var(--ok-600); }
.seg-gold { background: var(--gold-500); }
.seg-quiet { background: var(--surface-3); }
.segbar-legend .seg-quiet { border: 1px solid var(--border); }

@media (max-width: 860px) {
  .in-grid { grid-template-columns: 1fr; }
}

/* ── §31 Money by event, for the platform (issues #256, #260, #261) ───────────────────────────
   The release table carries eight money columns; figures are right-aligned and tabular so the
   eye can run down a column, and the table scrolls inside its card rather than the page. */
.pt-money-table th.num, .pt-money-table td.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.pt-money-table td .hint { font-size: var(--t-xs); }

/* ── §32 Dashboard tiles are doors (issue #258) ───────────────────────────────────────────────
   A .stat rendered as an <a> keeps the tile's own ink and loses the link underline; the existing
   hover lift already says it can be pressed. */
a.stat { text-decoration: none; color: inherit; display: block; }
a.stat:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }

/* ── §33 An event on more than one date (issue #219) ──────────────────────────────────────────
   The builder's extra-date inputs, the edit page's list of dates, and the public page's "Also on"
   chooser. Every date is its own event; these are the ways between them. */
.eb-dates { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--s-3); }
.eb-dates .field { margin-bottom: 0; }
.eb-series { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
.eb-series li { display: flex; align-items: center; gap: var(--s-2); }
.ev-dates { display: flex; flex-direction: column; gap: var(--s-2); padding: var(--s-3) 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.ev-dates ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s-2); }
.ev-dates .chip { display: inline-block; padding: 6px 12px; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--ink-700); text-decoration: none; font-size: var(--t-sm); background: var(--surface-0); }
.ev-dates a.chip:hover { border-color: var(--brand-600); color: var(--brand-600); }
.ev-dates .chip.is-current { background: var(--brand-600); border-color: var(--brand-600); color: var(--surface-0); font-weight: 650; }

/* ── §34 Do the two passwords agree? (issue #354) ──────────────────────────────────────────────
   The note under the confirmation box on every form that sets a password. It is a .hint until
   there is something to say; password-meter.js gives it one of these two classes and the server's
   own sentence. The tones are the ones the rest of the form already uses for "fine" and "not". */
.pw-match-ok { color: var(--ok-600); font-weight: 600; }
.pw-match-no { color: var(--danger-600); font-weight: 600; }

/* ── §35 The sign-up rail on a phone names the step you are on (issue #353) ─────────────────────
   Below 560px the rail drops every label to fit, leaving a row of dots: a tick, a "2", and no way
   to tell what either was — which is how a confirmed address read as an unfinished step. The
   current step's label comes back; the others stay dots, because four or five names do not fit
   and the one that matters is the one the reader is standing on. Scoped away from .ob-steps-nav,
   whose labels are already shown at this width. */
@media (max-width: 560px) {
  .ob-steps:not(.ob-steps-nav) .ob-step.is-current .ob-step-label { display: inline; white-space: nowrap; font-size: var(--t-xs); }

  /* Room for that name. Four dots, three connectors and a label ran 8px past a 375px screen
     with the connectors and gaps at their desktop widths; tighter connectors and gaps bring the
     automatic road inside it. The five-step manual road can still scroll a little — the rail is
     overflow-x: auto for exactly that — and it is the road the shipped default never draws. */
  .ob-steps:not(.ob-steps-nav) { gap: var(--s-1); }
  .ob-steps:not(.ob-steps-nav) .ob-step + .ob-step::before { width: 12px; margin-right: var(--s-1); }
}
