/* Hub chrome, extracted from base.html's inline <style> (UI batch 1,
   2026-07-22) so it caches instead of shipping ~150 lines with every
   response. Tokens live in rop-tokens.css, loaded before this file. */
/* In-page anchors (/#past, #logistics, #setupcard) land under the sticky
   chrome otherwise. Sized for the tallest chrome — header + a meet's
   sub-nav — since overshooting only adds whitespace above the target. */
html { scroll-padding-top: 100px; }
body {
  margin: 0; color: var(--ink); background:
    radial-gradient(1000px 500px at 85% -10%, rgba(19,148,206,.10), transparent 60%),
    linear-gradient(152deg, #f3f9fd, var(--paper));
  min-height: 100vh;
  font: 15px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* The whole chrome block sticks as one unit — main nav plus whatever the
   page puts in base.html's `subnav` (a meet's .meet-bar). Sticking the
   wrapper, not each row, is what lets the sub-nav stay mated to the main
   nav without hard-coding the header's height as a `top` offset. */
.appbar { position: sticky; top: 0; z-index: 10; }
header {
  display: flex; align-items: center; gap: 14px;
  padding: 10px 20px; background: rgba(255,255,255,.72); backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--hairline);
}
header .mark { font-weight: 700; color: var(--rop-blue); text-decoration: none; font-size: 17px; }
header nav { display: flex; gap: 4px; flex-wrap: wrap; }
header nav a {
  color: var(--ink); text-decoration: none; padding: 5px 10px; border-radius: 8px; font-size: 14px;
}
header nav a:hover { background: rgba(19,148,206,.10); }
header nav a.active { font-weight: 700; text-decoration: underline;
  text-decoration-color: var(--rop-blue); text-decoration-thickness: 2px;
  text-underline-offset: 7px; }
header .spacer { flex: 1; }
header .who { color: var(--muted); font-size: 13px; }
main { max-width: 1100px; margin: 24px auto 80px; padding: 0 20px; }
.card {
  background: var(--card); border: 1px solid var(--hairline); border-radius: 12px;
  padding: 16px 18px; margin-bottom: 18px; box-shadow: 0 24px 55px rgba(19,148,206,.08);
}
.card h2 { margin: 0 0 10px; font-size: 16px; }
/* Sub-headings use it too — the galleries page's meet_keyword/team groups
   and the delivery pre-flight's per-subfolder groups. Scoped to h2 alone,
   those rendered their counts in full heading weight. */
.card h2 .h-side, .card h3 .h-side, .card h4 .h-side {
  font-weight: 400; color: var(--muted); font-size: 13px;
}
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: left; padding: 6px 10px 6px 0; border-bottom: 1px solid rgba(70,70,72,.09); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; }
tr:last-child td { border-bottom: none; }
a { color: var(--rop-blue); }
.muted { color: var(--muted); }
.flag { color: var(--bad); font-weight: 600; }
.ok { color: var(--good); font-weight: 600; }
.status-pending { color: #8a6d00; }
/* .badge lived here for the home dashboard's "N to review / N open issues"
   banner. That banner is gone — each meet's Status pill links to its own
   issues now — and it was the only caller, so the rule went with it. */
.pill { display: inline-block; border: 1px solid var(--hairline); border-radius: 20px;
  padding: 1px 9px; font-size: 12px; color: var(--muted); }
/* A pill that IS the link to what it counted (the meets dashboard's Status
   column). It keeps the pill's own color; only the affordance is added. */
a.pill { text-decoration: none; }
a.pill:hover { background: rgba(19,148,206,.10); }
.pill.blue { border-color: var(--rop-blue); color: var(--rop-blue); }
/* .avail-* are the availability traffic lights (staffing grid +
   session whiteboard); green shares the generic success pill rule. */
.pill.green, .pill.avail-green { border-color: var(--good); color: var(--good); }
.pill.avail-red { border-color: var(--bad); color: var(--bad); }
/* .amber is the generic caution tone (avail-yellow keeps its
   availability-specific name); it is what a delivery "warn" wears. */
.pill.avail-yellow, .pill.amber { border-color: #b8860b; color: #8a6d00; }
/* Generic danger pill (avail-red keeps its availability-specific name). */
.pill.red { border-color: var(--bad); color: var(--bad); }
input[type=text], input[type=password], input[type=email], input[type=number], input[type=date],
input[type=search], select, textarea {
  font: inherit; color: var(--ink); border: 1px solid var(--hairline); border-radius: 8px;
  padding: 6px 9px; background: #fff; max-width: 100%;
}
/* Selects and text inputs get the same explicit height — browsers give
   <select> its own intrinsic height, which otherwise sits a couple px
   short of an identically-padded input beside it. */
input[type=text], input[type=password], input[type=email], input[type=number], input[type=date],
input[type=search], select { height: 34px; }
textarea { width: 100%; min-height: 60px; }
button {
  font: inherit; background: var(--rop-blue); color: #fff; border: 0; border-radius: 8px;
  padding: 7px 14px; cursor: pointer;
}
button.quiet { background: transparent; color: var(--bad); padding: 2px 6px; }
/* Secondary action: bordered, ink-colored — the pattern pages were
   hand-rolling inline with four different paddings. quiet stays the
   red destructive ghost; btn-outline is the neutral one. */
button.btn-outline {
  background: transparent; color: var(--ink); border: 1px solid var(--hairline);
  border-radius: 8px; padding: 3px 10px; font-size: 13px;
  /* A two-word label is one label: "+ Add Meet" broke after "Add" (U16). */
  white-space: nowrap;
}
/* Link-styled button: pages hand-rolled `class="quiet"` plus an inline
   blue color override for this ~15 times. quiet stays the red
   destructive ghost; btn-link is the neutral inline action. */
button.btn-link { background: transparent; color: var(--rop-blue); padding: 2px 6px; }
button:disabled { opacity: .5; cursor: not-allowed; }
form.inline { display: inline; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 18px; }
/* min-width:0 lets a card shrink to its grid track; overflow-x lets a wide
   table (gear serials, drive rows) scroll inside the card instead of
   bleeding past its border or dragging the whole page wider. */
.grid .card { min-width: 0; }
/* Only a grid's direct card children drop their bottom margin (the grid
   gap spaces them); cards stacked inside a column div keep it, so e.g.
   Customer / Find swimmer stay consistently 18px apart. */
.grid > .card { margin-bottom: 0; }
.card { overflow-x: auto; }
.kv { display: grid; grid-template-columns: 150px 1fr; gap: 4px 12px; font-size: 14px; }
.kv dt { color: var(--muted); }
/* min-width:0 lets grid cells shrink below input intrinsic width — without
   it text inputs blow out the card edge. */
.kv dd { margin: 0; min-width: 0; }
.kv dd input[type=text] { width: 100%; }
.kv dd input[type=date] { max-width: 48%; }
.stat { font-size: 22px; font-weight: 700; }
.checks label { display: block; font-size: 14px; padding: 2px 0; }
.flash { background: rgba(30,142,62,.1); border: 1px solid var(--good); border-radius: 8px;
  padding: 8px 12px; margin-bottom: 14px; }
/* flash("...", "error") — refusals and failures must not read as green
   success banners. */
.flash.error { background: rgba(192,57,43,.08); border-color: var(--bad); }
.meet-logo { width: 26px; height: 26px; object-fit: contain; border-radius: 6px;
  vertical-align: -7px; margin-right: 6px; background: #fff; border: 1px solid var(--hairline); }
/* Meets dashboard tables: every card shares one fixed column grid so
   Year/Dates/Orders/Status line up from card to card instead of each
   table auto-sizing to its own contents. min-width keeps narrow screens
   scrolling inside the card (.card has overflow-x) rather than crushing
   the columns. */
table.meets { table-layout: fixed; min-width: 860px; }
table.meets th:nth-child(1) { width: 24%; }  /* Meet */
table.meets th:nth-child(2) { width: 6%; }   /* Year */
table.meets th:nth-child(3) { width: 27%; }  /* Dates */
table.meets th:nth-child(4) { width: 7%; }   /* Orders */
table.meets th:nth-child(5) { width: 22%; }  /* Status */
table.meets th:nth-child(6) { width: 14%; }  /* venue */
#navtoggle { display: none; background: transparent; color: var(--ink); font-size: 22px;
  padding: 2px 8px; border: 1px solid var(--hairline); border-radius: 8px; }
/* Menu-only rows: on a wide screen .who and the header's own Sign out say
   this already, so these two stay hidden until the hamburger owns the nav. */
.nav-who, .nav-signout { display: none; }
/* The drawer's backdrop (U14). Closed here as well as by the `hidden`
   attribute, on the .modal-overlay pattern; .open outranks both. Desktop
   never opens it — the nav is a row up there — so the rule that shows it
   lives inside the mobile media query. */
.nav-scrim { display: none; position: fixed; inset: 0; z-index: 39;
  background: rgba(70, 70, 72, .45); }
.grid.cols-2 { grid-template-columns: 1.2fr 1fr; align-items: start; }
/* Entry layout: wide data-entry column + narrow cart sidebar. The sidebar
   sticks below the sticky header so the cart stays visible while the
   operator scrolls swimmer results. */
.grid.cols-entry { grid-template-columns: 2fr 1fr; align-items: start; }
.sidebar-sticky { position: sticky; top: 60px; }
/* Page scaffold: the h1/backlink margins every page hand-rolled inline. */
.page-title { margin: 4px 0 2px; }
.backlink { margin: 0; }
/* Flex form row — the display:flex;gap:6px;flex-wrap:wrap trio pages
   repeated inline. */
.form-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
/* Meet workspace sub-nav (UI batch 5): the meet's tools in one strip on
   every meet-scoped page. It is the second row of the sticky chrome now —
   the meet's logo and name ride at its left as the Overview link, so the
   page below opens on content instead of repeating the meet's identity. */
.meet-bar {
  display: flex; align-items: flex-end; gap: 0 12px; flex-wrap: wrap;
  padding: 5px 20px 0; background: rgba(255,255,255,.72);
  backdrop-filter: blur(14px); border-bottom: 1px solid var(--hairline);
}
/* h1 on the meet's own page, span elsewhere (see _meet_nav.html) — either
   way it sits in the row rather than carrying heading margins. The -8px
   cancels the link's own left padding so the logo lines up under the
   header's ROP mark; line-height is what the logo is sized against. */
.meet-bar-title { margin: 0 0 0 -8px; font-size: 17px; font-weight: 700;
  line-height: 1.3; padding: 4px 0 7px; }
.meet-bar-title a { color: var(--ink); text-decoration: none;
  display: inline-flex; align-items: center; gap: 8px;
  padding: 0 8px 7px; margin-bottom: -7px; border-radius: 8px 8px 0 0; }
.meet-bar-title a:hover { background: rgba(19,148,206,.10); }
.meet-bar-title a.active { color: var(--rop-blue);
  border-bottom: 2px solid var(--rop-blue); margin-bottom: -8px; }
/* Height-matched to the name; width auto so a wide logo keeps its shape
   instead of being letterboxed into a square. */
.meet-bar-logo { height: 22px; width: auto; max-width: 46px;
  object-fit: contain; border-radius: 4px; }
.meet-nav { display: flex; gap: 2px; flex-wrap: wrap; }
.meet-nav a { color: var(--ink); text-decoration: none; font-size: 13px;
  padding: 5px 10px 7px; border-radius: 8px 8px 0 0; }
.meet-nav a:hover { background: rgba(19,148,206,.10); }
.meet-nav a.active { font-weight: 700; color: var(--rop-blue);
  border-bottom: 2px solid var(--rop-blue); margin-bottom: -1px; }
/* Labeled input: a small caption that survives typing (batch 5 forms
   polish — placeholder-only fields lose their label on first keypress). */
label.fld { display: inline-flex; flex-direction: column; gap: 2px;
  font-size: 12px; color: var(--muted); }
/* Closed by default twice over: `hidden` in the markup, which is the UA
   stylesheet's display:none and therefore applies with no network at all,
   and display:none here once this file lands. The pair is deliberate —
   this rule alone left five overlays' worth of forms in the layout while
   the stylesheet was in flight (U3, 2026-08-05). .open is an author rule
   with a class selector, so it outranks both and opening a modal stays
   one class either way. */
.modal-overlay {
  position: fixed; inset: 0; z-index: 50; display: none;
  align-items: flex-start; justify-content: center;
  padding: 6vh 16px; background: rgba(70,70,72,.45);
}
.modal-overlay.open { display: flex; }
.modal {
  background: var(--card); border: 1px solid var(--hairline); border-radius: 12px;
  padding: 16px 18px; width: 100%; max-width: 560px; max-height: 85vh;
  overflow-y: auto; box-shadow: 0 24px 55px rgba(19,148,206,.25);
}
.modal h2 { margin: 0 0 10px; font-size: 16px; display: flex; justify-content: space-between; }
.modal-close { background: transparent; color: var(--muted); padding: 0 4px; font-size: 18px; line-height: 1; }
@media (max-width: 720px) {
  .grid.cols-2 { grid-template-columns: 1fr; }
  .grid.cols-entry { grid-template-columns: 1fr; }
  .sidebar-sticky { position: static; }
  #navtoggle { display: block; }
  /* A blurred backdrop makes an element the containing block for every
     position:fixed DESCENDANT — and the drawer is a fixed descendant of
     this header. Opaque rather than frosted on a phone, which is the
     price of the drawer being able to leave the header's box at all. */
  header { flex-wrap: wrap; backdrop-filter: none; background: var(--card); }
  /* U14: the menu used to expand INLINE, shoving the page ~520px down,
     with no backdrop and no way out but the hamburger again. It is a
     drawer now — off-canvas, over the page, dismissed by the backdrop,
     by Escape, or by picking something.
     visibility, not display, so the slide can animate AND the links stay
     out of the tab order while it is shut. */
  header nav {
    display: flex; flex-direction: column; gap: 0;
    position: fixed; top: 0; right: 0; bottom: 0;
    width: min(78vw, 300px); padding: 54px 16px 20px;
    background: var(--card); border-left: 1px solid var(--hairline);
    box-shadow: -14px 0 44px rgba(70, 70, 72, .22);
    overflow-y: auto; z-index: 40;
    transform: translateX(100%); visibility: hidden;
    transition: transform .18s ease-out, visibility .18s;
  }
  header nav.open { transform: none; visibility: visible; }
  .nav-scrim.open { display: block; }
  /* The page behind a drawer does not scroll. */
  body.nav-open { overflow: hidden; }
  header nav a { padding: 10px 6px; border-bottom: 1px solid rgba(70,70,72,.08); }
  header nav a.active { text-decoration: none; }
  header .who, header .signout { display: none; }
  /* Sign out moves into the menu, under a line saying who is signed in. */
  header nav .nav-signout { display: block; color: var(--rop-blue); }
  header nav .nav-who { display: block; color: var(--muted); font-size: 12px;
    padding: 10px 6px 4px; }
  /* One strip on a phone: the meet name wraps above its own tabs, and the
     tabs scroll sideways rather than stacking three rows of chrome. */
  .meet-bar { gap: 0; padding: 4px 12px 0; }
  .meet-bar-title { font-size: 15px; padding-bottom: 4px; }
  .meet-nav { flex-wrap: nowrap; overflow-x: auto; max-width: 100%;
    -webkit-overflow-scrolling: touch; }
  .meet-nav a { white-space: nowrap; }
  main { margin-top: 12px; }
}

/* ===================================================================
   UI framework (2026-07 backlog batch). Appended; nothing above changes.
   This file owns the class namespace for the whole batch so two parallel
   tracks cannot mint the same selector with different meanings.
   =================================================================== */

/* Single-column page scaffold. Sections stack full width in reading
   order; .grid stays for the pages that genuinely want columns. */
.page-stack { display: flex; flex-direction: column; gap: 18px; }
.page-stack > .card { margin-bottom: 0; }
/* Card header with a right-aligned action (the "+ Add" button), replacing
   the style="float:right" that pages hand-rolled.
   flex-wrap, because on a phone "+ Add Meet" was squeezed by the heading
   and its h-side until it broke across two lines mid-label (U16). It
   drops to its own line instead. */
.card h2, .card h3, .card h4 {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.card h2 .h-side, .card h3 .h-side, .card h4 .h-side { margin-right: auto; }
/* The class="muted" style="font-size:12px" pair, ~20 of them on meet.html
   alone, as one name. */
.hint { color: var(--muted); font-size: 12px; }

/* .kv at two columns. A full-width Logistics card in one column is a mile
   of stacked inputs; from 900px the dt/dd pairs auto-flow two per row. A
   wide field must pin BOTH its dt and dd — pinning only the dd orphans the
   label on the previous row once the dt auto-places into column 3. */
@media (min-width: 900px) {
  .kv.kv-2col { grid-template-columns: 170px minmax(0, 1fr) 170px minmax(0, 1fr); column-gap: 24px; }
  .kv.kv-2col dt.kv-wide { grid-column: 1; }
  .kv.kv-2col dd.kv-wide { grid-column: 2 / -1; }
}

/* Logistics status dot. is-none is the empty field: a hollow placeholder
   that holds the label's indent so the column does not jitter as fields
   get filled in. */
.status-dot {
  display: inline-block; width: 11px; height: 11px; padding: 0; margin-right: 7px;
  border-radius: 50%; border: 1px solid var(--hairline); background: transparent;
  vertical-align: 1px; font-size: 0; line-height: 0; cursor: pointer;
}
.status-dot.is-green { background: var(--good); border-color: var(--good); }
.status-dot.is-yellow { background: var(--warn); border-color: #b8860b; }
.status-dot.is-none { background: transparent; border-color: rgba(70,70,72,.20); cursor: default; }
/* button:disabled's opacity would grey out the traffic light itself. */
.status-dot:disabled { opacity: 1; cursor: default; }
.status-dot:focus-visible { outline: 2px solid var(--rop-blue); outline-offset: 2px; }

/* data_table chrome. Base th/td rules already come from `table`. */
.data-table th a.sort { color: var(--muted); text-decoration: none; }
.data-table th a.sort:hover { color: var(--ink); }
.data-table th a.sort .arrow { font-size: 9px; margin-left: 3px; }
.data-table tbody tr:hover { background: rgba(19,148,206,.04); }
.data-table th.num, .data-table td.num { text-align: right; }
/* Actions hug the right edge and never wrap the edit away from the delete.
   `width: 1%` is the AUTO-layout idiom for that: the browser floors a
   percentage at the column's min-content width, so 1% means "as narrow as
   these buttons need and no narrower". */
.data-table th.row-actions, .data-table td.row-actions { white-space: nowrap; text-align: right; width: 1%; }
/* A table-layout:fixed table honours the same 1% LITERALLY — content is
   not consulted at all — so the action column came out ~10px wide holding
   ~85px of buttons. They overflowed the cell, .fixed's overflow:hidden
   (below) clipped them at the card edge, and the card's overflow-x
   container never learned about them because the table box was still
   exactly 100% wide: measured 2026-08-05 on the orphan-galleries table,
   card edge x=1170, the row's ✎ boxed out to x=1225, scrollWidth ==
   clientWidth == 1022. On a phone that hid every repair control behind a
   column too narrow to see, let alone scroll to.
   So a fixed table gets a REAL column, sized for the widest action set the
   app renders — a labelled button plus a hover tool ("Match…" + ✎ ≈ 86px
   inside a 100px content box). A call site needing more passes its own
   `width` in the column dict; data_table writes that inline, which
   outranks this rule. */
.data-table.fixed th.row-actions, .data-table.fixed td.row-actions { width: 110px; }
.data-table td.empty { color: var(--muted); }
/* Prior-year money columns read as reference, not as this year's number. */
.data-table td.prior, .data-table th.prior { color: var(--muted); text-align: right; }
.data-table tr.total td { border-top: 1px solid var(--hairline); font-weight: 600; }
/* data_table's `sub` hook: child rows belonging to the row above. Tied to
   their parent by losing the top border and sitting on a faint wash, so a
   family and its swimmers read as one block rather than as five
   indistinguishable rows in a two-hundred-row table. */
.data-table tr.subrow td { border-top: 0; padding-top: 2px; padding-bottom: 2px;
  background: rgba(19,148,206,.03); font-size: 13px; }
.data-table tr.subrow:hover td { background: rgba(19,148,206,.07); }
.data-table tr.subrow + tr:not(.subrow) td { border-top: 1px solid var(--hairline); }
/* Row states for data_table's row_css hook (D-028). These were inline
   styles repeated on every <td> because the caller could not reach the
   <tr>; the hook exists so they are stated once, here. */
.data-table tr.is-retired { opacity: .5; }
.data-table tr.is-current td { font-weight: 600; }
.toggle-check { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; }

/* Explained absence (the deck board's missing prerequisites). */
.empty-state { border: 1px dashed var(--hairline); border-radius: 10px; padding: 14px 16px; color: var(--muted); font-size: 14px; }
.empty-state-title { margin: 0 0 6px; color: var(--ink); font-weight: 600; }
.empty-state ol, .empty-state ul { margin: 0; padding-left: 20px; }
.empty-state li { margin: 3px 0; }

/* AJAX states. */
.is-pending { opacity: .55; cursor: progress; pointer-events: none; }
.is-error { outline: 2px solid var(--bad); outline-offset: 2px; }
.is-saved { animation: hub-saved 1.2s ease-out; }
@keyframes hub-saved {
  from { box-shadow: 0 0 0 3px rgba(30,142,62,.45); }
  to { box-shadow: 0 0 0 3px rgba(30,142,62,0); }
}
.hub-toasts { position: fixed; right: 16px; bottom: 16px; z-index: 60; display: flex; flex-direction: column; gap: 8px; max-width: 340px; }
.hub-toast {
  background: var(--card); border: 1px solid var(--hairline);
  border-left: 4px solid var(--good); border-radius: 10px; padding: 10px 14px;
  box-shadow: 0 12px 30px rgba(19,148,206,.18); font-size: 14px;
}
.hub-toast.error { border-left-color: var(--bad); }
.form-error { color: var(--bad); font-size: 13px; margin: 8px 0 0; }

/* Issues. A row is a HEADER for its detail, so the two <tr>s have to read
   as one thing: the detail cell drops the row hairline and indents under
   the disclosure triangle rather than floating in the table. */
.disclosure {
  background: transparent; border: 0; color: var(--muted);
  font-size: 13px; line-height: 1; padding: 2px 4px; cursor: pointer;
}
.disclosure:hover { color: var(--rop-blue); }
.disclosure:focus-visible { outline: 2px solid var(--rop-blue); outline-offset: 1px; }
.issue-table th.issue-toggle, .issue-table td.issue-toggle { width: 1%; padding-right: 0; }
.issue-table td.issue-note { max-width: 340px; color: var(--muted); font-size: 13px; }
.issue-table tr.issue-detail > td { border-top: 0; padding: 0 10px 14px 34px; background: rgba(19,148,206,.03); }
.issue-table tr.issue-row.is-done { opacity: .55; }
.issue-body .kv.issue-facts { grid-template-columns: 190px minmax(0, 1fr); margin: 10px 0; }
.issue-details { font-size: 14px; margin: 6px 0 12px; white-space: pre-wrap; }
.issue-history { border-top: 1px solid var(--hairline); padding-top: 10px; }
.history-list { margin: 0; padding-left: 22px; font-size: 13px; }
.history-list li { margin: 0 0 8px; }
.history-note { margin: 3px 0 0; white-space: pre-wrap; color: var(--ink); }
.race-picker { display: flex; flex-wrap: wrap; gap: 4px 14px; }
/* The status select carries its own state colour, set by hub.js after a
   save — so the class list, not the render, is what says where it is. */
.issue-status { font-size: 13px; padding: 2px 4px; max-width: 100%; }
.issue-status.is-resolved { color: var(--good); }
.issue-status.is-unresolved, .issue-status.is-awaiting_reply { color: var(--bad); }
.issue-status.is-potential { color: var(--muted); }
.issue-status.is-requested { color: #8a6d00; }
.resolved-meet { margin: 6px 0; }
.resolved-meet > summary { cursor: pointer; font-size: 14px; padding: 4px 0; }
/* The duplicate-review merge buttons name the survivor in full and carry
   their email with it. At one size that is two equal shouts on a button,
   so the address rides a step down and a shade back — on the filled
   button and the outlined one alike, which is why it is opacity rather
   than a colour. */
.merge-who { font-size: 12px; opacity: .8; }
/* ===================================================================
   Gallery groups + dashboards (galleries and delivery, 2026-08).
   Both pages render the same shapes, so the classes live here once
   rather than as inline styles repeated per group on two templates.
   =================================================================== */

/* A subfolder group (age band, GHSA classification, team). The pages
   previously separated these with a bare <h3> and nothing else, so on a
   five-band meet the tables ran together into one wall. This is the
   .card idea one level down: same hairline and radius, no shadow, and a
   tinted head strip so the eye can find where a band starts. */
.group { border: 1px solid var(--hairline); border-radius: 10px; margin: 14px 0 0; }
.group-head {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 8px 12px; background: rgba(19,148,206,.06);
  border-bottom: 1px solid var(--hairline); border-radius: 9px 9px 0 0;
}
.group-head h3, .group-head h4 { margin: 0; font-size: 15px; }
/* Sub-buckets (GHSA's team level) nest one deeper: no second border, just
   a rule and an indent, or three nested boxes read as noise. */
.group-body { padding: 4px 12px 10px; }
.group-body > .data-table { margin-top: 6px; }
.subgroup { margin: 12px 0 0; padding-left: 10px; border-left: 3px solid rgba(19,148,206,.22); }
.subgroup h4 { margin: 0 0 2px; font-size: 14px; }

/* Dashboard strip. Number first, label under it — the same "read the
   figure, then what it is" order as .stat, which it borrows from. */
.dash { display: flex; gap: 22px; flex-wrap: wrap; align-items: flex-end; margin: 0; }
.dash-item { display: flex; flex-direction: column; gap: 1px; }
.dash-num { font-size: 20px; font-weight: 700; line-height: 1.1; }
.dash-num.is-low { color: var(--bad); }
.dash-num.is-part { color: #8a6d00; }
.dash-num.is-done { color: var(--good); }
.dash-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
/* The issue list under a dashboard, only drawn past ISSUE_LIST_RATIO. */
.dash-issues { margin: 8px 0 0; font-size: 13px; }
.dash-issues summary { cursor: pointer; color: var(--bad); }
.dash-issues ul { margin: 6px 0 0; padding-left: 18px; }
.dash-issues li { margin: 2px 0; color: var(--muted); }
.dash-issues li b { color: var(--ink); font-weight: 600; }

/* Per-row hover tools (the ⚙ / ↻ / ✎ beside a gallery name). Hidden until
   the row is hovered so a 60-row table isn't a field of icons, but ALWAYS
   revealed on keyboard focus — opacity-only, never display:none, so the
   buttons stay in the tab order and the column keeps its width whether or
   not the pointer is over the row. */
.row-tools { display: inline-flex; gap: 2px; margin-left: 6px; vertical-align: baseline; }
.row-tools .row-tool {
  opacity: 0; transition: opacity .12s; background: transparent; border: 0;
  color: var(--muted); font-size: 13px; line-height: 1; padding: 1px 3px;
  cursor: pointer; text-decoration: none;
}
.data-table tr:hover .row-tool, .row-tool:focus-visible, .row-tools:hover .row-tool { opacity: 1; }
.row-tools .row-tool:hover { color: var(--rop-blue); }
.row-tools form { display: inline; }
@media (hover: none) {
  /* No hover on touch: showing them always beats hiding them forever. */
  .row-tools .row-tool { opacity: .7; }
}

/* Fixed column grid, so every group's table on a page lines up with every
   other one instead of each auto-sizing to its own rows. .card already
   scrolls horizontally, which is what min-width leans on. */
.data-table.fixed { table-layout: fixed; }
.data-table.fixed { min-width: 620px; }
.data-table.fixed td { overflow: hidden; text-overflow: ellipsis; }

/* Staffing grid availability controls, moved out of the page's own
   <style> block so the whole app's chrome is in one file.

   24px minimum in both directions (U12): these were 11px glyphs in an
   18px box, three of them per cell, on the page most likely to be worked
   from a phone deck-side. The type stays small; the TARGET does not. */
.avail-form { display: block; margin-top: 3px; white-space: nowrap; }
.avail-btn {
  background: transparent; color: var(--muted); border: 1px solid var(--hairline);
  border-radius: 6px; padding: 0 6px; font-size: 12px; cursor: pointer;
  min-width: 24px; min-height: 24px; line-height: 22px;
}
.avail-btn:hover { color: var(--ink); background: rgba(19,148,206,.10); }
.avail-btn:focus-visible { outline: 2px solid var(--rop-blue); outline-offset: 1px; }

/* The one unstyled control left on a styled page: a native file input
   sat next to bordered buttons on the staffing import card and the
   document-upload modal. Site-wide, so every upload looks like the app. */
input[type=file] {
  font: inherit; color: var(--ink); border: 1px solid var(--hairline);
  border-radius: 8px; padding: 4px 8px; background: #fff; max-width: 100%;
}
input[type=file]::file-selector-button {
  font: inherit; background: transparent; color: var(--ink);
  border: 1px solid var(--hairline); border-radius: 6px;
  padding: 3px 10px; margin-right: 8px; cursor: pointer; min-height: 24px;
}
input[type=file]::file-selector-button:hover { background: rgba(19,148,206,.10); }

/* Field documentation, folded (U7). The ? is reachable — hover, and
   focus for the keyboard — and out of the way the rest of the time,
   which is the whole trade: the sentence still exists, it has just
   stopped competing with the value it describes. */
.help-tip {
  display: inline-block; width: 20px; height: 20px; margin-left: 5px;
  border: 1px solid var(--hairline); border-radius: 50%;
  color: var(--muted); font-size: 12px; line-height: 18px; text-align: center;
  cursor: help; vertical-align: -3px; user-select: none; flex: none;
}
.help-tip:hover, .help-tip:focus-visible {
  color: var(--rop-blue); border-color: var(--rop-blue);
}
.help-tip:focus-visible { outline: 2px solid var(--rop-blue); outline-offset: 1px; }
/* Card-level "How this works". Closed is the point: the card explains
   itself to whoever asks and stays quiet for everyone who already knows. */
details.how { margin: 8px 0 0; font-size: 13px; }
details.how > summary { cursor: pointer; color: var(--muted); }
details.how > summary:hover { color: var(--rop-blue); }
.how-body { color: var(--muted); padding: 6px 0 0; max-width: 62em; }
.how-body p { margin: 0 0 6px; }
.how-body p:last-child { margin-bottom: 0; }

/* ===================================================================
   Mobile tables (U13). At 375px the card tables scrolled sideways with a
   bare scrollbar sliver as the only affordance, and the columns that
   landed off the end were the ones being looked for: delivery's STATUS
   and its preview/send-test links, the orphan card's Match button, the
   dashboard's DATES clipped mid-word to "Wed. 202".

   Two halves. Column PRIORITY drops the columns a phone can do without,
   in two steps, so what is left fits; and where a table still has to
   scroll, an edge FADE says so.

   The class goes on the <th> AND its <td>s — the column dict's `css` and
   the caller's cell — the same contract row-actions already has.
   =================================================================== */
@media (max-width: 900px) {
  .data-table th.col-lo, .data-table td.col-lo,
  table.meets th.col-lo, table.meets td.col-lo { display: none; }
}
@media (max-width: 620px) {
  .data-table th.col-mid, .data-table td.col-mid,
  table.meets th.col-mid, table.meets td.col-mid { display: none; }
  /* The min-widths exist to keep columns legible on a desktop card; with
     the low-priority columns gone they are just a reason to scroll. */
  table.meets, .data-table.fixed { min-width: 0; }
}
/* Scroll shadow on the card, which IS the scroll container (.card has
   overflow-x:auto). The white pair scrolls WITH the content
   (background-attachment: local) and so uncovers the grey pair, which is
   pinned: the fade appears only on the side there is more table on, and
   a card with nothing to scroll shows neither. No JS, no wrapper markup,
   and it costs nothing on the pages that never overflow. */
.card {
  background-image:
    linear-gradient(to right, var(--card) 40%, rgba(255, 255, 255, 0)),
    linear-gradient(to left, var(--card) 40%, rgba(255, 255, 255, 0)),
    linear-gradient(to right, rgba(70, 70, 72, .16), rgba(70, 70, 72, 0)),
    linear-gradient(to left, rgba(70, 70, 72, .16), rgba(70, 70, 72, 0));
  background-position: 0 0, 100% 0, 0 0, 100% 0;
  background-repeat: no-repeat;
  background-size: 36px 100%, 36px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}

/* Typed-confirm delete, the second view of a ✎ modal. */
.danger-zone { border-top: 1px solid var(--hairline); margin-top: 14px; padding-top: 12px; }
.danger-zone p { margin: 0 0 8px; font-size: 13px; color: var(--muted); }
.danger-zone .btn-danger { border-color: var(--bad); color: var(--bad); }

/* Media-day pricing panel (team page): a count the operator typed over. */
#md-table input.md-overridden { border-color: var(--rop-blue); font-weight: 600; }
