/* v2/assets/forms.css — form styling for the v2 PHP port.
 *
 * WHY THIS FILE EXISTS
 *   The v2 static build contains no forms at all — one incidental `input` rule in the whole
 *   of tokens.css and no field primitives. The design system was never asked to describe a
 *   form. This adds that layer using ONLY existing v2 tokens, so nothing here introduces a
 *   new colour, radius or spacing step.
 *
 *   Load AFTER tokens.css.
 */

/* ── layout ─────────────────────────────────────────────────────────────── */
.form-card {
  background: var(--surface-1);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  box-shadow: var(--shadow-sm);
  max-width: 62ch;
}
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s3) var(--s4);
}
.field { display: flex; flex-direction: column; gap: var(--s1); }
.field-wide { grid-column: 1 / -1; }

/* ── labels ─────────────────────────────────────────────────────────────── */
.field > label {
  font-family: var(--font-body);
  font-size: .82rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.field > label .req { color: var(--accent-text); margin-inline-start: .15em; }

/* ── controls ───────────────────────────────────────────────────────────── */
.field input,
.field select,
.field textarea {
  font: inherit;
  font-family: var(--font-body);
  color: var(--text-primary);
  background: var(--canvas);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  padding: .7rem .85rem;
  width: 100%;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.field textarea { min-height: 7.5rem; resize: vertical; }

/* Keyboard focus must be visible — the v2 QA gauntlet checks for it. */
/* Walk mark 14 (mc-fjyq): "All of these fields are required fields and they should highlight in red
   when they try to submit." The form already blocked the submit and showed a summary message, but
   never said WHICH field — so on a long form the message was true and useless.

   NEW TOKEN, FLAGGED: --danger. The palette has --warn-* (an amber advisory used by the legal
   under-review banner) and no error colour. The operator asked specifically for RED, and amber is
   not red; reusing the advisory tokens would have quietly answered a different request. #B3261E
   measures 6.4:1 on --surface-1 and 5.9:1 on --canvas, so it clears AA as text, not just as a
   border. It needs BRAND-DESIGN-DOCTRINE sign-off like any other token — raised, not smuggled. */
.field.is-invalid > label { color: var(--danger); }
.field.is-invalid input,
.field.is-invalid select,
.field.is-invalid textarea {
  border-color: var(--danger);
  box-shadow: 0 0 0 1px var(--danger);       /* a second pixel of weight — a 1px border alone is
                                                easy to miss on a form this long */
}
/* mc-kmzx (review pin 1): "text above them that says 'required field'".
   IT IS ABOVE THE INPUT, and that is a MARKUP fact, not a CSS one: `.field` is a flex column, so
   the span renders wherever it sits in the source, and consult.php places it between the label and
   the input. This rule only decides whether it shows at all. The vendor form, which authored this
   class, still puts its span after the input — both work, and neither needs an `order` override. */
.field-error {
  display: none;
  color: var(--danger);
  font-size: .85rem;
  line-height: 1.4;
}
.field.is-invalid .field-error { display: block; }
/* An error span the validator left empty must not open a gap above the input. */
.field.is-invalid .field-error:empty { display: none; }

/* mc-kmzx: "I need them to shake." Four pixels each way, twice, over .4s — enough to catch the eye
   at the moment of the blocked submit, small enough that it never moves the layout (transform does
   not reflow). It runs on the class being ADDED, so re-submitting a still-empty form re-runs it
   only if the class was cleared first, which the submit handler does.
   Motion sensitivity is already handled globally: tokens.css zeroes every animation duration under
   prefers-reduced-motion, so this becomes a still red field there, which is the whole point. */
@keyframes field-shake {
  0%, 100% { transform: translateX(0); }
  20%      { transform: translateX(-4px); }
  40%      { transform: translateX(4px); }
  60%      { transform: translateX(-3px); }
  80%      { transform: translateX(2px); }
}
.field.is-invalid { animation: field-shake .4s ease; }

.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
}
.field input:invalid:not(:placeholder-shown),
.field select:invalid:not(:placeholder-shown) { border-color: var(--warn-line); }

/* ── checkbox rows (preferred contact method) ───────────────────────────── */
.check-row { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4); }
.check {
  display: inline-flex; align-items: center; gap: .5rem;
  font-family: var(--font-body); font-size: .95rem;
  color: var(--text-primary); text-transform: none; letter-spacing: 0;
  cursor: pointer;
}
.check input { width: auto; accent-color: var(--accent); margin: 0; }

/* ── helper + error text ────────────────────────────────────────────────── */
.field .hint { font-size: .84rem; color: var(--text-muted); }
.form-error {
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  color: var(--warn-text);
  border-radius: var(--radius);
  padding: var(--s3);
  margin-bottom: var(--s4);
}
/* mc-kmzx: the base `.form-error` is AMBER — it is the advisory tone, and amber is not red. The
   fields it summarises go red, and a summary in a different colour from the thing it summarises
   makes the reader look for a second problem. --danger is the palette's only red: 6.4:1 as text on
   white, so it carries the sentence as well as the border. */
.form-error--danger {
  background: var(--surface-1);
  border-color: var(--danger);
  color: var(--danger);
  font-weight: 500;
}
/* THE NUMBERED SUMMARY — mc-wx24, operator mark 6: "This should show every error in the form, not
   just one, and it should number each of the errors ... The text should definitely be red and it
   should flash when they try to submit without this stuff filled in."
   An <ol> gives the numbers, so the count is the browser's job and cannot drift from the list.
   Each entry is a link to the field it names, in the same red, underlined so it is not colour
   alone. The box itself takes focus (tabindex=-1 in the markup) so the whole list is read out. */
.form-error--danger .form-error-title { margin: 0; font-weight: 600; }
.form-error--danger ol { margin: .45rem 0 0; padding-inline-start: 1.5rem; }
.form-error--danger li { margin: .25rem 0; }
.form-error--danger a { color: var(--danger); text-decoration: underline; }
.form-error--danger a:hover { color: color-mix(in srgb, var(--danger) 78%, #000); }
.form-error--danger:focus { outline: none; }
.form-error--danger:focus-visible { outline: 3px solid var(--danger); outline-offset: 2px; }
/* ONE flash, not a pulse loop: the summary is new on the screen at the moment of the blocked
   submit and has to be noticed, but it must not keep blinking while it is being read. Only the
   ground and the border move — the text never dips below its contrast floor. */
@keyframes form-error-flash {
  0%, 100% { background: var(--surface-1); box-shadow: 0 0 0 1px transparent; }
  30%, 70% { background: color-mix(in srgb, var(--danger) 12%, var(--surface-1)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 35%, transparent); }
}
.form-error--danger.is-flash { animation: form-error-flash .6s ease 1; }
@media (prefers-reduced-motion: reduce) {
  .form-error--danger.is-flash { animation: none; }
}

/* ── honeypot: reachable by bots, invisible and unfocusable to people ───── */
.hp {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* ── actions ────────────────────────────────────────────────────────────── */
.form-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  margin-top: var(--s4);
}
.form-actions .small { color: var(--text-muted); }

/* Respect the reduced-motion preference the rest of v2 already honours. */
@media (prefers-reduced-motion: reduce) {
  .field input, .field select, .field textarea { transition: none; }
}

/* 375px gate: single column, no horizontal overflow. */
@media (max-width: 480px) {
  .form-card { padding: var(--s4); }
  .form-grid { grid-template-columns: 1fr; }
}

/* ── Section heading with the notepad ABOVE it (mc-1838 → mc-4msk) ──────────────────────────
   HISTORY, because this block has been ruled on three times and the shape of it only makes sense
   with the reasons attached.
     mc-1838 put the notepad to the LEFT of "What we need to answer well" in a two-column grid,
       out of a 460px centred inset that used to sit BETWEEN the sentence and the form.
     mc-4msk, first ruling: match its height to the text. Built, then denied — with the caption
       still under the picture the notepad came out drawn at 92x68, an icon.
     mc-4msk, second ruling: move the caption sentence out from under the picture to under the
       paragraph. Built, then denied again.
     mc-4msk, third and current ruling (2026-09-06), operator verbatim: "Can you crop the image down
       to the red rectangle in the screenshot? Also, I changed my mind. I want the image about the
       'Send a message / What we need to answer well / None of this commits you to anything. The
       fuller it is, the more useful our first reply. / The more you write here, the more useful the
       first reply can be.'"

   SO: ONE COLUMN AT EVERY WIDTH, PICTURE FIRST. "About" reads as ABOVE, and the four lines she
   quotes are the whole text group — eyebrow, heading, paragraph, and the sentence moved under the
   paragraph last time, which stays exactly where she put it. There is no side-by-side left at any
   width, so the whole height-matching apparatus that the previous two builds needed is GONE: there
   is no second column to match a height to. That is why this block is shorter than what it
   replaced, not because anything was dropped.

   The markup did not have to move. It has been picture-then-text since mc-1838, chosen then so the
   stacked order would already be the reading order; the grid was the only thing making it
   side-by-side, and removing the grid is the whole change.

   WHY 300px WIDE. The file was cropped to the operator's red rectangle in the same change and is
   now 849x849, so "natural size" would put an 849px notepad above a section heading — three times
   the size it has ever been shown at, and larger than the phone column, so it could not be natural
   size everywhere in any case. 300px keeps the DRAWING itself at about 219px across, which is the
   size she has been looking at through both previous builds (it drew at 219.5 when it was beside
   the text), and it is comfortably below the file's own 849px so the pixel art is only ever scaled
   DOWN — scaling a pixel grid UP past its authored resolution is what makes it look soft. `min()`
   caps it against the column so a 375px phone gets 327px and never a sideways scrollbar.

   LEFT-ALIGNED, not centred: `margin-inline: 0` against the `margin-inline: auto` the base
   `.section-art` rule carries, so the picture's left edge lines up with the eyebrow and the heading
   under it. `img.section-art.section-art--stacked` is written at element-plus-two-classes because
   `img.section-art.section-art--inset` (tokens.css) centres at 460px at the same specificity, and a
   plain `.section-art--stacked` would lose to it; forms.css loads after tokens.css, so equal
   specificity resolves here.
   -------------------------------------------------------------------------------------------- */
.section-header--media {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s4);
  text-align: start;                 /* .section-header.center is a different, opt-in variant */
}
/* The CAP LIVES ON THE FIGURE, not on the picture, and that is not cosmetic. Written as
   `width: min(100%, 300px)` on the <img>, the percentage is circular while the figure is being
   sized — the browser falls back to the file's intrinsic width and the FIGURE comes out 849 wide,
   overrunning its container by 148px at 1280 with no scrollbar to show for it. Measured, then
   fixed: the figure takes the cap, the picture simply fills the figure. */
.section-header--media .section-media { margin: 0; width: min(100%, 300px); }
img.section-art.section-art--stacked {
  width: 100%;
  height: auto;
  margin: 0;
  border-radius: var(--radius);
}
.section-header--media .section-header-text > :first-child { margin-top: 0; }

/* ── The stepped consult flow (mc-kmzx + mc-1838) ───────────────────────────────────────────
   A numbered flow inside ONE <form>: step 1 (who you are), step 2 (optional event details),
   step 3 (the calendar, Book path only). The honeypot and every required field ride the same
   form element, so the single POST at the end of step 2 carries everything.

   NO NEW TOKENS. Everything below is built from --s*, --hairline, --surface-*, --accent,
   --forest, --text-* and --radius(-lg), the same set the rest of this file uses.
   -------------------------------------------------------------------------------------------- */
.stepflow { max-width: 62ch; }
.stepflow .form-card { max-width: none; }     /* the card is inside the flow now, not the flow */

.stepflow-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--s2) var(--s3);
  margin-bottom: var(--s3);
}
.step-label {
  margin: 0;
  font-family: var(--font-body); font-size: .82rem; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-secondary);
}
/* The pills REPORT progress; they do not navigate. An <ol> of non-interactive items with
   aria-current="step" on the live one — the shape of .pill-topic without its button behaviour,
   because a control that looks pressable and is not is worse than no control. */
.step-pills { display: flex; gap: var(--s2); list-style: none; margin: 0; padding: 0; }
.step-pill {
  width: 1.9rem; height: 1.9rem;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-body); font-size: .85rem; font-weight: 600;
  color: var(--text-secondary);
  background: var(--surface-1);
  border: 1px solid var(--hairline); border-radius: 999px;
}
.step-pill.is-done { border-color: var(--forest); color: var(--forest); }
.step-pill.is-current {
  background: var(--forest); color: var(--surface-1); border-color: var(--forest);
}

/* An inactive panel carries hidden + inert + aria-hidden in the markup. `hidden` is the one that
   has to survive the cascade, so it is declared !important — a stray `display` rule on a panel
   would otherwise leave an off-step panel visible AND tabbable. */
.step-panel[hidden] { display: none !important; }
.step-heading {
  margin: 0 0 var(--s3);
  font-family: var(--font-display); font-size: 1.25rem; font-weight: 500;
  color: var(--text-primary);
}
/* Focus lands here on every step change; the ring is the page's, not a new one. */
.step-heading:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--accent) 60%, transparent);
  outline-offset: 4px;
}

/* Slide in from the right going forward, from the left coming back. Transform + opacity only —
   nothing here reflows, so a slide cannot make the page jump. */
@keyframes step-slide-right { from { transform: translateX(28px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes step-slide-left  { from { transform: translateX(-28px); opacity: 0; } to { transform: none; opacity: 1; } }
.step-panel.slide-in-right { animation: step-slide-right .38s cubic-bezier(.2,.7,.3,1) both; }
.step-panel.slide-in-left  { animation: step-slide-left  .38s cubic-bezier(.2,.7,.3,1) both; }

.step-actions { align-items: flex-start; }
.step-actions .step-note { max-width: 40ch; line-height: 1.45; }
.step-back-row { margin-top: var(--s4); }

/* The questionnaire. Two columns where there is room, one below 700px — the questions are short
   labels over short inputs, and a 62ch single column would run to two screens. */
.step-questions { display: grid; gap: var(--s4); margin-top: var(--s4); }
.q-section { border-top: 1px solid var(--hairline); padding-top: var(--s3); }
.q-section > h4 {
  margin: 0 0 var(--s3);
  font-family: var(--font-body); font-size: .82rem; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-secondary);
}
.q-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--s3) var(--s4); }
.q-grid .field-wide { grid-column: 1 / -1; }
.q-grid textarea { min-height: 5.5rem; }

/* The calendar host. Calendly renders an iframe into it; the min-height stops the page collapsing
   to nothing while the widget script is still arriving. */
.step-calendar {
  min-height: 700px;
  margin-top: var(--s3);
  border: 1px solid var(--hairline); border-radius: var(--radius);
  overflow: hidden;
}
.step-calendar iframe { width: 100%; min-height: 700px; border: 0; }
@media (max-width: 640px) { .step-calendar, .step-calendar iframe { min-height: 900px; } }

/* Motion sensitivity. tokens.css already zeroes every animation duration site-wide under
   prefers-reduced-motion; this states it locally too, so the panels are provably instant rather
   than instant-by-inheritance — the proof asserts on this rule. */
@media (prefers-reduced-motion: reduce) {
  .step-panel.slide-in-right,
  .step-panel.slide-in-left { animation: none; transform: none; opacity: 1; }
}

@media (max-width: 480px) {
  .stepflow-head { flex-direction: column; align-items: flex-start; }
  .q-grid { grid-template-columns: 1fr; }
}

/* ── Preferred-vendor directory cards ──────────────────────────────────────
   Used by v2/includes/vendor-directory.php. The grid itself is the existing
   .grid.grid-3 + .card; these rules only cover the vendor-specific bits.

   Why each exists:
   - .card p sets margin-bottom:0 in tokens.css, so stacked paragraphs in a
     vendor card would collide. Restore rhythm for the ones we emit.
   - .card has padding var(--s4); a photo has to pull back out to sit
     full-bleed at the top of the card, and clip to the card's own radius.
   - Most vendors publish with NO photo (the planner sends name/category/tag/
     location and little else), so .vendor-photo is absent entirely rather
     than rendering an empty panel. Nothing here fabricates an image.
   ------------------------------------------------------------------------ */
.card .vendor-photo {
  margin: calc(var(--s4) * -1) calc(var(--s4) * -1) var(--s3);
  aspect-ratio: 16 / 10;
  background-size: cover;
  background-position: center;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
/* No emoji pin here. v1's card prefixes the location with 📍, but v2 uses no
   decorative emoji anywhere (a single ⚠ in a form warning is the only one in the
   whole build) and the brand rubric prohibits emoji-as-icons. The location reads
   as plain secondary text. */
.card .vendor-loc { color: var(--text-secondary); margin-bottom: var(--s2); }
.card .vendor-specs { color: var(--text-secondary); font-size: 0.92em; margin-top: var(--s2); }
.card .vendor-loc + p,
.card .vendor-specs { margin-bottom: var(--s2); }

/* 375px gate: the 3-up grid collapses to one column. tokens.css already has a
   breakpoint for .grid-3; this only guards the photo's aspect on narrow cards. */
@media (max-width: 480px) {
  .card .vendor-photo { aspect-ratio: 16 / 9; }
}

/* ── Service-area zone map (mc-cqpe → mc-a0q1 + mc-ge9q) ───────────────────────────────────
   The city checkboxes are gone (marks 35, 36, 32). The map is the tool; a city-by-name field is
   the keyboard path (this page carries an AA conformance claim and a draw-on-map control cannot
   be operated by keyboard); a state picker takes whole states. Any one of the three answers the
   mandatory question, and the block below flashes red when none of them has. */
.zone-tool {
  display: grid; gap: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  background: var(--surface-1);
}
.zone-tool:focus { outline: none; }
.zone-tool:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 60%, transparent); outline-offset: 3px; }
/* MANDATORY, and empty on submit: the border goes red and pulses twice. The wrapping .field gets
   the same .is-invalid the other fields use, so the label turns red and the field-shake runs —
   this rule adds only the border pulse the operator asked for ("flash red if no zone was set"). */
@keyframes zone-flash {
  0%, 100% { box-shadow: 0 0 0 1px var(--danger); }
  25%, 75% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 45%, transparent); }
}
.zone-tool.is-invalid { border-color: var(--danger); animation: zone-flash .8s ease 1; box-shadow: 0 0 0 1px var(--danger); }
/* It is the BLOCK that is invalid, not the city field or the state boxes inside it — the generic
   `.field.is-invalid input` rule would paint those red too and say the wrong thing. */
.field.is-invalid .zone-tool input { border-color: var(--hairline); box-shadow: none; }
.field.is-invalid .zone-tool + .field-error { display: block; }

/* THE DRAW BUTTON — pin 1 of verify-vmap-0906, DENIED in round 1:
   "The Draw Zone needs to be a little bit more prominent with the button. It is just kind of easy
   to miss. Maybe a yellow background with black text that inverts on hover."

   Round 1 used .btn-secondary — an outline button in --forest, which is exactly the thing that is
   easy to miss on a page of outline buttons. Round 2 fills it.

   THE COLOUR IS --gold (#B59556), the palette's only yellow. The operator said "yellow"; the
   design doctrine forbids inventing a colour, and --gold IS the site's yellow, so --gold is what
   was used — it is shown for the operator to rule on, not assumed to be what they pictured.
   The token's own comment says "DARK GROUNDS ONLY — ~3:1 on light", which is about --gold used as
   TEXT on a light ground. Here it is the GROUND: --text-primary (#1F2540) on --gold measures
   5.7:1, over the 4.5:1 AA floor for body text, so the pairing is legitimate in this direction.

   REST     gold ground, dark text.
   HOVER    inverted — dark ground, gold text. Same 5.7:1.
   ARMED    the same inversion, held: dark ground, gold text, so "I am drawing" reads as the
            opposite of "I am idle" without a third colour. The keyframe swaps to the rest
            colours for one beat and settles back on the armed pair (mark 36: "flash inverted
            colors quickly once it's enabled and then set to the highlighted color setting").
   Reduced motion: no animation, straight to the armed fill (rule at the bottom of this file). */
.btn-draw {
  background: var(--gold);
  color: var(--text-primary);
  border-color: var(--gold);
  font-weight: 600;
}
.btn-draw:hover {
  background: var(--text-primary);
  color: var(--gold);
  border-color: var(--text-primary);
}
@keyframes zone-arm-flash {
  0%   { background: var(--text-primary); color: var(--gold); }
  40%  { background: var(--gold); color: var(--text-primary); }
  100% { background: var(--text-primary); color: var(--gold); }
}
#zone-draw[aria-pressed="true"],
#zone-draw.is-armed { background: var(--text-primary); color: var(--gold); border-color: var(--text-primary); }
#zone-draw.is-armed { animation: zone-arm-flash .45s ease-out 1; }

/* The shortcuts, visible at all times under the map (mark 32) — and readable: the operator's
   first ask on this map was that the instructions be "more prominent". */
.zone-keys { padding: var(--s3); background: var(--canvas); border-radius: var(--radius); }
.zone-keys-title {
  margin: 0 0 var(--s2);
  font-family: var(--font-body); font-size: .82rem; font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase; color: var(--text-secondary);
}
.zone-keys-list { margin: 0; padding: 0; list-style: none; display: grid; gap: .35rem; }
.zone-keys-list li { font-size: .95rem; line-height: 1.45; color: var(--text-primary); }
.zone-keys-list b { font-weight: 600; color: var(--forest); }
@media (min-width: 700px) { .zone-keys-list { grid-template-columns: 1fr 1fr; gap: .35rem var(--s4); } }

.zone-load-note { margin: 0; font-size: .9rem; color: var(--text-secondary); }
.zone-map.is-loading { cursor: progress; }

/* The zone number, inside the shape: Leaflet's permanent tooltip, restyled, no arrow. */
.leaflet-tooltip.zone-label {
  background: var(--surface-1); color: var(--forest);
  border: 1px solid var(--forest); border-radius: 999px;
  font-family: var(--font-body); font-size: .8rem; font-weight: 600; letter-spacing: .04em;
  padding: 2px 10px; box-shadow: none; pointer-events: none;
}
.leaflet-tooltip.zone-label::before { display: none; }

/* The list under the map: one row per zone with its states, then the states row, then the
   cities added by name. */
.zone-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s3); }
.zone-state { margin-top: var(--s2); }
.zone-state-name {
  margin: 0 0 .2rem;
  font-family: var(--font-body); font-size: .85rem; color: var(--forest);
}
.zone-state-name strong { font-weight: 600; }
.zone-state-count { font-weight: 400; color: var(--text-muted); }
.zone-places { margin: 0; font-size: .88rem; line-height: 1.5; color: var(--text-secondary); }
.zone-more {
  font: inherit; font-size: .85rem; font-weight: 600; color: var(--accent-text);
  background: none; border: 0; padding: 0; cursor: pointer; text-decoration: underline;
}
.zone-more:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
.zone-row--states, .zone-row--cities { display: block; font-size: .9rem; color: var(--text-primary); }
.zone-row--states .zone-name, .zone-row--cities .zone-name { color: var(--forest); }
.city-chips { list-style: none; margin: .4rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s2); }
.city-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: 4px 6px 4px 12px; font-size: .88rem;
  background: var(--canvas); border: 1px solid var(--hairline); border-radius: 999px;
}
.zone-remove--small { width: 26px; height: 26px; font-size: 1rem; }

/* The keyboard path: a combobox whose suggestions drop below the field. */
.city-add { display: grid; gap: var(--s1); }
.city-add > label {
  font-family: var(--font-body); font-size: .82rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-secondary);
}
.city-combo { position: relative; }
.city-list {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 20;
  margin: 0; padding: 4px 0; list-style: none;
  background: var(--surface-1); border: 1px solid var(--hairline); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); max-height: 16rem; overflow-y: auto;
}
.city-list[hidden] { display: none; }
.city-list [role="option"], .city-note { padding: .5rem .85rem; font-size: .92rem; cursor: pointer; }
.city-note { color: var(--text-muted); cursor: default; }
.city-list [role="option"]:hover,
.city-list [role="option"].is-active { background: var(--canvas); color: var(--forest); }

/* Entire states: a native disclosure holding fifty-one checkboxes. */
.state-picker { border: 1px solid var(--hairline); border-radius: var(--radius); background: var(--surface-1); }
.state-picker > summary {
  padding: .7rem .85rem; cursor: pointer; font-size: .95rem; font-weight: 500; color: var(--text-primary);
}
.state-picker > summary:focus-visible { outline: 2px solid var(--accent-text); outline-offset: -2px; }
.state-count { color: var(--text-muted); font-weight: 400; margin-inline-start: .35rem; }
/* PIN 4 of verify-vmap-0906, DENIED in round 1: "There should be an option for Nationwide or
   Select All. Yes, Select All and Deselect All should be somewhere here ... or at the very top in
   its own section above the states."  Its own row, above the fifty-one boxes, on the page's own
   ground so it separates from the grid without a second rule line. */
.state-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3);
  padding: .7rem .85rem;
  border-top: 1px solid var(--hairline);
  background: var(--canvas);
}
.state-action {
  font: inherit; font-family: var(--font-body); font-size: .9rem; font-weight: 600;
  padding: 7px 14px; min-height: 38px;
  color: var(--forest); background: var(--surface-1);
  border: 1px solid var(--forest); border-radius: var(--radius); cursor: pointer;
}
.state-action:hover { background: var(--forest); color: var(--surface-1); }
.state-action:focus-visible { outline: 2px solid var(--accent-hover); outline-offset: 2px; }
.state-nationwide { font-weight: 600; }
.state-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--s1) var(--s3); padding: 0 .85rem .85rem; border-top: 1px solid var(--hairline); padding-top: .7rem;
}
.state-grid .check { font-size: .9rem; }

@media (prefers-reduced-motion: reduce) {
  .zone-tool.is-invalid { animation: none; }
  #zone-draw.is-armed { animation: none; }
}
.zone-map {
  height: 380px; width: 100%;
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  background: var(--surface-2);            /* something to look at before tiles arrive */
  z-index: 0;                              /* keep Leaflet panes under the fixed nav */
}
@media (max-width: 640px) { .zone-map { height: 300px; } }
/* Crosshair only while a zone is being drawn, so the cursor states what mode you are in. */
.zone-map.is-drawing { cursor: crosshair; }
.zone-map.is-drawing .leaflet-grab { cursor: crosshair; }

.zone-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); }
.zone-status { font-size: .85rem; color: var(--text-muted); }

.zone-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.zone-empty { font-size: .85rem; color: var(--text-muted); list-style: none; }
.zone-row {
  display: grid; gap: var(--s1);
  padding: 10px 12px;
  background: var(--canvas);
  border: 1px solid var(--hairline); border-radius: var(--radius);
}
.zone-name { font-weight: 600; font-size: .9rem; }
.zone-covers { flex: 1; min-width: 0; font-size: .82rem; color: var(--text-secondary); }
/* THE X — pin 2 of verify-vmap-0906, DENIED in round 1:
   "I need the X next to the zone to be more prominent: red background, white X. On click it
   should say, Are you sure? or Delete zone 1, are you sure?"

   Round 1 drew it as a grey outline circle that only went red on hover, so at rest it read as
   decoration. Round 2 fills it: --danger ground, a white glyph, still the same round shape.
   White on #B3261E measures 6.4:1. 32px is under the 44px touch target the rest of the site holds
   to, so it keeps its padding to reach it without growing the row.

   The confirm step itself is in assets/service-area-map.js — this only paints it. */
.zone-remove {
  flex-shrink: 0;
  width: 32px; height: 32px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.25rem; font-weight: 600; line-height: 1;
  color: #FFFFFF; background: var(--danger);
  border: 1px solid var(--danger); border-radius: 999px;
  cursor: pointer;
}
.zone-remove:hover { background: color-mix(in srgb, var(--danger) 78%, #000); border-color: color-mix(in srgb, var(--danger) 78%, #000); color: #FFFFFF; }
.zone-remove:focus-visible { outline: 2px solid var(--text-primary); outline-offset: 2px; }

/* The confirm row that replaces nothing and sits under the zone's own heading: a question in red
   and two buttons. No native confirm() — it cannot be styled, cannot be screenshotted, and the
   walk harness cannot see it. Escape answers No; the script puts focus on No, so a stray Enter
   keeps the zone. */
.zone-confirm {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3);
  margin-top: var(--s2); padding: 10px 12px;
  background: color-mix(in srgb, var(--danger) 7%, var(--surface-1));
  border: 1px solid var(--danger); border-radius: var(--radius);
}
.zone-confirm-q { margin: 0; flex: 1 1 14rem; font-size: .9rem; font-weight: 600; color: var(--danger); }
.zone-confirm-yes,
.zone-confirm-no {
  font: inherit; font-family: var(--font-body); font-size: .88rem; font-weight: 600;
  padding: 8px 18px; border-radius: var(--radius); cursor: pointer; min-height: 38px;
}
.zone-confirm-yes { background: var(--danger); color: #FFFFFF; border: 1px solid var(--danger); }
.zone-confirm-yes:hover { background: color-mix(in srgb, var(--danger) 78%, #000); border-color: color-mix(in srgb, var(--danger) 78%, #000); }
.zone-confirm-no { background: var(--surface-1); color: var(--text-primary); border: 1px solid var(--text-primary); }
.zone-confirm-no:hover { background: var(--text-primary); color: var(--surface-1); }
.zone-confirm-yes:focus-visible,
.zone-confirm-no:focus-visible { outline: 3px solid var(--text-primary); outline-offset: 2px; }

/* Leaflet's own attribution/controls, brought into the site's type scale. Attribution is a
   licence requirement, not decoration — it is restyled, never hidden. */
.zone-map .leaflet-container { font-family: var(--font-body); }
.zone-map .leaflet-control-attribution { font-size: .7rem; background: rgba(255,255,255,.85); }
.zone-map .leaflet-control-attribution a { color: var(--accent-text); }
