/* ===========================================================================
   TOLArt — app.css
   Town of Leesburg Art Submission System

   Structure:
     1. Design tokens (TOL standard, adapted — see the two notes below)
     2. Accessibility floor (ADA.md section 3 — DO NOT DELETE these rules)
     3. Layout shell
     4. Forms
     5. Components
     6. Responsive

   Two deliberate departures from the shared TOL design system used by TOLMRpt
   and TOLSEC. Both are accessibility fixes, not preferences:

   (a) Font sizes are in rem, not px. The shared tokens use px (--text-base:14px),
       which violates ADA.md non-negotiable 13 and WCAG 1.4.4 — px text does not
       respond to the browser's font-size setting.

   (b) --color-border (#cbd5e1) measures 1.48:1 on white. That is fine for a
       decorative rule but fails the 3:1 floor WCAG 1.4.11 requires for anything
       that identifies a control or its boundary. Functional borders therefore
       use --color-border-control (#7791B0 — 3.25:1 on white, 3.03:1 on the page
       background). Never put an input border on --color-border.

   Every color below was measured, not eyeballed. Re-measure before changing one.
   =========================================================================== */

/* ===== 1. Design tokens ==================================================== */

:root {
  /* Surfaces */
  --color-bg:            #f5f7fa;
  --color-surface:       #ffffff;
  --color-surface-alt:   #eef2f6;

  /* Borders — see note (b) */
  --color-border:         #cbd5e1;   /* decorative rules only, 1.48:1 */
  --color-border-control: #7791B0;   /* controls and their states, 3.25:1 */

  /* Text — all measured against --color-surface and --color-bg */
  --color-text:        #1f2937;      /* 14.68:1 on white */
  /* Darkened from #6b7280 on 2026-08-12. The old value measured 4.83:1 on white
     but only 4.50:1 on --color-bg (exactly at the threshold, no margin) and
     4.30:1 on --color-surface-alt — a real 1.4.3 FAILURE wherever muted text sat
     on a tinted row, which axe caught on the outcomes grid. This value clears
     4.5:1 on all three surfaces (6.43 white / 5.99 bg / 5.72 surface-alt), so
     the token is safe wherever it is used rather than only on white. */
  --color-text-muted:  #585f6b;      /*  6.43:1 white, 5.99:1 bg, 5.72:1 surface-alt */

  /* Status — all >= 4.5:1 on white */
  --color-primary:       #1f4e79;    /* 8.66:1 */
  --color-primary-hover: #163a5b;
  --color-primary-light: #dce8f5;
  --color-success:       #1f7a3d;    /* 5.37:1 */
  --color-success-bg:    #dcfce7;
  --color-warning:       #a16207;    /* 4.92:1 */
  --color-warning-bg:    #fef9c3;
  --color-danger:        #b42318;    /* 6.57:1 */
  --color-danger-bg:     #fee2e2;
  --color-info:          #1d4ed8;    /* 6.70:1 */
  --color-info-bg:       #dbeafe;

  /* Focus ring — visible on both light and dark surfaces */
  --color-focus: #b45309;            /* 5.02:1 on white */

  /* Per-commission brand. Defaults are COPA; templates/layout.php overrides
     these on :root from the commission row. The admin UI rejects any value
     that fails contrast, so whatever lands here has already been checked. */
  --brand-primary:  #334DA2;         /* COPA blue,  7.71:1 on white */
  --brand-accent:   #C31F26;         /* COPA red,   5.92:1 on white */
  --brand-on-light: #94711A;         /* COPA gold DARKENED to 4.53:1.
                                        The real gold #D9A627 is 2.23:1 and is
                                        decorative fill ONLY — it can never
                                        carry text, a border, an icon, or a
                                        focus ring on a light background. */
  --brand-decorative: #D9A627;       /* large blocks of color, never meaning */

  /* Spacing */
  --sp-1: .25rem; --sp-2: .5rem;  --sp-3: .75rem; --sp-4: 1rem;
  --sp-5: 1.25rem; --sp-6: 1.5rem; --sp-7: 1.75rem; --sp-8: 2rem;

  /* Typography — rem, see note (a) */
  --font-base: 'Segoe UI', system-ui, -apple-system, sans-serif;
  --font-mono: 'Cascadia Code', 'Consolas', monospace;
  --text-sm:   .8125rem;
  --text-base: 1rem;
  --text-lg:   1.125rem;
  --text-xl:   1.375rem;
  --text-2xl:  1.75rem;

  /* Borders and shadow */
  --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px;
  --shadow-sm: 0 1px 3px rgba(0,0,0,.06);
  --shadow-md: 0 2px 8px rgba(0,0,0,.08);

  /* Measure — long prose stays readable */
  --measure: 68ch;
}

/* ===== 2. Accessibility floor (ADA.md section 3) =========================== */
/* Do not delete these rules. */

/* Border-box everywhere. This is a 1.4.10 fix, not housekeeping: form controls
   are `width: 100%` with padding and a border, and under the default
   content-box that makes every input WIDER than the column holding it. At a
   320px viewport the submission form overflowed by 10px and scrolled sideways —
   caught by tests/a11y-public.spec.js, which is why that test exists. */
*, *::before, *::after { box-sizing: border-box; }

/* 1.4.4: base font in a relative unit so browser zoom and user font size work */
html { font-size: 100%; }
body {
  font-size: 1rem;
  line-height: 1.5;
  color: var(--color-text);
  background: var(--color-bg);
  font-family: var(--font-base);
  margin: 0;
}

/* 1.4.12 Text Spacing — content MUST survive these user overrides with no
   clipping: line-height 1.5, letter-spacing .12em, word-spacing .16em,
   p margin-bottom 2em. That means no fixed heights on text containers.
   Use min-height + overflow: visible. */

/* 2.4.7 + 1.4.11: never `outline: none` without this replacement */
:focus-visible {
  outline: 3px solid var(--color-focus);
  outline-offset: 2px;
  border-radius: 2px;
}

/* 2.4.1: skip link — hidden until focused, never display:none */
.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--color-surface); color: var(--color-text);
  padding: .75rem 1rem; z-index: 999;
  border: 2px solid var(--color-focus);
}
.skip-link:focus { left: 0; }

/* Screen-reader-only text. NEVER display:none or visibility:hidden here [F87]. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* 1.4.1: color is never the only cue — status carries an icon and text too */
.status--error::before   { content: "\26A0"; margin-inline-end: .4em; }
.status--success::before { content: "\2713"; margin-inline-end: .4em; }

/* 2.3.1 / 2.2.2: honor the OS reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* 1.4.10 Reflow: nothing forces horizontal scroll at 320px */
img, table, pre, iframe { max-width: 100%; }

/* Wide content scrolls inside its own container, never the page body.

   position: relative is load-bearing, not decoration — it makes this a containing
   block. `.visually-hidden` is `position: absolute !important`, so without it those
   spans are positioned against the viewport, escape the scroller, and extend the ROOT
   scroll width even when the table itself is properly contained. Measured on rank.php
   at 320px: documentElement.scrollWidth was 537 while body.scrollWidth was 320, and 537
   was exactly the right edge of one hidden span inside the table. An accessibility
   helper was causing a reflow failure. */
.scroll-x { overflow-x: auto; position: relative; }

/* ===== 3. Layout shell ===================================================== */

/* Role-testing banner (test servers only — templates/layout.php).
 *
 * Deliberately unlike anything else in the app, because its whole job is to stop
 * a tester mistaking a simulated view for the real one. Measured contrast on
 * #5a3a00: white text 10.29:1, and the white button carries #5a3a00 text at the
 * same ratio. Not sticky — a fixed bar eats the vertical space 1.4.10 reflow
 * needs at 400% zoom. Wraps rather than scrolls at narrow widths.
 *
 * The 1px white inset keeps the bar's edge visible in Windows High Contrast,
 * where background-color is discarded and a colour-only banner would vanish.
 */
.sim-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  background: #5a3a00;
  color: #ffffff;
  box-shadow: inset 0 -1px 0 #ffffff;
}
.sim-banner__what { margin: 0; max-width: none; }
.sim-banner__tag {
  display: inline-block;
  margin-inline-end: var(--sp-2);
  padding: 0 var(--sp-2);
  border: 2px solid #ffffff;
  border-radius: 3px;
  font-size: var(--text-sm); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.sim-banner form { margin: 0 0 0 auto; }
.sim-banner__stop {
  font: inherit; font-weight: 600;
  padding: var(--sp-2) var(--sp-4);
  border: 2px solid #ffffff; border-radius: 4px;
  background: #ffffff; color: #5a3a00;
  cursor: pointer;
}
.sim-banner__stop:hover { background: var(--color-surface-alt); }
.sim-banner__link { color: #ffffff; }
/* The page focus ring (#b45309) measures 2.05:1 against this background, under
   the 3:1 that 1.4.11 requires, so the banner supplies its own at 10.29:1. */
.sim-banner :focus-visible { outline: 3px solid #ffffff; outline-offset: 2px; }

/* Outcomes grid. One <tbody> per artist, so their pieces read as a group and a
   mixed result is visible while the decision is being made. */
.outcomes-table { width: 100%; }
.outcomes-artist + .outcomes-artist { border-top: 2px solid var(--color-border-control); }
.outcomes-artist__head th {
  background: var(--color-surface-alt);
  text-align: start;
  padding-top: var(--sp-4);
}
.outcomes-artist__meta {
  display: block;
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}
.outcomes-medium {
  display: block;
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}
/* The note column should not crowd out the piece title on a narrow screen. */
.outcomes-table input[type="text"] { min-width: 8rem; }

/* Session-expiry warning dialog (WCAG 2.2.1). Native <dialog>, so the browser
   supplies the focus trap, the backdrop and Escape handling. Sized in rem so it
   still fits at 400% zoom, and capped by the viewport rather than a fixed width. */
.session-dialog {
  max-width: min(32rem, calc(100vw - 2rem));
  padding: var(--sp-5);
  border: 2px solid var(--color-border-control);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}
.session-dialog::backdrop { background: rgba(0, 0, 0, .55); }
.session-dialog h2 { margin-top: 0; }
.session-dialog p { max-width: none; }
.session-dialog__note { color: var(--color-text-muted); font-size: var(--text-sm); }
.session-dialog__actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4);
  margin-top: var(--sp-5);
}
.session-dialog__link { color: var(--color-primary); }

header {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  background: var(--color-surface);
  border-bottom: 4px solid var(--brand-primary);
}
/* Renders as <a> for staff and as <p> on public pages (see templates/layout.php),
   so the margin reset matters — a <p> would otherwise push the header open. */
.brand {
  display: flex; align-items: center; gap: var(--sp-3);
  text-decoration: none; color: var(--color-text); margin: 0;
}
.brand-text { display: flex; flex-direction: column; }
/* rem, so the seal grows with the user's text size instead of shrinking beside
   it. flex: none stops it being squeezed when the header wraps at 320px. */
.brand-seal { display: block; width: 3rem; height: 3rem; flex: none; }
.brand-name { font-size: var(--text-sm); color: var(--color-text-muted); }
.brand-app  { font-size: var(--text-lg); font-weight: 600; }

.commission-badge { display: flex; align-items: center; gap: var(--sp-3); margin: 0; font-weight: 600; }
/* Same 3rem box as .brand-seal so the two marks sit level and read as a pair.
   object-fit: contain because a commission logo is whatever shape it is —
   COPA's is 3:4 — and letterboxing it beats distorting it. */
.commission-mark { display: block; width: 3rem; height: 3rem; flex: none; object-fit: contain; }

header nav[aria-label="Primary"] { margin-inline-start: auto; }
header nav ul, footer nav ul {
  display: flex; flex-wrap: wrap; gap: var(--sp-4);
  list-style: none; margin: 0; padding: 0;
}
header nav a { color: var(--color-primary); text-decoration: underline; }
header nav a[aria-current="page"] { font-weight: 700; text-decoration-thickness: 3px; }

/* flex-wrap matters here: this row gained a third item ("Request an enhancement")
   on 2026-10-08, and a non-wrapping flex row of name + link + Sign out overflows
   at 320px. The header itself wraps, which moves this block to its own line, but
   that does not let its CONTENTS break. 1.4.10 — check with documentElement, not
   body. */
.signed-in {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-3); margin: 0; font-size: var(--text-sm);
}

nav[aria-label="Breadcrumb"] { padding: var(--sp-3) var(--sp-5) 0; }
nav[aria-label="Breadcrumb"] ol {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  list-style: none; margin: 0; padding: 0; font-size: var(--text-sm);
}
nav[aria-label="Breadcrumb"] li + li::before { content: "/"; margin-inline-end: var(--sp-2); color: var(--color-text-muted); }

main {
  display: block;
  max-width: 72rem;
  margin: 0 auto;
  padding: var(--sp-6) var(--sp-5) var(--sp-8);
}
main > h1 { font-size: var(--text-2xl); margin-top: 0; }
main p, main li { max-width: var(--measure); }

footer {
  border-top: 1px solid var(--color-border);
  padding: var(--sp-5);
  background: var(--color-surface);
}
footer a { color: var(--color-primary); }
.env-note { color: var(--color-text-muted); font-size: var(--text-sm); margin: var(--sp-3) 0 0; }

a { color: var(--color-primary); }
a:hover { color: var(--color-primary-hover); }

/* Long unbreakable strings must not push the page sideways (1.4.10).
 *
 * An email address and a portfolio URL have no spaces to wrap at, so at 320px
 * they run straight past the viewport and the whole document scrolls — measured
 * on submission.php at documentElement.scrollWidth 385 against a 320 viewport,
 * with `bmcintyre+art022@leesburgva.gov` and an https:// link as the two
 * offenders. It stayed hidden while the only test artist had a short address and
 * no portfolio link.
 *
 * `anywhere`, not `break-word`: break-word does not shrink the element's
 * min-content size, so a long word inside a grid or flex track still forces the
 * track wider. This is scoped to links and description lists rather than set
 * globally, because breaking mid-word is right for a URL and wrong for prose. */
a, dd { overflow-wrap: anywhere; }

/* ===== 4. Forms ============================================================ */

.field { margin-bottom: var(--sp-5); max-width: 40rem; }
.field label { display: block; font-weight: 600; margin-bottom: var(--sp-1); }
.field .req { font-weight: 400; color: var(--color-text-muted); }

/* Persistent hint text, wired by aria-describedby. Never a placeholder,
   never a tooltip — a hover-only tooltip is a mobile dead end (1.4.13). */
.hint {
  margin: 0 0 var(--sp-2);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.field input, .field select, .field textarea,
.field-group input, .field-group select {
  font: inherit;
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--color-border-control);   /* 3:1 — see note (b) */
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}
.field textarea { min-height: 6rem; resize: vertical; }  /* min-height, not height */
.field input[type="file"] { padding: var(--sp-2); }
.field-group input[type="checkbox"],
.field-group input[type="radio"] { width: auto; }

/* Error state carries a border, an icon, and text — never color alone (1.4.1) */
.field--error input, .field--error select, .field--error textarea {
  border-width: 2px;
  border-color: var(--color-danger);
}
.error {
  margin: 0 0 var(--sp-2);
  color: var(--color-danger);
  font-weight: 600;
  font-size: var(--text-sm);
}
.error::before { content: "\26A0"; margin-inline-end: .4em; }

/* Grouped controls (3.3.2, 1.3.1) — agreement items, radio sets */
.field-group {
  margin: 0 0 var(--sp-6);
  padding: var(--sp-4);
  border: 1px solid var(--color-border-control);
  border-radius: var(--radius-md);
  max-width: 48rem;
}
.field-group legend { font-weight: 700; padding: 0 var(--sp-2); }
.check {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
.check input { margin-top: .25rem; flex: 0 0 auto; }
.check label { font-weight: 400; margin: 0; }

/* Error summary — focusable, first thing inside <main> on a failed POST */
.error-summary {
  border: 3px solid var(--color-danger);
  background: var(--color-danger-bg);
  padding: var(--sp-4) var(--sp-5);
  margin-bottom: var(--sp-6);
  border-radius: var(--radius-md);
  max-width: 48rem;
}
.error-summary h2 { margin-top: 0; font-size: var(--text-lg); }
.error-summary ul { margin: 0; padding-inline-start: var(--sp-5); }
.error-summary a { color: var(--color-danger); font-weight: 600; }

/* ===== 5. Components ======================================================= */

button, .btn {
  font: inherit;
  font-weight: 600;
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--color-primary);
  color: #fff;                    /* 8.66:1 on --color-primary */
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
button:hover, .btn:hover { background: var(--color-primary-hover); }
.btn-secondary {
  background: var(--color-surface);
  color: var(--color-primary);
  border-color: var(--color-border-control);
}
.btn-danger { background: var(--color-danger); }
button[disabled] { opacity: 1; background: var(--color-text-muted); cursor: not-allowed; }

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-5);
  margin-bottom: var(--sp-6);
}
.card > h2 { margin-top: 0; font-size: var(--text-xl); }

/* A card that needs attention without being an error — an unfinished
   prerequisite, or a warning that some artists have had this message before.
   Body text stays --color-text (13.67:1 here); the tint carries no meaning on
   its own, so every one of these also says in words what is wrong (1.4.1). */
.card-warn {
  background: var(--color-warning-bg);
  border-color: var(--color-warning);
  border-inline-start: 4px solid var(--color-warning);
}

/* Confirmation after a successful save. Paired with role="status" in the markup:
   the colour is decoration, the announcement is the actual mechanism. */
.notice {
  background: var(--color-success-bg);
  border-inline-start: 4px solid var(--color-success);
  padding: var(--sp-3) var(--sp-4);
  margin-bottom: var(--sp-5);
  max-width: none;
}

/* Vertical rhythm for a long form built out of cards and fields. */
.stack > * + * { margin-top: var(--sp-5); }

/* A rendered email body, shown inside an admin page. Indented and tinted so it
   reads as "this is the message", not as part of the surrounding page. It cannot
   be an <iframe srcdoc> — the CSP forbids it — so it is inlined, which is safe
   because render_email_body() escapes everything it did not generate itself. */
.email-preview {
  background: var(--color-surface-alt);
  border-inline-start: 3px solid var(--color-border-control);
  padding: var(--sp-3) var(--sp-4);
  margin: var(--sp-3) 0;
}
.email-preview p:first-child { margin-top: 0; }
.email-preview p:last-child  { margin-bottom: 0; }

/* A list inside a table cell, where bullets and indentation are noise. */
ul.plain { list-style: none; margin: 0; padding: 0; }

/* An inline caution beside a name in a table. Says what it means in words. */
.warn-inline {
  display: inline-block;
  margin-inline-start: var(--sp-2);
  padding: 0 var(--sp-2);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning);
  border-radius: 3px;
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--color-text);
}

/* RM-3 certificate as rendered on the web page. The printed version carries its
   own inline styles (includes/rm3.php) because Edge reads it off disk with no
   web server, so the app stylesheet is not available to it. */
.rm3-signature { margin-top: 2.5rem; font-family: monospace; }
.rm3-note { font-size: var(--text-sm); color: var(--color-text-muted); margin-top: 2rem; }

/* Ranking controls. The three buttons per row wrap rather than overflow, and
   stay ordinary buttons — there is no drag-and-drop here on purpose (WCAG 2.1.1
   / 2.5.1: a drag-only reorder has no keyboard equivalent). */
/* Artwork thumbnails in review lists.
   The endpoint serves full-size bytes until gd is enabled on the server, so the cell
   is what actually constrains the picture. A fixed box with object-fit: contain means
   a portrait piece cannot stretch a row taller than its neighbours, and letterboxing
   inside the box is honest about the aspect ratio where cropping would not be - this
   is artwork being judged, and a crop changes the composition.
   The background sits behind transparent PNGs so a pale artwork is still visible. */
.thumb-cell { width: 6.5rem; }

/* The tick column on admin/portfolio.php.
   Its label is visually hidden — the row already names the piece in the next two
   cells, and a visible "select" beside every checkbox is noise. That means the
   checkbox itself IS the whole target, so it is sized to 24px rather than left at
   the browser default of about 13px. */
.pick-cell { width: 2.75rem; text-align: center; }
.pick-cell input { width: 1.5rem; height: 1.5rem; }
.thumb {
  display: block;
  width: 6rem;
  height: 6rem;
  object-fit: contain;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
}

/* The full-size image on the submission page. Replaces an inline style: width and
   height attributes now carry the real dimensions so the box is reserved before the
   bytes land, and max-width keeps it inside the card on a narrow screen. */
.artwork-full {
  max-width: 100%;
  height: auto;
}

/* One option in the narrow-down radio group: radio, label, thumbnail.
   The thumbnail is last in the DOM so a screen reader reaches the choice and its
   evidence before the picture, but sits at the end of the row visually too, which
   keeps the radios in a straight column down the left where they are scannable. */
.narrow-option {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding-block: var(--sp-2);
  flex-wrap: wrap;
}
.narrow-option + .narrow-option { border-block-start: 1px solid var(--color-border); }
.narrow-option label { flex: 1 1 16rem; }
.narrow-option__meta {
  display: block;
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}
.narrow-option .thumb { flex: 0 0 auto; }

/* The like and ranking controls that sit inside each piece's card.
   Separated from the artwork's own details by a rule, because they are the one part
   of the card that DOES something rather than describes something, and on a
   three-piece submission the reader needs to see where one piece's controls end. */
.piece-actions {
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.piece-actions form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.piece-actions .rank-actions form { margin: 0; }
.piece-actions p { margin: 0; }

.rank-actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.rank-actions button { font-size: var(--text-sm); padding: var(--sp-2) var(--sp-3); }

/* Rubric scoring — separated from piece-actions the same way piece-actions is
   separated from the artwork's own details, for the same reason: it does
   something rather than describes something. */
.rubric-scoring {
  margin-block-start: var(--sp-4);
  padding-block-start: var(--sp-3);
  border-block-start: 1px solid var(--color-border);
}
.rubric-scoring h4 { margin: 0 0 var(--sp-3); }
.rubric-scoring .field { max-width: 24rem; }

/* The typed-position route: label and number side by side, wrapping narrow. */
.rank-positions li {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--color-border);
}
.rank-positions label { flex: 1 1 12rem; }
.rank-positions input { width: 5rem; flex: none; }

/* Two or more buttons on one row, wrapping rather than overflowing at 320px. */
.actions { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center; }

/* Data tables only — never for layout [F49] */
table { border-collapse: collapse; width: 100%; }
caption { text-align: left; font-weight: 600; padding-bottom: var(--sp-2); }
th, td {
  text-align: left;
  padding: var(--sp-3);
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
}
thead th { border-bottom: 2px solid var(--color-border-control); }

/* Status chips pair color with text; the ::before icons come from the floor rules */
.chip {
  display: inline-block;
  padding: var(--sp-1) var(--sp-3);
  border-radius: 999px;
  font-size: var(--text-sm);
  font-weight: 600;
  border: 1px solid currentColor;
}
.chip--open    { color: var(--color-success); background: var(--color-success-bg); }
.chip--closed  { color: var(--color-text-muted); background: var(--color-surface-alt); }
.chip--pending { color: var(--color-warning); background: var(--color-warning-bg); }
.chip--failed  { color: var(--color-danger);  background: var(--color-danger-bg); }

/* Pagination (ADA.md section 7) */
nav[aria-label$="pages"] ul {
  display: flex; flex-wrap: wrap; gap: var(--sp-2);
  list-style: none; padding: 0;
}
nav[aria-label$="pages"] a,
nav[aria-label$="pages"] span {
  display: inline-block; padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--color-border-control); border-radius: var(--radius-sm);
}
nav[aria-label$="pages"] [aria-current="page"] {
  background: var(--color-primary); color: #fff; border-color: var(--color-primary);
}

/* Native dialog — the only sanctioned modal. Do not hand-roll focus trapping. */
dialog {
  border: 2px solid var(--color-border-control);
  border-radius: var(--radius-md);
  padding: var(--sp-6);
  max-width: 32rem;
}
dialog::backdrop { background: rgba(0,0,0,.5); }

/* Repeating artwork blocks on the submission form */
.repeat-item {
  border: 1px solid var(--color-border-control);
  border-radius: var(--radius-md);
  padding: var(--sp-5);
  margin-bottom: var(--sp-5);
  background: var(--color-surface);
}
.repeat-item legend { font-weight: 700; }

/* Decorative brand band — the one place COPA gold is allowed to be itself,
   because it carries no information and no text sits on it. */
.brand-band {
  height: 6px;
  background: linear-gradient(90deg, var(--brand-primary), var(--brand-accent), var(--brand-decorative));
}

/* ===== 6. Responsive ======================================================= */

@media (max-width: 40rem) {
  header { flex-direction: column; align-items: flex-start; }
  header nav[aria-label="Primary"] { margin-inline-start: 0; }
  main { padding: var(--sp-5) var(--sp-4) var(--sp-7); }
  .field, .field-group { max-width: 100%; }
}

/* Hover must keep button text readable (WCAG 1.4.3). Two rules above collided:
   `a:hover` recoloured the text of link-buttons (a.btn) to --color-primary-hover — the
   same colour `.btn:hover` paints behind it (measured 1:1 on "Manage commissions") — and
   `button:hover` gave .btn-secondary the dark hover background behind dark text.
   tests/a11y-staff.spec.js pins it. Same fix as TOLAppEnhancement (2026-10-06). */
a.btn:hover, a.btn:focus-visible { color: #fff; }
.btn-secondary:hover,
a.btn.btn-secondary:hover {
  background: var(--color-surface-alt);
  color: var(--color-primary-hover);
  border-color: var(--color-primary);
}
