/* ══════════════════════════════════════════════════════════════════════
   Classical design system — ported subset for Phase 2.5 (base templates &
   static assets). Source of truth for the full system is
   mockups/design_handoff_weekly_meal_planner/styles.css - this file is a
   deliberately curated subset of it, not a full copy.

   Ported here: every design token (:root custom properties), base
   typography (headings/body/links/rules), buttons, form input/label
   primitives, header/nav styling, and (Phase 3.1) the card/tag/elevation
   components - everything the shared base template and today's built
   pages (login/register/home/family, recipe library) actually use.

   Phase 3.1 addition: .card / .card-kicker / .card-title / .card-meta,
   .tag / .tag-accent / .tag-accent-2 / .tag-neutral / .tag-outline, and
   .elev-sm / .elev-md / .elev-lg, ported verbatim from the mockup's
   styles.css for the recipe library's card grid. Also added
   .chip-checkbox, which is NOT a literal mockup class - the mockup's own
   sidebar filter chips (`.tag.chip`, defined inline in the .dc.html file
   rather than styles.css) are onClick-driven spans that toggle an
   `active` class via JS. Step 3.1's brief is a plain GET-form filter UI
   with no JS, so .chip-checkbox reproduces the same visual (outline chip,
   accent-tinted when active) using a hidden checkbox + `:has(input:checked)`
   instead - same look, no script. Recipe-library-specific layout (sidebar
   width, grid, photo placeholder) lives in static/css/recipes.css instead
   of here since it's one page's layout, not a shared component - see that
   file's header comment.

   Phase 3.2 addition: .plate (recipe photo mat, ported verbatim from the
   mockup's styles.css) and .no-print (mirrors the mockup's own print
   utility class - toolbar/nav chrome hidden on paper) for the recipe
   detail/print view.

   Phase 4.1 addition: .radio / .seg / .seg-opt, ported verbatim from the
   mockup's styles.css. The mockup itself only used these for its
   Viewer/Editor role-toggle demo control (not present in the real app -
   role comes from auth, see accounts/permissions.py) - here they're
   repurposed for the add/edit recipe form's single-select tag facets
   (meal_type/cuisine/season/category), same "reuse an existing hidden
   input-driven component instead of inventing a new one" approach as
   Phase 3.1's .chip-checkbox for the library's filter chips. Only .seg/
   .seg-opt are actually used by that form (radio inputs wrapped in
   .seg-opt spans, grouped in a .seg container); .radio is ported
   alongside since the mockup documents the pair together and it's a
   trivial single extra component to keep available for the next screen
   that needs a standalone (non-segmented) radio control - see
   recipes.css for where .seg is actually used.

   Phase 4.3 addition: .dialog / .dialog-title / .dialog-body /
   .dialog-actions, ported from the mockup's styles.css for the recipe
   delete-confirm page (see below) - .dialog-backdrop is NOT ported since
   this app renders the confirm as its own page rather than a JS-toggled
   overlay (Phase 6's assign-recipe modal is the more likely place that
   one ends up mattering, if it ever needs a true overlay).

   Phase 11.8 addition: .filter-chip-group (the flex-wrap row a group of
   .chip-checkbox chips sits in) moved here from recipes.css, which is
   where it originally lived as "recipe-library-specific layout" (see the
   Phase 3.1 paragraph above) - it stopped being page-specific once the
   weekly planner's assign-dialog filter drawer (planner/templates/
   planner/week.html, static/css/planner.css) needed the exact same row
   layout for its own chips and that page never loads recipes.css. Code
   review finding: the class was reused as-is in planner.css's own
   comments/markup without checking it was actually reachable from that
   page's stylesheet, so the drawer's chips silently rendered with no
   layout at all - real found-in-review bug, not a hypothetical.

   Phase 6 addition: .btn-icon, ported verbatim for the weekly planner's
   prev/next week buttons. That screen's assign-recipe modal did NOT end up
   needing .dialog-backdrop after all (see the Phase 4.3 note above): it's a
   native <dialog>, whose ::backdrop pseudo-element replaces the mockup's
   backdrop div - static/css/planner.css styles it there.

   Still NOT ported (left for the phase that builds the screen that needs
   it):
     - .table (not used by any of the mockup's 6 screens - nothing to
       port yet; add it when a screen actually needs a tabular layout)
   ══════════════════════════════════════════════════════════════════════ */

@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600&family=Lora:wght@400;600&display=swap');

:root {
  --color-bg: #f3f2f2;
  --color-surface: #eae9e9;
  --color-text: #201f1d;
  --color-accent: #b68235;
  --color-accent-2: #ac803e;
  --color-divider: color-mix(in srgb, #201f1d 16%, transparent);

  /* Tonal ramps - generated in OKLCH on one shared lightness scale, so the
     same step of any role matches the others in visual value. */
  --color-neutral-100: #f8f4f4;
  --color-neutral-200: #eae7e7;
  --color-neutral-300: #d7d3d3;
  --color-neutral-400: #bab6b6;
  --color-neutral-500: #9b9797;
  --color-neutral-600: #7d7979;
  --color-neutral-700: #605d5d;
  --color-neutral-800: #444141;
  --color-neutral-900: #2d2b2b;

  --color-accent-100: #fff3e4;
  --color-accent-200: #ffe3bf;
  --color-accent-300: #facb8d;
  --color-accent-400: #e1ad66;
  --color-accent-500: #c28d41;
  --color-accent-600: #a06f24;
  --color-accent-700: #7d5411;
  --color-accent-800: #5a3b0a;
  --color-accent-900: #3a270d;

  --color-accent-2-100: #fff3e4;
  --color-accent-2-200: #ffe3be;
  --color-accent-2-300: #f5cd96;
  --color-accent-2-400: #dbaf70;
  --color-accent-2-500: #bc8f4e;
  --color-accent-2-600: #9b7232;
  --color-accent-2-700: #79561f;
  --color-accent-2-800: #573d14;
  --color-accent-2-900: #382810;

  --font-heading: "Cormorant Garamond", system-ui, sans-serif;
  --font-heading-weight: 600;
  --font-body: "Lora", system-ui, sans-serif;

  --space-1: 4.6px;
  --space-2: 9.2px;
  --space-3: 13.8px;
  --space-4: 18.4px;
  /* --space-5 is missing from the mockup's own styles.css token export
     (only 1/2/3/4/6/8 are defined there) despite var(--space-5) being
     used 6 times in Weekly Meal Planner.dc.html's inline styles - the
     library/dashboard card grid gaps and the recipe detail screen's
     margin-top under the photo among them. Every other defined step
     is exactly 4.6px * N (1=4.6, 2=9.2, 3=13.8, 4=18.4, 6=27.6, 8=36.8),
     so 23px fills the gap consistent with that scale rather than
     silently falling back to `gap: normal` / no gap wherever the mockup
     markup references it (Phase 3.2 finding - see recipes.css/detail.html).
   */
  --space-5: 23px;
  --space-6: 27.6px;
  --space-8: 36.8px;

  --radius-sm: 2px;
  --radius-md: 4px;
  --radius-lg: 7px;

  --shadow-sm: 0 1px 2px color-mix(in srgb, #2d2b2b 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2d2b2b 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2d2b2b 22%, transparent);
}

/* — base typography & elements — */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  font-weight: 400;
}
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  line-height: 1.12; letter-spacing: -0.015em; margin: 0 0 var(--space-2);
}
h1 { font-size: 42px; }
h2 { font-size: 32px; }
h3 { font-size: 25px; }
h4 { font-size: 20px; }
h5 { font-size: 16px; }
h6 { font-size: 14px; letter-spacing: 0.08em; text-transform: uppercase; }
p { margin: 0 0 var(--space-3); }
a { color: var(--color-accent); text-underline-offset: 3px; }
img { display: block; max-width: 100%; }
figure { margin: 0; }
figcaption {
  font-size: 11px; margin-top: var(--space-1);
  /* 78%, not 65% - see .text-muted below for why. */
  color: color-mix(in srgb, var(--color-text) 78%, transparent);
}
/* The mockup's own token here (55%) computes to ~3.6:1 against
   --color-bg / --color-surface - below WCAG AA's 4.5:1 minimum for
   normal-size text. First bumped to 65% (~4.92:1 / ~4.75:1 respectively),
   which cleared the 4.5:1 legal floor but was still directly reported as
   too light in practice (the "N Users" count on admin_users.html,
   specifically) - a control sitting exactly at a documented floor and
   still getting flagged means the floor itself was too low, not that
   this one instance was a special case (ui-design-principles.md §1).
   Jumped (not nudged) to 78%, which measures ~7.5:1 / ~7.14:1 - solidly
   past the revised 5.5:1 target with real margin, rather than another
   marginal bump that would risk the same complaint a third time.
   figcaption, .field .helptext, .form-gate-reason and .card-meta all
   share this same value for the same "muted secondary text a user is
   meant to read as information" role, so all four are bumped identically
   - see each below. */
.text-muted { color: color-mix(in srgb, var(--color-text) 78%, transparent); }
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
::selection { background: color-mix(in srgb, var(--color-accent) 30%, transparent); }

/* — rules — */
.hr {
  height: 1px; border: 0; margin: var(--space-4) 0;
  background: var(--color-divider);
}

/* — buttons — */
/* `font-weight: 700` and `font-size: 15px` below are a deliberate
   deviation from --font-heading-weight (600) / 14px used by every other
   heading-styled element (h1-h6, .card-title, .dialog-title, .nav-brand)
   - not drift. Per direct user feedback on the login/register buttons'
   legibility ("need to be around 1px bigger and possibly with thicker
   font"), buttons intentionally render bolder/larger than the rest of
   the heading system. Keep this as-is; don't "fix" it back to the token. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  cursor: pointer; text-decoration: none;
  /* Found live 2026-08-12: a .btn inside a flex row that's short on room
     (the nav login form's "Log In" button, squeezed between two inputs
     once the nav had enough other content next to it) shrank below its
     own label's natural width - flex-shrink defaults to 1, letting any
     flex item compress past its content size unless told not to - and
     with nothing stopping the label text from wrapping once that
     happened, "Log In" split across two lines inside the button. A
     button's own label should never wrap or get compressed to fit; if a
     row is too tight, something else in that row should give first (a
     sibling input shrinking, or the row wrapping as this app's own .nav
     already does) - not the button's text itself. Applies to every .btn
     everywhere, not just the nav login form - any other flex-row button
     had the same latent risk. */
  white-space: nowrap; flex-shrink: 0;
  font-family: var(--font-heading); font-weight: 700;
  /* 17px, not 16px - the previous sweep bumped this to exactly
     ui-design-principles.md §1's 16px floor, but that still read as too
     small live (2026-08-12, "Log Out" specifically - .btn-ghost). Per
     this project's own established rule for exactly this situation
     (feedback-ui-text-brightness-size-defaults: when something's flagged
     as still too small after already being bumped, jump clearly past the
     bare floor rather than re-confirming the floor is technically met) -
     the floor is the documented minimum other controls must clear, not a
     ceiling this one should sit exactly on when a human's told you twice
     it still reads small. */
  font-size: 17px; line-height: 1.2; color: var(--color-text);
  background: transparent; border: 1px solid transparent;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
  border-radius: var(--radius-md);
}
/* A disabled button gets its own flat neutral treatment rather than a
   plain opacity fade of whatever it looked like enabled. Per
   ui-design-principles.md §2, the enabled state needs to read as clearly
   actionable and the disabled one as clearly inert - fading .btn-primary's
   accent gold to 45% just reads as "the same gold button, dimmed". Grey
   text/border with no accent anywhere is unambiguous, and
   --color-neutral-700 still measures ~5.3:1 on this tint so the label stays
   readable. `.btn:disabled` is (0,2,0) and beats `.btn-primary`'s (0,1,0)
   on specificity, so this wins regardless of source order. */
.btn:disabled {
  opacity: 1; cursor: not-allowed;
  color: var(--color-neutral-700);
  border-color: var(--color-divider);
  background: color-mix(in srgb, var(--color-text) 5%, transparent);
}
/* `.btn { display: inline-flex }` above is an author rule, so it beats the
   UA stylesheet's `[hidden] { display: none }` - without this, setting
   `button.hidden = true` from JS changes the attribute and nothing else.
   Phase 7 addition: the shopping list's servings slider ships with a
   no-JS "Apply" submit button that shopping.js hides once it takes over.
   Same class of clash planner.css documents for the assign dialog and its
   filtered rows; fixed once here rather than per-screen because it
   applies to every .btn in the app. */
.btn[hidden] { display: none; }
/* The hover/active rules are all guarded with :not(:disabled). CSS :hover
   still matches a disabled button (it's a rendering state, not an event),
   and these rules are the same (0,2,0) specificity as `.btn:disabled` above
   but come later in the file - so without the guard, hovering a disabled
   primary button would paint the accent tint back over the grey
   disabled treatment. `:not(:disabled)` rather than `:enabled` because
   .btn is also used on <a> elements (e.g. the recipe form's Cancel link),
   which `:enabled` doesn't match. */
.btn-primary { color: var(--color-accent); border-color: var(--color-accent); }
.btn-primary:not(:disabled):hover { background: color-mix(in srgb, var(--color-accent) 12%, transparent); }
.btn-primary:not(:disabled):active { background: color-mix(in srgb, var(--color-accent) 22%, transparent); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:not(:disabled):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:not(:disabled):active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-ghost { color: var(--color-accent); padding-inline: var(--space-1); }
.btn-ghost:not(:disabled):hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.btn-ghost:not(:disabled):active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
.btn-block { width: 100%; margin-top: var(--space-2); }
/* Phase 6 addition, ported verbatim from the mockup's styles.css - the
   planner's prev/next week buttons are the first square icon-sized
   buttons in the app. */
.btn-icon { width: 36px; height: 36px; padding: 0; }

/* — forms —
   Today's pages render Django forms with a manual {% for field in form %}
   loop (see accounts/templates/*) wrapping each field in a `.field` div,
   rather than `.input`/`.field` classes on the widgets themselves - so
   these rules target the raw input/textarea/select elements directly and
   need no changes to accounts/forms.py widget attrs. */
.field { margin-bottom: var(--space-3); }
/* Field labels render at body size (15px), semibold, at full --color-text
   brightness - NOT as muted secondary text. Previously 12px / weight 400 /
   70% opacity, which measured 5.77:1 against --color-bg: technically over
   WCAG AA's 4.5:1, but visibly small and washed out, and directly reported
   as "too small and too light" by the user on the login page. Full
   brightness measures ~14.7:1. Per planning/ui-design-principles.md §1 this
   is a deliberate jump rather than a nudge - a small muted form label is
   called out there as a recurring failure spot, and small relative
   adjustments to an already-dim value are imperceptible in practice.
   Three other rules elsewhere had copied this same 12px/70% treatment
   (.recipe-form-facet > label, .planner-assign-search-label,
   .shopping-scale label) and were swept to match - see each file. */
.field > label {
  display: block; font-size: 15px; font-weight: 600; margin-bottom: 6px;
  color: var(--color-text);
}
/* min-height 44px / font-size 16px, not the previous 36px / 14px, per
   ui-design-principles.md §3: 14px is below the 16px threshold at which
   iOS Safari auto-zooms the page on focus, and a 36px control is under the
   ~40-44px minimum touch target. Both applied to every text-entry field in
   the app, so both are fixed here on the shared rule rather than per page.
   Padding is 8px (not 10px) vertical so the 16px text's own line box plus
   padding lands just under min-height, letting min-height set the final
   height rather than the two fighting. */
input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="search"], input[type="file"], textarea, select {
  width: 100%; min-height: 44px; padding: 8px 12px; font: inherit;
  font-size: 16px; color: var(--color-text); caret-color: var(--color-accent);
  background: transparent;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
/* input[type="file"] gets the same box as every other field above, but
   its native "Choose File" button portion doesn't respect width: 100%
   the way a plain text input does - left unset, its intrinsic content
   width can push past the field's own border, which is what caused a
   1px horizontal-overflow bug on the add/edit recipe form's Photo field
   at 768px viewport width (#id_photo's right edge sitting at 769.4px
   against a 768px viewport). max-width: 100% plus the box-sizing:
   border-box every element already gets (top of this file) keeps the
   whole control - including the button - inside the field's own bounds. */
input[type="file"] { max-width: 100%; }

/* Reliable autofill detection for static/js/auth_form.js's gated forms
   (found live 2026-08-12: Chrome autofilling the login form's email/
   password left the submit button disabled with "Email address is
   required" until the user clicked anywhere on the page). Chrome/Safari/
   Edge (WebKit/Blink) don't fire `input`/`change` when autofill populates
   a field, and the timing is not reliably bounded - a fixed-delay re-check
   after page load (this file's own auth_form.js already had one) can
   simply run before Chrome gets around to it. `:-webkit-autofill` IS
   applied by the browser the instant autofill happens, reliably, so
   attaching a no-op animation to it and listening for the resulting
   `animationstart` DOM event (auth_form.js) gives a genuine event to react
   to instead of guessing a delay. Firefox doesn't support
   `:-webkit-autofill` at all, but Firefox's own autofill already fires
   `input` normally, so this only needs to cover the WebKit/Blink gap. */
@keyframes autofill-detect { from {} to {} }
input:-webkit-autofill { animation-name: autofill-detect; animation-duration: 0.01ms; }
input[type="text"]:hover, input[type="email"]:hover, input[type="password"]:hover,
input[type="number"]:hover, input[type="search"]:hover, input[type="file"]:hover, textarea:hover, select:hover {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
}
input[type="text"]:focus-visible, input[type="email"]:focus-visible,
input[type="password"]:focus-visible, input[type="number"]:focus-visible,
input[type="search"]:focus-visible, input[type="file"]:focus-visible, textarea:focus-visible, select:focus-visible {
  border-color: var(--color-accent); outline-offset: 0;
}
textarea { min-height: 90px; resize: vertical; }
/* 14px, not 12px - same "normal-or-larger body text" rule as .field > label
   above (ui-design-principles.md §1). The 65% muted tone stays (it measures
   ~4.9:1 on --color-bg, over AA) since this genuinely is secondary text,
   unlike the label. */
.field .helptext { display: block; font-size: 14px; margin-top: 4px; color: color-mix(in srgb, var(--color-text) 78%, transparent); }
.field .errorlist, .field-error { list-style: none; margin: 4px 0 0; padding: 0; font-size: 14px; color: #8a3b2c; }
.field-error { display: block; }
.field-error:empty { display: none; }

/* — client-side form gating — (static/js/auth_form.js)
   Touched-based invalid styling: auth_form.js only sets .is-invalid once a
   field has been blurred at least once, so a freshly-loaded empty form
   never renders as an error (ui-design-principles.md §2). */
input.is-invalid, textarea.is-invalid, select.is-invalid {
  border-color: #8a3b2c;
}
input.is-invalid:focus-visible, textarea.is-invalid:focus-visible, select.is-invalid:focus-visible {
  border-color: #8a3b2c;
}

/* The wrapper span that carries the disabled submit button's explanatory
   tooltip. A `title` on a disabled <button> never shows one - browsers
   don't dispatch hover events from a disabled control at all - so the
   title and the not-allowed cursor go on a wrapper that can receive hover
   instead (ui-design-principles.md §2). Critically this wrapper must NOT
   get `pointer-events: none`, which would suppress the very hover the
   tooltip depends on. Block-level so a .btn-block child still fills the
   form's width. */
.submit-gate { display: block; }
/* Block so a .btn-block child can still be width: 100%, but shrunk to the
   button's own width when it isn't one - otherwise the not-allowed cursor
   and the tooltip would both trigger across the whole empty rest of the
   line. Same :has() approach the chip/segmented controls already use. */
.submit-gate:not(:has(.btn-block)) { width: fit-content; }
.submit-gate[title] { cursor: not-allowed; }

/* The visible "why is this disabled" reason (§2 wants a specific reason
   shown, not just a greyed-out button). Deliberately muted rather than
   error-red: on a fresh page load this reads as "here's what's still
   needed", not as a mistake the user has already made. */
.form-gate-reason {
  margin: var(--space-1) 0 0; font-size: 14px;
  color: color-mix(in srgb, var(--color-text) 78%, transparent);
}
.form-gate-reason:empty { display: none; }

/* Live search (static/js/live_search.js) supersedes the any-filled submit
   gate on the recipe library's filter form once JS actually initializes
   it - the gate's whole point was "don't offer a submit that does
   nothing," which no longer applies once every field change already
   re-fetches results live. Hidden rather than removed from the markup, so
   the no-JS fallback (a real GET submit, gated exactly as before) is
   untouched if the script never runs. See live_search.js's header comment
   for the full resolution. */
.live-search-active .submit-gate,
.live-search-active .form-gate-reason {
  display: none;
}

/* — cards — (Phase 3.1, ported verbatim from the mockup's styles.css) */
.card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: var(--radius-md); background: transparent; border: 1px solid var(--color-divider);
}
/* font-size bumped 10px -> 14px (ui-design-principles.md floor for
   secondary text). Color intentionally untouched - accent-color contrast
   here is a separate, still-open question (action-plan.md Phase 11.1). */
.card-kicker { font-size: 14px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent); }
.card-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 17px; line-height: 1.2;
}
.card-meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 14px;
  /* 78%, not the mockup's original 50% (~3.15:1) - same WCAG AA
     contrast fix and rationale as .text-muted above. */
  color: color-mix(in srgb, var(--color-text) 78%, transparent);
  /* flex-wrap added Phase 11.13 follow-up: recipes/_library_results.html
     now folds a parenthesized "date added" into this row alongside
     time/servings, so a 3-item row on a 260px-wide library card
     (.library-grid's minmax(260px, 1fr)) needs to be able to wrap to a
     second line instead of overflowing/clipping. .card-meta's only live
     use in the app is that one row, so this is safe to add on the shared
     class rather than needing a new scoped one (verified via grep - other
     .card-meta hits are static, unserved mockup HTML files only). */
}
.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

/* — photo mat — (Phase 3.2, ported verbatim from the mockup's styles.css) */
.plate {
  filter: sepia(0.22) saturate(0.82) contrast(1.05);
  box-sizing: border-box;
  border: 6px solid var(--color-surface);
  outline: 1px solid var(--color-divider);
}

/* — tags — (Phase 3.1, ported verbatim from the mockup's styles.css) */
.tag {
  display: inline-flex; align-items: center; font-size: 14px;
  letter-spacing: 0.02em; padding: 3px 10px;
  border-radius: calc(var(--radius-md) * 0.75);
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }

/* — radio / segmented control — (Phase 4.1, ported verbatim from the
   mockup's styles.css) */
/* 16px, not 14px - this is a clickable option label, same interactive-text
   floor as .seg-opt above, even though no live screen uses .radio yet
   (see this file's header comment). */
.radio { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 16px; }
.radio input, .seg-opt input {
  position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
}
.radio .dot {
  width: 16px; height: 16px; flex: none; border-radius: 50%;
  border: 1.5px solid var(--color-divider);
}
.radio:hover .dot { border-color: var(--color-accent); }
.radio input:checked + .dot {
  border-color: var(--color-accent); background: var(--color-accent);
  box-shadow: inset 0 0 0 4px var(--color-bg);
}
.radio input:focus-visible + .dot { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.seg {
  display: inline-flex; overflow: hidden;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
}
.seg-opt {
  display: inline-flex; align-items: center; gap: 6px;
  /* 16px, not 15px - these are clickable option labels (the recipe form's
     Meal Type/Season/Cuisine/Category single-select facets), in scope for
     ui-design-principles.md §1's interactive-text floor. */
  padding: 7px 12px; font-size: 16px; cursor: pointer;
}
.seg-opt + .seg-opt { border-left: 1px solid var(--color-divider); }
.seg-opt:has(input:checked) { color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
.seg-opt:not(:has(input:checked)):hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.seg-opt:has(input:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* — filter chips (checkbox-driven, see header comment) — */
.chip-checkbox {
  position: relative; display: inline-flex; align-items: center; cursor: pointer;
  user-select: none; border: 1px solid var(--color-divider);
}
.chip-checkbox:hover { border-color: var(--color-accent); }
.chip-checkbox input {
  position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
}
.chip-checkbox:has(input:checked) {
  border-color: var(--color-accent); background: var(--color-accent-100); color: var(--color-accent-800);
}
/* .tag's own 14px (above) is fine for the purely-decorative status/role
   badges that make up most of its usage (admin_users.html's Admin/
   Unverified/Locked tags, recipe card tags, etc.) - those aren't
   clickable. .chip-checkbox is a real interactive control built on top of
   .tag (the library sidebar's filter chips, the recipe form's dietary
   tags), so it needs its own override up to the interactive-text floor
   rather than widening the shared .tag rule for every non-interactive
   use too (ui-design-principles.md §1 / "never widen a shared class"). */
.chip-checkbox { font-size: 16px; }

/* The flex-wrap row a group of .chip-checkbox chips sits in (one per
   filter facet - Meal Type, Cuisine, etc.). Moved here from recipes.css
   (Phase 11.8) - see this file's own header comment for why. */
.filter-chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* — dialog — (Phase 4.3, ported verbatim from the mockup's styles.css).
   The mockup uses .dialog-backdrop/.dialog as a JS-toggled overlay
   (deleteConfirmOpen); Step 4.3's delete-confirm flow is a real
   GET-confirm/POST-delete *page* instead (no JS), so only .dialog/
   .dialog-title/.dialog-body/.dialog-actions are used here, centered on
   the page via .container - .dialog-backdrop is left unported since
   there's no overlay to dim (nothing else it's needed for yet either). */
.dialog {
  width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
  padding: var(--space-4); border-radius: var(--radius-lg);
  background: var(--color-surface); box-shadow: var(--shadow-lg); border: 1px solid var(--color-divider);
}
/* Bug fix (found live 2026-08-16, accounts:family-detail's #confirm-dialog
   - Phase 10.6): `.dialog { display: flex }` above is an author rule, so it
   beats the UA's own `dialog:not([open]) { display: none }` regardless of
   specificity - any native <dialog class="dialog"> renders visible
   unconditionally without this, ignoring its own `open` attribute
   entirely (so JS calling .close() has no visible effect either). This
   exact fix already existed, independently duplicated, in planner.css
   (for #assign-dialog) and recipes.css (for #quick-add-dialog) - each
   comment even correctly named the risk being guarded against - but
   accounts/family_detail.html has no CSS file of its own to hold a third
   copy, so #confirm-dialog shipped without it and was visibly "open",
   showing its default unpopulated "Are you sure?" text, on every family
   page load. Centralized here instead, since every page loads base.css
   and `.dialog` is defined here - the two other files' copies are now
   redundant and have been removed in favor of this one, and any future
   native <dialog class="dialog"> gets this for free without needing to
   remember to add it again. */
dialog.dialog:not([open]) {
  display: none;
}
.dialog-title {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 20px;
}
.dialog-body { font-size: 14px; opacity: 0.85; }
/* gap is var(--space-3) (13.8px), not var(--space-2) (9.2px) - this is a
   row of distinct action buttons (Cancel + a primary action), which
   ui-design-principles.md §4 sets a 12px floor for, not the 8px floor for
   merely-adjacent controls. */
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: var(--space-2); }
.dialog-actions form { margin: 0; }

/* — navigation — */
.nav {
  display: flex; align-items: center; gap: var(--space-4);
  /* The nav is the one row in this app whose width is driven by content
     it can't shorten (the signed-in user's email, an unverified-email
     notice, one link per screen), so at phone widths it was pushing the
     whole document sideways - every page picked up a horizontal scrollbar,
     not just the one being looked at. Found at 390px while QA'ing the
     shopping list, which adds a seventh item to the row; wrapping is the
     whole fix and needs no per-page work. */
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-divider);
}
/* 16px, not 15px - ui-design-principles.md §1's interactive-text floor
   applies to every nav/utility link ("Log Out", "Change Password", "Back
   Home", ...), not just primary CTAs. */
.nav a { color: inherit; text-decoration: none; font-size: 16px; font-weight: 500; }
/* .nav-brand is itself an <a> inside .nav, so it also matches ".nav a"
   above - ".nav .nav-brand" (2 classes) beats ".nav a" (1 class + 1
   element) on specificity regardless of source order, so this actually
   wins rather than silently losing to the plain-nav-link rule. */
.nav .nav-brand {
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 22px; margin-right: auto; color: var(--color-text); text-decoration: none;
}
.nav a:hover, .nav a[aria-current='page'] { color: var(--color-accent); }
.nav form { margin: 0; }

/* Inline nav login (2026-08-12, templates/base.html) - a compact email +
   password + button row that fits alongside the rest of .nav's links
   rather than the shared text-input rule's width:100% (which would blow
   out the whole nav bar for a control that's meant to sit inline next to
   plain links). Sized to comfortably fit a real email address without
   trying to fit an arbitrarily long one - .nav already wraps
   (flex-wrap on .nav itself) if this doesn't fit next to everything else
   on a narrow viewport, same mechanism every other nav item already
   relies on. min-height/font-size/padding are intentionally NOT
   overridden here - every text/email/password input already gets the
   sitewide 44px/16px/8px-12px floor from the shared selector earlier in
   this file, and this form is no exception to that floor just because
   it's compact. */
.nav-login-form { display: inline-flex; align-items: center; gap: 8px; }
.nav-login-input { width: 16ch; }

/* Small utility (not a mockup class) for a one-button (or select+button)
   POST form sitting inline next to text in a list row (member/invitation/
   admin-user action rows) - forms are block-level by default, which would
   otherwise stack each action onto its own line.

   display:inline-flex + gap, not the previous display:inline +
   margin-left: a bare `display:inline` wrapper provides no real spacing
   or baseline-alignment guarantee between a form's own child controls
   (e.g. admin_users.html's role <select> next to its "Update Role"
   button) - ui-design-principles.md §4 wants an explicit flex row with
   align-items:center and a real gap, not implicit inline whitespace.
   margin is 0 here because spacing *between* sibling .inline-forms/tags/
   text within one row is now the row's own job (.row-list > li below),
   not this element's. */
.inline-form { display: inline-flex; align-items: center; gap: 8px; margin: 0; }
/* 16px, not 15px - ui-design-principles.md §1's interactive-text floor.
   Padding bumped from 3px to a real (if still compact) touch target - was
   tuned to look right on desktop and landed well under the ~40-44px
   minimum touch-target guidance (§3) once measured on a real mobile
   viewport. */
.inline-form .btn { padding: 10px var(--space-3); font-size: 16px; }

/* — repeated list-of-rows (member/invitation/user/backup rows with inline
   actions) —
   Shared class for the plain <ul>/<li> lists in admin_users.html,
   family_detail.html (members + pending invitations), family_invitations.html
   and admin_backups.html - each renders one row per user/invite/backup
   with a handful of tags/inline-action-forms/links next to it. Neither the
   list nor its rows had any layout rule at all before: browsers apply zero
   default spacing to <li>, so per ui-design-principles.md §4 this is a new,
   narrowly-scoped class (not a change to a bare, unstyled <ul>/<li>
   selector, which would reach into every other list in the app - the
   messages list and every errorlist among them). 12px both ways: between
   rows (vertical) and between a row's own children (horizontal), matching
   §4's "distinct action buttons in a group" minimum. */
.row-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* Each row stacks two independently-wrapping groups (info: email/badges;
   actions: the role-select form plus every button form) rather than
   putting all of a row's children - text, badges, AND multiple <form>s -
   into one shared flex-wrap group. Found live (2026-08-12, after this
   class first shipped): a row with several badges (Unverified, Locked,
   role tag) AND several action buttons (Update Role, Deactivate, Reset
   Password) is easily 8+ inline items, and one flat wrapping group
   interleaves badges and buttons unpredictably mid-line as the row
   reflows - reads as cluttered/"klunky" even though every individual gap
   technically met §4's minimum. Two named groups wrap independently and
   predictably instead. */
.row-list > li {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--color-divider);
}
.row-list > li:last-child {
  border-bottom: 0;
  padding-bottom: 0;
}
.row-list-info, .row-list-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.row-list-actions { gap: 12px; }

/* — short-enum <select> width cap —
   admin_users.html's role picker (User/Editor/Admin) is the one bare
   <select> in the app - per ui-design-principles.md §3, a 3-option
   dropdown has no reason to stretch to the shared `select { width:100% }`
   rule's generic full-width default. Originally capped to the longest
   option's raw text width ("Editor", 6 characters) + ~2ch, which turned
   out too tight live (2026-08-12) - a <select>'s rendered box needs room
   for its own internal padding and the browser-drawn dropdown arrow on
   top of the text itself, not just the text's ch-width, so 8ch read as
   barely fitting 3-4 characters rather than the full word. Widened with
   real headroom for that chrome, verified against the actual rendered
   width (not just the CSS value) with "Editor" selected. max-width (not
   width) so it can still shrink narrower on a small viewport. */
.role-select { max-width: 12ch; }

/* — invite forms (family_invite_existing.html/family_invite_email.html) —
   Found live 2026-08-16 (Jesse): the email field rendered at the shared
   .input rule's full-width default, which reads as a real defect once
   anyone looks closely (ui-design-principles.md §3) - and the new role
   picker (same section) is exactly the "short-enum <select> stretched to
   match a free-text field's width" bug that section calls out by name.
   .invite-role-select is wider than .role-select above (22ch, not 12ch) -
   its longest real option is "Meal Plan Editor" (17 characters, vs.
   .role-select's "Editor" at 6), and .role-select's own comment already
   documents that a <select>'s rendered box needs real headroom beyond the
   option text's raw ch-width for its own padding/dropdown-arrow chrome,
   not just +2ch - verified against the actual rendered width, not just
   the CSS value, same as that one was. */
.invite-row {
  display: flex;
  /* flex-start, not flex-end: a validation error renders an <ul
     class="errorlist"> below whichever field failed, growing that
     .field block's height - flex-end would bottom-align the row, so the
     *other* field (and its label) visibly drops out of alignment
     whenever only one side has an error. flex-start keeps both fields'
     labels/inputs top-aligned regardless, since the error text only
     ever extends the block downward from there. Found in review
     (2026-08-16), verified live at both viewport widths and in the
     error state specifically. */
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.invite-email-field { flex: none; }
.invite-email-input { max-width: 32ch; }
.invite-role-field { flex: none; }
.invite-role-select { max-width: 22ch; }

/* — layout primitives —
   Not literal mockup classes (the mockup's screens each hardcode their own
   max-width per screen - 1280px library, 840px recipe detail, 760px form,
   900px shopping list); .container is a general-purpose default in that
   same spirit for pages that don't have a mockup screen of their own yet. */
.container { max-width: 720px; margin: 0 auto; padding: var(--space-8) var(--space-4); }
/* Narrower variant of .container for the credential screens (log in,
   register, the two password-reset forms). ui-design-principles.md §3 asks
   for controls sized to their real expected content rather than a generic
   full-width default: in the 720px .container an email/password input
   rendered 683px wide, which is several times any realistic value and was
   half of the "field sizing looks wrong" report (the other half being the
   36px height, fixed on the shared input rule above). 420px puts the input
   at ~383px - roughly 45 characters of the body font, comfortably more than
   any real email address. Scoped to its own class rather than narrowing
   .container or the shared input rule, per §4's "never widen (or narrow) a
   shared class to fix one page". */
.auth-container { max-width: 420px; margin: 0 auto; padding: var(--space-8) var(--space-4); }

/* — messages (Django messages framework) —
   No equivalent component in the mockup (none of its 6 screens show a
   flash-message state); minimal styling added here, built only from
   existing tokens, so alerts don't render as a bare bulleted list. */
.messages { list-style: none; margin: 0 0 var(--space-4); padding: 0; }
.messages li {
  padding: var(--space-2) var(--space-3);
  /* 12px, not var(--space-2) (9.2px) - ui-design-principles.md §4's
     12px-between-repeated-rows floor applies here too (a flash message
     list is a repeated list of rows, same as any other). */
  margin-bottom: 12px;
  border: 1px solid var(--color-divider); border-radius: var(--radius-md);
  font-size: 14px;
}

/* — print —
   .no-print mirrors the mockup's own convention: pages that need print
   support (recipe detail - Phase 3.2; shopping list - Phase 7.3) mark
   their on-screen-only chrome (toolbars, buttons) with this class. Global
   chrome (nav/header) lives here since it's shared across every page;
   page-specific print polish (e.g. recipe detail's two-column layout
   collapsing to one) lives in that page's own stylesheet instead. */
@media print {
  header, .no-print {
    display: none !important;
  }
  body {
    background: #fff;
  }
  a {
    color: inherit;
    text-decoration: none;
  }
}
