/* Public site styles. No framework, no build step. */

:root {
  /* Congress palette: the party's sky blue, darkened to clear 4.5:1 on white,
     with the tricolour kept for trim. The header also injects --accent from
     the accent_color setting, so keep that setting in step with this value. */
  --accent: #0071b8;
  --accent-fg: #ffffff;
  --link: var(--accent);

  --saffron: #ff9933;
  --india-green: #138808;

  --bg: #f7fafc;
  --surface: #ffffff;
  --surface-2: #eef4f8;
  --border: #dbe6ee;
  --text: #0f1b26;
  --muted: #5b7183;

  /* Entries the roll itself has struck off (death, moved, duplicate). */
  --struck: #b3261e;
  --struck-soft: rgba(179, 38, 30, .07);

  /* One height for everything sitting in the header bar — the Scan pill, the
     search box, the nav links, Sign out, the hamburger. They were each sized by
     their own padding and came out between 32 and 42px, which reads as a row of
     near-misses. Set the height once and let padding do only the width. */
  --ctl-h: 38px;

  /* Both marks in the header are this square: the candidate's photo at the left
     edge and the party's symbol at the right are a pair, and a pair that is not
     the same size reads as one of them being an afterthought. */
  --mark: 34px;

  --radius: 12px;
  --radius-sm: 8px;
  --shadow: 0 1px 2px rgba(15, 23, 42, .06), 0 8px 24px -12px rgba(15, 23, 42, .18);
  --maxw: 1120px;
}

/* Dark is offered three ways: follow the device (the default, and what this
   was before there was a choice), or be pinned light or dark by the reader.
   A pin is an attribute on <html> written by assets/js/theme.js.

   Hence the shape every dark rule in this file takes, twice over:

     @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) … }
     :root[data-theme="dark"] …

   The first is "the device says dark and the reader has not overruled it", the
   second is "the reader asked for dark". Neither implies the other, so both are
   written out. Never put a colour's only definition in one of them — the light
   value belongs on bare :root, or a reader on Light gets nothing. */
:root { color-scheme: light; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #08141d;
    --surface: #0f2130;
    --surface-2: #172d3f;
    --border: #26445b;
    --text: #e6f0f7;
    --muted: #93aec2;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 10px 30px -14px rgba(0, 0, 0, .7);
    --struck: #ff8f85;
    --struck-soft: rgba(255, 143, 133, .10);
    /* The inline accent from the header wins over any --accent set here, so
       links get their own token that lightens whatever accent is in force. */
    --link: color-mix(in srgb, var(--accent) 55%, #ffffff);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #08141d;
  --surface: #0f2130;
  --surface-2: #172d3f;
  --border: #26445b;
  --text: #e6f0f7;
  --muted: #93aec2;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 10px 30px -14px rgba(0, 0, 0, .7);
  --struck: #ff8f85;
  --struck-soft: rgba(255, 143, 133, .10);
  --link: color-mix(in srgb, var(--accent) 55%, #ffffff);
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.65 system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* Safety net: an unbroken run of characters — a hostname, an EPIC number, a
     pasted URL in a news item — overflows its box rather than wrapping. This
     keeps it inside. It does not lower min-content size, so anything sitting in
     a flex or grid track that must also shrink needs its own rule as well. */
  overflow-wrap: break-word;
}

img { max-width: 100%; display: block; }
a { color: var(--link); }

h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 .5em; font-weight: 700; letter-spacing: -.01em; }
h1 { font-size: clamp(1.7rem, 1.2rem + 2vw, 2.5rem); }
h2 { font-size: clamp(1.3rem, 1.1rem + 1vw, 1.7rem); }
h3 { font-size: 1.1rem; }
p { margin: 0 0 1em; }

.container { max-width: var(--maxw); margin: 0 auto; padding: 0 20px; }
.muted { color: var(--muted); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--accent); color: var(--accent-fg); padding: 10px 16px;
}
.skip:focus { left: 8px; top: 8px; }

:where(a, button, input, select, textarea, summary):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ------------------------------------------------------------------ header */

.site-header {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 40;
}
/* Tricolour trim — a hairline of saffron / white / green under the header and
   above the footer, so it reads as party trim, not a copy of the flag. */
.site-header::after, .site-footer::before {
  content: ""; display: block; height: 3px;
  background: linear-gradient(
    90deg,
    var(--saffron) 0 33.33%,
    #ffffff 33.33% 66.66%,
    var(--india-green) 66.66% 100%
  );
}
/* The footer has top padding, so its stripe is pinned to the top edge. */
.site-footer { position: relative; }
.site-footer::before { position: absolute; top: 0; left: 0; right: 0; }

/* The bar runs the full width of the window, unlike the page below it: it
   holds the brand at one edge and the controls at the other, and the two
   reading edge to edge is what makes it a bar rather than a wide row floating
   over a 1120px column. Only the header opts out — the content and the footer
   keep --maxw, so lines of text stay a readable length. */
.site-header .container { max-width: none; }

.header-inner {
  display: flex; align-items: center;
  gap: 16px; min-height: 68px;
}

.brand {
  display: flex; align-items: center; gap: 12px;
  color: inherit; text-decoration: none; font-size: 15px;
  /* The bar is two groups, not a row of items spread evenly: the brand holds
     the left edge, everything you can press gathers at the right. Setting the
     slack here rather than with space-between is what keeps that true — split
     between the items, the slack stranded the Scan pill and the links out in
     the middle of the bar, and moved them again every time a nav entry or the
     search box came or went. One auto margin holds whichever of the optional
     pieces are on the page in one group at the right. */
  margin-right: auto;
}
.brand strong { display: block; font-size: 17px; letter-spacing: -.01em; }
.brand small { display: block; color: var(--muted); font-size: 12.5px; font-weight: 400; }
/* One mark everywhere: the header tile is the favicon itself, so the tab icon
   and the header logo cannot drift apart. Its colours are baked into
   assets/img/favicon.svg — edit that file if the accent setting changes. */
.brand-mark {
  width: var(--mark); height: var(--mark); flex: none;
  background: url("/assets/img/favicon.svg") center / contain no-repeat;
}
/* An account carrying its own logo shows it in that tile's place. Contained
   rather than cropped: a party symbol loses its meaning the moment an edge of
   it is cut off. */
.brand-logo {
  width: var(--mark); height: var(--mark); flex: none;
  object-fit: contain;
}
/* The house mark is a wordmark — a name set beside a symbol — not a badge.
   Squeezed into the square tile above, its type comes out a few pixels tall and
   reads as a smudge, so it keeps its own ratio and is bounded by height
   instead. Only ever applied to the company logo, never to an account's or a
   party's: those really are square. See Branding::header()['logoIsCompany']. */
.brand-logo.is-wordmark,
.auth-brand img.is-wordmark {
  width: auto; height: calc(var(--mark) + 6px); max-width: 170px;
}

/* A candidate's face is the exception, and gets the treatment a face gets
   everywhere else on the site — a circle, filled rather than fitted. A portrait
   letterboxed into a square reads as a photo somebody forgot to crop; the same
   photo in a circle reads as a person. The slip banner and the profile page
   already showed it this way, so the header now agrees with them. */
.brand-logo.is-photo {
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
}
/* The party symbol at the header's right edge, past Sign out, with the whole
   header between it and the candidate's photo on the left — the same two ends
   as the voter slip, so the two read as one campaign rather than two designs.
   Never in the logo's place. The same square as the photo — they are a pair at
   two ends of one bar — and set apart by a hairline so it does not read as
   another nav item. */
.brand-symbol {
  width: var(--mark); height: var(--mark); flex: none;
  box-sizing: content-box;
  object-fit: contain;
  padding-left: 12px;
  border-left: 1px solid var(--border);
}

/* The hamburger stays the last thing in the bar once the symbol sits after the
   nav in the markup. No effect above the breakpoint, where it is not shown. */
.nav-toggle { order: 1; }

.site-nav { display: flex; gap: 4px; align-items: center; }
/* Height from --ctl-h rather than from padding, so a link, the search box, Sign
   out and the Scan pill all sit on the same two lines. */
.site-nav a {
  display: inline-flex; align-items: center;
  height: var(--ctl-h); padding: 0 13px;
  color: var(--text); text-decoration: none;
  border-radius: 999px; font-size: 15px; font-weight: 500;
}
.site-nav a:hover { background: var(--surface-2); }
.site-nav a.is-active { background: var(--accent); color: var(--accent-fg); }

.nav-toggle {
  display: none; width: var(--ctl-h); height: var(--ctl-h); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface); cursor: pointer;
  position: relative;
}
.nav-toggle .bars, .nav-toggle .bars::before, .nav-toggle .bars::after {
  position: absolute; left: 50%; width: 18px; height: 2px; margin-left: -9px;
  background: var(--text); border-radius: 2px; content: "";
}
.nav-toggle .bars { top: 50%; margin-top: -1px; }
.nav-toggle .bars::before { top: -6px; left: 0; margin-left: 0; }
.nav-toggle .bars::after { top: 6px; left: 0; margin-left: 0; }

@media (max-width: 820px) {
  .nav-toggle { display: block; }
  .site-nav {
    display: none; position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; gap: 0; padding: 8px;
    background: var(--surface); border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow);
  }
  .site-nav.is-open { display: flex; }
  /* A stacked menu, not a bar: the row height comes back from its padding. */
  .site-nav { align-items: stretch; }
  .site-nav a { height: auto; border-radius: var(--radius-sm); padding: 12px 14px; }
  .header-inner { position: relative; }
}

/* -------------------------------------------------------------------- hero */

.hero {
  padding: 56px 0 44px;
  background:
    radial-gradient(1000px 340px at 15% -10%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%),
    radial-gradient(720px 300px at 92% -20%, color-mix(in srgb, var(--saffron) 20%, transparent), transparent 72%),
    radial-gradient(620px 280px at 60% 120%, color-mix(in srgb, var(--india-green) 12%, transparent), transparent 70%),
    var(--surface);
  border-bottom: 1px solid var(--border);
}
/* The rolls front page has no headline to make room for — just the counts and
   the search under them. */
.hero-slim { padding: 26px 0 22px; }

.hero h1 { margin-bottom: .35em; max-width: 20ch; }
.hero p { font-size: 1.08rem; color: var(--muted); max-width: 62ch; }

.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }

.countdown {
  display: inline-flex; align-items: baseline; gap: 8px; margin-bottom: 16px;
  padding: 6px 14px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent); font-size: 14px; font-weight: 600;
}

/* ----------------------------------------------------------------- buttons */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 11px 20px; border-radius: var(--radius-sm); border: 1px solid transparent;
  font: inherit; font-weight: 600; font-size: 15px; text-decoration: none;
  cursor: pointer; background: var(--accent); color: var(--accent-fg);
}
.btn:hover { filter: brightness(1.08); }
.btn-outline { background: transparent; color: var(--text); border-color: var(--border); }
.btn-outline:hover { background: var(--surface-2); filter: none; }
.btn-sm { padding: 7px 14px; font-size: 14px; }

/* ------------------------------------------------------------------- cards */

.section { padding: 48px 0; }
.section-head {
  display: flex; align-items: end; justify-content: space-between;
  gap: 16px; margin-bottom: 22px; flex-wrap: wrap;
}
.section-head h2 { margin: 0; }
.section-head p { margin: 4px 0 0; color: var(--muted); }

.grid { display: grid; gap: 18px; }
.grid-cards { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.grid-wide  { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }

.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden;
  display: flex; flex-direction: column;
  transition: transform .15s ease, box-shadow .15s ease;
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.card-body { padding: 16px 18px 18px; display: flex; flex-direction: column; flex: 1; }
.card h3 { margin: 0 0 2px; font-size: 1.05rem; }
.card h3 a { color: inherit; text-decoration: none; }
.card h3 a:hover { color: var(--accent); }
.card .meta { color: var(--muted); font-size: 14px; margin: 0 0 10px; }
.card p.excerpt { font-size: 14.5px; color: var(--muted); margin: 0 0 14px; }
.card .card-foot { margin-top: auto; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* The band across the top of a news card: fixed ratio so mixed uploads still
   line up. A candidate card no longer uses it — a face gets .photo-avatar. */
.photo {
  aspect-ratio: 4 / 3; width: 100%; object-fit: cover;
  background: var(--surface-2); border-bottom: 1px solid var(--border);
}
.photo-round {
  width: 116px; height: 116px; border-radius: 50%; object-fit: cover;
  border: 3px solid var(--surface); box-shadow: var(--shadow);
}

/* The face on a candidate card. A circle above the name rather than the 4:3
   band `.photo` gives a news card: the two sit in the same grid on the home
   page, and this is what tells them apart at a glance — a story has a picture
   across the top, a person has a portrait. */
.photo-avatar {
  display: block;
  width: 108px; height: 108px;
  margin: 20px auto 0;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-2);
  border: 3px solid var(--surface);
  box-shadow: var(--shadow);
}

/* With a round photo the card's text wants to line up under it rather than
   run to the left edge past it. */
.card-person .card-body { text-align: center; }
.card-person .card-foot { justify-content: center; }

/* ------------------------------------------------------------------ badges */

.badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: 999px;
  font-size: 12.5px; font-weight: 600; line-height: 1.6;
  background: var(--surface-2); color: var(--muted);
  text-decoration: none; white-space: nowrap;
}
.badge-party { background: var(--party, var(--surface-2)); color: var(--party-fg, var(--text)); }
.badge-featured { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--party, var(--muted)); flex: none; }

/* ------------------------------------------------------------------ filter */

.filters {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px; margin-bottom: 24px;
}
.filters form { display: grid; gap: 12px; grid-template-columns: 2fr 1fr 1fr auto; align-items: end; }
@media (max-width: 780px) { .filters form { grid-template-columns: 1fr 1fr; } }
@media (max-width: 480px) { .filters form { grid-template-columns: 1fr; } }

.field label { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 5px; }
.field input, .field select, .field textarea {
  width: 100%; padding: 10px 12px; font: inherit; font-size: 15px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text);
}
.field textarea { min-height: 130px; resize: vertical; line-height: 1.6; }

.result-count { color: var(--muted); font-size: 14.5px; margin: 0 0 16px; }

/* ------------------------------------------------------------------ detail */

.profile {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden; margin-bottom: 26px;
}
.profile-top {
  padding: 28px; display: flex; gap: 24px; align-items: center; flex-wrap: wrap;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 8%, transparent), transparent);
  border-bottom: 1px solid var(--border);
}
.profile-top h1 { margin: 0 0 6px; }
.profile-top .tagline { color: var(--muted); font-size: 1.05rem; margin: 8px 0 0; max-width: 55ch; }
.profile-badges { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }

.layout { display: grid; gap: 26px; grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); align-items: start; }
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }

/* Sidebar on the left. The aside is still second in the markup — order moves it
   into the first column, so stacked mobile keeps the main content on top. */
.layout-aside-left { grid-template-columns: minmax(260px, 1fr) minmax(0, 3fr); }
.layout-aside-left > aside { order: -1; }

@media (max-width: 900px) {
  .layout-aside-left { grid-template-columns: 1fr; }
  .layout-aside-left > aside { order: 0; }
}

.panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 22px 24px; margin-bottom: 20px;
}
.panel h2 { font-size: 1.15rem; margin-bottom: .6em; }
.panel :last-child { margin-bottom: 0; }
.prose p { margin: 0 0 1em; }
.prose p:last-child { margin-bottom: 0; }

.promises { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.promises li {
  position: relative; padding-left: 30px; font-size: 15.5px;
}
.promises li::before {
  content: "✓"; position: absolute; left: 0; top: 0;
  width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
  font-size: 12px; font-weight: 700;
  background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent);
}

.facts { margin: 0; display: grid; gap: 0; }
.facts > div {
  display: flex; justify-content: space-between; gap: 14px;
  padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 14.5px;
}
.facts > div:last-child { border-bottom: 0; }
.facts dt { color: var(--muted); flex: none; }

/* A value with no spaces in it cannot be broken, so it sets a min-content floor
   that a flex item will not shrink below — and the source hostname is 27
   characters. In the narrow sidebar column that pushed the value out past the
   panel edge. `anywhere` is the one wrap mode that lowers the min-content size
   as well as wrapping the text, which is what stops it widening the grid. */
.facts dd {
  margin: 0; font-weight: 600; text-align: right;
  min-width: 0; overflow-wrap: anywhere;
}

.social { display: flex; gap: 8px; flex-wrap: wrap; }
.social a {
  padding: 8px 14px; border: 1px solid var(--border); border-radius: 999px;
  font-size: 14px; text-decoration: none; color: var(--text); font-weight: 500;
}
.social a:hover { border-color: var(--accent); color: var(--accent); }

.breadcrumb { font-size: 14px; color: var(--muted); padding: 20px 0 0; }
.breadcrumb a { color: var(--muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--accent); text-decoration: underline; }

/* -------------------------------------------------------------- pagination */

.pagination { display: flex; gap: 6px; justify-content: center; margin: 32px 0 0; flex-wrap: wrap; }
.pagination a, .pagination span {
  min-width: 40px; padding: 9px 12px; text-align: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  text-decoration: none; color: var(--text); font-size: 14.5px; background: var(--surface);
}
.pagination .current { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); font-weight: 600; }
.pagination a:hover { border-color: var(--accent); }

/* ------------------------------------------------------------------- empty */

.empty {
  text-align: center; padding: 56px 24px; color: var(--muted);
  background: var(--surface); border: 1px dashed var(--border); border-radius: var(--radius);
}
.empty h3 { color: var(--text); }

/* ------------------------------------------------------------------ footer */

.site-footer {
  margin-top: 56px; background: var(--surface);
  border-top: 1px solid var(--border); padding: 36px 0 20px;
}
.footer-inner { display: grid; gap: 24px; grid-template-columns: 2fr 1fr 1fr; }
@media (max-width: 720px) { .footer-inner { grid-template-columns: 1fr; } }
.footer-inner p { font-size: 14.5px; margin: 6px 0 0; max-width: 46ch; }
.footer-links, .footer-contact { display: flex; flex-direction: column; gap: 7px; font-size: 14.5px; }
.footer-links a, .footer-contact a { color: var(--text); text-decoration: none; }
.footer-links a:hover, .footer-contact a:hover { color: var(--accent); }
.footer-bottom {
  display: flex; justify-content: space-between; align-items: center; gap: 12px;
  margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--border); font-size: 13.5px;
}
.footer-bottom a { text-decoration: none; }
.footer-bottom a:hover { color: var(--accent); }

/* Who runs the software, above the copyright line. Sits apart from the
   campaign's own block because it does not change with the acting campaign —
   the same company on every page, for every account. */
.footer-company {
  display: flex; align-items: center; gap: 10px;
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--border);
  font-size: 13px;
}
/* Height-bounded with the width left free: the mark is a wordmark, so a fixed
   square would letterbox it and shrink the type to nothing. */
.footer-company-logo {
  flex: none; width: auto; height: 34px; max-width: 170px; object-fit: contain;
}
.footer-company-text { display: flex; flex-wrap: wrap; gap: 2px 12px; min-width: 0; }
.footer-company-name { font-weight: 600; }

/* The copyright line already draws a rule, and two hairlines 16px apart read
   as a mistake rather than as two sections. */
.footer-company + .footer-bottom { margin-top: 14px; padding-top: 0; border-top: 0; }

/* --------------------------------------------------------------- documents */

.doc-head { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 12px; }
.doc-head h3 { margin: 0 0 2px; }
.doc-head .meta { margin: 0; }

.doc-icon {
  flex: none; display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: 9px;
  background: color-mix(in srgb, #dc2626 14%, transparent);
  color: #dc2626; font-size: 11px; font-weight: 800; letter-spacing: .04em;
}
.doc-icon-lg { width: 76px; height: 76px; border-radius: 14px; font-size: 15px; }

.doc-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }

/* Sticky roll bar: the summary and the search box follow the reader down a long
   results table. --header-h is measured in main.js; 68px matches .header-inner. */
.roll-bar {
  position: sticky; top: var(--header-h, 68px); z-index: 30;
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px 18px;
  margin: 18px 0 22px; padding: 11px 16px;
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: saturate(140%) blur(8px);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: 0 2px 10px rgb(0 0 0 / .05);
}
.roll-bar-id { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.roll-bar-id strong { font-size: 15px; letter-spacing: -.01em; white-space: nowrap; }
.roll-bar-id span { color: var(--muted); font-size: 13px; white-space: nowrap; }

.roll-bar-stats { display: flex; flex-wrap: wrap; gap: 6px; }
.roll-bar-stats .chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 9px; border-radius: 999px; font-size: 12.5px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--muted);
  white-space: nowrap;
}
.roll-bar-stats .chip b { color: var(--text); font-weight: 700; font-size: 13px; }
.roll-bar-stats .chip i { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.roll-bar-stats .chip-lead {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.roll-bar-stats .chip-lead b { color: var(--accent); }
.roll-bar-stats .chip-struck {
  background: var(--struck-soft);
  border-color: color-mix(in srgb, var(--struck) 35%, transparent);
  color: color-mix(in srgb, var(--struck) 78%, var(--text));
}
.roll-bar-stats .chip-struck b { color: var(--struck); }

.roll-bar-search { display: flex; gap: 8px; margin-left: auto; }
.roll-bar-search input[type=search] {
  width: clamp(150px, 26vw, 250px); padding: 7px 11px; font: inherit; font-size: 14px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text);
}
.roll-bar-state {
  flex-basis: 100%; margin: 0; font-size: 13px; color: var(--muted);
  padding-top: 9px; border-top: 1px solid var(--border);
}

/* Narrow screens: the identity line and figures matter more than a wide box. */
@media (max-width: 720px) {
  .roll-bar { gap: 8px 12px; padding: 10px 12px; }
  .roll-bar-search { margin-left: 0; flex-basis: 100%; }
  .roll-bar-search input[type=search] { width: auto; flex: 1; }
}

/* Roll summary figures */
.roll-stats {
  display: grid; gap: 12px; margin: 16px 0 20px;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.roll-stat {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 14px 16px;
}
.roll-stat .n { font-size: 1.6rem; font-weight: 700; line-height: 1.15; letter-spacing: -.02em; }
.roll-stat .l { color: var(--muted); font-size: 13px; margin-top: 2px; }
.roll-stat-lead {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.roll-stat-lead .n { color: var(--accent); font-size: 1.9rem; }

.split-bar {
  display: flex; height: 12px; border-radius: 999px; overflow: hidden;
  background: var(--surface-2); margin-bottom: 10px;
}
.split-bar .seg { display: block; height: 100%; }
.seg-m { background: #2563eb; }
.seg-f { background: #db2777; }
.seg-o { background: #9ca3af; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .seg-m { background: #60a5fa; }
  :root:not([data-theme="light"]) .seg-f { background: #f472b6; }
}
:root[data-theme="dark"] .seg-m { background: #60a5fa; }
:root[data-theme="dark"] .seg-f { background: #f472b6; }

.split-legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13.5px; color: var(--muted); margin: 0; }
.split-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; }

.table-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.data-table { width: 100%; border-collapse: collapse; font-size: 14.5px; }
.data-table th {
  text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); padding: 10px 14px; background: var(--surface-2);
  border-bottom: 1px solid var(--border); white-space: nowrap;
}
.data-table td { padding: 9px 14px; border-bottom: 1px solid var(--border); }
.data-table tr:last-child td { border-bottom: 0; }

.doc-list { display: flex; flex-direction: column; gap: 10px; }
.doc-list a {
  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  text-decoration: none; color: inherit; background: var(--surface);
}
.doc-list a:hover { border-color: var(--accent); }
.doc-list .doc-icon { width: 34px; height: 34px; border-radius: 7px; font-size: 9.5px; }

/* --------------------------------------------------------------- ward-wise */

/* A ward card stands for a ward, not a file, so the mark carries the ward
   number instead of saying PDF three times down the page. */
.ward-icon {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  font-size: 14px; letter-spacing: 0;
}
.doc-icon-lg.ward-icon { font-size: 23px; }

/* The parts of a ward, listed on its card. Each one is a link into the ward
   page at that part, so a visitor who knows their part still gets there in one
   click. */
.ward-parts {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin: 0 0 14px; font-size: 13px; color: var(--muted);
}
.ward-parts > span:first-child { font-weight: 600; margin-right: 2px; }
.ward-parts a {
  padding: 2px 9px; border: 1px solid var(--border); border-radius: 999px;
  color: var(--muted); text-decoration: none; background: var(--surface-2);
}
.ward-parts a:hover { border-color: var(--accent); color: var(--accent); }

/* The parts of one ward, on the ward page itself. Two actions per row, so the
   rows cannot be anchors the way .doc-list's are. */
.part-list { display: flex; flex-direction: column; gap: 10px; }
.part-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 14px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  scroll-margin-top: calc(var(--header-h, 68px) + 14px);
}
.part-row .doc-icon { width: 34px; height: 34px; border-radius: 7px; font-size: 9.5px; }
.part-row:target { border-color: var(--accent); }
.part-main { flex: 1 1 200px; min-width: 0; }
.part-title { font-weight: 600; font-size: 15px; color: inherit; text-decoration: none; }
.part-title:hover { color: var(--accent); }
.part-meta { margin: 2px 0 0; color: var(--muted); font-size: 13.5px; }
.part-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-left: auto; }

/* ------------------------------------------------------------------- stats */

.stats { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-top: 28px; }
.stat {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 18px;
}
.stat .n { font-size: 1.9rem; font-weight: 700; line-height: 1.1; letter-spacing: -.02em; }
.stat .l { color: var(--muted); font-size: 14px; margin-top: 2px; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  .card:hover { transform: none; }
}

/* Anchor jumps must clear the site header and the sticky roll bar above them. */
#roll-search { scroll-margin-top: calc(var(--header-h, 68px) + var(--roll-bar-h, 56px) + 14px); }

/* Search form: the two query boxes sit side by side above the buttons, and the
   sex/age/sort row below them reads as secondary to both. */
.roll-search { display: grid; gap: 14px; margin: 16px 0 0; }

/* Two names, two numbers and an EPIC. The numbers need a fraction of the width
   the names do, and on a narrow screen the five stack. */
.roll-query { display: grid; gap: 12px; grid-template-columns: 1fr 1fr 0.45fr 0.55fr 0.8fr; }
/* At 1000px the names take the first row and the numbers the second; the EPIC
   is one long token, so it takes a row of its own rather than being squeezed
   beside the house number. */
@media (max-width: 1000px) {
  .roll-query { grid-template-columns: 1fr 1fr; }
  .field-epic { grid-column: 1 / -1; }
}
/* On a phone the boxes stack — except the two that hold a number.

   Entry no. and House no. are three or four characters wide and were taking a
   full row each, which pushed the rest of the form down for nothing. Two
   columns, everything that holds a name spanning both, and the two short ones
   sharing a line. */
@media (max-width: 620px) {
  .roll-query { grid-template-columns: 1fr 1fr; }
  .roll-query .field { grid-column: 1 / -1; }
  .roll-query .field-serial,
  .roll-query .field-house { grid-column: auto; }
}
.field-serial input, .field-house input, .field-epic input { font-variant-numeric: tabular-nums; }
.field-epic input { text-transform: uppercase; letter-spacing: 0.02em; }

.roll-search input[type=search] {
  width: 100%; padding: 11px 13px; font: inherit; font-size: 15px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text);
}
.roll-search input[type=search]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
/* These two are the query, not facets — their labels read as primary text. */
.roll-query .field label { color: var(--text); font-size: 14px; margin-bottom: 6px; }
.roll-query .latin { margin-top: 1px; font-weight: 400; }

/* Submit sits at the foot of the form, under the filters, so a ward or status
   chosen last does not need a scroll back up to apply. The rule separates it
   from the filter grid it now follows. */
.roll-actions {
  display: flex; gap: 10px; flex-wrap: wrap;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
/* The ward and भाग pair, hoisted above the boxes on the front page. Wider than
   a filter cell and set apart by a rule underneath: there they are a question
   being asked, not one of six ways to narrow an answer. Two columns that stay
   two on a phone — a ward and its भाग are read together, and stacking them put
   the whole of the search below the fold. */
.roll-wards {
  display: grid; gap: 12px; grid-template-columns: 1fr 1fr;
  padding-bottom: 14px; border-bottom: 1px solid var(--border);
}
.roll-wards .field { margin: 0; }
.roll-wards label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
.roll-wards select {
  width: 100%; height: var(--ctl-h); padding: 0 10px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text); font: inherit;
}
.roll-wards select:disabled { color: var(--muted); background: var(--surface-2); }

.roll-hint { font-size: 13px; margin: -6px 0 0; }
.roll-filters-head {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  margin: 4px 0 -6px;
}

.roll-filters {
  display: grid; gap: 12px; width: 100%; margin-top: 4px;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.roll-filters .field { margin: 0; }
.roll-filters label { display: block; font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 5px; }
.roll-filters input, .roll-filters select {
  width: 100%; padding: 9px 11px; font: inherit; font-size: 14.5px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg); color: var(--text);
}
.latin {
  display: block; font-size: 12.5px; color: var(--muted);
  font-weight: 400; letter-spacing: .01em;
}

/* The roll's own entry number. Tabular figures so a column of them lines up,
   and narrow, because it is the least of the seven columns. */
.roll-serial {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
  width: 1%;
}

.epic {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; letter-spacing: .02em; white-space: nowrap;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 5px; padding: 2px 7px;
}

/* The Name cell: the elector's photograph beside their name.

   The picture is the roll's own — the passport square printed inside the entry
   box — and it is here because a worker at a door is matching a face and not a
   string. It is small on purpose: the name is still what the column is for, and
   a photograph big enough to study would set the row height of a table that is
   read twenty rows at a time. It does nothing on a tap: it is a picture in a
   cell, not a control.

   `align-items: flex-start` rather than centre: the name wraps to two lines on
   a phone and a picture centred against it drifts below the name it belongs to.

   Rows with no picture close up completely — no frame, no reserved gutter — so
   a roll that has never been transcribed looks exactly as it did before. */
.voter-name { display: flex; align-items: flex-start; gap: 9px; }
.voter-name-text { min-width: 0; }

.voter-photo {
  flex: 0 0 auto;
  display: block;
  width: 34px; height: 43px;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface-2);
}
.voter-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Struck off on the roll, so struck off here: the photograph is dimmed rather
   than hidden, because "is this the person who was removed" is exactly the
   question a face answers. */
.voter-struck .voter-photo { opacity: .6; filter: grayscale(1); }

.search-error {
  margin: 14px 0 0; padding: 10px 14px; border-radius: var(--radius-sm);
  background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; font-size: 14.5px;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .search-error { background: #450a0a; border-color: #7f1d1d; color: #fecaca; }
}
:root[data-theme="dark"] .search-error { background: #450a0a; border-color: #7f1d1d; color: #fecaca; }

/* ------------------------------------------------------------ small screens */

/* A long title in the roll bar must not push the page wider than the screen. */
.roll-bar-id strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

@media (max-width: 720px) {
  /* iOS Safari zooms the whole page when a focused field is under 16px, and
     never zooms back out. Every field the reader types into is bumped. */
  .field input, .field select, .field textarea,
  .roll-search input[type=search],
  .roll-filters input, .roll-filters select,
  .roll-bar-search input[type=search] { font-size: 16px; }

  /* Tap targets: 31px-tall small buttons are hard to hit with a thumb. */
  .btn { padding: 12px 18px; }
  .btn-sm { padding: 10px 16px; }

  /* Two sticky layers were taking a third of a phone screen: the header, plus
     a roll bar that wraps to three or four rows at this width. The bar scrolls
     away here and the header keeps the nav; the anchor offset drops with it. */
  .roll-bar { position: static; }
  #roll-search { scroll-margin-top: calc(var(--header-h, 68px) + 14px); }

  /* Trim chrome so body text is not squeezed into the middle of the screen. */
  .container { padding: 0 16px; }
  .hero { padding: 34px 0 30px; }
  .section { padding: 34px 0; }
  .profile-top { padding: 18px; gap: 16px; }
  .panel { padding: 18px 16px; }
}

/* ---------------------------------------------------------------------------
   Entries the roll has struck off.

   The source PDFs mark deleted electors with a letter keyed to a legend they
   print themselves: E death, S moved away, R duplicate registration. Those
   people are still ON the published roll — they are printed, then cancelled —
   so the row stays visible and is marked, rather than being hidden. Colour is
   not the only carrier: the name is also struck through and the stamp says
   what it is in words.
   --------------------------------------------------------------------------- */
.voter-struck { background: var(--struck-soft); }

.voter-struck strong {
  color: var(--struck);
  text-decoration: line-through;
  text-decoration-thickness: 2px;
  text-decoration-color: color-mix(in srgb, var(--struck) 65%, transparent);
}

.voter-struck .latin { color: var(--struck); opacity: .85; }

.voter-struck .epic {
  color: var(--struck);
  border-color: color-mix(in srgb, var(--struck) 45%, var(--border));
}

/* The rubber stamp. Sized in em so it tracks the name it sits beside. */
.stamp {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 7px;
  border: 2px solid var(--struck);
  border-radius: 4px;
  color: var(--struck);
  background: transparent;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .68em;
  font-weight: 700;
  letter-spacing: .08em;
  line-height: 1.25;
  text-transform: uppercase;
  white-space: nowrap;
  vertical-align: middle;
  transform: rotate(-4deg);
  opacity: .92;
}

@media (prefers-reduced-motion: reduce) {
  .stamp { transform: none; }
}

/* The "Found on" column of the site-wide roll search: which ward and part a
   person turned up on, and where in that PDF. */
.found-on { display: block; font-weight: 600; text-decoration: none; white-space: nowrap; }
.found-on:hover { text-decoration: underline; }
.found-page { display: block; font-size: 12.5px; }

/* One-box roll search in a home hero. Sized to sit under the intro copy and
   above the stat tiles, and to stay a single row until the viewport is genuinely
   narrow. */
.home-search {
  display: flex;
  gap: 10px;
  margin-top: 24px;
  max-width: 620px;
}
.home-search input[type="search"] {
  flex: 1 1 auto;
  /* A flex item's min-width is auto, so a long placeholder or a pasted name
     would otherwise push this wider than its track instead of shrinking. */
  min-width: 0;
  padding: 12px 15px;
  font: inherit;
  font-size: 16px;                 /* 16px or iOS zooms the page on focus. */
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.home-search input[type="search"]::placeholder { color: var(--muted); }
.home-search .btn { flex: none; }

.home-search-hint {
  margin: 10px 0 0;
  max-width: 620px;
  color: var(--muted);
  font-size: 14px;
}

@media (max-width: 520px) {
  .home-search { flex-wrap: wrap; }
  .home-search .btn { width: 100%; }
}

/* Roll search in the header. Lives inside .site-nav so the mobile menu picks it
   up without the header needing another collapsing element. */
.header-search { display: flex; align-items: center; margin-left: 6px; }
.header-search input[type="search"] {
  width: 190px;
  min-width: 0;
  height: var(--ctl-h);
  padding: 0 14px;
  font: inherit;
  font-size: 16px;              /* 16px or iOS zooms the page on focus. */
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 999px 0 0 999px;
  border-right: 0;
}
.header-search input[type="search"]::placeholder { color: var(--muted); }
.header-search button {
  display: flex; align-items: center; justify-content: center;
  flex: none; width: var(--ctl-h); height: var(--ctl-h); padding: 0;
  color: var(--muted); cursor: pointer;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 0 999px 999px 0;
  border-left: 0;
}
.header-search button:hover { color: var(--accent); }
/* One ring around the pair rather than two touching ones. */
.header-search:focus-within input[type="search"],
.header-search:focus-within button {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The header runs out of room well before the mobile breakpoint: the brand,
   the links and a 230px control do not fit side by side on a small laptop. */
@media (max-width: 1080px) and (min-width: 821px) {
  .header-search input[type="search"] { width: 130px; }
}

@media (max-width: 820px) {
  .header-search { margin: 6px 0 0; }
  .header-search input[type="search"] { flex: 1 1 auto; width: auto; }
}

/* ------------------------------------------------------------- signing in */

/* A page that asked for no header or footer (`bare => true`, see
   layouts/site.blade.php) gets the whole viewport, so its card holds the middle
   of the screen instead of sitting under an empty strip. */
#main.is-bare {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
}
#main.is-bare > .section { flex: 1; padding: 32px 0; }

/* With the header gone, the campaign's mark goes on the card itself — the page
   still has to say whose sign-in this is. The same two ends as the header and
   the slip: photo on the left, party symbol on the right. */
.auth-brand {
  display: flex; align-items: center; gap: 12px;
  margin: 0 0 20px; padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.auth-brand img { width: var(--mark); height: var(--mark); flex: none; object-fit: contain; }
.auth-brand > span { flex: 1; min-width: 0; }
.auth-brand strong { display: block; font-size: 17px; }
.auth-brand small { display: block; color: var(--muted); font-size: 12.5px; }
.auth-symbol { margin-left: auto; }

/* Sign-in and account pages: one narrow column, because both are a short form
   and nothing else. */
.auth-page { max-width: 460px; }
.auth-page .panel { margin-bottom: 18px; }
.auth-form { display: grid; gap: 14px; }
.auth-form .btn { justify-self: start; }
.auth-note { font-size: 14px; margin: 0; }

/* This page is bare, so there is no footer to carry the operator's line. It
   goes under the card instead, and smaller than the note above it: it is a
   credit, not something the visitor has to act on. The mark rides with the
   name — the same pairing the footer uses on every other page. */
.auth-powered {
  display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; margin: 10px 0 0;
}
/* Smaller than the card above, which already carries the identity at full
   size — but not square: at 16px a wordmark is an illegible smear, so it is
   bounded by height and keeps its ratio. */
.auth-powered-logo { flex: none; width: auto; height: 22px; max-width: 130px; object-fit: contain; }

/* Light / dark / follow-the-device, cycled. Square, and the same height as
   everything else sitting in the header bar — see --ctl-h. The icon is drawn by
   theme.js, so this styles a button that is empty until it runs; with the
   script blocked it collapses to nothing rather than leaving a dead box. */
.theme-toggle {
  display: inline-grid; place-items: center;
  width: var(--ctl-h); height: var(--ctl-h); padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--muted);
  cursor: pointer;
}
.theme-toggle:hover { color: var(--text); border-color: var(--accent); }
.theme-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Empty until theme.js draws the icon; with the script blocked it collapses
   rather than leaving a dead box. :empty stops matching once the svg is in, so
   this cannot hide a working button. */
.theme-toggle:empty { display: none; }

/* Which mode you are on, named on hover and on keyboard focus. theme.js keeps
   data-label in step with the icon, so the two never disagree.

   position: relative on the button rather than the tooltip's own ancestor: the
   header nav scrolls on a phone and an absolutely positioned label anchored
   further up would part company with the button it belongs to. */
.theme-toggle { position: relative; }
.theme-toggle::after {
  content: attr(data-label);
  position: absolute; top: calc(100% + 6px); right: 0;
  z-index: 40;
  padding: 4px 8px; border-radius: 6px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  box-shadow: var(--shadow);
  font-size: 12px; line-height: 1.4; white-space: nowrap;
  opacity: 0; visibility: hidden;
  transition: opacity .12s ease;
}
.theme-toggle:hover::after,
.theme-toggle:focus-visible::after { opacity: 1; visibility: visible; }

/* A touch has no hover to leave, so the label would stick after the tap that
   changed the mode. Pointers that cannot hover get no tooltip at all — there
   the icon and the change itself are the feedback. */
@media (hover: none) {
  .theme-toggle::after { content: none; }
}

/* On sign-in there is no bar to sit in, so it goes to the card's top corner,
   out of the way of the heading rather than above it. */
.auth-theme { float: right; margin: -4px -4px 0 10px; }

/* Who you are signed in as, in the header. Pushed to the end of the bar so it
   reads as chrome rather than as another section of the site. */
.nav-account, .nav-signin { margin-left: auto; }
.nav-signout { margin: 0; }
.nav-signout button {
  display: inline-flex; align-items: center;
  height: var(--ctl-h); padding: 0 14px;
  font: inherit; font-size: 14.5px; cursor: pointer;
  border-radius: 999px;
  background: none; border: 1px solid var(--border); color: var(--muted);
}
.nav-signout button:hover { border-color: var(--accent); color: var(--accent); }

/* The sign-in call to action where the home page's search box would be. */
.home-gate { margin-top: 24px; max-width: 620px; }
.home-gate .muted { margin: 12px 0 0; font-size: 14px; }

/* One-shot messages — a saved profile, a changed password. */
/* "Your account covers ward 42" — a stated limit, not a warning, so it reads
   quieter than a flash message. */
.ward-scope-note {
  margin: 0 0 16px; padding: 10px 14px; font-size: 14.5px;
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--muted);
}

.site-flash {
  margin: 0 0 12px; padding: 11px 15px; font-size: 14.5px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--text);
}
.site-flash-success { background: #dcfce7; border-color: #86efac; color: #166534; }
.site-flash-error { background: #fef2f2; border-color: #fecaca; color: #991b1b; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .site-flash-success { background: #052e16; border-color: #166534; color: #86efac; }
  :root:not([data-theme="light"]) .site-flash-error { background: #450a0a; border-color: #7f1d1d; color: #fecaca; }
}
:root[data-theme="dark"] .site-flash-success { background: #052e16; border-color: #166534; color: #86efac; }
:root[data-theme="dark"] .site-flash-error { background: #450a0a; border-color: #7f1d1d; color: #fecaca; }

/* 820px is where .site-nav becomes the mobile column, so "push to the end of
   the bar" stops meaning anything there. */
@media (max-width: 820px) {
  .nav-account, .nav-signin { margin-left: 0; }
  .nav-signout button {
    width: 100%; height: auto; padding: 12px 14px;
    justify-content: flex-start; border-radius: var(--radius-sm);
  }
}

/* --- Canvassing: ticking voters off, and the slip printer ------------------ */

/* The bar above a results table. It carries the bulk marks and the print
   buttons, and it is a strip rather than a panel so it reads as part of the
   table it acts on. */
.canvass-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  align-items: center;
  margin: 0 0 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: 14px;
}

.canvass-group { display: flex; gap: 6px; align-items: center; }

/* The message this send's slips carry. It takes the rest of the row rather
   than sitting between two buttons: it is a sentence, and a box the width of a
   button reads as a search field. */
.canvass-msg { flex: 1 1 260px; min-width: 0; display: flex; }

.canvass-msg input {
  font: inherit;
  font-size: 13.5px;
  width: 100%;
  min-width: 0;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
}

.canvass-check {
  display: inline-flex;
  gap: 7px;
  align-items: center;
  color: var(--muted);
  cursor: pointer;
}

.canvass-count { color: var(--muted); min-width: 92px; }
.canvass-note { margin: 0 0 10px; font-size: 14px; }

/* Both tick boxes sit in narrow columns of their own, labelled in Hindi
   because that is what the person holding the phone reads. */
.pick-cell { width: 34px; text-align: center; }
.mark-cell { width: 74px; text-align: center; white-space: nowrap; }

.mark {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  cursor: pointer;
  font-size: 12px;
  color: var(--muted);
}

.mark input { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }
.mark input:disabled { cursor: not-allowed; }
.mark-voted input { accent-color: var(--india-green); }

/* A row that has been called on, and one whose vote is in. Kept to a tint on
   the row rather than a badge: the table is read by someone scanning down it
   for the next door, not by someone reading each row. */
.data-table tr.is-visited td { background: rgba(0, 113, 184, .06); }
.data-table tr.is-voted td { background: rgba(19, 136, 8, .09); }
.data-table tr.is-saving { opacity: .55; }
.data-table tr.is-failed td { background: rgba(179, 38, 30, .12); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .data-table tr.is-visited td { background: rgba(0, 113, 184, .18); }
  :root:not([data-theme="light"]) .data-table tr.is-voted td { background: rgba(19, 136, 8, .2); }
}
:root[data-theme="dark"] .data-table tr.is-visited td { background: rgba(0, 113, 184, .18); }
:root[data-theme="dark"] .data-table tr.is-voted td { background: rgba(19, 136, 8, .2); }

/* Progress, in the sticky roll bar beside the elector counts. */
.chip-visited b { color: var(--accent); }
.chip-voted b { color: var(--india-green); }

@media (max-width: 640px) {
  .canvass-bar { font-size: 13.5px; }
  .canvass-count { min-width: 0; }
}

/* ---------------------------------------------------------------------------
   The booth scanner (/scan).

   Built for one hand on a phone in a queue: the camera is the biggest thing on
   the page, the verdict under it is readable at arm's length, and every control
   is a thumb's width. It stays usable on a desktop, but the phone is the case
   it is laid out for.
   --------------------------------------------------------------------------- */

.scan-page { max-width: 620px; padding-bottom: 40px; }

.scan-head { align-items: flex-start; gap: 12px; }
.scan-head h1 { margin-bottom: 4px; }
.scan-head p { margin: 0; font-size: 14.5px; }

.scan-mode {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; margin: 0 0 14px;
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.scan-mode-now { font-size: 14.5px; }

/* The camera. A fixed 4:3 well so the page does not jump when the stream
   arrives and the video finally has a size. */
.scan-stage {
  position: relative; aspect-ratio: 4 / 3;
  background: #0b1620; border-radius: var(--radius); overflow: hidden;
  display: grid; place-items: center;
}
.scan-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  display: none;
}
.is-scanning .scan-video { display: block; }

/* The aiming frame: shown only while the camera is, so an idle page is not
   asking to be aimed. */
.scan-frame { display: none; }
.is-scanning .scan-frame {
  display: block; position: absolute;
  width: min(62%, 260px); aspect-ratio: 1;
  border: 3px solid rgba(255, 255, 255, .9);
  border-radius: var(--radius);
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .35);
}
.is-saving .scan-frame { border-color: var(--saffron); }

.scan-hint {
  position: relative; margin: 0; padding: 0 18px;
  color: #dce8f2; font-size: 14.5px; text-align: center;
}
.is-scanning .scan-hint {
  position: absolute; bottom: 10px; left: 0; right: 0;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
}

.scan-controls { display: flex; gap: 10px; margin: 14px 0; }
.scan-controls .btn { flex: 1; }
.btn-lg { padding: 14px 18px; font-size: 16px; }

/* The verdict. Colour carries it at a glance, the words carry it for anyone
   who cannot use the colour. */
.scan-result {
  border: 2px solid var(--border); border-left-width: 6px;
  border-radius: var(--radius); padding: 14px 16px; margin: 0 0 14px;
  background: var(--surface);
}
.scan-result.is-ok { border-color: var(--india-green); background: rgba(19, 136, 8, .06); }
.scan-result.is-already { border-color: var(--saffron); background: rgba(255, 153, 51, .08); }
.scan-result.is-error { border-color: var(--struck); background: var(--struck-soft); }

.scan-verdict {
  margin: 0 0 6px; font-size: 13px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted);
}
.scan-result.is-ok .scan-verdict { color: var(--india-green); }
.scan-result.is-error .scan-verdict { color: var(--struck); }

/* The face, over the entry the scan found.

   Bigger than the thumbnail in the results table, and for the opposite reason:
   this popup shows one elector at a time and the worker holding the phone is
   looking from the screen to the person in front of them. It is the whole job
   of the scanner — the slip proves which entry, the picture proves which
   person. Centred, because there is nothing beside it to align to. */
.scan-photo {
  display: block;
  width: 96px; height: 122px;
  margin: 0 auto 10px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface-2);
}
.scan-result.is-struck .scan-photo { opacity: .6; filter: grayscale(1); }

.scan-name { margin: 0 0 4px; font-size: 22px; font-weight: 700; line-height: 1.25; }
.scan-name .latin { font-size: 15px; font-weight: 500; color: var(--muted); }
.scan-rel, .scan-meta { margin: 0 0 3px; font-size: 14.5px; color: var(--muted); }
.scan-epic { margin: 6px 0 0; font-size: 13px; letter-spacing: .04em; color: var(--muted); }
.scan-warn { margin: 8px 0 0; font-size: 14px; font-weight: 600; color: var(--struck); }
.scan-result.is-struck .scan-name { text-decoration: line-through; }

.scan-manual { margin: 0 0 14px; }
.scan-manual label { display: block; font-size: 14px; color: var(--muted); margin-bottom: 6px; }
.scan-manual-row { display: flex; gap: 8px; }
.scan-manual-row input {
  flex: 1; min-width: 0; padding: 12px 14px; font-size: 16px;
  text-transform: uppercase; letter-spacing: .04em;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}

.scan-tally {
  display: flex; gap: 16px; flex-wrap: wrap;
  padding: 12px 14px; margin: 0 0 14px;
  background: var(--surface-2); border-radius: var(--radius-sm); font-size: 14.5px;
}
.scan-tally strong { font-size: 17px; }

.scan-log { list-style: none; margin: 0; padding: 0; font-size: 14px; }
.scan-log li {
  padding: 8px 12px; border-left: 3px solid var(--border);
  background: var(--surface); margin-bottom: 4px; border-radius: var(--radius-sm);
}
.scan-log-ok { border-left-color: var(--india-green); }
.scan-log-already { border-left-color: var(--saffron); }
.scan-log-error { border-left-color: var(--struck); color: var(--struck); }

/* ---------------------------------------------------------------------------
   The scanner as a layer (partials/scan-overlay.blade.php).

   The rules above dress the /scan page's own well; these put the same pieces
   on top of whatever page the worker was already on. Without them the overlay
   un-hides as an ordinary block at the foot of the document — the camera comes
   on and nothing is on screen — which is exactly how it read on a phone.
   --------------------------------------------------------------------------- */

/* One tap away, outside the collapsing nav, sized for a thumb. */
.header-scan {
  display: inline-flex; align-items: center; gap: 6px;
  margin-left: auto; height: var(--ctl-h); padding: 0 16px; border-radius: 999px;
  background: var(--accent); color: var(--accent-fg);
  font-size: 14.5px; font-weight: 600; line-height: 1; text-decoration: none;
  white-space: nowrap;
}
.header-scan:hover, .header-scan:focus { color: var(--accent-fg); filter: brightness(1.08); }
.header-scan svg { flex: none; }

/* A sheet off the bottom edge: a phone held one-handed reaches the bottom of
   the screen and not the top. Desktop centres it instead. */
.scan-overlay {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(8, 18, 28, .62);
}
/* Beats the flex above on specificity, so `hidden` still hides it. */
.scan-overlay[hidden] { display: none; }

/* The page behind must not scroll under the sheet — on a phone that is how a
   worker loses the camera without noticing. */
body.scan-open { overflow: hidden; }

.scan-sheet {
  width: min(560px, 100%); max-height: 100%; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--surface); color: var(--text);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: var(--shadow);
  padding: 12px 16px calc(16px + env(safe-area-inset-bottom, 0px));
}

.scan-sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 12px;
}
.scan-sheet-head strong { font-size: 17px; }

.scan-close {
  flex: none; width: 40px; height: 40px; padding: 0;
  font-size: 26px; line-height: 1;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface); color: var(--muted); cursor: pointer;
}
.scan-close:hover, .scan-close:focus { color: var(--text); background: var(--surface-2); }

/* The stage keeps its 4:3 well inside the sheet, but a tall phone should not
   spend the whole screen on it and leave the marks below the fold. */
.scan-overlay .scan-stage { max-height: 46vh; margin-bottom: 12px; }

/* The frame turns while a lookup or a mark is in flight. The JS class is
   is-working; is-saving above is the older page's name for the same thing. */
.is-working .scan-frame, .is-saving .scan-frame { border-color: var(--saffron); }

/* The two marks, thumb-sized and side by side, reading their own state. */
.scan-marks { display: flex; gap: 10px; margin-top: 12px; }
.scan-mark {
  flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 10px; font-size: 15.5px; text-align: center;
}
.scan-mark.is-on { background: var(--india-green); border-color: var(--india-green); }
.scan-mark.is-failed { background: var(--struck); border-color: var(--struck); }
.scan-mark-state { font-weight: 700; }
.scan-mark .latin { font-weight: 500; opacity: .85; }

.scan-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.scan-actions .btn { flex: 1; }

/* The page's own button, for the gesture a phone wants before it hands over
   the camera. */
.scan-open-row { margin: 0 0 16px; }
.scan-open-row .btn { width: 100%; }

/* Offered in the hint when the camera would not start on its own. */
.scan-retry { margin-top: 10px; }

@media (min-width: 720px) {
  .scan-overlay { align-items: center; padding: 24px; }
  .scan-sheet { border-radius: var(--radius); max-height: calc(100vh - 48px); }
}

/* A narrow phone spends its header width on the campaign's own name; the icon
   alone still says what the button is, and aria-label carries it either way. */
@media (max-width: 400px) {
  .header-scan { padding: 9px 11px; }
  .header-scan span { display: none; }
}

/* --- The voter's mobile number, on a result row --------------------------- */

/* Collected at the door rather than read off the roll, so it is an editable
   box in the table rather than a value. Narrow: it holds ten digits and the
   table already has more columns than a phone wants. */
.mobile-cell { width: 1%; white-space: nowrap; }

.mobile-box {
  width: 116px; padding: 5px 8px;
  font: inherit; font-size: 13.5px; letter-spacing: .02em;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
.mobile-box::placeholder { color: var(--muted); opacity: .7; }
.mobile-box:focus { border-color: var(--accent); outline: 2px solid rgba(0, 113, 184, .18); }
.mobile-box[disabled] { background: var(--surface-2); color: var(--muted); }
.mobile-box.is-failed { border-color: var(--struck); background: var(--struck-soft); }

/* The number and the button that opens that voter's chat, as one control. */
.mobile-wrap { display: flex; align-items: center; gap: 6px; }

.wa-open {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 999px;
  background: #1c8a4a; color: #fff; text-decoration: none;
}
.wa-open:hover, .wa-open:focus { background: #16733d; color: #fff; }
.wa-open[hidden] { display: none; }

/* Sending outright. Outlined rather than filled: the green circle beside it is
   the one a worker presses all day, and two solid greens in a 30px cell read as
   one button that has been drawn twice. */
.wa-send {
  flex: none; height: 30px; padding: 0 9px; border-radius: 999px;
  border: 1px solid #1c8a4a; background: transparent; color: #1c8a4a;
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
}
.wa-send:hover:not([disabled]) { background: #1c8a4a; color: #fff; }
.wa-send[hidden] { display: none; }
.wa-send[disabled] { opacity: .55; cursor: default; }
.wa-send.is-sent { border-color: var(--india-green); background: var(--india-green); color: #fff; }
.wa-send.is-failed { border-color: var(--struck); color: var(--struck); }

tr.is-saved .mobile-box { border-color: var(--india-green); }

@media (max-width: 820px) {
  .mobile-box { width: 104px; }
}

/* The number, taken while the voter is standing at the booth. */
.scan-mobile {
  display: flex; align-items: center; gap: 8px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
}
.scan-mobile label { font-size: 14px; color: var(--muted); flex: none; }
.scan-mobile input {
  flex: 1; min-width: 0; padding: 10px 12px; font-size: 16px;
  letter-spacing: .04em;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
.scan-mobile input:focus { border-color: var(--accent); outline: 2px solid rgba(0, 113, 184, .18); }

.scan-wa { flex: none; background: #1c8a4a; border-color: #1c8a4a; color: #fff; text-decoration: none; }
.scan-wa:hover, .scan-wa:focus { background: #16733d; border-color: #16733d; color: #fff; }
.scan-wa[hidden] { display: none; }

/* --- The banner on its own ------------------------------------------------- */

/* /parchi/banner — the campaign's picture with a message on it and no voter's
   entry anywhere near it, which is what makes it the one thing here that may
   go into a group chat or a status. Its own page rather than a corner of the
   slip page, because it has no selection: see Site\ParchiController. */

.banner-compose .field { display: block; margin: 0; }
.banner-compose .field > span {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  color: var(--muted, #616a76);
}

.banner-compose textarea {
  font: inherit;
  font-size: 15px;
  line-height: 1.5;
  width: 100%;
  padding: 11px 13px;
  border: 1px solid var(--line, #d5dbe3);
  border-radius: 9px;
  resize: vertical;
}

.banner-compose .hint {
  margin: 8px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--muted, #616a76);
}

/* The face the message is drawn in.

   Every row is the same sentence set in the face it stands for, because that
   is the only honest way to offer a font: the name of a typeface tells a ward
   worker nothing, and the shape of मतदान tells them everything. The Hindi is
   the point — the message in that box is always Devanagari, so a picker that
   showed the faces in Latin would be showing the wrong alphabet.

   Wide rows in a single column rather than a grid of tiles: the sample is a
   sentence and the thing being judged is how it sets as a line, which a tile
   two words wide cannot show. The radio itself is left visible and the row is
   its label, so the keyboard and the screen reader get an ordinary group and
   nothing here depends on the script. */
.banner-fonts {
  margin: 16px 0 0;
  padding: 0;
  border: 0;
}

.banner-fonts legend {
  padding: 0;
  margin-bottom: 8px;
  font-size: 13px;
  color: var(--muted, #616a76);
}

.font-pick {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 9px 12px;
  margin-bottom: 6px;
  border: 1px solid var(--line, #d5dbe3);
  border-radius: 9px;
  background: var(--surface, #fff);
  cursor: pointer;
}

.font-pick:hover { border-color: var(--accent); }

/* The chosen row is marked by more than the dot: on a phone the dot is at the
   far left of a wide row and the eye is on the sample at the other end. */
.font-pick.is-on {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.font-pick input { flex: 0 0 auto; margin: 0; accent-color: var(--accent); }

/* Big enough to judge — the sample is doing the work of a preview, and at
   label size the difference between a serif and a display face disappears.
   `serif` behind it on purpose: while the face is still loading the sample
   must not look like the page's own font, which is what an unloaded row would
   otherwise be mistaken for. */
.font-sample {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 21px;
  line-height: 1.45;
  color: var(--text);
  /* Wraps rather than trails off: a face is judged on its letters, and an
     ellipsis eating the last word of the sample hides the very shapes — the
     conjuncts at the end of करें — that tell two of these apart. */
  overflow-wrap: anywhere;
}

.font-name {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--muted, #616a76);
}

@media (max-width: 480px) {
  .font-sample { font-size: 19px; }
}

.banner-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 14px;
}

/* The send is green because what it opens is WhatsApp, the same as the send on
   a slip. */
.btn-wa { background: #1c8a4a; border-color: #1c8a4a; color: #fff; }
.btn-wa:hover { background: #16733d; border-color: #16733d; color: #fff; }
.btn-wa[disabled] { opacity: .6; cursor: default; }

.banner-note { margin: 12px 0 0; font-size: 13px; color: var(--muted, #616a76); }
.banner-note.is-error { color: #b0202a; }
.banner-note[hidden] { display: none; }

/* The picture itself, at the width it will be seen at rather than at 1080px. */
.banner-preview h2 { margin: 0 0 12px; font-size: 16px; }
.banner-preview img {
  display: block;
  width: 100%;
  max-width: 540px;
  height: auto;
  border: 1px solid var(--line, #d5dbe3);
  border-radius: 8px;
}

/* The character count under the message box. Quiet until it matters, and
   then the one thing on the page saying the tail is about to be trimmed —
   there is no maxlength on the box, deliberately, so this is the only
   warning there is. */
.char-count {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--muted);
}

.char-count.is-over { color: #b0202a; font-weight: 600; }

/* Which campaign the account is printing as — full access only, and only
   where there is more than one to choose from. In the nav so the mobile menu
   carries it; sized down because it is a setting, not a destination. */
.nav-campaign { display: flex; align-items: center; gap: 6px; }

.nav-campaign select {
  font: inherit;
  font-size: 13px;
  padding: 5px 8px;
  max-width: 11rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: #fff;
}

.nav-campaign button {
  font: inherit;
  font-size: 13px;
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: #fff;
  cursor: pointer;
}

.nav-campaign button[hidden] { display: none; }

/* The search page's prose, under the boxes rather than over them.

   It used to be a hero and a sub-heading, which on a phone meant most of a
   screen of type before the one control the page is for. What it says is read
   once, by somebody working out what the page does; the worker who opens this
   twenty times a morning needs the boxes and nothing else. So it keeps its
   words and loses its position, sized and ruled off as small print. */
.search-about {
  margin: 22px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 13.5px;
}

.search-about p { margin: 0 0 10px; }
.search-about p:last-child { margin-bottom: 0; }

/* Already ruled off by the block it now sits in. */
.search-about .ward-scope-note { margin-top: 0; }

/* The vote boxes and buttons, on a day that is not the poll. Greyed rather
   than hidden: a control that vanishes reads as a bug, and one that is plainly
   there and plainly off says the rule instead. */
.mark.is-closed { opacity: .45; cursor: not-allowed; }

.canvass-closed { font-size: 12px; color: var(--muted); align-self: center; }

/* ---------------------------------------------------------------------------
   The report (/report).

   Three readings of the same figures — two rings, a fortnight of columns, and
   a bar per ward — laid out so that each answers one question and stops. The
   bar block below is deliberately the same shape as the panel's own on
   admin.css: the two charts show the same numbers and a worker who has seen
   both should not have to learn the second one.

   Every series is stated in text on its own row as well as in colour, so the
   page carries its meaning in daylight, in greyscale and on paper.
   --------------------------------------------------------------------------- */

:root {
  --series-visited: #2563eb;
  --series-voted: var(--india-green);
  --chart-grid: rgba(15, 23, 42, .1);
  --chart-track: #eef2f7;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --series-visited: #3b82f6;
    --series-voted: #4ade80;
    --chart-grid: rgba(230, 237, 247, .14);
    --chart-track: #1b2740;
  }
}
:root[data-theme="dark"] {
  --series-visited: #3b82f6;
  --series-voted: #4ade80;
  --chart-grid: rgba(230, 237, 247, .14);
  --chart-track: #1b2740;
}

.report-head { align-items: flex-start; gap: 14px; }
.report-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* The three headline figures. The two rings and the plain count share a row on
   a desktop and stack on a phone, where the ring is the first thing under the
   heading and is worth its full width. */
.report-rings {
  display: grid; gap: 14px; margin-bottom: 22px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

.report-ring {
  display: flex; align-items: center; gap: 16px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 18px;
}
.report-ring-plain { align-items: flex-start; }
.report-ring-body { min-width: 0; }
.report-ring-n { font-size: 2.1rem; font-weight: 700; line-height: 1.05; letter-spacing: -.02em; }
.report-ring-l { font-size: 14px; margin-top: 3px; }
.report-ring-sub { color: var(--muted); font-size: 13px; margin-top: 4px; }

.ring { width: 78px; height: 78px; flex: 0 0 78px; }
.ring-track { stroke: var(--chart-track); }
.ring-visited .ring-fill { stroke: var(--series-visited); }
.ring-voted .ring-fill { stroke: var(--series-voted); }

.report-panel { margin-bottom: 20px; }
.report-panel-head { margin-bottom: 16px; }
.report-panel-head h2 { margin: 0 0 4px; }

.report-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin: 0; font-size: 13.5px; }
.report-legend .key { display: inline-block; width: 11px; height: 11px; border-radius: 3px; }
.report-legend .key-visited { background: var(--series-visited); }
.report-legend .key-voted { background: var(--series-voted); }
.report-legend .muted { font-size: 13px; }

/* --------------------------------------------------------------- by day */

/* One slot per day, always — an unmarked day keeps its place, because the gaps
   are what the chart is being read for. --days is set inline so the slots stay
   even whether the range is a week or a fortnight. */
.daychart {
  display: grid;
  grid-template-columns: repeat(var(--days, 14), 1fr);
  gap: 4px;
  align-items: end;
}

.daycol { display: flex; flex-direction: column; gap: 6px; min-width: 0; }

.daycol-bars {
  display: flex; align-items: flex-end; justify-content: center; gap: 2px;
  height: 120px;
  border-bottom: 1px solid var(--border);
  /* A half-height gridline, so a column can be priced without a scale. */
  background: linear-gradient(to top, transparent 50%, var(--chart-grid) 50%, transparent calc(50% + 1px));
}

.daybar { display: block; width: 44%; max-width: 14px; border-radius: 3px 3px 0 0; }
.daybar-visited { background: var(--series-visited); }
.daybar-voted { background: var(--series-voted); }
.daycol.is-quiet .daycol-bars { opacity: .85; }

/* Rotated, because fourteen dates across a phone do not fit any other way. */
.daycol-label { font-size: 10.5px; color: var(--muted); text-align: center; min-height: 34px; }
.daycol-label span { display: inline-block; transform: rotate(-55deg); transform-origin: top center; white-space: nowrap; }

/* -------------------------------------------------------------- by ward */

.wardchart { display: grid; gap: 20px; }
.wardrow { display: grid; gap: 6px; }

.wardrow-head { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; font-size: 15px; }
.wardrow-head a { font-weight: 600; text-decoration: none; }
.wardrow-head a:hover { text-decoration: underline; }
.wardrow-head .muted { font-size: 13px; }

.bar { display: grid; grid-template-columns: 58px 1fr 46px; gap: 10px; align-items: center; font-size: 12.5px; }
.bar-label { color: var(--muted); text-transform: lowercase; }
.bar-value { font-variant-numeric: tabular-nums; text-align: right; }

/* Quarter gridlines drawn into the track, so they stay behind the fill. */
.bar-track {
  position: relative; display: block; height: 12px; border-radius: 4px;
  background:
    linear-gradient(to right, var(--chart-grid) 1px, transparent 1px) 25% 0 / 25% 100% repeat-x,
    var(--chart-track);
}
.bar-fill { display: block; height: 100%; border-radius: 0 4px 4px 0; }
.bar-fill.bar-visited { background: var(--series-visited); }
.bar-fill.bar-voted { background: var(--series-voted); }

.report-parts { margin-top: 4px; }
.report-parts > summary { cursor: pointer; color: var(--muted); font-size: 13px; padding: 4px 0; }
.report-parts > summary:hover { color: var(--accent); }
.report-parts .table-scroll { margin-top: 8px; }

.report-foot { font-size: 13.5px; margin: 4px 0 0; }

@media (max-width: 640px) {
  .bar { grid-template-columns: 48px 1fr 42px; gap: 8px; }
  .daycol-bars { height: 90px; }
  .daycol-label { font-size: 9.5px; }
  .report-ring { padding: 14px; gap: 12px; }
  .ring { width: 64px; height: 64px; flex-basis: 64px; }
}

/* Printing the page itself, for the reader who wants this sheet rather than the
   PDF the server draws. The chrome goes, the folded parts come open — a printed
   page cannot be expanded — and nothing is allowed to break across a leaf. */
@media print {
  .site-header, .site-footer, .breadcrumb, .report-actions, .skip { display: none !important; }
  .section { padding: 0; }
  .panel, .report-ring { break-inside: avoid; border-color: #ccc; }
  .wardrow { break-inside: avoid; }
  /* The parts are opened by the print handler in main.js rather than by a
     rule here: a closed <details> hides its own children in the UA sheet, and
     forcing them back is a fight this stylesheet loses in one browser or
     another. The summary goes, because "3 parts, one by one" over an open
     table is a control on paper. */
  .report-parts > summary { display: none; }
  .table-scroll { overflow: visible; }
}

/* ---------------------------------------------------------------------------
   चुनाव सहायक — the assistant, as a bubble in the corner and as its own page.

   One set of rules for both, because it is one partial (partials/assistant-chat)
   in two homes: .as is the chat, .as-dock is the panel it sits in when it is
   floating, .as-page is the same chat given a whole screen. Every colour is a
   token, so the light/dark toggle carries it without a second block.

   The shape is the one everybody already knows — launcher at the bottom right,
   a panel above it, bubbles, a composer along the bottom — on purpose: a worker
   should not have to learn what this is before asking it something.
   --------------------------------------------------------------------------- */

/* The launcher. A link to /assistant with the script blocked, a toggle with it.
   Above the page but under the scanner overlay, which is a full-screen job. */
.as-fab {
  position: fixed; right: 18px; bottom: 18px; z-index: 150;
  display: inline-flex; align-items: center; gap: 8px;
  height: 52px; padding: 0 20px 0 16px; border-radius: 999px;
  background: var(--accent); color: var(--accent-fg);
  font-size: 15px; font-weight: 600; line-height: 1; text-decoration: none;
  box-shadow: 0 10px 24px -10px rgba(15, 23, 42, .55);
  transition: transform .15s ease, filter .15s ease;
}
.as-fab:hover, .as-fab:focus { color: var(--accent-fg); filter: brightness(1.07); transform: translateY(-1px); }
.as-fab-mark { font-size: 20px; }
.as-fab.is-open .as-fab-mark { font-size: 22px; }

/* The panel. Anchored to the same corner, sized so it never covers the page it
   is helping with — on a phone there is no such room, so it takes the screen. */
.as-dock {
  position: fixed; right: 18px; bottom: 82px; z-index: 151;
  width: min(390px, calc(100vw - 36px));
  border-radius: 18px; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: 0 24px 60px -20px rgba(15, 23, 42, .55);
  animation: as-rise .16s ease-out;
}
.as-dock[hidden] { display: none; }

@keyframes as-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

@media (max-width: 560px) {
  .as-dock { right: 0; left: 0; bottom: 0; width: 100%; border-radius: 18px 18px 0 0; }
  .as-fab { right: 14px; bottom: 14px; }
  .as-fab-say { display: none; }
  .as-fab { padding: 0; width: 52px; justify-content: center; }
}

/* The chat itself. */
.as { display: flex; flex-direction: column; min-height: 0; }
.as-page { padding-top: 22px; padding-bottom: 26px; }
.as-page .as {
  height: min(74vh, 720px);
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); box-shadow: var(--shadow); overflow: hidden;
}
.as-in-dock { height: min(70vh, 560px); }

/* The bar: who this is, and the two things it is worth changing. */
.as-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.as-avatar {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent); color: var(--accent-fg);
  font-weight: 700; font-size: 16px;
}
.as-who { display: flex; flex-direction: column; line-height: 1.25; margin-right: auto; min-width: 0; }
.as-who strong { font-size: 15px; }
.as-who small { font-size: 11.5px; }

/* The small round controls in the bar. */
.as-chip {
  flex: none; width: 32px; height: 32px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font-size: 15px; line-height: 1; cursor: pointer;
}
.as-chip:hover { border-color: var(--accent); }
.as-chip[aria-pressed="false"] { opacity: .45; }
.as-x { font-size: 22px; }

/* The conversation. It scrolls, the composer does not. */
.as-log {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: flex; flex-direction: column; gap: 10px;
  padding: 14px; background: var(--bg);
}

.as-msg {
  max-width: 86%; padding: 9px 13px; border-radius: 16px;
  font-size: 14.5px; line-height: 1.55;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .05);
}
.as-msg p { margin: 0 0 6px; }
.as-msg p:last-child { margin-bottom: 0; }

/* Theirs on the left with a squared corner at the tail, ours on the right in
   the accent — the two are told apart at a glance and not by reading them. */
.as-from-bot {
  align-self: flex-start; border-bottom-left-radius: 5px;
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
}
.as-from-me {
  align-self: flex-end; border-bottom-right-radius: 5px;
  background: var(--accent); color: var(--accent-fg);
}
.as-from-me .muted { color: inherit; opacity: .85; }

/* "…" while the server is looking, as three dots rather than a sentence: it is
   the one message that is not an answer, and it should not read like one. */
.as-typing { display: inline-flex; gap: 4px; padding: 3px 0; }
.as-typing i {
  width: 7px; height: 7px; border-radius: 50%; background: var(--muted);
  animation: as-blink 1s infinite ease-in-out;
}
.as-typing i:nth-child(2) { animation-delay: .16s; }
.as-typing i:nth-child(3) { animation-delay: .32s; }
@keyframes as-blink { 0%, 80%, 100% { opacity: .25; } 40% { opacity: 1; } }

/* The example questions, and the number chips a photograph comes back as. */
.as-tips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.as-tip {
  padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
  font: inherit; font-size: 12.5px; line-height: 1.3; cursor: pointer;
}
.as-tip:hover { border-color: var(--accent); color: var(--link); }

/* A number the reader put in the box and nobody has accepted yet — the same
   warned yellow the sheet's own page uses, so a suggestion looks like a
   suggestion wherever it is met. Cleared the moment it is saved or typed over. */
.as-card .mobile-box.is-suggested {
  border-color: #b8860b;
  background: rgba(255, 196, 0, .12);
}

/* An elector, as a card inside the answer. */
.as-card {
  display: grid; grid-template-columns: 48px 1fr; gap: 10px;
  margin-top: 9px; padding: 9px; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--border);
}
.as-card img {
  width: 48px; height: 58px; object-fit: cover; border-radius: 8px;
  background: var(--border);
}
.as-card .as-name { font-weight: 600; font-size: 14px; }
.as-card .muted { font-size: 12.5px; line-height: 1.45; }
.as-card-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 7px; }
.as-card input[type="tel"] { width: 148px; height: 34px; font-size: 14px; }
.as-card .btn { height: 34px; padding: 0 12px; font-size: 13px; }

/* The two elector lists on the report — visited and voted, each as a page, a
   spreadsheet and a PDF. One row per mark, the buttons to the right of it, and
   stacked on a phone where there is no room for two columns. */
.report-lists { display: grid; gap: 12px; }
.report-list-row {
  display: flex; flex-wrap: wrap; gap: 8px 14px;
  align-items: center; justify-content: space-between;
  padding: 10px 12px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface-2);
}
.report-list-do { display: flex; flex-wrap: wrap; gap: 8px; }

/* A ward or one भाग, as a card of figures. One column: there is no face on it
   — the same shape as the confirmation card, and for the same reason. */
.as-place { grid-template-columns: 1fr; }
.as-place .as-name { font-size: 14.5px; }

/* The number waiting on a yes. Warned in the roll's own struck colour family
   rather than the accent: it is the one card that changes something. */
.as-confirm {
  grid-template-columns: 1fr;
  border-color: var(--saffron);
  background: rgba(255, 153, 51, .10);
}

/* The composer. One rounded row: mic, box, camera, picture, send. */
.as-ask {
  display: flex; align-items: center; gap: 6px;
  padding: 10px; border-top: 1px solid var(--border); background: var(--surface);
}
.as-box {
  flex: 1 1 auto; min-width: 0; height: 40px;
  padding: 0 14px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--bg); color: var(--text);
  font-size: 15px;
}
.as-box:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0, 113, 184, .16); }

.as-icon {
  flex: none; width: 38px; height: 38px; border-radius: 50%;
  display: grid; place-items: center;
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  font-size: 16px; line-height: 1; cursor: pointer;
}
.as-icon:hover { border-color: var(--accent); }
/* Recording. The one control on the page that is doing something while you
   look at it, so it says so in the roll's alarm colour and keeps moving. */
.as-icon.is-on {
  border-color: var(--struck); color: var(--struck);
  animation: as-pulse 1.1s infinite ease-in-out;
}
@keyframes as-pulse { 0%, 100% { box-shadow: 0 0 0 0 var(--struck-soft); } 50% { box-shadow: 0 0 0 7px var(--struck-soft); } }

.as-send {
  flex: none; width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center; border: 0;
  background: var(--accent); color: var(--accent-fg);
  font-size: 16px; line-height: 1; cursor: pointer;
}
.as-send:hover { filter: brightness(1.08); }
.as-send:disabled { opacity: .5; cursor: default; }

/* The camera, in the panel rather than over it: here it is one of the ways to
   ask, not a layer over some other page. */
.as-cam { display: grid; gap: 8px; justify-items: start; padding: 10px; border-top: 1px solid var(--border); }
.as-cam[hidden] { display: none; }
.as-video { width: 100%; border-radius: 12px; background: #000; }
.as-camsay { font-size: 13px; margin: 0; }

.as-warn { color: var(--struck); font-size: 14px; }
