/* Cheryl's Menu — base styles.
   Mobile-first and high contrast: the shopping view gets used one-handed under
   fluorescent supermarket light, so nothing here depends on hover and no tap
   target is under 44px. */

:root {
  --bg:        #ffffff;
  --bg2:       #f4f5f7;
  --border:    #d3d7de;
  --text:      #16181d;
  --text-mute: #5b6270;
  --accent:    #1a4d80;
  --accent-fg: #ffffff;
  --alert:     #9b1c1c;
  --alert-bg:  #fdecec;
  --radius:    8px;
  --tap:       44px;
}

* { box-sizing: border-box; }

/* The [hidden] attribute's browser-default display:none loses to ANY author
   rule that sets display on the same element -- author CSS always outranks
   the user-agent stylesheet, specificity ties or not. Anything in this file
   giving an element display:flex/grid/inline-flex (the overlay, buttons,
   toggles) will silently defeat el.hidden = true unless [hidden] is forced
   back to none here. This bit dishes.php's edit panel: it opened on every
   page load, un-closeable, because .panel-overlay { display:flex } beat the
   hidden attribute outright. One global rule closes the whole bug class
   rather than patching each element that happens to trip over it. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

h1 { font-size: 1.4rem; margin: 0 0 .5rem; }

.muted { color: var(--text-mute); }

a { color: var(--accent); }

/* ---------- layout ---------- */
.wrap {
  max-width: 900px;
  margin: 0 auto;
  padding: 1rem;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .75rem 1rem;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
}

.topbar .brand { font-weight: 700; }
/* The brand links to Home on every page except Home itself. Was four copies of
   an inline style="color:inherit;text-decoration:none" before inc/topbar.php
   consolidated the header. */
.topbar .brand a { color: inherit; text-decoration: none; }
.topbar nav { display: flex; align-items: center; gap: 1rem; }
.topbar nav a {
  text-decoration: none;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  font-size: .82rem;
}
.topbar .who { color: var(--text-mute); font-size: .9rem; font-weight: 400; }

/* shop.php puts its store picker in the topbar's left slot instead of the
   brand + user name. Styled to read as the page's one piece of state rather
   than as a nav control: accent-coloured, larger than the nav select beside
   it, and left-aligned where the brand was. */
.topbar-store-select {
  min-height: 40px;
  max-width: 55vw;
  padding: .3rem 1.6rem .3rem .6rem;
  font: inherit;
  font-size: .95rem;
  font-weight: 700;
  color: var(--accent);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* Mobile nav: the same fixed link set collapses into one dropdown instead of
   wrapping or scrolling -- swapped for the inline links below 640px (matches
   the breakpoint the edit-panel already uses). app.js wires the navigation. */
.topbar-nav-select { display: none; }
@media (max-width: 640px) {
  .topbar nav { display: none; }
  .topbar-nav-select {
    display: block;
    min-height: 40px;
    padding: .3rem 1.6rem .3rem .6rem;
    font: inherit;
    font-size: .82rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--text);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
  }
}

/* ---------- login ---------- */
.login-body {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background: var(--bg2);
  padding: 1rem;
}

.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.5rem;
}

.login-card h1 { text-align: center; margin-bottom: 1.25rem; }

/* The bare `label` rule below is display:block, which would stack the checkbox
   above its own text. Overridden here, and given a full tap-target height so
   the whole row is hittable on a phone rather than just the 16px box. */
.login-remember {
  display: flex;
  align-items: center;
  gap: .55rem;
  min-height: var(--tap);
  margin: .9rem 0 .25rem;
  cursor: pointer;
}

.login-remember input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
  flex: none;
  accent-color: var(--accent);
}

.login-remember span { font-size: .9rem; color: var(--text-mute); }

label {
  display: block;
  margin: .75rem 0 .25rem;
  font-size: .9rem;
  color: var(--text-mute);
}

input[type="text"],
input[type="password"] {
  width: 100%;
  min-height: var(--tap);
  padding: .5rem .7rem;
  font-size: 1rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

input:focus-visible,
button:focus-visible,
a:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.alert {
  margin: 0 0 1rem;
  padding: .6rem .75rem;
  font-size: .9rem;
  color: var(--alert);
  background: var(--alert-bg);
  border: 1px solid var(--alert);
  border-radius: var(--radius);
}

/* ---------- misc ---------- */
.stats {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  list-style: none;
  margin: 1.25rem 0 0;
  padding: 0;
}

.stats li {
  flex: 1 1 120px;
  padding: .75rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-mute);
  font-size: .9rem;
}

.stats strong { display: block; font-size: 1.5rem; color: var(--text); }

/* Any table anywhere scrolls inside its own container rather than pushing the
   page sideways. Con101 shipped that bug five separate times. */
.table-wrap { overflow-x: auto; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  min-height: var(--tap);
  padding: .5rem 1rem;
  font-size: .95rem;
  font-weight: 600;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}
/* MUST stay after .btn. Both are single-class selectors, so specificity ties
   and the later rule wins -- while this block sat ABOVE .btn, .btn's plain
   background overrode the accent and every `class="btn btn-primary"` button
   rendered white. That was index.php's chat Send button, reported 2026-08-28,
   and it is the second time this exact tie has bitten (CLAUDE.md gotcha 4a).
   login.php uses .btn-primary WITHOUT .btn, which is why Sign in always looked
   right and this stayed hidden. */
.btn-primary {
  width: 100%;
  min-height: var(--tap);
  margin-top: 1.25rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--accent-fg);
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  cursor: pointer;
}
.btn-accent { color: var(--accent-fg); background: var(--accent); border-color: var(--accent); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-mute); }
.btn-danger { color: var(--alert); border-color: var(--alert); background: var(--alert-bg); }
.btn-sm { min-height: 36px; padding: .35rem .7rem; font-size: .85rem; }
.btn:disabled { opacity: .5; cursor: default; }

/* ---------- toolbar / filters ---------- */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  align-items: center;
  margin: 1rem 0;
}
.toolbar input[type="text"],
.toolbar input[type="search"],
.toolbar select {
  min-height: var(--tap);
  padding: .5rem .6rem;
  font-size: .95rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.toolbar .search-input { flex: 1 1 200px; }
.toolbar .spacer { flex: 1 1 auto; }

/* plan.php's search box grows to fill the toolbar row, same as the shared
   default above -- the type + sort selects stay their own natural width
   (flex-grow:0, unset), so the search field is what absorbs the row's spare
   width instead of everything splitting it evenly. min-width:0 keeps it able
   to shrink below its intrinsic content size on a narrower window instead of
   forcing the row to wrap (the same trap noted on .plan-grid above). */
.page-plan .plan-dishes .search-input { flex: 1 1 200px; min-width: 0; }

.check-inline {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-size: .9rem;
  color: var(--text-mute);
  min-height: var(--tap);
}

/* ---------- dish cards ---------- */
.dish-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: .75rem;
}

.dish-card {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: .85rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  text-align: left;
}
.dish-card:hover { border-color: var(--accent); }
.dish-card.inactive { opacity: .55; }

.dish-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
}
.dish-card-name { font-weight: 700; }

.badge {
  display: inline-block;
  padding: .1rem .5rem;
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  border-radius: 999px;
  color: var(--text-mute);
  background: var(--bg);
  border: 1px solid var(--border);
  white-space: nowrap;
}
/* var(--accent), not a second copy of the accent hex -- these were the same
   colour written twice, and the palette change to dark blue found the drift.
   The other badge hues stay literal: they are their OWN colours, deliberately
   distinct from the accent so dish types read apart at a glance. */
.badge-main { color: var(--accent); border-color: var(--accent); }
.badge-side { color: #8a6d00; border-color: #8a6d00; }
.badge-dessert { color: #9b1c6b; border-color: #9b1c6b; }
.badge-other { color: var(--text-mute); }
.badge-staple { color: var(--accent); border-color: var(--accent); }
.badge-review { color: var(--alert); border-color: var(--alert); background: var(--alert-bg); }

.dish-card-meta { font-size: .82rem; color: var(--text-mute); }
.dish-card-inactive-tag { font-size: .78rem; color: var(--alert); }

.empty-state {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--text-mute);
}

/* ---------- edit panel ---------- */
.panel-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 12, 16, .35);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 50;
}
@media (min-width: 640px) {
  .panel-overlay { align-items: center; }
}

.edit-panel {
  width: 100%;
  max-width: 640px;
  max-height: calc(100vh - 2rem);
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
  overflow: hidden;
}
@media (min-width: 640px) {
  .edit-panel { border-radius: var(--radius); max-height: calc(100vh - 4rem); }
}

.edit-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.edit-panel-header h2 { margin: 0; font-size: 1.1rem; }

.edit-panel-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 1rem;
  min-height: 0;
}

.edit-panel-footer {
  display: flex;
  gap: .6rem;
  padding: 1rem;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}
.edit-panel-footer .spacer { flex: 1 1 auto; }

.form-row { margin-bottom: .9rem; }
.form-row label {
  display: block;
  margin-bottom: .3rem;
  font-size: .85rem;
  color: var(--text-mute);
}
.form-row input[type="text"],
.form-row input[type="url"],
.form-row select,
.form-row textarea {
  width: 100%;
  min-height: var(--tap);
  padding: .5rem .6rem;
  font-size: .95rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: inherit;
}
.form-row textarea { min-height: 4.5rem; resize: vertical; }
.hint { margin-top: .3rem; font-size: .82rem; color: var(--text-mute); }

/* URL + Fetch button. min-width:0 on the flex child is required or the input
   refuses to shrink below its intrinsic width and pushes the button off. */
.url-import-row { display: flex; gap: .5rem; align-items: stretch; }
.url-import-row input { flex: 1 1 auto; min-width: 0; }
.url-import-row .btn { flex: 0 0 auto; }


.form-cols-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .75rem;
}

/* ---------- ingredient rows ---------- */
.ing-rows { display: flex; flex-direction: column; gap: .6rem; margin-bottom: .5rem; }

.ing-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .4rem .5rem;
  padding: .6rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.ing-row-name { grid-column: 1; position: relative; }
.ing-row-remove { grid-column: 2; }

/* An imported line that resolved to nothing in the catalog. MUST come after
   .ing-row: both are single-class selectors, so on a specificity tie the
   later rule wins -- defined above .ing-row, its background and border were
   silently overridden and the highlight never rendered at all. Same trap as
   the [hidden] gotcha in CLAUDE.md, different victim.
   Colour alone is not the signal: .ing-row-unmatched-tag spells it out, so
   this reads correctly in greyscale and to anyone who doesn't parse a pink
   background as "needs work". */
.ing-row.ing-row-unmatched {
  background: var(--alert-bg);
  border-color: var(--alert);
  border-left: 4px solid var(--alert);
}

.ing-row-unmatched-tag {
  display: inline-block;
  padding: .1rem .45rem;
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--alert);
  background: var(--bg);
  border: 1px solid var(--alert);
  border-radius: 999px;
  white-space: nowrap;
}

.ing-row-fields {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: .4rem;
}
@media (max-width: 480px) {
  .ing-row-fields { grid-template-columns: 1fr 1fr; }
}

.ing-row-fields input {
  width: 100%;
  min-height: 40px;
  padding: .4rem .5rem;
  font-size: .85rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.ing-row-fields input::placeholder { color: var(--text-dim, #999); font-size: .78rem; }

.ing-row-extra {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
}

.ing-row-cat-fix {
  display: flex;
  align-items: center;
  gap: .35rem;
  font-size: .78rem;
  color: var(--accent);
}
/* Store select and the purchase-unit input joined category/staple here in
   Phase 1c, so this rule covers inputs too -- an unstyled text input next to
   three styled selects reads as a different kind of control. .ing-row-extra
   already wraps, which is what keeps four controls plus the suggestion line
   usable on a phone. */
.ing-row-cat-fix select,
.ing-row-cat-fix input[type="text"] {
  min-height: 32px;
  font-size: .8rem;
  padding: .1rem .3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
}
/* The suggestion line and its accept button sit on their own full-width row
   rather than wrapping into the controls, so "Use suggestion" is never
   mistaken for one more field. */
.ing-suggest-slot {
  flex: 1 1 100%;
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
}
.ing-suggest-slot .hint { margin-top: 0; }

.ing-remove-btn {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--alert);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}

/* ---------- type-ahead ---------- */
.typeahead-wrap { position: relative; }

.typeahead-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  max-height: 260px;
  overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 6px 16px rgba(0, 0, 0, .15);
}

.typeahead-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  width: 100%;
  min-height: 40px;
  padding: .5rem .7rem;
  font-size: .9rem;
  text-align: left;
  color: var(--text);
  background: var(--bg);
  border: none;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.typeahead-item:last-child { border-bottom: none; }
.typeahead-item:hover { background: var(--bg2); }
.typeahead-cat { font-size: .74rem; color: var(--text-mute); }
.typeahead-create { color: var(--accent); font-weight: 600; }
.typeahead-label {
  padding: .4rem .7rem;
  font-size: .78rem;
  color: var(--text-mute);
  border-bottom: 1px solid var(--border);
}

/* ---------- toast ---------- */
.toast-host {
  position: fixed;
  bottom: 1.5rem;
  right: 1rem;
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  max-width: calc(100vw - 2rem);
}
.toast {
  padding: .65rem .9rem;
  font-size: .9rem;
  color: var(--accent-fg);
  background: var(--text);
  border-radius: var(--radius);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .18s ease, transform .18s ease;
  box-shadow: 0 4px 12px rgba(0, 0, 0, .2);
}
.toast.toast-in { opacity: 1; transform: translateY(0); }
.toast-error { background: var(--alert); }

/* ---------- three-column shell (index.php) ---------- */
.app-grid {
  display: grid;
  /* Lists and chat split the remaining space evenly; the rolling plan keeps a
     fixed width sized for a day tile's three slot rows. */
  grid-template-columns: 1fr 1fr 320px;
  gap: 1rem;
  align-items: start;
  max-width: 1400px;
  margin: 0 auto;
  padding: 1rem;
}

/* Mobile: single column, chat first -- it's the primary interaction
   (BUILD-PLAN section 1), so on a phone it leads rather than getting
   scrolled past. Shopping has its own dedicated page (shop.php). */
@media (max-width: 960px) {
  .app-grid {
    grid-template-columns: 1fr;
  }
  .col-chat { order: 1; }
  .col-plan { order: 2; }
  .col-lists { order: 3; }
}


.col-title {
  margin: 0 0 .6rem;
  font-size: .78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-mute);
}

/* ---------- left column: permanent per-store lists ---------- */
.store-section {
  margin-bottom: .5rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.store-section > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: var(--tap);
  padding: .6rem .8rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none;
}
.store-section > summary::-webkit-details-marker { display: none; }
.store-section > summary::before {
  content: '\25B8';
  margin-right: .5rem;
  color: var(--text-mute);
  transition: transform .15s ease;
}
.store-section[open] > summary::before { transform: rotate(90deg); }
.store-count {
  font-weight: 400;
  font-size: .82rem;
  color: var(--text-mute);
}
.store-body {
  padding: .5rem .8rem .8rem;
  border-top: 1px solid var(--border);
}
.store-empty {
  padding: .5rem 0;
  font-size: .85rem;
  color: var(--text-mute);
}

/* ---------- centre column: chat (Phase 5a) ---------- */
.chat-log {
  min-height: 220px;
  max-height: 52vh;
  overflow-y: auto;
  padding: .75rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.chat-empty {
  margin: auto;
  text-align: center;
  color: var(--text-mute);
  font-size: .9rem;
  padding: 1.5rem 1rem;
}

.chat-msg {
  max-width: 85%;
  padding: .5rem .7rem;
  border-radius: var(--radius);
  font-size: .92rem;
  line-height: 1.45;
  white-space: normal;
  overflow-wrap: anywhere; /* a long ingredient list must not widen the column */
}

.chat-msg-user {
  align-self: flex-end;
  background: var(--accent);
  color: var(--accent-fg);
}

.chat-msg-assistant {
  align-self: flex-start;
  background: var(--bg);
  border: 1px solid var(--border);
}

.chat-choice {
  margin-top: .6rem;
  padding: .7rem;
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
}

.chat-choice-prompt {
  font-weight: 600;
  font-size: .92rem;
  margin-bottom: .5rem;
}

.chat-choice-options {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
}

/* Tap targets, not text links -- same reasoning as the staple pills. */
.choice-chip {
  min-height: 44px;
  padding: .45rem .8rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: inherit;
  font: inherit;
  font-size: .9rem;
  cursor: pointer;
}

.choice-chip:hover { border-color: var(--accent); }
.choice-chip:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.choice-chip-escape { color: var(--text-mute); font-style: italic; }
.choice-hint { color: var(--text-mute); font-size: .8rem; }
.chat-msg-user .choice-hint { color: inherit; opacity: .8; }

.chat-form {
  display: flex;
  gap: .5rem;
  margin-top: .6rem;
  align-items: flex-end;
}

.chat-form textarea {
  flex: 1;
  min-width: 0; /* without this the textarea refuses to shrink in the flex row */
  resize: vertical;
  padding: .5rem .6rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font: inherit;
  /* 16px exactly: iOS zooms the whole page when a focused input is under
     16px, and this one is focused constantly on a phone. */
  font-size: 1rem;
}

.chat-form textarea:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }
/* .btn-primary carries width:100% and a margin-top for the login form's
   full-width submit. Dropped into a flex row it squeezes the textarea to ~20px
   at 375px, which is unusable on the column that is PRIMARY on mobile. Same
   override .url-import-row already needs, for the same reason. */
.chat-form button {
  flex: 0 0 auto;
  width: auto;
  margin-top: 0;
  min-height: 44px;
  white-space: nowrap;
}
.chat-form textarea:disabled, .chat-form button:disabled { opacity: .6; }

/* ---------- right column: rolling day tiles ---------- */
.day-tiles {
  display: flex;
  flex-direction: column;
  gap: .6rem;
}

.day-tile {
  padding: .7rem .8rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  text-align: left;
  width: 100%;
}
.day-tile:hover { border-color: var(--accent); }
.day-tile.is-today { border-color: var(--accent); border-width: 2px; }

.day-tile-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: .4rem;
}
.day-tile-date { font-weight: 700; }
.day-tile-today-tag {
  font-size: .7rem;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--accent);
}

.day-tile-note {
  margin-bottom: .4rem;
  padding: .3rem .5rem;
  font-size: .8rem;
  font-style: italic;
  color: var(--text-mute);
  background: var(--bg);
  border-radius: var(--radius);
}

.slot-row {
  display: flex;
  gap: .5rem;
  padding: .2rem 0;
  font-size: .85rem;
}
.slot-label {
  flex: 0 0 5.5rem;
  color: var(--text-mute);
}
.slot-value { flex: 1 1 auto; }
.slot-empty { color: var(--text-dim, #9aa0ab); }
.slot-dining-out { color: var(--accent); }

/* ---------- calendar.php ---------- */

.cal-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .75rem;
  margin-bottom: 1rem;
}
.cal-mode-toggle { display: flex; gap: .4rem; }
/* .is-active is set by JS, both on click and once on load (matching the CSS
   @media default via matchMedia -- see calendar.php) so the pressed button
   always agrees with which view is actually visible. */
.cal-mode-btn.is-active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }

.cal-month-nav { align-items: center; gap: .5rem; display: none; }
.cal-month-label { font-weight: 700; min-width: 9rem; text-align: center; }

/* Default visibility by viewport, mirroring the topbar's own nav-vs-dropdown
   split (app.css ~line 76) -- CSS decides before first paint, no JS device
   check and no flash. */
@media (max-width: 768px) {
  .cal-agenda { display: block; }
  .cal-month { display: none; }
}
@media (min-width: 769px) {
  .cal-agenda { display: none; }
  .cal-month { display: block; }
  .cal-month-nav { display: flex; }
}

/* An explicit tap (JS sets data-view on #cal-main, the shared ancestor of
   both the toolbar and the view panels -- not on either of them directly,
   which is what made an earlier version of this rule depend on DOM order
   between siblings and silently never match) always overrides the default
   above, via !important beating an unqualified @media rule regardless of
   which one is later in the file. */
#cal-main[data-view="agenda"] .cal-month,
#cal-main[data-view="agenda"] .cal-month-nav { display: none !important; }
#cal-main[data-view="agenda"] .cal-agenda { display: block !important; }
#cal-main[data-view="month"] .cal-agenda { display: none !important; }
#cal-main[data-view="month"] .cal-month { display: block !important; }
#cal-main[data-view="month"] .cal-month-nav { display: flex !important; }

/* ---------- agenda mode ---------- */
.cal-agenda-list { display: flex; flex-direction: column; gap: .5rem; }
/* .day-tile was written for a <button> (index.php); reused here on an <a>
   for the days that link out, so its default anchor styling needs undoing. */
a.cal-agenda-row { display: block; color: inherit; text-decoration: none; }
.cal-agenda-row:not(a) { cursor: default; }
.cal-agenda-row:not(a):hover { border-color: var(--border); }

.cal-dish-side { color: var(--text-mute); }

/* A dish with a recipe link, inside a day cell/row that is ITSELF a link to
   the day editor -- underline + a small glyph is the visible signal that
   tapping this specific word does something different (opens the recipe)
   from tapping the rest of the row (opens the day). Bigger tap target than
   the text alone so it's easy to hit on a phone mid-cook, the actual reason
   this exists. */
.cal-dish-link {
  text-decoration: underline;
  text-underline-offset: .15em;
  cursor: pointer;
  display: inline-block;
  padding: .2rem 0;
}
.cal-dish-link::after { content: ' \2197'; font-size: .82em; }
.cal-dish-link:hover { color: var(--accent); }

/* ---------- month mode ---------- */
.cal-month-wrap { overflow-x: auto; }
.cal-weekday-row {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  /* gap and horizontal padding MUST match .cal-month-grid's gap and border, or
     the two grids compute different column widths and the labels drift out of
     line with the cells they name -- ~3px per column by the right-hand edge. */
  gap: 2px;
  padding: 0 1px .3rem;
  min-width: 640px;
  font-size: .78rem;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--text-mute);
}
.cal-weekday-row span { text-align: center; }

.cal-month-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  min-width: 640px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.cal-month-cell {
  display: block;
  /* A grid item defaults to min-width:auto -- never narrower than its own
     contents -- which lets one long dish name widen the whole column past its
     1fr share and knock the grid out of line with the weekday header. */
  min-width: 0;
  min-height: 84px;
  padding: .3rem .35rem;
  background: var(--bg);
  color: inherit;
  text-decoration: none;
}
a.cal-month-cell:hover { background: var(--bg2); }
.cal-month-cell:not(a) { cursor: default; }
.cal-month-cell.is-outside-month { background: var(--bg2); }
.cal-month-cell.is-outside-month .cal-month-daynum { color: var(--text-mute); }
.cal-month-cell.is-today { box-shadow: inset 0 0 0 2px var(--accent); }

.cal-month-cell-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: .2rem;
}
.cal-month-daynum { font-size: .8rem; font-weight: 700; }
.cal-month-note-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
}
.cal-month-dish {
  font-size: .72rem;
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* In a month cell the recipe link and the dish row are the SAME element --
   entrySpanAttrs() merges both classes onto one div -- so .cal-dish-link's
   inline-block has to be undone here. inline-block contributes its full text
   width as the column minimum, which defeated .cal-month-dish's ellipsis AND
   pushed the columns out of alignment with the header row above. Reported
   2026-08-28; measured before/after with getBoundingClientRect, not by eye.
   The padding goes too: the tap-target padding is for the agenda list, where
   the link really is its own inline element. */
.cal-month-dish.cal-dish-link {
  display: block;
  padding: 0;
}

.cal-month-more { font-size: .7rem; color: var(--text-mute); }

/* ---------- day-edit modal ---------- */
.slot-edit-block {
  margin-bottom: 1.1rem;
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--border);
}
.slot-edit-block:last-child { border-bottom: none; }

.slot-edit-title {
  margin: 0 0 .5rem;
  font-weight: 700;
  font-size: .9rem;
}

.entry-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .4rem .5rem;
  margin-bottom: .3rem;
  background: var(--bg2);
  border-radius: var(--radius);
  font-size: .88rem;
}

/* Dining Out auto-adds with no note; this is the tap-to-add-it-afterward
   affordance (BUILD-PLAN: less frequent, so the extra tap is an OK trade). */
.entry-row-editable {
  cursor: pointer;
  border-bottom: 1px dotted var(--text-mute);
}
.entry-row .entry-note-input {
  flex: 1 1 auto;
  min-height: 36px;
  padding: .3rem .5rem;
  font-size: .85rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.entry-add-row {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
  margin-top: .4rem;
}
.entry-add-row select,
.entry-add-row input[type="text"],
.entry-add-row input[type="number"] {
  min-height: 40px;
  padding: .4rem .5rem;
  font-size: .85rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.entry-add-row .dish-pick-wrap { flex: 1 1 160px; position: relative; }
.entry-add-row .qty-input { width: 70px; }

.day-note-row textarea {
  width: 100%;
  min-height: 4rem;
  padding: .5rem .6rem;
  font-size: .9rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: inherit;
  resize: vertical;
}

/* ---------- left column: shopping list items ---------- */
.quick-add-row { margin-bottom: .75rem; }
.quick-add-row input {
  width: 100%;
  min-height: var(--tap);
  padding: .5rem .7rem;
  font-size: .9rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.list-item-row {
  padding: .5rem 0;
  border-bottom: 1px solid var(--border);
}
.list-item-row:last-of-type { border-bottom: none; }
.list-item-row.is-acquired { opacity: .55; }

/* The 3s "bought it" undo window, and the fade that closes it. Same timings AND
   the same line-through as shop.php's acquired flow (.shop-item.is-fading,
   .shop-item-row.is-acquired) because it is the same action on the same status
   -- see the BOUGHT_WINDOW_MS comment in index.php. */
.list-item-row { transition: opacity .4s ease; }
.list-item-row.is-leaving { opacity: .5; }
.list-item-row.is-leaving .list-item-name { text-decoration: line-through; font-weight: 400; }
.list-item-row.is-fading { opacity: 0; }

.list-item-main {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem;
  font-size: .88rem;
}
.list-item-main .check-inline { flex: 1 1 auto; min-height: auto; gap: .5rem; }
.list-item-name { font-weight: 600; }
.is-acquired .list-item-name { text-decoration: line-through; font-weight: 400; }
.list-item-qty { color: var(--text-mute); font-size: .82rem; }

.staple-tag, .acquired-tag {
  padding: .05rem .4rem;
  font-size: .7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .02em;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-mute);
}
.acquired-tag { color: var(--accent); border-color: var(--accent); }

.item-sources {
  margin: .2rem 0 0 1.7rem;
  font-size: .78rem;
  color: var(--text-mute);
}

.list-item-controls {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: .35rem 0 0 1.7rem;
}
.item-store-select {
  min-height: 34px;
  padding: .15rem .4rem;
  font-size: .8rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}


/* ---------- shop.php: one store at a time, mobile-first ---------- */
.shop-sticky-head {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--bg);
}

/* "Bought" and quick-add share one band. They were two stacked rows, which
   with the two rows of store tabs above them meant four headers before the
   first item. The checkbox keeps the left edge it already had; the input
   takes whatever is left. */
.shop-controls {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .4rem .8rem;
  border-bottom: 1px solid var(--border);
}
.shop-show-acquired {
  display: flex;
  align-items: center;
  gap: .35rem;
  color: var(--text-mute);
  font-size: .82rem;
  white-space: nowrap;
  flex: 0 0 auto;
}
/* Same 16px rule as the chat input: anything under 1rem makes iOS zoom the
   page every time she taps it mid-aisle. */
.shop-controls #shop-quick-add {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  padding: .45rem .7rem;
  font-size: 1rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* Desktop: match the width the item list centres to. */
@media (min-width: 961px) {
  .shop-controls { max-width: 640px; margin: 0 auto; }
}

.shop-list {
  padding: .25rem .8rem 3rem;
  max-width: 640px;
  margin: 0 auto;
}

/* One item = the tappable row plus its optional "from: ..." line, and the
   dividing rule belongs around that pair. It used to sit on .shop-item-row
   while the source line was rendered as the row's next sibling, which drew
   the line between an item's name and its own source line -- so every source
   line read as belonging to the item below it. Rule between items only, none
   above the first or below the last. */
.shop-item + .shop-item { border-top: 1px solid var(--border); }
.shop-item { transition: opacity .4s ease; }
.shop-item.is-fading { opacity: 0; }

/* The whole row is the tap target -- there is no separate checkbox or button
   to hit. "Already have" is a planning-time decision made on index.php;
   standing in an aisle there's exactly one thing to tap this row for. */
.shop-item-row {
  display: flex;
  align-items: center;
  gap: .6rem;
  /* 44px is the iOS tap-target floor, and the whole row is the target. It was
     56px, which on a 375px screen bought comfort nobody asked for at the cost
     of roughly two items per screenful. */
  min-height: 44px;
  padding: .15rem 0;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}
.shop-item-row.is-acquired .shop-item-name { text-decoration: line-through; color: var(--text-mute); }

.shop-item-checkmark {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border: 2px solid var(--border);
  border-radius: 6px;
  position: relative;
}
.is-acquired .shop-item-checkmark {
  background: var(--accent);
  border-color: var(--accent);
}
.shop-item-checkmark::after {
  content: '';
  position: absolute;
  left: 5px;
  top: 2px;
  width: 10px;
  height: 6px;
  border-left: 3px solid var(--accent-fg);
  border-bottom: 3px solid var(--accent-fg);
  transform: rotate(-45deg);
  opacity: 0;
}
.is-acquired .shop-item-checkmark::after { opacity: 1; }

.shop-item-text { display: flex; align-items: baseline; flex-wrap: wrap; gap: .5rem; flex: 1 1 auto; }
.shop-item-name { font-weight: 600; font-size: .95rem; }
.shop-item-qty { font-size: .82rem; color: var(--text-mute); }
.shop-item-tags { display: flex; gap: .3rem; }
/* Indent to clear the checkmark (24px) plus the row's .6rem gap.
   The negative top margin is the point: the row is 44px tall for the tap
   target and centres its name, which left ~10.6px of dead air under the name
   against ~3.2px below the dish line -- so the dish line sat nearer the
   divider than the item it belongs to, and read as detached. Pulling it up
   inverts that to ~4.2px above / ~7.2px below, which groups it with its own
   item. Measured in the browser, not guessed; a wrapped item name has ~24px
   of clearance, so nothing collides. */
.shop-item-sources {
  font-size: .74rem;
  line-height: 1.3;
  color: var(--text-mute);
  margin-left: 2.1rem;
  margin-top: -.4rem;
  padding-bottom: .45rem;
}

/* ---------- Confirm You Have ---------- */
.staples-block {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-top: .75rem;
  padding: .7rem .8rem;
  background: var(--bg2);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  font-size: .85rem;
}
.staples-label { color: var(--text-mute); flex: 0 0 auto; }
.staples-pills { display: flex; flex-wrap: wrap; gap: .4rem; }
/* Pill geometry, shared by the staple names and the Clear control so the whole
   block reads as one row of pills. Colour is set per role BELOW this rule --
   those are single-class selectors too, so a tie is broken by source order and
   moving them above this would silently blank them out (the .btn/.btn-primary
   note further up is the same trap, which this file has now hit twice). */
.staple-name-btn,
.staples-clear-pill {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: .25rem .75rem;
  font: inherit;
  font-size: .8rem;
  font-weight: 600;
  background: var(--bg);
  border-radius: 999px;
  white-space: nowrap;
  cursor: pointer;
}

/* Accent: tap it and this joins the shopping list. */
.staple-name-btn { color: var(--accent); border: 1px solid var(--accent); }
.staple-name-btn:hover { background: var(--accent); color: var(--accent-fg); }

/* The Clear sweep. Its own line under the pills -- "below the list", not one
   more thing sitting in it. .staples-block is a wrapping flex row, so a
   full-basis child is what breaks the line. */
.staples-clear-row {
  flex: 0 0 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
}
.staples-clear-confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
}
.staples-clear-ask { color: var(--alert); font-weight: 600; }

/* The theme's button colour, same as .btn-accent / .btn-primary -- so this
   repaints with the accent preset chosen on the Admin page rather than being
   a colour of its own. FILLED, where the staple pills beside it are outlined:
   they share the accent because both belong to this block, and the fill is
   what keeps "clear all of these" from looking like a seventh thing to tap
   onto the shopping list.

   White-on-accent is safe in every preset -- scripts/check_palette.php holds
   all six at 7:1 or better, which is exactly the contract --accent-fg is
   maintained under. Hover darkens with a filter rather than a second colour,
   since there is no per-preset "darker accent" to name. */
.staples-clear-pill {
  color: var(--accent-fg);
  background: var(--accent);
  border: 1px solid var(--accent);
}
.staples-clear-pill:hover { filter: brightness(1.15); }

/* Cancel is the one pill here that must NOT carry the accent: it is the way
   out, and painting the escape like the thing being escaped is how a confirm
   step gets clicked through without being read. */
.staples-clear-pill.is-quiet {
  color: var(--text-mute);
  background: var(--bg);
  border-color: var(--border);
}
.staples-clear-pill.is-quiet:hover { color: var(--text); background: var(--bg2); filter: none; }

/* The commit, and the only tap that deletes anything -- red, matching the
   .btn-danger confirm on prompt.php's merge. Deliberately NOT the accent:
   the accent opened the step, and the button that actually does it should not
   look like the button that only asked. --alert is fixed across presets. */
.staples-clear-pill.is-danger { color: var(--accent-fg); background: var(--alert); border-color: var(--alert); }

/* .btn:disabled no longer applies now that this is its own pill, and the
   confirm button disables itself for the length of the request. */
.staples-clear-pill:disabled { opacity: .5; cursor: default; }

/* Phone: the home page is ONLY the chat.
   Chat is the primary interaction (BUILD-PLAN section 1), and on a phone the
   stacked three-column layout buried it above a plan and five store accordions
   she had to scroll past. Neither column is lost -- the menu is calendar.php
   (mobile-default agenda, whose day links deep-link back here via ?open=DATE,
   still working because the edit modal is position:fixed and renders from
   PLAN_DATA rather than from the hidden tiles) and shopping is shop.php.

   768px, not the 960px stacking breakpoint: a tablet can still scroll a stacked
   plan usefully, a phone cannot.

   ⚠ This block MUST stay after the .chat-log rules above. Media queries add no
   specificity, so when it sat up beside .app-grid the later plain .chat-log
   { max-height: 52vh } beat the override and the log measured 422px instead of
   filling the screen -- the same "later single-class selector wins" trap
   already in CLAUDE.md.

   ⚠ NO height arithmetic here, deliberately. The first version used
   calc(100vh - 13rem) and measured perfectly in an emulated 375x812 viewport,
   because an emulator has no browser chrome. On a real phone 100vh INCLUDES the
   area behind the URL bar, so the subtraction came up short, the page ran taller
   than the visible screen and the nav scrolled off the top. The flex shell below
   has no magic number to be wrong: the log simply takes whatever room is left. */
@media (max-width: 768px) {
  .col-lists,
  .col-plan { display: none; }

  /* Fill the visible screen without measuring it.
     height, NOT min-height: a min-height shell can grow past the viewport, and
     then the log grows with it instead of scrolling -- measured at 2298px tall
     with 40 messages, pushing the input clean off the screen. A definite height
     is what gives .chat-log a fixed box to scroll inside.
     dvh after a vh fallback: dvh tracks the browser chrome, vh does not. */
  .page-home { height: 100vh; height: 100dvh; display: flex; flex-direction: column; }

  /* Safety net: if anything ever does overflow, the nav stays reachable instead
     of scrolling away. Scoped to this page -- shop.php has its own sticky head
     at top:0 that a global sticky topbar would collide with. */
  .page-home .topbar { position: sticky; top: 0; z-index: 20; }

  .page-home .app-grid {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: .75rem;
  }

  .page-home .col-chat {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }

  /* min-height:0 is load-bearing on both: a flex item defaults to min-height
     auto and refuses to shrink below its content, which is what turns an
     overflowing log into a taller page instead of an internal scroll. */
  .page-home .chat-log {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
    height: auto;
  }

  .page-home .col-title,
  .page-home .col-title-row,
  .page-home .chat-choice,
  .page-home .chat-form { flex: 0 0 auto; }
}

/* ---------- house rules page (prompt.php) ---------- */
.prompt-intro { margin: 0 0 1rem; font-size: .92rem; }

.prompt-box {
  width: 100%;
  min-height: 16rem;
  padding: .7rem .8rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font: inherit;
  /* 16px: under that, iOS zooms the page the moment this is focused. */
  font-size: 1rem;
  line-height: 1.5;
  resize: vertical;
}
.prompt-box:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }

.prompt-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin-top: .6rem;
}
/* .btn-accent is content-sized; .btn-primary would stretch to 100% here for the
   same reason it did in the chat row. */
.prompt-footer .btn { flex: 0 0 auto; }

.prompt-meta { margin: 1rem 0 0; font-size: .85rem; }

/* Admin page now carries two unrelated sections (Appearance, House Rules), so
   each needs its own heading under the page's single h1. */
.admin-section-title {
  margin: 2rem 0 .4rem;
  font-size: 1.05rem;
}
.admin-section-title:first-of-type { margin-top: 1rem; }

/* Accent swatches. The real radio stays in the DOM (focusable, keyboard
   arrow-navigable, works with no JS) and is visually replaced by the chip --
   position/opacity rather than display:none, which would drop it out of the
   tab order entirely. */
.swatch-row {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
}
.swatch {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .35rem;
  padding: .5rem .6rem;
  border: 2px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  background: var(--bg);
  min-width: 4.5rem;
}
.swatch input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}
.swatch-chip {
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  border: 1px solid rgba(0, 0, 0, .15);
}
.swatch-label { font-size: .82rem; color: var(--text-mute); }
.swatch:hover { border-color: var(--text-mute); }
/* Selection is a border + a bolder label, never colour alone -- the chips are
   already six different colours, so a colour-only "which is active" cue would
   be exactly the half-signal this file warns about elsewhere. */
.swatch.is-active { border-color: var(--text); }
.swatch.is-active .swatch-label { color: var(--text); font-weight: 700; }
.swatch input:focus-visible + .swatch-chip {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- merge ingredients (prompt.php) ---------- */
/* Two independent pickers side by side, stacking at the compact breakpoint.
   Keeping them in one grid rather than two rows is what makes "this one, into
   that one" readable at a glance on a laptop. */
.merge-picks {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-bottom: .4rem;
}
@media (max-width: 640px) {
  .merge-picks { grid-template-columns: 1fr; }
}

/* min-width:0 or a long ingredient name in the results list forces the grid
   column past its 1fr share -- the same trap .plan-grid and .cal-month-cell
   both hit. */
.merge-pick { position: relative; min-width: 0; }

.merge-pick-label {
  display: block;
  margin-bottom: .3rem;
  font-size: .85rem;
  font-weight: 700;
  color: var(--text-mute);
}

/* input[type="search"] is only styled inside .toolbar, so these carry their
   own box rather than falling back to the browser default. */
.merge-pick .search-input {
  width: 100%;
  min-height: var(--tap);
  padding: .5rem .7rem;
  font-size: 1rem;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* Overlays rather than pushing the second picker down the page, so choosing on
   the left does not move the right-hand box out from under the cursor. */
.merge-results {
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .12);
}

.merge-result {
  display: flex;
  align-items: center;
  gap: .4rem;
  width: 100%;
  min-height: var(--tap);
  padding: .5rem .7rem;
  font: inherit;
  font-size: .95rem;
  text-align: left;
  color: var(--text);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.merge-result:last-child { border-bottom: 0; }
.merge-result:hover { background: var(--bg2); }

.merge-result-empty {
  padding: .6rem .7rem;
  font-size: .9rem;
  color: var(--text-mute);
}

.merge-chosen {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-height: var(--tap);
  padding: .35rem .35rem .35rem .7rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.merge-chosen-name {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 600;
  overflow-wrap: break-word;
}
.merge-chosen .btn { flex: 0 0 auto; }

/* An inactive row is findable here on purpose (it is exactly the clutter a
   merge is for), so it has to be labelled -- otherwise the picker silently
   offers rows the rest of the app hides. */
.merge-chosen-tag {
  flex: 0 0 auto;
  font-size: .75rem;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--text-mute);
}

.merge-report {
  margin-top: 1rem;
  padding: .9rem 1rem;
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.merge-report-lead { margin: 0 0 .8rem; font-size: .95rem; }

.merge-sec + .merge-sec { margin-top: .9rem; }
.merge-sec-title {
  margin: 0 0 .3rem;
  font-size: .85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--text-mute);
}
.merge-list {
  margin: 0;
  padding-left: 1.1rem;
  font-size: .92rem;
  line-height: 1.55;
}

/* The one thing in the report that is a loss rather than a move, so it is the
   one thing coloured. */
.merge-warn-inline { color: var(--alert); }

.merge-confirm { margin-top: 1.1rem; }
.merge-warn {
  margin: 0 0 .6rem;
  font-size: .9rem;
  font-weight: 600;
  color: var(--alert);
}
/* Cancel first, destructive action last and on the right: the report is read
   top to bottom and the confirm should be the deliberate end of it, not the
   first thing under the thumb. */
.merge-confirm-row {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  justify-content: flex-end;
}

.alert-ok {
  color: var(--accent);
  /* Pale tint of --accent. Kept literal rather than a colour-mix() so it works
     without any assumption about browser support; if --accent changes again,
     this has to change with it. */
  background: #eef3fa;
  border-color: var(--accent);
}

/* Chat column heading + its "New chat" control on one line. The title keeps its
   own bottom margin, so the row carries none of its own. */
.col-title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
}
.col-title-row .col-title { margin-bottom: .6rem; }
/* Keeps .btn-sm's existing 36px rather than inventing a third, smaller size --
   this file's own rule is that nothing is an under-sized tap target, and 36px
   is already the secondary size used by the day panel's Close button. */
.col-title-row .btn { flex: 0 0 auto; }

/* Keyboard highlight for the quick-add type-ahead. Hover styling alone is
   useless here: the arrow keys are the point on desktop, and there is no hover
   at all on the phone. */
.typeahead-item.is-highlighted { background: var(--bg2); box-shadow: inset 3px 0 0 var(--accent); }

/* ---------- plan page (plan.php) ---------- */

/* Desktop-only nav entry. Plan is hidden below 768px by user decision -- the
   phone is for chat and shopping, not for dragging. The mobile <select> nav
   simply omits Plan rather than hiding an <option>, which is unreliable. */
@media (max-width: 768px) {
  .nav-desktop-only { display: none; }
}

.plan-grid {
  display: grid;
  grid-template-columns: 1fr 360px;
  /* minmax(0, 1fr), not a plain 1fr: without the 0 minimum this row refuses to
     shrink below its content's natural height, which is exactly what a
     scrollable child needs to be ABLE to shrink to (same trap as min-width:0
     on a flex item -- see the search-box fix above). */
  grid-template-rows: minmax(0, 1fr);
  gap: 1rem;
  /* Was 1400px -- left a wide monitor with dead margins on both sides and
     starved the dish grid of the width it needs for 3 fixed columns (see
     #plan-dish-list below). min(1800px, 96vw) still caps on an ultra-wide
     screen instead of stretching edge to edge. */
  max-width: min(1800px, 96vw);
  /* width:100% is load-bearing now that body.page-plan is a flex container
     (see the @media block below): a flex item with width left at its default
     "auto" resolves via shrink-to-fit the instant it also has auto margins on
     the cross axis, so margin:0 auto centering silently collapsed .plan-grid
     down to its own max-content width instead of stretching out to max-width
     -- this is what actually produced the huge side gutters and squeezed the
     dish grid to fewer columns than intended. Giving it a definite width
     (100% of the flex line) before max-width caps it restores the same
     block-flow centering behavior this rule always relied on. */
  width: 100%;
  margin: 0 auto;
  padding: 1rem;
}

/* Dishes and days scroll independently instead of the whole page scrolling as
   one -- desktop only (this page is already gated off below 768px), so no
   height arithmetic to get wrong on a real phone's chrome, the trap the
   mobile chat log already hit once (see CLAUDE.md). The body becomes the one
   fixed-height shell (100vh, no scroll of its own); .plan-grid's row is
   stretched to fill what's left of it (default grid align-items, no longer
   overridden to "start"); each section is a header (title/toolbar, flex:0 0
   auto below) stacked on a scrolling list (flex:1 1 auto + overflow-y:auto +
   min-height:0, same shrink-to-fit requirement as the row above). */
@media (min-width: 769px) {
  body.page-plan {
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .page-plan > .topbar { flex: 0 0 auto; }
  .page-plan .plan-grid { flex: 1 1 auto; min-height: 0; }

  .page-plan .plan-dishes,
  .page-plan .plan-days {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  /* Both the bare heading and the wrapped row are listed, because which one
     each column uses has already changed twice: the "Fill empty dinners"
     button landed in .plan-days on 2026-08-29 and moved to .plan-dishes on
     2026-08-30. Missing the right selector here does not look like a missing
     style -- the header stops being pinned and quietly steals height from the
     scrolling tile list instead. */
  .page-plan .plan-dishes > .col-title-row,
  .page-plan .plan-dishes > .toolbar,
  .page-plan .plan-days > .col-title,
  .page-plan .plan-days > .col-title-row {
    flex: 0 0 auto;
  }
  .page-plan #plan-dish-list,
  .page-plan #plan-day-tiles {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }

  /* Fixed 3 columns of dish cards, not dishes.php's auto-fill (which reflows
     to however many 260px cards fit and can land on 2 as easily as 3 -- the
     user's own comparison point was "before" showing a reliable 3). */
  .page-plan #plan-dish-list {
    grid-template-columns: repeat(3, 1fr);
    align-content: start;
  }
}

.plan-mobile-gate { display: none; }

/* The gate, not a redirect: a bookmarked visit on a phone gets a real answer
   and two ways onward, rather than a drag interface that cannot be dragged. */
@media (max-width: 768px) {
  .plan-grid { display: none; }
  .plan-mobile-gate {
    display: block;
    max-width: 32rem;
    margin: 0 auto;
    padding: 2rem 1rem;
    text-align: center;
  }
  .plan-gate-links {
    display: flex;
    flex-direction: column;
    gap: .6rem;
    margin-top: 1.5rem;
  }
  .plan-gate-links .btn { width: 100%; text-decoration: none; }
}

.plan-hint { font-size: .82rem; }

/* The plan page's tiles are not clickable as a whole -- the slot rows are --
   so drop index.php's whole-tile pointer affordance. */
.page-plan .day-tile { cursor: default; }
.page-plan .day-tile:hover { border-color: var(--border); }
.page-plan .day-tile.is-today:hover { border-color: var(--accent); }

/* ---- Dining Out / Leftovers, and the header row that holds them ----
   These are entry types with no catalog row, so they sit beside the Dishes
   heading rather than in the list. Pill-shaped like the staple names for the
   same reason: a pill reads as a distinct tappable thing, where inline text
   reads as prose (the lesson from the Confirm You Have block, 2026-08-24). */
.col-title-tail {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex: 0 0 auto;
}

.plan-specials { display: flex; gap: .4rem; }

.plan-special {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: .25rem .75rem;
  font: inherit;
  font-size: .78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--text-mute);
  background: var(--bg2);
  border: 1px dashed var(--border);
  border-radius: 999px;
  white-space: nowrap;
  cursor: grab;
}
/* Dashed, where a dish card is solid: these place a MEAL, not a recipe --
   nothing is cooked and nothing reaches the shopping list. The border carries
   that difference without a word of explanation. */
.plan-special:hover { color: var(--accent); border-color: var(--accent); }
.plan-special:active { cursor: grabbing; }
.plan-special.is-dragging { opacity: .45; }
.plan-special.is-selected {
  color: var(--accent-fg);
  background: var(--accent);
  border-color: var(--accent);
  border-style: solid;
}

/* The type pill is a real button on the Plan page (it opens the dish), so it
   needs the affordance the read-only .badge never had. Scoped to .dish-type-btn
   so every other .badge in the app is untouched. */
.dish-type-btn {
  font-family: inherit;
  cursor: pointer;
}
.dish-type-btn:hover { color: var(--accent); border-color: var(--accent); }

/* ---- read-only dish view (opened from the type pill) ---- */
.dish-view-panel .dv-meta { display: flex; gap: .4rem; margin-bottom: .9rem; }

.dv-ings { margin: 0; padding: 0; list-style: none; }
.dv-ing {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem;
  padding: .45rem 0;
  border-bottom: 1px solid var(--border);
}
.dv-ing:last-child { border-bottom: 0; }
.dv-ing-name { font-weight: 600; }
.dv-ing-qty { color: var(--text-mute); font-size: .85rem; }
.dv-prep { color: var(--text-mute); font-size: .82rem; font-style: italic; }
.dv-optional {
  font-size: .72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .03em;
  color: var(--text-mute);
}
.dv-h3 { font-size: .85rem; margin: 1.1rem 0 .3rem; }
.dv-notes p { margin: 0; white-space: normal; }

.plan-dish-card { cursor: grab; position: relative; }
.plan-dish-card:active { cursor: grabbing; }
.plan-dish-card.is-dragging { opacity: .45; }
.plan-dish-card.is-selected {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* How many times this dish is currently on the plan (menu_items row count --
   see api/dishes.php). Bottom-right corner per the user's placement call,
   2026-08-27; omitted entirely at zero rather than showing "x0". */
.plan-dish-usage {
  position: absolute;
  right: .6rem;
  bottom: .55rem;
  font-size: .72rem;
  color: var(--text-mute);
}

/* Drop targets. The slot row is the unit -- renderTiles() already emits one per
   slot per day, which is exactly the granularity needed to highlight only the
   meal under the cursor rather than the whole day. */
.slot-row.is-droppable { border-radius: 4px; }

/* While a drag is in flight, show every slot as a candidate so it is obvious
   what can receive the dish -- without this the page looks inert until the
   cursor happens to cross a valid target. */
.plan-dragging .slot-row.is-droppable,
.plan-has-selection .slot-row.is-droppable {
  outline: 1px dashed var(--border);
  outline-offset: 1px;
}

.slot-row.is-drop-target {
  background: var(--bg);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.plan-has-selection .slot-row.is-droppable { cursor: copy; }

.plan-entry {
  display: inline-flex;
  align-items: center;
  gap: .15rem;
}

/* Always present, not hover-revealed: a mis-drop has to be correctable without
   hunting for the control, and this app's own rule is that nothing depends on
   hover. */
.plan-entry-x {
  padding: 0 .2rem;
  color: var(--text-mute);
  background: none;
  border: none;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}
.plan-entry-x:hover { color: var(--alert); }

/* ---------- complete the menu (plan.php "Fill empty dinners") ---------- */

/* Wider than the shared .edit-panel default: each row carries a date plus two
   dish picks with an explanation under each, and at 640px the "why" text wraps
   to three lines and the panel becomes a wall. */
.fill-panel { max-width: 760px; }

.fill-intro { margin: 0 0 .9rem; }

.fill-row {
  display: flex;
  align-items: flex-start;
  gap: .8rem;
  padding: .6rem 0;
  border-bottom: 1px solid var(--border);
}
.fill-row:last-child { border-bottom: none; }

.fill-row-date {
  flex: 0 0 8.5rem;
  padding-top: .35rem;
  font-size: .88rem;
  font-weight: 700;
}

.fill-row-picks {
  flex: 1 1 auto;
  /* Without this a long dish name ("Beef and Grilled Cheese Sandwiches with
     Horseradish Mayonnaise" is a real row) refuses to shrink and pushes the
     panel sideways -- the flex item min-width:auto trap this file already
     documents for the plan toolbar's search box. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: .35rem;
}

.fill-pick {
  display: flex;
  align-items: flex-start;
  gap: .55rem;
  padding: .4rem .5rem;
  background: var(--bg2);
  border-radius: var(--radius);
  cursor: pointer;
}
.fill-pick input[type="checkbox"] {
  /* Nudged down to sit on the first line of text rather than centred against
     the whole two-line block. */
  margin-top: .2rem;
  flex: 0 0 auto;
  width: 1rem;
  height: 1rem;
  cursor: pointer;
}
.fill-pick-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.fill-pick-name {
  font-size: .9rem;
  overflow-wrap: break-word;
}
.fill-pick-why {
  font-size: .8rem;
  color: var(--text-mute);
}

/* An unchecked row should read as declined at a glance, without hiding it. */
.fill-pick:has(input:not(:checked)) .fill-pick-name { text-decoration: line-through; }
.fill-pick:has(input:not(:checked)) { opacity: .55; }

/* The two plan-page column headings read as real headings rather than tiny
   field labels (user, 2026-08-30). Scoped to .page-plan on purpose: .col-title
   is shared with index.php and shop.php, which were not part of the ask -- and
   scoping to the page rather than to "Dishes" alone keeps BOTH plan columns
   the same size as each other. */
.page-plan .col-title { font-size: 1rem; }

/* A column heading and its action button, kept together on the left while
   .col-title-row's space-between pushes the drag hint to the right. Without
   this wrapper the three items spread across the full width and the button
   ends up nowhere near the title it belongs to. */
.col-title-lead {
  display: flex;
  align-items: center;
  gap: .7rem;
  min-width: 0;
  /* Takes over the bottom margin the heading gives up below. Leaving it on the
     .col-title would centre the button against a box that is .6rem taller than
     the text, nudging it visibly off the heading's line. */
  margin-bottom: .6rem;
}
.col-title-lead .col-title { margin-bottom: 0; }
