
  /* Only color-scheme survives here. The seven colour tokens this block used to
     define were dead: the second :root below redefines every one of them, and a
     custom property resolves by cascade rather than by where it is used, so nothing
     ever read these values. Removing them is what makes the palette below the only
     one. (No backticks in these comments: the sheet is a template literal.) */
  :root { color-scheme: light; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  * { box-sizing: border-box; }
  body { margin: 0; background: var(--chalk); color: var(--ink); line-height: 1.5; }
  .skip-link { position: absolute; left: -999px; top: 0; }
  .skip-link:focus { left: 0.5rem; top: 0.5rem; background: #fff; padding: 0.5rem 0.75rem;
    border-radius: 4px; z-index: 10; }
  .site-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;
    gap: 0.5rem 1rem; padding: 0.75rem 1.25rem; background: var(--court-dark); color: #fff;
    border-bottom:4px solid var(--shuttle); }
  .site-header a { color: #fff; text-decoration: none; }
  .site-header .brand { font-weight: 950; font-size: 1.1rem; letter-spacing:-.03em; }
  .site-header nav { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
  .nav-user { overflow-wrap: anywhere; }
  .platform-announcement { position:relative; display:flex; align-items:center; gap:.75rem;
    margin:0 0 1.25rem; padding:.85rem 3rem .85rem 1rem; border:1px solid var(--accent-shade-2);
    border-radius:10px; background:#fff7d6; color:#3b3214; box-shadow:0 4px 14px rgba(64,52,8,.08); }
  .platform-announcement-message { flex:1 1 auto; white-space:pre-wrap; overflow-wrap:anywhere; }
  .platform-announcement-cta { flex:0 0 auto; font-weight:800; color:var(--court-shade-4); }
  .platform-announcement-dismiss { position:absolute; top:.35rem; right:.4rem; min-height:2rem;
    width:2rem; padding:0; border-radius:999px; background:transparent; color:#554817;
    font-size:1.35rem; line-height:1; }
  .platform-announcement-dismiss:hover { background:rgba(83,69,18,.1); }
  @media (max-width:560px) { .platform-announcement { align-items:flex-start; flex-direction:column; } }
  @media (max-width:560px) {
    .site-header .header-billing { font-size:.8rem; padding:.4rem .62rem; }
    /* Drop the trailing "· See plans"/"· Subscribe" on a phone: the base label
       fits on one line and the pill is still the link. */
    .site-header .header-billing .chip-action { display:none; }
  }
  main { max-width: 820px; margin: 0 auto; padding: 1.25rem 1rem 3rem; }
  @media (min-width: 640px) { main { padding: 1.5rem 1.25rem 3rem; } }
  h1 { font-size: clamp(1.8rem, 5vw, 2.7rem); line-height:1; letter-spacing:-.045em; }
  h2 { font-size: clamp(1.1rem, 3vw, 1.25rem); }
  form.card, .card { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1.25rem;
    margin-bottom: 1.5rem; }
  .field { margin-bottom: 1rem; display: flex; flex-direction: column; }
  label { font-weight: 600; margin-bottom: 0.25rem; }
  input, select, textarea { padding: 0.7rem; border: 1px solid var(--court-tint-2); border-radius: 3px; font-size: 1rem;
    font-family: inherit; width:100%; min-width:0; max-width: 100%; }
  .hint { font-weight: 400; color: #555; font-size: 0.85rem; }
  button, .site-header button { background: var(--court-dark); color: #fff; border: none; border-radius: 3px;
    padding: 0.65rem 1.1rem; font-size: 1rem; cursor: pointer; min-height: 2.75rem; }
  .button-link { display:inline-flex; align-items:center; justify-content:center; background:var(--court-dark);
    color:#fff; border-radius:3px; padding:.65rem 1.1rem; min-height:2.75rem; text-decoration:none; }
  button:hover { background: var(--court); }
  button.danger { background: #7a1f1f; }
  button.danger:hover { background: #9c2828; }
  button.secondary { background: #fff; color: var(--court-dark); border: 1px solid var(--court-dark); }
  /* A QR has to stay square and stay scannable: no cropping, no upscaling past
     the pixels it actually has. */
  .payment-qr { display:block; width:180px; height:180px; object-fit:contain; background:#fff;
    border:1px solid var(--court-tint-8); border-radius:8px; padding:6px; }
  .button-link.secondary { background:#fff; color:var(--court-dark); border:1px solid var(--court-dark); }
  button.compact, .button-link.compact { min-height:2rem; padding:.3rem .65rem; font-size:.875rem; }
  .row-actions { display:flex; align-items:center; gap:.4rem; white-space:nowrap; }
  .inline-form { display: inline; }
  /* The save button and its confirmation. The region is never display:none —
     that removes it from the accessibility tree, and a live region has to be
     in the tree BEFORE its text changes to be announced reliably, which is the
     whole reason the server renders it empty rather than the script creating
     it. So an empty one simply has no text, no tick and no margin, and takes
     no space: the gap lives on the filled state, not on .save-row. */
  .save-row { display:flex; align-items:center; flex-wrap:wrap; }
  .saved-flag { color:var(--court-shade-2); font-size:.9rem; font-weight:600; }
  .saved-flag:not(:empty) { margin-left:.6rem; }
  .saved-flag:not(:empty)::before { content:"✓ "; }
  @media (prefers-reduced-motion: no-preference) {
    .saved-flag:not(:empty) { animation: saved-flag-in .18s ease-out; }
    .saved-flag[data-fading] { opacity:0; transition:opacity .4s ease; }
    @keyframes saved-flag-in { from { opacity:0; transform:translateX(-.25rem); } to { opacity:1; } }
  }
  .error-summary { background: #fdecea; border: 1px solid #f5c2c0; color: #611a15; border-radius: 6px;
    padding: 0.75rem 1rem; margin-bottom: 1rem; }
  /* Inside .player-link, which is a green .notice — an abuse count read in that
     voice looks like good news. Warm, not alarming: it is information, and the
     organizer's response to it is a deliberate one (close registrations). */
  .blocked-joins { margin: 10px 0 0; padding-top: 10px; border-top: 1px solid var(--court-tint-7);
    color: #7a4a12; font-weight: 600; }
  .notice { background: var(--court-tint-17); border: 1px solid var(--court-tint-7); color: var(--court-shade-2); border-radius: 6px;
    padding: 0.75rem 1rem; margin-bottom: 1rem; }
  .table-scroll { overflow-x: auto; margin-bottom: 1.5rem; scrollbar-width: thin;
    scrollbar-color: #7b8498 #e7e9ee; }
  table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; }
  .table-scroll table { margin-bottom: 0; }
  th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #e2e2e2; }
  @media (max-width: 480px) { th, td { padding: 0.4rem; font-size: 0.9rem; } }
  code { background: #eee; padding: 0.15rem 0.35rem; border-radius: 4px; word-break: break-all; }
  .invite-link { display:flex; align-items:center; gap:.5rem; }
  .invite-link code { flex:1; min-width:0; }
  /* The sharing line and its "?" sit on one row so the tip reads as belonging
     to that sentence rather than to the link box under it. */
  .link-intro { position:relative; display:flex; align-items:flex-start; gap:.4rem; }
  .link-intro p { flex:1; margin:0; }
  .copy-button { flex:0 0 auto; }
  .reported-prices { display:grid; grid-template-columns:auto 1fr; gap:.15rem .75rem;
    margin:0 0 .75rem; }
  .reported-prices dt { color:#4a5568; font-size:.85rem; }
  .reported-prices dd { margin:0; font-weight:600; }
  /* Author display rules (.icon-button is a grid) otherwise beat the UA's
     [hidden] rule, and /copy.js hides its buttons where there is no clipboard. */
  [hidden] { display:none !important; }
  /* The summary element included: it is the control of every filter menu and of the hint
     tips, and both suppress the UA marker, so without a ring there is nothing
     to show a keyboard user where they are. */
  a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
  summary:focus-visible, textarea:focus-visible { outline: 3px solid #4c8bf5;
    outline-offset: 1px; }
  .group-nav { display: flex; gap: 0.25rem; margin-bottom: 1.25rem; border-bottom: 1px solid #ddd;
    overflow-x: auto; }
  .group-nav a { padding: 0.6rem 0.75rem; text-decoration: none; color: #14213d;
    border-bottom: 3px solid transparent; white-space: nowrap; }
  .group-nav a[aria-current="page"] { border-bottom-color: #14213d; font-weight: 700; }
  .mobile-only { display: none; }
  .audit-list { list-style: none; padding: 0; margin: 0; }
  .audit-list li { padding: 0.9rem 0; border-bottom: 1px solid #e2e2e2; overflow-wrap: anywhere; }
  .audit-list li:first-child { padding-top: 0; }
  .audit-action { font-weight: 700; color: #14213d; }
  .audit-meta { color: #555; font-size: 0.85rem; margin: 0.15rem 0 0.45rem; }
  .admin-heading { border-bottom:1px solid var(--line); padding-bottom:1.25rem; }
  .admin-section { margin:2rem 0; }
  .section-title { display:flex; justify-content:space-between; align-items:baseline; gap:1rem;
    border-bottom:3px solid var(--court-dark); }
  .section-title span { color:var(--muted); font-size:.8rem; }
  .stripe-note { color:var(--muted); font-size:.85rem; margin:0 0 1.5rem; }
  /* Filter row (M2, menus since §8.8): anchors, not a <select> — the operator
     page ships no form controls beyond the account lookup's GET field (OP-02).
     What is left here dresses that lookup group; the menus style themselves
     under the op-filter classes below. */
  .filter-row { display:flex; flex-wrap:wrap; gap:.9rem 1.5rem; border-bottom:0; margin:1.25rem 0; }
  .filter-group { display:flex; flex-wrap:wrap; align-items:baseline; gap:.3rem .5rem; }
  .filter-label { color:var(--muted); font-size:.75rem; text-transform:uppercase; letter-spacing:.07em;
    font-weight:800; }
  .filter-group a { text-decoration:none; color:var(--court-dark); font-size:.85rem; }
  @media (max-width: 480px) {
    .group-nav { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); overflow: visible;
      gap: 0; }
    .group-nav a { padding: 0.6rem 0.25rem; text-align: center; font-size: 0.9rem; }
    .desktop-only { display: none; }
    .mobile-only { display: block; }
  }
  .status-badge { display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.8rem;
    background: #eee; }
  .status-badge.registered { background: var(--court-tint-17); color: var(--court-shade-2); }
  .status-badge.waitlisted { background: #fff6e0; color: #6b4c00; }
  .status-badge.cancelled { background: #fdecea; color: #611a15; }
  .status-badge.settled { background: var(--court-tint-17); color: var(--court-shade-2); }
  .status-badge.unsettled { background: #fff6e0; color: #6b4c00; }
  /* Billing's own modifiers (§8.4): kept separate from the registration/
     settlement ones above so "cancelled" (a registration outcome) never
     collides visually with a billing state that means something else. */
  .status-badge.trial { background: #e7eefb; color: #1c3d6e; }
  /* Distinct from .scheduled's bare grey (F-1-3): on the detail page the two
     pills sit side by side, and two identical greys read as one repeated
     label rather than two different facts. */
  .status-badge.series-badge { background: #ede9fb; color: #3b2a72; }
  .status-badge.active { background: var(--court-tint-17); color: var(--court-shade-2); }
  .status-badge.attention { background: #fff6e0; color: #6b4c00; }
  .status-badge.ended { background: #f1f1f1; color: #444; }
  /* §8.3's banner: text carries the meaning (§8.4 — never colour alone), the
     background just draws the eye. Not a .notice or .error-summary variant —
     this is not a transient result of the reader's own action. */
  .billing-banner { background: #fff6e0; border: 1px solid var(--shuttle); color: #6b4c00;
    border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 1.25rem; }
  .billing-banner p { margin: 0; }
  .billing-banner a { color: inherit; font-weight: 700; }
  .site-footer { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; justify-content: center;
    max-width: 820px; margin: 2rem auto 0; padding: 1.25rem 1rem 2rem; font-size: 0.85rem; }
  .site-footer a { color: var(--muted); }
  .actions { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
  .empty-state { background: #fff; border: 1px dashed #bbc3d6; border-radius: 8px; padding: 1.25rem;
    margin-bottom: 1.5rem; }
  .empty-state h2 { margin-top: 0; }
  .checklist { list-style: none; margin: 0 0 1.5rem; padding: 0; display: flex; flex-direction: column;
    gap: 0.5rem; }
  .checklist li { display: flex; align-items: baseline; gap: 0.5rem; }
  .checklist .done { color: var(--court-shade-2); }
  .checklist .todo a { font-weight: 600; }
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .page-heading { margin:2rem 0 1.5rem; max-width:45rem; }
  .page-heading h1 { margin:.15rem 0 .6rem; }
  .page-heading p:last-child { color:var(--muted); margin:0; }
  .kicker { margin:0; color:var(--court); text-transform:uppercase; letter-spacing:.14em;
    font-size:.76rem; font-weight:900; }
  .session-sheet { background:#fff; border:1px solid var(--line); border-top:6px solid var(--court);
    box-shadow:8px 8px 0 var(--court-tint-10); margin-bottom:2rem; overflow:hidden; }
  /* Each sheet section is a <details> (sheetSection, src/views/sessions.ts), so
     what used to style its <legend> styles its <summary> instead. The row keeps
     the disclosure marker the folded cards use, which is why it borrows .fold's
     rules by name rather than growing a second chevron of its own. */
  .form-section { border:0; border-bottom:1px solid var(--line); margin:0; padding:1.35rem; }
  .form-section > summary { list-style:none; cursor:pointer; display:flex; align-items:center;
    gap:.6rem; padding:0; color:var(--court-dark); font-weight:900; font-size:1.08rem; }
  .form-section > summary::-webkit-details-marker { display:none; }
  .form-section > summary::after { content:""; margin-left:auto; flex:none; width:.5rem;
    height:.5rem; border-right:2px solid currentColor; border-bottom:2px solid currentColor;
    opacity:.55; transform:rotate(-45deg); transition:transform .15s ease; }
  .form-section[open] > summary::after { transform:rotate(45deg); }
  @media (prefers-reduced-motion:reduce) { .form-section > summary::after { transition:none; } }
  .form-section > summary:hover::after { opacity:1; }
  .form-section > summary:focus-visible { outline:3px solid #4c8bf5; outline-offset:2px;
    border-radius:4px; }
  /* Only an open section needs the gap its legend's padding used to provide. */
  .form-section[open] > summary { margin-bottom:.9rem; }
  .form-section .field:last-child { margin-bottom:0; }
  .field-note { color:var(--muted); font-size:.82rem; margin:.3rem 0 0; }
  /* An inline badge sits in its own box, so a newline in the template renders
     as a visible double gap. Laying the line out with a gap makes the spacing
     independent of how the markup happens to be wrapped. */
  .badge-line { display:flex; flex-wrap:wrap; align-items:center; gap:.4rem; }
  /* Money summary inside a card: rows, not disc bullets, to match the tables
     it sits between. */
  .totals-list { list-style:none; margin:.75rem 0 0; padding:0; }
  .totals-list li { display:flex; justify-content:space-between; gap:1rem;
    padding:.45rem 0; border-bottom:1px solid var(--line); }
  .totals-list li:last-child { border-bottom:0; }
  .card fieldset { border:0; margin:0 0 1rem; padding:0; }
  .card fieldset legend { padding:0 0 .5rem; font-weight:800; font-size:.9rem;
    text-transform:uppercase; letter-spacing:.07em; color:var(--court-dark); }
  /* Option rows read as sentences, so they opt out of the shouty uppercase
     treatment the base label rule gives field captions. */
  .choice { display:flex; align-items:flex-start; gap:.55rem; margin:0; padding:.3rem 0;
    color:var(--ink); font-size:.95rem; font-weight:400; letter-spacing:normal;
    text-transform:none; cursor:pointer; }
  /* Aligns the control with the first line of a label that wraps, rather than
     with the centre of the whole block. */
  .choice input { flex:0 0 auto; margin:.28em 0 0; }
  .choice strong { font-weight:800; }
  .choice span { min-width:0; }
  .schedule-section { background:var(--court-dark); color:#fff; }
  .schedule-section > summary { color:var(--shuttle); }
  .schedule-strip { display:grid; grid-template-columns:1.25fr 1fr 1fr; gap:1px; background:var(--court-shade-5);
    border:1px solid var(--court-shade-5); }
  .schedule-strip .field { background:#fff; color:var(--ink); margin:0; padding:1rem; }
  .schedule-strip .field,.form-grid .field { min-width:0; }
  .schedule-strip label { font-size:.78rem; text-transform:uppercase; letter-spacing:.08em; }
  .schedule-strip input,.schedule-strip select { border:0; border-bottom:2px solid var(--court);
    border-radius:0; padding:.55rem 0; font-weight:800; }
  .form-grid { display:grid; grid-template-columns:1fr 1fr; gap:0 1.25rem; }
  .full-width { grid-column:1/-1; }
  /* Per-block courts: folded away until the organizer says the courts change.
     CSS-only, because this page ships no JavaScript — the checkbox is a real
     form field the server reads, not a widget. */
  .court-blocks-rows { display:none; }
  /* :has(), not a sibling combinator — the checkbox lives inside its own label. */
  .court-blocks:has(input:checked) .court-blocks-rows { display:block; }
  .court-block-row { display:grid; grid-template-columns:11rem 1fr 1fr; gap:0 1rem;
    align-items:start; padding:.15rem 0; }
  .court-block-time { align-self:center; font-weight:800; font-size:.85rem; white-space:nowrap; }
  .court-block-row .field { min-width:0; margin:0 0 .5rem; }
  .court-labels { color:var(--muted); font-weight:600; }
  /* Repeat-this-session disclosure (F-1-3 M3), same CSS-only idiom as the
     court blocks above: folded away until the checkbox is ticked. */
  .repeat-rows { display:none; grid-template-columns:1fr 1fr; gap:0 1rem; margin-top:.5rem; }
  .repeat-fields:has(input:checked) .repeat-rows { display:grid; }
  /* Previous-session roster tick-list (SHUTTLETAB-21 M3): a third use of the
     CSS-only :has() idiom above, this time to show which of up to twenty
     names are ticked rather than to fold a section away. */
  .picks { display:grid; grid-template-columns:repeat(auto-fill,minmax(9rem,1fr)); gap:.4rem;
    margin:.6rem 0 1rem; }
  /* Opts out of the base label rule's uppercase field-caption treatment, the
     same way .choice above does and for a stronger reason: this label is a
     person's name, and a name is not a caption to be shouted. */
  .pick { display:flex; align-items:center; gap:.4rem; padding:.5rem .6rem; border:1px solid var(--line);
    border-radius:6px; cursor:pointer; margin-bottom:0; color:var(--ink); font-size:.95rem;
    font-weight:600; letter-spacing:normal; text-transform:none;
    /* A typed name may be 80 characters with no space in it. Grid tracks have
       an automatic minimum of the content's size, so without min-width:0 one
       long name pushes the track wider than the card and the page scrolls
       sideways on a phone. */
    min-width:0; overflow-wrap:anywhere; }
  .pick input { flex:none; }
  .pick:has(input:checked) { border-color:var(--court); background:rgba(var(--court-rgb),.08); }
  /* The whole chip is the click target, so the focus ring belongs on it — the
     checkbox's own 3px outline sits inside a much larger control and is easy
     to lose against twenty of them. Suppressing the inner ring and drawing the
     outer one is ONE rule: keep both and the chip paints two concentric rings;
     drop the outer and a keyboard user gets a ring most of the chip's area
     away from where their attention is. Never split this pair. */
  .pick input:focus-visible { outline:none; }
  .pick:has(input:focus-visible) { outline:3px solid #4c8bf5; outline-offset:2px; }
  /* "Me" is a name in the list like any other, but the only one that is the
     reader's own — and the only one whose write can be waitlisted. A filled
     chip says "this one is you" before anything is ticked, without a second
     row or a label to explain it. */
  .pick-me { border-color:var(--court); }
  /* "Other" sits among the ticked names but is not one: a dashed edge says so
     without needing a second row or a heading. It keeps every other .pick
     rule, the checked treatment included — once ticked it is a choice like
     the rest. */
  .pick-other { border-style:dashed; color:var(--muted); }
  /* Folded away until "Other" is ticked, the same CSS-only :has() idiom as
     .repeat-rows above. The input inside is deliberately not marked required:
     a hidden required control makes the browser refuse the submit with an
     error it cannot show ("not focusable"), so the name is checked on the
     server instead. */
  .other-name { display:none; }
  .picks:has(.pick-other input:checked) + .other-name { display:block; }
  /* Plan picker (SHUTTLETAB-26 M2, layout B): the interval toggle sits above
     three cards — two priced tiers and, since SHUTTLETAB-31, a non-priced
     "clubs & coaching" contact CTA in the third slot, which carries no radio
     and so is unaffected by the :has(input:checked) rules below.
     .plan-cards is three columns by default and collapses
     to one under 560px — narrower than the schedule/form-grid breakpoint
     above, because a plan card's own radio rows need less room than a
     schedule strip's date/time inputs before they get cramped. */
  .plan-toggle { display:flex; flex-wrap:wrap; gap:.5rem 1rem; margin:0 0 1.25rem; padding:0; border:0; }
  .plan-cards { display:grid; grid-template-columns:repeat(3,1fr); gap:1rem; margin:0 0 1.25rem; padding:0; }
  .plan-card { border:1px solid var(--line); border-radius:10px; padding:1rem; margin:0; }
  /* :has(), the same idiom .pick uses above — the whole card reads as
     chosen, not just the radio inside it. */
  .plan-card:has(input:checked) { border-color:var(--court); background:rgba(var(--court-rgb),.06); }
  .plan-option { display:block; margin:.5rem 0; }
  .plan-price { font-weight:800; }
  .plan-save { display:inline-block; margin-left:.4rem; padding:.1rem .5rem; border-radius:999px;
    background:var(--shuttle); color:var(--ink); font-size:.78rem; font-weight:800; }
  @media (max-width:560px) { .plan-cards { grid-template-columns:1fr; } }
  /* Order panel hierarchy (SHUTTLETAB-27): price large and bold, the cadence
     sentence beneath it smaller but still prominent, the scope sentence at
     body size, and the terms muted and small — largest fact first. */
  .order-price { font-size:1.5rem; font-weight:800; margin:0 0 .2rem; }
  .order-cadence { font-size:1.05rem; font-weight:600; color:var(--muted); margin:0 0 1rem; }
  .order-scope { margin:0 0 1rem; }
  .order-terms { color:var(--muted); font-size:.85rem; margin:0 0 .4rem; }
  .form-submit { display:flex; align-items:center; gap:1rem; padding:1.25rem; background:var(--court-tint-14); }
  .form-submit button { background:var(--shuttle); color:var(--ink); font-weight:900; }
  .form-submit span { color:var(--muted); font-size:.85rem; }
  /* The rule above is more specific than .saved-flag and would grey the
     confirmation into the hint beside it. The tick is not a hint. */
  .form-submit .saved-flag { color:var(--court-shade-2); font-size:.9rem; }
  @media (max-width:600px) {
    .schedule-strip,.form-grid,.repeat-rows { grid-template-columns:1fr; }
    .full-width { grid-column:auto; }
    .court-block-row { grid-template-columns:1fr 1fr; }
    .court-block-time { grid-column:1/-1; padding:0; }
    .form-submit { align-items:stretch; flex-direction:column; }
    .form-submit button { width:100%; }
  }
  @media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }

  /* Reference-grounded organizer UI: court-green utility with shuttle-yellow actions. */
  :root { font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
    --accent-shade-1:#d3a900; --accent-shade-2:#d9b42d; --accent-shade-3:#e6bd7a; --accent-shade-4:#ffd95e; --chalk:#f8fbf9; --court:#006e84; --court-dark:#005061; --court-dark-rgb:0,80,97; --court-rgb:0,110,132; --court-shade-1:#0f3c44; --court-shade-1-rgb:15,60,68; --court-shade-2:#00494a; --court-shade-3:#004a56; --court-shade-4:#005f74; --court-shade-5:#356a79; --court-shade-6:#3b879a; --court-tint-1:#708488; --court-tint-10:#dbe7e8; --court-tint-11:#dce7e9; --court-tint-12:#e1ecee; --court-tint-13:#ecf1f2; --court-tint-14:#eaf3f2; --court-tint-15:#ebf3f4; --court-tint-16:#ecf3f4; --court-tint-17:#e3f7f8; --court-tint-18:#e9f8fa; --court-tint-19:#f0f7f8; --court-tint-2:#9cafb5; --court-tint-20:#f3f8f9; --court-tint-21:#fbfcfb; --court-tint-3:#c3d5d8; --court-tint-4:#cad8db; --court-tint-5:#a9dfe8; --court-tint-6:#cedddf; --court-tint-7:#97e9ed; --court-tint-8:#d4e0e2; --court-tint-9:#d8e6e8; --ink:#123037; --line:#d6e1e3; --muted:#5f7479; --shuttle:#f2c200; --shuttle-rgb:242,194,0; --surface:#fff;
  }
  body { background:
      linear-gradient(rgba(var(--court-rgb),.035) 1px,transparent 1px),
      linear-gradient(90deg,rgba(var(--court-rgb),.035) 1px,transparent 1px),var(--court-tint-16);
    background-size:32px 32px; }
  /* Pinned on every signed-in screen. overflow:hidden is what clips the dashed
     circle drawn by ::after, so it stays — a sticky element clips its own
     decoration fine. It DOES clip children, and since SHUTTLETAB-34 there is
     one that must escape: the operator bell's panel. That panel is
     position:fixed in OPERATOR_CSS for exactly this reason, because a fixed
     box is laid out against the viewport and an overflow ancestor therefore
     does not clip it. Do not "fix" that rule away. */
  .site-header { position:sticky; top:0; z-index:30; padding:0; border:0; overflow:hidden;
    background:linear-gradient(145deg,var(--court),var(--court-dark));
    box-shadow:0 5px 20px rgba(var(--court-dark-rgb),.2); }
  .site-header::before { position:absolute; inset:8px 14px; border:1px solid rgba(255,255,255,.2);
    border-radius:10px; pointer-events:none; content:""; }
  .site-header::after { position:absolute; width:190px; height:190px; right:-85px; top:-125px;
    border:2px dashed rgba(var(--shuttle-rgb),.58); border-radius:50%; pointer-events:none; content:""; }
  .site-header-inner { position:relative; z-index:1; width:min(100%,1000px); min-height:68px; margin:auto;
    padding:12px 18px; display:flex; align-items:center; justify-content:space-between; gap:1rem; }
  .site-header .brand { display:inline-flex; align-items:center; gap:.65rem; font-family:"Trebuchet MS",Inter,sans-serif;
    font-size:1.24rem; letter-spacing:-.045em; }
  .brand-mark { display:grid; width:36px; height:36px; place-items:center; border-radius:50%;
    background:var(--shuttle); color:var(--ink); box-shadow:0 4px 0 #a98600; font-size:1.05rem; }
  .brand strong { font-weight:950; }
  .site-header nav { gap:.55rem; font-size:.86rem; font-weight:750; }
  .site-header nav > a,.header-action { min-height:36px; padding:.45rem .72rem; border:1px solid rgba(255,255,255,.26);
    border-radius:9px; display:inline-flex; align-items:center; background:rgba(255,255,255,.08); }
  .site-header .header-cta { border-color:var(--accent-shade-1); background:var(--shuttle); color:var(--ink); }
  .site-header .header-action { color:#fff; font-size:.86rem; }
  /* Square and thumb-sized: the label is gone, so the target has to carry the
     whole affordance. */
  .site-header .header-icon { display:grid; place-items:center; width:38px; height:38px;
    min-height:38px; padding:0; }
  .site-header .header-icon svg { display:block; }
  /* Header billing chip (SHUTTLETAB-27): trial/lapsed reads as a call to
     action (same yellow as .header-cta beside it); a live paid subscription
     stays the neutral outlined pill every other nav link already is. Kept on
     one line so "See plans"/"Subscribe" never orphans to a second row; on a
     phone the whole chip drops below the brand (the nav already wraps) rather
     than the label breaking mid-pill. */
  .site-header .header-billing { white-space:nowrap; }
  .site-header .header-billing-cta { border-color:var(--accent-shade-1); background:var(--shuttle); color:var(--ink); }
  .nav-user { max-width:230px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
    color:rgba(255,255,255,.78); font-weight:600; }

  main { width:min(100%,960px); max-width:none; padding:26px 18px 64px; }
  /* The group dashboard is a set of independent widgets rather than one
     reading column, so it earns extra width — and only it, hence the :has(). */
  main:has(.dashboard-grid) { width:min(100%,1180px); }
  .card-heading { position:relative; display:flex; align-items:center; gap:.5rem; margin:0 0 .75rem; }
  .card-heading h2 { margin:0; }
  /* A subsection inside a card (the QR block under "How players pay you"):
     same heading row, one size down, ruled off from the form above it. */
  .card-subheading { margin-top:1.4rem; padding-top:1rem; border-top:1px solid var(--line); }
  .card-subheading h3 { margin:0; font-size:1.05rem; }
  /* Folded card (foldedCard, src/views/sessions.ts): a whole card behind its
     own heading, built on <details> rather than a button — the reason every
     other disclosure here is one: the page ships no JavaScript, and the
     browser's own control keeps keyboard and screen-reader behaviour for free.
     The card padding stays on the <details>, so an open card is
     indistinguishable from the plain .card it replaced. */
  .fold > summary { list-style:none; cursor:pointer; display:flex; align-items:center;
    gap:.6rem; margin:0; }
  .fold > summary::-webkit-details-marker { display:none; }
  .fold > summary > :first-child { margin:0; }
  /* The marker is drawn, not typed: a rotating chevron built from a border pair
     stays crisp at any size and needs no font that has the glyph. It is last in
     the row and pushed there, so headings of different lengths still line their
     markers up down the page. */
  .fold > summary::after { content:""; margin-left:auto; flex:none; width:.5rem; height:.5rem;
    border-right:2px solid var(--muted); border-bottom:2px solid var(--muted);
    transform:rotate(-45deg); transition:transform .15s ease; }
  .fold[open] > summary::after { transform:rotate(45deg); }
  @media (prefers-reduced-motion:reduce) { .fold > summary::after { transition:none; } }
  .fold > summary:hover::after { border-color:var(--court); }
  .fold > summary:focus-visible { outline:3px solid #4c8bf5; outline-offset:2px; border-radius:4px; }
  /* What a closed card still says: how many rows are inside it. Without this a
     folded roster hides the one fact the page is read for. */
  /* Sections inside a folded card (the cost card's prices, shuttles, extras,
     totals and who-to-pay). Ruled off from one another the way
     .card-subheading rules off a heading, so one long card still reads as
     parts rather than as one run-on form. */
  .card-part + .card-part,
  /* The who's-playing card opens on its roster table, and the controls that
     change that roster are the part under it. */
  .table-scroll + .card-part { margin-top:1.4rem; padding-top:1rem; border-top:1px solid var(--line); }
  .card-part > h3 { margin:0 0 .6rem; font-size:1.05rem; }
  .fold-count { flex:none; padding:.1rem .5rem; border-radius:999px; background:var(--shuttle);
    color:var(--ink); font-size:.78rem; font-weight:800; }
  /* The card's own top padding already separates the heading, so the body only
     needs space below the summary it was folded under. */
  .fold[open] > summary { margin-bottom:1rem; }
  .field-label { position:relative; display:flex; align-items:center; gap:.4rem; }
  .field-label label { margin:0; }
  .hint-tip { position:relative; }
  .hint-tip > summary { list-style:none; cursor:pointer; width:22px; height:22px; border-radius:50%;
    border:1px solid var(--line); display:inline-flex; align-items:center; justify-content:center;
    font-size:.78rem; font-weight:800; color:var(--muted); background:var(--surface); }
  .hint-tip > summary::-webkit-details-marker { display:none; }
  .hint-tip > summary:hover,.hint-tip[open] > summary { border-color:var(--court); color:var(--court); }
  /* Click-anywhere-else closes the tip. An open tip lays an invisible full-viewport
     sheet over the page as part of its own summary, so a click outside the bubble
     lands on the summary and toggles the disclosure shut — the dismissal a reader
     expects from a popover, with no JavaScript (the CSP forbids inline script) and
     no change to the markup. The bubble sits above the sheet, so clicking inside it
     does nothing; the sheet is fixed, so the page still scrolls under a drag. The
     one cost: with a tip open, the first click elsewhere only dismisses, so opening
     a second tip takes two clicks. */
  .hint-tip[open] > summary::before { content:""; position:fixed; inset:0; z-index:19;
    cursor:default; }
  /* Escapes the card rather than reflowing it, so opening a tip never moves the
     controls under the reader's finger. */
  .hint-bubble { position:absolute; z-index:20; top:calc(100% + .4rem); left:0;
    width:min(22rem,calc(100vw - 3rem)); margin:0; padding:.7rem .8rem; border:1px solid var(--line);
    border-radius:10px; background:var(--surface); box-shadow:0 10px 24px rgba(var(--court-shade-1-rgb),.14);
    color:var(--muted); font-size:.85rem; font-weight:500; line-height:1.45; }
  /* A bubble anchored to a 22px target runs off a phone screen, and nothing but
     JavaScript could measure its way back. So on narrow screens it anchors to
     the whole heading row instead and spans the card — still floating, so the
     controls underneath never move. */
  @media (max-width:700px) {
    .hint-tip { position:static; }
    .hint-bubble { left:0; right:0; width:auto; }
  }
  /* Its own line under the choice, so the labels scan as a list of options
     rather than three sentences. */
  .choice-note { display:block; color:var(--muted); font-size:.82rem; font-weight:500;
    line-height:1.4; }
  .dashboard-grid { display:grid; gap:1.15rem; align-items:start; margin-top:1.15rem; }
  .dashboard-grid > * { margin:0; }
  /* Banners speak to the whole page, so they never sit in a column. */
  .dashboard-grid > .dashboard-full { grid-column:1/-1; }
  @media (min-width:1000px) {
    .dashboard-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
  }
  h1,h2,h3 { font-family:"Trebuchet MS",Inter,sans-serif; color:var(--ink); }
  h1 { margin:.25rem 0 1rem; font-size:clamp(2rem,5vw,3.2rem); font-weight:950; }
  h2 { font-size:1.35rem; letter-spacing:-.025em; }
  a { color:var(--court-dark); text-underline-offset:3px; }
  form.card,.card,.empty-state { border:1px solid var(--line); border-radius:14px; background:var(--surface);
    box-shadow:0 8px 26px rgba(var(--court-shade-1-rgb),.06); padding:clamp(16px,3vw,22px); }
  .card h2:first-child,.empty-state h2:first-child { margin-top:0; }
  .auth-shell { width:min(100%,460px); margin:clamp(2rem,8vh,5rem) auto 0; }
  .auth-intro { margin-bottom:1.15rem; }
  .auth-intro h1 { margin:.15rem 0 .55rem; }
  .auth-intro p:last-child { margin:0; color:var(--muted); }
  .auth-card { margin-bottom:1rem; }
  .auth-card button { width:100%; background:var(--shuttle); border-color:var(--accent-shade-1); color:var(--ink); }
  .auth-switch { text-align:center; font-size:.9rem; }
  /* Pins directly under the header, whose height /app.js measures into
     --header-h: the header wraps to two rows on a narrow phone, so a written-
     down offset would leave a gap or an overlap on exactly those screens. The
     fallback keeps the rule sane for the one frame before the script runs, and
     forever if it never does. */
  /* display:flex is restated because the pre-restyle stylesheet still turns
     this into a 4-column grid under 700px (a rule from when the nav had four
     tabs), which would make .nav-links one narrow grid cell instead of the
     row it now is. */
  .group-nav { position:sticky; top:var(--header-h,68px); z-index:20; display:flex;
    gap:.35rem; margin:0 0 1.8rem; padding:.35rem; border:1px solid var(--line);
    border-radius:12px; background:rgba(255,255,255,.94); box-shadow:0 5px 18px rgba(var(--court-shade-1-rgb),.04); }
  .group-nav a { flex:1; padding:.65rem .85rem; border:0; border-radius:8px; color:var(--muted);
    text-align:center; font-size:.86rem; font-weight:800; }
  .group-nav a[aria-current="page"] { border:0; background:var(--court); color:#fff; }
  /* The group switcher (#56). The pre-restyle sheet above makes .group-nav a
     horizontal scroll container, from when its tabs could overflow; that clips
     an absolutely positioned menu to a sliver, and the restyled nav gives every
     tab flex:1 and never scrolls, so the scroller is dead weight wherever the
     switcher renders. */
  .group-nav:has(.nav-switch) { overflow:visible; }
  .nav-switch { position:relative; flex:none; }
  .nav-switch > summary { display:inline-flex; align-items:center; gap:.45rem; min-height:38px;
    padding:.5rem .7rem; border-radius:8px; background:var(--court-tint-19); color:var(--court-dark);
    font-size:.86rem; font-weight:800; cursor:pointer; list-style:none; max-width:15rem;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .nav-switch > summary::-webkit-details-marker { display:none; }
  .nav-switch > summary::after { content:""; flex:none; width:.4rem; height:.4rem;
    border-right:2px solid currentColor; border-bottom:2px solid currentColor;
    transform:rotate(45deg) translateY(-2px); }
  .nav-switch[open] > summary { background:var(--court); color:#fff; }
  .nav-switch-menu { position:absolute; top:calc(100% + .4rem); left:0; z-index:40; min-width:16rem;
    padding:.35rem; border:1px solid var(--line); border-radius:10px; background:var(--surface);
    box-shadow:0 14px 34px rgba(var(--court-dark-rgb),.22); }
  /* Overrides the tab rules above, which match every anchor in the nav. */
  .nav-switch-menu a { flex:none; display:block; padding:.55rem .65rem; border-radius:7px;
    color:var(--ink); font-size:.9rem; font-weight:650; text-align:left; }
  .nav-switch-menu a:hover { background:var(--court-tint-19); }
  .nav-switch-menu a[aria-current="true"] { background:var(--court); color:#fff; }
  .nav-switch-menu hr { margin:.35rem .2rem; border:0; border-top:1px solid var(--line); }
  .nav-switch-menu .switch-new { color:var(--court); font-weight:800; }
  .nav-switch-menu .switch-note { display:block; font-size:.74rem; font-weight:600; opacity:.74; }
  /* The burger only exists when /app.js made it, so it is hidden until the
     condensed state asks for it rather than needing a no-JS rule of its own. */
  .nav-burger { display:none; min-height:38px; padding:.4rem .6rem; border:1px solid var(--line);
    border-radius:9px; background:#fff; color:var(--court-dark);
    box-shadow:0 5px 16px rgba(20,40,32,.14); }
  .nav-burger:hover { background:var(--court-tint-20); }
  .nav-burger svg { display:block; }
  .nav-links { display:flex; flex:1; gap:.35rem; min-width:0; }
  /* Condensed: the row shrinks to the burger alone, still pinned under the
     header, and the links become one panel it drops. The panel is positioned
     against the nav rather than the viewport, so it lands under the burger at
     any header height, and it floats, so opening it never moves the page
     underneath the reader's thumb. */
  /* Condensed, the band itself goes: a full-width white bar holding one small
     button was more chrome than the button it framed, and it hid a strip of
     the list behind it. The nav keeps its sticky box so the panel still has
     something to anchor to and the burger still has its place, but paints
     nothing — the button carries its own surface and shadow. */
  [data-nav-condensed] .group-nav { padding:0; border:0; border-radius:0; background:none;
    box-shadow:none; pointer-events:none; }
  [data-nav-condensed] .group-nav .nav-burger,
  [data-nav-condensed] .group-nav .nav-switch,
  [data-nav-condensed] .group-nav .nav-links { pointer-events:auto; }
  [data-nav-condensed] .group-nav .nav-burger { display:inline-flex; align-items:center; }
  [data-nav-condensed] .nav-links { display:none; }
  [data-nav-condensed] .group-nav[data-open] .nav-links { display:block; position:absolute;
    top:calc(100% + .3rem); left:0; right:0; flex:none; padding:.35rem; border:1px solid var(--line);
    border-radius:12px; background:#fff; box-shadow:0 14px 30px rgba(20,40,32,.18); }
  /* Rows, not a line of text: an <a> is inline by default, so in the panel's
     block container all three would run together on one line. */
  [data-nav-condensed] .group-nav[data-open] .nav-links a { display:block; text-align:left; }
  label { margin-bottom:.35rem; color:var(--muted); font-size:.72rem; font-weight:850;
    letter-spacing:.055em; text-transform:uppercase; }
  /* Optional is supporting information, not part of the label's all-caps
     emphasis. Keep it at the label size so it reads cleanly beside the name. */
  label .hint { font-size:inherit; font-weight:400; font-style:italic; letter-spacing:normal;
    text-transform:none; }
  input,select,textarea { min-height:44px; padding:.7rem .8rem; border:1px solid var(--court-tint-3); border-radius:9px;
    outline:0; background:var(--court-tint-21); color:var(--ink); }
  /* Radios and checkboxes are not text fields: the sizing above would render a
     44px padded box around a 13px control. */
  input[type="radio"],input[type="checkbox"] { width:auto; min-height:0; padding:0; border:0;
    border-radius:0; background:none; accent-color:var(--court); }
  textarea { min-height:110px; resize:vertical; }
  input:focus,select:focus,textarea:focus { border-color:var(--court); box-shadow:0 0 0 3px rgba(var(--court-rgb),.13); }
  button,.button-link,.site-header button { min-height:40px; padding:.62rem .92rem; border:1px solid var(--court-dark);
    border-radius:9px; background:var(--court); color:#fff; font-weight:800; transition:transform .13s ease,background .13s ease; }
  button:hover,.button-link:hover { background:var(--court-dark); transform:translateY(-1px); }
  button.secondary,.button-link.secondary { border-color:var(--court-tint-4); background:#fff; color:var(--court); }
  button.secondary:hover,.button-link.secondary:hover { background:var(--court-tint-20); }
  .form-submit button,.header-cta { border-color:var(--accent-shade-1); background:var(--shuttle); color:var(--ink); }
  button.danger { border-color:#e2c2be; background:#fff; color:#a12a20; }
  button.danger:hover { background:#fff3f1; }
  /* Between secondary and danger: an action with real consequences that is
     undone by the button that replaces it. Closing sign-ups is the one control
     in the danger zone that reverses in a single click, so it stops short of
     the red that cancelling earns. */
  button.warn { border-color:var(--accent-shade-3); background:#fff; color:#a05a06; }
  button.warn:hover { background:#fff8ee; }
  button.compact,.button-link.compact { min-height:34px; padding:.35rem .65rem; font-size:.8rem; }
  /* Touch targets: a 34px control is below the 44px guideline, and these are
     destructive actions (revoke, remove) that must not be easy to mis-tap. */
  @media (pointer:coarse) {
    button.compact,.button-link.compact { min-height:44px; padding:.5rem .8rem; }
  }
  .actions { align-items:center; gap:.55rem; }
  /* Square, thumb-sized targets: this row is used mostly on phones, where four
     text buttons wrapped onto two lines. */
  .icon-button { display:grid; place-items:center; width:44px; height:44px; min-height:44px;
    padding:0; border-radius:12px; }
  .icon-button svg { display:block; }
  .icon-button.danger { border-color:#e2c2be; background:#fff; color:#a12a20; }
  .icon-button.danger:hover { background:#fff3f1; }
  .notice,.error-summary { border-radius:11px; padding:.85rem 1rem; }
  .notice { border-color:var(--court-tint-5); background:var(--court-tint-18); color:var(--court-shade-3); }
  .empty-state { border-style:dashed; border-width:2px; text-align:center; }
  .table-scroll { border:1px solid var(--line); border-radius:12px; background:#fff; }
  /* The one-line stand-in for the court table when every block agrees — same
     card chrome, minus a header row that would only label a single value.
     See courtPlanTable in views/sessions.ts. */
  .court-plan-summary { margin-bottom:1.5rem; padding:.85rem 1rem; border:1px solid var(--line);
    border-radius:12px; background:#fff; }
  .court-plan-summary h3 { margin:0; font-size:1rem; font-weight:900; }
  .court-plan-summary p { display:flex; flex-wrap:wrap; gap:.1rem .6rem; margin:.3rem 0 0;
    color:var(--muted); font-size:.9rem; }
  .court-plan-summary strong { color:var(--ink); font-weight:800; }
  table { margin:0; font-size:.9rem; }
  caption { padding:.85rem 1rem; color:var(--ink); text-align:left; font-family:"Trebuchet MS",Inter,sans-serif;
    font-size:1rem; font-weight:900; }
  th { padding:.58rem .7rem; background:var(--court-tint-20); color:var(--court-tint-1); font-size:.66rem;
    text-transform:uppercase; letter-spacing:.06em; }
  td { padding:.7rem; border-bottom-color:var(--court-tint-13); }
  tbody tr:last-child td { border-bottom:0; }
  .page-heading { margin:1.1rem 0 1.6rem; }
  .kicker { color:var(--court); }
  .session-sheet { border-radius:15px; border-color:var(--line); border-top-color:var(--court);
    box-shadow:9px 9px 0 var(--court-tint-9); }
  .schedule-section { background:linear-gradient(145deg,var(--court),var(--court-dark)); }
  .schedule-strip { border-color:var(--court-shade-6); background:var(--court-shade-6); }
  .status-badge { padding:.18rem .56rem; font-weight:800; }
  code { background:var(--court-tint-15); color:var(--court-dark); }
  /* The one loud control per screen (docs/UI_RESTYLE_PLAN.md UI-03): the amber
     pill the reference tool uses for its single primary action. Opt-in class —
     the default button stays green so "Save prices" never competes with it. */
  button.primary,.button-link.primary { min-height:48px; padding:.72rem 1.5rem; border-color:var(--accent-shade-1);
    border-radius:999px; background:var(--shuttle); color:var(--ink); font-size:1.02rem; font-weight:900; }
  button.primary:hover,.button-link.primary:hover { background:var(--accent-shade-4); }
  /* Sessions as fat rows, the reference tool's list anatomy: bold weekday-first
     date, muted detail line, count column, the whole row one link (UI-04). */
  .session-rows-head { display:flex; justify-content:space-between; margin:0 0 .35rem; padding:0 1.1rem;
    color:var(--court-tint-1); font-size:.66rem; font-weight:800; letter-spacing:.06em; text-transform:uppercase; }
  .session-rows { list-style:none; margin:0 0 1.5rem; padding:.3rem 1.1rem; border:1px solid var(--line);
    border-radius:14px; background:var(--surface); box-shadow:0 8px 26px rgba(var(--court-shade-1-rgb),.06); }
  .session-rows li { display:flex; align-items:center; gap:.75rem; border-bottom:1px solid var(--court-tint-13); }
  .session-rows li:last-child { border-bottom:0; }
  .session-row { flex:1; display:flex; align-items:center; gap:1rem;
    min-width:0; padding:1rem .15rem; color:var(--ink); text-decoration:none; }
  .session-controls { display:flex; flex:0 0 auto; align-items:center; gap:.75rem; }
  .session-actions { display:flex; flex:0 0 auto; gap:.4rem; }
  .players-word { display:none; }
  .session-row strong { display:block; font-family:"Trebuchet MS",Inter,sans-serif; font-size:1.2rem;
    font-weight:900; letter-spacing:-.02em; }
  .session-row .session-sub { display:block; margin-top:.12rem; color:var(--muted); font-size:.85rem; }
  .session-when .status-badge { display:inline-block; margin-top:.4rem; }
  /* A cancelled row (user call, 2026-08-25). The red pill alone sat below the
     date and was easy to skim past on a phone; muting the row and striking the
     date says "not happening" while scrolling. The strike goes on the date and
     time only — never on the venue or the badge, which still have to be read.
     Kept above AA contrast, because a cancelled session is still a link people
     open to settle its costs. */
  .session-row.is-cancelled strong,
  .session-row.is-cancelled .session-sub { color:var(--muted); }
  .session-row.is-cancelled strong { text-decoration:line-through;
    text-decoration-thickness:2px; text-decoration-color:#c0392b; }
  .session-row.is-cancelled:hover strong { text-decoration:line-through underline;
    text-underline-offset:3px; }
  .session-players { flex:0 0 auto; min-width:2.4rem; color:var(--ink); font-size:1.18rem;
    font-weight:900; font-variant-numeric:tabular-nums; text-align:center; }
  .session-row:hover strong { text-decoration:underline; text-underline-offset:3px; }
  /* Small screens (variant B of the mobile-row review, user pick): the text
     block keeps the full width and the controls move into a tinted footer
     strip — switch first, "N playing" beside it, icons at the far end. */
  @media (max-width:640px) {
    /* The stacked row explains itself, so the DATE / PLAYERS column labels
       point at nothing here (user call). */
    .session-rows-head { display:none; }
    .session-rows li { display:block; }
    .session-rows .session-row { padding:.9rem 0 .6rem; }
    .session-controls { margin:0 -1.1rem; padding:.5rem 1.1rem; background:var(--court-tint-19);
      border-top:1px solid var(--court-tint-12); }
    .visibility-toggle { order:-1; }
    .session-actions { margin-left:auto; }
    .session-rows .session-players { min-width:0; text-align:left; color:var(--muted);
      font-size:.82rem; font-weight:700; }
    .players-word { display:inline; }
  }
  /* The dashboard's "needs attention" list (SHUTTLETAB-13 M3): one row per
     session, its reasons beneath it as inline chips that wrap to a second
     line once they overflow the row's width — a row can carry two reasons
     and each goes somewhere different (ledger vs. session page), so they are
     not one link. */
  .attention-rows { list-style:none; margin:0; padding:0; }
  .attention-row { padding:.6rem 0; border-bottom:1px solid var(--court-tint-13); }
  .attention-row:last-child { border-bottom:0; }
  .attention-row-title { margin:0; display:flex; flex-wrap:wrap; align-items:baseline; gap:.5rem; }
  .attention-row-when { color:var(--muted); font-size:.85rem; }
  .attention-reasons { list-style:none; margin:.3rem 0 0; padding:0; display:flex; flex-wrap:wrap; gap:.4rem 1rem; font-size:.9rem; }
  /* The sessions list's filter row: one GET form whose selects apply
     themselves (/app.js); the Apply button exists for a reader with no
     JavaScript, and the [data-app-js] marker set by the script hides it.
     Phone: a two-column grid — When beside Visibility, From beside To,
     the venue select on its own full row (user call, 2026-08-24). */
  .session-filters { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
    gap:.7rem .75rem; padding:.9rem 1rem; margin-bottom:1.25rem; }
  /* Venue takes a full row, so the pairs that belong together stay together:
     Visibility beside Status, When beside Sort, then From beside To (user
     call, 2026-08-25 — the point was that the date range must not be split
     across rows, which an odd number of preceding fields would do). */
  .session-filters .filter-venue { grid-column:1/-1; }
  /* The series filter's hidden input plus its clear link (F-1-3): link-only
     entry, so it takes a full row rather than fighting a paired field for
     half of one. Scoped like .filter-venue above, and given a flex basis too
     — past the 641px breakpoint the row is flex, where grid-column is
     inert. */
  .session-filters .filter-series-notice { grid-column:1/-1; flex:1 1 100%; margin:0; }
  .filter-field { display:flex; flex-direction:column; margin:0; min-width:0; }
  .filter-field > span { margin-bottom:.3rem; }
  /* One explicit height for every control in the row: iOS gives a date input
     its own tall intrinsic box, which broke the label alignment beside the
     selects. appearance:none stops Safari sizing the date input to its UA
     widget (which also overflowed the grid column into the From/To gap). */
  .filter-field select,.filter-field input { height:44px; min-height:44px; padding:.4rem .6rem;
    width:100%; min-width:0; }
  .filter-field input[type="date"] { -webkit-appearance:none; appearance:none; display:block; }
  /* An empty iOS date input renders as a blank sliver without this. */
  .filter-field input[type="date"]::-webkit-date-and-time-value { text-align:left; margin:0; }
  .filter-apply { justify-self:start; align-self:end; }
  [data-app-js] .filter-apply { display:none; }
  @media (min-width:641px) {
    .session-filters { display:flex; flex-wrap:wrap; align-items:flex-end; gap:.7rem 1rem; }
    .filter-field { flex:1 1 8rem; }
  }
  /* The sessions list's pager (F-4-4). "Newest" sits left and "Older" right,
     so the pair reads in the direction the list itself runs. margin-left:auto
     on Older rather than space-between on the row, because either link can be
     absent: alone, Older must still sit right and Newest must still sit left. */
  .session-pager { display:flex; gap:1rem; margin:1.25rem 0 0; padding:0 .25rem; }
  .session-pager .pager-older { margin-left:auto; }
  .status-badge.hidden-badge { background:var(--court); color:#fff; }
  /* The toggle switch (docs/SESSION_VISIBILITY_PLAN.md D5): a submit button or
     a checkbox drawn as a track and knob, so it works with no JavaScript. */
  .switch,input[type="checkbox"].switch { appearance:none; -webkit-appearance:none; position:relative;
    display:inline-block; flex:0 0 auto; width:46px; height:26px; min-height:26px; margin:0; padding:0;
    border:1px solid var(--court-tint-3); border-radius:999px; background:var(--court-tint-11); cursor:pointer;
    transition:background .15s ease,border-color .15s ease; }
  .switch::after { content:""; position:absolute; top:2px; left:2px; width:20px; height:20px;
    border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(20,40,32,.3);
    transition:transform .15s ease; }
  .switch.on,input[type="checkbox"].switch:checked { border-color:var(--court-dark); background:var(--court); }
  .switch.on::after,input[type="checkbox"].switch:checked::after { transform:translateX(20px); }
  button.switch:hover { background:var(--court-tint-6); transform:none; }
  button.switch.on:hover { background:var(--court-dark); }
  .switch-row { display:flex; align-items:center; gap:.6rem; }
  .cancel-session { margin:1.1rem 0 1.6rem; }
  /* The foot-of-page card for what cannot be undone in a click, folded shut by
     default. A red edge rather than a red fill: the controls inside carry their
     own colour, and a wash behind them would leave the reopen button shouting
     as loudly as the close one. Its children are already .card forms, so those
     are flattened — a card inside a card reads as two separate offers. */
  .danger-zone { border-color:#e2c2be; border-left:4px solid #a12a20; }
  /* One rule owns the spacing between the parts, so a control that arrived
     with its own margin (.cancel-session did) cannot open a gap the separator
     line does not follow. */
  .danger-zone > * { margin:0; }
  .danger-zone > .card { border:0; border-radius:0; background:none; box-shadow:none; padding:0; }
  .danger-zone > * + * { margin-top:1.3rem; padding-top:1.3rem; border-top:1px solid var(--line); }
  /* The rule above cannot draw the divider over a flattened card on its own:
     the reset is a two-class selector and the separator a one-class one, so the
     reset wins on specificity whichever order they are written in. This states
     the same thing at matching specificity, for the one child that needs it. */
  .danger-zone > * + .card { padding-top:1.3rem; border-top:1px solid var(--line); }

  /* The heading now lives in the fold's summary, so the red is on that h2 and
     on the disclosure arrow beside it — a closed card has nothing else to say
     what it holds. */
  .danger-zone > summary h2 { color:#a12a20; }
  /* One action per part: what it is called, what it does, then the control.
     The button used to carry all three jobs at once, which is how it came to
     read "Close sign-ups now" — a label explaining itself because nothing else
     was there to. With the sentence above it, one word does. */
  .danger-zone h3 { margin:0 0 .25rem; font-size:1.02rem; }
  .danger-zone .hint { margin:0 0 .85rem; max-width:52ch; }
  .danger-part .cancel-session { display:block; margin:0; }
  .danger-part .cancel-series { display:block; margin:.9rem 0 0; }
  .danger-zone > summary::after { border-color:#a12a20; }
  .session-heading { display:flex; align-items:flex-start; justify-content:space-between;
    gap:1rem; margin:1.1rem 0 .4rem; }
  .session-heading h1 { margin:0; }
  .session-heading-time { display:block; margin-top:.15rem; color:var(--muted);
    font-size:.45em; font-weight:800; letter-spacing:0; }
  .legal-copy { max-width:44rem; }
  .legal-copy h2 { margin:1.5rem 0 .4rem; font-size:1.05rem; }
  .legal-copy p { margin:.55rem 0; }
  .visibility-toggle { display:flex; align-items:center; margin:0; padding:0; border:0; background:none; box-shadow:none; }
  @media (prefers-reduced-motion:reduce) { .switch,.switch::after { transition:none; } }
  @media (max-width:640px) {
    .site-header-inner { align-items:flex-start; padding:14px 16px; }
    .site-header nav { justify-content:flex-end; }
    .nav-user { display:none; }
    main { padding:20px 14px 50px; }
    /* Equal columns on a phone, on the links row rather than on .group-nav
       itself: the nav is now a flex row holding the burger *and* that row, and
       making the nav a 3-column grid put the links into one narrow cell. */
    .nav-links { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); }
    .group-nav a { padding:.6rem .25rem; }
    /* The switcher takes its own row on a phone rather than stealing width from
       three tabs that are already at their narrowest. */
    .group-nav:has(.nav-switch) { flex-wrap:wrap; }
    .nav-switch,.nav-switch > summary { width:100%; max-width:none; }
    .nav-switch-menu { right:0; }
    .nav-switch-menu a { padding:.6rem .65rem; }
    .actions > form,.actions > a { flex:1 1 auto; }
    .actions button,.actions .button-link { width:100%; }
    /* Icon actions stay square rather than stretching into wide blank slabs. */
    .actions > form:has(.icon-button),.actions > a.icon-button { flex:0 0 auto; }
    .actions .icon-button { width:44px; }
    th,td { padding:.58rem; }
  }
  @media (prefers-reduced-motion:reduce) { button,.button-link { transition:none; } }

  /* Self-contained: this menu renders on a light footer in the app and on the dark
     court on the player page, so its palette is defined here rather than inherited
     from either document's :root. */
  .lang-menu { position:relative;
    --menu-court-shade-1:#123037; --menu-court-shade-2:#0f3c44; --menu-court-shade-2-rgb:15,60,68; --menu-court-shade-3:#006e84; --menu-court-tint-1:#374f54; --menu-court-tint-2:#cad8db; --menu-court-tint-3:#f3f8f9;
  }
  .lang-menu > summary { list-style:none; display:inline-flex; align-items:center; gap:.45rem;
    cursor:pointer; padding:.35rem .65rem; border:1px solid var(--menu-court-tint-2); border-radius:9px;
    background:#fff; color:var(--menu-court-tint-1); font-weight:700; }
  .lang-menu > summary::-webkit-details-marker { display:none; }
  .lang-menu > summary:hover { border-color:var(--menu-court-shade-3); color:var(--menu-court-shade-3); }
  .lang-caret { font-size:.7em; }
  .lang-flag { width:20px; height:14px; flex:0 0 auto; border-radius:2px;
    box-shadow:0 0 0 1px rgba(0,0,0,.14); }
  .lang-menu[open] > summary::before { content:""; position:fixed; inset:0; z-index:19;
    cursor:default; }
  .lang-menu-list { position:absolute; z-index:20; bottom:calc(100% + .4rem); left:0; margin:0;
    padding:.25rem; list-style:none; min-width:9.5rem; background:#fff; border:1px solid var(--menu-court-tint-2);
    border-radius:10px; box-shadow:0 10px 24px rgba(var(--menu-court-shade-2-rgb),.18); }
  .lang-menu-list a { display:flex; align-items:center; gap:.55rem; padding:.5rem .6rem;
    border-radius:7px; text-decoration:none; color:var(--menu-court-shade-1); font-size:.9rem; }
  .lang-menu-list a:hover { background:var(--menu-court-tint-3); }
  .lang-menu-list a[aria-current="true"] { font-weight:800; }

  body.operator {
    /* Not a colour, so not in the pack: the operator dashboard's monospace stack
       is the same in every product. */
    --op-mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
    background:var(--op-sheet);
    --op-accent-shade-1:#f5c842; --op-accent-shade-1-rgb:245,200,66; --op-court-shade-1:#002a32; --op-court-shade-1-rgb:0,42,50; --op-court-shade-2:#2c4750; --op-court-shade-3:#006479; --op-court-tint-1:#88adb8; --op-court-tint-2:#99b2b9; --op-court-tint-3:#a3c1c7; --op-court-tint-4:#b4cdd2; --op-court-tint-5:#c6dee5; --op-court-tint-6:#edf2f3; --op-court-tint-7:#eff6f7; --op-court-tint-8:#f1f6f7; --op-court-tint-9:#f7faf8; --op-floor:#00242e; --op-quiet:#5c757d; --op-rule:#dce5e7; --op-sheet:#edf2f3;
  }
  .op-shell { width:min(100%,1340px); margin:0 auto; display:grid;
    grid-template-columns:214px minmax(0,1fr); gap:1.9rem; padding:1.5rem 1.5rem 4rem;
    align-items:start; }
  /* main is the content column now, so it drops BASE_CSS's own page sizing. */
  main.op-body { width:auto; max-width:none; min-width:0; margin:0; padding:0; }
  .op-rail { position:sticky; top:1.5rem; background:var(--op-floor); color:#fff;
    border-radius:14px; padding:1.05rem .85rem 1.15rem; }
  .op-rail-mark { margin:0 0 .9rem; padding:0 .6rem;
    font-family:var(--op-mono); font-size:.7rem; letter-spacing:.19em; text-transform:uppercase;
    color:var(--shuttle); }
  .op-rail-nav { display:flex; flex-direction:column; gap:.15rem; }
  .op-rail-nav a { color:var(--op-court-tint-5); text-decoration:none; font-size:.92rem; padding:.5rem .6rem;
    border-left:3px solid transparent; border-radius:0 8px 8px 0; }
  .op-rail-nav a:hover { background:rgba(255,255,255,.07); color:#fff; }
  .op-rail-nav a[aria-current="page"], .op-rail-nav a[aria-current="true"] {
    background:rgba(var(--op-accent-shade-1-rgb),.13); color:#fff; border-left-color:var(--shuttle);
    font-weight:700; }
  /* The scoreboard: the only place on the page allowed an accent colour, and
     the only figures set at display size. Everything below it is a detail of
     one of these three numbers. */
  .op-scoreboard { display:grid; grid-template-columns:repeat(3,minmax(0,1fr));
    background:var(--op-floor); color:#fff; border-radius:14px; overflow:hidden; margin:0 0 1.15rem; }
  .op-scoreboard > div { padding:1rem 1.15rem; border-left:1px solid rgba(255,255,255,.11); }
  .op-scoreboard > div:first-child { border-left:0; }
  .op-scoreboard span { display:block; font-size:.67rem; text-transform:uppercase;
    letter-spacing:.1em; font-weight:800; color:var(--op-court-tint-1); }
  .op-scoreboard strong { display:block; margin-top:.4rem; font-family:var(--op-mono);
    font-size:2.05rem; line-height:1; letter-spacing:-.03em; font-weight:600;
    font-variant-numeric:tabular-nums; }
  .op-scoreboard .op-alarm strong { color:var(--shuttle); }
  .op-scoreboard .op-quiet-figure strong { color:var(--op-court-tint-1); }
  /* max-width drops BASE_CSS's 45rem cap, a reading measure for prose. Here the
     block holds the page's own rule and (on the account page) its actions menu,
     so it takes the console's width: capped, the hairline stopped short of every
     section rule beneath it and the menu could not reach the page edge. */
  body.operator .page-heading { margin:0 0 1.15rem; max-width:none; }
  body.operator .admin-heading { border-bottom:1px solid var(--op-rule); padding-bottom:.9rem; }
  body.operator h1 { font-size:clamp(1.5rem,2.6vw,2rem); letter-spacing:-.035em; margin:.1rem 0 .3rem; }
  body.operator .kicker { font-family:var(--op-mono); font-size:.68rem; text-transform:uppercase;
    letter-spacing:.17em; color:var(--op-quiet); margin:0; }
  body.operator .page-heading p:last-child { margin:0; color:var(--op-court-shade-2); }
  /* A section header reads like a line on a score sheet: the label against a
     hairline, its own count right-aligned in the same figures the table uses. */
  body.operator .admin-section { margin:1.7rem 0; }
  body.operator .section-title { border-bottom:1px solid var(--op-rule); padding-bottom:.5rem;
    margin-bottom:.85rem; }
  body.operator .section-title h2 { margin:0; font-size:1rem; letter-spacing:-.015em;
    border-left:3px solid var(--shuttle); padding-left:.6rem; }
  body.operator .section-title span { font-family:var(--op-mono); font-size:.72rem;
    color:var(--op-quiet); font-variant-numeric:tabular-nums; white-space:nowrap; }
  /* Secondary figures: one ruled sheet, not eight drop-shadowed cards. */
  /* The figure sheet. These four selectors used to live in BASE_CSS as
     drop-shadowed cards on a chalk ground; nothing outside the console renders
     a .metric any more, so they live here and ship only here. */
  .metric-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); background:#fff;
    border:1px solid var(--op-rule); border-radius:14px; overflow:hidden; margin-bottom:1.7rem; }
  .metric { display:flex; flex-direction:column; justify-content:flex-start; gap:.35rem;
    padding:.8rem 1rem .9rem; border-left:1px solid var(--op-rule);
    border-top:1px solid var(--op-rule); }
  .metric:nth-child(4n + 1) { border-left:0; }
  .metric:nth-child(-n + 4) { border-top:0; }
  .metric > span { font-size:.66rem; text-transform:uppercase; letter-spacing:.08em;
    font-weight:800; color:var(--op-quiet); }
  .metric strong { font-family:var(--op-mono); font-size:1.55rem; font-weight:600;
    letter-spacing:-.025em; line-height:1; font-variant-numeric:tabular-nums; }
  /* Panels that answer one question each sit side by side rather than adding
     two more screens of scrolling to a page that already has nine sections. */
  body.operator .op-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
    gap:0 1.9rem; }
  body.operator .op-pair > .admin-section { margin-top:0; }
  body.operator .table-scroll { border-color:var(--op-rule); border-radius:12px; margin-bottom:0; }
  body.operator table { font-size:.86rem; }
  body.operator th { background:var(--op-court-tint-8); color:var(--op-quiet); font-size:.64rem;
    letter-spacing:.08em; white-space:nowrap; }
  body.operator td { padding:.6rem .7rem; border-bottom-color:var(--op-court-tint-6); }
  body.operator tbody tr:hover { background:var(--op-court-tint-9); }
  body.operator .op-name { font-weight:700; white-space:nowrap; }
  /* Dates and counts in the score-sheet figures, so a column of them reads as
     a column: tabular-nums keeps the digits on a shared grid, and nowrap stops
     "Sep 1, 2026" from breaking across two lines in a narrow column. */
  body.operator .op-date, body.operator td.op-num { font-family:var(--op-mono); font-size:.79rem;
    font-variant-numeric:tabular-nums; white-space:nowrap; }
  /* Alignment is all a header wants from this class: with the face applied to
     th as well, "Days left" rendered monospaced at .79rem beside its sibling
     headers at .64rem sans. */
  body.operator th.op-num, body.operator td.op-num { text-align:right; }
  body.operator .status-badge { font-size:.72rem; letter-spacing:.01em; }
  body.operator .op-age { white-space:nowrap; color:var(--op-quiet); }
  /* A figure cell that is also a filter link. */
  a.metric { text-decoration:none; color:inherit; }
  a.metric:hover { background:var(--op-court-tint-9); }
  a.metric:hover strong { color:var(--court); }
  .metric-grid.op-grid-2 { grid-template-columns:repeat(2,minmax(0,1fr)); }
  .op-grid-2 .metric:nth-child(4n + 1) { border-left:1px solid var(--op-rule); }
  .op-grid-2 .metric:nth-child(odd) { border-left:0; }
  .op-grid-2 .metric:nth-child(-n + 2) { border-top:0; }
  body.operator .mix-split { margin:.7rem 0 0; font-size:.86rem; color:var(--op-quiet); }
  body.operator .mix-split a { color:var(--court-dark); }
  body.operator .op-nowrap { white-space:nowrap; }
  /* The pager. Both ends are always rendered — a spent direction as plain
     muted text rather than a missing control, so the row does not change
     width or shift the position label when you reach the last page. */
  body.operator .op-pager { display:flex; align-items:center; gap:1rem; margin:.9rem 0 0;
    padding:.55rem .1rem; }
  body.operator .op-pager a { color:var(--court-dark); font-weight:700; font-size:.88rem; }
  body.operator .op-pager > span { color:var(--op-quiet); font-size:.88rem; }
  body.operator .op-pager-position { margin-left:auto; font-family:var(--op-mono);
    font-size:.76rem; font-variant-numeric:tabular-nums; }
  /* A cell that says "nothing here yet" rather than holding a value. */
  body.operator .op-quiet-cell { color:var(--op-quiet); }
  /* A cell holding a value that needs a look — a failed or stalled scheduled
     job. The op-alarm class was scoreboard-only until the scheduled-jobs table
     needed the same emphasis inside a row, which would otherwise have been a
     class with no rule behind it. */
  body.operator td.op-alarm { color:var(--shuttle); font-weight:600; }
  /* Same emphasis for a metric tile that needs a look — the outbox panel's
     failed count. Without this the class is inert on a tile, the way it was on
     a table cell before the rule above. */
  body.operator .metric.op-alarm strong { color:var(--shuttle); }
  /* The free-access marker on an accounts-list row. Amber on the console's own
     accent rather than a red alarm: a comp is a deliberate grant, not a fault —
     it needs finding again, not fixing. Sized to the address it follows and
     given a pointer cursor, so it reads as the tooltip trigger it is. */
  body.operator .op-comp-marker { color:#8a6d1a; font-size:.9em; cursor:help; }
  body.operator .op-account-actions { margin:.7rem 0 1rem; }
  /* Drupal-style row operations: quiet while closed, then one compact docket
     of the approved account actions. The panel is styled here for its default
     (closed / no-JS) in-flow layout; admin.js switches it to position:fixed
     on open, anchored to the summary via getBoundingClientRect, because its
     table lives inside a horizontal scroller (overflow-x:auto, which forces
     overflow-y:auto too) that would otherwise clip an absolutely-positioned
     panel. Without JS the panel still opens in flow, pushing rows down. */
  body.operator .op-operations-heading, body.operator .op-operations-cell { text-align:right; }
  body.operator .op-operations-cell { vertical-align:top; }
  body.operator .op-row-actions { display:inline-block; text-align:left; }
  body.operator .op-row-actions > summary { display:inline-flex; align-items:center; gap:.35rem;
    min-height:34px; padding:.3rem .6rem; border:1px solid var(--op-rule); border-radius:8px;
    background:#fff; color:var(--court-dark); cursor:pointer; list-style:none; font-size:.8rem;
    font-weight:800; white-space:nowrap; }
  body.operator .op-row-actions > summary::-webkit-details-marker { display:none; }
  body.operator .op-row-actions > summary::after { content:"▾"; color:var(--op-quiet);
    font-size:.67rem; }
  body.operator .op-row-actions > summary:hover { border-color:var(--op-court-tint-3); background:var(--op-court-tint-9); }
  body.operator .op-row-actions[open] > summary { border-color:var(--court-dark); }
  body.operator .op-row-actions-menu { width:min(21rem,calc(100vw - 3rem)); margin-top:.35rem;
    border:1px solid var(--op-rule); border-radius:10px; background:#fff;
    box-shadow:0 12px 26px rgba(var(--op-court-shade-1-rgb),.12); overflow:hidden; z-index:20; }
  /* The header bell (SHUTTLETAB-34): the badge sits over the icon rather than
     beside it, so the summary needs the positioning context. NOTE: no
     backticks in this comment - it sits inside a tagged template. */
  /* Fixed, not merely absolute: .site-header sets overflow:hidden for its
     dashed-circle decoration, and that clips an absolutely-positioned
     descendant. A fixed box is laid out against the viewport, so the overflow
     ancestor does not clip it, and with no offsets set it renders at its
     static position - directly under the summary, which is where it belongs.
     /admin.js's positionMenu sets the same position plus real coordinates on
     open; this rule is what makes the panel visible with scripting off.
     NOTE: no backticks in this comment - it sits inside a tagged template. */
  body.operator .op-bell .op-row-actions-menu { position:fixed; }
  body.operator .op-bell > summary { position:relative; padding:.3rem .5rem; }
  body.operator .op-bell > summary::after { content:none; }
  body.operator .op-bell-badge { position:absolute; top:-.25rem; right:-.25rem; min-width:1.05rem;
    height:1.05rem; padding:0 .3rem; border-radius:999px; background:var(--shuttle);
    color:var(--op-floor); font-size:.62rem; font-weight:800; line-height:1.05rem; text-align:center; }
  body.operator .op-bell-panel { padding:.3rem 0; }
  body.operator .op-bell-item { display:flex; align-items:baseline; gap:.5rem; padding:.55rem .7rem;
    text-decoration:none; color:var(--court-dark); border-top:1px solid var(--op-rule); }
  body.operator .op-bell-item:first-child { border-top:none; }
  body.operator .op-bell-item:hover { background:var(--op-court-tint-9); }
  body.operator .op-bell-item span { flex:1 1 auto; font-size:.86rem; }
  /* The count is IN the label ("6 scheduled jobs need a look"), so the item
     renders the sentence and nothing else. A separate numeric column read as
     "6 6 scheduled jobs need a look" in the browser - invisible to the test
     suite, which asserts on the label substring and is satisfied either way.
     NOTE: no backticks in this comment - it sits inside a tagged template. */
  body.operator .op-bell-item.op-alarm { color:var(--court-dark); }
  body.operator .op-bell-empty { margin:0; padding:.7rem; font-size:.82rem; color:var(--op-quiet); }
  body.operator .op-row-actions-groups { padding:.65rem .7rem; border-top:1px solid var(--op-rule); }
  body.operator .op-row-actions-groups:first-child { border-top:none; }
  body.operator .op-row-actions-groups h3 { margin:0 0 .4rem; font-size:.7rem; font-weight:800;
    text-transform:uppercase; letter-spacing:.06em; color:var(--op-quiet); }
  body.operator .op-row-actions-groups-status { margin:0; font-size:.82rem; color:var(--op-quiet); }
  body.operator .op-row-actions-groups-list { list-style:none; margin:0; padding:0; max-height:14rem;
    overflow-y:auto; }
  body.operator .op-row-actions-groups-list li { display:flex; align-items:baseline;
    justify-content:space-between; gap:.6rem; padding:.3rem 0; }
  body.operator .op-row-actions-groups-list li + li { border-top:1px solid var(--op-rule); }
  body.operator .op-row-actions-groups-meta { color:var(--op-quiet); font-size:.76rem;
    white-space:nowrap; }
  body.operator .op-row-action-form, body.operator .op-row-action-line {
    margin:0; padding:.65rem .7rem; border-top:1px solid var(--op-rule); }
  body.operator .op-row-action-form > label:not(.op-confirm) { display:block; }
  body.operator .op-row-action-field { display:flex; align-items:center; gap:.4rem; }
  body.operator .op-row-action-field input { min-height:34px; padding:.35rem .5rem;
    font-size:.84rem; }
  body.operator .op-row-action-form .hint { margin:.35rem 0 0; font-size:.76rem; }
  body.operator .op-row-action-line { display:flex; align-items:center; justify-content:space-between;
    gap:.8rem; }
  body.operator .op-row-action-line strong, body.operator .op-row-action-line small { display:block; }
  body.operator .op-row-action-line strong { font-size:.83rem; }
  body.operator .op-row-action-line small { color:var(--op-quiet); font-size:.74rem; }
  body.operator .op-row-action-danger { background:#fffafa; }
  body.operator .op-row-actions-locked { max-width:19rem; margin:0; padding:.7rem;
    color:var(--op-quiet); font-size:.82rem; }
  body.operator .op-confirm { display:flex; flex-direction:row; align-items:flex-start; gap:.55rem;
    margin:.8rem 0; font-size:.85rem; font-weight:500; }
  body.operator .op-confirm input { width:auto; margin-top:.15rem; }
  /* The lookup field, sitting at the far end of the filter row: narrowing a
     population and resolving one address are different questions. Its label
     uses the filter-row label style, so it still reads as a control of the
     same family as the menus. */
  body.operator .op-lookup { gap:.4rem .5rem; }
  body.operator .op-lookup input { width:min(100%,17rem); min-height:2rem; padding:.25rem .5rem;
    font-size:.85rem; }
  /* Its own affordance, now that the chip rules that used to lend it one are
     gone: bare text beside a filled button reads as a caption, not a control. */
  body.operator .op-lookup-clear { padding:.25rem .5rem; border:1px solid transparent;
    border-radius:8px; font-size:.85rem; }
  body.operator .op-lookup-clear:hover { border-color:var(--op-rule); }
  @media (max-width:700px) {
    body.operator .op-lookup { width:100%; }
    body.operator .op-lookup input { width:100%; }
  }
  /* An address as a heading: long, and never worth two lines of display type. */
  body.operator h1.op-account-heading { font-size:clamp(1.2rem,2vw,1.5rem); overflow-wrap:anywhere; }
  body.operator .op-empty-page { border:1px solid var(--op-rule); border-radius:12px;
    background:#fff; padding:1rem 1.1rem; }
  body.operator .op-empty-page p { margin:0; }
  body.operator .op-empty-page .op-pager { margin-top:.6rem; padding:0; }
  /* An email field is not a paragraph: at console width the input ran the
     whole page. */
  body.operator .card .field input { max-width:26rem; }
  /* Facts, not a table: one ruled cell per field. */
  /* The rules are drawn OUTSIDE each cell, by shadow, and the container clips
     them: with auto-fit there is no way to know which cell starts a row, so
     per-cell borders doubled against the container's edge on every row after
     the first. A 1px gap over a coloured background fixes that but paints the
     last row's empty tail grey, which is worse — a shadow the container clips
     leaves exactly one line between cells and nothing at the edges. */
  body.operator .op-facts { display:grid; grid-template-columns:repeat(auto-fit,minmax(196px,1fr));
    background:#fff; border:1px solid var(--op-rule); border-radius:14px; overflow:hidden; }
  body.operator .op-fact { padding:.75rem 1rem .8rem;
    box-shadow:-1px 0 0 var(--op-rule), 0 -1px 0 var(--op-rule); }
  body.operator .op-fact > span { display:block; font-size:.64rem; text-transform:uppercase;
    letter-spacing:.08em; font-weight:800; color:var(--op-quiet); }
  body.operator .op-fact > div { margin-top:.3rem; font-weight:600; }
  body.operator .op-fact > div.op-date { font-weight:400; font-size:.85rem; }
  /* --- The account page ------------------------------------------------
     Everything below was added because this page was the one console screen
     the score-sheet vocabulary never reached: its forms wore
     op-row-action-form, which is sized for the 21rem actions docket and
     carries no container of its own. At console width that class plus
     BASE_CSS's own input width:100% rendered a five-character date field a
     thousand pixels across, one field per full-width row, on a page with
     three such forms. */
  /* The identity band. The address, the account's live status and its
     operations describe one another, so they are one line. They used to be
     three stacked blocks — heading, hairline, then a right-floated toolbar
     with a negative top margin trying to climb back up to the heading it
     belonged to. */
  body.operator .op-account-band { display:flex; flex-wrap:wrap; align-items:center;
    gap:.5rem .9rem; margin:.15rem 0 .35rem; }
  /* The badge qualifies the address, so it sits against it; only the actions
     menu is pushed to the far edge. A growing h1 put "Active" halfway across
     the console from the account it described. */
  body.operator .op-account-band h1 { flex:0 1 auto; margin:0; }
  body.operator .op-account-band > .status-badge { flex:0 0 auto; }
  body.operator .op-account-band > .op-row-actions { flex:0 0 auto; margin-left:auto; }
  /* A docket: a page-level operator form given the panel and the field grid
     the dropdown version gets from its menu. One per lever, so the boundary
     between "start manual billing" and "record a renewal" is a visible edge
     rather than a gap between two labels. */
  body.operator .op-docket { margin:.9rem 0 0; padding:.8rem .9rem .9rem;
    border:1px solid var(--op-rule); border-radius:12px; background:#fff; }
  body.operator .op-docket > h3 { margin:0; font-size:.7rem; font-weight:800;
    text-transform:uppercase; letter-spacing:.08em; color:var(--op-quiet); }
  body.operator .op-docket > .hint { margin:.45rem 0 0; font-size:.79rem; max-width:46rem; }
  body.operator .op-docket > h3 + .hint { margin-top:.35rem; }
  /* The two levers that take access away read as such without a second colour
     in the palette: the same sheet, tinted, the way op-row-action-danger
     already tints its half of the actions docket. */
  body.operator .op-docket-danger { background:#fffaf8; border-color:#ecdcd6; }
  /* Fields sized by the value they hold, capped at 48rem: past that the grid
     spaces five controls across the console rather than grouping them.
     Four explicit columns rather than auto-fill: the spans below are chosen
     against a known column count, and auto-fill silently resolved to three at
     console width, which pushed each span-2 control onto a row of its own. */
  body.operator .op-field-grid { display:grid; align-items:end; gap:.7rem .9rem;
    grid-template-columns:repeat(4,minmax(0,1fr)); max-width:48rem; margin:.85rem 0 0; }
  body.operator .op-field { display:flex; flex-direction:column; gap:.25rem; min-width:0; }
  body.operator .op-field > label { font-size:.64rem; text-transform:uppercase;
    letter-spacing:.08em; font-weight:800; color:var(--op-quiet); }
  body.operator .op-field input, body.operator .op-field select,
  body.operator .op-field textarea { width:100%; min-height:34px; padding:.35rem .5rem;
    font-size:.86rem; }
  body.operator .op-field textarea { min-height:5.5rem; resize:vertical; }
  body.operator .op-field-2 { grid-column:span 2; }
  body.operator .op-field-full { grid-column:1 / -1; }
  /* The submit row, ruled off so a docket ends where it ends — on the same
     measure as the field grid above it, so the rule stops where the last
     control stops rather than running on to the docket's own edge. */
  body.operator .op-docket-actions { display:flex; flex-wrap:wrap; align-items:center;
    gap:.6rem; max-width:48rem; margin:.9rem 0 0; padding:.75rem 0 0;
    border-top:1px solid var(--op-rule); }
  body.operator .op-docket-actions .hint { margin:0; }
  /* The one warning on this page that is not about the operator: what is typed
     into the field below it is printed verbatim on the customer's own billing
     page. It is attached to that textarea rather than floated above the
     docket, because it is a fact about that control and nothing else — and it
     takes the page's only accent, since nothing else here can be read by
     somebody who is not an operator. */
  body.operator .op-field-warn { margin:0 0 .1rem; padding:.4rem .6rem;
    border-left:3px solid var(--shuttle); background:#fffdf2; font-size:.78rem;
    line-height:1.45; color:#4b3f14; }
  /* One fact and one lever is a line, not a section body: complimentary state
     and the pending-request summary both read as a sentence against the rule. */
  body.operator .op-state-line { margin:0; font-size:.9rem; }
  body.operator .op-state-line + .op-docket { margin-top:.75rem; }
  @media (max-width:900px) {
    body.operator .op-field-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
  }
  @media (max-width:620px) {
    body.operator .op-field-grid { grid-template-columns:1fr; max-width:none; }
    body.operator .op-field-2 { grid-column:span 1; }
    body.operator .op-account-band > .op-row-actions { margin-left:0; }
  }
  /* The funnel is the one figure row that is a sequence, so it says so: an
     arrow riding the hairline between stages. Hidden once the grid wraps to
     two columns, where the arrow would point at the wrong stage. */
  .op-funnel .metric + .metric { position:relative; }
  .op-funnel .metric + .metric::before { content:"→"; position:absolute;
    left:-.63rem; top:50%; transform:translateY(-50%); width:1.26rem; text-align:center;
    background:#fff; color:var(--op-court-tint-2); font-size:.85rem; line-height:1; }
  /* The filter row, attached to the table it filters. One closed menu per
     axis: five axes of chips put 25 words above the table and made the active
     selection something to find rather than read (plan §8.8). */
  body.operator .filter-row { position:relative; background:#fff; border:1px solid var(--op-rule);
    border-radius:12px; padding:.6rem .7rem; gap:.45rem .5rem; align-items:flex-end;
    margin:1.7rem 0 .85rem; }
  /* Narrowing on the left, resolving one address on the right: the two are
     different questions and a shared edge made them read as one control. */
  body.operator .filter-row .op-lookup { margin-left:auto; }
  body.operator .op-filter { position:relative; }
  body.operator .op-filter > summary { display:flex; align-items:baseline; gap:.4rem;
    padding:.3rem .55rem; border:1px solid var(--op-rule); border-radius:9px; cursor:pointer;
    list-style:none; }
  /* Two markers to suppress: the WebKit pseudo-element and the list marker
     every other engine draws. */
  body.operator .op-filter > summary::-webkit-details-marker { display:none; }
  body.operator .op-filter > summary::after { content:"▾"; margin-left:.15rem;
    color:var(--op-quiet); font-size:.7rem; }
  body.operator .op-filter > summary:hover { border-color:var(--op-court-tint-4); }
  body.operator .op-filter[open] > summary { border-color:var(--court-dark); }
  /* The same sheet the hint tips use: an open menu covers the table it filters,
     and a native disclosure closes on nothing but its own summary. A fixed sheet
     under the panel turns the next click anywhere into a dismissal. The cost is
     the same one recorded there — with a menu open, opening a second takes two
     clicks. Escape still does not close it; only script could. */
  body.operator .op-filter[open] > summary::before { content:""; position:fixed; inset:0;
    z-index:19; cursor:default; }
  body.operator .op-filter-axis { font-size:.64rem; text-transform:uppercase; letter-spacing:.08em;
    font-weight:800; color:var(--op-quiet); }
  body.operator .op-filter-value { font-size:.85rem; font-weight:700; color:var(--court-dark);
    white-space:nowrap; }
  /* Absolute, so opening a menu does not push the table down the page; above
     the table's own stacking so the panel is not clipped by it. */
  body.operator .op-filter-menu { position:absolute; z-index:20; top:calc(100% + .3rem); left:0;
    min-width:11rem; display:flex; flex-direction:column; padding:.3rem; background:#fff;
    border:1px solid var(--op-rule); border-radius:10px;
    box-shadow:0 14px 30px rgba(var(--op-court-shade-1-rgb),.15); }
  body.operator .op-filter-menu a { padding:.3rem .5rem; border-radius:6px; font-size:.86rem;
    text-decoration:none; color:var(--court-dark); white-space:nowrap; }
  body.operator .op-filter-menu a:hover { background:var(--op-court-tint-7); }
  body.operator .op-filter-menu a[aria-current="page"] { background:var(--court-dark); color:#fff; }
  /* Narrow: the menus stay inline and wrap two or three to a row. Stretching
     each to full width would stack five rows again, which is the wall the
     menus replaced. Only the lookup takes the whole line.
     The panel, though, cannot stay anchored to its own summary: a menu in the
     right half of a wrapped row would open past the screen edge, and nothing
     but JavaScript could measure its way back. So the open menu stops being
     the positioning parent and the panel spans the row instead — the same
     answer the hint tips give on a phone. */
  @media (max-width:700px) {
    body.operator .filter-row .op-lookup { margin-left:0; width:100%; }
    body.operator .op-filter[open] { position:static; }
    body.operator .op-filter[open] .op-filter-menu { left:.55rem; right:.55rem;
      min-width:0; max-width:none; }
  }
  body.operator .card { border-color:var(--op-rule); border-radius:12px; }
  body.operator .stripe-note { margin:.6rem 0 0; }
  /* The trends sheet: same ruled-grid idiom as .metric-grid, four panels of
     small-multiple charts. Desktop-first on purpose — the console's primary
     reader sits at a computer; below console width it simply stacks. */
  .op-trend-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); background:#fff;
    border:1px solid var(--op-rule); border-radius:14px; overflow:hidden; }
  .op-trend { padding:.85rem 1rem 1rem;
    box-shadow:-1px 0 0 var(--op-rule), 0 -1px 0 var(--op-rule); min-width:0; }
  .op-trend > span { display:block; font-size:.66rem; text-transform:uppercase;
    letter-spacing:.08em; font-weight:800; color:var(--op-quiet); }
  .op-trend-fig { display:flex; align-items:baseline; gap:.5rem; margin:.35rem 0 .45rem; }
  .op-trend-fig strong { font-family:var(--op-mono); font-size:1.55rem; font-weight:600;
    letter-spacing:-.025em; line-height:1; font-variant-numeric:tabular-nums; }
  .op-trend-fig em { font-style:normal; font-family:var(--op-mono); font-size:.72rem;
    color:var(--op-quiet); }
  .op-trend-chart { display:block; width:100%; height:auto; }
  .op-trend-mix { margin-bottom:.4rem; }
  .op-trend-legend { display:flex; gap:1rem; flex-wrap:wrap; margin-top:.4rem;
    font-family:var(--op-mono); font-size:.72rem; color:var(--op-quiet); }
  .op-chip { display:inline-block; width:.7rem; height:.7rem; border-radius:2px;
    margin-right:.3rem; vertical-align:-1px; }
  .op-chip-active { background:var(--op-court-shade-3); }
  .op-chip-trial { background:var(--op-accent-shade-1); }
  .op-chip-attention { background:#b0721f; }
  .op-chip-ended { background:var(--op-court-tint-2); }
  .op-trend-note { margin:.45rem 0 0; font-family:var(--op-mono); font-size:.72rem;
    color:var(--op-quiet); }
  /* /admin.js's pointer tooltip. Fixed-position so it rides the cursor;
     pointer-events none so it never traps the hover that opened it. */
  .op-tip { position:fixed; z-index:20; background:var(--op-floor); color:#fff;
    font-family:var(--op-mono); font-size:.72rem; padding:.3rem .55rem; border-radius:6px;
    pointer-events:none; white-space:nowrap; }
  @media (max-width:1080px) {
    body.operator .op-pair { grid-template-columns:minmax(0,1fr); gap:0; }
    body.operator .op-pair > .admin-section { margin-top:1.7rem; }
    body.operator .op-pair > .admin-section:first-child { margin-top:0; }
  }
  @media (max-width:960px) {
    .op-shell { grid-template-columns:minmax(0,1fr); gap:1.1rem; padding:1rem .9rem 3rem; }
    .op-rail { position:static; display:flex; align-items:center; gap:.6rem 1.1rem;
      flex-wrap:wrap; padding:.7rem .8rem; }
    .op-rail-mark { margin:0; padding:0; }
    .op-rail-nav { flex-direction:row; flex-wrap:wrap; }
    .op-rail-nav a { border-left:0; border-bottom:3px solid transparent; border-radius:8px 8px 0 0; }
    .op-rail-nav a[aria-current="page"], .op-rail-nav a[aria-current="true"] { border-left:0;
      border-bottom-color:var(--shuttle); }
    .op-scoreboard { grid-template-columns:minmax(0,1fr); }
    .op-scoreboard > div { border-left:0; border-top:1px solid rgba(255,255,255,.11); }
    .op-scoreboard > div:first-child { border-top:0; }
    .metric-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
    .metric:nth-child(4n + 1) { border-left:1px solid var(--op-rule); }
    .metric:nth-child(odd) { border-left:0; }
    .metric:nth-child(-n + 2) { border-top:0; }
    .metric:nth-child(3), .metric:nth-child(4) { border-top:1px solid var(--op-rule); }
    .op-funnel .metric + .metric::before { display:none; }
    .op-trend-grid { grid-template-columns:minmax(0,1fr); }
  }
  /* /admin/contact (contact-us M3): the filter chips above the list, and the
     detail page's key/value strip and message body. */
  body.operator .op-chip-filters { display:flex; flex-wrap:wrap; gap:.4rem; margin:0 0 1.1rem; }
  body.operator .op-chip-filters a { color:var(--op-quiet); text-decoration:none; font-size:.82rem;
    padding:.3rem .7rem; border:1px solid var(--op-rule); border-radius:999px; }
  body.operator .op-chip-filters a[aria-current="page"] { background:var(--op-floor); color:#fff;
    border-color:var(--op-floor); }
  /* The contact list's pin (0043's important column), violet so it reads as a
     different axis from the amber/blue/grey status pills beside it rather
     than as a fourth status. Operator-scoped: nothing outside /admin pins. */
  body.operator .status-badge.pinned { background:#ede9fb; color:#3b2a72; }
  body.operator .op-message { white-space:pre-wrap; }
