/* Space Grotesk + Space Mono are loaded via <link> in index.html (not
   @import here) so the fetch runs in parallel with style.css instead of
   behind it. They're a matched pair from the same foundry, used only on
   Today (see .today-view below) as a scoped typographic identity. */

/* ─── Design Tokens ──────────────────────────────────────── */
:root {
  --bg:           #0d0d0f;
  --surface:      #16161a;
  --surface-2:    #1e1e24;
  --border:       #2c2c35;
  --text:         #f0eff4;
  --text-muted:   #8b8a96;
  /* Was #4a4a55: 2.06:1 on a card, against a 4.5:1 floor — and not decoration,
     it draws the goal figure beside every macro ("/ 166g"), the DAILY GOAL
     heading and the trend notes. The nav bar was moved off it for exactly this
     reason and the other ~100 uses were left behind. #7e7e90 is 4.53:1 on a
     card and 4.94:1 on the page. The gap to --text-muted (5.30:1) is narrower
     than it was, so the hierarchy now leans on size and weight, which is where
     it should have been: a tier you cannot read is not a tier. */
  --text-faint:   #7e7e90;
  --accent:       #c8b8ff;
  --accent-hover: #b8a4ff;
  --accent-dim:   #2a2540;
  --protein:      #1E88E5;   /* blue      — lean, muscle */
  --carbs:        #FFA726;   /* orange    — energy fuel  */
  --fat:          #FFCA28;   /* yellow    — calorie dense */
  --fiber:        #66BB6A;   /* green     — plant / health */
  --success:      #5dca8a;
  --warning:      #ffb347;
  --over:         #cf6b41;   /* burnt orange — calories exceeded (5.0:1 on --surface) */
  /* Ring "past this point" shades: a darker, more saturated version of each
     ring's own hue. One rule for all three, so nothing has to be learned twice.
     Per THEME rather than one fixed set — light mode's bases are already dark,
     so a single value would come out lighter than the base there and invert the
     whole signal. */
  /* ── Calorie rings ──
     Scoped to the rings on purpose: --accent stays exactly as it is, because
     it is the app's colour in 28 other places and none of them asked to move.
     Only the three arcs are tuned, and only relative to each other.

     Built by holding perceptual lightness and chroma CONSTANT and varying only
     hue, which is what makes different hues read as one family. The old set was
     three colours that happened to share a card: measured in OKLCH the base
     arcs spread L 0.286 (lavender at L 0.82, workout track at L 0.54) where a
     family holds together under about 0.06. This set spreads L 0.001.
     The hues themselves are untouched — violet, green and orange each mean
     something here (goal, eat target, burn) and that meaning is the point. */
  --ring-goal:         #c7b8ff;   /* L .82 C .10 H 294 */
  --ring-target:       #8ed8a8;   /* L .82 C .10 H 155 */
  --ring-workout:      #ecba7a;   /* L .82 C .10 H  73 — the allowance you earned */
  /* Past this point: same hues, one step down in lightness and up in chroma.
     The exceeded step used to sit at L .60, which made the workout fill a
     genuine burnt orange — rusty rather than bright. Lifting the exceeded step
     to L .66 alone would have dropped the arcs to 1.56:1 against their bases,
     under the floor; lifting the BASES by the same amount buys that separation
     back, so the whole family moved up together rather than the orange alone.
     The fill also moved from hue 48 to 58: 48 is a red-orange, and most of what
     read as "burnt" was that redness, not the darkness. */
  --ring-goal-over:    #9a79ef;   /* L .66 C .17 */
  --ring-target-over:  #00af61;   /* L .66 C .16 */
  --ring-workout-over: #dc7200;   /* L .66 C .17 H 58 — eaten into the allowance */
  --error:        #ff6b6b;
  --on-accent:    #0d0d0f;   /* ink that sits on top of an --accent fill */
  --gold:         #ffd166;   /* favourite star / favourited row edge */
  --alert:        #f9a35c;   /* "this needs your attention" text, softer than --error */
  --mint:         #7ee7c7;   /* scanned-food tag */
  --shadow:       rgba(0,0,0,.5);
  /* Profile accents are chosen to glow on near-black, so on a light ground the
     same hex is pale text on white. Every inline `color: <accent>` runs through
     color-mix() with this: 100% leaves it untouched in dark, 52% pulls it
     toward black in light. Keeps one accent per profile instead of two. */
  --ink-mix:      100%;
  /* 1 = 100%. Set per profile from Settings → Appearance. See the zoom block
     under App Shell for why this isn't just a root font-size. */
  --ui-scale:     1;
  --radius-sm:    8px;
  --radius-md:    14px;
  --radius-lg:    20px;
  --radius-full:  9999px;
  --nav-h:        64px;
  --safe-bottom:  env(safe-area-inset-bottom, 0px);
  --safe-top:     env(safe-area-inset-top, 0px);
  /* Read through --safe-top, never env() directly: the standalone override
     below is the whole point, and a rule that reaches past it double-counts. */
  --font-display: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono:    'Space Mono', ui-monospace, 'SF Mono', monospace;
}

/* ─── Light theme ────────────────────────────────────────── */
/* Set by the inline boot script in index.html (so the page never flashes dark
   before app.js parses) and toggled in Settings → Appearance. Only tokens move
   here — every rule below already reads through them, which is why this block
   is the whole theme. Deliberately NOT following prefers-color-scheme: the
   choice is explicit, and an app that flips itself at sunset is a surprise. */
:root[data-theme="light"] {
  --bg:           #f6f5f9;
  --surface:      #ffffff;
  --surface-2:    #efedf4;
  --border:       #e0dde8;
  --text:         #1a1922;
  --text-muted:   #6a6779;
  --text-faint:   #726e86;   /* was #9b97a9 — 2.84:1 on white. Now 4.89:1, and 4.51:1 on --bg. */
  --accent:       #6b4fd6;   /* the dark-mode lavender is unreadable on white */
  --accent-hover: #5a3fc0;
  --accent-dim:   #ece7fb;
  --protein:      #1565C0;
  --carbs:        #D97706;
  --fat:          #A98200;   /* #FFCA28 vanishes on white */
  --fiber:        #388E3C;
  --success:      #267e4f;
  --warning:      #a05f00;
  --over:         #c2410c;
  /* Darker again, because these bases already are. #8b5cf6 would read LIGHTER
     than light mode's #6b4fd6 accent and say the opposite of what it means. */
  /* Same construction, pitched dark-on-white instead of light-on-dark:
     bases at L .58, exceeded at L .40. Spread L 0.002. */
  --ring-goal:         #856fc6;
  --ring-target:       #2c965d;
  --ring-workout:      #ae7200;
  --ring-goal-over:    #5f41a2;
  --ring-target-over:  #006c34;
  --ring-workout-over: #8c3f00;
  --error:        #d64545;
  --on-accent:    #ffffff;
  --gold:         #a97900;
  --alert:        #c2410c;
  --mint:         #0f8f70;
  --shadow:       rgba(24,20,40,.16);
  /* 45, not 52. accentInk() text sits on a wash of the SAME accent, and f104ebe
     raised those washes (pill 12%->20%, phase chip 9%->15%) to make the chips
     findable — which lightens the ground under the text as well. At 52% the
     pill fell to 4.39:1 on #7ee7c7 and 4.14:1 on #ffd98e, under the 4.5 floor;
     the gold one had been under it since before the change. 45% clears every
     accent with room: pill 5.20:1 worst case, phase chip 6.18:1. */
  --ink-mix:      45%;
}

/* ─── Accessibility overrides ────────────────────────────── */
/* All three are per-profile toggles in Settings → Appearance, applied as
   attributes on <html> by applyA11y() in app.js. */

/* Bolder text. Body is the only declaration needed: almost nothing in the app
   sets its own font-weight, so raising the inherited value carries everywhere,
   while headings and numbers that DO set 700 stay exactly as they are. */
:root[data-bold="1"] body { font-weight: 600; }

/* Stronger contrast. The muted greys are deliberately dim to build hierarchy,
   which makes them the first thing to vanish with poor eyesight. Must sit
   AFTER the light block — same specificity, so source order decides. */
:root[data-contrast="1"] {
  --text-muted: #c6c4d2;
  --text-faint: #9c99ab;
  --border:     #45454f;
}
:root[data-theme="light"][data-contrast="1"] {
  --text-muted: #3b3949;
  --text-faint: #55526a;
  --border:     #b3aec3;
}

/* Bigger tap targets — independent of text size, for unsteady hands. 44px is
   Apple's minimum. The exclusions are elements that are already large
   (numpad, nav) or deliberately small and square (colour swatches), where a
   min-height would only make them lopsided. */
:root[data-targets="1"] button:not(.numpad-key):not(.nav-item):not(.accent-swatch) {
  min-height: 44px;
}
:root[data-targets="1"] .sheet-close,
:root[data-targets="1"] .fav-toggle,
:root[data-targets="1"] .fav-btn { min-width: 44px; }

/* ─── Reset ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
  font-size: 14px;
  -webkit-tap-highlight-color: transparent;
  /* Turn WebKit's text autosizing OFF. Reported from the phone as "when phone
     rotate, the fonts got messed up. Larger", and it is not this app doing it:
     nothing in the JS listens for a rotation and nothing writes a font size.
     iOS inflates text on its own when the viewport gets wider, which on a
     portrait-shaped app means every rotation to landscape blows the type up and
     every rotation back shrinks it. width=device-width does not stop it — the
     idempotent autosizer added in iOS 13 scales from the viewport width itself.
     100%, never none: none also stops the type growing for someone who has
     turned text size up in Settings, and the person this app is built for is
     exactly that someone. 100% means "leave my sizes alone", which is all that
     was wrong here. (The same line normalize.css has carried for a decade,
     with the same one-line reason.) */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100dvh;
  overflow: hidden;
}
button { cursor: pointer; border: none; background: none; color: inherit; font: inherit; }
input, select, textarea { font: inherit; color: inherit; }
a { color: var(--accent); text-decoration: none; }
img { display: block; max-width: 100%; }

/* ─── Utilities ──────────────────────────────────────────── */
.hidden { display: none !important; }
.label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.num {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.muted { color: var(--text-muted); }

/* WHY THERE IS NO STANDALONE OVERRIDE HERE.
   v235 zeroed --safe-top for standalone, reasoning from one measurement on an
   iPhone 15 Plus: screen 932, viewport 873, top inset 59, and 932 - 873 = 59,
   so iOS looked to have already moved the view below the status bar and to be
   reporting the inset a second time.
   The device disagreed. With the padding gone the name pill went under the
   Dynamic Island — which means the viewport is 873 tall but still anchored at
   the TOP of the screen, not below the status bar. The size told us how tall
   the viewport is and nothing about where it starts, and the difference
   happening to equal the inset is what made a guess look like arithmetic.
   So the inset is spent once, here, the way it was before: the page starts
   under the island and pads itself out. Do not remove it again without a
   screenshot of the top of the screen — the numbers alone cannot tell these
   two arrangements apart. */

/* ─── App Shell ──────────────────────────────────────────── */
.app {
  /* inset:0, not height:100dvh — reverting 851372f, which was reasoned from the
     spec and is wrong on the device.
     Measured on an iPhone 15 Plus home-screen app: screen 932 tall, and both
     innerHeight and 100dvh report 873, exactly 932 minus the 59px top inset.
     But the page is NOT laid out below the status bar — it starts at the top of
     the screen (which is why .profile-bar has to pad itself clear of the
     island) and .bottom-nav's `bottom: 0` lands flush at the true bottom, 932.
     So dvh under-reports the viewport by the top inset, and sizing the shell
     from it left the column 59px short: the scroll area ended above the nav
     instead of behind it, and the bottom of the screen went unused. That is the
     "not fully at the bottom" report.
     inset:0 asks no questions. A fixed element's box resolves against the
     viewport the browser is actually painting, whatever it decides that is,
     which is the one number here that has been right all along. */
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
}
/* UI scale. Every size in this app is px — 88 declarations in here and 234
   more in inline styles — so scaling the root font-size does nothing at all.
   zoom is a real layout scale: text, padding, buttons and icons grow together,
   which is what "make it bigger" has to mean if a fixed-height row isn't going
   to clip the label inside it.
   Applied to the two scrolling content areas rather than <body> on purpose. A
   position:fixed element inside a zoomed subtree resolves its percentage width
   against the UNzoomed viewport and then paints scaled, so anything shaped like
   `left:0; right:0` ends up wider than the screen. .bottom-nav and
   .toast-container are exactly that shape — they scale by calc() below instead.
   ponytail: the thorough fix is px→rem everywhere, which is 322 edits and a
   re-test of every screen. Do that if zoom ever fights a browser update.
   The nav, toasts and profile pill therefore carry * var(--ui-scale) inline in
   their own rules below rather than being overridden from here. */
.view-container, .sheet-content { zoom: var(--ui-scale); }

.view-container {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  padding-bottom: calc(var(--nav-h) + var(--safe-bottom) + 16px);
  scrollbar-width: none;
}
.view-container::-webkit-scrollbar { display: none; }

/* ─── Bottom Navigation ──────────────────────────────────── */
.bottom-nav {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  height: calc(var(--nav-h) * var(--ui-scale) + var(--safe-bottom));
  padding-bottom: var(--safe-bottom);
  background: var(--surface);
  border-top: 1px solid var(--border);
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  z-index: 100;
}
.nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* --text-faint used to be 2.06:1 on dark and 2.84:1 on light — well under
     the 4.5:1 floor — and this is the most-tapped control in the app. The token
     itself now clears the floor, but --text-muted (5.31:1) stays the right
     choice here: it reads as "not the active tab" next to --accent. */
  color: var(--text-muted);
  font-size: calc(10px * var(--ui-scale));
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 8px 4px;
  transition: color 0.15s;
}
.nav-item.active { color: var(--accent); }
.nav-icon { width: calc(22px * var(--ui-scale)); height: calc(22px * var(--ui-scale)); }

/* ─── View transition ────────────────────────────────────── */
/* opacity:1 is the resting state and the animation only decorates it. With
   `forwards` and no base opacity, the ONLY route to a visible view ran through
   the animation — so a view mounted while the PWA was backgrounded, where iOS
   pauses animations, could sit at the 0% keyframe and render invisible. */
.view-enter {
  opacity: 1;
  animation: viewIn 150ms ease-out;
}
@keyframes viewIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .view-enter { animation: none; }
}

/* ─── Cards ──────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 16px;
}
.card + .card { margin-top: 12px; }

/* ─── Section headers ────────────────────────────────────── */
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 16px 8px;
}
.section-header h2 {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ─── Inputs ─────────────────────────────────────────────── */
.input {
  width: 100%;
  height: 48px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0 14px;
  font-size: 14px;
  color: var(--text);
  outline: none;
  transition: border-color 0.15s;
}
.input:focus { border-color: var(--accent); }
.input::placeholder { color: var(--text-faint); }

/* ─── Buttons ────────────────────────────────────────────── */
.btn {
  height: 48px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 600;
  padding: 0 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transition: opacity 0.15s, transform 0.1s;
}
.btn:active { transform: scale(0.97); }
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-secondary {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
}
.btn-ghost { background: transparent; color: var(--accent); }
.btn-danger { background: rgba(255,107,107,.12); color: var(--error); }
.btn-full { width: 100%; }
.btn:disabled { opacity: 0.4; pointer-events: none; }

/* ─── Calorie Ring ───────────────────────────────────────── */
.calorie-ring {
  width: 140px; height: 140px;
  transform: rotate(-90deg);
}
.ring-bg { fill: none; stroke: var(--surface-2); stroke-width: 10; }
.ring-fill {
  fill: none;
  stroke: var(--ring-goal);
  stroke-width: 10;
  stroke-linecap: round;
  /* stroke-dasharray set via inline style= so CSS cannot override it */
  transition: stroke-dasharray 600ms ease-out;
}
/* Overflow arc drawn on top of the base fill — only the exceeded slice, in a
   darker, more saturated version of that ring's own hue.
   It used to be the SAME token plus filter: brightness(1.25), which on a pale
   #c8b8ff clipped to near-white: the slice that means "you went over" was the
   lightest thing on the card, and read as a highlight rather than a warning.
   (The comment above calRingSvg claimed it used --over. It never did.)
   Still physically thicker so the boundary is findable without relying on
   colour alone — which is the part that has to keep working at 200% zoom. */
.ring-fill.ring-over         { stroke: var(--ring-goal-over);   stroke-width: 13; filter: drop-shadow(0 0 5px var(--ring-goal-over)); }
.ring-fill.target.ring-over  { stroke: var(--ring-target-over); stroke-width: 13; filter: drop-shadow(0 0 5px var(--ring-target-over)); }
.ring-center {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  pointer-events: none;
  /* Inherited line-height turned a wrapped "/ 1041 kcal · nothing yet" into
     five tall lines that spilled past the arc. Tightening only ever reduces
     the block's height. */
  line-height: 1.15;
}
.ring-wrap-relative {
  position: relative;
  display: flex; align-items: center; justify-content: center;
}
.ring-kcal { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
.ring-goal { font-size: 12px; color: var(--text-muted); }
/* "640 left" — the number you actually act on, and it was the quietest thing
   on the card: 11px at --text-faint, dimmer than the goal above it. Now the
   second-loudest thing in the ring, behind the intake figure itself. Colour is
   set inline because it carries meaning (left / hit / over). */
.ring-hint {
  font-size: 14px;
  font-weight: 700;
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
}
/* The two side rings are 82px across and carry three lines. At the full 14px
   the hint was nearly as loud as the intake figure above it (15px) and the
   stack filled the circle corner to corner — the "wonky" look. Smaller here,
   and a tighter margin: the hierarchy inside a small ring should read number,
   then hint, then denominator, and it has to fit the arc. */
.ring-hint-sm { font-size: 10px; margin-top: 2px; }

/* ─── Three-ring calorie layout (goal · burned · eat target) ── */
.tri-rings {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.ring-col { display: flex; flex-direction: column; align-items: center; }
.ring-title {
  font-size: 10px; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--text-faint);
  margin-bottom: 6px; text-align: center;
}
/* 92, not 82: three lines of text need the extra 10px of chord more
   than the layout needs the 10px of gap. */
.calorie-ring.ring-sm { width: 92px; height: 92px; }
/* The workout ring reads differently from the other two. Its TRACK is the
   allowance you earned (amber, full circle) and its FILL is how much of that
   allowance you have eaten (orange, drawn over it). Before, the earned amount
   was invisible — a grey track that filled amber — so the ring answered "how
   much of it have you eaten" without ever showing how much there was.
   .earned only goes on when a workout actually exists: a full amber circle on
   a rest day would claim calories that were never earned. */
/* The earned track no longer needs dimming to separate from the fill it sits
   under. It did when both were pinned to --warning/#f97316 at nearly the same
   lightness (1.6:1); with the fill moved a full step down in the family, a
   proper bright amber clears it at 2.1:1 and joins the other two bases. */
.ring-bg.earned    { stroke: var(--ring-workout); }
.ring-fill.workout { stroke: var(--ring-workout-over); }
.ring-fill.target  { stroke: var(--ring-target); }
.ring-kcal-sm { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
.ring-goal-sm { font-size: 9px; color: var(--text-muted); }

/* Phones: three rings (316px wide) overflow the card and clip to a single
   visible ring. Scale them with the viewport so all three always fit, down
   to a ~320px screen. The SVGs use a viewBox so they scale cleanly. */
@media (max-width: 430px) {
  .tri-rings { gap: 2px; }
  .tri-rings .calorie-ring         { width: clamp(94px, 30vw, 140px); height: clamp(94px, 30vw, 140px); }
  .tri-rings .calorie-ring.ring-sm { width: clamp(62px, 21vw, 92px);  height: clamp(62px, 21vw, 92px); }
  .tri-rings .ring-kcal    { font-size: clamp(18px, 6vw, 26px); }
  .tri-rings .ring-kcal-sm { font-size: clamp(12px, 4vw, 15px); }
  /* The captions never scaled with the rings, so "nothing yet" broke in two. */
  .tri-rings .ring-goal    { font-size: clamp(9px, 2.8vw, 12px); }
  .tri-rings .ring-goal-sm { font-size: clamp(8px, 2.3vw, 9px); }
  /* Scales with its siblings so it can't outgrow the arc: .ring-center wraps
     at the ring's own width, and a wrapped hint is what spilled past the
     circle last time. Worst realistic string is "12,345 left". */
  .tri-rings .ring-hint    { font-size: clamp(11px, 3.6vw, 14px); }
  .tri-rings .ring-hint-sm { font-size: clamp(8px, 2.6vw, 10px); }
}

/* ─── Macro Bar ──────────────────────────────────────────── */

/* Confirms a row just got added when the list may be off-screen (batch
   builder on a phone) — the highlight fades out on its own. */
@keyframes flash-in {
  from { background: var(--accent-dim); }
  to   { background: transparent; }
}
.flash-in { animation: flash-in 1.4s ease-out; border-radius: var(--radius-sm); }

/* ─── Progress bar / pill-button primitives ─────────────────
   Named after the water tracker that originally introduced them; that
   feature is gone but batches (remaining-portion bar) and the quick-log
   gram picker (pill buttons) still use these classes. */
.water-bar-outer {
  flex: 1;
  height: 8px;
  background: var(--surface-2);
  border-radius: var(--radius-full);
  overflow: hidden;
}
.water-bar-fill {
  height: 100%;
  background: var(--fat);
  border-radius: var(--radius-full);
  transition: width 400ms ease-out;
  max-width: 100%;
}
.water-btn {
  height: 36px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 600;
  padding: 0 14px;
  color: var(--fat);
  transition: background 0.1s;
}
.water-btn:active { background: rgba(98,216,255,.12); }

/* ─── IronLog Panel ──────────────────────────────────────── */
.ironlog-panel { border-color: var(--accent-dim); }
.ironlog-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 4px 0;
}
.ironlog-row + .ironlog-row { border-top: 1px solid var(--border); margin-top: 6px; padding-top: 10px; }

/* ─── Meal Sessions ──────────────────────────────────────── */
.meal-session { border-radius: var(--radius-md); overflow: hidden; border: 1px solid var(--border); margin: 0 16px 12px; }
.meal-session-header {
  background: var(--surface);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}
.meal-session-title { font-size: 13px; font-weight: 600; }
.meal-session-kcal  { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* Right-aligned by margin rather than space-between, so the macros can be
   re-ordered onto their own line below without the title losing its place. */
.meal-session-macros  { margin-left: auto; white-space: nowrap; }
.meal-session-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* The title button is flex:1 — basis 0 — so it only ever got the space the
   macros didn't want, and "671 kcal · 62g P · 54g C · 19g F" wants ~224px of
   monospace. On a phone that left ~45px for the title, breaking "Meal 1" and
   "8:27 AM" across two lines each and colliding them with the figures. Drop
   the macros to their own full-width line; the title keeps line one. */
@media (max-width: 430px) {
  .meal-session-macros {
    order: 3;
    flex: 1 1 100%;
    margin-left: 0;
    white-space: normal;
  }
}

.meal-entry {
  display: flex;
  /* Wraps so the button cluster drops to its own line instead of eating the
     name. The four buttons never shrink (flex-shrink:0) and grow with the text
     size — 132px at Normal, 198px at Largest — while the row's content box
     shrinks from 356px to 227px. .meal-entry-info had `flex: 1`, i.e. a basis
     of 0, so it absorbed the whole squeeze: the food name measured 108px,
     67px, 25px and finally 0 across the four sizes. At Largest the meal list
     showed a weight, some macros and four buttons, and not what you ate. */
  flex-wrap: wrap;
  align-items: center;
  padding: 11px 14px;
  background: var(--surface);
  gap: 10px;
  position: relative;
  overflow: hidden;
}
.meal-entry + .meal-entry { border-top: 1px solid var(--border); }

/* ── Dragging a food into another meal ──────────────────────────────────────
   `manipulation` rather than `none`: it drops the 300ms tap delay without
   taking scrolling away from the rows, which cover most of the day's screen.
   The drag suppresses scroll from JS once it has armed (the non-passive
   touchmove in log.js), so the browser keeps it until then. */
.meal-entry { touch-action: manipulation; }

/* The row left behind. Dimmed rather than removed, so the list does not reflow
   under the finger mid-drag and the food is still visibly where it came from. */
.meal-entry.dragging { opacity: .32; }

/* The thing that actually follows the finger. On <body>, so it escapes both
   `.meal-session`'s overflow:hidden clip and the `zoom` on the scroll
   container — log.js sizes it unzoomed and scales it back up, which is why the
   origin has to be the top-left corner it was measured from.
   pointer-events:none is not decoration: elementFromPoint is how the drop
   target is found, and without it the ghost is the only thing ever under the
   finger. */
.entry-ghost {
  position: fixed;
  z-index: 60;
  transform-origin: top left;
  pointer-events: none;
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 28px rgba(0,0,0,.45);
  border: 1px solid var(--accent);
  opacity: .96;
}

/* The meal under the finger. A ring and a tint, not a colour swap — this has
   to be legible with high contrast on and at 150%, and it sits on a card whose
   own background already changes with the theme. */
.meal-session.drop-ok {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
  background: var(--accent-dim);
}
/* `1 1 0` with a FLOOR, not `flex: 1` (which is `1 1 0` with min-width 0).
   The floor is what makes the row wrap instead of crushing the name: a flex
   line breaks on hypothetical size, which is the basis clamped by min-width, so
   a floor of 5em means the button cluster drops to its own line the moment the
   name would go under it. `1 1 auto` doesn't work here — with an auto basis the
   line breaks at max-content and the row wraps even at Normal, making every
   list taller for everyone. Measured, name width by text size:
       flex:1        108 / 67 / 25 / 0     (one line, then nothing)
       1 1 auto      250 / 230 / 209 / 238 (wraps at every size — too eager)
       1 1 0, 5em    108 / 230 / 209 / 182 (one line at Normal, wraps above it)
   Normal is byte-for-byte what it was: a 59px single-line row. */
.meal-entry-info { flex: 1 1 0; min-width: 5em; }
.meal-entry-actions { display: flex; gap: 4px; flex-shrink: 0; margin-left: auto; }
.meal-entry-name {
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.meal-entry-sub { font-size: 12px; color: var(--text-muted); margin-top: 2px; font-variant-numeric: tabular-nums; }
.meal-entry-kcal { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; flex-shrink: 0; }
.meal-entry-sub span { white-space: nowrap; }
.meal-session-add {
  width: 100%;
  padding: 10px 14px;
  background: var(--surface);
  border-top: 1px solid var(--border);
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 8px;
}
.meal-session-add:active { background: var(--accent-dim); }
/* The Add Food / Quick Log pair. `.meal-session-add` sets width:100% because it
   is also used alone, spanning the card — and a `flex: 0 0 auto` item sized from
   that width came out as wide as the whole card, so the second button's box ran
   61px off a 390px screen at Normal and 92px at Largest. Invisible, because the
   card clips it, but it also stopped the labels sitting on one line: "Add Food"
   broke in two at the larger sizes. width:auto and a real flex hand the row back
   to the layout — measured, the row gets SHORTER at every size (56->39 at
   Normal, 84->58 at Largest) and nothing leaves the card. */
.meal-session-footer { display: flex; flex-wrap: wrap; border-top: 1px solid var(--border); }
.meal-session-footer > .meal-session-add { width: auto; flex: 1 1 auto; min-width: 0; }

/* ─── New Meal Button ────────────────────────────────────── */
.new-meal-btn {
  margin: 8px 16px 16px;
  border: 1.5px dashed var(--border);
  border-radius: var(--radius-md);
  padding: 14px;
  width: calc(100% - 32px);
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  transition: border-color 0.15s, color 0.15s;
}
.new-meal-btn:active { border-color: var(--accent); color: var(--accent); }

/* ─── Floating log button ────────────────────────────────── */
/* Logging is the whole point of the app, and it used to live at the very
   bottom of Today's scroll — past the rings, bodyweight and IronLog cards.
   This keeps it one thumb-reach away regardless of scroll position.
   z-index sits below the nav (100) and the sheet (201). */
.fab-log {
  position: fixed;
  right: 16px;
  bottom: calc(var(--nav-h) + var(--safe-bottom) + 14px);
  height: 52px;
  padding: 0 22px;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--on-accent);
  font-size: 15px;
  font-weight: 700;
  display: flex;
  align-items: center;
  gap: 7px;
  box-shadow: 0 6px 22px var(--shadow);
  z-index: 90;
  transition: transform .12s ease, opacity .18s ease;
}
.fab-log:active { transform: scale(.94); }
/* Tucked away while the New Meal / Quick Log row is on screen — down there the
   FAB sat right on top of Quick Log, and the same action is already sitting in
   the card underneath it. After :active on purpose: equal specificity, so
   source order decides which transform wins. */
.fab-log.tucked {
  opacity: 0;
  transform: scale(.85);
  pointer-events: none;
}
/* Icon only at the larger text sizes. The label makes the button 116px wide at
   Largest, which is what put it over the fiber row of the goal card on first
   paint. The previous answer was to tuck it there too — but tucked means
   opacity 0 and pointer-events none, so the app's primary action was simply
   absent when you opened it, at exactly the setting a low-vision reader uses.
   A 52px circle in the corner clears the numbers instead of hiding the button. */
.fab-log.compact { padding: 0; width: 52px; justify-content: center; gap: 0; }
.fab-log.compact .fab-label { display: none; }

/* A canvas with no CSS width falls back to the HTML default of 300px, which at
   the largest text size is 450 device px in a 292px card — 109px off the right
   of a 390px screen, with nothing to scroll to it. Chart.js sets an inline
   width once it draws, so this only bites before the first draw and whenever it
   never draws at all: the CDN blocked, offline, or a <details> opened for the
   first time. The charts that had drawn measured 291px and sat inside their
   card; the three that hadn't measured 450px and did not. */
canvas { max-width: 100%; }

/* Stand-in where a chart would be when Chart.js hasn't loaded (offline). */
.chart-offline {
  height: 100%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; color: var(--text-faint);
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
}
/* Clear the FAB so it never covers the last meal card's Add Food row. */
.today-view { padding-bottom: 76px; }

/* ─── Bottom Sheet ───────────────────────────────────────── */
.sheet-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.7);
  z-index: 200;
  opacity: 0;
  transition: opacity 0.3s;
}
.sheet-overlay.visible { opacity: 1; }

.sheet {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  z-index: 201;
  max-height: 90dvh;
  display: flex;
  flex-direction: column;
  transform: translateY(100%);
  transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1);
}
.sheet.open { transform: translateY(0); }

.sheet-handle-bar {
  padding: 12px 0 4px;
  display: flex;
  justify-content: center;
  flex-shrink: 0;
}
.sheet-handle {
  width: 36px; height: 4px;
  background: var(--border);
  border-radius: var(--radius-full);
}
.sheet-content {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 8px 0 calc(16px + var(--safe-bottom));
  -webkit-overflow-scrolling: touch;
}
.sheet-content::-webkit-scrollbar { display: none; }

.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 16px 12px;
}
.sheet-title { font-size: 16px; font-weight: 700; }
.sheet-close {
  width: 32px; height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  display: flex; align-items: center; justify-content: center;
  color: var(--text-muted);
  font-size: 18px;
}

/* ─── Food Search Sheet ──────────────────────────────────── */
.search-bar-wrap { padding: 0 16px 12px; position: sticky; top: 0; background: var(--surface); z-index: 1; }
.search-bar { display: flex; align-items: center; gap: 10px; }
.search-input-wrap { position: relative; flex: 1; }
.search-input-wrap .input { padding-left: 40px; }
.search-icon {
  position: absolute;
  left: 12px; top: 50%;
  transform: translateY(-50%);
  color: var(--text-faint);
  pointer-events: none;
}
.search-tabs {
  display: flex;
  /* Wraps rather than overflowing. It was nowrap with overflow visible, so at
     Largest the Trends metric strip needed 259px in a 195px box and the "Fat"
     tab sat off the edge with no way to scroll to it. The Foods strip got this
     treatment when the same bug was found there; this instance was missed. */
  flex-wrap: wrap;
  gap: 4px;
  row-gap: 6px;
  padding: 0 16px 10px;
}
.search-tab {
  height: 32px;
  padding: 0 14px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 600;
  background: var(--surface-2);
  color: var(--text-muted);
  border: 1px solid var(--border);
  transition: all 0.15s;
}
.search-tab.active {
  background: var(--accent-dim);
  color: var(--accent);
  border-color: var(--accent-dim);
}
/* Foods' tab strip, which flex-wrap could not lay out evenly once there were
   six of them. Measured at 390px: the pills need 535px against 358px of row, so
   they broke two / three / one, with "+ Add" parked beside the first line.
   Ragged, and three lines tall for two lines of content.
   A grid makes every pill the same width and every row full. auto-fit is what
   keeps it honest at larger text sizes: the whole app is scaled with zoom, so
   at 130% the usable width is ~268 CSS px and three 100px columns no longer
   fit — it drops to two columns of two rows rather than overflowing or
   squeezing the labels. Scoped to this strip, because .search-tabs is also the
   day chips, the bug/idea toggle and the Trends metric row, none of which want
   to be a grid. */
.search-tabs.tab-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  gap: 6px;
}
/* The cell decides the width now, so a long label ellipsises instead of
   pushing its pill wider than its neighbours. */
.search-tabs.tab-grid .search-tab {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Food search chips ───────────────────────────────────── */
.fs-chip {
  height: 28px;
  padding: 0 12px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 600;
  background: var(--surface-2);
  color: var(--text-muted);
  border: 1px solid var(--border);
  transition: all 0.15s;
  white-space: nowrap;
}
.fs-chip.active {
  background: var(--accent-dim);
  color: var(--accent);
  border-color: rgba(200,184,255,.3);
}

.food-list-item {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  gap: 12px;
  border-bottom: 1px solid var(--border);
  transition: background 0.1s;
}
.food-list-item:active { background: var(--surface-2); }
.food-list-icon {
  width: 36px; height: 36px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  font-size: 18px;
}
/* Favourites: mark the ROW, not the icon. Swapping the food's icon for a star
   read as "this item is a star" rather than "this item is favourited", cost you
   the at-a-glance food identity, and sat right next to the star toggle. A gold
   edge + tinted tile makes the state louder while every icon stays put. */
.food-list-item.is-fav {
  background: linear-gradient(90deg, rgba(255,209,102,.09), rgba(255,209,102,0) 55%);
  box-shadow: inset 3px 0 0 var(--gold);
}
.food-list-item.is-fav .food-list-icon {
  background: rgba(255,209,102,.16);
  box-shadow: inset 0 0 0 1px rgba(255,209,102,.35);
}
.food-list-item.is-fav:active { background: var(--surface-2); }

.food-list-info { flex: 1; min-width: 0; }
.food-list-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.food-list-sub  { font-size: 12px; color: var(--text-muted); margin-top: 2px; }

/* ─── Quantity Sheet ─────────────────────────────────────── */
.qty-food-name { font-size: 18px; font-weight: 700; padding: 4px 16px 2px; }
.qty-food-brand { font-size: 13px; color: var(--text-muted); padding: 0 16px 16px; }
.qty-input-wrap {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 16px;
  margin-bottom: 8px;
}
.qty-input {
  flex: 1;
  height: 56px;
  background: var(--surface-2);
  border: 1.5px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 0 14px;
  font-size: 22px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  outline: none;
  text-align: center;
}
.qty-unit {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-muted);
}
.qty-macros {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  padding: 12px 16px;
}
.qty-macro { text-align: center; }
.qty-macro-val { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.qty-macro-lbl { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.qty-confirm { padding: 12px 16px 0; }

/* ─── Barcode Scanner ────────────────────────────────────── */
.scanner-wrap {
  width: 100%;
  aspect-ratio: 16/9;
  background: #000;
  border-radius: var(--radius-md);
  overflow: hidden;
  position: relative;
}
/* Horizontal guide rail — marks where to centre the barcode */
.scanner-rail {
  position: absolute;
  top: 50%;
  left: 7%;
  right: 7%;
  transform: translateY(-50%);
  height: 44%;
  border: 2px solid var(--accent);
  border-radius: 6px;
  pointer-events: none;
}
.scanner-rail::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 1.5px;
  background: var(--accent);
  opacity: 0.5;
  transform: translateY(-50%);
  animation: scan-sweep 1.6s ease-in-out infinite;
}
@keyframes scan-sweep {
  0%,100% { top: 15%; }
  50%      { top: 85%; }
}

/* ─── History Calendar Strip ─────────────────────────────── */
.cal-strip {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 12px 16px;
  scrollbar-width: none;
}
.cal-strip::-webkit-scrollbar { display: none; }
.cal-day {
  flex-shrink: 0;
  width: 44px;
  display: flex; flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 4px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  transition: all 0.15s;
}
.cal-day.selected {
  background: var(--accent-dim);
  border-color: var(--accent);
}
.cal-day.has-data .cal-day-dot { background: var(--accent); }
.cal-day-name { font-size: 10px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; }
.cal-day-num  { font-size: 16px; font-weight: 700; }
.cal-day-dot  { width: 5px; height: 5px; border-radius: 50%; background: transparent; }
/* Future days (plan-ahead) read dimmer so "past | today | planned" scans at a glance */
.cal-day.future .cal-day-num  { color: var(--text-faint); }
.cal-day.future .cal-day-name { color: var(--text-faint); }
.cal-day.future.selected .cal-day-num { color: var(--text); }

/* ─── Trends ─────────────────────────────────────────────── */

/* ─── Toast ──────────────────────────────────────────────── */
.toast-container {
  position: fixed;
  top: calc(16px + var(--safe-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 500;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
  width: min(calc(100% - 32px), 360px);
}
.toast {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  font-size: calc(13px * var(--ui-scale));
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 10px;
  box-shadow: 0 8px 24px var(--shadow);
  animation: toastIn 250ms ease-out forwards;
}
.toast.out { animation: toastOut 200ms ease-in forwards; }
.toast.success { border-color: var(--success); }
.toast.error   { border-color: var(--error); }
.toast-icon    { font-size: 16px; flex-shrink: 0; }
@keyframes toastIn {
  from { opacity: 0; transform: translateY(-12px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-8px); }
}

/* ─── Skeleton Shimmer ───────────────────────────────────── */
.skeleton {
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--border) 50%, var(--surface-2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
  border-radius: var(--radius-sm);
}
@keyframes shimmer {
  from { background-position: 200% center; }
  to   { background-position: -200% center; }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background: var(--surface-2); }
}
.skel-line {
  height: 14px;
  border-radius: var(--radius-full);
}
.skel-line + .skel-line { margin-top: 8px; }

/* ─── Offline Banner ─────────────────────────────────────── */
.offline-banner {
  /* Fixed amber + dark ink in both themes: --warning is tuned to be readable
     AS text on each background, which makes it too dark to be a fill in light. */
  background: #ffb347;
  color: #0d0d0f;
  text-align: center;
  padding: 6px 16px;
  font-size: 12px;
  font-weight: 600;
  position: fixed;
  /* The inset, like every other top-anchored fixed element. Without it this sat
     under the notch and overlapped the toast container (which does pad for it),
     clipping the top of the first toast — and that is the toast that says a
     write didn't save. */
  top: 0; left: 0; right: 0;
  padding-top: calc(6px + var(--safe-top));
  z-index: 1000;
  transform: translateY(-100%);
  transition: transform 0.3s;
}
.offline-banner.visible { transform: translateY(0); }

/* ─── Update ("new version") prompt ───────────────────────── */
.update-banner {
  position: fixed;
  left: 16px; right: 16px;
  /* Above the bottom nav, not on top of it. At bottom:16px this covered the
     nav's middle tabs as well as anything else down there. Same anchoring the
     FAB uses, so the two stack predictably. */
  bottom: calc(var(--nav-h) * var(--ui-scale) + var(--safe-bottom) + 12px);
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  /* Scales with the text setting, like .toast and .nav-item. It was a flat
     14px — the one banner that asks for a decision was the one that didn't
     grow for the reader who needs it to. */
  font-size: calc(14px * var(--ui-scale));
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text);
  font-size: 14px;
  font-weight: 600;
  box-shadow: 0 8px 24px var(--shadow);
  transform: translateY(140%);
  transition: transform 0.3s ease;
}
.update-banner.visible { transform: translateY(0); }
.update-banner button {
  flex: none;
  padding: 8px 16px;
  border: none;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
  font-size: 13px;
  cursor: pointer;
}
.update-banner button:hover { background: var(--accent-hover); }

/* Segmented pills in Settings → Appearance (theme, size, and the on/off
   accessibility switches) — same shape as the kcal/kJ pair, but as a class
   rather than inline styles so toggling is one classList call. */
.seg-btn {
  height: 34px;
  padding: 0 14px;
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 700;
  border: 1.5px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
}
.seg-btn.active { border-color: var(--accent); background: var(--accent-dim); color: var(--accent); }
/* The size buttons carry their own scale so each one previews what it does. */
.scale-btn[data-scale="115"] { font-size: 14px; }
.scale-btn[data-scale="130"] { font-size: 16px; }
.scale-btn[data-scale="150"] { font-size: 18px; height: 40px; }

/* ─── Forms ──────────────────────────────────────────────── */
.form-group { margin-bottom: 16px; }
.form-label { font-size: 12px; font-weight: 600; color: var(--text-muted); margin-bottom: 6px; display: block; letter-spacing: 0.06em; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

/* ─── Quantity mode buttons ──────────────────────────────── */
.qty-mode-btn {
  flex: 1;
  height: 32px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 600;
  background: var(--surface-2);
  color: var(--text-muted);
  border: 1px solid var(--border);
  transition: all 0.15s;
}
.qty-mode-btn.active {
  background: var(--accent-dim);
  color: var(--accent);
  border-color: rgba(200,184,255,.3);
}

/* ─── Settings ───────────────────────────────────────────── */
/* gap + min-width let a long label wrap instead of butting against the field,
   and keep the field at its full 80px instead of being squeezed by one. */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.settings-row:last-child { border-bottom: none; }
.settings-row-label { font-size: 14px; font-weight: 500; min-width: 0; }
.settings-row-input {
  height: 36px;
  width: 80px;
  flex-shrink: 0;
  text-align: right;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0 10px;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  outline: none;
}
.settings-row-input:focus { border-color: var(--accent); }

/* ─── Empty State ────────────────────────────────────────── */
.empty-state {
  text-align: center;
  padding: 40px 24px;
  color: var(--text-muted);
}
.empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
.empty-text { font-size: 14px; }

/* ─── Today View Header ──────────────────────────────────── */
.today-header {
  padding: 20px 16px 8px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.today-date { font-size: 22px; font-weight: 700; }
.today-day  { font-size: 13px; color: var(--text-muted); margin-top: 2px; }

/* ─── Today's type identity ──────────────────────────────────
   Scoped to .today-view only, so History/Trends/Settings keep the
   system stack untouched. Space Grotesk carries section labels and
   titles; Space Mono carries every numeric readout — calories, grams,
   ml, kg — so Today reads like an instrument display, not a form. */
.today-view .today-date,
.today-view .label,
.today-view .ring-title,
.today-view .section-header h2 {
  font-family: var(--font-display);
}
/* :not(.ring-kcal-sm) — the two small side rings are clamped down to
   58-82px on narrow phones (see the @media block below); monospace
   digits run wider per-glyph than the system-sans tabular figures this
   was tuned against, so the small rings keep the plain numeral face
   to avoid risking overflow at that breakpoint. */
.today-view .num:not(.ring-kcal-sm),
.today-view .meal-session-kcal,
.today-view .meal-entry-kcal {
  font-family: var(--font-mono);
}

/* ─── Trends' instrument type (scoped) ───────────────────────
   Same Space Grotesk + Space Mono pairing as Today, applied to Trends
   so the analytics page reads like a lab instrument: Grotesk on the
   uppercase labels, Mono on every figure. The number-line signature
   lives in trends.js. */
.trends-view .label,
.trends-view .stat-lbl,
.trends-view .today-date {
  font-family: var(--font-display);
}
.trends-view .num,
.trends-view .stat-num {
  font-family: var(--font-mono);
}
/* One consistent window indicator on every card so it's clear which cards
   follow the 7d/30d/90d toggle ("N days") vs which are fixed. */
.trends-view .range-chip {
  font-size: 10px;
  font-weight: 600;
  color: var(--text-faint);
  white-space: nowrap;
  font-family: var(--font-mono);
}
/* Collapsible disclosures (Trends + Today) — hide the native marker, flip the chevron. */
.trends-view summary::-webkit-details-marker,
.today-view summary::-webkit-details-marker { display: none; }
.nl-chev { display: inline-block; transition: transform 0.15s; }
.nl-details:not([open]) .nl-chev { transform: rotate(-90deg); }

.gap-summary {
  font-size: 10px;
  color: var(--text-muted);
  font-family: var(--font-mono);
  margin-left: auto;
  text-align: right;
}
/* "SEE THE BREAKDOWN" (~131px) plus "−22 vs real maint · −658 vs TDEE" (~186px
   of monospace) plus gaps overruns a 390px phone by ~15px, and a 375px one by
   more. Drop the figures onto their own right-aligned line and leave the
   chevron with the heading. */
@media (max-width: 430px) {
  .nl-details > summary { flex-wrap: wrap; }
  .nl-details > summary .nl-chev { order: 2; margin-left: auto; }
  .gap-summary { order: 3; flex: 1 1 100%; margin-left: 0; font-size: 9px; }
}

/* ─── Summary Cards Row ──────────────────────────────────── */
.macro-totals-row {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 8px;
  margin-top: 12px;
}
.macro-total { text-align: center; }
.macro-total-val { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
.macro-total-lbl { font-size: 11px; margin-top: 2px; }
/* Five columns share ~298px on a 390px phone and ~267px on a 375px one, so a
   column is 53-60px. "PROTEIN" at 11px with 0.08em tracking is ~54px of that.
   Tighten both a step so the widest label can't touch its neighbour. */
@media (max-width: 430px) {
  .macro-totals-row { gap: 4px; }
  .macro-total-val  { font-size: 14px; }
  .macro-total-lbl  { font-size: 9px; letter-spacing: 0.04em; }
}

/* ─── IronLog callout banner inside Today ────────────────── */

/* ─── Weight Form ────────────────────────────────────────── */
.weight-entry {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
}
.weight-date { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.weight-del {
  color: var(--text-faint);
  padding: 8px;
  border-radius: 50%;
  transition: color 0.15s;
}
.weight-del:active { color: var(--error); }

/* ─── Dual-column nutrition table (per 100g | per serving) ── */
.nutr-table {
  display: grid;
  grid-template-columns: 56px 1fr 1fr;
  gap: 6px 8px;
  align-items: center;
  margin-bottom: 14px;
}
.nutr-col-head {
  font-size: 9px;
  font-weight: 700;
  color: var(--text-faint);
  text-align: center;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding-bottom: 2px;
}
.nutr-lbl {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
}
.nutr-input { height: 40px !important; }

/* ─── Energy unit toggle (Add/Edit food forms) ───────────── */
.eunit-btn {
  height: 38px;
  padding: 0 12px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 700;
  border: 1.5px solid var(--border);
  background: var(--surface-2);
  color: var(--text-muted);
  transition: all 0.15s;
  flex-shrink: 0;
}
.eunit-btn.active {
  border-color: var(--accent);
  background: var(--accent-dim);
  color: var(--accent);
}

/* ─── Notes / Ideas checklist ────────────────────────────── */

/* ─── Favourite star ─────────────────────────────────────── */
.fav-btn { color: var(--text-faint); padding: 6px; transition: color 0.15s; }
.fav-btn.active { color: var(--gold); filter: drop-shadow(0 0 4px rgba(255,209,102,.45)); }

/* ─── Profile top bar ────────────────────────────────────── */
.profile-bar {
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  /* +20, not +8. The inset clears the status bar; it does not clear the Dynamic
     Island, which hangs below it and is a black mass on a black background. At
     +8 the pill landed about 19px under the island's bottom edge and read as
     part of it — "still blurry under the island". +20 puts real space there.
     The 12px this costs comes back many times over from the shell fix above. */
  padding: calc(var(--safe-top, 0px) + 20px) 16px 6px;
}
.profile-pill {
  /* 14, not 13. It names whose day you are looking at on a shared phone, it is
     the smallest text on the screen, and it sits in the one band the Dynamic
     Island is already competing for. */
  font-size: calc(14px * var(--ui-scale));
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 5px 16px;
  border-radius: 999px;
  border: 1.5px solid;
  max-width: 70%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ─── Lock / Login screen ────────────────────────────────── */
/* The text-size setting reaches the lock screen too.
   The zoom goes on the CHILDREN, not on .lock-screen itself: this element is
   `position: fixed; inset: 0`, and zooming a box pinned to all four edges
   scales the box past the viewport — the caveat already recorded above the
   .view-container rule. The backdrop stays 1:1 and everything inside it grows.
   Before this, every screen in the app scaled except the one the primary user
   meets on every single launch: a 22px numpad, the 14px "that code isn't
   recognised" line that is the lock screen's only feedback, and a 12px
   "Forgot your passcode?" at 70% opacity — the smallest, faintest text in the
   app, on the recovery path. index.html also sets user-scalable=no, so the
   in-app scale was the only enlargement available and it stopped at this door. */
.lock-screen > * { zoom: var(--ui-scale); }

.lock-screen {
  position: fixed; inset: 0;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 30px;
  padding: 24px 24px calc(24px + var(--safe-bottom));
  /* Above the nav (100) and the app, but BELOW the sheet (201), the toasts
     (500) and both banners (1000/1100). At 1000 this opaque full-screen layer
     swallowed all three: "Forgot your passcode?" opened a sheet nobody could
     see, "that code is taken" was an invisible toast — so people retried until
     rate-limiting locked out the whole household — and an offline launch showed
     no offline banner. Everything the lock screen needs to say lives in a layer
     above it, so it cannot be the top of the stack. */
  z-index: 150;
  overflow-y: auto;
}
.lock-brand { text-align: center; }
.lock-logo { display: flex; justify-content: center; margin-bottom: 12px; }
.lock-title { font-size: 28px; font-weight: 700; letter-spacing: -0.5px; }
.lock-subtitle { font-size: 14px; color: var(--text-muted); margin-top: 4px; }

.lock-dots { display: flex; gap: 16px; }
.dot {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: transparent;
  transition: background 0.15s, border-color 0.15s;
}
.dot.filled { background: var(--accent); border-color: var(--accent); }
.dot.error  { background: var(--error);  border-color: var(--error); }

.numpad {
  display: grid;
  grid-template-columns: repeat(3, 80px);
  gap: 12px;
}
.numpad-key {
  width: 80px; height: 80px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 22px;
  font-weight: 500;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 2px;
  transition: background 0.1s, transform 0.08s;
  user-select: none;
}
.numpad-key span { font-size: 9px; letter-spacing: 0.1em; color: var(--text-muted); font-weight: 600; }
.numpad-key:active { background: var(--surface-2); transform: scale(0.94); }
.numpad-empty { background: transparent; border-color: transparent; cursor: default; }
.numpad-back { color: var(--text-muted); }

.lock-create-link {
  background: none;
  color: var(--accent);
  font-size: 14px;
  font-weight: 600;
  padding: 8px 16px;
}

/* ─── Create profile ─────────────────────────────────────── */
.create-form { width: 100%; max-width: 320px; }
.accent-row { display: flex; gap: 12px; flex-wrap: wrap; }
.accent-swatch {
  width: 38px; height: 38px;
  border-radius: 50%;
  border: 3px solid transparent;
  transition: transform 0.1s;
}
.accent-swatch.selected { border-color: var(--text); transform: scale(1.08); }
.accent-swatch:active { transform: scale(0.94); }

/* ─── Food source tags ───────────────────────────────────── */
.food-tag {
  display: inline-block;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 1px 6px;
  border-radius: 999px;
  vertical-align: middle;
  margin-left: 4px;
}
.tag-custom  { background: var(--accent-dim); color: var(--accent); border: 1px solid rgba(200,184,255,.25); }
.tag-scanned { background: rgba(126,231,199,.12); color: var(--mint); border: 1px solid rgba(126,231,199,.25); }

/* ─── Concern chips (Today at-a-glance status) ───────────── */
.concern-chip {
  display: inline-flex; align-items: center;
  font-size: 12px; font-weight: 600;
  padding: 3px 10px; border-radius: 999px; white-space: nowrap;
}
.concern-chip.bad  { background: rgba(255,107,107,.12); color: var(--error);   border: 1px solid rgba(255,107,107,.28); }
.concern-chip.warn { background: rgba(255,179,71,.12);  color: var(--warning); border: 1px solid rgba(255,179,71,.28); }
.concern-chip.good { background: rgba(93,202,138,.12);  color: var(--success); border: 1px solid rgba(93,202,138,.28); }

/* ─── "Did you fully log?" banner (Today) ────────────────── */
.logcheck-banner {
  background: rgba(249,115,22,.10);
  border: 1px solid rgba(249,115,22,.30);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.logcheck-title { font-size: 13px; font-weight: 700; color: var(--alert); }
.logcheck-sub   { font-size: 12px; color: var(--text-muted); margin-top: 2px; line-height: 1.4; }
.logcheck-text  { min-width: 0; flex: 1; }
.logcheck-actions { display: flex; gap: 8px; flex-shrink: 0; }
.logcheck-actions .btn { height: 34px; padding: 0 14px; font-size: 13px; }
.btn-ghost {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
}

/* ─── Trends: consistency stats + goal progress + review ─── */
.stat-grid { display: flex; gap: 8px; align-items: stretch; }
.stat-cell {
  flex: 1;
  /* Without this a flex item refuses to shrink below its content, so a long
     label ("real maint. kcal") pushes the row wider than the card on a phone. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  text-align: center;
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  padding: 12px 6px;
}
.stat-num { font-size: 22px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-lbl { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
/* Pinned to the bottom of the cell. The three labels aren't the same length, so
   on a narrow screen one wraps to two lines and the others don't — pushing that
   cell's sub-label a line lower than its neighbours. margin-top:auto lands all
   three on one baseline however the labels break. */
.stat-sub {
  font-size: 9px;
  color: var(--text-faint);
  margin-top: auto;
  padding-top: 1px;
}

/* Phones: three cells share ~330px, so ~93px of text each. "real maint. kcal"
   at 11px is right on that limit. Shrink the type a step rather than let every
   verdict card wrap. */
@media (max-width: 430px) {
  .stat-grid { gap: 6px; }
  .stat-cell { padding: 10px 4px; }
  .stat-num  { font-size: 18px; }
  .stat-lbl  { font-size: 10px; }
  .stat-sub  { font-size: 8px; }
}

.review-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 10px 4px;
}
.review-row + .review-row { border-top: 1px solid var(--border); }
/* "Forgot — add" and "Ate light" are ~170px of flex-shrink:0 buttons. On a
   phone that leaves ~150px for "3 meals · 1,457 kcal · only 67% of goal", which
   then wraps into three cramped lines beside them. Give the text the whole
   width and drop the buttons underneath, still right-aligned. */
@media (max-width: 430px) {
  .review-row { flex-wrap: wrap; }
  .review-row > :first-child { flex: 1 1 100%; }
  .review-row > :last-child  { margin-left: auto; }
}
.review-add, .review-light {
  height: 30px; padding: 0 10px; font-size: 12px; font-weight: 600;
  border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2);
  color: var(--text-muted);
}
.review-add { color: var(--accent); border-color: rgba(200,184,255,.3); background: var(--accent-dim); }
.review-flag {
  display: inline-block; margin-left: 6px; font-size: 9px; font-weight: 700;
  letter-spacing: 0.03em; text-transform: uppercase; padding: 1px 6px;
  border-radius: 999px; vertical-align: middle;
  background: rgba(249,115,22,.12); color: var(--alert); border: 1px solid rgba(249,115,22,.3);
}

/* ─── Collapsible settings sections ──────────────────────── */
.sec-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 14px 16px;
  font-size: 13px; font-weight: 700; letter-spacing: 0.02em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
  cursor: pointer; user-select: none; -webkit-user-select: none;
}
.sec-head.danger { color: var(--error); }
.sec-chevron { font-size: 11px; color: var(--text-faint); transition: transform 0.2s; flex-shrink: 0; }
.sec-head:active { background: var(--surface-2); }
.card.collapsed > *:not(.sec-head) { display: none; }
.card.collapsed .sec-head { border-bottom: none; }
.card.collapsed .sec-chevron { transform: rotate(-90deg); }

/* Goal Phase — prominent resulting calorie goal */
.phase-goal {
  background: var(--accent-dim);
  border: 1px solid rgba(200,184,255,.25);
  border-radius: var(--radius-sm);
  padding: 12px 14px; text-align: center;
}
.phase-goal-label { font-size: 10px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-faint); }
.phase-goal-num { font-size: 28px; font-weight: 800; color: var(--accent); font-variant-numeric: tabular-nums; line-height: 1.1; margin-top: 2px; }
.phase-goal-num span { font-size: 13px; font-weight: 600; color: var(--text-muted); }
.phase-goal-sub { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
.phase-goal-empty { font-size: 12px; color: var(--text-faint); text-align: center; }
/* Shown only when a direct IronLog sync has overridden the phase math. */
.phase-goal-warn {
  font-size: 11px; line-height: 1.5; text-align: left;
  color: var(--warning);
  background: rgba(255,179,71,.10);
  border: 1px solid rgba(255,179,71,.28);
  border-radius: var(--radius-sm);
  padding: 8px 10px; margin-top: 10px;
}
