/* ---------- 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 {
  display: flex;
  align-items: center;
  min-width: 0;
  overflow: hidden;
}

.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;
}

/* ---------- 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;
}

/* `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;
}

/* ---------- 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;
  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);
}

.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);
}

.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;
}

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

.owner-controls .danger {
  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;
}
