/* ==========================================================================
   Design tokens. Modeled on claude.ai's own look: a tight, neutral,
   icon-first sidebar that follows the light/dark theme like everything
   else (no bold brand-color panel). Theme is applied as
   data-theme="light|dark" on <html> by an inline script in base.html's
   <head> (runs before paint, no flash) plus js/theme_toggle.js for the
   toggle button itself.
   ========================================================================== */

:root, [data-theme="light"] {
  --bg: #f5f7f6;
  --surface: #ffffff;
  --surface-2: #eef2f0;
  --border: #e1e7e3;
  --text: #171a18;
  --text-muted: #666f6a;
  --primary: #0b8f5f;
  --primary-hover: #077249;
  --primary-contrast: #ffffff;
  --accent-soft: #e6f6ee;
  --danger: #c8382c;
  --danger-soft: #fbe9e7;
  --warning: #96650a;
  --warning-soft: #fdf1dc;
  --alert-orange: #a15218;
  --alert-orange-soft: #fbe6d3;
  --info: #1f5fa8;
  --info-soft: #e7f0fc;
  --success: #187a3d;
  --success-soft: #e5f6ea;
  --shadow-sm: 0 1px 2px rgba(20, 25, 22, 0.06), 0 1px 1px rgba(20, 25, 22, 0.04);
  --shadow-md: 0 8px 24px rgba(20, 25, 22, 0.10);
  --focus-ring: 0 0 0 3px rgba(11, 143, 95, 0.25);

  --sidebar-bg: #faf9f6;
  --sidebar-border: #e6e4dc;
  --sidebar-text: #3d3d3a;
  --sidebar-text-muted: #8a887f;
  --sidebar-hover: rgba(0, 0, 0, 0.055);
  --sidebar-active: rgba(0, 0, 0, 0.09);
}

[data-theme="dark"] {
  --bg: #14181a;
  --surface: #1b2023;
  --surface-2: #21272a;
  --border: #2c3336;
  --text: #eaeeec;
  --text-muted: #98a29c;
  --primary: #29c98a;
  --primary-hover: #3fdb9b;
  --primary-contrast: #06140e;
  --accent-soft: rgba(41, 201, 138, 0.14);
  --danger: #f0776d;
  --danger-soft: rgba(240, 119, 109, 0.14);
  --warning: #e3ac4c;
  --warning-soft: rgba(227, 172, 76, 0.14);
  --alert-orange: #e2934f;
  --alert-orange-soft: rgba(226, 147, 79, 0.16);
  --info: #7cabe6;
  --info-soft: rgba(124, 171, 230, 0.14);
  --success: #57cf85;
  --success-soft: rgba(87, 207, 133, 0.14);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 10px 28px rgba(0, 0, 0, 0.4);
  --focus-ring: 0 0 0 3px rgba(41, 201, 138, 0.3);

  --sidebar-bg: #1f1e1c;
  --sidebar-border: rgba(255, 255, 255, 0.08);
  --sidebar-text: #e8e6e0;
  --sidebar-text-muted: #96948b;
  --sidebar-hover: rgba(255, 255, 255, 0.06);
  --sidebar-active: rgba(255, 255, 255, 0.1);
}

* { box-sizing: border-box; }

html { color-scheme: light dark; }

body {
  margin: 0;
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--text);
  background: var(--bg);
  transition: background-color 0.15s ease, color 0.15s ease;
}

/* ==========================================================================
   App shell: a wide, always-expanded, grouped nav rail modeled on
   platform.claude.com's console sidebar -- icon+label rows, muted
   uppercase group labels, a soft rounded pill for the active link,
   instead of the old icon-only hover-flyout rail.
   ========================================================================== */

.app-shell {
  display: flex;
  min-height: 100vh;
  align-items: stretch;
}

.sidebar {
  width: 248px;
  flex-shrink: 0;
  background: var(--sidebar-bg);
  border-right: 1px solid var(--sidebar-border);
  color: var(--sidebar-text);
  display: flex;
  flex-direction: column;
  padding: 0;
  position: sticky;
  top: 0;
  height: 100vh;
}

.sidebar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1rem 0.75rem;
  flex-shrink: 0;
}

.sidebar .brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  color: var(--text);
  text-decoration: none;
}
.sidebar .brand svg { color: var(--primary); flex-shrink: 0; }
.brand-name { font-size: 0.95rem; font-weight: 650; letter-spacing: -0.01em; }

.sidebar-close, .sidebar-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  border: none;
  background: none;
  color: var(--sidebar-text-muted);
  cursor: pointer;
  padding: 0;
}
.sidebar-close:hover, .sidebar-toggle:hover { background: var(--sidebar-hover); color: var(--sidebar-text); }

.sidebar-scrim { display: none; }

.sidebar-nav {
  flex: 1;
  overflow-y: auto;
  padding: 0.25rem 0.75rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.nav-group { display: flex; flex-direction: column; }

.nav-group summary {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--sidebar-text);
  font-weight: 650;
  padding: 0.4rem 0.6rem;
  border-radius: 8px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.nav-group summary::-webkit-details-marker,
.nav-group summary::marker { display: none; content: ""; }
.nav-group summary:hover { background: var(--sidebar-hover); }
.nav-group summary svg:first-child { flex-shrink: 0; opacity: 0.85; color: var(--sidebar-text-muted); }
.nav-group summary svg:last-child {
  margin-left: auto;
  flex-shrink: 0;
  color: var(--sidebar-text-muted);
  transition: transform 0.15s ease;
}
.nav-group[open] summary svg:last-child { transform: rotate(90deg); }

.nav-group-links {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  padding: 0.15rem 0 0.4rem 0.9rem;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: 0.87rem;
  font-weight: 480;
  padding: 0.45rem 0.6rem;
  border-radius: 8px;
  white-space: nowrap;
}
.nav-link svg { flex-shrink: 0; opacity: 0.75; color: var(--sidebar-text-muted); }
.nav-link:hover { background: var(--sidebar-hover); }
.nav-link.active {
  background: var(--sidebar-active);
  color: var(--text);
  font-weight: 620;
}
.nav-link.active svg { opacity: 1; color: var(--primary); }

.sidebar-footer {
  flex-shrink: 0;
  padding: 0.6rem 0.75rem;
  border-top: 1px solid var(--sidebar-border);
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

/* Quick-access icons (Hockey, Baseball, Weather) pinned above the
   account/logout row -- unlike the rest of the sidebar these have no
   dropdown/subpages, just a single icon each straight to its page. */
.sidebar-quicklinks {
  flex-shrink: 0;
  margin-top: auto;
  padding: 0.5rem 0.75rem 0;
  display: flex;
  justify-content: center;
  gap: 1.1rem;
}
.sidebar-quicklinks a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  text-decoration: none;
}
/* Same convention as .nav-link svg elsewhere in the sidebar: muted and
   slightly dim by default, full-strength primary green only for the
   section currently open -- not a fixed color of their own. */
.sidebar-quicklinks a svg { opacity: 0.75; color: var(--sidebar-text-muted); }
.sidebar-quicklinks a:hover { background: var(--sidebar-hover); }
.sidebar-quicklinks a.active svg { opacity: 1; color: var(--primary); }

/* Mobile/PWA-only twin of .sidebar-quicklinks above, pinned to the very
   bottom of the screen instead of living inside the slide-out sidebar --
   Tim: "those are common things I access and I don't want to hit the
   menu each time for them." Hidden on desktop (see .sidebar-quicklinks,
   already reachable there); switched on in the same <=900px breakpoint
   where the sidebar itself goes off-canvas, further down this file. */
.bottom-dock { display: none; }

.sidebar-account {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  flex: 1;
  min-width: 0;
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: 0.85rem;
  font-weight: 550;
  padding: 0.4rem 0.55rem;
  border-radius: 8px;
}
.sidebar-account span { overflow: hidden; text-overflow: ellipsis; }
.sidebar-account svg { flex-shrink: 0; color: var(--sidebar-text-muted); }
.sidebar-account:hover, .sidebar-account.active { background: var(--sidebar-hover); }

.sidebar-footer .link-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  color: var(--sidebar-text-muted);
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.sidebar-footer .link-button:hover { color: var(--sidebar-text); background: var(--sidebar-hover); }

.content-area {
  flex: 1;
  min-width: 0;
}

main {
  padding: 2rem 2.5rem;
  max-width: 960px;
  margin: 0 auto;
}

main.wide { max-width: 1220px; }

/* ==========================================================================
   Typography / layout basics
   ========================================================================== */

.flashes { list-style: none; padding: 0; margin: 0 0 1.5rem; }

.flash {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.7rem 1rem;
  border-radius: 8px;
  margin-bottom: 0.5rem;
  font-size: 0.92rem;
  border: 1px solid transparent;
}
.flash-success { background: var(--success-soft); color: var(--success); }
.flash-error { background: var(--danger-soft); color: var(--danger); }
.flash-warning { background: var(--warning-soft); color: var(--warning); }
.flash-info { background: var(--info-soft); color: var(--info); }

/* Weather alerts (Environment Canada), coloured by their own severity
   scale -- yellow/orange/red, matching the colour EC itself puts on the
   alert. */
.weather-alert {
  padding: 0.75rem 1rem;
  border-radius: 8px;
  margin-bottom: 0.75rem;
  border: 1px solid transparent;
}
.weather-alert-title { font-weight: 600; margin-bottom: 0.35rem; }
.weather-alert-region { font-weight: 400; font-size: 0.85rem; opacity: 0.85; }
.weather-alert-text { font-size: 0.82rem; }
.weather-alert-yellow { background: var(--warning-soft); color: var(--warning); border-color: var(--warning); }
.weather-alert-orange { background: var(--alert-orange-soft); color: var(--alert-orange); border-color: var(--alert-orange); }
.weather-alert-red { background: var(--danger-soft); color: var(--danger); border-color: var(--danger); }

.flash-dismiss {
  background: none;
  border: none;
  color: inherit;
  font-size: 1.2rem;
  line-height: 1;
  padding: 0 0.15rem;
  cursor: pointer;
  opacity: 0.6;
}
.flash-dismiss:hover { opacity: 1; }

h1 {
  font-size: 1.65rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 1.25rem;
}
h2 {
  font-size: 1.15rem;
  font-weight: 700;
  margin-top: 2.25rem;
  margin-bottom: 0.85rem;
}

.section-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.section-heading h1, .section-heading h2 { margin: 0; }

/* Like .section-heading but the action sits right beside the heading
   text instead of pushed to the far edge (space-between) -- for a
   heading + its one directly-related button/links (Net Worth History's
   "Add this week's snapshot" + Save, Sports' team name + external links). */
.heading-inline { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.heading-inline h1, .heading-inline h2 { margin: 0; }

/* No underline on any link, anywhere in the app (Tim: "the underline
   makes it look tacky, I like the just green text") -- component-level
   rules further down that also set text-decoration:none on their own
   links (nav, tags, breadcrumbs, ...) are now redundant with this but
   left in place rather than hunted down one by one. */
a { color: var(--primary); text-decoration: none; }

/* ==========================================================================
   Tables — real borders, a proper header treatment, sortable affordance.
   ========================================================================== */

.table-wrap {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow-x: auto;
  overflow-y: hidden;
  box-shadow: var(--shadow-sm);
  margin-bottom: 1.75rem;
}

table { width: 100%; border-collapse: collapse; }

/* Denser variant for small, simple tables (Sports' schedule/injuries)
   where the default padding + full-width stretch leaves columns spread
   out with a lot of dead space between them. */
table.table-compact { width: auto; }
table.table-compact th, table.table-compact td { padding: 0.25rem 1.25rem; }

/* Same "don't force-stretch every column to fill width:100%" fix as
   .table-compact, for the Shopping List / Grocery list table specifically
   -- its Item/Notes boxes are already capped to a sane width (see
   grocery/_items_table.html), but a width:100% table still redistributes
   the leftover space across columns anyway, which is what was pushing
   Notes far away from Item even though they're adjacent columns. */
#shopping-list-table { width: auto; }

/* .table-wrap is a block-level bordered box that normally stretches to
   its container's full width -- fine for a table that also fills that
   width, but with .table-compact's own table now hugging its content,
   the wrap's border kept stretching out into empty space around a small
   tight table. Shrinks the border box down to the table's actual size. */
.table-wrap-compact { display: inline-block; width: auto; max-width: 100%; }

/* Tighter still, for Sports' box score (linescore/batting/pitching) --
   dense stat-grid tables like MLB.com's own box score, not the more
   breathing-room .table-compact used for schedule/injuries. Forces every
   cell to one line instead of letting a long column header wrap and
   blow out the row height. */
table.table-boxscore th, table.table-boxscore td { padding: 0.22rem 0.55rem; white-space: nowrap; font-size: 0.82rem; }

/* Batting box score: the batter who made the team's most recent out,
   called out visually since it's the one line most worth a glance
   mid-game. */
tr.row-last-out td { font-weight: 700; background: var(--surface-2); }

/* Each team's batting/pitching box score line, collapsed by default
   (same plain <details>/<summary> pattern as Overview's "How this is
   calculated") -- a consistent row height per team/section instead of
   one big always-expanded block, and consistent spacing between them. */
.boxscore-details summary { cursor: pointer; font-weight: 700; padding: 0.4rem 0; display: flex; align-items: center; gap: 0.4rem; }
.boxscore-details + .boxscore-details { border-top: 1px solid var(--border); }

/* Tightened from 0.75rem/1rem vertical (Tim: "tighten up... all the
   tables, all pages" -- more rows visible per screen without scrolling,
   started from Net Worth History's table specifically then generalized
   app-wide). Horizontal padding untouched -- the wasted space was
   between rows, not between columns. */
th, td {
  text-align: left;
  padding: 0.22rem 1rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.92rem;
}

tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: var(--surface-2); }

/* Pins the row-actions column (delete button, plus a "got it" checkbox
   on Cooking/Costco shopping lists -- see .checkbox-slot) to the right
   edge of a horizontally-scrolling table so it stays fully visible and
   tappable no matter how wide the row's other content gets (long item
   names, several sale-price tags, etc) instead of being scrolled/
   truncated off the visible edge -- the whole point of that checkbox is
   checking things off while walking around a store. */
.col-pin-delete {
  position: sticky;
  right: 0;
  background: var(--surface);
  z-index: 1;
}
th.col-pin-delete { background: var(--surface-2); }
tbody tr:hover td.col-pin-delete { background: var(--surface-2); }

/* Honey-do's actions column (mark-done/edit/delete -- three icons, one
   more than most .col-pin-delete tables) has no width of its own, so
   the table's default auto layout was splitting the leftover width
   between it and the Task column instead of giving Task all of it --
   Task ends up an inconsistent-looking distance from Time Estimate, and
   the icons sit a big, equally inconsistent-looking distance from Date
   Added (Tim: "the white space between task and time estimate and date
   added and the icons is not equal"). A fixed width sized to the three
   icons removes it from that contest entirely. */
#honeydo-table .col-pin-delete { width: 8.75rem; }

/* Same idea as .col-pin-delete but pins a table's first column to the
   left edge instead -- Weather's hourly table transposes hours into
   columns, so the row label ("Temp", "Precip", ...) needs to stay
   visible while scrolling through 48 hours instead of the usual
   row-actions column on the right. */
.col-pin-left {
  position: sticky;
  left: 0;
  background: var(--surface);
  z-index: 1;
}
th.col-pin-left { background: var(--surface-2); }
tbody tr:hover td.col-pin-left { background: var(--surface-2); }

th {
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--surface-2);
}

table.sortable th { cursor: pointer; user-select: none; white-space: nowrap; }
table.sortable th:hover { color: var(--primary); }
.th-tip { border-bottom: 1px dotted var(--text-muted); cursor: help; }
table.sortable th .sort-arrow { font-size: 0.7em; color: var(--text-muted); margin-left: 0.35em; }
table.sortable th.sorted-asc .sort-arrow::after { content: "\25B2"; color: var(--primary); }
table.sortable th.sorted-desc .sort-arrow::after { content: "\25BC"; color: var(--primary); }

/* Sales review table: rows should never wrap (Tim: "do not text wrap any
   lines... I want this list much tighter vertically") -- a wrapped flyer
   item name blows a single row up to 2-3 lines, which both looks messy
   and defeats the tightened th/td padding above. The raw flyer item name
   has no length limit though, so it's still allowed to overflow --
   .item-name clips it to an ellipsis instead, with the full text on
   hover via its title attribute (see admin/sales.html). */
#sales-table td { white-space: nowrap; }
#sales-table .item-name { display: inline-block; max-width: 26rem; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }

/* Retirement's Year by year table: every column centered (Tim: "center
   all the columns") -- unlike the app's usual left-text/right-numbers
   convention, this table is dense enough (20+ mostly-numeric columns)
   that a uniform alignment reads more scannable than mixed. */
#yearly-table th, #yearly-table td { text-align: center; }
/* Overrides table.sortable th's app-wide white-space:nowrap -- with 20+
   columns some headers ("Monthly spending") are long enough that forcing
   one line squeezed the column far wider than its (short, numeric)
   values need (Tim: "it's OK to have the title heading text wrap when
   appropriate"). vertical-align:middle is the table-cell default anyway,
   but set explicitly so a short one-line header still centers correctly
   next to a taller two-line one in the same header row. */
#yearly-table th { white-space: normal; vertical-align: middle; }

/* Right-align a numeric column (header and cells together) so a
   right-aligned value (e.g. .currency-input) lines up under its heading
   instead of sitting flush against the header's left-aligned text. */
th.num, td.num { text-align: right; }

/* Filter box shown above a table (data-filter-for="<table-id>") */
.table-filter {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px 10px 0 0;
  border-bottom: none;
  padding: 0.6rem 0.85rem;
  color: var(--text-muted);
}
/* [type=text] added purely to outrank the global input[type=text] rule
   below on specificity -- a bare ".table-filter input" ties it and, tie
   going to whichever is later in the file, was silently losing. */
.table-filter input[type=text] {
  border: none;
  background: none;
  padding: 0.2rem 0;
  font-size: 0.92rem;
  color: var(--text);
  width: 50%;
  flex: 0 0 auto;
}
.table-filter input:focus { outline: none; box-shadow: none; }
/* Opt-in modifier: the default 50% width above is sized for filter bars
   with nothing else beside them (e.g. Costco's). Honey-do's
   "In progress" checkbox sits directly beside the filter box instead, so
   it needs the box to hug its own short placeholder text ("Filter
   items...") rather than stretching halfway across the bar and leaving
   the checkbox stranded out past the filter word or two. */
.table-filter.filter-compact input[type=text] { width: auto; flex: 0 1 14rem; }
.table-filter + .table-wrap { border-top-left-radius: 0; border-top-right-radius: 0; margin-top: -1px; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Bulk-action bar (e.g. inventory's "N selected · Move to...") -- hidden
   via the `hidden` attribute until at least one row checkbox is checked. */
.bulk-move-bar {
  position: sticky;
  top: 0.75rem;
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  background: var(--surface);
  border: 1px solid var(--primary);
  border-radius: 10px;
  padding: 0.35rem 0.85rem;
  margin-bottom: 1rem;
  box-shadow: var(--shadow-md);
}
.bulk-move-bar select { width: auto; flex: 1; max-width: 220px; padding: 0.3rem 0.6rem; }
.bulk-move-bar .btn { margin: 0; padding: 0.3rem 0.85rem; }

/* ==========================================================================
   Cards
   ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.25rem;
  margin-bottom: 1.25rem;
  box-shadow: var(--shadow-sm);
}

.card-title { font-weight: 700; margin-bottom: 0.35rem; }
.card-meta { color: var(--text-muted); font-size: 0.85rem; }

/* A Stocks-page ticker's Name cell (#stocks) links out to its Yahoo
   Finance quote page -- green rather than this app's usual link color.
   Inherits its cell's font-size rather than setting its own, so it
   reads the same as the plain text it replaced. */
.stock-name-link { color: var(--success); text-decoration: none; }

/* Sports: "Visitor 8:3 Home" read as one tight inline string, with each
   team's logo bracketing its name instead of bare text. */
.score-line { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; font-weight: 600; }
.score-line strong { font-size: 1.3rem; }
.team-logo { width: 28px; height: 28px; object-fit: contain; flex-shrink: 0; }
.team-logo-sm { width: 18px; height: 18px; }

/* Baseball's current at-bat (#sports: "I'd like to see the current
   status of the current at bat too") -- balls/strikes/outs plus a small
   diamond showing which bases are occupied, same info mlb.com's own
   Gameday shows next to the score. */
/* No margin-top of its own -- it sits right after the box score's
   .table-wrap, which already carries a 1.75rem margin-bottom (shared by
   every table in the app), so adding more on top of that was stacking
   into a noticeably bigger gap than everywhere else (Tim: "a little too
   much extra space between the box score and the current play"). */
.atbat-status { display: flex; align-items: center; gap: 0.9rem; }
.atbat-diamond { position: relative; width: 2.2rem; height: 2.2rem; flex-shrink: 0; transform: translateY(0.4rem); }
.atbat-diamond .base {
  position: absolute;
  width: 0.6rem;
  height: 0.6rem;
  background: var(--surface-2);
  border: 1.5px solid var(--border);
  transform: rotate(45deg);
}
.atbat-diamond .base.occupied { background: var(--success); border-color: var(--success); }
/* True diamond geometry: 2nd at top-center, 1st/3rd at mid-height on the
   right/left edges -- all three equidistant from the box's center, same
   as a real infield with home plate (not drawn, since it's not a base
   that varies) at the unmarked bottom point. (A previous pass anchored
   1st/3rd to the *bottom* corners instead to fix text-centering below,
   but that stretched them down into empty home-plate space and the
   diamond stopped looking like a diamond -- Tim: "the baseball diamond
   doesn't look like a baseball diamond any more".) */
.atbat-diamond .base-2nd { top: 0; left: 50%; margin-left: -0.3rem; }
.atbat-diamond .base-1st { top: 50%; right: 0; margin-top: -0.3rem; }
.atbat-diamond .base-3rd { top: 50%; left: 0; margin-top: -0.3rem; }
/* With home plate undrawn, the visible 3 points sit higher than the
   box's true center, so .atbat-status's align-items:center (centered on
   the full 2.2rem box) put the adjacent count text visibly below the
   diamond's actual visual weight (Tim: "the count... is a little too
   low, should be centered with the baseball diamond"). A transform
   nudges the graphic itself down to meet the text instead of distorting
   its shape to meet the box (see the translateY on .atbat-diamond
   above). */
.atbat-count { font-weight: 600; }
.atbat-count .card-meta { font-weight: 500; }

/* ==========================================================================
   Forms
   ========================================================================== */

form.inline { display: inline; }

label { display: block; margin: 1rem 0 0.3rem; font-size: 0.88rem; font-weight: 600; color: var(--text); }

input[type=text], input[type=number], input[type=date], input[type=email],
input[type=password], select, textarea {
  width: 100%;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.95rem;
  font-family: inherit;
  background: var(--surface);
  color: var(--text);
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: var(--focus-ring);
}
/* A read-only field (the Menus/Costco export pages' "select all to copy"
   textarea) isn't something you edit, so it shouldn't pick up the same
   green "you're editing this" focus ring/border as a real input --
   especially distracting there since select()-on-click focuses it. */
textarea[readonly]:focus, input[readonly]:focus {
  border-color: var(--border);
  box-shadow: none;
}
textarea { min-height: 6rem; }

/* Quick inline-edit fields directly in a table row (Inventory's ingredient
   name/quantity -- #inventory tweak), styled to read as plain text until
   hovered/focused instead of looking like a full form field. Selectors
   are qualified with "input" (not just the class) so their specificity
   actually beats the global `input[type=text], input[type=number]...`
   rule above -- a bare class selector ties that rule on specificity and
   loses, since attribute selectors count the same as a class. */
input.inline-edit {
  width: auto;
  padding: 0;
  border: none;
  background: transparent;
  font: inherit;
  color: inherit;
  border-radius: 4px;
}
input.inline-edit:focus { background: var(--surface); }
input.inline-edit-qty {
  width: 1.6rem;
  min-width: 0;
  padding-right: 1px;
  text-align: right;
  -moz-appearance: textfield;
}
input.inline-edit-qty::-webkit-outer-spin-button,
input.inline-edit-qty::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input.inline-edit-unit {
  width: 3rem;
  min-width: 0;
}
input.inline-edit-protein, input.inline-edit-sides, input.inline-edit-exception {
  width: 100%;
  min-width: 8rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
input.inline-edit-exception { display: block; margin-top: 0.2rem; color: var(--text-muted); font-size: 0.85rem; }

/* Opt-in modifier for any other inline-edit field whose box is narrower
   than its value can get (Costco/Cooking shopping list item names and
   notes) -- clips with "..." instead of just hiding the overflow, and
   pairs with a title="" attribute (see inline_edit.js) for the full text
   on hover. */
input.inline-edit-truncate { min-width: 8rem; overflow: hidden; text-overflow: ellipsis; }
input.inline-edit-truncate.inline-edit-notes { color: var(--text-muted); font-size: 0.85rem; }

/* Label beside its field instead of stacked above it, for compact dialogs
   with several short fields (e.g. the Inventory add/edit dialog) -- cuts
   the form's vertical space roughly in half. min-width: 0 overrides the
   flex item's default content-based minimum so it can actually shrink to
   fit next to the label instead of forcing a wrap; width: auto overrides
   the global `width: 100%` on inputs/selects, which otherwise fights the
   flex sizing here. */
.field-row { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.75rem; margin: 1rem 0 0.3rem; }
.field-row > label { margin: 0; flex: 0 0 5rem; white-space: nowrap; }
.field-row > input, .field-row > select, .field-row > textarea { margin: 0; flex: 1 1 auto; width: auto; min-width: 0; }
/* .field-row's own `display: flex` above beats the browser's built-in
   `[hidden] { display: none }` rule on a specificity tie (author beats
   UA) -- same issue as #chat-panel below, just here for any conditionally
   shown field-row (e.g. the Inventory dialog's "Last modified" field,
   only meant to show in Edit mode). Without this, JS setting .hidden =
   true (or the `hidden` attribute in markup) visibly does nothing. */
.field-row[hidden] { display: none; }

/* Password fields with a press-and-hold "show password" icon button
   (see js/password_reveal.js) -- the icon overlaps the input's right
   padding rather than sitting beside it, so the field keeps the same
   width as every other input on the form. */
.password-field { position: relative; }
.password-field input { padding-right: 2.5rem; }
.password-field .password-toggle {
  position: absolute;
  right: 0.35rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.35rem;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}
.password-field .password-toggle:hover { color: var(--text); background: var(--surface-2); }

/* Protein/sides fields on the meal dialog: free-form text, plus a button
   that drops down a checklist of what's in inventory (use-soon items
   first) to append to the field as a comma-separated list. */
.inventory-pick-field { position: relative; }
.inventory-pick-field input { padding-right: 2.5rem; }
.inventory-pick-toggle {
  position: absolute;
  right: 0.35rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.35rem;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}
.inventory-pick-toggle:hover { color: var(--text); background: var(--surface-2); }
.inventory-pick-list {
  position: absolute;
  z-index: 5;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  max-height: 12rem;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  padding: 0.35rem;
}
.inventory-pick-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.4rem 0.5rem;
  font-size: 0.88rem;
  font-weight: 400;
  border-radius: 6px;
  cursor: pointer;
}
.inventory-pick-option:hover { background: var(--surface-2); }
.inventory-pick-option input[type="checkbox"] { width: auto; flex: 0 0 auto; }
.inventory-pick-option span:first-of-type { flex: 1; display: flex; align-items: baseline; gap: 0.4rem; }
.inventory-pick-empty { margin: 0.3rem; font-size: 0.85rem; color: var(--text-muted); }

/* Read-only "peek at what's on hand" dialogs (Protein/Sides column headers
   on the weekly menu, shopping lists' peek icon) -- see
   partials/inventory_peek.html. */
.peek-btn { flex: 0 0 auto; }
/* Column-header variant: smaller and inline with the header text/th-tip
   span instead of the full 2rem row-action size. */
th .peek-btn { width: 1.5rem; height: 1.5rem; vertical-align: middle; margin-left: 0.1rem; }
.peek-dialog { width: min(360px, 92vw); max-height: 80vh; overflow-y: auto; }
.peek-group-label { margin: 1rem 0 0.35rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.02em; color: var(--text-muted); }
.peek-group-label:first-of-type { margin-top: 0.75rem; }
.peek-list { list-style: none; margin: 0; padding: 0; }
.peek-item {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.4rem 0.1rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.88rem;
}
.peek-item:last-child { border-bottom: none; }
.peek-item span:first-child { flex: 1; }

/* No spinner arrows on number inputs -- this app has no fields where
   incrementing by one via a tiny click target is actually useful. */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type=number] { -moz-appearance: textfield; appearance: textfield; }

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 1.05rem;
  border-radius: 8px;
  border: 1px solid var(--primary);
  background: var(--primary);
  color: var(--primary-contrast);
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  margin: 0.2rem 0.4rem 0.2rem 0;
  transition: background-color 0.12s ease, transform 0.05s ease;
}
.btn:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.btn:active { transform: translateY(1px); }
.btn.secondary { background: var(--surface); color: var(--primary); border-color: var(--border); }
.btn.secondary:hover { background: var(--surface-2); }
.btn.danger { background: var(--danger); border-color: var(--danger); color: #fff; }

/* Round icon-only buttons (edit/delete/add/etc.) */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 8px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  text-decoration: none;
  padding: 0;
}
.icon-btn:hover { background: var(--surface-2); color: var(--text); }
.icon-btn.danger:hover { background: var(--danger-soft); color: var(--danger); }
.icon-btn.primary { background: var(--primary); color: var(--primary-contrast); }
.icon-btn.primary:hover { background: var(--primary-hover); }

.icon-btn-add {
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 999px;
  background: var(--primary);
  color: var(--primary-contrast);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  flex-shrink: 0;
}
.icon-btn-add:hover { background: var(--primary-hover); }

.row-actions { display: flex; gap: 0.3rem; align-items: center; }
/* A table row's actions column (edit/delete icons, etc.) -- push its
   icons flush against the table's right edge instead of hugging the left
   side of that cell. Scoped to table cells only (td AND th, so a header's
   "select all" lines up with the row checkboxes below it): row-actions
   inside a card (Honey-do, Eat out) stays left-aligned as part of that
   layout. */
td .row-actions, th .row-actions { justify-content: flex-end; }

/* A checkbox mixed into a .row-actions group (e.g. Costco's pickup
   checkbox beside its icon-btns) sits in a box the same size as an
   icon-btn, so the group's middle icon reads as visually centered
   between two equal-width neighbors instead of one tiny checkbox and one
   2rem button. */
.row-actions .checkbox-slot { display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; flex-shrink: 0; }

.link-button {
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  padding: 0;
}

/* ==========================================================================
   Misc small components
   ========================================================================== */

.ingredient-row, .dynamic-row {
  display: grid;
  grid-template-columns: 2fr 1fr 2fr auto;
  gap: 0.6rem;
  align-items: center;
  margin-bottom: 0.6rem;
}

.tag {
  display: inline-block;
  background: var(--accent-soft);
  color: var(--primary);
  border-radius: 999px;
  padding: 0.18rem 0.6rem;
  font-size: 0.78rem;
  margin: 0 0.3rem 0.3rem 0;
  font-weight: 600;
  white-space: nowrap;
}
.tag.danger { background: var(--danger-soft); color: var(--danger); }
.tag.warning { background: var(--warning-soft); color: var(--warning); }
.tag.success { background: var(--success-soft); color: var(--success); }
.tag.info { background: var(--info-soft); color: var(--info); }

.audible { color: var(--danger); font-weight: 600; }

/* Read-only 1-5 star display (recipes, eat-out places) */
.star-display { letter-spacing: 1px; }
.star-filled { color: var(--warning); }
.star-empty { color: var(--border); }

/* Clickable 1-5 star form input -- radios rendered 5..1 in DOM order,
   displayed 1..5 via row-reverse, so the ~ sibling selector can light up
   "this star and everything before it" with pure CSS. */
.star-rating-input {
  display: inline-flex;
  flex-direction: row-reverse;
  gap: 0.1rem;
}
.star-rating-input input { position: absolute; opacity: 0; width: 0; height: 0; }
.star-rating-input label {
  font-size: 1.6rem;
  line-height: 1;
  color: var(--border);
  cursor: pointer;
  margin: 0;
  padding: 0 0.05rem;
}
.star-rating-input input:checked ~ label,
.star-rating-input label:hover,
.star-rating-input label:hover ~ label {
  color: var(--warning);
}

/* Breadcrumb trail at the top of every page */
.breadcrumbs {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.82rem;
  color: var(--text-muted);
  margin-bottom: 1rem;
  flex-wrap: wrap;
}
.breadcrumbs a { color: var(--text-muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--primary); }
.breadcrumbs .crumb-current { color: var(--text); font-weight: 600; }

/* A row of cards/entries grouped under a sub-heading (recipe protein
   sections, honey-do lists, etc) */
.list-section { margin-top: 1.75rem; }
.list-section h2 { margin-top: 0; }

.day-card { display: flex; justify-content: space-between; align-items: center; gap: 1.25rem; }

.grocery-item.checked { text-decoration: line-through; opacity: 0.6; }

/* --------------------------------------------------------------------- */
/* Weekly meal table + lightbox dialogs (meal add/edit, inventory add/edit) */

.meal-table tr[data-day-offset] td:first-child { white-space: nowrap; }
.meal-table .meal-empty { color: var(--text-muted); font-style: italic; }

dialog {
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 1.5rem;
  width: min(420px, 92vw);
  color: var(--text);
  background: var(--surface);
  box-shadow: var(--shadow-md);
}
dialog::backdrop { background: rgba(15, 18, 16, 0.45); }
dialog h2 { margin: 0 0 1rem; font-size: 1.05rem; }

/* Wider than the default dialog -- its label-beside-field rows (see
   .field-row) need more room than a 420px dialog gives them. */
#inventory-dialog { width: min(480px, 92vw); }

.info-icon {
  display: inline-flex;
  vertical-align: middle;
  color: var(--text-muted);
  cursor: help;
  margin-left: 0.3rem;
}

.use-soon-icon { display: inline-flex; color: var(--warning); cursor: help; }

/* Honey-do's "in progress" status indicator (left of the task name) --
   a plain neutral icon-btn handles the toggle in the row actions, so this
   only carries color/cursor for the status itself. */
.in-progress-icon { display: inline-flex; align-items: center; color: var(--info); cursor: help; }
.in-progress-icon svg { display: block; }

/* Recipe ideas shown under the meal dialog's Protein field when a day has
   a protein set but no recipe yet (see js/meal_dialog.js) -- empty and
   collapsed (no margin) when there's nothing to suggest. */
.protein-suggestions:not(:empty) { margin: 0.5rem 0 0.25rem; }
.protein-suggestion-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.35rem 0;
}
.protein-suggestion-row form { margin: 0; }
.protein-suggestion-row .btn { padding: 0.3rem 0.65rem; font-size: 0.8rem; }

/* How much of each item to take out of inventory, shown on the "Add to
   Menu" dialog only for items with more than 1 unit on hand -- see
   js/inventory_add_to_menu.js. */
.add-to-menu-qty-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.3rem 0;
}
.add-to-menu-qty-row label { margin: 0; font-weight: 400; flex: 1; }
.add-to-menu-qty-row input { width: 5.5rem; flex: 0 0 auto; }

.meal-mode-toggle {
  display: flex;
  gap: 1.25rem;
  margin-bottom: 0.75rem;
  font-size: 0.88rem;
}
.meal-mode-toggle label { display: flex; align-items: center; gap: 0.4rem; font-weight: 500; margin: 0; }
.meal-mode-toggle input { width: auto; }

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.25rem;
}

/* Stocks page: gain/green, loss/red -- used for the watchlists' %-change
   column and the site-wide ticker. */
.stock-up { color: var(--success); }
.stock-down { color: var(--danger); }

/* Every Stocks-page list (#stocks) used to be its own <table>, one per
   card -- with the default auto table layout each one sized its Symbol/
   Price/Chg/Name columns to fit only its own content, so Price started
   at a different x per card (Tim: "DOW is not vertically aligned with
   Oil"). Pinning every column to an explicit width (table-layout:fixed)
   didn't actually hold: a nowrap cell whose content is wider than its
   column still forces both the column *and the whole table* wider
   regardless of table-layout or the cell's own overflow value -- a
   table's automatic-sizing pass considers cell content no matter what.
   CSS Grid has no such quirk (a grid item's overflow:hidden genuinely
   zeroes out its automatic minimum size, so a track's specified width
   actually sticks), so each list is a grid instead: one shared column
   spec (below) makes every card line up, and it's what finally let the
   columns run tight without the card needing to horizontally scroll
   (Tim: "make the columns tighter, I don't want any horizontal
   scroll... if the name is too long, fit as much as you can and have
   the rest as ..."). row_reorder.js doesn't care that a "row" is a
   plain <div> instead of a <tr> -- Stocks' own list-of-lists already
   reorders the same way. */
.stock-grid {
  display: grid;
  /* Name's minmax(0, 1fr) is the only flexible track -- it soaks up
     whatever width is left over, so the grid always fills its card
     exactly (no dead space, no overflow) instead of a fixed total that
     has to be re-guessed every time a column changes (Tim: "make it fit
     the size of the card, but no larger, no horizontal scrolling"). The
     0 minimum is what lets that track's overflow:hidden ellipsis
     actually engage instead of forcing the row wider for a long name. */
  grid-template-columns: 1.5rem 7.5rem 8rem 6rem 2.5rem 2.5rem minmax(0, 1fr) 2.5rem;
  width: 100%;
  overflow-x: auto;
  font-size: 0.92rem;
  /* Grid items default to align-items:stretch -- every cell in a row was
     being stretched to match the row's tallest cell (the ones with an
     <input>), and since .stock-grid-cell isn't flex (see below), that
     extra stretched height had nowhere to go but below the text (a plain
     block's content sits at its own top padding, not vertically
     centered), reading as uneven row padding (Tim: "the space underneath
     XRP to the line is not the same as above XRP to the line above").
     Centering here instead means every cell sizes to its own content and
     centers within the row, regardless of which cell is tallest. */
  align-items: center;
}
/* display:contents takes a row <div> out of the box tree entirely, so
   its children (the cells) become direct items of .stock-grid's column
   track -- that's what makes every row share one column layout instead
   of each row laying itself out independently. Drag/drop events fired
   on a cell still bubble up through it to row_reorder.js's listener
   like normal, despite it having no box of its own. */
.stock-grid-row { display: contents; }
/* Wraps just the item rows (not the header row) so row_reorder.js's
   data-reorder-url container only ever sees item rows as .children --
   also display:contents, so nesting it inside .stock-grid doesn't add
   an extra level to the column layout. */
.stock-grid-body { display: contents; }
/* Deliberately not display:flex -- text-overflow:ellipsis isn't
   reliably applied to a flex container's aggregate overflow across
   several children (the input+spans that make up the Symbol cell,
   say), only to a plain block's single line box. Vertical centering
   comes from .stock-grid's align-items:center instead (see above) --
   equal padding here now that each cell sizes to its own content rather
   than being stretched to the row's tallest cell. */
.stock-grid-cell {
  /* Asymmetric on purpose -- line-height:1 alone (below) wasn't enough to
     stop the top gap from reading bigger than the bottom one (Tim: "you
     got the bottom better but not the top"), so this pulls top down
     further rather than guessing at line-height alone. */
  padding: 0.16rem 0.6rem 0.34rem;
  /* Default line-height leaves uneven half-leading above/below the glyphs
     themselves -- line-height:1 collapses most of that so the padding
     above is what's actually driving the remaining gap. */
  line-height: 1;
  border-bottom: 1px solid var(--border);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stock-grid-row:last-child > .stock-grid-cell { border-bottom: none; }
.stock-grid-row:hover > .stock-grid-cell { background: var(--surface-2); }
.stock-grid-headrow > .stock-grid-cell {
  color: var(--text-muted);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  background: var(--surface-2);
}

/* The $ / % ticker-format checkbox columns (#stocks) -- centered, same
   treatment for both the select-all checkbox in the header row and each
   ticker's own checkbox in the body. Overrides .stock-grid-cell's
   overflow/ellipsis -- a lone checkbox has no text to truncate, but
   Chromium was rendering a stray "…" in this narrow a column anyway. */
.stock-ticker-format-col { text-align: center; overflow: visible; text-overflow: clip; }

/* Site-wide stock ticker (header, every page) -- left-aligned and
   static when its content fits on one line; when it doesn't,
   stock_ticker_scroll.js adds .scrolling and this animates it as a slow
   marquee instead. */
.stock-ticker-bar {
  display: flex;
  justify-content: flex-start;
  margin-bottom: 0.3rem;
  text-decoration: none;
}
.stock-ticker-bar:last-of-type { margin-bottom: 0.9rem; }
.stock-ticker-viewport { overflow: hidden; max-width: 100%; }
.stock-ticker-track { display: flex; align-items: center; gap: 0.3rem; white-space: nowrap; width: max-content; font-size: 0.7rem; font-weight: 600; }
.stock-ticker-track.scrolling { animation-name: stock-ticker-scroll; animation-timing-function: linear; animation-iteration-count: infinite; }
.stock-ticker-sep { color: var(--text-muted); font-weight: 400; }
/* The watchlist's own name, leading its line in the ticker -- deliberately
   not .stock-up/.stock-down colored (those mean price direction, and a
   title isn't one), just the ordinary muted/bold treatment other labels
   in the app use. margin-right beyond .stock-ticker-track's own 0.3rem
   gap indents the items a bit further from the label than they are from
   each other, reading more like "label: items" than one flat run --
   tried a fixed-width label column instead (lining up every list's
   first item at the same x) but that squeezed the items' own space too
   much, especially on a narrow/phone screen. */
.stock-ticker-label { color: var(--text-muted); font-weight: 700; margin-right: 0.25rem; }
@keyframes stock-ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

#stock-add-dialog { width: min(440px, 92vw); }
.stock-search-results {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  max-height: 16rem;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.stock-search-results:empty { border: none; }
.stock-search-results li {
  padding: 0.5rem 0.75rem;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.88rem;
}
.stock-search-results li:last-child { border-bottom: none; }
.stock-search-results li:hover { background: var(--surface-2); }

/* --------------------------------------------------------------------- */
/* Drag-and-drop (menus: dragging a day's meal onto another day) */

.drop-target { transition: background-color 0.15s, border-color 0.15s; border-radius: 10px; }
.drop-target.drag-over { background-color: var(--accent-soft) !important; }
.drop-target.drag-source-active { opacity: 0.4; }

th.drag-over, .stock-grid-cell.drag-over { background: var(--accent-soft); }
th.drag-source-active, .stock-grid-cell.drag-source-active { opacity: 0.4; }

[draggable="true"] { cursor: grab; }

.drag-handle {
  display: inline-flex;
  color: var(--text-muted);
  margin-right: 0.35rem;
  cursor: grab;
  font-weight: 700;
}
.drag-handle:active { cursor: grabbing; }

/* Column-resize handle (column_reorder.js) -- a thin grab strip along a
   header cell's right edge, on every .reorderable-columns table/grid.
   Positioned inside the cell's own padding box (not protruding past its
   right edge) so .stock-grid-cell's overflow:hidden doesn't clip it. */
th.has-col-resize-handle, .stock-grid-cell.has-col-resize-handle { position: relative; }
.col-resize-handle {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 8px;
  cursor: col-resize;
  touch-action: none;
  z-index: 2;
}
.col-resize-handle:hover, .col-resize-handle:active { background: var(--primary); opacity: 0.5; }

/* ==========================================================================
   Tabs + compact field grid (Retirement: Assumptions)
   ========================================================================== */

.tabs {
  display: flex;
  gap: 0.15rem;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.5rem;
}
.tab-btn {
  padding: 0.6rem 0.8rem;
  border: none;
  background: none;
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.9rem;
  white-space: nowrap;
  flex-shrink: 0;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  text-decoration: none;
  display: inline-block;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }

.tab-panel { display: none; }
.tab-panel.active { display: block; }
.tab-panel-title { font-size: 1.15rem; margin: 0 0 0.3rem; }

.field-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem 1rem;
}
.field-group {
  display: flex;
  flex-direction: column;
  flex: 0 1 90px;
}
.field-group label { margin: 0 0 0.3rem; white-space: nowrap; font-size: 0.82rem; }
.field-group input { width: 100%; padding: 0.45rem 0.5rem; }

/* Flex items default to min-width:auto, which means a long label with
   white-space:nowrap (e.g. "Contribution while working, per year") was
   overriding the 90px flex-basis and forcing the whole box wide, no
   matter how short the value inside actually is. flex-basis:auto lets
   each box size to its own label's natural width (so most labels show
   in full, no ellipsis) up to max-width, beyond which it truncates with
   an ellipsis instead of wrapping or overflowing into the next field
   (hover/focus for the full text via its title attribute) -- only the
   genuinely long labels hit that cap, instead of every label being cut
   by 1-2 characters against a fixed 90px box. min-width still lets it
   shrink below either when space is tight. Scoped to .field-grid
   (Assumptions/Expenses/Quick adjust) so Net Worth History's grid-based
   layout -- which doesn't have this problem -- is untouched. */
.field-grid .field-group { flex: 0 1 auto; min-width: 70px; max-width: 170px; }
.field-grid .field-group label { overflow: hidden; text-overflow: ellipsis; text-align: center; }
.field-grid .field-group input { text-align: center; }

/* A tighter grid variant for short "$xxx,xxx"-sized fields (Net Worth
   History's weekly entry form) -- a fixed 9-column grid instead of
   flex-wrap, so column N always lines up under column N on every row.
   Date spans columns 1-2. Row 2 (Crypto/HISA/Business Corp/RESP/Notes)
   is explicitly pinned to columns 3-6 and 7-9 so it lines up directly
   under T-TD/R-TD/R-CL/Amanda-T and Amanda-R, with Notes spanning under
   Amanda-R through the end instead of being squeezed into one box.
   9 columns (not 10) is deliberate -- with the sidebar's width factored
   in, a 10-column version of this grid doesn't actually fit at a common
   1280px window width and forces horizontal scroll. */
.field-grid-tight {
  display: grid;
  grid-template-columns: repeat(9, minmax(90px, 1fr));
  gap: 0.9rem 1rem;
}
/* A person's name as a same-line prefix ("Sam:") in front of their row
   of fields, instead of a heading on its own line above them -- vertically
   centered against the input boxes specifically, not the field-group
   column as a whole (which also includes each field's own small label on
   top, e.g. "Annual cost" -- centering against the full column left "Sam:"
   sitting noticeably higher than the middle of the actual input boxes).
   .field-inline-label-spacer is invisible and mirrors the real per-field
   label's font-size/margin so it takes up exactly the same height,
   leaving .field-inline-label-text to stretch across (and center within)
   whatever's left -- exactly the input's own height, whatever that
   happens to render as, no hardcoded rem guess needed. Relies on the row
   container using the default align-items:stretch (no flex-end/center
   override) so both columns stretch to match. */
.field-inline-label { display: flex; flex-direction: column; white-space: nowrap; }
.field-inline-label-spacer { visibility: hidden; font-size: 0.82rem; margin: 0 0 0.3rem; }
.field-inline-label-text { flex: 1; display: flex; align-items: center; font-weight: 600; font-size: 0.85rem; }

/* Narrower field-group variant (Expenses page) so 3-5 short fields fit
   on one line instead of wrapping under .field-grid's normal 170px cap. */
.field-grid-compact .field-group { max-width: 108px; min-width: 64px; }
.field-grid-compact .field-group input { padding: 0.4rem 0.4rem; }

.field-grid-tight .field-group.wide { grid-column: span 2; }
.field-grid-tight .field-group.gc-3 { grid-column: 3; }
.field-grid-tight .field-group.gc-4 { grid-column: 4; }
.field-grid-tight .field-group.gc-5 { grid-column: 5; }
.field-grid-tight .field-group.gc-6 { grid-column: 6; }
.field-grid-tight .field-group.gc-notes { grid-column: 7 / -1; }
/* Without this, a grid item's default min-width:auto uses its input's
   min-content size (an <input>'s intrinsic ~170-200px, ignoring the
   width:100% override, and ignoring the long Notes placeholder entirely),
   which can force tracks wider than the 90px minmax floor and overflow the
   page horizontally. min-width:0 lets every box (including Notes) actually
   shrink to its track instead of pushing the row wider than the viewport. */
.field-grid-tight .field-group { min-width: 0; }

/* Below this, 9 fixed 90px-min columns no longer fit -- fall back to a
   wrapping grid instead of forcing horizontal scroll. The precise
   row-2 alignment above is a desktop nicety, not worth a scrollbar on a
   narrow screen. */
@media (max-width: 1250px) {
  .field-grid-tight { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
  .field-grid-tight .field-group.gc-3,
  .field-grid-tight .field-group.gc-4,
  .field-grid-tight .field-group.gc-5,
  .field-grid-tight .field-group.gc-6,
  .field-grid-tight .field-group.gc-notes { grid-column: auto; }
}

/* The "you've touched this" highlight for Net Worth History's entry form
   (see net_worth_entry_highlight.js) -- reuses the app's existing green
   "success" semantic rather than inventing a new color. */
.field-grid-tight input.field-touched {
  border-color: var(--success);
  background: var(--success-soft);
}

/* min-width so a table-cell currency input (net worth snapshots, extra
   expenses) doesn't clip its own value -- width:100% alone shrinks the
   input to whatever the auto-layout column ends up giving it, which for
   an <input> (unlike plain text) isn't based on its value's length, so a
   figure like "$175,489" was rendering as "$175,48" with the last digit
   cut off inside the box. */
.currency-input { text-align: right; font-variant-numeric: tabular-nums; min-width: 5.5rem; }

.text-good { color: var(--success); }
.text-bad { color: var(--danger); }
.nowrap { white-space: nowrap; }

/* ==========================================================================
   Stat tiles + Retirement chart
   ========================================================================== */

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.15rem;
}
.stat-label { color: var(--text-muted); font-size: 0.82rem; font-weight: 600; }
.stat-value { font-size: 1.6rem; font-weight: 700; margin-top: 0.2rem; }
.stat-sub { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.15rem; }
.stat-tile.status-good .stat-value { color: var(--success); }
.stat-tile.status-bad .stat-value { color: var(--danger); }

/* Forces every tile onto one row -- Weather's 7-day forecast, one tile
   per day. Unlike a scrolling row, these shrink evenly to fit (no
   min-width floor) so all 7 stay visible without a horizontal scrollbar,
   even on a narrower viewport. */
.stat-grid-nowrap { display: flex; flex-wrap: nowrap; gap: 0.5rem; margin-bottom: 1.5rem; }
.stat-grid-nowrap .stat-tile { flex: 1 1 0; min-width: 0; padding: 0.7rem 0.4rem; text-align: center; }

/* Home page tiles -- indented sub-item links, same treatment as the
   sidebar's nav-group-links, single line each (no wrap onto a second
   line for longer labels like "Shopping List History"). */
.home-tile-links {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.75rem;
  padding-left: 0.9rem;
}
.home-tile-link {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  white-space: nowrap;
}
.home-tile-link svg { flex-shrink: 0; opacity: 0.75; color: var(--text-muted); }

/* Wider minimum than the default .stat-grid (200px) -- at 200px, longer
   single-line labels like "Shopping List History" don't fit and spill
   past the card's edge. */
.home-tile-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

.chart-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; margin-bottom: 1.5rem; }
.chart-card svg { width: 100%; height: auto; display: block; }
.chart-gridline { stroke: var(--border); stroke-width: 1; }
.chart-zeroline { stroke: var(--text-muted); stroke-width: 1; }
.chart-line { fill: none; stroke: var(--primary); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.chart-area { fill: var(--primary); opacity: 0.1; }
.chart-marker { stroke: var(--text-muted); stroke-width: 1; stroke-dasharray: 4 3; }
.chart-tick { fill: var(--text-muted); font-size: 10px; }
.chart-endlabel { fill: var(--text); font-size: 11px; font-weight: 600; }
.chart-marker-label { fill: var(--text-muted); font-size: 10px; }
.chart-hover-dot { fill: transparent; }
.chart-hover-dot:hover { fill: var(--primary); fill-opacity: 0.25; }

/* ==========================================================================
   Quick-add chat bubble (#chat)
   ========================================================================== */

#chat-widget { position: fixed; right: 1.25rem; bottom: 1.25rem; z-index: 80; }

#chat-bubble-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.25rem;
  height: 3.25rem;
  border-radius: 999px;
  border: none;
  background: var(--primary);
  color: var(--primary-contrast);
  box-shadow: var(--shadow-md);
  cursor: pointer;
}
#chat-bubble-toggle:hover { background: var(--primary-hover); }
/* Swaps to an X while the panel's open, same affordance as a dialog's
   own close button, without needing a second element layered on top. */
#chat-bubble-toggle.chat-bubble-open svg { display: none; }
#chat-bubble-toggle.chat-bubble-open::before {
  content: "";
  width: 16px;
  height: 16px;
  background: currentColor;
  clip-path: polygon(
    10% 0%, 0% 10%, 40% 50%, 0% 90%, 10% 100%, 50% 60%,
    90% 100%, 100% 90%, 60% 50%, 100% 10%, 90% 0%, 50% 40%
  );
}

#chat-panel {
  position: absolute;
  right: 0;
  bottom: calc(100% + 0.75rem);
  width: min(22rem, calc(100vw - 2.5rem));
  height: min(28rem, calc(100vh - 8rem));
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
/* #chat-panel's own `display: flex` above is an ID selector, which beats
   the browser's built-in `[hidden] { display: none }` rule (an ID beats
   an attribute selector regardless of source order) -- so the panel was
   rendering open unconditionally, and closing it (JS just sets
   .hidden = true) visibly did nothing. This is what actually makes
   hidden work again. */
#chat-panel[hidden] { display: none; }

#chat-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem 0.9rem;
  font-weight: 700;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}

#chat-messages {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.chat-msg {
  max-width: 85%;
  padding: 0.5rem 0.75rem;
  border-radius: 12px;
  font-size: 0.88rem;
  line-height: 1.35;
  white-space: pre-wrap;
}
.chat-msg-assistant { align-self: flex-start; background: var(--surface-2); color: var(--text); }
.chat-msg-user { align-self: flex-end; background: var(--primary); color: var(--primary-contrast); }

#chat-form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.6rem;
  border-top: 1px solid var(--border);
}
#chat-form input { margin: 0; flex: 1 1 auto; min-width: 0; }
#chat-mic-btn, #chat-send-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  flex-shrink: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}
#chat-mic-btn:hover, #chat-send-btn:hover { background: var(--surface-2); color: var(--text); }
#chat-send-btn:disabled { opacity: 0.5; cursor: default; }
/* Recording -- a plain solid red dot pulse is enough to read as "it's
   listening", no need for a differently-shaped icon. */
#chat-mic-btn.chat-mic-listening { color: var(--danger); animation: chat-mic-pulse 1.2s ease-in-out infinite; }
@keyframes chat-mic-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

@media (max-width: 480px) {
  /* A floating ~22rem panel doesn't leave room for anything else on a
     phone screen, so it goes full-width and anchors to the bottom
     (where the bubble itself lives) instead -- but only about half the
     screen tall, not a full-page takeover (Tim: "on the PWA the chat
     window takes up the whole page, I want it only taking up maybe
     half"). */
  #chat-panel { position: fixed; left: 0.75rem; right: 0.75rem; bottom: calc(4.3rem + env(safe-area-inset-bottom) + 0.5rem); top: auto; width: auto; height: 52vh; }
}

/* ==========================================================================
   Responsive
   ========================================================================== */

@media (max-width: 900px) {
  .app-shell { position: relative; }

  .sidebar-toggle {
    display: flex;
    position: fixed;
    top: 0.6rem;
    left: 0.6rem;
    z-index: 50;
    background: var(--sidebar-bg);
    border: 1px solid var(--sidebar-border);
  }
  .sidebar-close { display: flex; }

  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 70;
    width: 260px;
    max-width: 82vw;
    transform: translateX(-100%);
    transition: transform 0.18s ease;
    box-shadow: var(--shadow-md);
  }
  .sidebar.open { transform: translateX(0); }

  .sidebar-scrim {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.35);
    z-index: 65;
  }
  .sidebar.open ~ .sidebar-scrim,
  .sidebar-toggle[aria-expanded="true"] ~ .sidebar-scrim {
    display: block;
  }

  main { padding: 1.25rem 1.25rem 1.25rem 1.25rem; padding-top: 3.5rem; }

  /* Fixed dock, full width, safe-area-aware so it clears the home
     indicator on notched phones when installed as a PWA. */
  .bottom-dock {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 60;
    justify-content: space-around;
    align-items: center;
    background: var(--sidebar-bg);
    border-top: 1px solid var(--sidebar-border);
    padding: 0.35rem 0.5rem calc(0.35rem + env(safe-area-inset-bottom));
  }
  .bottom-dock a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    text-decoration: none;
  }
  .bottom-dock a svg { opacity: 0.75; color: var(--sidebar-text-muted); }
  .bottom-dock a:hover { background: var(--sidebar-hover); }
  .bottom-dock a.active svg { opacity: 1; color: var(--primary); }

  /* Room at the bottom of the page so content/the chat bubble never sit
     underneath the now-fixed dock. */
  main { padding-bottom: calc(4.3rem + env(safe-area-inset-bottom)); }
  #chat-widget { bottom: calc(4.3rem + env(safe-area-inset-bottom) + 0.5rem); }
}

@media (max-width: 640px) {
  .ingredient-row, .dynamic-row { grid-template-columns: 1fr 1fr; }
  th, td { font-size: 0.85rem; padding: 0.2rem 0.5rem; }

  /* Nice-to-have columns (e.g. the Cooking Shopping List's Store) push
     some tables wider than a phone screen, shoving the "got it" checkbox
     and delete button off the visible edge -- drop them on narrow
     screens so the checkbox stays reachable without scrolling. */
  .hide-narrow { display: none; }

  /* Stocks watchlists: the $/% "show in site-wide ticker" checkbox
     columns are a nice-to-have, not something worth fighting a phone
     screen's width for -- same "drop it on narrow screens" call as
     .hide-narrow above, just not reusing that class since these columns
     also need .stock-grid's own track list shortened to match (a plain
     display:none leaves the *tracks* themselves still reserving their
     width -- see .stock-grid-cell's grid layout). Symbol/Price/Chg get a
     little tighter too so the trio Tim actually wants at a glance --
     "price and % [change] without horizontal scroll" -- fits without
     needing those two columns gone as well. */
  .stock-grid { grid-template-columns: 1.5rem 7rem max-content max-content minmax(0, 1fr) 2.5rem; }
  .stock-ticker-format-col { display: none; }
  /* Price/Chg tracks above are now max-content (sized to the number
     itself, not a fixed rem guess) -- that alone pulls them close, but
     each cell's own left/right padding still leaves a gap between them.
     Trimming just the touching sides keeps price and % read as one
     tightly-coupled unit (Tim: "price and % tightly coupled ... instead
     of ... everywhere") without touching the padding on their outer
     sides that still separates them from Symbol/Name. */
  .stock-grid-row > .stock-grid-cell:nth-child(3) { padding-right: 0.25rem; }
  .stock-chg-cell { padding-left: 0.25rem; }

  .grocery-check { width: 1.6rem !important; height: 1.6rem; flex-shrink: 0; }
  .grocery-item { padding: 0.85rem 1rem; }
  .day-card { flex-direction: column; align-items: flex-start; }
  .btn { padding: 0.65rem 1.1rem; }

  /* Inventory's bulk-action bar: "N selected", the target-location
     picker, and 3 buttons (Move/Add to menu/Delete) don't fit on one
     phone-width line -- stack each onto its own full-width row instead
     of letting them overflow past the screen edge. */
  .bulk-move-bar #move-selected-count { flex: 1 1 100%; }
  .bulk-move-bar select { flex: 1 1 100%; max-width: none; }
  .bulk-move-bar .btn { flex: 1 1 auto; }
}
