/* ---------- Design tokens ----------
   "Kitchen docket" theme: the week as an order-ticket board on a pale
   kitchen-tile background. Light only, by design — no dark-mode branch. */
:root {
  color-scheme: light;
  --paper:     #F2F6F0; /* page background — pale mint-white tile */
  --card:      #FFFFFF; /* panels, inputs */
  --ink:       #20281F; /* primary text */
  /* Was #63715F, which measured 4.74:1 on --paper — nominally AA, but this
     colour is used at 0.72rem, uppercase and letter-spaced, which is the
     configuration that ratio is least forgiving of. #55624F measures 5.92:1
     on --paper and 6.47:1 on --card, and still sits a clear 2.34:1 away
     from --ink so the two don't collapse into one another. */
  --ink-soft:  #55624F; /* secondary/muted text, derived borders */
  --line:      rgba(32, 40, 31, 0.14);
  --teal:      #1E6E63; /* brand — primary actions, nav, focus, filled slots */
  --mustard:   #DE9C2E; /* "today" marker, and "expiring soon" on invites */
  --danger:    #9A3B2A; /* delete/error */

  /* There used to be a --tomato (Lunch) and a --basil (Dinner) here, used
     for a dot beside each meal label and the left border of every filled
     slot in that row. Both are gone: the rows are labelled "Lunch" and
     "Dinner" in text, so the colours carried nothing the label didn't —
     three encodings of one binary — and --tomato (#D14B2E) was close enough
     to --danger (#9A3B2A) at small sizes that the two didn't reliably read
     as different, which made red mean two things at once. A filled slot's
     left border is --teal now: it marks *filled*, which the grid does not
     otherwise say. */

  --mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* ---------- Breakpoints ----------
   There are two, deliberately, and they are not the same number:

     900px — the calendar swaps its 7-column grid for the mobile day-cards /
             single-day views.
     720px — the dish table becomes cards, and the top bar wraps its tabs
             onto their own row.

   The calendar's is higher because a 7-column grid doesn't become usable
   again at 721px — that is exactly where it is most cramped (~90px a day,
   less the meal gutter), and the whole 721–900 tablet band was landing on
   it. Everything else has real room at those widths. Don't tidy the two
   into one value. These live only here now: index.html used to mirror them
   as Tailwind `mobile:` / `tablet:` screens, but that CDN is gone (see the
   comment in its <head>), so there is no second copy to keep in sync. */

* {
  box-sizing: border-box;
}

/* An element with its own `display` rule (e.g. .day-cards-list's
   `display: flex`) otherwise beats the browser's default `[hidden] {
   display: none }` at equal specificity — toggling `.hidden = true` in JS
   would silently stop hiding it. This guarantees `[hidden]` always wins,
   for every element, present or future. */
[hidden] {
  display: none !important;
}

/* Visually hidden, still read aloud — for labels whose field is already
   self-evident on screen (the search box, the sort select) but which a
   screen reader would otherwise meet as an unlabelled control. A
   placeholder is not a substitute: it disappears the moment there's a
   value, and assistive tech may not announce it at all. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  line-height: 1.4;
}

/* ---------- Pre-app screens (sign-in, invite accept) ----------
   Both fill the page in place of <main id="app-shell">: the sign-in form
   until Clerk confirms a user (auth.js), the invite prompt when the page was
   opened via an /invite/<token> link (app.js's showInviteAccept()). They
   share this wrapper deliberately — they're the same "before you're in the
   app" screen seen at two different moments, and each used to carry its own
   near-identical copy of the centring rules.

   Vertical centring is min-height + justify-content, not the `margin: 10vh
   auto 0` both used to use: 10vh is a dead band on a laptop and nowhere near
   centre on a tall monitor. min-height (not height) is what keeps it safe on
   a short viewport — content taller than the screen grows the box and
   scrolls normally, instead of being centred into a clipped overflow whose
   top can't be reached. */
.auth-screen {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px clamp(16px, 4vw, 48px);
  text-align: center;
}

/* The app's name lived only in <title> before this, so the sign-in screen
   was an unlabelled third-party widget on an empty page. Sans and sentence
   case: the mono/uppercase docket accent is for section labels within a
   panel, not for the one real heading on a screen (see the note above
   `button`). */
.auth-heading {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 600;
  color: var(--ink);
}

.auth-tagline {
  margin: 4px 0 20px;
  color: var(--ink-soft);
  font-size: 0.9rem;
}

/* Clerk's mounted SignIn component brings its own card styling — this only
   caps the width and hosts the skeleton until that arrives. */
.clerk-sign-in {
  width: 100%;
  max-width: 420px;
  color: var(--ink-soft);
}

/* Stand-in for Clerk's SignIn card while its two remote scripts load (see
   index.html) — roughly that card's footprint, so mounting the real form
   doesn't shove the page around. */
.auth-skeleton {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 28px 24px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
}

.auth-skeleton-bar {
  height: 34px;
  border-radius: 4px;
  background: var(--line);
}

.auth-skeleton-title {
  height: 20px;
  width: 55%;
  align-self: center;
  margin-bottom: 10px;
}

/* Tinted teal: the real card's submit button is the one coloured element in
   it, and a uniformly grey skeleton reads as a broken card rather than a
   loading one. */
.auth-skeleton-button {
  background: rgba(30, 110, 99, 0.18);
  margin-top: 6px;
}

/* Same gate as .save-status.status-saving's spinner below: the pulse says
   "still coming", which is a hint and not information, so it's dropped
   entirely rather than slowed when reduced motion is asked for. */
@media (prefers-reduced-motion: no-preference) {
  .auth-skeleton-bar {
    animation: auth-skeleton-pulse 1.4s ease-in-out infinite;
  }
}

@keyframes auth-skeleton-pulse {
  50% {
    opacity: 0.45;
  }
}

/* The two states auth.js can dead-end in — Clerk not configured, and its
   scripts failing to load. Both rendered as plain --ink-soft body text
   before, i.e. indistinguishable from the loading placeholder they replaced,
   so a configuration problem looked like a slow network. Same --danger +
   bold treatment .save-status.status-error uses for every other failure in
   the app. */
.auth-error {
  margin: 0;
  color: var(--danger);
  font-weight: 700;
}

.auth-error-actions {
  margin-top: 14px;
}

/* ---------- Layout: top bar + content ----------
   A sticky full-width top bar over the content, replacing the 256px left
   sidebar it used to sit beside — three destinations never justified a
   permanent column, and giving those ~280px back to the calendar's day
   columns is the whole point of the change. The bar is edge-to-edge (its
   own horizontal padding matches .content's, so the switcher and the
   panels below it line up), which is why <main> carries no padding of its
   own anymore.

   ⚠️ Why there is no CSS framework here, and why adding one back is not
   free: Tailwind's Play CDN appends its generated utilities to <head>
   *after* this stylesheet's <link>, so a plain class selector here silently
   loses every equal-specificity tie to a utility on the same element. (Not
   hypothetical: the old mobile drawer's `position: fixed` once lost to
   Tailwind's `sticky`, leaving a ~100vh-tall invisible sidebar in flow that
   pushed the calendar below the fold.) That CDN is gone now — nothing had
   used a utility class since the sidebar went — so this file wins
   everything, and it only keeps doing so while that stays true. */

main {
  width: 100%;
}

.top-bar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px clamp(16px, 4vw, 48px);
  background: var(--card);
  border-bottom: 1px solid var(--line);
}

/* The left and right slots share the leftover width evenly so the tab group
   sits optically centred in the bar whatever the household name's length —
   and stays put when #clerk-account flips from hidden to shown (auth.js)
   once Clerk resolves. */
.top-bar-left,
.top-bar-right {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
}

.top-bar-right {
  justify-content: flex-end;
}

.top-nav {
  flex: 0 0 auto;
  display: flex;
  gap: 4px;
}

/* Docket-styled tabs (4px radii, hairline border, solid teal when active —
   the same active signature .mobile-toggle-btn already uses), not the grey
   --paper pill the old sidebar links had. Own hover background rather than
   the global button:hover brightness filter, which is a no-op on a
   transparent background. */
.nav-btn {
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid transparent;
}

.nav-btn:hover {
  filter: none;
  background: var(--paper);
  color: var(--ink);
}

.nav-btn.active {
  background: var(--teal);
  color: white;
  border-color: transparent;
}

/* ---------- Household switcher ----------
   One control, replacing the old #household-label + rail-of-initials pair
   (two representations of the same thing, and the rail wrapped badly past
   ~4 households): a button showing the current household, and a menu
   listing all of them plus "New household…". app.js's
   renderHouseholdSwitcher() fills both in; setHouseholdMenuOpen() toggles
   #household-menu's `hidden` and the button's aria-expanded. */

.household-switcher {
  position: relative;
  min-width: 0;
}

.household-switcher-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 280px;
  padding: 4px 8px;
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--line);
}

.household-switcher-btn:hover {
  filter: none;
  background: var(--paper);
  border-color: var(--teal);
}

.household-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.household-chevron {
  flex: none;
  width: 14px;
  height: 14px;
  color: var(--ink-soft);
}

@media (prefers-reduced-motion: no-preference) {
  .household-chevron {
    transition: transform 0.15s ease;
  }
}

.household-switcher-btn[aria-expanded='true'] .household-chevron {
  transform: rotate(180deg);
}

/* Square-ish (4px, like every other surface here) rather than the old rail's
   circles — the initials are a small docket stamp, not an avatar. */
.household-initials {
  flex: none;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  background: var(--teal);
  color: white;
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

.household-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 40;
  min-width: max(100%, 240px);
  max-width: min(90vw, 340px);
  max-height: min(60vh, 420px);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: 0 8px 24px rgba(32, 40, 31, 0.16);
}

.household-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 6px;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border: 1px solid transparent;
  font-weight: 500;
}

.household-menu-item:hover {
  filter: none;
  background: var(--paper);
}

.household-menu-item.active {
  background: var(--teal);
  color: white;
}

/* The chip would otherwise be teal-on-teal on the selected row. */
.household-menu-item.active .household-initials {
  background: rgba(255, 255, 255, 0.24);
}

.household-menu-item .household-initials {
  width: 22px;
  height: 22px;
}

.household-menu-item-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* "New household…" — the only way to get a second one, so it's always
   present, set apart by a rule and a dashed (rather than filled) chip. */
.household-menu-add {
  color: var(--ink-soft);
}

.household-menu-add .household-initials {
  background: transparent;
  color: var(--ink-soft);
  border: 1px dashed var(--line);
  font-size: 0.85rem;
}

.household-menu-sep {
  height: 1px;
  margin: 4px 0;
  background: var(--line);
}

/* ---------- Account (top bar, right) ---------- */

.clerk-account {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.clerk-user-button {
  flex: none;
  display: flex;
  align-items: center;
}

/* Our own first-name text, not Clerk's — see auth.js's render() for why.
   min-width: 0 + the flex item's own ellipsis lets it shrink ahead of
   #logout-btn (flex: none, like household-initials/-chevron) rather than
   pushing the account block wider than the row has room for. */
.clerk-user-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink);
}

.logout-btn {
  flex: none;
  background: transparent;
  color: var(--danger);
  border-color: var(--line);
}

.logout-btn:hover {
  filter: none;
  background: var(--paper);
  border-color: var(--danger);
}

.content {
  display: flex;
  flex-direction: column;
  gap: 20px;
  min-width: 0;
  padding: 24px clamp(16px, 4vw, 48px) 48px;
}

@media (max-width: 720px) {
  /* Three tabs don't fit beside the switcher and the account block on a
     phone, so the bar wraps: switcher + account on the first row, the tabs
     stretched across a second one. No drawer, no toggle, no scroll lock —
     that machinery existed only because a 256px sidebar had nowhere to go
     at this width. */
  .top-bar {
    flex-wrap: wrap;
    gap: 10px;
  }

  .top-bar-left,
  .top-bar-right {
    flex: 0 1 auto;
  }

  .top-bar-right {
    margin-left: auto;
  }

  .top-nav {
    order: 3;
    flex: 1 0 100%;
  }

  .top-nav .nav-btn {
    flex: 1;
  }

  .household-switcher-btn {
    max-width: 52vw;
  }
}

/* ---------- Panels ---------- */

.panel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 20px;
}

.panel h2 {
  margin: 0 0 4px;
  font-family: var(--mono);
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  border-left: 3px solid var(--teal);
  padding-left: 8px;
}

.section-hint {
  margin: 4px 0 14px;
  color: var(--ink-soft);
  font-size: 0.85rem;
}

/* Shown in place of .content when the signed-in user belongs to no
   household (see renderHouseholdPresence() in app.js). It's a sibling of
   .content, not a child, so it carries its own copy of that page padding;
   capped narrow because two short paragraphs and a button read as adrift
   when stretched across a desktop width. */
.no-household {
  margin: 24px clamp(16px, 4vw, 48px) 48px;
  max-width: 520px;
}

/* ---------- Forms ---------- */

.inline-form {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: end;
}

.inline-form label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

input, select, textarea {
  font: inherit;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 6px 8px;
  min-width: 0;
}

input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 1px;
}

input[type="text"] {
  width: 100%;
}

/* Sans, sentence case, comfortably sized. The mono/uppercase docket accent
   used to be *the default* here — on every button, both headings levels,
   every table header, the meal labels, the week label, day names, day
   dates, modal titles — which left most of the app set in 0.72–0.78rem
   letter-spaced uppercase, i.e. the least legible configuration on offer,
   with nothing actually emphasised because everything was. The accent is
   now reserved for section labels (.panel h2/h3) and table column headers
   (th), which is what it's good at. */
button {
  font: inherit;
  cursor: pointer;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 7px 14px;
  background: var(--teal);
  color: white;
  font-family: var(--sans);
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: normal;
}

/* Only does anything on a *filled* button — see .secondary/.danger below,
   which are transparent and so need a real background change instead. */
button:hover {
  filter: brightness(1.1);
}

button:disabled {
  opacity: 0.45;
  cursor: default;
  filter: none;
}

/* A control blocked by the plan limit (applyLimit() in app.js). Looks
   disabled, but deliberately isn't: `disabled` would swallow the click, and
   on a touch device — no hover, so no title tooltip — the button would just
   do nothing with no way to find out why. It stays clickable and opens
   #upgrade-modal instead, so the explanation is always one tap away.
   Matches button:disabled above so the two read the same. */
button.limit-reached {
  opacity: 0.45;
  cursor: default;
  filter: none;
}

button.limit-reached:hover {
  filter: none;
}

/* `filter: brightness()` is a no-op on `background: transparent`, and that
   covers most of the buttons in this app (week nav, Close, Cancel, Delete,
   the ingredients disclosure) — they had no hover feedback at all. */
button.secondary {
  background: transparent;
  color: var(--ink-soft);
  border-color: var(--line);
}

button.secondary:hover:not(:disabled) {
  filter: none;
  background: var(--paper);
  border-color: var(--ink-soft);
  color: var(--ink);
}

button.danger {
  background: transparent;
  color: var(--danger);
  border-color: var(--line);
}

button.danger:hover:not(:disabled) {
  filter: none;
  background: rgba(154, 59, 42, 0.08);
  border-color: var(--danger);
}

/* ---------- Tables ---------- */

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}

th, td {
  padding: 8px 10px;
  text-align: left;
  border-bottom: 1px solid var(--line);
}

th {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-weight: 700;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

td input, td select {
  width: 100%;
  min-width: 90px;
}

.row-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

/* Fixed-size glyph, not variable-length text — its box never changes width,
   so a neighboring Clear/Delete button never shifts when a save completes.
   The actual message is on `title`/`aria-label` (see setSaveStatus() in
   app.js), not in the visible glyph. */
.save-status {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 16px;
  height: 16px;
  font-size: 0.7rem;
  line-height: 1;
  cursor: default;
}

.save-status.status-idle {
  color: var(--ink-soft);
  opacity: 0.45;
}

.save-status.status-saving {
  color: var(--ink-soft);
}

@media (prefers-reduced-motion: no-preference) {
  .save-status.status-saving {
    animation: status-spin 0.8s linear infinite;
  }
}

@keyframes status-spin {
  to {
    transform: rotate(360deg);
  }
}

.save-status.status-saved {
  color: var(--teal);
}

.save-status.status-error {
  color: var(--danger);
  font-weight: 700;
}

.empty-state {
  color: var(--ink-soft);
  font-style: italic;
  padding: 8px 0;
}

/* ---------- Weekly menu grid ---------- */

/* The week label is the heading of the view and the three controls are one
   group — not label-between-buttons, which is how this read before
   ("← Previous | Mar 3–9 | This week | Next →"). */
.week-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}

/* Overrides .panel h3's mono/uppercase accent (hence the descendant
   selector — it has to outrank `.panel h3`): this is the primary orienting
   information on the screen, not a section label. */
.week-nav .week-label {
  margin: 0;
  font-family: var(--sans);
  font-size: 1.05rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
  color: var(--ink);
}

.week-nav-buttons {
  display: flex;
  gap: 6px;
}

/* Square-ish, so the two arrows read as a pair of steppers rather than two
   more wordy buttons beside "This week". */
.week-arrow {
  padding: 7px 12px;
  line-height: 1;
}

/* Fixed layout so the 7 day columns divide up whatever width the panel
   actually has (driven by the header row's widths below) instead of each
   demanding its own minimum — that's what was forcing a horizontal
   scrollbar on ordinary desktop widths. */
.menu-table {
  table-layout: fixed;
}

.menu-table th, .menu-table td {
  white-space: normal;
  vertical-align: top;
  overflow-wrap: break-word;
}

/* Row-label column ("Meal" header / "Lunch" / "Dinner" row headers) — a
   narrow gutter, not the 96px it used to take. It held two permanently
   labelled rows, coloured dots, and those same colours again on every
   filled slot's left border: three encodings of one binary, paid for out
   of the width the day columns need most. The dots are gone (see the
   --tomato note in the tokens block) and the rest of that width went to
   the days. Typography comes from the shared `th` rule now that these are
   real row headers. */
.menu-table .meal-col {
  /* A real pixel width, not the classic width:1% shrink-to-content trick —
     that trick only works under table-layout:auto. Under the fixed layout
     used here, 1% is taken literally (≈1% of the table's total width),
     which collapsed this column to ~11px and pushed its text out over the
     first day column. */
  width: 62px;
  padding-right: 6px;
  white-space: nowrap;
  vertical-align: middle;
}

.menu-table th.day-col {
  text-transform: none;
  letter-spacing: normal;
  border-bottom: 2px solid transparent;
}

/* Sans, and bigger than body text rather than smaller: the day names and
   the week label are the primary orienting information in a weekly grid,
   and they were previously set at 0.78rem uppercase mono. */
.day-name {
  display: block;
  font-family: var(--sans);
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
  font-size: 0.95rem;
}

.day-date {
  display: block;
  color: var(--ink-soft);
  font-size: 0.8rem;
  font-weight: 400;
}

/* Sat/Sun get a flat tint down the whole column: for a *family* menu
   planner they're structurally different days (different rhythms, more
   people home), and a weekly grid that doesn't show where the week bends
   makes you count columns. Declared before .is-today so today's marker
   wins on a weekend day. */
.menu-table .is-weekend {
  background: rgba(32, 40, 31, 0.035);
}

/* Today's column gets a mustard underline tab + a faint tint — kept off the
   text itself (mustard-on-white fails contrast at this size), so the day
   name stays fully legible while the marker is still unmistakable. The tint
   covers the body cells too, not just the <th>: a header-only tint pointed
   at the two cells underneath without actually reaching them, which is the
   fastest orienting cue a weekly grid has. */
.menu-table .is-today {
  background: rgba(222, 156, 46, 0.16);
}

.menu-table th.day-col.is-today {
  border-bottom-color: var(--mustard);
}

/* A slot in the grid is a plain button showing a compact summary — all
   actual editing happens in #slot-modal. The left edge goes teal once
   something's assigned, so a scan of the week shows what's filled at a
   glance without reading a word. */
.slot-summary {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
  min-height: 44px;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border: none;
  border-left: 3px solid transparent;
  border-radius: 0;
  padding: 4px 4px 4px 8px;
  font: inherit;
  text-transform: none;
  letter-spacing: normal;
  cursor: pointer;
}

.slot-summary:hover {
  background: var(--paper);
  filter: none;
}

.slot-summary.is-filled {
  border-left-color: var(--teal);
}

/* An empty slot is empty — no "— empty —" placeholder repeated across all
   14 of them, which made the word "empty" the dominant content of a fresh
   week. The "+" is the affordance, and only when you're pointing at (or
   tabbed to) the slot. */
.slot-summary-add {
  color: var(--ink-soft);
  font-size: 1rem;
  line-height: 1;
  opacity: 0;
}

.slot-summary:hover .slot-summary-add,
.slot-summary:focus-visible .slot-summary-add {
  opacity: 1;
}

/* Touch has no hover, so there'd be nothing at all to point at — show it
   faintly instead of not at all. */
@media (hover: none) {
  .slot-summary-add {
    opacity: 0.45;
  }
}

.slot-summary-line {
  font-size: 0.85rem;
  line-height: 1.3;
}

.slot-summary-course {
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-right: 4px;
}

/* ---------- Mobile/tablet Calendar (day cards / single day) ----------
   Desktop shows the 7-day-column grid; below 900px it's replaced entirely
   by one of two views sharing the same slot-summary buttons.

   900, not the 720 the rest of the app switches at: the grid doesn't
   suddenly become usable at 721px — that's exactly where it's at its most
   cramped (7 columns in ~700px, minus the meal gutter, is ~90px a day for
   real dish names), and the 721–900 band was landing on it. The dish table
   and the top bar still switch at 720, where they actually need to. */

.calendar-mobile {
  display: none;
}

/* Lives outside .calendar-mobile in the HTML (it needs to sit above
   .week-nav, not just above the day-cards/single-day content), so it needs
   its own show/hide rather than inheriting .calendar-mobile's. */
.mobile-view-toggle {
  display: none;
  gap: 6px;
  margin-bottom: 14px;
}

@media (max-width: 900px) {
  .calendar-desktop {
    display: none;
  }

  .calendar-mobile {
    display: block;
  }

  .mobile-view-toggle {
    display: flex;
  }

  /* Day view already lets you reach any week by changing day (and the
     single-day-nav / swipe make that explicit) — a second, week-level
     paginator on top of that is redundant, not a second way to navigate. */
  #calendar-view.day-view-active .week-nav {
    display: none;
  }
}

@media (max-width: 720px) {
  .week-nav {
    justify-content: center;
  }

  .week-nav .week-label {
    flex: 1 1 100%;
    min-width: 0;
    text-align: center;
  }

  .week-nav-buttons {
    flex: 1 1 100%;
  }

  .week-nav-buttons button {
    flex: 1;
  }
}

.mobile-toggle-btn {
  flex: 1;
  background: var(--card);
  color: var(--ink-soft);
  border: 1px solid var(--line);
}

.mobile-toggle-btn.active {
  background: var(--teal);
  color: white;
  border-color: transparent;
}

.day-cards-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.day-card {
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 10px;
}

.day-card.is-weekend {
  background: rgba(32, 40, 31, 0.035);
}

/* After .is-weekend, so today wins on a Saturday or Sunday. */
.day-card.is-today {
  background: rgba(222, 156, 46, 0.1);
  border-color: var(--mustard);
}

.day-card-header {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
}

.day-card-meal-row {
  display: flex;
  align-items: stretch;
  gap: 8px;
}

.day-card-meal-row + .day-card-meal-row {
  margin-top: 6px;
}

/* The mobile counterpart of the grid's .meal-col row header — it isn't in
   a table here, so it carries the `th` accent itself. */
.day-card-meal-label {
  flex: 0 0 58px;
  display: flex;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}

/* Boxed here (the grid's cells provide their own edges; a card's don't).
   The left border has to be restated rather than left to `border:` above —
   this selector and `.slot-summary.is-filled` have equal specificity, so
   `border-left-color: var(--line)` would win on source order alone and
   the teal filled-marker would never show inside a day card. */
.day-card-meal-row .slot-summary {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--line);
  border-left: 3px solid transparent;
  border-radius: 4px;
}

.day-card-meal-row .slot-summary.is-filled {
  border-left-color: var(--teal);
}

.single-day-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}

/* Same demotion as .week-label — this is the day you're looking at, the
   most orienting text on a phone screen, not a section label. */
.single-day-label {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 1.05rem;
}

.single-day-view {
  touch-action: pan-y; /* keep vertical page scroll; horizontal drags are the swipe gesture */
}

/* ---------- Slot edit modal ---------- */

.slot-modal {
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 20px;
  width: min(520px, 92vw);
  background: var(--card);
  color: var(--ink);
}

.slot-modal::backdrop {
  background: rgba(32, 40, 31, 0.45);
}

.slot-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}

/* A modal title is a title, not a section label — sentence case, sans.
   (Same for .form-modal-header h3 further down.) */
.slot-modal-header h3 {
  margin: 0;
  font-family: var(--sans);
  font-size: 1.05rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
}

.slot-modal-list {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.slot-modal-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px;
}

/* Dish picker first and widest — it's the field you actually came here to
   fill in; the course beside it is a secondary qualifier that defaults to
   Main and is usually left alone. */
.slot-modal-list li .dish-picker,
.slot-modal-dish-wrap {
  flex: 2 1 180px;
  min-width: 0;
}

.slot-modal-list li select,
.slot-modal-add select {
  flex: 0 1 110px;
  min-width: 0;
}

/* ---------- Plan-a-dish modal (Dish Library -> Calendar) ----------
   The mirror of the slot modal above: pick the day and meal to drop one
   already-chosen dish into. Sits next to it here because the two are the
   same job from opposite ends, and they share the meaning of teal
   ("something is already in this slot").

   This used to draw the whole week as 7 rows of 2 slot buttons, each
   showing its contents. It measured ~720px on a 390px phone — 480px of
   that the day list alone — and still scrolled, because fourteen cells is
   what "show me everything" costs. It's two controls now: a row of day
   chips and a pair of meal buttons, ~376px with nothing scrolling. What a
   slot holds is on the meal buttons, for the selected day only.

   Deliberately no `display` on the <dialog> itself — an author `display`
   rule beats the UA's `dialog:not([open]) { display: none }` (author sheet
   wins over UA regardless of specificity) and the dialog would then be
   permanently on screen. The flex column lives on .plan-modal-inner
   instead. Same class of trap as the [hidden] guard at the top of this
   file, one layer further down. */

.plan-modal {
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0; /* .plan-modal-inner owns it — see the bottom-sheet rules below */
  width: min(460px, 94vw);
  background: var(--card);
  color: var(--ink);
  /* Nothing inside is expected to overflow at this height, but a very short
     window (a phone held sideways) can still outgrow it — scroll rather
     than clip, or the confirm button would be unreachable. */
  overflow: auto;
}

.plan-modal::backdrop {
  background: rgba(32, 40, 31, 0.45);
}

/* dvh over vh where supported: on mobile Safari/Chrome, vh is measured
   against the *retracted* URL bar, so a footer sized in vh sits under it
   until you scroll. */
.plan-modal-inner {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px;
  max-height: 92vh;
  max-height: 92dvh;
}

/* An icon button, so it needs its own square rather than the text button's
   padding — and a real touch target, since it sits in the corner where a
   thumb reaching for the day strip lands. */
.plan-close-btn {
  flex: none;
  width: 40px;
  height: 40px;
  padding: 0;
  font-size: 1.3rem;
  line-height: 1;
}

.plan-week-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

/* Same demotion as .week-label on the Calendar: sans and sentence case, not
   the mono/uppercase accent. */
.plan-week-label {
  font-family: var(--sans);
  font-size: 0.95rem;
  font-weight: 600;
  text-align: center;
}

.plan-week-nav button {
  flex: none;
  min-width: 40px;
  padding: 6px 10px;
}

/* A week that wouldn't load. It sits between the nav and the day strip and
   is `hidden` the rest of the time, so it never moves the controls below it
   — the whole point of it is that a failed fetch leaves the sheet's layout
   alone (this used to replace the day strip outright). */
.plan-week-error {
  margin: 0;
  color: var(--danger);
  font-size: 0.85rem;
}

/* Seven equal columns rather than a scrolling strip: a week is a fixed,
   small number, and hiding two days off the edge of a swipe would cost more
   than the width it saves. The 3px gutter is not arbitrary — at 360px (the
   narrowest phone worth designing for) it puts each chip at 44.3px, and 4px
   put it at 43.4px, just under the touch-target floor. */
.plan-day-strip {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 3px;
}

.plan-day-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-height: 52px;
  padding: 6px 2px;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  font-weight: 400;
  overflow: hidden;
}

/* No text-transform: WEEKDAY_FMT already hands back the locale's own short
   form ("Mon", "lun"), and the rest of the app leaves it alone (.day-name). */
.plan-chip-day {
  font-size: 0.68rem;
  color: var(--ink-soft);
}

.plan-chip-date {
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.1;
}

/* The whole week's shape in one row: a day with anything on it gets a dot.
   Teal because that is already this app's "filled" colour, on the Calendar's
   slots and on the meal buttons below.

   `visibility`, not a dot that comes and goes from the DOM: every chip
   always reserves its 4px, so the weekday and date never shift as a week
   loads or as one week's dots give way to another's. That stillness is the
   whole reason paging feels like paging rather than a rebuild — the strip is
   built once (buildPlanControls) and only ever repainted. */
.plan-chip-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--teal);
  visibility: hidden;
}

/* Only "planned" shows it. The other two values are "none" (we know: nothing
   there) and "unknown" (a week still loading, or one that failed) — and
   those two must never look alike, which is why the meal buttons say "…"
   rather than "Free" in the unknown case. A dotless strip claiming a free
   week is the one wrong answer this dialog can't give. */
.plan-day-chip[data-occupancy='planned'] .plan-chip-dot {
  visibility: visible;
}

.plan-day-chip.is-weekend {
  background: rgba(32, 40, 31, 0.035);
}

/* After .is-weekend, so today wins on a Saturday or Sunday — same ordering
   dependency as .day-card's pair. */
.plan-day-chip.is-today {
  background: rgba(222, 156, 46, 0.14);
  border-color: var(--mustard);
}

/* Last of the three state rules on purpose: .is-selected, .is-today and
   .is-weekend all have equal specificity, so source order is the only thing
   making the pick win over the today marker underneath it. */
.plan-day-chip.is-selected {
  background: var(--teal);
  border-color: var(--teal);
  color: white;
}

.plan-day-chip.is-selected .plan-chip-day {
  color: rgba(255, 255, 255, 0.85);
}

.plan-day-chip.is-selected .plan-chip-dot {
  background: white;
}

.plan-day-chip:hover:not(.is-selected) {
  filter: none; /* a brightness() bump does nothing on a near-white surface */
  background: var(--paper);
  border-color: var(--ink-soft);
}

/* The selected day's two meals, and the only place the dialog says what is
   already in a slot — two buttons' worth, at the moment it can still change
   your mind, instead of fourteen up front. */
.plan-meal-toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.plan-meal-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 2px;
  min-height: 56px;
  padding: 8px 10px;
  overflow: hidden;
  text-align: left;
  font-weight: 400;
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--line);
  border-left-width: 3px;
}

.plan-meal-name {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--ink-soft);
}

/* One line, then ellipsis — planSlotSummary() has already shortened the
   contents to "first dish + n", and the full breakdown is in the button's
   aria-label. */
.plan-meal-what {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.88rem;
}

.plan-meal-what.is-free {
  color: var(--ink-soft);
  font-style: italic;
}

/* "…" — the slot's contents aren't known yet. Muted like .is-free but
   upright, and a mark rather than a word, so a slot we can't see into is
   never mistaken for one we can see is empty. No animation: this normally
   shows for a fraction of a second, if it paints at all (renderPlanOccupancy
   is held off for 150ms), and a pulse would be the flicker back again. */
.plan-meal-what.is-loading {
  color: var(--ink-soft);
}

.plan-meal-btn.is-filled {
  background: var(--paper);
  border-left-color: var(--teal);
}

/* After .is-filled, so the pick's border-color shorthand overrides that
   teal left edge — the button still names its contents, so nothing is lost
   by dropping the marker while it's selected. */
.plan-meal-btn.is-selected {
  background: var(--teal);
  border-color: var(--teal);
  color: white;
}

.plan-meal-btn.is-selected .plan-meal-name,
.plan-meal-btn.is-selected .plan-meal-what {
  color: white;
}

.plan-meal-btn:hover:not(.is-selected) {
  filter: none;
  background: var(--paper);
  border-color: var(--ink-soft);
  border-left-color: var(--ink-soft);
}

.plan-meal-btn.is-filled:hover:not(.is-selected) {
  border-left-color: var(--teal);
}

.plan-course {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.plan-course select {
  flex: 0 1 140px;
}

.plan-modal-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
}

/* Full width rather than shrink-wrapped: it's the only action down here, its
   label is a whole sentence ("Add to Tue Sep 8 · Dinner", or the instruction
   while it's disabled), and on a phone it's the last thumb target in the
   flow. */
.plan-modal-actions button {
  flex: 1 1 auto;
}

/* This is the one disabled button in the app whose label has to stay
   readable — it's the only thing saying which step is missing. The global
   `button:disabled { opacity: 0.45 }` can't do that here: white on
   45%-opacity teal measures about 1.9:1. So while it's disabled it drops
   the fill entirely and becomes muted text on the paper surface
   (--ink-soft on --paper is 5.92:1, per the token comment at the top). */
.plan-modal-actions button:disabled {
  opacity: 1;
  background: var(--paper);
  color: var(--ink-soft);
  border-color: var(--line);
}

/* Bottom sheet on phones — the same 720px breakpoint at which the dish
   library (which launches this) becomes cards. A centred dialog puts its
   controls in the middle of the screen, which is the hardest place for a
   thumb to reach; anchored to the bottom edge, the day chips, meal buttons
   and confirm button are all in the easy arc, in the order you use them.
   `margin`, never `display` — see the note at the top of this section. */
@media (max-width: 720px) {
  .plan-modal {
    width: 100%;
    max-width: 100%;
    margin: auto auto 0;
    border-width: 1px 0 0;
    border-radius: 12px 12px 0 0;
  }

  .plan-modal-inner {
    /* Bottom padding clears the home-indicator gesture area on phones that
       have one; env() is 0 everywhere else. */
    padding: 16px 16px calc(16px + env(safe-area-inset-bottom));
  }
}

/* A phone held sideways (~360–430px tall). The sheet is 368px upright, which
   is comfortable; at 92dvh of a 360px-tall window there are only ~331px to
   have, and without this the overflow landed on the confirm button — the one
   control that must never be below the fold, since it is both the action and
   the sentence saying what the action will do. Measured at 740x360 and
   667x375: with these it fits and stops scrolling. Touch targets stay at or
   above 44px; that floor is the reason this only trims to 44/46 and not
   further. */
@media (max-height: 480px) {
  .plan-modal-inner {
    gap: 8px;
    padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
  }

  .plan-day-chip {
    min-height: 44px;
  }

  .plan-meal-btn {
    min-height: 46px;
  }

  .plan-modal-actions {
    padding-top: 8px;
  }
}

/* ---------- Dish picker (combobox) ----------
   Input + chevron + our own listbox. A native <input list> + <datalist>
   was tried first and is why this is hand-rolled: a datalist is invisible
   — the field reads as a plain text box, and browsers show nothing at all
   on first focus while it's empty, so the dish library looks like it's
   gone. This opens on focus (or the chevron) with the whole library
   showing, before a character is typed. */

.dish-picker {
  position: relative;
  display: flex;
  flex-wrap: wrap; /* lets .dish-picker-list.is-inline drop to its own line below */
  align-items: center;
  min-width: 0;
}

.dish-picker-input {
  width: 100%;
  padding-right: 26px; /* clear of the chevron */
}

.dish-picker-toggle {
  position: absolute;
  right: 1px;
  padding: 4px 6px;
  background: transparent;
  color: var(--ink-soft);
  border: none;
  font-size: 0.7rem;
  line-height: 1;
  cursor: pointer;
}

.dish-picker-toggle:hover {
  filter: none;
  color: var(--ink);
}

/* Fixed, with left/top/width/max-height written inline by positionList()
   in app.js — *not* absolute inside .dish-picker. A <dialog> has
   `overflow: auto` from the UA stylesheet, which clips an absolutely
   positioned child at the modal's edge; the list was rendering but cut
   off. Fixed escapes that clip (a dialog is `position: absolute`, so it
   isn't in the containing-block chain for fixed descendants) and still
   paints inside the dialog's top-layer stacking context, above the
   backdrop. Don't "simplify" this back to absolute. */
.dish-picker-list {
  position: fixed;
  z-index: 50;
  max-height: 260px;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: 4px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: 0 8px 24px rgba(32, 40, 31, 0.16);
}

/* Below the mobile breakpoint (app.js's inlineQuery), the list renders as
   a normal flow child instead of the fixed/floating box above.
   position:fixed while a mobile on-screen keyboard is up turned out to be
   a long-standing WebKit inconsistency in practice — a fixed element's
   actual on-screen position doesn't reliably track the visible viewport
   while the keyboard's opening/panning the page, and no amount of JS
   repositioning (window resize/scroll, visualViewport resize/scroll) held
   up against it on iOS Safari; the list kept ending up rendered over the
   input instead of next to it. Inline sidesteps the whole category — it's
   just the next box in the row, and the dialog scrolls it into view like
   anything else, no coordinates involved. flex-wrap on .dish-picker plus
   flex-basis 100% here is what pushes it onto its own line below the
   input+chevron instead of squeezing in beside them. */
.dish-picker-list.is-inline {
  position: static;
  flex: 1 0 100%;
  width: auto;
  margin-top: 4px;
  max-height: 45vh;
}

/* ...and while it's open the picker is two rows tall, which is a problem for
   the rows it sits in: both centre their items, so the course select and the
   Add/Remove button float down the side of the open list, out of line with the
   input they belong to. Top-align for exactly that state. Not unconditionally:
   with the list closed (and on every desktop open, where the list floats and
   costs no height) centring is right, because a button is a couple of px
   shorter than the two fields beside it. */
.slot-modal-add:has(> .slot-modal-dish-wrap .dish-picker-list.is-inline:not([hidden])),
.slot-modal-list li:has(> .dish-picker .dish-picker-list.is-inline:not([hidden])) {
  align-items: flex-start;
}

.dish-picker-option {
  padding: 6px 8px;
  border-radius: 4px;
  font-size: 0.85rem;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dish-picker-option:hover {
  background: var(--paper);
}

/* Keyboard highlight (arrow keys), which is a different thing from hover —
   solid, so it's unambiguous which row Enter will take. */
.dish-picker-option.is-active {
  background: var(--teal);
  color: white;
}

/* Nothing in the library matched. The picker is library-only — there's no
   free-text escape hatch to offer — so this says what to do instead;
   without it an empty list just reads as the picker being broken. Same
   type treatment as the options above, muted and italic to mark it as a
   message rather than something you can pick (it's `aria-disabled`). */
.dish-picker-empty {
  padding: 6px 8px;
  color: var(--ink-soft);
  font-size: 0.85rem;
  font-style: italic;
  cursor: default;
}

.slot-modal-add {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  border-top: 1px solid var(--line);
  padding-top: 14px;
}

/* ---------- Dish library ---------- */

/* Search leads, "Add dish" (which opens #dish-modal) trails: finding a
   dish is the frequent action here, adding one the occasional one, and the
   add form used to sit above the search box taking permanent space. */
.dish-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}

.dish-search {
  flex: 1 1 auto;
  max-width: 340px;
}

/* Desktop sorts by clicking a column header; this is the same control for
   the mobile card list, which has no headers to click. */
.dish-sort {
  display: none;
  flex: 1 1 auto;
  min-width: 0;
}

@media (max-width: 720px) {
  .dish-toolbar {
    flex-wrap: wrap;
  }

  .dish-search {
    flex: 1 1 100%;
    max-width: none;
  }

  .dish-sort {
    display: block;
  }
}

/* The category vocabulary is closed (CATEGORY_OPTIONS in app.js) but was
   only reachable by typing a category into the search box — chips are what
   make a fixed vocabulary worth having. */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}

.filter-chips:empty {
  display: none;
}

.chip {
  padding: 4px 10px;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--line);
  font-size: 0.8rem;
  font-weight: 500;
}

.chip:hover {
  filter: none;
  background: var(--paper);
  border-color: var(--ink-soft);
  color: var(--ink);
}

.chip.active {
  background: var(--teal);
  color: white;
  border-color: transparent;
}

.chip.active:hover {
  background: var(--teal);
  color: white;
}

/* A sortable column header. The <th>'s own mono/uppercase accent is
   inherited (font: inherit), so this only has to undo the button chrome. */
.th-sort {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  background: transparent;
  border: none;
  color: inherit;
  font: inherit;
}

.th-sort:hover {
  filter: none;
  color: var(--ink);
}

.th-sort-arrow {
  font-size: 0.6rem;
  color: var(--teal);
}

/* Read mode for an editable field: text, until clicked. The permanent
   input-per-field it replaces meant 40 live form controls on screen at 20
   dishes, and a stray click-and-type silently mutating a dish. */
.editable {
  display: block;
  min-width: 0;
}

.editable-view {
  display: block;
  width: 100%;
  padding: 6px 8px;
  text-align: left;
  background: transparent;
  color: var(--ink);
  border: 1px solid transparent;
  border-radius: 4px;
  font: inherit;
  font-weight: 400;
}

.editable-view:hover {
  filter: none;
  background: var(--paper);
  border-color: var(--line);
}

.editable-view.is-blank {
  color: var(--ink-soft);
}

.last-planned {
  white-space: nowrap;
}

.last-planned.is-never {
  color: var(--ink-soft);
  font-style: italic;
}

.times-planned {
  font-family: var(--mono);
  color: var(--ink-soft);
  text-align: center;
}

/* A disclosure, not a button in a column of its own — expanding a panel
   isn't the same weight of action as Delete beside it. */
.disclosure {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 8px;
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid transparent;
  font-weight: 500;
  white-space: nowrap;
}

.disclosure:hover {
  filter: none;
  background: var(--paper);
  color: var(--ink);
}

.disclosure-chevron {
  font-size: 0.7rem;
  line-height: 1;
}

@media (prefers-reduced-motion: no-preference) {
  .disclosure-chevron {
    transition: transform 0.15s ease;
  }
}

.disclosure[aria-expanded='true'] .disclosure-chevron {
  transform: rotate(90deg);
}

.dish-modal-fields {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.dish-modal-fields label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

.dish-modal-fields input,
.dish-modal-fields select {
  width: 100%;
}

.ingredients-row td {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  padding: 12px 10px 16px;
}

/* One editable row per ingredient (name / amount / unit / status / remove)
   — stacked, not chips, since each row now holds real form fields rather
   than a static label. */
.ingredient-list {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.ingredient-list li {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 4px 6px;
  font-size: 0.85rem;
}

.ingredient-list li.empty-state {
  border: none;
  background: none;
  padding: 4px 0;
}

.ingredient-edit-name {
  flex: 1 1 160px;
  min-width: 0;
}

.ingredient-edit-amount {
  flex: 0 1 80px;
  min-width: 0;
}

.ingredient-edit-unit {
  flex: 0 1 90px;
  min-width: 0;
}

.ingredient-remove {
  background: transparent;
  color: var(--ink-soft);
  border: none;
  border-radius: 3px;
  padding: 0 4px;
  font-size: 0.9rem;
  font-weight: 400;
  line-height: 1.4;
}

.ingredient-remove:hover {
  filter: none;
  color: var(--danger);
  background: rgba(154, 59, 42, 0.08);
}

.ingredient-add-form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}

.ingredient-add-form input {
  width: auto;
  flex: 1 1 160px;
}

.ingredient-add-form .ingredient-amount {
  flex: 0 1 90px;
}

.ingredient-add-form .ingredient-unit {
  flex: 0 1 90px;
}

/* ---------- Mobile Dish Library (cards) ----------
   Desktop shows the full table; below 720px a 6-column table has no room
   to breathe (same reasoning as the Calendar's grid->cards swap at the
   same breakpoint), so it's replaced entirely by one stacked card per dish.
   Both are always rendered (mountDishTable's renderTable()), so this is a
   pure CSS visibility switch, not a JS one. */

.dish-cards-mobile {
  display: none;
}

@media (max-width: 720px) {
  .dish-table-desktop {
    display: none;
  }

  .dish-cards-mobile {
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
}

.dish-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 12px;
}

.dish-card-fields {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.dish-card-field {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.dish-card-field-label {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

.dish-card-field input,
.dish-card-field select {
  width: 100%;
}

/* The card's read-mode value is the card's headline — bigger than the
   table's, since there's only one dish per card here. */
.dish-card-field .editable-view {
  padding: 4px 6px;
  font-size: 0.95rem;
}

.dish-card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.dish-card-meta .times-planned {
  text-align: left;
}

/* Wraps the same .ingredients-panel content buildRow()'s <td> houses on
   desktop (see buildIngredientsPanel's comment) — styled like
   .ingredients-row td since it's no longer an actual table cell. */
.dish-card-ingredients {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 10px;
}

.dish-card-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  border-top: 1px solid var(--line);
  padding-top: 10px;
}

/* ---------- Households & members ----------
   Per-household member/invite management inside Settings, and the
   standalone invite-accept prompt (shown in place of #auth-screen — see
   app.js's showInviteAccept()). The household switcher itself lives in the
   top bar — see .household-switcher in the layout section. */

.panel h3 {
  margin: 24px 0 4px;
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}

/* ---------- Plan panel (Settings) ----------
   Current plan plus what it's using, built by renderPlanPanel() in app.js
   from counts already in memory. Same bordered-row vocabulary as
   .members-list below, so the three Settings sections read as one thing. */
.plan-panel {
  margin: 0 0 14px;
}

.plan-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

/* The plan name is the one place in Settings that names a *tier*, so it
   gets the docket-label treatment (mono/uppercase) the section headings
   use — it reads as a stamp on the household rather than body text. */
.plan-name {
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 3px 10px;
  border-radius: 4px;
  border: 1px solid var(--line);
  color: var(--ink-soft);
  background: var(--card);
}

.plan-name-pro {
  border-color: var(--teal);
  background: var(--teal);
  color: #fff;
}

.plan-usage {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Two columns (label, count) with the bar spanning both underneath, so the
   numbers stay aligned however long the label is. */
.plan-usage-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 4px 10px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 4px;
  font-size: 0.85rem;
}

.plan-usage-count {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--ink-soft);
}

.plan-usage-bar {
  grid-column: 1 / -1;
  height: 3px;
  border-radius: 2px;
  background: var(--line);
  overflow: hidden;
}

.plan-usage-bar-fill {
  display: block;
  height: 100%;
  background: var(--teal);
}

/* At the limit the row says "· full" in text as well — the colour is the
   redundant half, not the only signal.

   --mustard, NOT --danger: being at the cap is the resting state for most
   free households, not a fault. Every free user owns exactly 1 of 1
   household and always will, and a couple sits at 2 of 2 members forever —
   painting those red says something is broken when nothing is. Mustard is
   already this app's "worth noticing" colour (the today marker, invites
   expiring soon), which is exactly the register wanted here. Red stays for
   the moment you actually try something and are blocked — the upgrade
   dialog. The count text keeps --ink-soft because #DE9C2E on white doesn't
   carry text at this size. */
.plan-usage-full .plan-usage-bar-fill {
  background: var(--mustard);
}

.members-list,
.pending-invites-list {
  list-style: none;
  margin: 0 0 14px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.members-list li,
.pending-invites-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: 4px;
  font-size: 0.85rem;
}

.members-empty {
  color: var(--ink-soft);
  font-style: italic;
  justify-content: flex-start !important;
}

.member-info {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.member-identity {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.member-you {
  font-weight: 400;
  font-size: 0.75rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.member-meta {
  font-size: 0.75rem;
  color: var(--ink-soft);
}

/* Invite expiring within EXPIRY_SOON_HOURS (app.js's formatExpiry) —
   reuses the --mustard "today" marker's amber as a general soon/urgency
   color rather than introducing a new token for it. */
.invite-expiring {
  color: var(--mustard);
  font-weight: 600;
}

.member-actions {
  display: flex;
  gap: 6px;
  flex: none;
}

/* Below 720px (the app's general mobile breakpoint — see the Breakpoints
   note at the top of this file) a member/invite row has no room to stay a
   single line: .member-actions (flex: none) refused to shrink, so it ate
   most of the row's width, .member-identity got clipped almost
   immediately, and .member-meta (no white-space rule) wrapped to 2-3
   ragged lines while the still vertically-centered action buttons read as
   uncentered against that ragged block. Stacking text above actions gives
   each its own full-width line instead. */
@media (max-width: 720px) {
  .members-list li,
  .pending-invites-list li {
    flex-direction: column;
    align-items: stretch;
  }

  .member-actions {
    justify-content: flex-end;
  }
}

.owner-controls {
  margin-top: 10px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}

/* Scoped to the id, not `.owner-controls .danger` — that descendant
   selector used to also match "Revoke" (.danger, buildInviteRow in app.js)
   inside #pending-invites-list, which also lives under #owner-controls.
   .member-actions has no align-items of its own (default: stretch), so
   Revoke's stray margin-top stretched its "Copy link" sibling 10px taller
   to match, leaving the two buttons in the same row visibly different
   heights instead of top-aligned. */
#delete-household-btn {
  margin-top: 10px;
}

.invite-link-wrap {
  display: flex;
  gap: 8px;
  margin: 10px 0;
}

.invite-link-input {
  flex: 1;
  font-family: var(--mono);
  font-size: 0.8rem;
}

/* ---------- Invite / confirm modals ---------- */
/* Shares #slot-modal's basic dialog chrome (border/radius/backdrop) —
   see the comment there — under its own class since these two are simple
   single-purpose forms rather than the slot editor's repeating-row list. */

.form-modal {
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 20px;
  width: min(420px, 92vw);
  background: var(--card);
  color: var(--ink);
}

.form-modal::backdrop {
  background: rgba(32, 40, 31, 0.45);
}

.form-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
}

.form-modal-header h3 {
  margin: 0;
  font-family: var(--sans);
  font-size: 1.05rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: normal;
}

.form-modal-hint {
  margin: 0 0 14px;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.form-modal-form label {
  display: block;
  margin-bottom: 12px;
}

.form-modal-form input[type='email'] {
  width: 100%;
  margin-top: 4px;
}

.form-modal-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.confirm-modal p {
  font-size: 0.9rem;
  margin: 0 0 18px;
}

.confirm-modal .form-modal-actions,
#prompt-modal .form-modal-actions {
  justify-content: flex-end;
}

/* Deliberately its own selector rather than widening the
   input[type='email'] rule above to type='text': #invite-link-input is also
   a text input inside a .form-modal-form, and it sits in a flex row beside
   its Copy button where `width: 100%` and a top margin would misalign it. */
.prompt-modal-input {
  width: 100%;
  margin-top: 4px;
}

/* readonly = the manual-copy fallback, which is always a link or token —
   mono so the characters are distinguishable while copying by hand. */
.prompt-modal-input[readonly] {
  font-family: var(--mono);
  font-size: 0.8rem;
}

/* Shown in place of the sign-in screen when this page was opened via an
   /invite/<token> link — see app.js's showInviteAccept() and
   pendingInviteToken. The centring and the heading come from .auth-screen
   (near the top of this file), which #invite-accept carries directly rather
   than through a wrapper, since app.js toggles that element's own `hidden`;
   all that's left here is the inner column's width. */
.invite-accept-panel {
  width: 100%;
  max-width: 420px;
}

.invite-accept-actions {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 16px;
}
