﻿/* ==========================================================================
   Sabah Diabolo — production UI additions
   Layers on top of theme.css. Only components the demo never needed live here:
   toasts, tabs, stat tiles, sticky action bars, empty states, and the print
   stylesheet for the official result sheet.
   ========================================================================== */

/* ------------------------------------------------------------------ toasts */

#toastHost {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 4000;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: min(420px, calc(100vw - 32px));
}

.toast {
  background: var(--bg-surface-2);
  border: 1px solid var(--border-medium);
  border-left-width: 4px;
  border-radius: var(--radius-md, 10px);
  padding: 12px 14px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  font-size: 13px;
  color: var(--text-main);
  animation: toastIn .18s ease-out;
}

.toast-ok { border-left-color: var(--success); }
.toast-err { border-left-color: var(--danger); }
.toast-warn { border-left-color: var(--warning); }

.toast-title { font-weight: 700; margin-bottom: 2px; }
.toast ul { margin: 6px 0 0 16px; color: var(--text-sub); }
.toast li { margin-bottom: 3px; }

@keyframes toastIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* -------------------------------------------------------------------- tabs */

.tabbar {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-bottom: 10px;
  margin-bottom: 16px;
  scrollbar-width: thin;
}

.tabbar a, .tabbar button { white-space: nowrap; flex: 0 0 auto; }

/* -------------------------------------------------------------- stat tiles */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: 22px;
}

.stat {
  background: var(--bg-glass-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md, 10px);
  padding: 14px 16px;
}

.stat-label { font-size: 12px; color: var(--text-muted); }
.stat-value {
  font-family: var(--font-mono);
  font-size: 24px;
  font-weight: 800;
  margin: 4px 0 2px;
  color: var(--text-main);
}
.stat-note { font-size: 12px; color: var(--text-sub); }
.stat-value.is-good { color: var(--success); }
.stat-value.is-warn { color: var(--warning); }
.stat-value.is-bad { color: var(--danger); }

/* --------------------------------------------------------- sticky task bar */

.actionbar {
  position: sticky;
  top: 0;
  z-index: 60;
  background: var(--bg-glass);
  backdrop-filter: blur(8px);
  border: 1px solid var(--border-accent);
  border-radius: var(--radius-md, 10px);
  padding: 12px 14px;
  margin-bottom: 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}

.actionbar .grow { flex: 1 1 260px; min-width: 0; }
.actionbar h1 { font-family: var(--font-title); font-size: 19px; font-weight: 800; margin: 0; }
.actionbar .sub { font-size: 12px; color: var(--text-muted); margin-top: 2px; }

/* ------------------------------------------------------------ form helpers */

.form-select {
  width: 100%;
  padding: 9px 11px;
  background: var(--bg-surface-1);
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-sm, 6px);
  color: var(--text-main);
  font-family: var(--font-body);
  font-size: 14px;
}

.form-select:focus, .form-input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(6, 182, 212, .18);
}

.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
}

.form-hint { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.form-error { font-size: 12px; color: var(--danger); margin-top: 4px; }

.checkrow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 9px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm, 6px);
  margin-bottom: 6px;
  background: var(--bg-surface-1);
  font-size: 13px;
}

.checkrow:hover { border-color: var(--border-medium); }
.checkrow input { width: 16px; height: 16px; accent-color: var(--primary); }

/* ------------------------------------------------------------ definition list */

.deflist { display: grid; grid-template-columns: max-content 1fr; gap: 8px 18px; font-size: 14px; }
.deflist dt { color: var(--text-muted); }
.deflist dd { margin: 0; color: var(--text-main); font-weight: 600; }

/* ------------------------------------------------------------- empty state */

.empty {
  text-align: center;
  padding: 34px 18px;
  color: var(--text-muted);
  border: 1px dashed var(--border-medium);
  border-radius: var(--radius-md, 10px);
}

.empty i { font-size: 26px; display: block; margin-bottom: 10px; opacity: .55; }

/* ------------------------------------------------------- judge score sheet */

.scorepad { display: grid; gap: 12px; }

.slot {
  background: var(--bg-surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md, 10px);
  padding: 12px;
}

.slot-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 8px;
}

.slot-name { font-size: 14px; font-weight: 700; }
.slot-max { font-size: 12px; color: var(--text-muted); }

.slot-value {
  font-family: var(--font-mono);
  font-size: 22px;
  font-weight: 800;
  color: var(--primary);
  min-width: 62px;
  text-align: right;
}

.slot-input {
  width: 100%;
  padding: 12px;
  font-family: var(--font-mono);
  font-size: 22px;
  font-weight: 700;
  text-align: center;
  background: var(--bg-surface-1);
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-sm, 6px);
  color: var(--text-main);
}

.slot-range { width: 100%; margin-top: 8px; accent-color: var(--primary); }

.entrylist { display: flex; flex-direction: column; gap: 6px; }

.entrybtn {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  background: var(--bg-surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm, 6px);
  color: var(--text-main);
  font-size: 13px;
  cursor: pointer;
  font-family: inherit;
}

.entrybtn:hover { border-color: var(--border-accent); }
.entrybtn.active { border-color: var(--primary); box-shadow: var(--primary-glow); }
.entrybtn .no { font-family: var(--font-mono); font-weight: 800; margin-right: 6px; }
.entrybtn .tot { font-family: var(--font-mono); font-weight: 700; color: var(--primary); }
.entrybtn.done { border-left: 3px solid var(--success); }

.entrybtn.excluded {
  opacity: .45;
  cursor: not-allowed;
  background: repeating-linear-gradient(
    45deg, var(--bg-surface-1), var(--bg-surface-1) 8px,
    var(--bg-surface-2) 8px, var(--bg-surface-2) 16px);
}

.entrybtn.excluded:hover { border-color: var(--border-subtle); }

/* ----------------------------------------------------------- rank list view */

.ranklist { counter-reset: rankpos; }

.rankrow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm, 6px);
  margin-bottom: 6px;
  background: var(--bg-surface-1);
}

.rankpos {
  font-family: var(--font-mono);
  font-size: 18px;
  font-weight: 800;
  color: var(--primary);
  min-width: 34px;
}

/* A shared placing is a legal outcome, not an error — warning tint, never danger. */
.rankrow.tie { border-color: var(--warning); background: var(--warning-bg); }

/* ------------------------------------------------- modal requirement hints */
/* The confirm dialogue never disables its button; it states requirements instead. */
.sd-req { color: var(--danger); font-weight: 800; margin-right: 4px; }

.sd-hint {
  font-size: 12px;
  margin-top: 5px;
  min-height: 16px;
  color: var(--text-muted);
}
.sd-hint.is-ok  { color: var(--success); }
.sd-hint.is-err { color: var(--danger); font-weight: 600; }

.form-input.is-invalid {
  border-color: var(--danger);
  box-shadow: 0 0 0 2px var(--danger-bg);
}

/* --------------------------------------------------------------- utilities */

.muted { color: var(--text-muted); }
.mono { font-family: var(--font-mono); }
.nowrap { white-space: nowrap; }
.right { text-align: right; }
.center { text-align: center; }
.stack { display: flex; flex-direction: column; gap: 12px; }
/* `.rowflex` was declared here. It is byte-for-byte the same rule as the `.row`
   layout primitive added at the end of this file, so the two now share ONE rule
   down there rather than being maintained as a silent duplicate. */
.mt0 { margin-top: 0; }
.mb0 { margin-bottom: 0; }
.mb12 { margin-bottom: 12px; }
.mb20 { margin-bottom: 20px; }

.pill {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  font-family: var(--font-mono);
  background: var(--bg-surface-3);
  color: var(--text-sub);
}

/* ------------------------------------------------- bilingual label pairs */
/* Emitted by Ui.T2 (stacked) / Ui.T2i (inline). Chinese is primary; English is
   the quieter second voice. Keep the pair reading as ONE designed label. */

.t2 { display: inline-block; line-height: 1.3; vertical-align: middle; }
.t2-cn { display: block; }
.t2-en {
  display: block;
  font-size: .76em;
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: .02em;
  margin-top: 1px;
}

/* inline variant: 中文 English on one line */
.t2.i .t2-cn, .t2.i .t2-en { display: inline; }
.t2.i .t2-en { margin-left: 6px; margin-top: 0; font-size: .85em; }

/* buttons: stacked but tight and centred; secondary line takes the button colour */
.btn .t2 { text-align: center; line-height: 1.15; }
.btn .t2-en { color: inherit; opacity: .72; margin-top: 0; }

/* table headers: let the pair wrap instead of stretching the column */
th .t2 { white-space: normal; vertical-align: middle; }
th .t2-en { font-weight: 500; }

/* badges & pills: inline pair inherits the badge colour at reduced opacity */
.badge .t2-en, .pill .t2-en { color: inherit; opacity: .7; }
.badge .t2 { line-height: 1.2; }

/* alerts: the title pair sits inside <strong>; keep the en half calm */
.alert strong .t2-en { font-weight: 600; opacity: .8; color: inherit; }

/* nav: secondary line slightly tighter so the rail stays scannable */
.nav-item .t2 { line-height: 1.25; }
.nav-item .t2-en { font-size: .72em; margin-top: 0; }
.nav-item.active .t2-en { color: inherit; opacity: .75; }

/* form labels: keep the pair compact above the input */
.form-label .t2 { line-height: 1.25; }

/* stat tiles: the label is already small — shrink the en line a touch less */
.stat-label .t2-en { font-size: .84em; }

/* toasts: second (English) line of a bilingual server message */
.toast-sub { color: var(--text-muted); font-size: .88em; font-weight: 400; }

/* ------------------------------------------------------------------- print */

.print-only { display: none; }

/* screen preview of print sheets — @media print overrides these sizes */
.sheet-title { font-size: 17px; font-weight: 800; }
.sheet-sub { font-size: 12px; color: var(--text-muted); margin-bottom: 4px; }

@media print {
  @page { margin: 14mm; }

  html, body {
    background: #fff !important;
    color: #000 !important;
    font-family: 'Plus Jakarta Sans', Arial, sans-serif;
    font-size: 11pt;
  }

  .app-sidebar, .mobile-header, .mobile-drawer, .mobile-drawer-overlay,
  .context-strip, .sidebar-footer, .no-print, #toastHost, .actionbar,
  .modal-overlay, .tabbar { display: none !important; }

  .app-wrapper, .app-main, .content-area {
    display: block !important;
    margin: 0 !important;
    padding: 0 !important;
    max-width: none !important;
    width: auto !important;
  }

  .print-only { display: block; }

  .card, .card-glass {
    background: #fff !important;
    border: 1px solid #000 !important;
    box-shadow: none !important;
    page-break-inside: avoid;
  }

  .data-table { width: 100%; border-collapse: collapse; }

  .data-table th, .data-table td {
    border: 1px solid #000 !important;
    padding: 5px 7px !important;
    color: #000 !important;
    background: #fff !important;
  }

  .data-table thead { display: table-header-group; }
  .data-table tr { page-break-inside: avoid; }

  .badge, .pill {
    border: 1px solid #000 !important;
    background: #fff !important;
    color: #000 !important;
  }

  a { color: #000 !important; text-decoration: none !important; }

  .sheet-title { font-size: 16pt; font-weight: 800; margin-bottom: 2mm; color: #000 !important; }
  .sheet-sub { font-size: 10pt; margin-bottom: 5mm; color: #000 !important; }

  .signature-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10mm;
    margin-top: 14mm;
    page-break-inside: avoid;
  }

  .signature-line {
    border-top: 1px solid #000;
    padding-top: 2mm;
    font-size: 9pt;
    text-align: center;
  }

  .pagebreak { page-break-after: always; }

  /* bilingual pairs on paper: secondary line in solid black */
  .t2-en {
    color: #000 !important;
    font-size: 7.5pt;
    letter-spacing: .01em;
  }
  th .t2-en { font-size: 7pt; }
  .sheet-title .t2-en, .sheet-sub .t2-en { color: #000 !important; }
}

/* ------------------------------------------- mode A rank sheet (A4 landscape) */

/* The one paper document in this system with a hard column budget: four
   sub-columns for every judge on a panel whose size is not known in advance,
   plus rank, running order, contingent code, team, TPR and OR. Raw pt sizing
   and a named condensed family are the correct tool HERE and nowhere else —
   this is a sheet of paper with a fixed width, not screen chrome that has to
   reflow. Requested explicitly by the client; see PrintUi.ResultRankSheet.

   These metrics are NOT wrapped in @media print even though they are print
   rules. .tpr-table exists on this one sheet and on no other page, so the
   selector is already the scope — and the operator previews the sheet on
   screen before committing paper to it, so the preview has to be measured in
   the same type it will print in. Same reasoning as .sheet-title above. */
.tpr-table {
  font-family: 'Arial Narrow', Arial, sans-serif;
  font-stretch: condensed;
  font-size: 9pt;
  width: 100%;
  border-collapse: collapse;
}

.tpr-table th,
.tpr-table td { padding: 2px 3px; line-height: 1.25; }

/* Numeric columns shrink to their content and never wrap; .tpr-team is given
   99% so it absorbs every remaining millimetre and the numbers stay tight.
   That is what keeps a seven-judge panel on the page: the team column gives way
   first instead of the table growing past the paper. */
.tpr-table .tpr-num { text-align: center; white-space: nowrap; }
.tpr-table .tpr-code { white-space: nowrap; }

/* Ui.T2 wraps each half in its own span, and a two-word header in a column
   sized to a two-digit number otherwise breaks one CHARACTER per line — a
   column heading reading downwards is not a heading. The team column is the
   only one allowed to wrap. */
.tpr-table th .t2,
.tpr-table th .t2-cn,
.tpr-table th .t2-en { white-space: nowrap; }
.tpr-table .tpr-team { width: 99%; }
.tpr-table .tpr-rank,
.tpr-table .tpr-tpr { font-weight: 800; }

/* A rule between judges: without it fifteen or twenty-eight identical numeric
   columns are impossible to read across. */
.tpr-table .tpr-jend { border-right: 2px solid currentColor; }

.tpr-table .tpr-tag { font-size: .82em; color: var(--text-muted); white-space: nowrap; }

.tpr-note { font-size: 8pt; margin: 6px 0 0; }

.tpr-committee {
  margin-top: 10px;
  padding: 8px 10px;
  border: 1px solid var(--border-medium);
  border-radius: 6px;
  font-size: 9pt;
}
.tpr-committee-title { font-weight: 800; margin-bottom: 3px; }

/* @page cannot be narrowed by a selector, so the sheet declares a NAMED page
   and its wrapper opts in. A bare `@page { size: A4 landscape }` is document
   scoped: it would also rotate the start list, the blank score sheets, the
   official result sheet, the programme and the passcode cards, every one of
   which is laid out for portrait. The plain `@page { margin: 14mm }` above
   still governs those; this rule adds a second, named page beside it and
   restates the margin, because a named page inherits nothing from it. */
@page tprSheet { size: A4 landscape; margin: 9mm 9mm 11mm; }

@media print {
  .tpr-sheet { page: tprSheet; }

  /* .data-table's print padding is 5px 7px !important — far too generous once
     there are four columns per judge. */
  .tpr-table th,
  .tpr-table td { padding: 2px 3px !important; }

  .tpr-table .t2-en,
  .tpr-table th .t2-en { font-size: 6.5pt; }

  .tpr-note .t2-en { font-size: 7pt; }

  /* The sheet's own card must not draw a box around the table on paper — the
     cell borders already do, and the outer rule wastes 2mm of the budget. */
  .tpr-sheet .card { border: none !important; padding: 0 !important; }
}

.signature-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  margin-top: 40px;
}

.signature-line {
  border-top: 1px solid var(--border-medium);
  padding-top: 8px;
  font-size: 12px;
  text-align: center;
  color: var(--text-muted);
}

/* --------------------------------------------------------------- responsive */

@media (max-width: 860px) {
  .deflist { grid-template-columns: 1fr; gap: 3px 0; }
  .deflist dt { margin-top: 8px; }
  .actionbar { position: static; }
}

/* ------------------------------------------------------ judge list & keypad */

.tap-row { cursor: pointer; }
.tap-row:hover td { background: rgba(6, 182, 212, .07); }
.tap-row:active td { background: rgba(6, 182, 212, .14); }

.tie-row td { background: var(--warning-bg) !important; }

.excluded-row { opacity: .45; }

.keypad-grid + .keypad-grid { margin-top: 6px; }

.keypad-btn.active {
  background: var(--primary);
  border-color: var(--primary);
  color: #04121a;
  font-weight: 800;
  box-shadow: var(--primary-glow);
}

th.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.sortable:hover { color: var(--primary); }


/* -------------------------------------------------- slider & stepper scoring control */

.score-control-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  flex-wrap: wrap;
}

.score-slider-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 180px;
}

.score-slider {
  flex: 1;
  height: 10px;
  border-radius: 5px;
  accent-color: var(--primary);
  cursor: pointer;
}

.range-label {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
  min-width: 18px;
  text-align: center;
}

.stepper-group {
  display: flex;
  align-items: center;
  gap: 4px;
}

.btn-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  font-size: 20px;
  font-weight: 800;
  border-radius: 8px;
  border: 1px solid var(--border-medium);
  background: var(--bg-surface-1);
  color: var(--text-main);
  cursor: pointer;
  user-select: none;
  touch-action: manipulation;
  transition: all 0.15s ease;
}

.btn-step:hover {
  background: var(--bg-surface-3, rgba(255, 255, 255, 0.1));
  border-color: var(--primary);
  color: var(--primary);
}

.btn-step:active {
  background: var(--primary);
  color: #000;
  transform: scale(0.96);
}

.btn-step:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.score-input {
  width: 75px !important;
  height: 44px !important;
  font-size: 18px !important;
  font-weight: 800 !important;
  text-align: center;
  border-radius: 8px !important;
  padding: 0 4px !important;
}


/* --------------------------------------------- admin panel building blocks */
/* Lifted from inline style blocks that repeated across AdminUi /
   AdminContentUi. A .panel sits one layer ABOVE .card — surface-2 instead of
   surface-1, a tighter radius, and no margin of its own so the parent's
   flex/grid gap owns the spacing. That is why these are not just .card. */

.panel {
  background: var(--bg-surface-2);
  border: 1px solid var(--bg-surface-3);
  border-radius: 0.5rem;
}

/* padded variant — a plain content panel with no summary strip */
.panel-pad { padding: 1.25rem; }

/* heading line at the top of a panel (body font, unlike .card-title) */
.panel-title {
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--text-main);
  margin-bottom: 1rem;
}

/* clickable <summary> strip of a collapsible panel */
.panel-summary {
  padding: 0.85rem 1.25rem;
  font-weight: 700;
  font-size: 1rem;
  color: var(--text-main);
  cursor: pointer;
}

/* content area under a .panel-summary */
.panel-body {
  padding: 1rem 1.25rem;
  border-top: 1px solid var(--bg-surface-3);
}

/* ------------------------------------------------------------- page title */
/* Standalone <h1> at the top of a screen (account, judge login, public). */

.page-title {
  font-family: var(--font-title);
  font-size: 21px;
  font-weight: 800;
}

/* ------------------------------------------- judge screen 2 scoring overlay */
/* Moved verbatim (then tokenised) out of the inline <style> block that
   JudgeUi.BuildModeASheet used to emit on every Mode A sheet load. Selectors
   are unchanged — js/judge-sheet.js builds markup against these names. */

.sd-screen2-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9000;
  background: var(--bg-dark);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
}

.sd-screen2-header {
  height: 48px;
  background: var(--bg-surface-2);
  border-bottom: 1px solid var(--bg-surface-3);
  padding: 0 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

.sd-screen2-banner {
  padding: 0.5rem 1rem;
  background: var(--bg-surface-1);
  border-bottom: 1px solid var(--bg-surface-2);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.sd-criterion-card {
  background: var(--bg-surface-2);
  border: 1px solid var(--bg-surface-3);
  border-radius: 0.5rem;
  padding: 0.5rem 0.85rem;
  margin-bottom: 0.45rem;
}

.sd-criterion-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-top: 0.25rem;
}

.sd-criterion-slider {
  flex: 1;
  height: 10px;
  accent-color: var(--primary);
  cursor: pointer;
}

.sd-criterion-valbox {
  width: 60px;
  text-align: center;
  background: var(--bg-surface-1);
  border: 1px solid var(--primary);
  color: var(--primary);
  font-family: monospace;
  font-size: 1.15rem;
  font-weight: 800;
  padding: 0.25rem 0;
  border-radius: 0.375rem;
  cursor: pointer;
  user-select: none;
  transition: all 0.15s ease;
}

.sd-criterion-valbox:hover {
  background: var(--primary);
  color: #0f172a;
  box-shadow: 0 0 10px rgba(56, 189, 248, 0.4);
}

/* Fullscreen Numpad Modal (Shared) */
.sd-numpad-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9999;
  background: rgba(15, 23, 42, 0.85);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease, visibility 0.2s ease;
}

.sd-numpad-overlay.active {
  opacity: 1;
  visibility: visible;
}

.sd-numpad-card {
  width: 100%;
  max-width: 360px;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 12px;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.sd-numpad-header {
  padding: 16px;
  background: #1e293b;
  border-bottom: 1px solid #334155;
  text-align: center;
}

.sd-numpad-title {
  font-size: 0.9rem;
  color: #94a3b8;
  margin-bottom: 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sd-numpad-display {
  font-size: 2.2rem;
  font-weight: 800;
  color: #38bdf8;
  letter-spacing: 2px;
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 8px;
  padding: 4px 12px;
}

.sd-numpad-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  padding: 16px;
}

.sd-numpad-key {
  height: 56px;
  font-size: 1.5rem;
  font-weight: 700;
  border-radius: 8px;
  border: 1px solid #334155;
  background: #1e293b;
  color: #f8fafc;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease, transform 0.1s ease;
  user-select: none;
}

.sd-numpad-key:active {
  transform: scale(0.96);
  background: #334155;
}

.sd-numpad-key.key-clear {
  color: #f87171;
  font-size: 1.1rem;
}

.sd-numpad-key.key-back {
  color: #fbbf24;
  font-size: 1.1rem;
}

.sd-numpad-footer {
  padding: 12px 16px 16px;
  display: flex;
  gap: 10px;
  border-top: 1px solid #1e293b;
}

.sd-numpad-footer .btn {
  flex: 1;
  height: 44px;
  font-size: 1rem;
  font-weight: 600;
}

.sd-btn-updn-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sd-btn-updn {
  background: var(--bg-surface-3);
  color: var(--text-main);
  border: 1px solid var(--border-medium);
  border-radius: 0.25rem;
  width: 34px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  cursor: pointer;
}

.sd-btn-updn:hover { background: var(--primary-active); color: #ffffff; }


/* ------------------------------------------- responsive stacked "card" table */
/* Reusable phone fallback for a .data-table with more columns than a 412px
   screen can hold. Opt a table in with `class='data-table stack-table'`, then
   tag each <td> with the role it should play inside the card:

     .st-media    thumbnail — sits at the left of the top row
     .st-main     primary identity (title, ID) — fills the rest of the top row
     .st-meta     small secondary facts — these share one wrapped line below,
                  separated by a middot
     .st-actions  buttons — full-width row at the bottom, split evenly
     .st-full     a cell that owns the whole card (e.g. a colspan empty state)
     .st-hide-sm  a cell whose value is only meaningful in a real table
                  (placeholder dashes and the like) — dropped on phones

   Above the breakpoint the class contributes nothing, so the desktop table is
   byte-for-byte the layout it always was. The !important flags are here only
   because the cells these rules override carry inline style= padding, borders
   and alignment emitted by the C# page builders; without them the inline
   declarations would win. */

@media (max-width: 800px) {
  .stack-table {
    display: block;
    width: 100%;
    padding: 10px;
  }

  .stack-table thead { display: none; }
  .stack-table tbody { display: block; width: 100%; }

  .stack-table tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 10px;
    padding: 12px;
    margin-bottom: 10px;
    background: var(--bg-surface-1);
    border: 1px solid var(--border-subtle) !important;
    border-radius: var(--radius-md);
  }

  .stack-table tr:last-child { margin-bottom: 0; }

  .stack-table td {
    display: block;
    min-width: 0;
    border: 0 !important;
  }

  .stack-table td.st-media,
  .stack-table td.st-main,
  .stack-table td.st-meta,
  .stack-table td.st-actions { padding: 0 !important; }

  .stack-table td.st-media {
    flex: 0 0 auto;
    line-height: 0;
  }

  /* basis leaves less room than the row has, so the meta cells always wrap */
  .stack-table td.st-main { flex: 1 1 calc(100% - 80px); }

  .stack-table td.st-meta {
    flex: 0 0 auto;
    font-size: 12px;
    color: var(--text-muted);
  }

  .stack-table td.st-meta + td.st-meta::before {
    content: '\00B7';
    margin-right: 8px;
    color: var(--border-medium);
  }

  .stack-table td.st-full {
    flex: 1 1 100%;
    text-align: center;
  }

  .stack-table td.st-actions {
    flex: 1 0 100%;
    display: flex;
    gap: 8px;
    text-align: left !important;
    padding-top: 10px !important;
    border-top: 1px solid var(--border-subtle) !important;
  }

  .stack-table td.st-actions > * {
    flex: 1 1 0;
    margin: 0 !important;
  }

  .stack-table td.st-hide-sm { display: none; }
}


/* ------------------------------------------------- news article body (public) */
/* The stored body is whitelist-sanitised on save (engine/core/HtmlSanitizer.cs), so
   PublicUi renders it as real markup instead of escaping it. `pre-line` on the wrapper
   is for the older plain-text articles seeded before the rich editor worked — their
   paragraph breaks are bare newlines and would otherwise collapse into one block. The
   block children reset to `normal` so authored HTML does not get doubled breaks. */

.news-body {
  font-size: 15px;
  line-height: 1.7;
  white-space: pre-line;
}

.news-body p,
.news-body li,
.news-body h1,
.news-body h2,
.news-body h3,
.news-body h4,
.news-body blockquote { white-space: normal; }

.news-body p { margin: 0 0 12px; }
.news-body p:last-child { margin-bottom: 0; }

.news-body ul,
.news-body ol { margin: 0 0 12px 22px; }

.news-body li { margin-bottom: 4px; }

.news-body h1,
.news-body h2,
.news-body h3,
.news-body h4 {
  font-family: var(--font-title);
  font-weight: 700;
  margin: 18px 0 8px;
}

.news-body h1 { font-size: 20px; }
.news-body h2 { font-size: 18px; }
.news-body h3 { font-size: 16px; }
.news-body h4 { font-size: 15px; }

.news-body a { color: var(--primary); }

.news-body img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  margin: 6px 0;
}

.news-body blockquote {
  margin: 0 0 12px;
  padding: 8px 14px;
  border-left: 3px solid var(--primary);
  background: var(--bg-surface-2);
  color: var(--text-sub);
}

/* ------------------------------------------------- news editor (admin) */
/* This grid used to be an inline style= on the page builder, which no media query can
   beat. On a 412px phone that left a 216px main column and a 138px sidebar: the date
   input clipped its own value, "(Published)" fell off the status select, and the
   summary boxes were about four characters wide. As a class the 800px breakpoint —
   the same one theme.css and admin-shell.css use — can collapse it. Desktop is
   unchanged: 2fr / 1fr with the same gap. */

.news-edit-grid {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 1.25rem;
}

@media (max-width: 800px) {
  .news-edit-grid { grid-template-columns: 1fr; }

  /* The Save button lives in the page header, and the form runs several screens deep,
     so saving from the bottom meant scrolling all the way back up. On phones the
     header's action block detaches into a bar pinned to the bottom of the viewport;
     the page reserves room for it so the last panel is never covered. */
  .news-edit-page { padding-bottom: 76px; }

  .news-edit-actions {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 300;
    padding: 10px 16px;
    background: var(--bg-surface-1);
    border-top: 1px solid var(--border-subtle);
    box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.35);
  }

  .news-edit-actions .btn { width: 100%; }
}


/* ------------------------------------------- filter tabbar: wrap on phones */
/* .tabbar is a nowrap flex row with overflow-x:auto. On a 412px screen the
   status filters measured 673px of chips inside a 374px box, so half of them
   sat off-screen behind a horizontal scrollbar that a thumb has to find first.
   Below the breakpoint the ROW wraps onto as many lines as it needs. Each chip
   keeps its own white-space:nowrap (set above) so a label never breaks
   mid-word. Desktop keeps the single scrolling line it always had.

   This is shared with the category tabs in AdminOpsUi / AdminIntakeUi, where
   wrapping is wanted for the same reason. */

@media (max-width: 800px) {
  .tabbar {
    flex-wrap: wrap;
    overflow-x: visible;
    padding-bottom: 0;
  }

  /* a chip wider than the screen would still push the page sideways */
  .tabbar a, .tabbar button { max-width: 100%; }
}


/* ------------------------- stacked-table roles added for the school list */
/* /admin-contingents has eight columns — 722px of table inside a 372px box, so
   Fee, Application and Actions were all off the right edge. It opts into the
   .stack-table card fallback above, but needs three roles the news list did
   not:

     .st-lead    a short TEXT token leading the top row (the school code) where
                 the news table had a thumbnail. It cannot reuse .st-media,
                 which zeroes line-height for an image and would flatten text.
     .st-status  a cell that owns a full-width line of its own. The application
                 status is the thing an officer scans a screening list for, so
                 it does not belong in the middot-separated meta run.
     .st-label   an inline bilingual caption emitted inside a meta cell. The
                 news meta cells were self-describing; these are not — 112
                 people, 78 entries and 930.00 of fees are three bare numbers
                 that read identically without a caption, and the card has no
                 column headers to borrow from.

   .st-label is display:none by default because on the desktop table the column
   header already names the value and repeating it in every cell would be pure
   noise; the media query is what turns it on. That default is the only rule
   here outside the breakpoint, and it applies to a span that exists nowhere
   else, so the desktop tables are unchanged. */

.st-label { display: none; }

@media (max-width: 800px) {
  .stack-table td.st-lead {
    flex: 0 0 auto;
    padding: 0 !important;
    align-self: flex-start;
  }

  .stack-table td.st-status {
    flex: 1 1 100%;
    padding: 0 !important;
    text-align: left !important;
  }

  /* caption + value must not be split across lines, so each meta item stays
     whole and wraps as a unit */
  .stack-table td.st-meta { white-space: nowrap; }

  /* The news card's two meta cells fitted on one line, so the inherited middot
     between them was enough. Four labelled items wrap onto a second line, and
     the middot then leads that line looking like a bullet whose item went
     missing. A table that captions its own meta cells opts the separator out
     with .st-labelled and lets a wider column gap do the separating instead. */
  .stack-table.st-labelled tr { column-gap: 16px; }
  .stack-table.st-labelled td.st-meta + td.st-meta::before {
    content: none;
    margin-right: 0;
  }

  .stack-table td.st-meta .st-label {
    display: inline;
    margin-right: 5px;
    font-family: var(--font-body);
    font-weight: 600;
    color: var(--text-muted);
  }
}


/* -------------------------------------------------- responsive layout primitives */
/* Layout in this app has historically lived in inline style= attributes
   (`style='display:flex; align-items:center; gap:0.5rem;'`, scattered through the
   C# page builders). An inline declaration outranks every stylesheet rule,
   including one inside a media query, so a row built that way physically CANNOT
   be restacked for a phone. That single fact is the cause of the sideways-scroll
   bugs fixed so far. These classes are the replacement: put layout here, never in
   a style= attribute.

   The breakpoint is 800px — the same one .stack-table above uses. Do not add
   another one.

     .row          A horizontal cluster that WRAPS. The default for any run of
                   controls, badges or links. `.rowflex` is the LEGACY name for
                   exactly this rule; it is kept as a second selector because ~20
                   call sites across AdminOpsUi, AdminIntakeUi, PrintUi, PublicUi
                   and AccountUi still emit it. Do not rename those — share the
                   rule instead. Prefer `.row` in new markup.
     .row-between  .row with its children pushed to the two ends.
     .row-tight    A cluster whose parts are one unit and must not be split onto
                   separate lines — an icon and its input, a label and its value,
                   a pair of paired buttons. Narrower gap, and it does not wrap.
     .toolbar      A filter / search / action bar. Behaves like .row on desktop;
                   below 800px every DIRECT child takes a full line of its own, so
                   selects and buttons stop fighting over one 400px row. Controls
                   must therefore BE direct children — bury one inside a plain
                   <div> and it will not stack.
     .row-grow     The one child of a .row or .toolbar that should absorb the
                   leftover width (typically the search box). The flex-basis is a
                   preferred width, not a floor: min-width:0 lets it shrink or wrap
                   instead of shoving the row past the viewport, which is exactly
                   what the `min-width:260px` it replaces used to do.

   Spacing is in raw px because css/theme.css :root defines colour, radius and
   font tokens but no spacing scale; the px values match the ones .stack-table
   already uses. */

/* `.rowflex` is the legacy name kept because it has existing consumers. */
.row,
.rowflex {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.row-between {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
}

.row-tight {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
}

.toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}

.row-grow {
  flex: 1 1 220px;
  min-width: 0;
}

/* A form control carries an intrinsic width. Without this it refuses to shrink
   below that width and overflows its row instead of letting the row wrap. */
.row input,
.row select,
.toolbar input,
.toolbar select,
.row-tight input,
.row-tight select { min-width: 0; }

@media (max-width: 800px) {
  /* One control per line — the entire point of .toolbar. */
  .toolbar > * {
    flex: 1 1 100%;
    min-width: 0;
  }
}


/* ------------------------------------------- category picker: chips OR select */
/* Five ops screens (/admin-entries, /admin-roster, /admin-deductions,
   /admin-scoring, /admin-results) let the officer choose one of the tournament's
   categories. With 45 of them the chip row measured 412px tall on /admin-roster
   and 572px on /admin-entries at 412x892 — on /admin-roster the page's own
   content did not begin until y=738 of an 862px viewport. Wrapping is the right
   answer for the SEVEN status chips on /admin-contingents and /admin-screening
   and the wrong one here; forty-five chips are a list, not a tab strip.

   So AdminOpsUi.CategoryPicker renders the choice twice and this pair of rules
   picks which one is real:

     .tabbar.tabbar-cat   the chips. The modifier exists purely so this rule can
                          reach the category tabbar and NOT the status one, which
                          stays a bare .tabbar and keeps its wrapping.
     .cat-picker          a .form-group wrapping one <select> that navigates to
                          the same ?cat=<id> URL the chips link to.

   Above the breakpoint the select is display:none and the chips are untouched —
   desktop is byte-for-byte what it was. Below it they swap. Nothing here is
   device detection: both forms are always in the HTML and CSS decides. */

.cat-picker { display: none; }

@media (max-width: 800px) {
  .tabbar.tabbar-cat { display: none; }

  /* .form-group's own 16px matches the margin the chip row used to leave, so the
     gap before the first card is unchanged. */
  .cat-picker { display: block; }
}

/* A4 at 96dpi is under 800px wide, so the rule above would otherwise switch the
   select ON in print — where the @media print block already drops .tabbar as
   navigation chrome. The picker is the same chrome, so it goes too. */
@media print {
  .cat-picker { display: none !important; }
}


/* -------------------- stacked-table role added for the three ops tables */
/* /admin-entries (368px of table in a 330px box), /admin-scoring (476/330) and
   /admin-results (460/330) each opt into the .stack-table card fallback above.
   Between them they need one role the news and school lists did not:

     .st-line   a cell that owns a full-width line of its own, aligned left, and
                WRAPS.

   The performer list, a judge's outstanding marks and a tie ruling are all
   sentence-length runs of text. They cannot be .st-meta — those are
   white-space:nowrap and share one line, so a long value would push the card
   sideways, which is the very bug being fixed. They cannot be .st-full either;
   that centres its text, which is right for a colspan empty state and wrong for
   prose. .st-status has exactly the right shape but is named for the one thing
   it carries on the school list, so .st-line is the neutral sibling.

   .st-label is enabled inside .st-line for the same reason it is enabled inside
   .st-meta: a card has no column header to borrow a caption from, and a bare
   number or name is not self-describing once the header is gone. */

@media (max-width: 800px) {
  .stack-table td.st-line {
    flex: 1 1 100%;
    padding: 0 !important;
    text-align: left !important;
    white-space: normal;
  }

  .stack-table td.st-line .st-label {
    display: inline;
    margin-right: 5px;
    font-family: var(--font-body);
    font-weight: 600;
    color: var(--text-muted);
  }
}


/* ------------------- bilingual label in a squeezed table cell (phones) */
/* /admin-print's per-category table is one name column plus three print-button
   columns. At 412px the name column gets 112px, so its .t2 pair had 88px of
   content box and reported scrollWidth 91 — a real 3px overflow, on 17 rows.
   The line that overflowed was Chinese: UAX#14 forbids a break before a closing
   full-width bracket, so `云（幼小组）` is one 91px unbreakable run and Chromium
   laid it out past the edge rather than breaking it.

   `overflow-wrap: anywhere` permits a break inside that run, but ONLY when the
   line has no acceptable break point left — normal CJK and word breaking are
   untouched everywhere the text already fits. It has to sit on `.t2` itself, not
   on the inner `.t2-cn`: `.t2` is the inline-block that establishes the line
   boxes, and the property is read from the block container, not the inline span.
   Verified: setting it on `.t2-cn` changes nothing.

   Scoped to table cells below the breakpoint. Above it, cells have room and the
   property would be inert anyway; keeping it out means desktop min-content
   sizing — which `anywhere` does influence — is provably unchanged. */

@media (max-width: 800px) {
  .data-table td .t2 { overflow-wrap: anywhere; }
}

/* --------------------------------- tournament directory (/admin-tournaments) */
/* AdminUi.Tournaments() renders one .panel per tournament plus a .toolbar filter
   bar. Everything here is layout the old inline style= attributes owned, moved out
   so the 800px media query can actually reach it.

   The one genuinely new piece is .sd-hide. The client-side filter has to hide a
   card, and the obvious `card.style.display = 'none'` would leave an inline
   `display` on the element the moment it is shown again (CategoryList sets
   'flex' back). An inline display outranks every rule in this file including the
   media queries above, so the filter would quietly re-break the phone layout of
   any card the officer had filtered and then unfiltered. A class cannot: it
   toggles off cleanly and leaves .row-between owning display again. */

.sd-tourn-head { margin-bottom: 20px; }
.sd-tourn-filters { margin-bottom: 20px; }

.sd-tourn-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* The card itself is .panel .panel-pad .row-between; only the two halves need
   sizing. The identity half absorbs the width and may wrap its own contents; the
   action half stays its natural size at the far end. */
.sd-tourn-id {
  flex: 1 1 320px;
  min-width: 0;
}

.sd-tourn-name {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--text-main);
}

.sd-tourn-meta {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-bottom: 8px;
}

.sd-tourn-badges { gap: 6px; }

.sd-tourn-side {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

/* An archived tournament cannot be opened as a workspace (Ctx.Workspace filters
   on archived = 0), so it must not read as one of the live options. Dashed border
   plus a muted body distinguishes it without inventing a colour. */
.sd-tourn-card.is-archived {
  border-style: dashed;
  opacity: 0.62;
}

/* Stands in for the View Details button on an archived row: same slot, plainly
   not a control. */
.sd-tourn-nogo {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-muted);
}

/* A date caption and its control are one unit and must not be split across lines,
   so this is .row-tight's shape with the input allowed to absorb what is left.
   .form-input carries width:100%, which inside a flex row would size the input to
   the whole field and push the caption out; flex-basis replaces it. */
.sd-date-field {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px;
  white-space: nowrap;
}

.sd-date-field > .form-input {
  width: auto;
  flex: 1 1 140px;
  min-width: 0;
}

/* Filter visibility. !important is deliberate and is the whole point: it has to
   beat the display that .row-between / .row / .alert set on the same element. */
.sd-hide { display: none !important; }

@media (max-width: 800px) {
  /* .toolbar has already given each date field a full line of its own; let the
     control use the rest of it instead of sitting at its intrinsic width. */
  .sd-date-field > .form-input { flex: 1 1 auto; }

  /* Below the breakpoint the two halves of a card are stacked by .row-between's
     wrap, so the action sits under the details rather than beside them. */
  .sd-tourn-side { width: 100%; }
}


/* ------------------------------- category card live state (/admin) */
/* AdminUi.CategoryList renders one .sd-cat-card per category. AdminUi.LiveState
   picks exactly ONE state per card —

     公布 Published > 成绩已锁 Result Locked > 进分中 Scoring > 进分完毕 Finished

   — and emits it as BOTH the modifier class below and a badge inside the card, so
   the tint and the words are always the same value and cannot drift apart.

   The treatment is the one this codebase already uses for state (.sd-master-switch
   .on/.off in admin-shell.css, .badge-* in theme.css): a translucent *-bg tint plus
   the matching *-border, never a filled panel. The theme is dark — a saturated red,
   yellow, green or blue card would take --text-main from ~12:1 against the panel
   down to a few points and the grid would stop looking like the rest of the app.

   The tint is layered as a gradient IMAGE over the panel's own background COLOUR so
   --bg-surface-2 keeps sitting underneath. The *-bg tokens are rgba: used as a
   background colour on their own they would let whatever is behind the card show
   through, and the same state would render a different shade depending on where the
   card landed.

   There is no blue token in theme.css. The only blue-ish family there is the
   primary cyan the entire admin chrome already uses, so a locked card tinted with
   it would read as chrome, not as a state. theme.css is owned by another agent and
   cannot be edited in this change, so the three blue values are declared here
   instead. They belong beside the --danger / --warning / --success trios in
   theme.css's :root and should be moved there when that file is next open. */

:root {
  --sd-locked: #60a5fa;
  --sd-locked-bg: rgba(59, 130, 246, 0.14);
  --sd-locked-border: rgba(59, 130, 246, 0.4);
}

/* Layout the card used to carry in a style= attribute. An inline style cannot be
   reached by the 800px media query — the origin of every mobile bug in this app —
   and the filter script now toggles .sd-hide instead of writing `display` back onto
   the element, so the card keeps its layout after being filtered and unfiltered. */
/* Grid layout for the category card container. On desktop, displays as a
   responsive multi-column grid; on mobile (<800px), falls back to a single
   column with the app's standard vertical stacking. */
.sd-cat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 1rem;
}

.sd-cat-card {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 1rem;
  background: var(--bg-surface-2);
  border: 1px solid var(--bg-surface-3);
  border-radius: 0.5rem;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.sd-cat-card:hover {
  border-color: var(--border-accent);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}

/* Each pair overrides .panel's background shorthand (which resets background-image
   to none) and its border colour. Both rules are later in the file AND one class
   more specific, so they win on both counts. */
    /*.sd-cat-card.is-scoring {
        background-image: linear-gradient(var(--danger-bg), var(--danger-bg));
        border-color: var(--danger-border);
        color: black;
    }

    .sd-cat-card.is-scored {
        background-image: linear-gradient(var(--warning-bg), var(--warning-bg));
        border-color: var(--warning-border);
        color: black;
    }

    .sd-cat-card.is-locked {
        background-image: linear-gradient(var(--sd-locked-bg), var(--sd-locked-bg));
        border-color: var(--sd-locked-border);
        color: black;
    }

    .sd-cat-card.is-published {
        background-image: linear-gradient(var(--success-bg), var(--success-bg));
        border-color: var(--success-border);
        color: black;
    }*/

    .sd-cat-card.is-scoring {
        background-image: linear-gradient(#4a121a, #4a121a);
        border-color: #842029;
        color: #ffffff;
    }

    .sd-cat-card.is-scored {
        background-image: linear-gradient(#4d3800, #4d3800);
        border-color: #664d03;
        color: #ffffff;
    }

    .sd-cat-card.is-locked {
        background-image: linear-gradient(#212529, #212529);
        border-color: #495057;
        color: #ffffff;
    }

    .sd-cat-card.is-published {
        background-image: linear-gradient(#0f381e, #0f381e);
        border-color: #0f5132;
        color: #ffffff;
    }

    /*.sd-cat-card.is-scoring {
        background-image: linear-gradient(#721c24, #721c24);
        border-color: #a71d2a;
        color: #ffffff;
    }

    .sd-cat-card.is-scored {
        background-image: linear-gradient(#856404, #856404);
        border-color: #b8860b;
        color: #ffffff;
    }

    .sd-cat-card.is-locked {
        background-image: linear-gradient(#343a40, #343a40);
        border-color: #6c757d;
        color: #ffffff;
    }

    .sd-cat-card.is-published {
        background-image: linear-gradient(#155724, #155724);
        border-color: #1e7e34;
        color: #ffffff;
    }*/

/* The badge that says in words what the tint says in colour. Around 1 in 12 men
   cannot separate red from green, and red/green here is exactly the 进分中 /公布
   pair — the two an officer most needs to tell apart. So this badge is the primary
   signal and the tint is the shortcut, not the other way round.

   It also overrides the badge's TEXT colour to --text-main. The .badge-* set paints
   its label in the state colour itself, which on this tint measures 3.11:1 for
   danger and 3.92:1 for success — fine for a decorative tag, not fine for the one
   label a colour-blind officer has to read instead of the colour. The state is
   still carried by the badge's tint and border, so nothing is lost; only this badge
   is changed, every other .badge-* on the page keeps the theme's treatment.

   The selector is `.badge.sd-cat-state`, not `.sd-cat-state`: .badge-locked below
   lives in THIS file and would otherwise win the colour on equal specificity by
   coming later. Measured — with the single class it rendered 4.06:1, with this one
   it renders 11.2:1. */
.badge.sd-cat-state {
  font-weight: 700;
  color: var(--text-main);
}

.sd-cat-state i { margin-right: 4px; }

/* Blue badge, same shape as the .badge-* set in theme.css. Used for 锁定 Lock on
   the /admin-category status table so "locked" reads the same colour there as the
   card tint on /admin. */
.badge-locked {
  background: var(--sd-locked-bg);
  color: var(--sd-locked);
  border: 1px solid var(--sd-locked-border);
}


/* ------------------------------ 分节操作行 section action row (/admin-category) */
/* Action row inside an accordion section: the workflow buttons plus the link to
   that section's dedicated editor page. .row already wraps, so a fourth button
   drops to a second line on a phone instead of widening the panel.

   The 状态 Status table's rules (.sd-status, .sd-status-no, .sd-status-basis,
   .sd-status-table) stood here until the flat table was replaced by the vertical
   pipeline stepper. Nothing else referenced them, so they were removed with it —
   the pipeline's own rules are at the END of this file. */
.sd-sec-actions { margin-top: 1rem; }


/* ------------------- 项目 Details definition list (/admin-category) */
/* AdminUi.RenderCategoryDetails renders the whole category definition as one
   .deflist. The base rule is `max-content 1fr`, which is right on a desk: the
   caption column sizes to the longest bilingual label and every value lines up.

   At 412px it is wrong. "Max Teams per Contingent" and "Participants per Team"
   are the longest captions and max-content refuses to shrink below them, so the
   caption column claims ~190px of a ~330px content box and the value column is
   left with too little to wrap a bilingual name into — the grid then overflows
   rather than wrapping. Below the breakpoint each pair becomes two stacked
   lines instead, caption above value, which is the same shape .stack-table gives
   its cards on the rest of this page.

   Scoped to .sd-cat-details, NOT to .deflist: the plain .deflist is used by
   /admin-category-edit's "carried through unchanged" panel and by the tournament
   pages, whose captions are short and already fit. */

@media (max-width: 800px) {
  .sd-cat-details {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .sd-cat-details dt { margin-top: 12px; }
  .sd-cat-details dt:first-child { margin-top: 0; }

  /* A category name can be one unbreakable CJK run (see the .data-table rule
     above for the full explanation); permit a break inside it when the line has
     no other option left. */
  .sd-cat-details dd { overflow-wrap: anywhere; }
}


/* ----------------------------- criteria editor (/admin-category-criteria) */
/* One card per criterion, added and removed entirely in the browser by
   js/admin-category-criteria.js and applied by a single Save. Layout lives here
   rather than in that script or in a style= attribute, so the 800px media query
   can still reach it — a card built with element.style.display would be beyond
   the reach of every rule in this file.

   The card is a .panel already; these rules only arrange its inside. The three
   fields sit on one line on a desk and take a line each on a phone, which is
   what .form-row's `repeat(auto-fit, minmax(200px, 1fr))` does on its own — so
   the fields reuse .form-row and only the head strip is new. */

.sd-crit-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.sd-crit-card { padding: 1rem; }

/* The hint + running total strip under the last card. */
.sd-crit-foot { margin-top: 12px; }

/* Card number on the left, delete button hard right. .row-between wraps, so on a
   narrow card the button drops under the number instead of squeezing it. */
.sd-crit-head { margin-bottom: 12px; }

.sd-crit-no {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--text-muted);
}

/* The running total of every card's Max Score, recomputed in the browser on each
   keystroke. It is advisory, not a gate: Api.CriteriaTotals already tells the
   officer when a category does not add to 100 and explicitly allows it. */
.sd-crit-total {
  font-weight: 700;
  color: var(--text-main);
}

.sd-crit-total .sd-crit-sum { font-family: var(--font-mono); }

/* Read-only rendering, used when the server would refuse the save anyway
   (scores already exist, or the category is Mode B). Values are shown as plain
   text with no inputs and no Save button. */
.sd-crit-ro-val {
  font-family: var(--font-mono);
  color: var(--text-main);
}



/* ================= 名单操作 & 审计记录 (/admin-category sections 4 and 5) ======= */
/* Added with the Freeze Roster wiring. Everything here is layout that used to live in
   an inline style= attribute (and was therefore unreachable from the breakpoint below)
   or is new markup that section 4 did not have before. */

/* Why an action is not offered. AdminUi.RenderRosterActions prints the Workflow gate's
   reason where the button would have been, the same shape AdminCategoryOrderUi uses for
   its blocked list. Sits under .sd-sec-actions, so it needs its own top margin. */
.sd-sec-why {
  margin-top: 0.75rem;
  margin-bottom: 0;
  padding-left: 1.25rem;
  font-size: 13px;
  line-height: 1.5;
}

.sd-sec-why li + li { margin-top: 4px; }

/* The entries a freeze would leave out, named inside the standing warning. These are
   school and team names — client data — so they wrap rather than truncate: an officer
   checking whether their school is on this list must be able to read all of it. */
.sd-freeze-list {
  margin: 0.6rem 0 0;
  padding-left: 1.25rem;
  line-height: 1.9;
}

.sd-freeze-list li { overflow-wrap: anywhere; }

/* ---- 审计记录 Audit trail (section 5) */
/* Two lines per record: a fixed header (when · who · what) and, only when one was
   typed, the officer's reason underneath. The rule the C# used to carry inline was a
   single undifferentiated run with a bottom border. */
.sd-audit { font-size: 0.825rem; }

.sd-audit-row {
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--bg-surface-3);
}

.sd-audit-row:last-child { border-bottom: 0; }
.sd-audit-row:first-child { padding-top: 0; }

.sd-audit-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 8px;
}

/* Timestamps are field values, so monospace and muted — the same treatment
   .sd-status-basis gives the backing columns in the 状态 Status table above. */
.sd-audit-at {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
}

/* The action key is a stored constant ('roster-frozen', 'order-drawn'), not prose. */
.sd-audit-act {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-sub);
}

/* Free text an officer typed. It is the only part that can be long. */
.sd-audit-why {
  margin-top: 2px;
  color: var(--text-sub);
  overflow-wrap: anywhere;
}


@media (max-width: 800px) {
  /* .sd-sec-actions is a .row, which already wraps — but wrapping only moves a whole
     control, it does not stop two of them sharing a line and being squeezed to their
     text. Measured at 412px: the row is 332px, so 锁定名单 Freeze Roster and 编辑顺序
     Edit Order each settled at 162px and the longer bilingual label broke mid-phrase
     ("...Freeze" / "Roster").

     The basis is therefore deliberately LARGER than half the row. Two of them cannot
     share a line, so each takes one and grows to the full width — one full-width
     control per line, no label ever broken, and a tap target the width of the panel.
     A single-control row (section 3's Edit Judge) simply becomes full width, which is
     what it already looked like. */
  .sd-sec-actions > .btn { flex: 1 1 180px; }

  /* The header of an audit record is four short tokens; keeping them on one wrapped
     line rather than a flex row that can leave a lone word stranded reads better in a
     ~306px column. */
  .sd-audit-head { display: block; }
  .sd-audit-head strong { margin-right: 6px; }
}


/* ============ 全屏对话框 full-screen dialog (/admin-contingents) ============== */
/* AdminIntakeUi.AddContingentDialog emits this markup with the page, hidden behind
   .sd-hide, and js/admin-contingents.js only ever toggles that one class.

   It is NOT the shared sdOpenModal host. That one is .modal-card — a 440px confirm /
   typed-reason card used by every action button in the back office — and widening it
   to hold a four-field form would resize every confirmation dialogue in the
   application. This is a separate layer that owns the whole viewport, which is the
   point: at 412px the four fields get the full screen instead of a card inside a card
   inside a scrolling page.

   z-index sits ABOVE .modal-overlay's 2000 so that if a confirm is ever raised from
   inside this dialog it does not appear behind it. */

.sd-fsdialog {
  position: fixed;
  top: 0; right: 0; bottom: 0; left: 0;
  z-index: 2100;
  background: var(--bg-dark);
  display: flex;
  flex-direction: column;
}

/* Head and foot are fixed bars; only the body between them scrolls, so Save and Close
   are always reachable without scrolling to the end of the form. */
.sd-fsdialog-head,
.sd-fsdialog-foot {
  flex: 0 0 auto;
  padding: 12px 16px;
  background: var(--bg-surface-1);
}

.sd-fsdialog-head { border-bottom: 1px solid var(--border-subtle); }
.sd-fsdialog-foot { border-top: 1px solid var(--border-subtle); }

.sd-fsdialog-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 16px;
}

/* One shared measure for the three bars, so the title, the fields and the buttons all
   line up on a wide screen instead of the form floating alone in the middle. */
.sd-fsdialog-inner {
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
}

.sd-fsdialog-title {
  font-family: var(--font-title);
  font-size: 18px;
  font-weight: 800;
  margin: 0;
}

/* One field per line, always. .form-row's auto-fit would put two or three of these on
   one line inside a 560px measure, and the sketch this implements is a vertical list
   of four labelled boxes. */
.sd-fsdialog-fields {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

@media (max-width: 800px) {
  /* Two buttons, each at least 140px, so at 412px they share one line comfortably and
     never squeeze a bilingual label into a mid-phrase break. */
  .sd-fsdialog-foot .btn { flex: 1 1 140px; }
}


/* ------------------------------------------ 前台说明 frontend note (shared) */
/* The standing note that the office does not edit a contingent's own data. The two
   languages are two COMPLETE sentences on two lines — never interleaved word by word,
   which leaves neither language readable. */

.sd-frontend-note {
  margin: 1rem 0 0;
  font-size: 12px;
  line-height: 1.65;
  color: var(--text-muted);
}

.sd-frontend-note span { display: block; }
.sd-frontend-note .sd-note-en { opacity: 0.85; }


/* ================= 只读参赛单位页 read-only contingent (/admin-contingent) === */
/* Three sections — details, participants, categories — none of which edits anything.
   Everything here replaces a style= attribute the previous editable version carried
   inline, where no breakpoint could reach it. */

/* Section icon in a .card-title. Colour only; the tokens live in css/theme.css. */
.sd-sec-icon { color: var(--primary); }

/* The contingent code above the page title. */
.sd-ro-code {
  font-size: 12px;
  font-weight: 700;
  color: var(--primary);
}

.sd-ro-lastnote {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0 0 10px;
  overflow-wrap: anywhere;
}

/* A standing explanation above a list — currently the one that says 种族 Race has no
   backing column. */
.sd-ro-hint {
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-muted);
  margin: 0 0 12px;
}

/* Same treatment .sd-cat-details gets on /admin-category: `max-content 1fr` is right
   on a desk, and wrong at 412px where "参赛单位代码 Contingent Code" refuses to shrink
   and leaves the value column too narrow to wrap a school name into. Below the
   breakpoint each pair becomes two stacked lines, caption above value. Scoped to
   .sd-cont-details, not to bare .deflist, which other pages still rely on. */
@media (max-width: 800px) {
  .sd-cont-details {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .sd-cont-details dt { margin-top: 12px; }
  .sd-cont-details dt:first-child { margin-top: 0; }

  /* A contingent name can be one unbreakable CJK run; permit a break inside it when
     the line has no other option left. */
  .sd-cont-details dd { overflow-wrap: anywhere; }
}

/* One card per participant and one per entry. auto-fill with a 260px floor gives a
   single full-width column on a phone and as many as fit on a desk, with no fixed
   count to go wrong in between. */
.sd-ro-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 12px;
}

.sd-ro-block { padding: 0.9rem 1rem; }

/* Participant names are client data: rendered exactly as stored, so they must wrap
   rather than truncate — an officer checking a name against a form has to read all
   of it. */
.sd-ro-name {
  font-weight: 700;
  color: var(--text-main);
  overflow-wrap: anywhere;
}

.sd-ro-name-en {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
}

/* The per-participant fact list. Its captions are short enough that `max-content`
   holds at 412px, unlike the details list above. */
.sd-ro-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
  margin-top: 8px;
  font-size: 13px;
}

.sd-ro-facts dt { color: var(--text-muted); }

.sd-ro-facts dd {
  margin: 0;
  color: var(--text-sub);
  overflow-wrap: anywhere;
}

/* Category header: code, name and the participant count. It wraps rather than
   truncating — a category name is one long CJK run and the count belongs beside it,
   not clipped off the right edge. */
.sd-ro-cat-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
}

.sd-ro-cat-code {
  font-weight: 700;
  color: var(--primary);
}

.sd-ro-cat-name {
  font-weight: 600;
  color: var(--text-main);
  overflow-wrap: anywhere;
}

.sd-ro-cat-total {
  font-size: 12px;
  color: var(--text-muted);
}

.sd-ro-team {
  margin-top: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-sub);
}

.sd-ro-members {
  margin-top: 2px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-main);
  overflow-wrap: anywhere;
}

/* Small top gap, pairing with the existing .mb12/.mb20 spacing helpers. */
.mt12 { margin-top: 12px; }

/* ============ 阶段流程 Pipeline stepper (/admin-category) ==================== */
/* The six-stage lifecycle as a VERTICAL list with a connector line running down the
   left, replacing the flat status table. Vertical, not horizontal, because the flow
   is read on a phone in a sports hall: six stages laid sideways were what produced
   the 1017px-wide strip inside a 332px column that the status table was built to
   escape, and a vertical stepper is the shape a narrow column already wants.

   The connector is PURE CSS — one absolutely positioned border-left per row, drawn
   from the bottom of that row's marker to the bottom of the row, so it meets the
   next marker exactly. No JavaScript measures anything, and the line is KEPT on
   mobile: it is the whole point of the visual map, and the phone is the primary
   target here, not an afterthought.

   COLOUR IS NEVER THE ONLY SIGNAL. Each state also has its own icon inside the
   marker (check / open padlock / play / closed padlock) and its own badge saying
   the state in Chinese and English. Roughly one man in twelve cannot separate red
   from green, and this is a status display — the words are the signal and the
   colour is the shortcut, not the reverse. */

.sd-pipeline { margin-bottom: 1.5rem; }

.sd-pl {
  list-style: none;
  margin: 0;
  padding: 0;
}

.sd-pl-step {
  position: relative;
  padding: 0 0 22px 46px;
}

.sd-pl-step:last-child { padding-bottom: 0; }

/* The connector segment BELOW this row. Its style says what has already happened:
   solid through a completed stage, dashed from the active one downwards — so the
   line itself shows where certainty ends. */
.sd-pl-step::before {
  content: '';
  position: absolute;
  left: 15px;
  top: 34px;
  bottom: 0;
  border-left: 2px dashed var(--border-medium);
}

.sd-pl-step:last-child::before { display: none; }

.sd-pl-step.is-done::before {
  border-left-style: solid;
  border-left-color: var(--success);
}

/* 项目 Details is derived and never blocks anything downstream, so its segment is
   solid — progress genuinely flows through it — but not green, because no
   transition has been completed. */
.sd-pl-step.is-open::before { border-left-style: solid; }

.sd-pl-marker {
  position: absolute;
  left: 0;
  top: 0;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  border: 2px solid var(--border-medium);
  border-radius: 50%;
  background: var(--bg-surface-2);
  color: var(--text-muted);
  font-size: 13px;
}

.is-done .sd-pl-marker {
  background: var(--success);
  border-color: var(--success);
  color: var(--bg-surface-1);
}

.is-open .sd-pl-marker {
  border-color: var(--primary);
  color: var(--primary);
}

/* The one row the officer is meant to look at: filled, with a halo ring. */
.is-now .sd-pl-marker {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--bg-surface-1);
  box-shadow: 0 0 0 4px var(--border-accent);
}

.is-wait .sd-pl-marker {
  background: transparent;
  border-style: dashed;
}

/* min-height matches the marker so the first line of text sits level with it and
   the connector starts exactly under the circle. */
.sd-pl-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  min-height: 32px;
}

.sd-pl-no {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--text-muted);
}

.sd-pl-name {
  font-weight: 700;
  color: var(--text-main);
}

.is-done .sd-pl-name { color: var(--text-sub); }
.is-wait .sd-pl-name { color: var(--text-muted); }

.sd-pl-state i { margin-right: 4px; }

/* One line of context: how many teams, how many judges have submitted. */
.sd-pl-info {
  margin-top: 4px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-sub);
  overflow-wrap: anywhere;
}

.is-wait .sd-pl-info { color: var(--text-muted); }

/* The raw columns the row was derived from — evidence for the badge, not the answer
   itself, so monospace and muted. Carried over from the status table it replaced;
   an officer reporting a stuck category can read the actual values off the screen. */
.sd-pl-basis {
  margin-top: 3px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

.sd-pl-basis-lbl { opacity: 0.75; }

.sd-pl-act { margin-top: 10px; }

/* Why an action is not offered, printed where the button would have been. Rendered
   inline rather than as a title= tooltip on purpose: a tooltip needs a hover, and
   the primary target here has no pointer at all. */
.sd-pl-why {
  margin: 8px 0 0;
  padding-left: 1.15rem;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-muted);
}

.sd-pl-why li + li { margin-top: 5px; }

/* Ui.T2 renders an inline-block with vertical-align:middle, which drags the list
   marker down to the midpoint of the Chinese/English pair. Making it a block inside
   this list puts the bullet back beside the first line where it belongs. */
.sd-pl-why > li > .t2 { display: block; }

@media (max-width: 800px) {
  /* The gutter narrows but the line stays. 40px out of a 332px content column still
     leaves ~292px for the row, which fits the longest stage line without wrapping
     the badge onto a line of its own. */
  .sd-pl-step {
    padding-left: 40px;
    padding-bottom: 24px;
  }

  .sd-pl-step::before {
    left: 13px;
    top: 30px;
  }

  .sd-pl-marker {
    width: 28px;
    height: 28px;
    font-size: 12px;
  }

  .sd-pl-head { min-height: 28px; }

  /* Same reasoning as .sd-sec-actions above: a basis deliberately larger than half
     the row means two controls cannot share a line, so each takes the full width and
     no bilingual label is ever broken mid-phrase. */
  .sd-pl-act > .btn { flex: 1 1 180px; }
}
