/* Best Of 2.0 (25 Aug 2026) - the combined-ballot page and its own new
   components. Deliberately a plugin-owned, separately-enqueued
   stylesheet, never merged into the shared global Additional CSS - see
   CLAUDE.md's "never wholesale-touch global CSS for a feature" rule,
   written after the 25 Aug 2026 BOM-corruption incident. Reuses the
   site's own --md-* brand tokens (already defined globally) by reading
   them only, never redefining them here. Reuses several pre-existing
   .mdbo-* classes from the original Best Of build (candidatecard,
   pollheader, testbanner, verifyresult, etc, all still defined in the
   global stylesheet) - only what's genuinely new for the ballot lives
   in this file. */

/* THE REAL ROOT CAUSE of the desktop width difference (28 Aug 2026) -
   found from real production screenshots, not a local fixture, which
   originally (wrongly) reported every category as identical. The local
   fixture used in that earlier pass built its own hand-written wrapper
   around the ballot's raw HTML and never included the real GeneratePress
   theme markup that actually surrounds /best/{year}/ in production -
   that's what let the bug hide from it.

   maybe_render_archive_year() (class-mdbo-public.php) calls get_header()
   then prints its OWN <main id="primary" class="site-main"> by hand,
   rather than using a normal WordPress page template. A normal GP page
   (page.php) instead nests TWO elements here: an outer
   <div id="primary" class="content-area"> (the real flex item) wrapping
   an inner <main id="main" class="site-main"> (an ordinary block, never
   itself a flex item). GeneratePress's own theme CSS has exactly one
   rule that gives that outer wrapper a real, definite width:
   `.site-content .content-area{width:100%}` - and #content.site-content
   is display:flex (also GP's own CSS). Because this custom route's
   <main> carries class site-main instead of content-area, that width
   rule can never match it - it's the flex item, but with no width/
   flex-basis of its own, so flexbox's default (flex-basis:auto) makes
   it size via CSS shrink-to-fit against its own content's UNWRAPPED
   preferred width, rather than filling the real available width.
   Confirmed directly, not guessed: measuring #primary.site-main's own
   real width in a fixture built from the actual captured get_header()/
   get_footer() output showed exactly 1020px for Best Pubs (12
   candidates) and Best Café (5), but only 673px/679px/669px for Best
   Pizza (4)/Best Sunday Roast (3)/Best Fish & Chips (2) - a shrink-to-
   fit computation is inherently dependent on how much content there is
   to fit unwrapped, which is exactly why this tracked candidate count so
   precisely while every rule already in this file (.mdbo-ballotpage's
   own width:100%, the candidate grid's own flex-basis percentages) sits
   BELOW this element and can only ever say "100% of my parent" - not
   "100% of the page" - so none of them could ever have fixed this from
   where they sit.
   Fixed at the actual point of failure: giving this one flex item a
   real, definite width, exactly mirroring the width rule GP's own
   .content-area class would have supplied had the route nested its
   markup the normal way. Scoped with :has(.mdbo-ballotpage) rather than
   the bare #primary.site-main compound (which, while unique on today's
   markup, would rely on no other route ever reusing that exact id/class
   pair) - this ties the fix explicitly and only to wherever the real
   ballot page itself renders, never to any other template. */
#primary.site-main:has(.mdbo-ballotpage) {
  width: 100%;
  flex-basis: 100%;
  min-width: 0;
}

/* Width correction (27 Aug 2026, second pass - corrects a real error in
   the 26 Aug comment this replaces). The earlier comment's premise was
   wrong: .mdbo-ballotpage's actual parent on /best/{year}/ is NOT a bare
   .entry-content with no width of its own - maybe_render_archive_year()
   wraps the whole ballot in <article class="mdbo-archive-article"> (see
   class-mdbo-public.php), which ALREADY carries max-width:1020px and
   padding:32px 20px. Giving .mdbo-ballotpage its OWN second, identical
   max-width:1020px + padding:0 20px was therefore a genuine double-nest,
   losing ~40px of real content width for nothing - confirmed by pulling
   the live rendered HTML and tracing the actual ancestor chain, not
   assumed. Fixed by letting .mdbo-ballotpage simply inherit its parent's
   already-correct, already-centred width instead of re-declaring it. */
.mdbo-ballotpage { width: 100%; box-sizing: border-box; }
.mdbo-ballottitle { font-family: Georgia, serif; font-weight: 400; margin: 8px 0 12px; }
.mdbo-ballotintro { font-size: 1.05rem; max-width: 68ch; margin: 0 0 16px; }
.mdbo-participation { color: var(--md-muted); font-size: .92rem; margin: 4px 0 24px; }

/* Stepper pass (26 Aug 2026, item #6) - a plain, quiet hero: the old
   boxed "model strip" card is gone, replaced with two short reassurance
   lines above the CTA and one muted fact line below it, matching the
   brief's own literal example. The stepper itself does the ongoing
   reinforcing (Skip always present, a live step/selected count on every
   step), so the hero doesn't need to work hard or repeat itself. */
.mdbo-ballotintro-sub { font-size: 1rem; color: var(--md-muted); max-width: 60ch; margin: 0 0 20px; }
.mdbo-herofacts {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline;
  gap: 4px 0; list-style: none; font-size: .88rem; color: var(--md-muted);
  font-weight: 600; margin: 2px 0 18px; padding: 0;
}
.mdbo-herofacts li { white-space: nowrap; padding: 0 10px; position: relative; }
.mdbo-herofacts li:first-child { padding-left: 0; }
.mdbo-herofacts li:not(:first-child)::before {
  content: "\2022"; position: absolute; left: -2px; color: var(--md-border); font-weight: 400;
}
@media (max-width: 480px) {
  .mdbo-herofacts { font-size: .8rem; gap: 2px 0; }
  .mdbo-herofacts li { padding: 0 7px; }
}

/* Visual/conversion polish pass (5 Sep 2026, owner requirements #2/#4/
   #6) - previously an entirely unstyled bare <p> (confirmed by grep
   before this pass: no .mdbo-ballotprize rule existed anywhere in this
   file), which is exactly why the old, much longer sentence read as
   plain undifferentiated text among several other plain paragraphs.
   Deliberately NOT a boxed card (the site already has .mdbo-ballotcta
   for that heavier treatment on individual category pages) - a full box
   here would compete with the "Start voting" button that immediately
   follows it, adding visual bulk exactly where the brief asks for LESS
   of it. Instead: a normal-weight sentence with the prize itself bolded
   in the brand's own green-dark token, plus a small, deliberately
   discreet (muted, smaller, underlined) terms link right beside it -
   satisfies "near the short prize message, include a discreet clear
   link" without adding a second visual block. */
.mdbo-ballotprize { font-size: .95rem; color: var(--md-ink); margin: 0 0 18px; }
.mdbo-ballotprize strong { color: var(--md-green-dark); }
.mdbo-ballotprize a { margin-left: 4px; font-size: .85rem; color: var(--md-muted); white-space: nowrap; }

/* Visual polish pass (5 Sep 2026, owner requirements #1/#2/#10 - "a
   stronger campaign hero", "compact prize panel/badge", "more obvious
   but tasteful local identity"). The hero copy itself (H1/intro/
   intro-sub/prize line/CTA/fact strip, all in class-mdbo-public.php's
   shortcode_ballot()) is completely unchanged - only its container.
   Before this pass it was plain stacked text with no distinguishing
   background at all, directly above a "Jump to a category" disclosure
   and the first candidate grid - exactly the "feels a little bare"
   complaint. Deliberately NOT the sitewide full-bleed deep-green
   .md-pagehero band: this custom archive-year route never renders one
   (see maybe_render_archive_year()), and a heavy green block this high
   up, directly under the page's own sticky header, would compete with
   it and read as a loud landing-page banner - the brief's own explicit
   "without turning it into a loud competition landing page" line. A
   quiet cream panel with a thin green top accent (the same "3px green
   line" motif the site's own header already uses, not a new colour)
   gives the hero real presence and a touch of local/campaign identity
   with one small, restrained container - nothing else about the page's
   mechanics, copy, or the components below it changes. */
.mdbo-hero {
  background: var(--md-cream);
  border: 1px solid var(--md-border);
  border-top: 3px solid var(--md-green);
  border-radius: var(--md-radius);
  padding: 28px 28px 22px;
  margin: 0 0 28px;
}
.mdbo-hero-kicker {
  font-size: var(--md-eyebrow);
  text-transform: uppercase;
  letter-spacing: .08em;
  font-weight: 700;
  color: var(--md-green-dark);
  margin: 0 0 8px;
}
.mdbo-hero .mdbo-ballottitle { margin-top: 0; }
.mdbo-hero .mdbo-herofacts { margin-bottom: 2px; }
/* Mobile: keep this new container's OWN footprint as light as possible
   (measured live at 320px: the pre-existing, unchanged H1/copy alone -
   nothing this pass added - already runs to ~130px across 3 lines at
   this width, well before any padding is counted; the H1's own font
   size is the shared, sitewide --md-h1 clamp, deliberately not touched
   here). Trimming the panel's own padding is the one restrained lever
   available without editing copy or shared typography, so it's used
   here rather than left at the desktop value. */
@media (max-width: 480px) {
  .mdbo-hero { padding: 20px 20px 16px; margin-bottom: 20px; }
  .mdbo-hero-kicker { margin-bottom: 6px; }
}

/* A compact inline prize badge (item #2's own "consider a compact prize
   panel/badge integrated with the hero or intro") - a small pill around
   just the prize label itself, replacing the old plain <strong> tag in
   the same sentence position. Deliberately not a second box around the
   whole sentence (the hero panel above already gives it presence) and
   not a separate line - it reads inline, so the sentence stays one
   quiet line of text with one small highlighted term in it, never a
   loud standalone prize banner. */
.mdbo-prizebadge {
  display: inline-flex;
  align-items: center;
  background: #fff;
  border: 1px solid var(--md-green);
  color: var(--md-green-dark);
  font-weight: 700;
  padding: 1px 10px;
  border-radius: 100px;
  font-size: .95em;
  white-space: nowrap;
}

/* A single, obvious CTA near the top of the /best/ hub (item #8) -
   deliberately reuses the shared .md-btn shape, no new button style. */
.mdbo-hubcta { margin: 4px 0 32px; }

.mdbo-ballotcategory { margin: 40px 0; padding-top: 24px; border-top: 1px solid var(--md-border); }
.mdbo-ballotcategory:first-of-type { border-top: none; padding-top: 0; }
/* Scroll/focus targets for Back/Skip/Next/jump-nav/quick-finish (item
   #7 of the stepper brief) - tabindex="-1" makes each section (and the
   review panel, and the JS-built step header) a valid programmatic
   focus target without adding it to the normal Tab order; outline only
   shows for a genuine keyboard-driven focus-visible state, never for the
   JS scroll-then-focus calls these buttons make.
   scroll-margin-top clears the site's own real sticky header, measured
   directly from its live CSS rather than guessed: .md-header-inner is
   72px tall (60px once .is-scrolled, always 60px below 767px) plus a 3px
   top border + 1px bottom border - so the worst case is 76px desktop,
   64px mobile. Both get a ~12px buffer on top of that. */
.mdbo-ballotcategory, #mdbo-reviewpanel, .mdbo-stepheader { scroll-margin-top: 96px; }
.mdbo-ballotcategory:focus-visible { outline: 2px solid var(--md-green); outline-offset: 6px; }

/* Final visual polish (26 Aug 2026, item #6): the reported "sits too far
   underneath the sticky header" symptom was traced to the two [hidden]
   bugs below (progress pill and step header both silently ignoring
   their own `hidden` attribute, see the fixes further down this file) -
   with those genuinely fixed, nothing extra sits above the target any
   more. No second/JS-computed offset is ever added on top of
   scroll-margin-top (goToStep() calls a plain scrollIntoView() with no
   manual pixel subtraction) - only one offset source exists, so nothing
   can double-stack. The buffer here was bumped 88px/76px -> 96px/84px
   purely as extra headroom, not to compensate for a second offset. */
.mdbo-stepper-active .mdbo-ballotcategory { margin-top: 0; padding-top: 0; border-top: none; }

/* Category-focused entry gets more context (item #5 of the stepper
   brief) - "Starting with" / "One of 13 optional categories..." makes it
   unmistakable this is a STARTING POINT within one combined ballot, not
   a standalone single-category poll. */
.mdbo-startinglabel { text-transform: uppercase; letter-spacing: .04em; font-size: .78rem; font-weight: 700; color: var(--md-green); margin: 0 0 4px; }
.mdbo-startingsub { color: var(--md-muted); font-size: .92rem; margin: 6px 0 0; }

/* Final visual polish (26 Aug 2026, item #5): a short, quiet helper line
   under the heading for every NON-focused step - .mdbo-startingsub above
   already serves this exact reassuring role for a focused ("?cat=")
   entry, so this is only ever rendered as the alternative to it, never
   alongside it (see render_ballot_form()/ballot_category_preview()). */
.mdbo-categoryhelp { color: var(--md-muted); font-size: .95rem; margin: 6px 0 0; }

/* The category heading is now one of the strongest elements on the step
   (item #3/#5) - scoped ONLY to headings inside the ballot's own
   sections, never the shared, sitewide .md-sechead-title rule used by
   every other section heading on the site. */
.mdbo-ballotcategory .md-sechead-title { font-size: clamp(1.7rem, 1.45rem + 1.2vw, 2.3rem); }

/* Real, reproduced mobile horizontal-overflow root cause (28 Aug 2026),
   found and confirmed via a local computed-layout test harness built
   from the actual production markup/CSS, not guessed from static reading
   alone: the shared, sitewide .md-sechead component (Additional CSS) is
   `display:flex` with NO `flex-wrap` declared anywhere (default: nowrap),
   and its own narrow-viewport override (@media max-width:480px) forces
   `.md-sechead-title{white-space:nowrap}`. That combination is harmless
   everywhere else .md-sechead is used sitewide, since every other
   heading there is a short, fixed page-level title ("Pubs", "Food &
   Takeaways") that always fits on one line next to its link. The
   ballot's own usage is different in two ways the shared component was
   never tuned for: it has only two children (title + link, no
   .md-sechead-rule divider) and its title is a DYNAMIC, sometimes-long
   business/category name ("Best Real Ale Pub", "Best Indian Restaurant
   or Takeaway"). With wrapping disallowed and both children forced onto
   one unbroken line, 9 of the 13 real live categories were confirmed
   (via the local harness, activating each category and scanning for any
   element exceeding the true viewport width) to push their own
   "About this category ->"/"Live standings" link off-canvas at mobile
   widths - and for the single longest title, the H2 itself overflowed.
   This directly explains the reported mobile horizontal-scroll symptom.
   Fixed here, scoped to .mdbo-ballotcategory only (both the live
   interactive ballot_candidate_card()/render_ballot_form() path and the
   read-only ballot_category_preview() path share this exact wrapper, so
   both are covered) - never touching the shared global .md-sechead rule
   used correctly everywhere else on the site. flex-wrap:wrap lets the
   link drop to its own line under the title once they don't both fit;
   white-space:normal lets the title itself wrap onto a second line for
   the rare case where even the title alone is too long for the
   viewport, rather than forcing one overflowing line. */
.mdbo-ballotcategory .md-sechead { flex-wrap: wrap; }
.mdbo-ballotcategory .md-sechead-title { white-space: normal; }

/* One-category-at-a-time stepper (26 Aug 2026) - JS-applied only, never
   a default/server-rendered state, so a visitor without JavaScript still
   sees every section (see class-mdbo-public.php's own docblock on
   render_ballot_form() for the full progressive-enhancement reasoning). */
.mdbo-step-hidden { display: none; }

/* The persistent stepper header (item #1/#2) - built and inserted by JS
   only, directly above whichever category section is currently shown.
   "STEP N OF 13" never implies N of 13 selections are required - the
   live "N selected" count sits alongside it as a genuinely separate
   fact, and the copy itself never uses words like "completed" or
   "required" for step position. */
.mdbo-stepheader { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 4px 12px; margin: 0 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--md-border); }
.mdbo-stepheader-step { text-transform: uppercase; letter-spacing: .05em; font-size: .8rem; font-weight: 700; color: var(--md-green-dark); }
.mdbo-stepheader-count { font-size: .82rem; font-weight: 600; color: var(--md-muted); }
/* Real bug fix (26 Aug 2026, items #3/#4/#6) - the actual root cause of
   "the old progress pill still shows underneath the step header" and,
   separately, "Review & submit shows '0 categories selected' at zero
   selections": `hidden` is a real DOM attribute, and the browser's own
   built-in stylesheet has exactly one rule for it - [hidden]{display:
   none} - at the SAME specificity (0,1,0) as a plain class selector.
   Both .mdbo-stepheader and (further down) .mdbo-ballotprogress declare
   their OWN `display` value, and an author stylesheet always loads
   after the browser's built-in one - so at equal specificity, THIS rule
   wins the tie and silently overrides `hidden`, even though the JS was
   correctly setting `element.hidden = true` all along. This is the same
   bug class already fixed once before on this exact site (the Contact
   form's success panel, and MDA_Support's custom-amount field) - the
   fix is always the same: give the hidden state its own, more specific
   rule that a bare class selector can never out-rank. */
.mdbo-stepheader[hidden] { display: none; }

/* The divider between a focused category and the rest of the ballot
   (item #4) - a real H2 in the normal heading flow plus one reassuring
   line, so the transition out of the starting category is unmistakable. */
.mdbo-morecats { margin: 48px 0 0; padding-top: 24px; border-top: 1px solid var(--md-border); }
.mdbo-morecats-heading { font-family: Georgia, serif; font-weight: 400; text-transform: uppercase; letter-spacing: .03em; font-size: .95rem; margin: 0 0 4px; color: var(--md-green-dark); }
.mdbo-morecats-sub { color: var(--md-muted); font-size: .92rem; margin: 0; }

/* Back/Skip/Next(-or-Review) guided navigation at the foot of every step
   (stepper item #3) - Back and Skip both read as quieter, secondary
   actions (plain text, no fill); Next/Review as the one primary forward
   step. All three simply change step client-side; none requires a
   selection or submits anything (see assets/ballot-form.js). Back is
   only ever rendered from the second step onward (see class-mdbo-
   public.php) - there is nothing to hide/disable in CSS for the first
   step, since the button itself is never in the DOM there. */
/* Final visual polish (26 Aug 2026, item #4) - ONE sticky action dock,
   replacing the old separate floating "quick-finish" pill entirely (see
   quick_finish_bar() removal in class-mdbo-public.php). Rather than
   introduce a second, independently-positioned element, this reuses the
   EXACT nav row every step already renders - it becomes the dock simply
   by fixing its own position once the stepper is active. Since only one
   .mdbo-ballotcategory is ever visible at a time (every other one is
   display:none, which also fully hides its own descendant .mdbo-
   categorynav regardless of that descendant's own position value), only
   ONE fixed dock can ever be on screen at once - no stacking/overlap
   risk, and no JS needs to move/rebuild anything.
   Base rule below is the UNCHANGED, ORIGINAL in-flow row - this is what
   a visitor with JavaScript disabled still sees (a plain nav row after
   each category's own candidates, exactly as before the stepper
   existed). The fixed/sticky treatment is added ONLY under
   .mdbo-stepper-active, a class JS applies once the stepper genuinely
   initialises (see ballot-form.js's initStepper()) - never a blanket
   rule, so the no-JS fallback can never get 13 overlapping fixed bars. */
.mdbo-categorynav { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px 14px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--md-border); }
.mdbo-categorynav-back, .mdbo-categorynav-skip {
  background: none; border: none; padding: 10px 4px; min-height: 44px;
  color: var(--md-muted); font-weight: 600; font-size: .92rem;
  cursor: pointer; flex: 0 0 auto;
  appearance: none; -webkit-appearance: none;
}
.mdbo-categorynav-skip { text-decoration: underline; }
.mdbo-categorynav-back:hover, .mdbo-categorynav-back:focus-visible,
.mdbo-categorynav-skip:hover, .mdbo-categorynav-skip:focus-visible { color: var(--md-green-dark); background: none; }
.mdbo-categorynav-back:active, .mdbo-categorynav-skip:active { color: var(--md-green-dark); background: none; }
.mdbo-categorynav-back:focus-visible, .mdbo-categorynav-skip:focus-visible { outline: 2px solid var(--md-green); outline-offset: 2px; }
/* Next/Review is always the last child in the row regardless of whether
   Back is present, so a plain margin-left:auto reliably pins it to the
   far right without needing to know how many siblings precede it. */
.mdbo-categorynav-next, .mdbo-categorynav-review { margin-left: auto; }

/* The quiet "N selected · Review votes" action (item #4) - server-
   rendered once per non-final step (see render_ballot_form()), hidden
   by default and shown/updated by JS the moment ≥1 category is selected
   anywhere in the ballot (assets/ballot-form.js's updateQuickReview()).
   Deliberately quieter than Next (a filled cream pill, not a solid
   green one) and never rendered at all on the final step, where Review
   is already the primary action in that slot. */
.mdbo-categorynav-quickreview {
  display: inline-flex; align-items: center; min-height: 40px; padding: 8px 16px;
  background: var(--md-cream); border: 1px solid var(--md-border); border-radius: 100px;
  color: var(--md-green-dark); font-weight: 600; font-size: .88rem; white-space: nowrap;
  cursor: pointer; flex: 0 0 auto; appearance: none; -webkit-appearance: none;
}
.mdbo-categorynav-quickreview:hover, .mdbo-categorynav-quickreview:focus-visible { border-color: var(--md-green); background: var(--md-cream); }
.mdbo-categorynav-quickreview:active { background: var(--md-cream); border-color: var(--md-green); }
.mdbo-categorynav-quickreview:focus-visible { outline: 2px solid var(--md-green); outline-offset: 2px; }
/* Same [hidden]-vs-explicit-display bug as .mdbo-stepheader/.mdbo-
   ballotprogress above - fixed here from the start rather than
   introduced fresh. */
.mdbo-categorynav-quickreview[hidden] { display: none; }

.mdbo-stepper-active .mdbo-categorynav {
  position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
  z-index: 40; max-width: calc(100% - 32px); width: max-content;
  margin: 0; padding: 10px 18px; background: #fff;
  border: 1px solid var(--md-border); border-radius: 100px; box-shadow: var(--md-shadow);
}
/* Reserves enough trailing space on whichever step is currently visible
   so the fixed dock can never sit on top of its last candidate card
   (item #4's "must not obscure candidate cards" requirement) - scoped to
   the one section that isn't hidden, so it costs nothing on the 12
   sections that are.
   Bumped (28 Aug 2026): the dock's own real height is 10px+10px padding
   plus a 44px button row (.md-btn's sitewide 44px min-height, the
   tallest control in it) = 64px, plus its own 16px `bottom` offset from
   the viewport edge = 80px of genuinely occupied space - the previous
   96px value left only a bare 16px margin past that, which real use
   showed reads as visually crowded/overlapping rather than clearly
   clear. A ~34px buffer on top of the real 80px is a comfortable, not
   arbitrary, gap - large enough that the final candidate row always
   sits visibly above the dock, never just technically above it. */
.mdbo-stepper-active .mdbo-ballotcategory:not(.mdbo-step-hidden) { padding-bottom: calc(10px + 44px + 10px + 16px + 34px); }

.mdbo-ballotcta { background: var(--md-cream); border: 1px solid var(--md-border); border-radius: var(--md-radius); padding: 18px 20px; margin-top: 16px; display: flex; flex-direction: column; gap: 12px; align-items: flex-start; }
.mdbo-ballotcta p { margin: 0; }

/* Launch-readiness fix (26 Aug 2026): top was 8px, which sits WITHIN the
   site's own sticky header (position:sticky, z-index:200, 60px once
   .is-scrolled - the state it will genuinely be in by the time this
   pill's own stickiness engages, since the header's scroll threshold
   is much smaller). With a lower z-index than the header, the pill was
   rendering hidden behind it rather than visibly stuck below it. 68px
   clears the header's 60px scrolled height with an 8px gap. Mobile is
   unaffected - the 600px media query below already makes this static,
   not sticky, there.
   Voting-UX polish pass (26 Aug 2026, item #3): restyled from a solid
   filled green pill to a lighter cream one with green text - the solid
   fill read like a completion/loading indicator implying all categories
   must be finished, which is the opposite of this ballot's own "vote in
   as many as you like" model. The lighter treatment plus the trailing
   reassurance text (see the PHP template) keeps it genuinely secondary
   and reassuring rather than a pressing requirement. */
.mdbo-ballotprogress { position: sticky; top: 68px; z-index: 5; display: inline-block; background: var(--md-cream); border: 1px solid var(--md-border); color: var(--md-green-dark); font-weight: 600; font-size: .85rem; padding: 7px 16px; border-radius: 100px; margin: 0 0 16px; }
/* Final visual polish (26 Aug 2026, item #3): this is the no-JS fallback
   count line only - JS always hides it via `hidden` (see initStepper())
   the moment the real "Step N of 13 / N selected" header takes over.
   Without this rule the bug above meant it stayed visible underneath
   the new header, reading as duplicated/fragmented hierarchy exactly as
   reported. Now genuinely gone once the stepper is active; still fully
   present and correct for a visitor with JavaScript disabled. */
.mdbo-ballotprogress[hidden] { display: none; }

.mdbo-ballotgroup { margin-top: 8px; }
/* Candidate-card surface depth (item #6: "slightly richer card surface
   treatment" / "subtle shadow/surface depth"). The shared, sitewide
   base .mdbo-candidatecard rule (global Additional CSS) has no shadow
   at all in its resting state - only :has(input:checked) below and the
   existing hover-lift rule further down this file ever add one. On a
   touch device (no hover capability at all) an unselected card was
   therefore completely flat: a plain 1px border, zero depth. This adds
   one small, constant, restrained shadow to every resting card, scoped
   to the ballot only (never the read-only category-page/Place-
   nomination cards, which weren't part of this brief and haven't been
   reported as a problem) - layered UNDER the existing hover/selected
   treatments, both of which still read as a clear, distinct "lift"
   against this new, deliberately subtle baseline rather than against
   nothing at all. Kept light enough that it never reads as an
   ecommerce-style product shadow, per the brief's own explicit caution. */
.mdbo-ballotgroup .mdbo-candidatecard { box-shadow: 0 1px 4px rgba(18, 58, 43, .07); }
/* A selected candidate within the ballot gets a visible highlight beyond
   the native radio dot alone (colour is never the only cue - the label
   text and the dot itself both already change) - matches the pattern
   already used for Support's own amount-selection cards elsewhere on
   this site, reused here rather than reinvented. */
.mdbo-candidatecard:has(input[type=radio]:checked) { border-color: var(--md-green); box-shadow: 0 0 0 2px var(--md-green); }
.mdbo-candidatecard:has(input[type=radio]:focus-visible) { outline: 2px solid var(--md-green); outline-offset: 2px; }
/* Selection state made very obvious (item #7, 26 Aug 2026 UX pass) -
   beyond the existing border/shadow highlight above: the selected
   candidate's name turns green, its own "choose" row tints cream and
   gains a real checkmark (a genuine visual indicator, not colour alone -
   the row's own text also changes to "Selected" via a small JS toggle,
   see assets/ballot-form.js), and a plain hover tint helps every card
   read as clickable before it's ever touched. Kept within the site's
   existing --md-* palette throughout - no new colours introduced. */
.mdbo-candidatecard:has(input[type=radio]:checked) .mdbo-candidatecard-name { color: var(--md-green-dark); }
.mdbo-candidatecard-choose:hover { background: var(--md-cream); }
.mdbo-candidatecard:has(input[type=radio]:checked) .mdbo-candidatecard-choose {
  background: var(--md-cream); color: var(--md-green-dark);
}
.mdbo-candidatecard:has(input[type=radio]:checked) .mdbo-candidatecard-choose::after {
  content: '\2713'; margin-left: auto; color: var(--md-green); font-weight: 800; font-size: 1.15em; line-height: 1;
}

.mdbo-ballotpanel { background: var(--md-cream); border-radius: var(--md-radius); padding: 24px; margin-top: 32px; max-width: 560px; }
.mdbo-checkfield { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 14px; cursor: pointer; font-size: .96rem; line-height: 1.4; }
.mdbo-checkfield input { width: 20px; height: 20px; flex: 0 0 auto; margin-top: 2px; }
.mdbo-ballotform-submit { width: 100%; justify-content: center; }

.mdbo-drawconfirm, .mdbo-newsletterconfirm { background: var(--md-cream); border-radius: 8px; padding: 12px 16px; margin: 12px 0; }
.mdbo-sharecta { margin: 24px 0; padding-top: 16px; border-top: 1px solid var(--md-border); }
.mdbo-sharecta p { margin: 0 0 10px; }
/* Category-expansion pass (26 Aug 2026) - both deliberately restrained,
   secondary lines, never a giant table or a repeated/nagging prompt.
   Final-polish pass (26 Aug 2026): the standings block reads as one
   short heading plus a naturally-wrapping row of short category chips,
   never a repeated "View live standings for X" sentence per category -
   works cleanly whether one or several categories were confirmed. */
.mdbo-standingscta { font-size: .92rem; margin: 16px 0 0; }
.mdbo-standingscta-heading { font-weight: 700; margin: 0 0 8px; color: var(--md-ink); }
.mdbo-standingslinks { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.mdbo-standingslinks a { white-space: nowrap; }
.mdbo-ballothero-cta { margin: 4px 0 20px; }
.mdbo-supportcta { color: var(--md-muted); font-size: .88rem; margin-top: 20px; }
.mdbo-supportcta a { color: var(--md-muted); text-decoration: underline; }

.mdbo-pollsubtitle { font-size: .55em; font-weight: 600; color: var(--md-muted); vertical-align: middle; }

@media (max-width: 600px) {
  .mdbo-ballotpanel { padding: 18px; }
  .mdbo-ballotprogress { position: static; display: block; text-align: center; }
}

/* The real, measured sticky-header clearance below 767px (see the
   scroll-margin-top rule earlier in this file for the full reasoning) -
   a separate, slightly smaller value since the header itself is a fixed
   60px there regardless of scroll position. */
@media (max-width: 767px) {
  .mdbo-ballotcategory, #mdbo-reviewpanel, .mdbo-stepheader { scroll-margin-top: 84px; }
}

/* Homepage promotion module - prepared but disabled by default
   (homepage_promotion_enabled setting). See class-mdbo-public.php's
   maybe_append_homepage_module(). */
.mdbo-homepagemodule { margin: 40px 0; }
.mdbo-homepagemodule p { max-width: 60ch; }

/* ===== Best Of 2.1 (25 Aug 2026): category jump navigation ===== */
/* Restyled in the 26 Aug 2026 UX polish pass (items #2/#9) from a flat
   row of up to 13+ pills into a compact, collapsed-by-default <details>
   disclosure - a native element, so it works with zero JS: "Jump to a
   category" is always the visible control, the full wrapping link row
   only appears once opened. Never sticky (would compete with the page's
   own sticky header and the ballot's own sticky progress pill). */
.mdbo-jumpnav-details { margin: 20px 0 32px; }
.mdbo-jumpnav-details summary {
  display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 8px 16px;
  background: #fff; border: 1px solid var(--md-border); border-radius: 100px;
  color: var(--md-ink); font-size: .9rem; font-weight: 600; cursor: pointer; list-style: none;
}
.mdbo-jumpnav-details summary::-webkit-details-marker { display: none; }
.mdbo-jumpnav-details summary::after { content: '\25BE'; font-size: .8em; color: var(--md-muted); }
.mdbo-jumpnav-details[open] summary::after { content: '\25B4'; }
.mdbo-jumpnav-details summary:hover, .mdbo-jumpnav-details summary:focus-visible { border-color: var(--md-green); color: var(--md-green); }
.mdbo-jumpnav-details summary:focus-visible { outline: 2px solid var(--md-green); outline-offset: 2px; }
.mdbo-jumpnav { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; }
.mdbo-jumpnav a {
  display: inline-flex; align-items: center; min-height: 36px; padding: 6px 14px;
  background: var(--md-cream); border: 1px solid var(--md-border); border-radius: 100px;
  color: var(--md-ink); font-size: .88rem; font-weight: 600; text-decoration: none;
}
.mdbo-jumpnav a:hover, .mdbo-jumpnav a:focus-visible { border-color: var(--md-green); color: var(--md-green); }
.mdbo-jumpnav a:focus-visible { outline: 2px solid var(--md-green); outline-offset: 2px; }

/* ===== Best Of 2.1 (25 Aug 2026): live standings + final results ===== */
/* Deliberately its own visually distinct block (a bordered cream panel,
   the same shared surface language as .mdbo-ballotcta) so it reads as
   genuinely separate from the neutral candidate-selection cards above
   it, per the owner's own explicit "visually and functionally separate
   from the ballot" requirement. render_results() (final certified
   results) reuses the identical list/row structure via the shared
   .mdbo-standingslist/-standingsgroup/-standingsrow classes, with
   .mdbo-resultslist only adding a slightly stronger border tint - one
   real implementation, not two independently-drifting ones. */
.mdbo-standings { background: var(--md-cream); border: 1px solid var(--md-border); border-radius: var(--md-radius); padding: 22px 24px; margin-top: 28px; }
.mdbo-standings--pending { padding: 18px 24px; }
.mdbo-standingslist { list-style: none; margin: 12px 0 0; padding: 0; }
.mdbo-resultslist { margin-top: 0; }
.mdbo-standingsgroup { border-bottom: 1px solid var(--md-border); padding: 10px 0; }
.mdbo-standingsgroup:last-child { border-bottom: none; }
.mdbo-standingsrow { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; padding: 2px 0; }
.mdbo-standingsrow-medal { font-size: 1.3rem; line-height: 1; flex: 0 0 auto; }
/* The medal is decorative only - position/name/votes are always also
   present as real text (never medal-colour-only), so a screen reader or
   anyone who can't distinguish gold/silver/bronze still gets the full
   information. */
.mdbo-standingsrow-position { font-weight: 700; color: var(--md-green-dark); flex: 0 0 auto; min-width: 62px; }
.mdbo-standingsrow-name { font-weight: 600; flex: 1 1 160px; }
.mdbo-standingsrow-votes { color: var(--md-muted); font-size: .9rem; flex: 0 0 auto; }
.mdbo-standingsnote { color: var(--md-muted); font-size: .88rem; margin: 14px 0 0; }
/* Category-expansion pass (26 Aug 2026): the full ranked field now
   includes every eligible candidate, so a category with more nominees
   than votes cast so far will genuinely show some at zero. These are
   deliberately never given a numbered position (a large tied-at-zero
   group ranked "Joint 8th" would read as absurd) - a restrained, more
   muted row style is enough to distinguish "hasn't been voted for yet"
   from a real ranked result, without hiding the nominee or pretending
   it has votes. */
.mdbo-standingsgroup--zero { border-top: 1px dashed var(--md-border); margin-top: 4px; padding-top: 10px; }
.mdbo-standingsrow--zero { opacity: .6; }
.mdbo-standingsrow--zero .mdbo-standingsrow-name { font-weight: 500; }

@media (max-width: 480px) {
  .mdbo-standings { padding: 18px; }
  .mdbo-standingsrow { flex-direction: column; align-items: flex-start; gap: 2px; }
  .mdbo-standingsrow-position { min-width: 0; }
  /* Real bug found via live mobile QA, 25 Aug 2026: .mdbo-standingsrow-name's
     desktop `flex: 1 1 160px` sets a WIDTH flex-basis in the default row
     layout, but once this same rule flips the row to flex-direction:column
     above, flex-basis instead controls the (now vertical) main axis -
     turning that same 160px into a literal 160px-tall name span and
     stranding ~140px of empty space beneath a short business name. Reset
     to a content-sized flex-basis here so the column layout never inherits
     a row-only sizing hint. */
  .mdbo-standingsrow-name { flex: 0 0 auto; }
}

/* ===== Voting-UX polish pass (26 Aug 2026): pending/confirmed ballot
   states + the polished email-confirmation landing page (items #5/#6/#7)
   ===== */
/* .mdbo-ballotpending/.mdbo-ballotconfirmed are genuinely top-level
   siblings of #mdbo-ballotwrap (never nested inside it or inside the
   <form>) - this is what makes them safe to reveal even once the wrap
   is hidden; see the real bug this fixes, documented in ballot-form.js.
   Both share the exact same card language as .mdbo-verifyresult
   (max-width 560px, white/cream card) and the same inner component
   classes (.mdbo-verifytick, -verifyintro, -verifyselections, -verifyactions)
   as the server-rendered /best/verify/ landing page below, so all three
   surfaces - pending, JS-detected-confirmed, and the real email-click
   landing page - look and read identically. */
.mdbo-ballotpending, .mdbo-ballotconfirmed {
  max-width: 560px; background: #fff; border: 1px solid var(--md-border);
  border-radius: var(--md-radius); padding: 32px 28px; margin: 20px 0; text-align: center;
}
.mdbo-ballotpending { border-color: #E9D9A8; background: #FFFDF7; }
.mdbo-ballotconfirmed { border-color: #BFE3D2; background: #E7F5EE; }
.mdbo-ballotpending p, .mdbo-ballotconfirmed p { text-align: left; }
.mdbo-ballotpending > p:first-of-type,
.mdbo-ballotpending .mdbo-verifyintro,
.mdbo-ballotconfirmed .mdbo-verifyintro { text-align: center; }
.mdbo-pendinghint { color: var(--md-muted); font-size: .85rem; margin-top: 18px; }

/* Shared success/confirmation components - a restrained tick, a centred
   heading/intro, and a left-aligned, easily scannable "category ->
   business" selections list (never centred - a right-shifting label per
   row is genuinely harder to scan than a left-aligned one). */
.mdbo-verifyresult--ballot { text-align: center; }
.mdbo-verifyresult--ballot h2 { text-align: center; }
.mdbo-verifytick {
  width: 48px; height: 48px; margin: 0 auto 12px; border-radius: 50%;
  background: var(--md-green); color: #fff; display: flex; align-items: center; justify-content: center;
  font-size: 1.4rem; font-weight: 700;
}
.mdbo-ballotpending .mdbo-verifytick { background: #B5532B; }
.mdbo-verifyintro { color: var(--md-muted); margin: 0 0 20px; }
.mdbo-verifyselections {
  text-align: left; background: var(--md-cream); border: 1px solid var(--md-border);
  border-radius: 8px; padding: 16px 18px; margin: 0 0 20px;
}
.mdbo-verifyselections-heading { font-weight: 700; margin: 0 0 10px; color: var(--md-ink); }
.mdbo-verifyselections-list { list-style: none; margin: 0; padding: 0; }
.mdbo-verifyselections-list li {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 8px 0; border-bottom: 1px solid var(--md-border);
}
.mdbo-verifyselections-list li:last-child { border-bottom: none; }
.mdbo-verifyselections-cat { color: var(--md-muted); font-size: .9rem; }
.mdbo-verifyselections-name { font-weight: 700; color: var(--md-green-dark); text-align: right; }
.mdbo-verifyactions { margin: 0 0 8px; }
.mdbo-verifyactions .md-btn, .mdbo-verifyactions .md-btn-outline { min-width: 220px; justify-content: center; }

@media (max-width: 480px) {
  .mdbo-ballotpending, .mdbo-ballotconfirmed, .mdbo-verifyresult { padding: 24px 18px; }
  .mdbo-verifyselections-list li { flex-direction: column; align-items: flex-start; gap: 2px; }
  .mdbo-verifyselections-name { text-align: left; }
  .mdbo-verifyactions .md-btn, .mdbo-verifyactions .md-btn-outline { width: 100%; }
}

/* ===== Visual UX pass (26 Aug 2026): review panel + quick-finish bar
   (items #5/#8) ===== */
/* The review panel ("Ready to submit?") - a live, honest running count of
   how many categories have been selected, updated the moment any radio
   changes anywhere in the ballot (assets/ballot-form.js), plus the
   shared .mdbo-verifyselections list component reused for a compact
   "your selections so far" summary - never a giant table. outline-none
   is deliberate ONLY alongside the visible :focus-visible style further
   down; the panel itself is a Next/Skip/quick-finish scroll TARGET
   (tabindex="-1"), not a normal interactive control. */
.mdbo-panelsummary { font-weight: 600; color: var(--md-ink); margin: 0 0 14px; }
#mdbo-reviewpanel:focus-visible { outline: 2px solid var(--md-green); outline-offset: 4px; }
.mdbo-panelback { text-align: center; margin: 4px 0 24px; }

/* Review step "Change" link (stepper item #9) - a compact text button
   appended to a selected row, only ever present in the review step's own
   list (never the pending/confirmed lists, which reuse the same base
   .mdbo-verifyselections-list styling but pass no Change button - see
   renderSelectionsList() in ballot-form.js). The base li keeps its
   existing justify-content:space-between for those two-item rows;
   this modifier switches to flex-start + margin-left:auto on the button
   itself once a third child is present, so cat+name sit together on the
   left and Change sits alone on the right. */
.mdbo-verifyselections-list li.mdbo-verifyselections-item--withchange { justify-content: flex-start; }
.mdbo-verifyselections-change {
  background: none; border: none; padding: 6px 4px; min-height: 32px;
  margin-left: auto; color: var(--md-green-dark); font-weight: 600; font-size: .85rem;
  text-decoration: underline; cursor: pointer; flex: 0 0 auto;
  appearance: none; -webkit-appearance: none;
}
.mdbo-verifyselections-change:hover, .mdbo-verifyselections-change:focus-visible, .mdbo-verifyselections-change:active { color: var(--md-green); background: none; }
.mdbo-verifyselections-change:focus-visible { outline: 2px solid var(--md-green); outline-offset: 2px; }

/* The old standalone "quick-finish" pill (a separate floating element)
   is retired entirely this pass (item #4) - its job is now done by the
   .mdbo-categorynav-quickreview button living inside the ONE sticky
   action dock above, so there is only ever one bottom-fixed control on
   screen, never two competing ones. See class-mdbo-public.php - quick_
   finish_bar() and its call site are both removed, not just hidden. */

/* Mobile: don't cram Back/Skip/Next/quick-review into one tight row.
   The dock becomes a full-width bottom bar; Back/Skip/Next keep the one
   main row, and the quiet review action - only present once >=1
   category is selected - wraps onto ITS OWN row above them via `order`,
   matching the brief's own mobile example ("1 selected · Review votes"
   sits above "Back Skip Next"). */
@media (max-width: 600px) {
  .mdbo-stepper-active .mdbo-categorynav {
    left: 0; right: 0; bottom: 0; transform: none; width: auto; max-width: none;
    border-radius: 0; border-left: none; border-right: none; border-bottom: none;
    padding: 10px 14px max(10px, env(safe-area-inset-bottom));
    justify-content: space-between;
  }
  /* The mobile dock can genuinely be TWO rows tall once the quick-review
     action appears (its own 40px row + 4px margin, sitting above the
     normal 44px Back/Skip/Next row) = 88px of real content, plus the
     dock's own 10px top padding and up to a real device's
     safe-area-inset-bottom (commonly ~34px on a notched phone) on the
     bottom = up to ~132px of genuinely occupied space worst case - the
     previous 116px value could fall short of that exact worst case.
     Bumped with the same generous ~20px comfort buffer as desktop. */
  .mdbo-stepper-active .mdbo-ballotcategory:not(.mdbo-step-hidden) { padding-bottom: calc(40px + 4px + 44px + 10px + 34px + 20px); }
  .mdbo-categorynav-quickreview { order: -1; flex-basis: 100%; justify-content: center; margin-bottom: 4px; }
}

/* ===== Ballot candidate grid: flexbox rewrite (28 Aug 2026, 3rd
   revision) =====
   History, briefly (full detail was in prior revisions of this comment,
   now removed rather than left as several stacked, competing layers):
   a plain auto-fill/minmax grid (26 Aug) was silently overridden by the
   global stylesheet's own same-named rule; an explicit 3/2/1
   repeat(N,1fr) grid fixed that (27 Aug); a --mdbo-cols/--mdbo-col-w
   pair then applied a max-width + margin:auto to the GRID ITSELF for
   1-2-candidate categories, which the owner's own screenshots showed
   was producing a genuinely narrower ballot area on other categories
   too (removed, first revision); a 6-sub-column CSS Grid with
   grid-column spans and :nth-child()/:nth-last-child() centring maths
   replaced it (second revision) - mathematically sound against every
   real candidate count, but the owner's follow-up screenshots showed
   Best Pizza's entire active-step area (progress line, heading, grid)
   STILL narrower than Best Breakfast's at the same viewport, and a
   width difference that also affects the progress line and heading
   cannot come from anything inside the candidate grid at all (both of
   those sit OUTSIDE .mdbo-candidategrid, as plain siblings within the
   same .mdbo-ballotcategory section) - meaning the grid was never
   actually the site of the bug, on either revision. Exhaustively
   re-traced the entire chain instead this time (.mdbo-archive-article,
   .entry-content, .mdbo-ballotpage, .mdbo-ballotform, .mdbo-
   ballotcategory, the step header, the heading wrapper) via the real
   PHP output and every CSS selector capable of matching any of them -
   none carries any candidate-count-dependent class, inline style, or
   rule anywhere; the JS stepper (initStepper()/goToStep()) only ever
   toggles the .mdbo-step-hidden CLASS, never touches inline styles or
   measures anything. No further candidate-count-conditional cause could
   be found in the width chain above the grid, so the CSS Grid + explicit
   positioning technique itself (an inherently more complex mechanism
   than this problem needs, combining sub-column spans, explicit
   grid-column starts and modular-arithmetic selectors) is retired in
   favour of the simplest robust technique that can produce this exact
   requirement: plain flexbox with a fixed, unconditional per-card width
   and native wrapping/centring. This removes an entire class of
   uncertainty rather than adding a third layer of grid patches on top of
   two already-superseded ones. */
.mdbo-candidategrid.mdbo-ballotgroup {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 20px;
}
/* Every real card gets a FIXED width (flex-grow:0, flex-shrink:1 only as
   a sub-pixel-rounding safety margin, never to visibly narrow a card) -
   never a fraction of however many candidates happen to share its own
   row, so a category with 2 candidates and a category with 12 produce
   byte-for-byte the same per-card width at a given breakpoint. Native
   flex-wrap puts exactly 3 (desktop) / 2 (tablet) / 1 (mobile) cards per
   line before wrapping, since 3/2/1 of this exact width plus their gaps
   sum to precisely 100% - no separate column-count declaration is
   needed anywhere, unlike the Grid approach this replaces. Centring an
   incomplete final row (1 or 2 leftover cards) is entirely native
   flexbox behaviour (justify-content:center applies PER WRAPPED LINE,
   not to the whole container) - genuinely zero extra CSS is needed for
   this, in sharp contrast to the removed Grid technique, which needed
   dedicated :nth-child()/:nth-last-child() rules for exactly this. */
.mdbo-candidategrid.mdbo-ballotgroup > .mdbo-candidatecard {
  flex: 0 1 100%;
}
@media (min-width: 600px) and (max-width: 899px) {
  .mdbo-candidategrid.mdbo-ballotgroup > .mdbo-candidatecard {
    flex-basis: calc((100% - 20px) / 2);
  }
}
@media (min-width: 900px) {
  .mdbo-candidategrid.mdbo-ballotgroup > .mdbo-candidatecard {
    flex-basis: calc((100% - 40px) / 3);
  }
}

/* ===== Whole-card clickable candidate cards (27 Aug 2026, semantic-
   cleanup revision - supersedes the same-day earlier version, which
   made the OUTER .mdbo-candidatecard element itself a <label>) =====
   The outer element is back to being a plain, structurally normal
   <div class="mdbo-candidatecard"> (valid markup - a <label>'s
   permitted content is phrasing content only, and the card holds a
   heading + paragraphs). Whole-card click/tap is instead provided by
   ONE extra, purely-presentational child: .mdbo-candidatecard-hitarea,
   a real <label for="..."> stretched via position:absolute;inset:0 to
   cover the entire card - the exact same "stretched hit-area"
   technique already used elsewhere on this site for a whole-card link
   (.md-venuecard-name a::after), just applied to a form label instead
   of an anchor. It has no visible content of its own, so it can never
   obscure the image/name/settlement/choose-row beneath it; it paints
   after them in DOM order and additionally gets z-index:1 to make that
   explicit rather than relying on source order alone. The radio's
   accessible name comes from a real aria-labelledby pointing at the
   visible <h3> name (+ settlement paragraph, when present) - NOT from
   this stretched label, which is aria-hidden="true" and carries no
   text anyway - so a screen reader still gets a fully meaningful name
   regardless of the overlay. There is still exactly one radio and one
   meaningful tab-stop per card (a <label> is never independently
   focusable), so keyboard behaviour is unchanged.
   Scoped via :has(input[type=radio]), not a new class name, so this
   can only ever match the real interactive ballot card - the read-only
   candidate_card() used on individual category pages and Place-page
   nomination blocks has no radio input at all and stays completely
   unaffected (confirmed: that function was not touched, and still
   renders a plain, non-labelled <div>). */
.mdbo-candidatecard { position: relative; }
.mdbo-candidatecard-hitarea { position: absolute; inset: 0; z-index: 1; margin: 0; cursor: pointer; }
.mdbo-candidatecard:has(input[type=radio]) { cursor: pointer; }
@media (hover: hover) and (pointer: fine) {
  /* A deliberately muted grey lift, not the brand green already used
     for the real :checked state (line 2533 in the global stylesheet) -
     hovering an unselected card must never look like it's already
     selected. Skipped entirely on an already-checked card (:not(:has
     (input:checked))) so hover never layers a second, redundant shadow
     on top of the real selected-state ring. */
  .mdbo-candidatecard:has(input[type=radio]):not(:has(input:checked)):hover {
    border-color: var(--md-muted);
    box-shadow: 0 2px 10px rgba(0,0,0,.08);
  }
}
/* Touch devices get no hover state at all (the media query above never
   matches a coarse pointer) - the card is still fully tappable via the
   native label/input association, satisfying "must work normally
   without hover on touch devices" with no separate touch-specific code
   needed. */
/* "Selected" text turns brand green to stay clearly distinguishable
   from the plain "Vote" default state, on top of the card's own
   existing green border+shadow ring - two independent visual signals,
   never colour alone. */
.mdbo-candidatecard:has(input:checked) .mdbo-candidatecard-choose-text { color: var(--md-green); }

/* ===== Ballot candidate-card size consistency, 2nd pass (28 Aug 2026)
   =====
   The 27 Aug fix below floored only the NAME element to a 2-line
   minimum. That was correct as far as it went, but re-diagnosed per the
   owner's continued, repeated observation across several categories
   (Dog-Friendly Pub/Breakfast/Pizza/Indian) rather than assumed still
   complete: CSS Grid's default align-items:stretch only equalises cards
   WITHIN one row of one category's own grid - it can never make one
   CATEGORY's cards match a DIFFERENT category's cards, since every
   category renders its own separate .mdbo-candidategrid.mdbo-ballotgroup
   container. Flooring only the name left the total BODY height (name +
   settlement + padding, the only remaining source of natural variance
   once the media box and the choose row were already fixed-height/
   fixed-ratio) still assembled from two independently-sized pieces
   rather than one deliberately-reserved region - a small but genuine
   gap versus true cross-category consistency.
   Fix: reserve the WHOLE content region (name + settlement + the
   body's own padding) as ONE floor on .mdbo-candidatecard-body itself,
   not just the name - sized for a genuine 2-line name plus a 1-line
   settlement, every measurement derived from each element's own
   already-declared font-size (never a guessed pixel figure). A card
   whose real content is shorter than this pads out to match; a card
   whose name genuinely needs more than 2 lines simply grows past the
   floor with zero clipping/truncation/ellipsis, exactly as before -
   only the SOURCE of the reservation moved from one child element to
   their shared parent, which is what makes it now genuinely apply the
   same regardless of which category's grid it sits in. The narrower
   per-name min-height/line-height is kept (still correct, and still
   what actually wraps a long name at exactly 1.25 line-height), just no
   longer the only thing carrying the whole reservation.
   Still no full-card fixed height, still nothing category-specific, and
   the 3/2/1 grid/width system below is untouched. Scoped to
   .mdbo-ballotgroup only - the read-only category-page/Place-nomination
   cards, never reported as a problem, stay unaffected. */
.mdbo-ballotgroup .mdbo-candidatecard-name {
  line-height: 1.25;
}
.mdbo-ballotgroup .mdbo-candidatecard-settlement {
  line-height: 1.3;
}
.mdbo-ballotgroup .mdbo-candidatecard-body {
  min-height: calc(28px + (1.05rem * 1.25 * 2) + 4px + (0.82rem * 1.3) + 6px);
}
/* Width-side "small-candidate-count" handling (previously a --mdbo-cols/
   --mdbo-col-w custom-property pair and CSS Grid :nth-child() maths, in
   two separate now-superseded revisions) no longer lives near this
   comment at all - see the single, current explanation above the
   .mdbo-candidategrid.mdbo-ballotgroup flexbox rules earlier in this
   file for the full history and the present, much simpler mechanism.
   Neither --mdbo-cols nor --mdbo-col-w is declared anywhere in this file
   any more. */
