/*
 * Midwest Salute to the Arts — People's Choice Voting
 * ---------------------------------------------------
 * Purpose-built responsive stylesheet. No CDN, no framework — see ADR-010.
 *
 * Design constraints this file is answering:
 *   - Visitors load the artwork page on event Wi-Fi or LTE while standing in front of a
 *     piece. Small payload and fast first paint matter more than breadth of components.
 *   - WCAG 2.2 AA: every text/background pair below meets 4.5:1 (3:1 for large text),
 *     focus is always visible, and no status is conveyed by color alone.
 *   - The vote button is the single most important control in the application and is
 *     sized accordingly (minimum 56px tall, full width on phones).
 */

/* ---------------------------------------------------------------- design tokens -- */
/*
 * Palette taken from the organization's public site (midwestsalute.com) and then checked
 * against WCAG 2.2 AA. Measured contrast ratios are recorded beside each token — recheck
 * them if you change a value. See ADR-019.
 *
 * Two of the brand colors CANNOT be used as text or with white on them:
 *
 *   teal   #39ae88   2.77:1 against white  — FAILS as text, FAILS with white text
 *   yellow #fed302   1.45:1 against white  — FAILS as text
 *
 * Both are fine as BACKGROUNDS behind dark ink (5.47:1 and 10.47:1 respectively), which is
 * how they are used below. Do not promote either to a text or button color.
 */
:root {
  --ink:            #262626;   /* 15.13:1 on white — brand body color */
  --ink-soft:       #4a4a55;
  --ink-faint:      #6b6b78;
  --surface:        #ffffff;
  --surface-alt:    #f5f6f9;
  --surface-sunken: #e9ecf3;
  --line:           #d5d9e4;
  --line-strong:    #a3aabd;

  /* Brand blue. 7.81:1 on white, and 7.81:1 for white text on it — comfortably AA for
     body text in both directions, which is why it carries the vote button. */
  --brand:          #2f46b8;
  --brand-strong:   #24379a;   /* hover/active, and the admin sidebar */
  --brand-tint:     #eef0fa;

  /* Secondary blue from the site. 5.26:1 on white — safe for text. */
  --brand-soft:     #3d69c1;

  /* Focus indicator. Near-black from the brand palette at 18.26:1 on white. Sits on the
     page background via outline-offset, so it stays visible over any control color —
     a colored ring would fail against at least one of them. */
  --focus:          #151515;

  /* Teal: BACKGROUND ONLY, dark ink on top. */
  --accent:         #11ae89;
  --accent-tint:    #e6f6f1;

  --success:        #14684f;   /* darkened teal, 5.9:1 — safe as text */
  --success-tint:   #e6f6f1;
  --warning:        #6b5600;   /* darkened yellow, 6.4:1 — safe as text */
  --warning-tint:   #fff8dd;   /* pale yellow background */
  --danger:         #a3231f;
  --danger-tint:    #fdeceb;
  --info:           #24379a;
  --info-tint:      #eef0fa;

  /* Display typeface. Oswald and Roboto are the organization's web fonts; they are named
     first so a visitor who already has them installed gets an exact match, and the stack
     degrades to system fonts otherwise. They are deliberately NOT loaded from Google
     Fonts: the CSP forbids third-party origins and the brief requires local assets. To
     match exactly, self-host the WOFF2 files under wwwroot/fonts and add @font-face here. */
  --font-display: Oswald, "Segoe UI Semibold", system-ui, -apple-system, "Segoe UI",
                  Roboto, "Helvetica Neue", Arial, sans-serif;

  --radius:      10px;
  --radius-lg:   16px;
  --shadow-sm:   0 1px 2px rgba(27, 27, 31, .08);
  --shadow-md:   0 2px 10px rgba(27, 27, 31, .10);
  --shadow-lg:   0 8px 28px rgba(27, 27, 31, .14);

  --measure: 68ch;
  --page-max: 1180px;

  --font-sans: Roboto, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;

  /* ---- Festival Poster (1b) additive tokens ----------------------------------------
     New names only, so nothing that already renders changes by adding them. The values
     that 1b *changes* are scoped to the public layout below, not applied here — see the
     note above `body.public`. */

  --brand-on:          #ffffff;   /* text on brand blue — 7.81:1 */

  /* Festival accents. BACKGROUND ONLY, always behind --ink. Aliases of the existing
     --accent tokens so there is one source of truth for the value, under the names the
     approved token sheet uses. */
  --accent-yellow:     #fed302;   /* 10.47:1 behind ink; 1.45:1 as text — never as text */
  --accent-teal:       var(--accent);
  --accent-teal-tint:  var(--accent-tint);

  /* Voting status band. State is carried by color AND a dot AND text, never color alone. */
  --band-open-bg:      var(--accent-yellow);
  --band-open-ink:     var(--ink);
  --band-pending-bg:   var(--brand-tint);
  --band-pending-ink:  var(--brand-strong);
  --band-closed-bg:    var(--surface-sunken);
  --band-closed-ink:   var(--ink);

  --radius-card:       14px;
  --radius-pill:       999px;

  /* The signature festival-poster offset shadow. Flat, not blurred — it reads as print. */
  --shadow-pop:        4px 4px 0 var(--ink);
  --shadow-pop-active: 2px 2px 0 var(--ink);
  --border-bold:       2px solid var(--ink);

  --control-h:         44px;   /* WCAG 2.2 AA minimum touch target */
  --control-h-lg:      56px;
  --vote-h:            60px;

  /* Notch and home-indicator clearance for the sticky vote bar. Falls back to 0 where the
     browser does not supply it, so the bar is never mispositioned. */
  --safe-bottom:       env(safe-area-inset-bottom, 0px);
}

/* ------------------------------------------------------------------ web fonts -- */
/*
 * Self-hosted, subset, variable WOFF2. Never a third-party origin: the CSP is
 * `font-src 'self'` and the brief forbids external font services.
 *
 * Oswald and Source Sans 3, SIL Open Font License 1.1 (verified against the Fontsource
 * package metadata; upstream is Google Fonts). Attribution is recorded in
 * docs/17-Dependency-and-License-Inventory.md and wwwroot/fonts/README.txt.
 *
 * THE FAMILY NAMES ARE DELIBERATELY NOT "Oswald" AND "Source Sans 3".
 *
 * The admin stylesheet already names bare `Oswald` first in --font-display and has always
 * fallen back to Segoe UI Semibold because no such face existed. Defining a face called
 * `Oswald` would silently restyle every admin heading — a visual change to pages this work
 * is required to leave alone. Suffixing the family keeps the real font addressable by the
 * public layout only, and admin renders byte-identically to before.
 */
@font-face {
  font-family: "Oswald Variable";
  src: url("../fonts/oswald-latin-wght-normal.woff2") format("woff2-variations"),
       url("../fonts/oswald-latin-wght-normal.woff2") format("woff2");
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Source Sans 3 Variable";
  src: url("../fonts/source-sans-3-latin-wght-normal.woff2") format("woff2-variations"),
       url("../fonts/source-sans-3-latin-wght-normal.woff2") format("woff2");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

/* --------------------------------------------------- public-only token overrides -- */
/*
 * Everything 1b *changes* rather than adds lives here, scoped to the public layout.
 *
 * The approved token sheet describes itself as a drop-in replacement for :root. Applied
 * literally that would restyle the administrative interface too — site.css is shared, and
 * --line-strong, --radius and the font stacks are all used by admin components. The brief
 * puts admin explicitly out of scope, so the values are scoped instead. Custom properties
 * inherit, so a shared rule like .btn resolves to 12px radius under the public layout and
 * the original 10px under the admin one, with no duplicated selectors.
 *
 * _Layout.cshtml carries `class="public"`; _AdminLayout.cshtml does not.
 */
body.public {
  --line-strong: var(--ink);   /* 1b uses ink borders, not the pale grey */
  --radius:      12px;

  --font-display: "Oswald Variable", "Arial Narrow", "Segoe UI Semibold", system-ui, sans-serif;
  --font-sans:    "Source Sans 3 Variable", "Segoe UI", Helvetica, Arial, sans-serif;
}

/* ------------------------------------------------------------------ base reset -- */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--ink);
  background: var(--surface-alt);
  /* Prevents a long artwork title or URL from forcing a horizontal scrollbar on a phone. */
  overflow-wrap: break-word;
}

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 .5em;
  line-height: 1.25;
  font-weight: 650;
  font-family: var(--font-display);
  /* Oswald is a condensed face; a little tracking keeps headings from feeling cramped
     when it is present, and is harmless in the fallback stack. */
  letter-spacing: .005em;
}
h1 { font-size: clamp(1.6rem, 1.2rem + 1.8vw, 2.4rem); }
h2 { font-size: clamp(1.3rem, 1.1rem + 1vw, 1.75rem); }
h3 { font-size: 1.2rem; }
h4 { font-size: 1.05rem; }

p, ul, ol, dl, table, figure { margin: 0 0 1rem; }
ul, ol { padding-left: 1.4rem; }
li + li { margin-top: .25rem; }

a { color: var(--brand); text-decoration: underline; text-underline-offset: .15em; }
a:hover { color: var(--brand-strong); }

img { max-width: 100%; height: auto; display: block; }

hr { border: 0; border-top: 1px solid var(--line); margin: 1.5rem 0; }

code, kbd, samp { font-family: var(--font-mono); font-size: .9em; }

/* Focus is never removed, only restyled. A keyboard user must always know where they are. */
/*
 * The offset is what makes a single focus color work everywhere: the ring sits on the
 * page background rather than on the control, so it does not have to contrast with the
 * brand blue, the teal, and white all at once.
 */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ------------------------------------------------------------------- utilities -- */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: .75rem 1.25rem;
  background: var(--brand);
  color: #fff;
  text-decoration: none;
  border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; color: #fff; }

.text-muted   { color: var(--ink-faint); }
.text-small   { font-size: .875rem; }
.text-center  { text-align: center; }
.text-right   { text-align: right; }
.text-nowrap  { white-space: nowrap; }
.mono         { font-family: var(--font-mono); }
.mt-0 { margin-top: 0; }
.mt-1 { margin-top: .5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2.5rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: .5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.measure { max-width: var(--measure); }

.stack > * + * { margin-top: 1rem; }
.stack-sm > * + * { margin-top: .5rem; }

/* Page-level section rhythm. Grid gap rather than margins so adjacent sections cannot
   collapse or double up, and the spacing grows a little on wider screens. */
.page-sections { display: grid; gap: clamp(1.25rem, 3vw, 1.75rem); }
.page-sections > .card { margin-top: 0; }

.row-between {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
}

.inline-actions { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }

/* --------------------------------------------------------------------- layout -- */
.page { min-height: 100dvh; display: flex; flex-direction: column; }

.shell {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 2rem);
}

.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.site-header__inner {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  padding-block: .875rem;
}

.site-header__brand {
  display: flex;
  align-items: center;
  gap: .75rem;
  text-decoration: none;
  color: var(--ink);
  font-weight: 700;
  line-height: 1.2;
}
/* Uploaded logos are often wide banners. Constrain by height and cap the width so a very
   wide mark cannot crowd out the navigation.

   Note: a logo whose artwork does not fill its canvas — a reversed logo whose white text
   is invisible on this white header, or a PNG exported with transparent padding — still
   occupies its full box, and the empty part reads as a gap before the name. No amount of
   CSS can reclaim that space; the fix is to upload correctly cropped artwork, which the
   Branding page previews side by side against light and dark so the problem is visible
   before it ships. */
.site-header__brand img {
  max-height: 48px;
  width: auto;
  max-width: min(340px, 60vw);
  object-fit: contain;
}
.site-header__brand--has-logo { gap: .5rem; }
.site-header__brand span { display: block; }
.site-header__brand small {
  display: block;
  font-weight: 500;
  font-size: .8rem;
  color: var(--ink-faint);
}

.site-nav { display: flex; flex-wrap: wrap; gap: .25rem 1rem; align-items: center; }
.site-nav a { color: var(--ink-soft); text-decoration: none; font-size: .95rem; padding: .35rem 0; }
.site-nav a:hover, .site-nav a:focus-visible { color: var(--brand); text-decoration: underline; }

main { flex: 1; padding-block: clamp(1.25rem, 3vw, 2.5rem); }

.site-footer {
  background: var(--surface);
  border-top: 1px solid var(--line);
  padding-block: 1.75rem;
  font-size: .9rem;
  color: var(--ink-soft);
}
.site-footer a { color: var(--ink-soft); }
.site-footer nav { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; margin-bottom: .75rem; }

/* ---------------------------------------------------------------------- cards -- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(1rem, 3vw, 1.75rem);
  box-shadow: var(--shadow-sm);
}
.card + .card { margin-top: 1.25rem; }

/* A short brand rule under each section heading. Cheap, and it gives the page a rhythm
   that a run of identical white cards otherwise lacks. */
.card__title {
  margin-top: 0;
  padding-bottom: .5rem;
  margin-bottom: 1.1rem;
  position: relative;
}
.card__title::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 2.5rem;
  height: 3px;
  border-radius: 2px;
  background: var(--brand);
}
/* Suppressed where the heading sits inside a flex row beside a button, since the rule
   would collide with the control. */
.row-between .card__title { padding-bottom: 0; margin-bottom: 0; }
.row-between .card__title::after { display: none; }

.card-grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}

/*
 * For blocks of uneven length — Location, Hours, Parking, Accessibility — auto-fill left
 * empty tracks and stretched short blocks, producing the orphaned pair and the large void
 * beneath them. auto-fit plus top alignment packs them instead.
 */
.info-grid {
  display: grid;
  gap: 1.5rem 1.75rem;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  align-items: start;
}

.info-grid > div { min-width: 0; }

.info-grid h3 {
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .07em;
  font-weight: 700;
  color: var(--brand);
  margin-bottom: .4rem;
}

.info-grid p:last-child,
.info-grid ul:last-child { margin-bottom: 0; }

/* ------------------------------------------------------------------- hero area -- */
/*
 * The hero carries the brand. Without it the page opens on a white card containing black
 * text, which is legible but says nothing about whose event this is.
 *
 * White on the brand blue measures 7.81:1 and the pale blue lede 6.16:1, both clear of
 * the 4.5:1 AA threshold — checked, not assumed.
 */
.hero {
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: var(--shadow-md);
}

.hero__image {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 7;
  object-fit: cover;
  background: var(--surface-sunken);
}

.hero__body {
  padding: clamp(1.5rem, 4vw, 2.75rem);
  background: linear-gradient(135deg, var(--brand) 0%, var(--brand-strong) 100%);
  color: #ffffff;
}

.hero__title {
  color: #ffffff;
  margin-bottom: .35rem;
  text-wrap: balance;
}

.hero__tagline {
  font-size: clamp(1.05rem, 1rem + .4vw, 1.3rem);
  font-weight: 500;
  color: #dfe4f7;
  margin-bottom: .85rem;
}

.hero__lede {
  color: #dfe4f7;
  max-width: var(--measure);
  margin-bottom: 0;
}

/* The voting status keeps its own semantic colors, so it sits on a plain strip below
   the colored band rather than fighting it. */
.hero__status {
  background: var(--surface);
  padding: clamp(.875rem, 2vw, 1.25rem) clamp(1.5rem, 4vw, 2.75rem);
  border-top: 1px solid var(--line);
}
.hero__status .voting-status { border: 0; background: transparent; padding: 0; }
.hero__status .voting-status--open { background: transparent; }

/* -------------------------------------------------------------- primary action -- */
/*
 * Voting is what this site is for, so the entry point is its own panel rather than a
 * paragraph inside the hero.
 */
.vote-cta {
  display: grid;
  gap: 1.25rem;
  align-items: end;
  grid-template-columns: 1fr;
  background: var(--brand-tint);
  border: 1px solid #c7cff0;
  border-left: 5px solid var(--brand);
  border-radius: var(--radius-lg);
  padding: clamp(1.125rem, 3vw, 1.75rem);
}

.vote-cta__heading { margin: 0 0 .3rem; font-size: 1.2rem; color: var(--brand-strong); }

.vote-cta__form {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .75rem;
  align-items: end;
}

/* Matches the input height so the control and the button share a baseline — the previous
   layout floated the button above the field. */
.vote-cta__button { min-height: 44px; white-space: nowrap; }

@media (min-width: 720px) {
  .vote-cta { grid-template-columns: 1fr minmax(320px, 26rem); }
}

@media (max-width: 480px) {
  .vote-cta__form { grid-template-columns: 1fr; }
  .vote-cta__button { width: 100%; }
}

/* ------------------------------------------------------------------- statuses -- */
/*
 * Every status carries a text label and an icon glyph as well as its color, so the
 * meaning survives grayscale printing and color-vision differences.
 */
.status {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .3rem .7rem;
  border-radius: 999px;
  font-size: .85rem;
  font-weight: 600;
  border: 1px solid transparent;
  line-height: 1.3;
}
.status::before { font-weight: 700; }
.status--open    { background: var(--success-tint); color: var(--success); border-color: #b9d9c6; }
.status--open::before { content: "●"; }
.status--closed  { background: var(--surface-sunken); color: var(--ink-soft); border-color: var(--line-strong); }
.status--closed::before { content: "■"; }
.status--pending { background: var(--info-tint); color: var(--info); border-color: #b6cfdd; }
.status--pending::before { content: "◷"; }
.status--warning { background: var(--warning-tint); color: var(--warning); border-color: #e6cf9a; }
.status--warning::before { content: "▲"; }
.status--danger  { background: var(--danger-tint); color: var(--danger); border-color: #eeb7b4; }
.status--danger::before { content: "✕"; }

.voting-status {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1rem;
  align-items: center;
  padding: .875rem 1.125rem;
  border-radius: var(--radius);
  background: var(--surface-sunken);
  border: 1px solid var(--line);
}
.voting-status--open { background: var(--success-tint); border-color: #b9d9c6; }

/* -------------------------------------------------------------------- notices -- */
.notice {
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
  border: 1px solid;
  border-left-width: 5px;
}
.notice + .notice { margin-top: .75rem; }
.notice__title { font-weight: 650; margin: 0 0 .25rem; }
.notice p:last-child { margin-bottom: 0; }
.notice--success { background: var(--success-tint); border-color: var(--success); }
.notice--info    { background: var(--info-tint);    border-color: var(--info); }
.notice--warning { background: var(--warning-tint); border-color: var(--warning); }
.notice--danger  { background: var(--danger-tint);  border-color: var(--danger); }

/* -------------------------------------------------------------------- buttons -- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  /* 44px minimum target, per WCAG 2.2 Target Size (Minimum). */
  min-height: 44px;
  padding: .625rem 1.25rem;
  border-radius: var(--radius);
  border: 1px solid transparent;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .12s ease, border-color .12s ease, transform .06s ease;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .55;
  cursor: not-allowed;
  transform: none;
}

.btn--primary   { background: var(--brand); color: #fff; }
.btn--primary:hover { background: var(--brand-strong); color: #fff; }
.btn--secondary { background: var(--surface); border-color: var(--line-strong); color: var(--ink); }
.btn--secondary:hover { background: var(--surface-alt); color: var(--ink); }
.btn--danger    { background: var(--danger); color: #fff; }
.btn--danger:hover { background: #851c19; color: #fff; }
.btn--quiet     { background: transparent; color: var(--brand); padding-inline: .5rem; }
.btn--quiet:hover { background: var(--brand-tint); }
.btn--small     { min-height: 36px; padding: .3rem .75rem; font-size: .875rem; }
.btn--block     { width: 100%; }

/*
 * The vote button. Deliberately the largest control on the site: a visitor is holding a
 * phone one-handed, possibly in sunlight, possibly with a drink in the other hand.
 */
.btn--vote {
  width: 100%;
  min-height: 60px;
  font-size: 1.15rem;
  font-weight: 700;
  background: var(--brand);
  color: #fff;
  box-shadow: var(--shadow-md);
  letter-spacing: .01em;
}
.btn--vote:hover { background: var(--brand-strong); color: #fff; }

/* ---------------------------------------------------------------------- forms -- */
.field { margin-bottom: 1.125rem; }
.field:last-child { margin-bottom: 0; }

.field label, .field .label {
  display: block;
  font-weight: 600;
  margin-bottom: .3rem;
}

.field__hint {
  display: block;
  font-size: .85rem;
  color: var(--ink-faint);
  margin-bottom: .35rem;
}

input[type="text"], input[type="email"], input[type="password"], input[type="number"],
input[type="url"], input[type="tel"], input[type="search"], input[type="date"],
input[type="time"], input[type="datetime-local"], select, textarea {
  width: 100%;
  min-height: 44px;
  padding: .55rem .75rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  /* 16px minimum stops iOS Safari zooming the viewport when the field is focused. */
  font: inherit;
  font-size: max(1rem, 16px);
}
textarea { min-height: 8rem; resize: vertical; line-height: 1.5; }
select { appearance: auto; }

input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-sunken);
  color: var(--ink-faint);
  cursor: not-allowed;
}

.input-validation-error, [aria-invalid="true"] {
  border-color: var(--danger);
  border-width: 2px;
}

.field-validation-error {
  display: block;
  color: var(--danger);
  font-size: .875rem;
  font-weight: 600;
  margin-top: .3rem;
}
.field-validation-error::before { content: "✕ "; }
.field-validation-valid { display: none; }

.validation-summary-errors {
  background: var(--danger-tint);
  border: 1px solid var(--danger);
  border-left-width: 5px;
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
  margin-bottom: 1.25rem;
}
.validation-summary-errors ul { margin: .5rem 0 0; }
.validation-summary-valid { display: none; }

.checkbox {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .65rem;
  align-items: start;
}
.checkbox input[type="checkbox"], .checkbox input[type="radio"] {
  width: 22px;
  height: 22px;
  margin: .15rem 0 0;
  accent-color: var(--brand);
  flex: none;
}
.checkbox label { font-weight: 400; margin: 0; }

/*
 * Two fields sharing a row on wide screens, stacking on a phone.
 *
 * The vertical gap deliberately matches the margin between standalone fields: once the
 * row collapses to one column, its children are just two more stacked fields and should
 * be spaced identically. The horizontal gap stays smaller.
 */
.field-row {
  display: grid;
  gap: 1.125rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

/*
 * Fields inside a row must not carry their own bottom margin.
 *
 * It compounded with the grid gap (1.125rem + 1rem) between stacked fields, while
 * `.field:last-child` zeroed the margin on the last field of each row — leaving nothing
 * at all before the next row. On a phone that produced a form alternating between a large
 * gap and no gap: First/Last far apart, Last/Email touching, Email/ZIP far apart, ZIP
 * jammed against the consent checkbox.
 */
.field-row > .field { margin-bottom: 0; }

/*
 * The row itself now provides the separation, at the same rhythm as a standalone field.
 *
 * Wrapped in :where() so it carries zero specificity. `.mb-0` is declared far earlier in
 * this file, and the admin filter forms use `class="field-row mb-0"`; with equal
 * specificity the later rule would win and silently reintroduce a margin they explicitly
 * opted out of.
 */
:where(.field-row) { margin-bottom: 1.125rem; }
:where(.field-row:last-child) { margin-bottom: 0; }

fieldset {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
  margin: 0 0 1.25rem;
}
legend { font-weight: 650; padding-inline: .35rem; }

/* The honeypot. Hidden from people and from assistive technology, present in the DOM. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --------------------------------------------------------------------- tables -- */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: .925rem;
  background: var(--surface);
}
table.data caption {
  text-align: left;
  padding: .5rem 0;
  color: var(--ink-faint);
  font-size: .875rem;
}
table.data th, table.data td {
  padding: .6rem .75rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}
table.data thead th {
  background: var(--surface-sunken);
  font-weight: 650;
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}
table.data tbody tr:hover { background: var(--surface-alt); }
table.data td.numeric, table.data th.numeric { text-align: right; font-variant-numeric: tabular-nums; }

.rank { font-weight: 700; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- artwork page -- */
.artwork {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  max-width: 640px;
  margin-inline: auto;
}
.artwork__image {
  width: 100%;
  max-height: 60vh;
  object-fit: contain;
  background: var(--surface-sunken);
}
.artwork__body { padding: clamp(1rem, 4vw, 1.75rem); }
.artwork__number {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: .9rem;
  font-weight: 700;
  letter-spacing: .05em;
  color: var(--brand);
  background: var(--brand-tint);
  padding: .25rem .6rem;
  border-radius: 6px;
  margin-bottom: .6rem;
}
.artwork__title { margin: 0 0 .25rem; }
.artwork__artist { font-size: 1.05rem; color: var(--ink-soft); margin-bottom: .35rem; }

/* Deliberately understated. The vote button is the point of this page; the artist's site
   is a courtesy link and must not compete with it. */
.artwork__artist-link { font-size: .9rem; margin-bottom: .75rem; }
.artwork__artist-link a { color: var(--brand); font-weight: 600; }
.artwork__meta {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem .75rem;
  font-size: .9rem;
  color: var(--ink-faint);
  margin-bottom: 1rem;
}
.artwork__meta span + span::before { content: "·"; margin-right: .75rem; }
.artwork__vote { margin-top: 1.5rem; }
.artwork__reassurance {
  margin-top: .75rem;
  font-size: .875rem;
  color: var(--ink-faint);
  text-align: center;
}

/* --------------------------------------------------------------- artwork gallery -- */
/*
 * The optional browse view. Two columns on a phone so a visitor can scan the show quickly
 * without endless scrolling, widening on larger screens.
 */
.gallery {
  display: grid;
  gap: .875rem;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  list-style: none;
  margin: 0;
  padding: 0;
}

.gallery__item { margin: 0; }

.gallery__link {
  display: flex;
  flex-direction: column;
  height: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--surface);
  text-decoration: none;
  color: var(--ink);
  transition: border-color .12s ease, box-shadow .12s ease, transform .06s ease;
}

.gallery__link:hover {
  border-color: var(--brand);
  box-shadow: var(--shadow-md);
  text-decoration: none;
  color: var(--ink);
}

.gallery__link:active { transform: translateY(1px); }

.gallery__thumb {
  display: block;
  aspect-ratio: 4 / 3;
  background: var(--surface-sunken);
  overflow: hidden;
}

.gallery__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Plenty of entries have no photograph. A number on a tinted panel is more useful than an
   empty gray box, and it matches the number printed on the physical sign. */
.gallery__thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-family: var(--font-mono);
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--brand);
  background: var(--brand-tint);
}

.gallery__body {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  padding: .6rem .7rem .7rem;
}

.gallery__number {
  font-family: var(--font-mono);
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--brand);
}

.gallery__title { font-weight: 650; line-height: 1.25; }
.gallery__artist { font-size: .85rem; color: var(--ink-soft); }
.gallery__meta { font-size: .75rem; color: var(--ink-faint); }

/* -------------------------------------------------------------------- sponsors -- */
.sponsors {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  align-items: stretch;
}

/* Sponsors are paying for visibility, so give each one a defined tile rather than a line
   of small gray text. Equal-height cells also stop a logo-less sponsor collapsing. */
.sponsor {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .4rem;
  text-align: center;
  padding: 1rem .75rem;
  min-height: 92px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  text-decoration: none;
  color: var(--ink);
  font-size: .9rem;
  font-weight: 600;
  transition: border-color .12s ease, box-shadow .12s ease;
}

a.sponsor:hover {
  border-color: var(--brand);
  box-shadow: var(--shadow-sm);
  color: var(--brand-strong);
  text-decoration: none;
}

.sponsor img { max-height: 64px; width: auto; object-fit: contain; }

.sponsor small {
  font-weight: 500;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-faint);
}

/* ----------------------------------------------------------------- admin shell -- */
.admin { display: grid; grid-template-columns: 250px 1fr; min-height: 100dvh; }

.admin__sidebar {
  background: var(--brand-strong);
  color: #efe7ec;
  padding: 1.25rem 1rem;
  overflow-y: auto;
}
.admin__brand {
  display: block;
  color: #fff;
  font-weight: 700;
  text-decoration: none;
  line-height: 1.25;
  margin-bottom: 1.25rem;
}
.admin__brand small { display: block; font-weight: 500; font-size: .78rem; color: #bcc6ea; }

/* Reversed logo in the dark sidebar. Width-constrained because the sidebar is 250px. */
.admin__brand-logo {
  display: block;
  max-width: 100%;
  max-height: 44px;
  width: auto;
  object-fit: contain;
  margin-bottom: .35rem;
}

.admin__nav-group { margin-bottom: 1.25rem; }
.admin__nav-heading {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: #bda3b5;
  margin: 0 0 .35rem .5rem;
  font-weight: 700;
}
.admin__nav a {
  display: block;
  padding: .45rem .6rem;
  border-radius: 7px;
  color: #efe7ec;
  text-decoration: none;
  font-size: .92rem;
}
.admin__nav a:hover { background: rgba(255, 255, 255, .12); color: #fff; }
.admin__nav a[aria-current="page"] { background: #fff; color: var(--brand-strong); font-weight: 650; }

.admin__main { display: flex; flex-direction: column; background: var(--surface-alt); min-width: 0; }

.admin__topbar {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem 1rem;
  align-items: center;
  justify-content: space-between;
  padding: .875rem clamp(1rem, 3vw, 2rem);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.admin__content { padding: clamp(1rem, 3vw, 2rem); flex: 1; min-width: 0; }

.admin__clock {
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--ink-faint);
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
}

.breadcrumb { display: flex; flex-wrap: wrap; gap: .35rem; font-size: .875rem; margin-bottom: 1rem; padding: 0; list-style: none; }
.breadcrumb li + li::before { content: "/"; margin-right: .35rem; color: var(--line-strong); }
.breadcrumb a { color: var(--ink-soft); }

/* ------------------------------------------------------------------------ help -- */
/*
 * In-application help. Read on a phone in a marquee as often as at a desk, so the
 * measure is capped and the touch targets in the index are full-width rows rather
 * than bare inline links.
 */

.help-index { list-style: none; padding: 0; margin: 0; display: grid; gap: .25rem; }

.help-index li { border-radius: var(--radius); }

.help-index li + li { border-top: 1px solid var(--line); }

.help-index a {
  display: block;
  padding: .6rem .5rem .1rem;
  font-weight: 600;
}

.help-index span { display: block; padding: 0 .5rem .6rem; }

/* Numbered procedures. Wider line spacing than a plain list: these get followed one
   step at a time while doing something else. */
.help-steps { max-width: 62ch; padding-left: 1.25rem; }

.help-steps > li { margin-bottom: .75rem; }

.help-steps > li:last-child { margin-bottom: 0; }

.help-body > h2 {
  font-size: 1.15rem;
  margin-top: 2rem;
  margin-bottom: .5rem;
  padding-bottom: .35rem;
  border-bottom: 1px solid var(--line);
}

.help-body > h2:first-child { margin-top: 0; }

.help-body > ul { max-width: 62ch; }

.help-body > ul > li { margin-bottom: .4rem; }

.help-body .table { margin-bottom: 1.25rem; }

.help-nav {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 2.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.help-nav a { display: block; max-width: 20rem; }

.help-nav a > span { display: block; }

.help-nav__next { text-align: right; margin-left: auto; }

/* ------------------------------------------------------------- logo proofing -- */
/*
 * Shows an uploaded logo against both a light and a dark background at once. A reversed
 * logo dropped into the light slot is invisible on the site header but still occupies its
 * full width, which reads as a mysterious gap before the organization name. Seeing both
 * tiles together turns a puzzling layout bug into an obvious "wrong file" at upload time.
 */
.logo-proof {
  display: grid;
  gap: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

.logo-proof__tile {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .75rem;
  text-align: center;
}

.logo-proof__tile img {
  max-height: 60px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
  margin: 0 auto .5rem;
}

.logo-proof__tile figcaption { font-size: .75rem; line-height: 1.3; }

.logo-proof__tile--light { background: #ffffff; }
.logo-proof__tile--light figcaption { color: var(--ink-faint); }

/* Matches the administrative sidebar, so the dark tile is a true preview. */
.logo-proof__tile--dark { background: var(--brand-strong); border-color: var(--brand-strong); }
.logo-proof__tile--dark figcaption { color: #bcc6ea; }

/* ------------------------------------------------------------------ dashboard -- */
.metrics { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
.metric {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
}
.metric__label { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); font-weight: 700; }
.metric__value { font-size: 1.9rem; font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; margin-top: .2rem; }
.metric__note { font-size: .8rem; color: var(--ink-faint); margin-top: .2rem; }

/* -------------------------------------------------------------------- QR sheet -- */
.qr-preview {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.qr-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem;
  text-align: center;
}
.qr-card img { margin: 0 auto .5rem; width: 160px; height: 160px; }
.qr-card__code { font-family: var(--font-mono); font-weight: 700; }

/* ------------------------------------------------------------- import preview -- */
.import-row--error { background: var(--danger-tint); }
.import-row--update { background: var(--warning-tint); }
.import-errors { margin: 0; padding-left: 1.1rem; font-size: .85rem; color: var(--danger); }
.import-warnings { margin: 0; padding-left: 1.1rem; font-size: .85rem; color: var(--warning); }

/* ---------------------------------------------------------------- pagination -- */
.pager { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; margin-top: 1.25rem; }
.pager a, .pager span {
  min-width: 40px;
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: .6rem;
  border: 1px solid var(--line-strong);
  border-radius: 7px;
  text-decoration: none;
  background: var(--surface);
}
.pager [aria-current="page"] { background: var(--brand); color: #fff; border-color: var(--brand); font-weight: 650; }

/* ------------------------------------------------------------------ responsive -- */
@media (max-width: 900px) {
  .admin { grid-template-columns: 1fr; }
  .admin__sidebar { padding: 1rem; }
  .admin__nav { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .2rem; }
  .admin__nav-group { margin-bottom: .75rem; }
  table.data thead th { position: static; }
}

@media (max-width: 560px) {
  .btn { width: 100%; }
  .inline-actions .btn { width: auto; }
  .site-header__inner { flex-direction: column; align-items: flex-start; }
  table.data { font-size: .875rem; }
  table.data th, table.data td { padding: .5rem; }
}

/* --------------------------------------------------------------- motion, print -- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .btn:active { transform: none; }
}

@media (prefers-contrast: more) {
  :root { --line: #6b6b78; --line-strong: #3a3a44; --ink-faint: #4a4a55; }
  .btn { border-width: 2px; }
}

/* -------------------------------------------------------------- photo capture -- */
/*
 * Used one-handed, standing up, in a hall, by somebody working through a hundred pieces.
 * Every decision here is about that: one enormous tap target, the identifying information
 * large enough to match against the physical sign at arm's length, and nothing else
 * competing for the thumb.
 */

.capture__progress {
  font-size: 1.05rem;
  margin-bottom: 1rem;
}

.capture__target {
  display: flex;
  gap: 1rem;
  align-items: flex-start;
  margin-bottom: 1rem;
}

/* The large figure on the physical sign, so it is what you match on first. */
.capture__number {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 3.25rem;
  min-height: 3.25rem;
  padding: 0 .5rem;
  border-radius: var(--radius);
  background: var(--brand-blue);
  color: #fff;
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1;
}

.capture__names { min-width: 0; }

.capture__title {
  font-size: 1.35rem;
  margin: 0 0 .15rem;
  line-height: 1.2;
}

.capture__artist {
  margin: 0 0 .25rem;
  font-size: 1.05rem;
  color: var(--ink-soft);
}

.capture__meta {
  margin: 0;
  font-size: .85rem;
  color: var(--ink-faint);
}

.capture__existing { margin-bottom: 1rem; }

.capture__existing img {
  display: block;
  width: 100%;
  max-width: 18rem;
  height: auto;
  border-radius: var(--radius);
  margin-bottom: .5rem;
}

/*
 * The shutter. Deliberately huge — this is pressed once per artwork while holding a phone
 * in one hand, and a mis-tap costs a walk back across the room.
 */
.capture__shutter {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .75rem;
  width: 100%;
  min-height: 4.5rem;
  padding: 1rem;
  border: 0;
  border-radius: var(--radius);
  background: var(--brand-blue);
  color: #fff;
  font-size: 1.25rem;
  font-weight: 700;
  cursor: pointer;
  text-align: center;
}

.capture__shutter:active { transform: translateY(1px); }

.capture__shutter-icon { font-size: 1.5rem; line-height: 1; }

/*
 * Visually hidden but still focusable and still reachable by the label, so keyboard and
 * screen-reader users get the ordinary file input. display:none would remove it from the
 * accessibility tree entirely.
 */
.capture__input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.capture__input:focus-visible + .capture__hint { outline: 2px solid var(--brand-blue); }

.capture__hint { margin: .6rem 0 0; text-align: center; }

.capture__secondary {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: center;
  margin-top: 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.capture__saved-row { display: flex; gap: 1rem; align-items: center; }

.capture__saved-row img {
  width: 5rem;
  height: 5rem;
  object-fit: cover;
  border-radius: var(--radius);
  flex: 0 0 auto;
}

.capture__jump {
  display: flex;
  gap: .5rem;
  align-items: flex-end;
  flex-wrap: wrap;
}

.capture__jump .field { flex: 1 1 10rem; }

/* On a phone the shutter should reach both edges of the card. */
@media (max-width: 30rem) {
  .capture__title { font-size: 1.2rem; }
  .capture__number { min-width: 2.75rem; min-height: 2.75rem; font-size: 1.35rem; }
}

/* ---------------------------------------------------------- capture worklist -- */
/*
 * Pick-a-piece list. Code, title and booth are all that is needed to match a row to a
 * physical object, plus whether it still needs a photo — without that last one you cannot
 * tell what is left. Rows are full-width links so the whole row is the tap target.
 */

.capture-list { list-style: none; padding: 0; margin: 0; border-top: 1px solid var(--line); }

.capture-list__item { border-bottom: 1px solid var(--line); }

.capture-list__item a {
  display: grid;
  grid-template-columns: 6.5rem 1fr auto;
  grid-template-areas:
    "code  title state"
    "code  booth state";
  gap: .1rem .75rem;
  align-items: center;
  padding: .7rem .5rem;
  text-decoration: none;
  color: inherit;
  min-height: 3rem;
}

.capture-list__item a:hover { background: var(--surface-alt); }

.capture-list__item a:focus-visible { outline: 2px solid var(--brand-blue); outline-offset: -2px; }

.capture-list__code { grid-area: code; font-size: .9rem; font-weight: 600; }
.capture-list__title { grid-area: title; font-weight: 600; line-height: 1.25; }
.capture-list__booth { grid-area: booth; font-size: .85rem; color: var(--ink-faint); }
.capture-list__state { grid-area: state; }

/* A quiet left edge marker beats colouring the whole row, which would fight the status
   pill and fail in greyscale. */
.capture-list__item--todo { border-left: 3px solid var(--brand-blue); }
.capture-list__item--todo a { padding-left: calc(.5rem - 3px); }

@media (max-width: 30rem) {
  .capture-list__item a { grid-template-columns: 5rem 1fr; grid-template-areas: "code title" "code booth" "code state"; }
  .capture-list__state { padding-top: .2rem; }
}

/* ------------------------------------------------------ collapsible admin menu -- */
/*
 * The administration navigation is about twenty-five links. On a desktop sidebar that is
 * fine; stacked above the content on a phone it pushes the actual page most of a screen
 * down, on every single page.
 *
 * A <details> disclosure rather than a scripted panel, because it works with no JavaScript
 * at all, is keyboard operable, and is announced properly by screen readers. It ships with
 * the "open" attribute so that without scripting the menu simply behaves as it does today —
 * visible, just tall — rather than being permanently unreachable. site.js collapses it on
 * small screens and remembers the choice for the session.
 */

.admin__menu > summary { display: none; }

@media (max-width: 900px) {
  .admin__menu > summary {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .6rem .75rem;
    border-radius: var(--radius);
    background: rgba(255, 255, 255, .14);
    color: #fff;
    font-weight: 650;
    cursor: pointer;
    list-style: none;
    min-height: 2.75rem;
  }

  .admin__menu > summary::-webkit-details-marker { display: none; }

  .admin__menu > summary::before {
    content: "\2630";       /* three bars */
    font-size: 1.1rem;
    line-height: 1;
  }

  .admin__menu[open] > summary::before { content: "\2715"; }  /* close */

  .admin__menu > summary:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

  /*
   * Even open, cap the height and scroll inside it. Twenty-five links is a lot of thumb
   * travel, and this keeps the page itself reachable without collapsing the menu again.
   */
  .admin__menu[open] .admin__nav {
    max-height: 55vh;
    overflow-y: auto;
    margin-top: .75rem;
    padding-right: .25rem;
  }
}

/* ==================================================================================
   FESTIVAL POSTER (1b) — public component layer
   ==================================================================================
   Every rule here is prefixed `body.public`, which the admin layout does not carry.
   That is the whole isolation strategy: one stylesheet, two presentations, no
   duplicated component markup and nothing for the admin interface to inherit.

   Uppercase is applied only to headings the APPLICATION controls (hero title, card
   titles, section labels, buttons, status band). It is deliberately NOT applied to
   h1-h3 generally, even though the approved direction asks for it: admin-authored
   Markdown renders real h2/h3 elements, and shouting somebody's typed heading back at
   them is a content-flexibility regression dressed up as design fidelity.
   ================================================================================== */

/* ---------------------------------------------------------- display typography -- */
body.public h1,
body.public h2,
body.public h3 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: .01em;
}

/*
 * Application-authored headings get the full poster treatment. `.info-grid h3` is the
 * existing section label (Location, Hours, Parking…) and is already styled as one, so
 * no new .section-label class is introduced.
 */
body.public .card__title,
body.public .hero__title,
body.public .vote-cta__heading {
  text-transform: uppercase;
  letter-spacing: .03em;
}

/* ------------------------------------------------------------------- buttons -- */
body.public .btn {
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  min-height: var(--control-h);
  border-radius: var(--radius);
}

/* The signature offset shadow. Flat, not blurred: it reads as screen-printed poster
   rather than as a floating card. Primary actions only, so it stays a signal. */
body.public .btn--primary {
  background: var(--brand);
  color: var(--brand-on);
  border: var(--border-bold);
  box-shadow: var(--shadow-pop);
  min-height: var(--control-h-lg);
  font-size: 1.1875rem;      /* 19px */
}
body.public .btn--primary:hover { background: var(--brand-strong); color: var(--brand-on); }
body.public .btn--primary:active {
  transform: translate(2px, 2px);
  box-shadow: var(--shadow-pop-active);
}

body.public .btn--secondary {
  background: var(--surface);
  border: var(--border-bold);
  color: var(--ink);
  box-shadow: none;
}
body.public .btn--secondary:hover { background: var(--surface-alt); color: var(--ink); }

body.public .btn--vote {
  min-height: var(--vote-h);
  font-size: 1.25rem;        /* 20px */
  font-weight: 700;
  border: var(--border-bold);
  box-shadow: var(--shadow-pop);
  letter-spacing: .04em;
}
body.public .btn--vote:active {
  transform: translate(2px, 2px);
  box-shadow: var(--shadow-pop-active);
}

/* A disabled or busy control must not look pressable. */
body.public .btn[disabled],
body.public .btn[aria-disabled="true"] { box-shadow: none; transform: none; }

body.public .btn--small { min-height: 38px; font-size: .9375rem; }
body.public .btn--quiet { border: 0; box-shadow: none; text-transform: none; font-family: var(--font-sans); }

/* --------------------------------------------------------------------- inputs -- */
body.public .field input[type="text"],
body.public .field input[type="email"],
body.public .field input[type="search"],
body.public .field input[type="url"],
body.public .field input[type="tel"],
body.public .field input[type="number"],
body.public .field select,
body.public .field textarea {
  border: var(--border-bold);
  border-radius: var(--radius);
  min-height: var(--control-h-lg);
  font-size: 1rem;           /* never below 16px: smaller triggers iOS zoom-on-focus */
}

body.public .field textarea { min-height: 6rem; }

/*
 * Field captions get the display treatment — but a checkbox label is not a caption.
 * The mailing-list consent label renders the administrator's ConsentText verbatim
 * (Pages/Event/Index.cshtml), and uppercasing a whole consent sentence would both wreck
 * it typographically and change how agreed-to wording is presented. Excluded here, and
 * the existing `.checkbox label` rule keeps it at normal weight.
 */
body.public .field > label,
body.public .field > .label {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  font-size: 1.0625rem;
}

body.public .checkbox label {
  font-family: var(--font-sans);
  text-transform: none;
  letter-spacing: normal;
  font-size: 1rem;
  line-height: 1.45;
}

/* Validation classes are ASP.NET's and are never renamed — restyled only. */
body.public .input-validation-error { border-color: var(--danger); }
body.public .field-validation-error { color: var(--danger); font-size: 1rem; font-weight: 600; }
body.public .validation-summary-errors {
  border: var(--border-bold);
  border-color: var(--danger);
  border-radius: var(--radius);
  background: var(--danger-tint);
  padding: 1rem 1.125rem;
}

/* ------------------------------------------------------- voting status band -- */
/*
 * A full-width band rather than an inline pill. State is carried three ways — band
 * color, the pill glyph, and the wording itself — so it survives grayscale and
 * color-vision differences, and the wording stays admin-editable.
 *
 * :has() picks the variant off the pill the partial already renders, so no markup or
 * server change is needed. Browsers without :has() get the neutral band, which is
 * legible and correct, just not color-coded.
 */
body.public .voting-status {
  border: 0;
  border-radius: 0;
  border-top: var(--border-bold);
  border-bottom: var(--border-bold);
  padding: .8125rem 1.25rem;
  background: var(--band-closed-bg);
  color: var(--band-closed-ink);
  gap: .625rem;
}

body.public .voting-status--open,
body.public .voting-status:has(.status--open) {
  background: var(--band-open-bg);
  color: var(--band-open-ink);
}
body.public .voting-status:has(.status--pending) {
  background: var(--band-pending-bg);
  color: var(--band-pending-ink);
}
body.public .voting-status:has(.status--closed) {
  background: var(--band-closed-bg);
  color: var(--band-closed-ink);
}

/* Inside the band the pill loses its own chrome and becomes the dot + label. */
body.public .voting-status .status {
  background: transparent;
  border: 0;
  padding: 0;
  color: inherit;
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}
body.public .voting-status .status::before { font-size: 1.1em; }
body.public .voting-status > span:not(.status) { font-size: 1rem; }

/* -------------------------------------------------------------------- notices -- */
/* Outcome notices must never be mistakable for one another. Each keeps its own color,
   its own left band, and its own wording. */
body.public .notice {
  border: var(--border-bold);
  border-left-width: 8px;
  border-radius: var(--radius-card);
  padding: 1.125rem 1.25rem;
  font-size: 1.0625rem;
}
/*
 * Display face and size, but NOT uppercase. Unlike a button or a card title, a notice
 * title is a whole sentence — "Thank you. Your vote for this artwork has been
 * recorded." Setting that in condensed all-caps is both shouty and measurably slower
 * to read, and these are the sentences that matter most on the site.
 */
body.public .notice__title {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: .01em;
  margin-bottom: .35rem;
}
body.public .notice--success { border-color: var(--success); background: var(--success-tint); }
body.public .notice--info    { border-color: var(--info);    background: var(--info-tint); }
body.public .notice--warning { border-color: var(--warning); background: var(--warning-tint); }
body.public .notice--danger  { border-color: var(--danger);  background: var(--danger-tint); }

/* ---------------------------------------------------------------------- cards -- */
body.public .card {
  border: var(--border-bold);
  border-radius: var(--radius-card);
}
body.public .card__title { font-size: 1.375rem; }

/* ---------------------------------------------------------------------- chips -- */
/*
 * The direction shows pill "chips". Rather than introduce a .chip class and new markup,
 * these attach to the elements that are already chips in all but name: the pager links
 * and the metadata tags. The category filter deliberately stays a <select> — see the
 * note on .field-row below.
 *
 * Interactive chips: the pager. This also lifts the targets from 40px to 44px.
 */
body.public .pager a,
body.public .pager span {
  min-height: var(--control-h);
  min-width: var(--control-h);
  border: var(--border-bold);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: .04em;
}
body.public .pager a:hover { background: var(--surface-alt); text-decoration: none; }
body.public .pager [aria-current="page"] {
  background: var(--brand);
  border-color: var(--ink);
  color: var(--brand-on);        /* 7.81:1 */
}

/*
 * Non-interactive metadata pills. On the artwork page the source markup separates these
 * with a "·" pseudo-element, which becomes noise once each one is a pill, so it is
 * removed here rather than in the Razor file.
 */
body.public .artwork__meta { gap: .4rem; font-size: 1rem; }
body.public .artwork__meta span {
  display: inline-flex;
  align-items: center;
  padding: .3rem .75rem;
  border-radius: var(--radius-pill);
  font-size: .875rem;
  font-weight: 600;
  background: var(--accent-teal-tint);
  color: var(--success);         /* 6.03:1 */
}
body.public .artwork__meta span + span::before { content: none; }

body.public .gallery__meta {
  align-self: start;
  padding: .15rem .5rem;
  border-radius: var(--radius-pill);
  font-size: .78125rem;
  font-weight: 600;
  background: var(--accent-teal-tint);
  color: var(--success);
}

/* The artwork's own number, printed on the physical sign — an identifier, so it keeps
   the mono face and gains the ink outline instead of a tint-only pill. */
body.public .artwork__number {
  border: var(--border-bold);
  border-radius: var(--radius-pill);
  background: var(--surface);
  padding: .3rem .8rem;
  font-size: .9375rem;
}

/* ------------------------------------------------------------- gallery cards -- */
body.public .gallery__link {
  border: var(--border-bold);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
  background: var(--surface);
}
body.public .gallery__link:hover { box-shadow: var(--shadow-pop); }
body.public .gallery__link:active {
  transform: translate(2px, 2px);
  box-shadow: var(--shadow-pop-active);
}
body.public .gallery__number {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--brand);
}
body.public .gallery__title { font-size: 1.03125rem; font-weight: 600; line-height: 1.25; }
body.public .gallery__artist { font-size: .90625rem; color: var(--ink-soft); }

/* ------------------------------------------------------------ header, footer -- */
body.public .site-header { border-bottom: var(--border-bold); }
body.public .site-header__brand span { font-family: var(--font-display); letter-spacing: .02em; }
body.public .site-footer { border-top: var(--border-bold); font-size: .90625rem; }

/* --------------------------------------------------------------- busy states -- */
/*
 * site.js signals "submitting" by setting `disabled` + `aria-disabled="true"` and
 * swapping in `data-busy-label`. It does not set a data-busy attribute, so this keys
 * off the attributes it really writes. The flattened shadow above already removes the
 * pressable look; this adds the cursor.
 */
body.public .btn:disabled,
body.public .btn[aria-disabled="true"] { cursor: progress; }

/* ==================================================================================
   FESTIVAL POSTER (1b) — main journey (Stage 4)
   ==================================================================================
   Event landing, browse, artwork detail, the vote action, and the vote outcomes.
   Same isolation rule as the component layer: everything is prefixed `body.public`.
   ================================================================================== */

/* ----------------------------------------------------------------------- hero -- */
/*
 * `overflow: clip` rather than `hidden`. Both clip the rounded corners, but `hidden`
 * also makes the element a scroll container, which quietly changes how descendants
 * position themselves. Nothing here depends on that today; `clip` just avoids
 * inheriting a side effect the corner clipping never needed.
 */
body.public .hero {
  border: var(--border-bold);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
  overflow: clip;
}

/* Flat brand blue, not a gradient: a screen-printed poster has flat inks. */
body.public .hero__body { background: var(--brand); }

body.public .hero__title {
  font-size: clamp(2rem, 1.45rem + 3.4vw, 3.25rem);
  line-height: 1.04;
  margin-bottom: .4rem;
}

/* Yellow reads as the poster's second voice here and clears AA on brand blue at
   5.4:1 — but only because the blue behind it is dark. Never on a light surface. */
body.public .hero__tagline {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: .08em;
  font-weight: 600;
  color: var(--accent-yellow);
}

/* The status band runs edge to edge across the hero instead of floating inside a
   padded strip, so the open/closed state is the widest element on the page. */
body.public .hero__status { padding: 0; border-top: 0; }
body.public .hero__status .voting-status {
  border-left: 0;
  border-right: 0;
  border-bottom: 0;
  padding: .875rem clamp(1.5rem, 4vw, 2.75rem);
}
body.public .hero__status .voting-status--open,
body.public .hero__status .voting-status:has(.status--open) { background: var(--band-open-bg); }
body.public .hero__status .voting-status:has(.status--pending) { background: var(--band-pending-bg); }
body.public .hero__status .voting-status:has(.status--closed) { background: var(--band-closed-bg); }

/* --------------------------------------------------------------- primary action -- */
body.public .vote-cta {
  background: var(--surface);
  border: var(--border-bold);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
  padding: clamp(1.25rem, 4vw, 1.75rem);
}
body.public .vote-cta__heading {
  font-size: 1.5rem;
  color: var(--ink);
}
body.public .vote-cta__button { min-height: var(--control-h-lg); }

/*
 * The browse filters stay a <select>. Chips would have to be links, and a link cannot
 * carry the text currently typed in the search box — so picking a category would throw
 * away an unsubmitted search term. That is a behavior regression, not a restyle.
 */
body.public .field-row select { min-height: var(--control-h-lg); }

/* -------------------------------------------------------------- artwork detail -- */
body.public .artwork {
  border: var(--border-bold);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
  overflow: clip;                /* see the hero note */
}

body.public .artwork__title {
  font-size: clamp(1.75rem, 1.4rem + 1.6vw, 2.25rem);
  line-height: 1.08;
}

body.public .artwork__artist { font-size: 1.125rem; color: var(--ink); }

/* ------------------------------------------------------------------- vote bar -- */
/*
 * DEVIATION: the approved direction shows a sticky vote bar. It is not implemented,
 * and the group wrapper is plain. Both pinned variants were built and measured:
 *
 *   position: sticky — a bottom-stuck element is lifted above its natural position
 *     while its containing block is still in view, so its opaque background paints
 *     over the content before it. Measured at 390x844: the bar covered the voting
 *     status band, leaving a sliver of the yellow strip. Hiding "voting is open"
 *     behind the vote button is worse than not pinning at all.
 *
 *   position: fixed — never overlaps, but only if the page reserves exactly the bar's
 *     height at the bottom, which CSS cannot measure; a hard-coded reserve breaks when
 *     the reassurance copy wraps to a third line on a narrow screen. It also moves the
 *     button below the line that reads "the button above", making the wording wrong.
 *
 * What the direction wants here — an unmissable vote action — is already carried by
 * the 60px full-width button with the ink border and offset shadow, which is the
 * loudest element on the page. The page is ~1300px tall at 390px wide, so the button
 * is at the first fold and one short scroll away at worst.
 */
body.public .vote-bar { margin-top: 1rem; }

/* --------------------------------------------------------------- vote outcomes -- */
/*
 * The approved direction shows a full-page blue takeover on success. This takes over
 * the vote area rather than the whole page, so the artwork the visitor just voted for
 * stays on screen above the confirmation — which is the thing that actually confirms
 * the vote went where they meant it to.
 *
 * The other outcomes keep their distinct notice colors: success must never be
 * confusable with "already voted" or "voting closed".
 */
body.public .notice--vote-recorded {
  background: var(--brand);
  border-color: var(--ink);
  border-left-width: 2px;
  color: var(--brand-on);        /* 7.81:1 */
  text-align: center;
  padding: 1.75rem 1.25rem;
}
body.public .notice--vote-recorded .notice__title {
  color: var(--brand-on);
  font-size: 1.5rem;
}
body.public .notice--vote-recorded a { color: var(--brand-on); }

/*
 * The yellow check disc. Purely decorative — the notice already says "recorded" in
 * words and carries role="status", so the glyph must not be announced on top of it.
 * The `/ ""` is the CSS alternative-text syntax, which gives generated content an empty
 * accessible name. Where it is unsupported the glyph is announced as a check mark,
 * which is redundant but harmless.
 */
body.public .notice--vote-recorded::before {
  content: "✓" / "";
  display: flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  margin: 0 auto 1rem;
  border: var(--border-bold);
  border-radius: 50%;
  background: var(--accent-yellow);
  color: var(--ink);             /* 10.47:1 */
  font-size: 2.25rem;
  line-height: 1;
}

/* ==================================================================================
   FESTIVAL POSTER (1b) — legibility and target size on the public site
   ==================================================================================
   The brief sets a hard floor: body text 16px, small print 14.5px, touch targets
   44x44. The pre-existing stylesheet was built to a smaller scale and sits under both
   in several places. These are corrections, not restyling, and — like everything else
   here — they stop at `body.public` so the administrative interface keeps its own
   denser scale.

   Measured on a real page at 360px before the change; every value below replaced
   something that failed. Re-measured after.
   ================================================================================== */

/* ---- small print: nothing below 14.5px ------------------------------------------ */
body.public .text-small,
body.public .field__hint,          /* was 13.6px */
body.public .status,               /* was 13.6px — "Not in the vote" on gallery cards */
body.public .site-header__brand small,   /* was 12.8px */
body.public .gallery__meta,        /* was 12.5px */
body.public .gallery__number,      /* was 11.52px */
body.public .artwork__artist-link, /* was 14.4px */
body.public .artwork__reassurance, /* was 14px */
body.public .artwork__meta span {  /* was 14px */
  font-size: .90625rem;            /* 14.5px */
}

body.public .site-footer { font-size: .90625rem; }

/* ---- touch targets: nothing below 44x44 ----------------------------------------- */
/*
 * Navigation links were 36px (header) and 23px (footer) tall. Padding rather than a
 * fixed height, so a link that wraps to two lines still grows instead of clipping.
 */
body.public .site-nav a,
body.public .site-footer nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--control-h);
  padding-block: .35rem;
}
body.public .site-footer nav { display: flex; flex-wrap: wrap; gap: 0 1.25rem; }

/*
 * The consent checkbox measured 22x22. WCAG 2.2 target size (2.5.8, AA) sets a 24x24
 * minimum for the control itself; the brief's 44px is met by the row, because the
 * label is a real <label> and is therefore part of the same target. Sizing the box
 * itself to 44px would put a huge square next to a paragraph of consent text.
 */
body.public .checkbox { min-height: var(--control-h); }
body.public .checkbox input[type="checkbox"],
body.public .checkbox input[type="radio"] {
  width: 24px;
  height: 24px;
}

/* ==================================================================================
   FESTIVAL POSTER (1b) — secondary pages (Stage 5)
   ==================================================================================
   Event chooser, voting rules, privacy, error, sign-in. Same `body.public` scope.
   ================================================================================== */

/* -------------------------------------------------------------- page headings -- */
/*
 * Page titles take the same uppercase display treatment as card titles. The artwork
 * title is the one exception: it is the artist's own wording, arriving from an
 * administrator's CSV, and is set as typed.
 */
body.public h1:not(.artwork__title) {
  text-transform: uppercase;
  letter-spacing: .02em;
  line-height: 1.1;
}

/* ---------------------------------------------------------- long-form documents -- */
/*
 * The voting rules and the privacy notice. Section headings stay sentence case — they
 * are full phrases ("How we tell voters apart — and the limits of it"), not labels.
 * The short rule above each one gives the poster's sectioned feel without shouting.
 */
body.public .prose > h2 {
  margin-top: 2.25rem;
  padding-top: 1.25rem;
  border-top: var(--border-bold);
  font-size: 1.375rem;
}
body.public .prose > h2:first-of-type { margin-top: 1.75rem; }

body.public .prose p,
body.public .prose li { font-size: 1.0625rem; line-height: 1.6; }
body.public .prose li { margin-bottom: .4rem; }

/* An inline code span inside prose — cookie names, artwork codes — reads as a token. */
body.public .prose .mono {
  padding: .1rem .35rem;
  border-radius: 5px;
  background: var(--surface-sunken);
  font-size: .9375rem;
}

/* -------------------------------------------------------------- event chooser -- */
body.public .event-list {
  list-style: none;
  margin: 0 0 1.5rem;
  padding: 0;
  display: grid;
  gap: .75rem;
}
body.public .event-list__link {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  min-height: var(--control-h-lg);
  padding: .875rem 1.125rem;
  border: var(--border-bold);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-pop);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
}
body.public .event-list__link:hover {
  background: var(--surface-alt);
  color: var(--ink);
  text-decoration: none;
}
body.public .event-list__link:active {
  transform: translate(2px, 2px);
  box-shadow: var(--shadow-pop-active);
}
body.public .event-list__name {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .02em;
}
body.public .event-list__tagline { font-size: .9375rem; color: var(--ink-soft); }

/* ------------------------------------------------------------------------ forms -- */
/*
 * The error page needs nothing of its own: it is a .card.measure with an h1, an
 * explanation, one primary button out, and a monospace correlation reference, all of
 * which the component layer already covers. Deliberately left alone rather than given
 * a class it would not use.
 *
 * The sign-in form is a single column, so a full-width button matches the fields.
 */
body.public .btn--block { width: 100%; min-height: var(--control-h-lg); }

/* ==================================================================================
   FESTIVAL POSTER (1b) — mailing list (Stage 6)
   ==================================================================================
   Presentation only. The consent wording, the unticked default, the honeypot, the
   validation and the handler are all untouched — this section restyles what is already
   there and adds nothing to what is collected.
   ================================================================================== */

/*
 * The consent control gets its own bordered panel. The point is to make the thing the
 * visitor is agreeing to visually separate from the name and email fields above it,
 * rather than a checkbox trailing off the end of a form.
 *
 * A neutral surface, not a tint that suggests a recommended or pre-selected choice. The
 * box itself is the browser's own checkbox, recolored with accent-color: a custom
 * control here would risk the keyboard and screen-reader behavior of the one field on
 * the site where consent is actually recorded.
 */
body.public .checkbox {
  align-items: start;      /* aligns the box to the first line of a multi-line label */
  padding: .875rem 1rem;
  border: var(--border-bold);
  border-radius: var(--radius);
  background: var(--surface-alt);
}

body.public .checkbox input[type="checkbox"],
body.public .checkbox input[type="radio"] {
  accent-color: var(--brand);
  margin-top: .1rem;       /* optical alignment with the cap height of the first line */
}

/* The label is the larger half of the target, so make it behave like one. */
body.public .checkbox label { cursor: pointer; }

/*
 * Confirmation after signing up. This notice has no title element — it renders the
 * administrator's status wording as a single paragraph — so it needs its own sizing
 * rather than inheriting the small-print scale.
 */
body.public #subscribe .notice--success p { font-size: 1.0625rem; margin: 0; }

/* ==================================================================================
   Replacements for inline style attributes the CSP blocks (Stage 10)
   ==================================================================================
   The Content Security Policy is `style-src 'self'` with no 'unsafe-inline', so every
   style="…" attribute on a public page was being refused by the browser and had no
   effect at all. Chromium logs one violation per page:
   "Applying inline style violates the following Content Security Policy directive".

   That is not a theoretical problem. The event page's contact list was rendering with
   bullets and an indent because its list-style reset never applied, and the venue
   address was rendering italic because its font-style reset never applied. The document
   and sign-in cards were never centred.

   These classes do what those attributes were meant to do. The two account-card widths
   (26rem, 30rem and 32rem in the original attributes) are consolidated to one value —
   nothing was applying, so there is no previous rendering to preserve, and three
   near-identical widths are not worth three classes.
   ================================================================================== */

body.public .card--centered { margin-inline: auto; }
body.public .card--sheet    { max-width: 30rem; margin-inline: auto; }
body.public .form--narrow   { max-width: 32rem; }
body.public .field--end     { align-self: end; }

/* A postal address is not emphasis; the browser's italic default is wrong here. */
body.public address { font-style: normal; }

/* A list of contact links is navigation, not prose — no bullets, no indent. */
body.public .list-plain { list-style: none; padding: 0; margin: 0; }
