/* app/static/css/style.css
   Deliberately minimal, plain CSS -- just enough to make the app readable
   and usable, not a design system. Nothing here is Flask/Python-specific;
   it's included the normal HTML way via <link> in base.html. */

/* CSS custom properties ("variables"): defining the color palette once
   here, as named values, instead of typing the same hex codes over and
   over throughout this file. Two benefits: it's self-documenting (var()
   name says what the color MEANS, not just what it looks like), and
   changing the palette later is a five-line edit here instead of a
   find-and-replace across the whole file. Referenced elsewhere as
   var(--color-name). */
:root {
    /* The raw named palette -- these hex values themselves never change
       between light/dark mode. Some of them (onyx, cream) play
       a role that's the SAME regardless of theme (e.g. navbar text stays
       cream because the navbar itself stays a dark graphite in both
       modes) -- those are still referenced directly below. Anything whose
       role needs to actually FLIP between light and dark (page background,
       body text, primary button color...) goes through one of the
       SEMANTIC tokens further down instead, which is what actually gets
       redefined for dark mode. */
    --color-cream: #fffcf2;      /* "Floral White" */
    --color-taupe: #ccc5b9;      /* "Dust Grey" */
    --color-onyx: #0f0f0f;       /* "Onyx" */
    --color-graphite: #2d2e2e;   /* "Graphite" -- header/nav background, button hover; stays this same dark shade in both themes on purpose, so the top bar doesn't need its own light/dark logic */

    /* These carry actual MEANING (danger, success, ...) rather than being
       part of the decorative palette above. Redefined for dark mode below
       (same variable names, different values) since their light pastel
       backgrounds would look wrong -- too washed-out to read as a banner
       -- against a dark page. */
    --color-danger: #c0392b;
    --color-success-bg: #dff3e3;
    --color-success-text: #1e6b34;
    --color-error-bg: #fbe1e1;
    --color-error-text: #8a1f1f;
    --color-info-bg: #e3edfb;
    --color-info-text: #1e4a8a;
    /* A value the user has deliberately overridden away from what the food
       database says. Amber, NOT the app's red: red reads as "you have made
       a mistake, fix it before continuing", and this is the opposite --
       an intentional correction we accept and save. Amber is the standard
       "this is unusual, know what you're doing" register, and it also
       keeps --color-danger meaning exactly one thing (a real error) rather
       than two. */
    --color-edited-bg: #fdf1dc;
    --color-edited-text: #7a5312;
    --color-edited-border: #c98a2e;
    /* A pale wash of the app's own accent, for a callout that is
       INFORMATIVE rather than a warning -- the serving-size scaling
       notice. Deliberately not --color-bg: on a card, the page
       background is nearly invisible and the callout would read as
       plain text again, which is the complaint it exists to fix. */
    --color-accent-soft: #e4edf1;

    /* Semantic tokens -- most rules below use THESE, not the raw palette
       above directly. Light mode just points each one at the matching raw
       color; dark mode (the two blocks after this one) redefines only
       these few names, which is what actually makes the theme switch work
       without every single rule needing its own light/dark logic. */
    /* Palette redesign, 2026-09-03 (see TODO.md's "COLOR REDESIGN" entry
       for the full spec and the reasoning behind each value). The app
       moved off the onyx/cream base: light mode is now white with a
       slate-blue accent, and dark mode is built on that same slate hue
       taken down to a real background weight. The raw cream/onyx/taupe
       colors above are still defined -- a few deliberate one-offs below
       still use them directly (the graphite button-hover, the auth hero's
       fixed dark gradient). The old warm grey is gone entirely; see
       --color-muted-fill below for what replaced it and why. */
    --color-slate: #3b5d6b;        /* the accent -- 7.1:1 on white */
    --color-slate-light: #56889c;  /* the hero tracker, and dark mode's accent */
    --color-ink: #14181a;
    --color-ink-muted: #5f6b70;

    --color-page-bg: #ffffff;              /* body background */
    --color-surface: #ffffff;              /* cards, the bottom nav, modals -- anything "raised". Same as the page now, so .card's existing border + shadow are what separate it */
    --color-text: var(--color-ink);        /* primary body text, primary icons */
    --color-text-muted: var(--color-ink-muted); /* secondary/muted text */
    --color-accent-bg: var(--color-slate); /* primary buttons, active states -- the app's main accent */
    --color-accent-text: #ffffff;          /* text/icons ON the accent color above */
    --color-border: #e4e8ea;               /* borders, dividers, subtle fills */

    /* Solid "quiet button" fill -- .btn.secondary and the navbar avatar.
       Was --color-warm-grey (#716969) before the 2026-09-03 redesign, and
       was left behind by it: once everything around it went cool slate,
       that warm taupe was the only warm color still on screen and read as
       a stray. Same job, same weight (5.5:1 under cream text, so still
       AA), just on the new palette's hue. Deliberately the SAME value as
       --color-ink-muted, which restores the light-mode collision that the
       .hero-day-nav rule near the bottom of this file is written to work
       around -- see that rule's comment. */
    --color-muted-fill: var(--color-ink-muted);

    /* Dashboard header palette, 2026-09-04. Started as a rust/terracotta
       pair (the blue read badly on the phone), which Nick then asked to
       remove entirely from both themes.
       Neutral, NOT a second accent. Set 2026-09-04 on Nick's direct
       instruction to remove all terracotta from both themes.
       The reason it's neutral rather than another colour: the header
       had been rust while every interactive element -- buttons, the
       FAB, the active tab -- stayed slate, so the app carried TWO
       competing accent hues. A neutral header leaves slate as the
       single accent, which is also what the app icon's own
       "one accent plus white" brief asks for.
       10.84:1 on the cream ground, comfortably past 4.5:1. */
    --color-brand-mark: #3a3531;
    --color-avatar-bg: #e3ded6;
    /* The avatar's fallback person glyph. MUST be paired with
       --color-avatar-bg: .navbar-avatar used to hardcode --color-cream,
       which was right while the avatar was a dark slate but broke the
       moment the header went neutral -- cream on the light #e3ded6 chip
       is 1.3:1, an invisible glyph for every user without a profile
       picture, in the DEFAULT theme. Change one of these two and you
       must re-check the other. 9.29:1 here. */
    --color-avatar-fg: #3a3531;

    /* The Dashboard's main calorie tracker only -- direct request: it uses
       the LIGHTER blue even in light mode, and is the only light-mode
       element that does. 3.9:1 on white, above the 3:1 bar for a
       graphical element, and 125 away from the standard accent so it
       reads as a deliberate choice rather than an inconsistency. */
    --color-hero: var(--color-slate-light);

    /* The 3 macros. These were hardcoded per-file until this redesign --
       in main/routes.py, food/routes.py AND trends.js, none of them
       theme-aware. Defined once here now; everything reads from these, so
       a ring on the Dashboard and a bar on Trends can't drift apart.
       Replaced 2026-09-05, direct request: the original trio (a muted
       red/teal/purple) reads as generic "AI dashboard" colors -- it's a
       specific, recognizable combination, not just "colorful". Swapped
       for a warmer, food-evocative trio instead (terracotta/ochre/plum)
       that isn't that particular palette, while keeping each value clear
       of the app's own red (--color-danger/--color-chart-goal) and green
       (--color-chart-good) so a macro is never confused with a status
       color. Weight and calorie chart colors (--color-chart-line, below)
       were deliberately left untouched -- only these three moved. */
    --color-macro-protein: #111d26;
    --color-macro-carbs: #48626f;
    --color-macro-fat: #8095a0;

    /* Chart geometry uses a distinct slate instead of body-text onyx.
       Every solid graphical token here clears 3:1 against the card
       surface; dark mode supplies brighter counterparts below.

       T239 (Nick, 2026-09-15): DERIVED FROM THE ACCENT, not a literal.
       These were #2f6578 / rgba(47,101,120,.14) -- a slate that merely
       RESEMBLED the default accent without being linked to it, so T192's
       colour picker repainted buttons and left every graph unchanged.
       That was the whole of his report: "all they change are a couple
       buttons here and there, but the graphs and the rest of the app all
       stay the same". One token feeds both the Dashboard and Trends
       (dashboard.js TODAY_COLOR and trends.js CHART_LINE_COLOR both read
       --color-chart-line), so linking it here repaints every chart.

       Contrast: charts need 3:1 against the card surface, and every deep
       shade already clears 7:1 against white text -- a far higher bar
       against a #ffffff surface. Asserted in test_plus.py so a colour
       added later cannot quietly break it. */
    --color-chart-line: var(--color-slate);
    /* --color-chart-fill was REMOVED by T239, not renamed: the only CSS
       way to say "this colour at 14%" once the line became a var() is
       color-mix(), and getComputedStyle hands that back UNRESOLVED
       ("color-mix(in srgb, #3b5d6b 14%, transparent)") -- verified in a
       real browser -- which Chart.js's own colour parser cannot read.
       trends.js derives the fill from --color-chart-line instead, so
       there is one source of truth rather than two that can drift. */
    --color-chart-goal: #c0392b;
    --color-chart-good: #1e6b34;  /* unused since T186 (Trends now uses the steel line colour); kept for the palette notes above */
    --color-chart-grid: #777777;
    /* Dashboard streak widget's "completed" bubble. Originally gold to
       sit near the app's then-warm macro colors (a rust protein and an
       olive fat). Those are gone as of the 2026-09-03 palette redesign,
       so that rationale no longer holds -- gold is kept anyway because
       it now does a different job: it's the one warm note in an
       otherwise cool palette, which is exactly what a "you did the
       thing" marker wants to be. It was also deliberately protected
       during that redesign -- a warm amber accent was rejected for
       landing 38 away from this. Brightened slightly for dark mode
       below, same reasoning as --color-chart-line: a solid fill needs
       more contrast against a dark card than a thin ring stroke does. */
    --color-streak-gold: #b3862a;
}

/* Dark mode, system-preference version: applies automatically when the
   OS/browser is set to dark, UNLESS the user has explicitly picked Light
   via the toggle (see app/static/js/theme_toggle.js), which stamps
   data-theme="light" on <html> to opt back out of this. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        /* Warm-neutral dark ground, 2026-09-04. The previous page/card/
           border trio (#101a1e / #19282e / #22333a) repeated the blue
           accent hue across every large surface, which is why the whole
           theme read too blue on-device. These are low-chroma graphite
           values with a slight warm lean; the slate accent remains the
           same, so this is a ground adjustment rather than a rebrand. */
        --color-page-bg: #181716;
        --color-surface: #24211f;
        --color-text: #f0ede6;
        --color-text-muted: #aaa39a;
        --color-accent-bg: var(--color-slate-light);
        --color-accent-text: #0d1417;
        --color-border: #3a3531;
        --color-hero: var(--color-slate-light);
        /* Lighter rust for text/linework on the dark ground (6.56:1), plus
           a darker avatar fill that clears 3:1 against the page and card
           while keeping its cream person glyph at 5.17:1. */
        /* Warm grey, matching the light theme's neutral header. See
           --color-brand-mark in :root for why this is not a colour.
           10.71:1 on the warmed dark ground. */
        --color-brand-mark: #cfc7bd;
        --color-avatar-bg: #2b2926;
        /* Dark keeps the cream glyph -- see --color-avatar-fg in :root
           for why this is a token and not a hardcoded value. 14.13:1. */
        --color-avatar-fg: var(--color-cream);
        /* Brighter counterparts of the 2026-09-05 macro trio, same
           reasoning as --color-chart-line's own dark variant: a solid
           fill/stroke needs more contrast against a dark card than the
           light-mode value would provide. */
        --color-macro-protein: #e4edf2;
        --color-macro-carbs: #93a8b6;
        --color-macro-fat: #5e7280;
        --color-success-bg: #16351f;
        --color-success-text: #7fd99a;
        --color-error-bg: #3a1414;
        --color-error-text: #f5a3a3;
        --color-info-bg: #16273f;
        --color-info-text: #8fb8f0;
        --color-edited-bg: #3a2c12;
        --color-edited-text: #f0c479;
        --color-edited-border: #c99a45;
        --color-accent-soft: #1e2e36;
        /* T239: the accent's light shade, for the same reason as above. */
        --color-chart-line: var(--color-slate-light);
        --color-chart-goal: #e07171;
        --color-chart-good: #64b77b;
        --color-chart-grid: #75706b;
        --color-streak-gold: #c79a3a;
    }
}

/* Dark mode, explicit-choice version: wins over the system preference in
   BOTH directions (picking Dark on a light-mode system, or Light on a
   dark-mode system -- see the `:not([data-theme="light"])` guard above,
   which is what lets this second block override the first). Same values
   as the media-query block; kept as a literal duplicate rather than one
   shared block, since CSS custom properties don't have a way to combine
   a media query and an attribute selector into a single condition. */
:root[data-theme="dark"] {
    /* Keep this literal duplicate in sync with the system-preference
       block above: warm graphite ground, unchanged slate accent. */
    --color-page-bg: #181716;
    --color-surface: #24211f;
    --color-text: #f0ede6;
    --color-text-muted: #aaa39a;
    --color-accent-bg: var(--color-slate-light);
    --color-accent-text: #0d1417;
    --color-border: #3a3531;
    --color-hero: var(--color-slate-light);
    /* Keep the header pair in sync with the system-dark block above. */
    --color-brand-mark: #cfc7bd;
    --color-avatar-bg: #2b2926;
    --color-avatar-fg: var(--color-cream);
    --color-macro-protein: #e4edf2;
    --color-macro-carbs: #93a8b6;
    --color-macro-fat: #5e7280;
    --color-success-bg: #16351f;
    --color-success-text: #7fd99a;
    --color-error-bg: #3a1414;
    --color-error-text: #f5a3a3;
    --color-info-bg: #16273f;
    --color-info-text: #8fb8f0;
    --color-edited-bg: #3a2c12;
    --color-edited-text: #f0c479;
    --color-edited-border: #c99a45;
    --color-accent-soft: #1e2e36;
    /* T239: the accent's light shade, for the same reason as above. */
    --color-chart-line: var(--color-slate-light);
    --color-chart-goal: #e07171;
    --color-chart-good: #64b77b;
    --color-chart-grid: #75706b;
    --color-streak-gold: #c79a3a;
}

* {
    box-sizing: border-box;
}

/* Without this, jumping to any #anchor (e.g. a macro ring on the Dashboard
   linking to #macros-chart on Trends) lands that element right at the very
   top of the page -- which is now BEHIND the fixed .navbar bar, since it
   overlaps that space. scroll-padding-top tells the browser to stop that
   much short of the true top for every anchor jump on the site, same
   height reserved as .container's own padding-top below. */
/* An anchor jump no longer has to clear a fixed navbar (it scrolls away
   with the page now -- see .navbar), so this is just a little breathing
   room above the target, matching html.no-navbar's own value below. */
html {
    scroll-padding-top: calc(1rem + env(safe-area-inset-top));
}

/* Pages with no fixed navbar at all (see show_navbar in base.html --
   every authenticated page but Dashboard) don't need that same reserved
   offset for an anchor jump; the Trends page's own macro-ring link (see
   #macros-chart) is exactly the kind of jump this affects. */
html.no-navbar {
    scroll-padding-top: calc(1rem + env(safe-area-inset-top));
}

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    background: var(--color-page-bg);
    color: var(--color-text);
}

/* Soften the page-transition flash, direct request ("feels like an app,
   not a website"): this app has no client-side router at all (a
   deliberate choice, no frontend framework/build step -- see HANDOFF.md)
   -- every tab switch is a genuine full-page reload, with a brief flash
   straight to the browser's own blank/white paint before this page's
   real background ever shows, one of the most obvious "this is a
   website" tells there is. Doesn't (and can't, without an actual router)
   eliminate the reload itself -- just a quick fade-in of the finished
   page softens how the transition FEELS. A plain CSS `animation` runs
   on its own the instant this rule applies, no JS/DOMContentLoaded
   coordination needed -- and can't itself cause a flash of invisible
   content, since the page was already blank during that gap regardless. */
@media (prefers-reduced-motion: no-preference) {
    body {
        animation: page-fade-in 0.18s ease-out;
    }
}

@keyframes page-fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

.navbar {
    /* NOT fixed, as of 2026-09-01 -- direct request, the second half of
       "take the header bar out": the logo/name/avatar/gear stay at the
       top of the PAGE, but scroll away with everything else rather than
       following you down it. Combined with the flattened background
       below, there's no longer a "bar" in any visual sense -- just the
       page's own first row of content. Consistent with the Dashboard
       title, which was changed the same day to scroll away rather than
       dock (see .hero-greeting).
       Settings stays reachable while scrolled via the native tab bar in
       the app, and via the bottom nav on the web, so nothing is lost by
       letting this go. */
    position: static;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    /* Direct request: this used to be its own graphite bar, visually
       distinct from the page under it. Now it takes the page's own
       background instead, so it reads as part of the page rather than a
       separate header -- "the same one color look as the others."
       Still fixed and still opaque (NOT transparent): page content
       scrolls underneath it, so it needs a real background to hide that,
       it just needs to be the SAME one the page already uses. */
    background: var(--color-page-bg);
    padding: 0.75rem 1.5rem;
    /* Extra padding to clear the Dynamic Island / status bar on newer
       iPhones when installed as a standalone PWA (see viewport-fit=cover
       in base.html, required for this to have any effect at all). Without
       this, the top bar's content -- the "WeighTooMuch" brand text -- sits right
       against/behind that cutout and gets visually clipped. Same idea as
       the bottom nav's env(safe-area-inset-bottom), just for the top. */
    padding-top: calc(0.75rem + env(safe-area-inset-top));
}

.navbar .brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    /* Uses the dedicated warm header token so the brand row has its own
       identity while action buttons and the active tab stay slate. The
       light/dark values both clear 4.5:1 as text. */
    color: var(--color-brand-mark);
    font-weight: bold;
    text-decoration: none;
    /* Sized up alongside the icons below -- with no bar behind them
       framing them anymore, the old sizes read as small. Raised again to
       1.5rem on 2026-09-04, in the same pass that took the logo and gear
       to 38px, so the wordmark scales with the marks either side of it
       instead of being left behind by them. */
    font-size: 1.5rem;
}

/* A masked <span>, not an <img> -- direct request 2026-09-03 that the
   brand take the app's accent color.

   History worth keeping: logo-mark.png is a fixed CREAM glyph, drawn
   back when this bar was always graphite, and it went invisible once the
   bar took the page's own background. That was patched with
   filter: brightness(0) plus a dark-mode override switching it back off.
   That worked only because black and cream were the two colors needed --
   brightness(0) flattens every opaque pixel to BLACK and nothing else,
   so it could never produce an accent color, and no practical filter
   chain lands on an arbitrary hex.

   Masking inverts the problem: the PNG supplies only its ALPHA channel
   (the glyph's shape), and the color comes from a background
   that is just a token. So it tracks light/dark automatically with no
   per-theme override at all, and the two blocks that used to do that are
   gone. */
.navbar-logo {
    /* 38px, matching .navbar-avatar exactly -- direct request 2026-09-04:
       the profile icon read as disproportionately large next to the logo,
       wordmark and gear, and the call was to keep the avatar as-is and
       bring the other three UP to it rather than shrink it. */
    width: 38px;
    height: 38px;
    background-color: var(--color-brand-mark);
    -webkit-mask: url("../icons/logo-mark.png") center / contain no-repeat;
    mask: url("../icons/logo-mark.png") center / contain no-repeat;
    /* A span is inline by default and would collapse to zero height
       regardless of the width/height above. */
    display: inline-block;
    flex: none;
    /* No border-radius here -- that rounded a filled tile's corners (the
       old icon-192.png usage). logo-mark.png is a transparent glyph with
       no tile to round. */
}

/* Settings gear button, Dashboard-only (see base.html) -- opposite the
   brand, same icon-button treatment as elsewhere in the app (inline SVG,
   fill="currentColor" so it inherits this color rather than emoji). */
.navbar-icon-btn {
    /* Accent, matching the brand opposite it -- direct request. The SVG
       is fill="currentColor", so this one line colors the gear itself. */
    color: var(--color-brand-mark);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
}

.navbar-icon-btn svg {
    /* 38px to match the avatar and logo -- see .navbar-logo above. The
       button itself is bumped to 44px so a 38px glyph still has a little
       room inside its tap target rather than touching the edges. */
    width: 38px;
    height: 38px;
}

.navbar-icon-btn:hover {
    color: var(--color-text-muted);
}

.container {
    max-width: 720px;
    margin: 0 auto;
    padding: 1.5rem;
    /* padding-top is a plain 1.5rem as of 2026-09-01: the navbar above
       is no longer fixed (see its own rule), so there's nothing to
       reserve room for, and it now sits in normal page flow and absorbs
       env(safe-area-inset-top) itself. Pages with no navbar at all still
       need that inset here -- see .container.no-navbar further down,
       which is why this base rule can't just add it unconditionally.
       padding-bottom real bug found and fixed, direct report: 4.5rem only
       cleared the bottom-nav BAR's own height -- it didn't also account
       for the "+" FAB's circle poking up an extra 22px above the bar's
       own top edge (see .bottom-nav-fab-icon's margin-top), so the raised
       FAB could visually overlap the last line of .app-footer sitting
       right above it on some viewport heights. Bumped to 6rem to give
       real clearance above the FAB, not just the flat bar. */
    padding-top: 1.5rem;
    padding-bottom: 6rem;
}

/* No fixed navbar on this page (see show_navbar in base.html) -- the
   3.5rem above exists ONLY to reserve room for that bar, so without it
   that space would just be dead air above the page's own <h1>. Only
   padding-TOP changes here -- padding-bottom still reserves room for
   .bottom-nav, which renders on every authenticated page regardless of
   this, Dashboard included.

   Real bug, reported directly: this used to be a bare 1.5rem, missing
   the env(safe-area-inset-top) the WITH-navbar version above already
   includes -- so on an iPhone (notch or Dynamic Island), every page
   using this class (Settings and all of its subtabs -- everything but
   Dashboard, since that's the only page show_navbar is true for) had
   its own <h1> render right under the status bar/notch instead of below
   it. Same inset addition as the navbar rule, just without its extra
   3.5rem since there's no bar here to reserve room for. */
.container.no-navbar {
    padding-top: calc(1.5rem + env(safe-area-inset-top));
}

/* The site-wide disclaimer/Terms link (base.html) -- small and muted so
   it doesn't compete with actual page content, but present on every
   page. Sits inside .container, after the content block, so it flows
   with the page normally and stays clear of the fixed .bottom-nav
   (.container's existing padding-bottom already reserves room for that
   bar -- see the comment on .container above). */
.app-footer {
    margin-top: 2rem;
    padding-top: 1rem;
    border-top: 1px solid var(--color-border);
    /* Direct request, part of the same FAB-overlap fix as .container's
       padding-bottom above: smaller text alongside more clearance,
       together. */
    font-size: 0.7rem;
    color: var(--color-text-muted);
    text-align: center;
    line-height: 1.5;
}

.app-footer a {
    color: inherit;
    text-decoration: underline;
}

/* T233 / T08 (2026-09-21): the public landing page at `/`.

   ⚠️ Deliberately plain, and that is the brief. This page exists because
   Apple requires a public organization website; it is read once by a
   reviewer and occasionally by someone deciding whether to install. It is
   not a surface anyone spends time in, so it reuses the app's own tokens
   and type scale rather than growing a second visual language to maintain. */
.landing {
    max-width: 640px;
    margin: 0 auto;
}

.landing-hero {
    text-align: center;
    padding: 1rem 0 2.5rem;
}

.landing-hero h1 {
    font-size: clamp(1.8rem, 7vw, 2.4rem);
    line-height: 1.15;
    margin-bottom: 0.75rem;
}

.landing-sub {
    color: var(--color-text-muted);
    font-size: 1.05rem;
    line-height: 1.55;
    margin: 0 auto 1.5rem;
    max-width: 30rem;
}

.landing-cta {
    display: flex;
    gap: 0.75rem;
    justify-content: center;
    flex-wrap: wrap;
}

/* Sized like a real call to action rather than inheriting .btn's in-app
   proportions -- on this page it is the only thing to do. */
.landing-btn-primary {
    min-width: 11rem;
    padding: 0.8rem 1.75rem;
    font-size: 1.05rem;
}

.landing-note {
    color: var(--color-text-muted);
    font-size: 0.85rem;
    margin-top: 0.9rem;
}

.landing-points {
    display: grid;
    gap: 1rem;
    /* One column on a phone, two once there is room -- no breakpoint to keep
       in step with, because auto-fit does it from the available width. */
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

.landing-point {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 14px;
    padding: 1.25rem;
}

.landing-point h2 {
    font-size: 1.05rem;
    margin-bottom: 0.4rem;
}

.landing-point p {
    color: var(--color-text-muted);
    font-size: 0.95rem;
    line-height: 1.55;
    margin: 0;
}

.landing-about {
    margin-top: 2.5rem;
    text-align: center;
}

.landing-about h2 {
    font-size: 1.05rem;
    margin-bottom: 0.5rem;
}

.landing-about p {
    color: var(--color-text-muted);
    line-height: 1.6;
    margin-bottom: 0.6rem;
}

/* 2026-09-21 visual review, §1.1. ⚠️ THE SINGLE LOUDEST "THIS IS A WEBSITE"
   TELL IN THE NATIVE APP. A centered legal footer with underlined links at
   the bottom of every screen -- Home included -- is a web page's furniture;
   no Apple app has one. On the SHORT pages (Display, Custom Meals) it was
   also stranding itself mid-screen above ~700pt of void, because the page
   ran out of content long before the viewport did.

   The content is not lost: the same links, and the same medical-advice
   line, now live in Settings -> About (onboarding/profile_hub.html), which
   is where iOS users look for them. The footer still renders on the WEB,
   where a footer is exactly right and where search engines and a first-time
   visitor both expect it -- this hides it only inside the wrapper. */
.native-app .app-footer {
    display: none;
}

/* 2026-09-21 visual review, §1.2. THE STATUS-BAR SCRIM.

   ⚠️ THE BUG IT FIXES, seen in the Simulator: scrolling any page put live
   content directly behind the clock and the Dynamic Island -- "Grilled
   Chick... 420 kcal" rendered on top of 9:41. The WebView's content area
   extends into the top safe area (which is correct and is what lets the
   page background reach the top of the screen), but nothing was drawn
   between that content and the system status bar, so the two collided.

   Every iOS app solves this the same way: the area behind the status bar
   carries the navigation bar's material, so content passing under it is
   frosted rather than legible-on-top-of-the-clock. This is that material
   and nothing more.

   ⚠️ WHY A SCRIM AND NOT A FULL COLLAPSING NAV BAR. A real large-title bar
   that collapses on scroll is the right long-term answer (§3.1 of the
   review) but it needs every page's title moved out of its card first. This
   is the 10-line version that fixes the actual collision today and does not
   have to be undone to build that later -- the scrim is what would sit
   behind such a bar anyway.

   ⚠️ env(), NOT a pushed variable, DELIBERATELY -- and the failure mode is
   why. --native-safe-area-bottom exists (see its comment above) because a
   WRONG bottom inset put the dock off-screen. Here a zero read is harmless:
   the scrim collapses to nothing and we are back to today's behaviour, with
   nothing mispositioned. The top inset is also already trusted by the
   navbar and .container rules above, which do render correctly on device.

   pointer-events: none so a fixed strip across the top cannot eat a tap
   aimed at whatever is underneath it. */
.native-app::before {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: env(safe-area-inset-top);
    z-index: 100;
    background: color-mix(in srgb, var(--color-bg) 72%, transparent);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    pointer-events: none;
}

/* On a phone-width screen, give the page edges a bit less padding so cards
   use more of the available width. */
@media (max-width: 480px) {
    .navbar {
        padding: 0.6rem 1rem;
        padding-top: calc(0.6rem + env(safe-area-inset-top));
    }
    .container {
        padding: 1rem;
        /* Was calc(3.5rem + env(safe-area-inset-top)), reserving room for
           a fixed navbar -- caught 2026-09-01 while verifying that the
           navbar now scrolls away instead (see its own rule). The base
           .container rule had been updated but this one hadn't, and since
           a real iPhone sits squarely inside this 480px breakpoint, THIS
           is the rule that actually takes effect on the device -- the
           base fix alone would have left a ~56px empty gap at the top of
           every page on the phone. Exactly the sibling-rule gap the
           .container.no-navbar comment right below already warns about. */
        padding-top: 1rem;
        padding-bottom: 4.5rem;
    }
    .container.no-navbar {
        /* Same real bug as the base .container.no-navbar rule above --
           this media-query override was ALSO missing
           env(safe-area-inset-top), and since a real iPhone falls
           squarely inside this max-width: 480px breakpoint, THIS is the
           rule that was actually taking effect on the reported device --
           the earlier fix to the base rule alone would not have
           actually solved anything on a real phone. Found by grepping
           for every safe-area-inset usage after fixing the base rule,
           specifically to check for exactly this kind of sibling gap. */
        padding-top: calc(1rem + env(safe-area-inset-top));
    }
}

/* Bottom tab bar -- see base.html for why this replaced a top nav-links
   list. `env(safe-area-inset-bottom)` adds extra padding on phones with a
   home-indicator gesture bar (notably iPhones) so the tab bar's labels
   don't sit underneath it. */
.bottom-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    padding-bottom: env(safe-area-inset-bottom);
    z-index: 100;
}

.bottom-nav-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    padding: 0.5rem 0 0.4rem;
    text-decoration: none;
    color: var(--color-text-muted);
    font-size: 0.7rem;
}

.bottom-nav-icon-svg {
    width: 22px;
    height: 22px;
    /* No color set here on purpose -- fill="currentColor" on the <svg>
       itself (see base.html) means it just inherits whatever color
       .bottom-nav-item / .bottom-nav-item.active below already set, the
       same as the text label next to it. */
}

/* Tap feedback -- direct request, part of the "feels like an app, not a
   website" work. Every tab switch here is a REAL page navigation (this
   app has no client-side router), not a single persistent DOM node
   changing state -- so a `transition` on .active's color would never
   actually be visible; the old page is gone the instant the new one
   loads with .active already applied from its very first paint. The
   honest fix for that architecture is two separate, real effects:
   :active fires immediately on press-down, before navigation even
   starts, on WHICHEVER tab you're touching -- an instant, working press
   effect with no JS needed. */
.bottom-nav-item:active {
    transform: scale(0.9);
    transition: transform 0.1s ease-out;
}

/* ...and this plays once a NEW page finishes loading with a tab already
   marked .active, standing in for a "just got selected" transition the
   architecture can't otherwise provide. Gated behind prefers-reduced-
   motion, unlike the :active press feedback above (a momentary, user-
   initiated press state, not a decorative animation someone would want
   turned off). */
@media (prefers-reduced-motion: no-preference) {
    .bottom-nav-item.active {
        animation: bottom-nav-select 0.25s ease-out;
    }
}

@keyframes bottom-nav-select {
    from {
        transform: scale(1.15);
    }
    to {
        transform: scale(1);
    }
}

/* Direct feedback: the color-only change below was real but read as too
   subtle to notice at a glance -- a persistent background pill behind
   the active tab, not just its icon/label color, so it's obvious without
   having to compare shades against the others. color-mix() rather than a
   new hardcoded hex per theme: a flat % of the page's own existing text
   color already reads as "the right amount of contrast" in both light
   and dark automatically, the same reasoning --color-chart-line's own
   dark-mode override uses, just via a blend instead of a second literal
   value. */
.bottom-nav-item.active {
    color: var(--color-text);
    background: color-mix(in srgb, var(--color-text) 18%, transparent);
    border-radius: 14px;
}

/* The "+" FAB (floating action button) -- a direct shortcut straight to
   the Food page's search box, raised above the bar's edge and colored so
   it visually reads as "the fast way to do the most common thing" rather
   than a 5th equal tab. */
.bottom-nav-fab {
    justify-content: flex-start;
}

.bottom-nav-fab-icon {
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.6rem;
    line-height: 1;
    margin-top: -22px; /* half its own height -- makes it poke up above the bar */
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
    transition: box-shadow 0.2s ease-out;
}

/* Landing on /food/add itself, direct request: a glow rather than the
   plain .active pill above -- this button already pokes up above the bar
   with its own shadow, so a flat background behind it would look
   different from every other raised state it already has, while a
   brighter/wider glow in the SAME box-shadow property just reads as "the
   normal FAB, but lit up". Uses the streak widget's gold accent
   (--color-streak-gold) rather than introducing a 3rd one-off color --
   this app already has exactly 2 "something is active/earned" colors
   (the plain onyx/cream --color-accent-bg pill above, and gold for
   streak completion); reusing gold here reads as "you're here, doing the
   thing" rather than a random new hue. */
.bottom-nav-fab-active .bottom-nav-fab-icon {
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-streak-gold) 35%, transparent), 0 2px 10px rgba(0, 0, 0, 0.3);
}

/* Native iOS app only -- direct request (2026-09-01), built once the App
   Store was confirmed as the real goal: a real native UITabBar (see
   mobile/ios/App/App/TabBarViewController.swift) now drives the 4 plain
   tabs, so this app's own HTML .bottom-nav has no job left to do inside
   the wrapper -- hidden entirely rather than sitting duplicated
   underneath the real one. The "+" Food FAB is the one exception: see
   .native-fab below for why it's new, standalone markup instead of the
   existing .bottom-nav-fab just being un-hidden on its own.

   --native-tab-bar-height matches the real UITabBar's own fixed content
   height (49pt -- TabBarViewController.swift's own tabBarContentHeight
   constant, kept in sync by hand since there's no bridge call telling
   this page how tall the native view actually is) plus the home-
   indicator inset -- both expressed in the same "points" unit CSS px
   already uses in a WKWebView, so this lines up with the native bar's
   real rendered height. Every native-only bottom-clearance override
   below (.container, .search-card-overlay, .modal-box) is written in
   terms of this one variable so they can't drift out of sync with each
   other or with the bar itself.

   Reasoned the same way .container's own FAB-overlap fix above was (a
   flat bar's height, plus the FAB poking up above it, plus real
   breathing room) but built from the native bar's real height instead of
   the HTML bar's.

   REAL BUG found and fixed (2026-09-01), confirmed via on-device native
   logging: env(safe-area-inset-bottom) reports 0 at the moment this page
   first lays out inside the WKWebView, then WKWebView's own
   safeAreaInsets corrects to the real value (34pt on a home-indicator
   iPhone) a beat later -- but this page's CSS reads env() once and never
   notices that correction, leaving --native-tab-bar-height permanently
   too small and the FAB sitting almost entirely BEHIND the real tab bar
   instead of 40% into it. --native-safe-area-bottom exists specifically
   so TabBarViewController.swift can override it with an inline style
   once the corrected value is known (inline styles always beat a
   stylesheet rule in the cascade, so this can't go stale the way env()
   did) -- see that file's pushSafeAreaBottomToWebView(). Falls back to
   plain env() when no override has been pushed yet (a normal browser,
   or before that JS has run), same value as before this fix. */
html.native-app {
    --native-safe-area-bottom: env(safe-area-inset-bottom);
    --native-tab-bar-height: calc(49px + var(--native-safe-area-bottom));
}

.native-app .bottom-nav {
    display: none;
}

.native-app .container {
    padding-bottom: calc(var(--native-tab-bar-height) + 2.5rem);
}

.native-app .search-card-overlay {
    padding-bottom: calc(var(--native-tab-bar-height) + 1.25rem);
}

/* T241 (Nick, 2026-09-15, from a screenshot): the search results list ran
   UNDER the native tab bar, leaving a half-visible row clipped behind it.

   .search-results is capped at 65vh, which is measured from the VIEWPORT --
   it knows nothing about where the box actually starts. On a phone the box
   begins roughly 300px down (search field + the two scan buttons), so
   300px + 65vh lands past the bottom of the screen, and the real UITabBar
   then covers the last row of it.

   Subtracting the bar's own height is the whole fix, and it reuses the
   variable the rest of this file already trusts -- including the 2026-09-01
   correction that makes it a real value inside the WKWebView rather than the
   0 env() reports at first layout. Native only: on the web the HTML
   .bottom-nav is laid out in flow and never overlaps anything. */
.native-app .search-results {
    max-height: calc(65vh - var(--native-tab-bar-height));
}

/* The bottom-sheet modals (see the shared .modal-overlay/.modal-box
   comment further down) are deliberately flush with the real screen edge
   on web -- inside the native app that edge is now covered by the real
   UITabBar, so the sheet needs to stop short of it instead.
   margin-bottom does that (pushes the whole box up clear of the bar);
   padding-bottom drops back to a plain 1.5rem to match, since the box no
   longer touches the actual safe-area edge itself -- double-reserving
   env(safe-area-inset-bottom) here (once via the margin's
   --native-tab-bar-height, again in the base rule's own padding) would
   over-space it. */
/* ⚠️ REPLACED 2026-09-14 (T228, Nick, from an on-device screenshot): "It looks
   weird around the bar... Just fill in the cut off area with the same white so
   it's one big box."

   The old rule pushed the sheet UP with margin-bottom so it stopped at the tab
   bar's top edge. That was right for a flat, edge-to-edge 49pt UITabBar. It is
   wrong for the iOS 26 floating PILL -- inset from the screen edges with its own
   padding, as the SUPERSEDED note further down this file already established
   from a real on-device photo -- because the sheet now ends above a bar that
   does not reach the edges, leaving a grey band around and below the pill.

   So the white reaches the real screen bottom and the CONTENT clears the bar
   with padding instead. That is also the more robust shape of the same fix: if
   --native-tab-bar-height is off by a few points, the worst case here is
   slightly generous spacing, whereas the margin version showed the error
   directly as a visible band -- which is the bug Nick photographed. The file's
   own note says no CSS formula can know that pill's visual bounds, so the fix
   that does not depend on knowing them is the right one. */
.native-app .modal-box {
    margin-bottom: 0;
    padding-bottom: calc(var(--native-tab-bar-height) + 1.5rem);
}

/* The native "+" FAB overlaps the top of the sheet, and the number pad is
   the sheet where that actually costs something -- reported on device
   2026-09-06: "the keypad is getting covered by the + circle".
   The geometry is exact, not estimated. setUpFabButton() in
   TabBarViewController.swift pins the button's CENTER to tabBar.topAnchor
   (fabSinkIntoBar == 50% of fabDiameter), so a 56pt circle extends exactly
   28pt ABOVE the bar's top edge. The rule above stops the sheet AT that
   edge, so the FAB sits over the sheet's bottom 28pt -- which is where
   "Done" lives.
   36px = that 28pt plus a little breathing room.

   KEPT even though TabBarViewController now hides the FAB while this sheet
   is open, and that is deliberate rather than belt-and-braces for its own
   sake: web code deploys to the phone independently of the native app, so
   a device still running an OLDER build will receive this CSS with no
   FAB-hiding message handler behind it. Without this rule, Done would be
   unreachable there -- a web deploy silently breaking the app on a build
   nobody rebuilt. */
.native-app #numpad-overlay .modal-box {
    /* T228: this 36px was measured against the OLD scheme, where the sheet
       stopped at the tab bar's top edge and 1.5rem was the whole of its bottom
       padding. Now that the sheet runs to the screen edge, the bar's own height
       has to be in here too -- otherwise this override would give the number
       pad LESS clearance than an ordinary sheet and put "Done" back under the
       FAB on exactly the older builds the comment above exists to protect. */
    padding-bottom: calc(var(--native-tab-bar-height) + 1.5rem + 36px);
}

/* Standalone "+" Food shortcut, native app only -- see base.html for why
   this exists separately from the (now-hidden) HTML .bottom-nav-fab
   rather than that same markup just being un-hidden on its own: that
   markup is a flex ITEM inside .bottom-nav's own row, sized/positioned
   assuming 4 siblings around it (its "poke up 22px" trick is a negative
   margin relative to that row, not a real fixed position). This is new,
   independent markup instead -- same visual language (raised circle,
   accent color, a gold glow while already on the Food page, reusing
   .bottom-nav-fab-active's own reasoning for that color choice) but
   centered and fixed on its own, sitting just above the real native tab
   bar the same way the web FAB pokes up above the HTML one. */
.native-fab {
    display: none;
}

/* SUPERSEDED 2026-09-01 -- this whole CSS-positioned approach assumed
   the real native UITabBar renders as a flat, edge-to-edge, exactly-
   49pt bar (the classic pre-iOS-26 shape --native-tab-bar-height was
   built to match). A real on-device photo showed that's no longer
   true on this OS version: UITabBar now renders as a floating, rounded
   "pill" (Apple's iOS 26 redesign), inset from the screen edges with
   its own internal padding -- no CSS formula based on
   env(safe-area-inset-bottom) + a fixed content height can know that
   pill's real visual bounds, only its native LAYOUT frame (which is
   NOT the same thing, confirmed exactly why 2 rounds of "fix the CSS
   math" both measured correct on paper yet still looked wrong).
   Replaced with a genuinely native UIButton in TabBarViewController.swift
   (setUpFabButton()), positioned via Auto Layout directly against the
   real tabBar view -- the same coordinate system the pill itself uses,
   so it can't go out of sync the way CSS approximating it always could.
   This rule now stays `display: none` (see the un-overridden rule right
   above) rather than being deleted outright, so the positioning
   reasoning stays here for reference. */
.native-app .native-fab {
    display: none;
    align-items: center;
    justify-content: center;
    position: fixed;
    /* Direct on-device report: sitting entirely above the real tab bar
       (the original +14px gap here) read as floating way too high --
       the old web FAB's own "poke up above the bar" look (see
       .bottom-nav-fab-icon's -22px margin-top elsewhere in this file)
       actually SINKS partway into the bar instead of clearing it
       cleanly. Direct request: 40% of this circle's own 56px height
       (22.4px) should sit below the real tab bar's top edge, inside its
       area -- so `bottom` (the gap from the SCREEN's bottom edge to
       this element's own bottom edge) needs to be 22.4px LESS than
       --native-tab-bar-height, not more. */
    bottom: calc(var(--native-tab-bar-height) - 22.4px);
    left: 50%;
    transform: translateX(-50%);
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
    font-size: 1.8rem;
    line-height: 1;
    text-decoration: none;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
    /* Below toasts/modals (z-index 1000+) and below the real native tab
       bar (a separate native view with no z-index concept at all --
       always visually on top by virtue of being a later-added, opaque
       native subview, not something this stacking context can reach
       either way); above ordinary page content. */
    z-index: 90;
}

.native-app .native-fab:active {
    transform: translateX(-50%) scale(0.92);
    transition: transform 0.1s ease-out;
}

.native-app .native-fab-active {
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-streak-gold) 35%, transparent), 0 2px 10px rgba(0, 0, 0, 0.3);
}

/* 2026-09-21 visual review, §1.3. ⚠️ THERE WAS NO BASE RULE FOR <a> AT ALL.
   Every link in this file is styled per component, which works right up until
   a link is not part of one -- and then it falls back to the browser's own
   #0000EE underlined blue. That is not a small blemish inside a native app:
   it is the ONLY pure blue anywhere in the product, so it reads instantly as
   a web page showing through. Two were found just by looking: the "Data"
   link on Personal Info and "Log out" at the bottom of Settings.

   ⚠️ Fixed HERE rather than at the two call sites, because the call sites
   were never the bug -- the missing default was, and the next uncovered link
   would have been blue too. Specificity keeps this safe: a bare `a` is
   (0,0,1), so every existing component rule (.back-link, .app-footer a, ...)
   still wins without change.

   --color-accent-bg is the tint links already use elsewhere (.home-micros-more),
   and it is one of T192's repainted tokens -- so links now follow the user's
   chosen app colour, which a hard-coded browser blue never did.

   ⚠️ The UNDERLINE STAYS. iOS tints links rather than underlining them in
   chrome, but these are mostly links in prose (Terms, Privacy, helper text),
   and there colour alone is not an accessible distinction. */
a {
    color: var(--color-accent-bg);
}

/* 2026-09-21 visual review, §1.3, the other half of the browser-blue
   problem. WebKit draws its own #4a9eff ring on a focused field, which
   matches nothing in the palette -- on the food search and on login it was
   the brightest thing on screen and the clearest sign this is a web view.

   ⚠️ REPLACED, NEVER REMOVED. `outline: none` on a focusable control is the
   classic accessibility own-goal: it leaves keyboard and switch-control
   users with no idea where they are. This is the same ring in the app's own
   accent, and :focus-visible means a pointer tap does not draw it. */
:focus-visible {
    outline: 2px solid var(--color-accent-bg);
    outline-offset: 2px;
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
    /* Sits just inside the field's own border rather than outside it, so a
       focused input does not visibly grow and nudge the rows beneath it. */
    outline-offset: 0;
}

h1, h2, h3 {
    margin-top: 0;
}

/* The Dashboard's date heading -- smaller than a normal h1. Previously
   forced onto one line with an ellipsis if it didn't fit -- a real report
   showed that ellipsis actually kicking in and clipping the date on a
   narrow phone screen, which is a worse outcome than just wrapping to a
   second line (rare, and still fully readable) would have been. `clamp()`
   also shrinks the font a little on the narrowest screens before it'd
   ever need to wrap at all.

   Second real report on this same element: even with that fixed, the
   heading still looked cramped against -- and its letters looked
   slightly clipped by -- the fixed .navbar bar above it. Root cause: the
   global `h1, h2, h3 { margin-top: 0; }` reset (further up this file)
   leaves NOTHING between the navbar and this heading except whatever's
   left of .container's fixed 3.5rem top padding after the navbar's own
   real rendered height -- a thin, fragile margin that a small mismatch
   between the two (different browsers/zoom levels render the navbar's
   real height slightly differently) can shrink to almost nothing. A
   real, explicit margin-top here guarantees a visible gap regardless of
   that arithmetic, rather than relying on it landing exactly right. */
.dashboard-date {
    margin-top: 1rem;
    font-size: clamp(1rem, 5vw, 1.25rem);
    white-space: normal;
}

/* Card polish, direct request ("feels like a crappy website"): every
   card in the app used to be flat -- a 1px border, 8px radius, no
   shadow at all -- which read as a plain HTML box rather than a real
   app surface. This treatment was first built scoped to just the
   Trends page (.trends-card, now folded in here as the app-wide
   default instead of a one-page exception) -- bigger radius, a real
   two-layer shadow for actual depth, more breathing room. */
/* "The graph I'm looking at" -- direct request, built TAP-driven rather
   than scroll-driven on purpose. The scroll version (highlight whatever
   card is most centered) has to pick a winner while two cards are both
   half on screen, so the highlight ping-pongs between them as you scroll
   across the boundary, which reads as a bug. A tap is unambiguous, needs
   no scroll listener at all, and can't flicker.

   Applied by chart_focus.js to a .card that contains a <canvas>, and only
   on pages with at least two of them -- highlighting "the one you're on"
   is meaningless when there's only one. :focus-within gives keyboard and
   screen-reader users the same cue for free, without any extra JS. */
.card.chart-focused,
.card.chart-focusable:focus-within {
    border-color: var(--color-accent-bg);
    /* The inset ring is what actually reads as a border at this size --
       it doubles the 1px edge without shifting layout the way a real
       2px border would. The outer glow is the same accent at low alpha,
       deliberately soft: it should say "this one" at a glance, not
       announce itself. */
    box-shadow: inset 0 0 0 1px var(--color-accent-bg),
                0 0 0 3px color-mix(in srgb, var(--color-accent-bg) 14%, transparent),
                0 6px 16px rgba(0, 0, 0, 0.04);
}

.card.chart-focusable {
    /* Only the focusable cards animate, so the transition can't add cost
       to the many plain cards on a page. */
    transition: border-color 160ms ease, box-shadow 160ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .card.chart-focusable {
        transition: none;
    }
}

.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 12px;
    padding: 1.5rem;
    margin-bottom: 1.25rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 6px 16px rgba(0, 0, 0, 0.04);
    /* Lets a wide table scroll sideways within the card on a narrow phone
       screen, instead of forcing the whole page to scroll horizontally.
       Any value other than "visible" here forces this element into its
       own block-formatting-context -- which is also the likely fix for
       the rings card rendering narrower than every other card on iOS
       Safari specifically: a BFC's width is supposed to fill its parent,
       but WebKit has a long-documented history of instead shrink-to-
       fitting it around flex/SVG children with no explicit width of their
       own (exactly what the rings card has -- .ring-row is a flex
       container of SVGs -- and exactly why the table-based cards never
       showed the bug: tables have much more robust auto-width behavior
       everywhere). Explicit width:100% below is the standard fix -- it
       can't be wrong for a browser rendering correctly already (a block
       element should be 100% of its parent regardless), so this is safe
       to apply globally even though only one card was ever affected. */
    width: 100%;
    overflow-x: auto;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .card {
        /* A light-mode shadow all but disappears against a dark surface --
           same "brighten for dark mode" pattern as --color-chart-line
           elsewhere in this file, just via a lighter/more visible shadow
           instead of a color swap. */
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 6px 16px rgba(0, 0, 0, 0.25);
    }
}

:root[data-theme="dark"] .card {
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 6px 16px rgba(0, 0, 0, 0.25);
}

label {
    display: block;
    margin-top: 0.75rem;
    margin-bottom: 0.25rem;
    font-weight: 600;
    font-size: 0.9rem;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="email"],
select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid var(--color-border);
    border-radius: 4px;
    background: var(--color-surface);
    color: var(--color-text);
    /* Deliberately >= 16px: on iOS Safari, a focused input with a smaller
       font causes the browser to auto-zoom the whole page, which is a
       jarring, very "not an app" feeling experience. */
    font-size: 16px;
    min-height: 44px; /* a common minimum comfortable touch-target size */
}

/* Button polish, direct request ("feels like a crappy website"): flat
   4px corners with zero press feedback read as a plain unstyled HTML
   `<button>`, not a real app control. Bigger radius + a real press-down
   scale (same convention the bottom-nav's own :active already
   established -- an instant, direct response to touch, not a decorative
   animation, so it isn't gated behind prefers-reduced-motion either) --
   the primary variant also gets a soft shadow for a bit of real lift,
   same "give the main surfaces actual depth" idea as the .card polish
   above. Secondary/danger stay flat -- keeping that lift on ONLY the
   primary action is what actually preserves the visual hierarchy
   between "the main thing to tap" and everything else. */
/* ⚠️ `button, .btn` sets `display`, and ANY display rule beats the `hidden`
   attribute -- so `el.hidden = true` on a button silently does nothing, which
   is a trap for every button in this app, not just the one that exposed it
   (T229: "Use the original instead" stayed on screen on a verified food, where
   it has no original to go back to). One rule fixes the whole class. */
button[hidden], .btn[hidden] { display: none !important; }

button, .btn {
    display: inline-block;
    margin-top: 1rem;
    padding: 0.6rem 1.2rem;
    min-height: 44px;
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
    border: none;
    border-radius: 8px;
    font-size: 16px;
    cursor: pointer;
    text-decoration: none;
    box-sizing: border-box;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
    transition: transform 0.1s ease-out;
}

/* Nick, 2026-09-24, on the iPhone: "the hover button is too dark". A phone has
   no pointer, so a tapped button KEEPS :hover until you tap somewhere else --
   every button in the app went near-black after a tap, most visibly the date
   picker's month label (dark text on graphite). Hover only means something
   where there really is one. */
@media (hover: hover) {
    button:hover, .btn:hover {
        background: var(--color-graphite);
    }
    /* A text-coloured label button: the graphite above would hide its text. */
    button.date-picker-jump-trigger:hover {
        background: var(--color-accent-soft);
    }
}

button:active, .btn:active {
    transform: scale(0.96);
}

button.secondary, .btn.secondary {
    background: var(--color-muted-fill);
    color: var(--color-cream);
    box-shadow: none;
}

/* Row delete -- the small "x" at the end of a logged entry.
   Direct request 2026-09-04, and it fixed a real design problem rather
   than just a screenshot one: these used to be full red DANGER buttons
   reading "Delete", which made a destructive action the single loudest
   element on the Dashboard. On the App Store screenshot the eye went
   straight to two red "Delete" blocks instead of to the food.

   Small-LOOKING but large-TOUCHING. The glyph is 18px so it recedes;
   the button is a full 44x44 so it still meets Apple's minimum touch
   target. That combination is also SAFER than what it replaces: there
   is no confirmation step on these, and a big red bar is easier to hit
   by accident than a precise small target.

   Muted by default, red on hover/focus/active -- quiet until you reach
   for it, unmistakably destructive once you do. --color-text-muted is
   5.5:1 on the card, so "quiet" doesn't mean low-contrast. */
.btn-delete-row {
    width: 44px;
    height: 44px;
    padding: 0;
    margin: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: 50%;
    color: var(--color-text-muted);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    box-shadow: none;
    transition: color 120ms ease, background-color 120ms ease;
}

.btn-delete-row:hover,
.btn-delete-row:focus-visible {
    color: var(--color-danger);
    background: color-mix(in srgb, var(--color-danger) 10%, transparent);
}

.btn-delete-row:active {
    color: var(--color-danger);
    /* Overrides the app-wide button:active scale -- a 44px target
       shrinking under the finger makes it feel like it slipped. */
    transform: none;
    background: color-mix(in srgb, var(--color-danger) 18%, transparent);
}

@media (prefers-reduced-motion: reduce) {
    .btn-delete-row { transition: none; }
}

button.danger, .btn.danger {
    background: var(--color-danger);
    color: var(--color-cream);
    box-shadow: none;
}

/* A button with an inline SVG icon before its text (e.g. "Scan Barcode") --
   see base.html's bottom-nav icons for why inline SVG instead of emoji. */
.btn-with-icon {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}

.btn-icon-svg {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
}

/* The camera "lens" -- filled with the button's own background color so it
   reads as a cut-out circle rather than a solid dot. Matches the default
   button background; a camera icon on a differently-colored button variant
   would need its own override, but this is the only button using it. */
.btn-icon-svg-cutout {
    fill: var(--color-accent-text);
}

.flashes {
    margin-bottom: 1rem;
}

.flash {
    padding: 0.75rem 1rem;
    border-radius: 4px;
    margin-bottom: 0.5rem;
    font-size: 0.9rem;
}

.flash-error { background: var(--color-error-bg); color: var(--color-error-text); }
.flash-success { background: var(--color-success-bg); color: var(--color-success-text); }
.flash-info { background: var(--color-info-bg); color: var(--color-info-text); }
/* T151. Reuses the --color-edited-* amber rather than introducing a
   fourth flash palette, and for the same reason those vars exist: red
   means "you made a mistake, fix it", and this is the softer thing --
   something worth a look that the app will happily save anyway. Added
   because "warning" was previously an UNSTYLED category: flash() with it
   rendered a .flash with no background at all, which is a warning nobody
   can see. */
.flash-warning { background: var(--color-edited-bg); color: var(--color-edited-text); }

/* Toast-ified flash messages, direct request (2026-09-01, see toast.js
   and the comment in base.html): a real one-off flash() message now
   floats over the page near the top instead of sitting inline in the
   normal document flow, so it can appear/disappear without shoving page
   content up and down as it comes and goes. Layered on top of the
   .flash/.flash-{category} color rules above, not a replacement for
   them -- .toast only adds positioning/motion/shape. */
.toast-container {
    position: fixed;
    top: calc(0.75rem + env(safe-area-inset-top));
    left: 0;
    right: 0;
    z-index: 1200;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0 1rem;
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    max-width: 420px;
    width: 100%;
    margin: 0;
    border-radius: 10px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
    cursor: pointer;
    text-align: center;
}

@media (prefers-reduced-motion: no-preference) {
    .toast {
        animation: toast-slide-in 0.2s ease-out;
        transition: opacity 0.2s ease-in, transform 0.2s ease-in;
    }

    .toast-dismissing {
        opacity: 0;
        transform: translateY(-12px);
    }

    @keyframes toast-slide-in {
        from {
            opacity: 0;
            transform: translateY(-12px);
        }
        to {
            opacity: 1;
            transform: translateY(0);
        }
    }
}

/* A standing inline callout (unlike .flash, which is a one-time message
   that gets dismissed on the next page load) -- same visual weight as
   .flash-info on purpose, so it actually gets noticed instead of reading
   like another muted helper line. Currently only used by the Add Food
   page's 100g-fallback note (see add_food.html) but generic/reusable. */
.info-callout {
    background: var(--color-info-bg);
    color: var(--color-info-text);
    padding: 0.75rem 1rem;
    border-radius: 4px;
    margin: 0.75rem 0;
    font-size: 0.9rem;
}

/* The interactive serving-size fallback prompt (add_food.html) -- a
   number input + a grams/calories toggle sitting inside .info-callout
   above. Direct request, designed together (see TODO.md for the actual
   questions asked and answered): replaces what used to be a plain
   "do the math yourself" warning. */
/* ---- Details: scaled vs. overridden nutrition fields -------------------
 *
 * Two DIFFERENT states that must not look the same, because the user's
 * relationship to each number is different:
 *
 *   .field-scaled   -- the app did arithmetic the user asked for by
 *                      changing the serving size amount. The number is
 *                      still the source's data, just multiplied. Nothing
 *                      is wrong; this is a quiet accent, not a warning.
 *   .field-edited   -- the user typed over the source's own figure. That
 *                      is a real (and allowed) departure from the food
 *                      database, so it earns the amber treatment.
 *
 * Both are a left border rather than a full-field fill: the number itself
 * must stay the most legible thing in the row. A filled input with
 * coloured text is how a form says "error", and neither of these is one.
 */
/* `input.` rather than a bare class, and that is load-bearing rather than
   stylistic: the shared field styling above is `input[type="number"], ...`
   at specificity (0,1,1), which beats a lone class at (0,1,0) -- so the
   first version of these rules applied the class and changed nothing
   visible. Matching (0,1,1) and sitting later in the file wins the tie
   without reaching for !important. Caught by looking at a rendered
   screenshot; the markup and the class list were both already correct,
   which is exactly why a DOM-level assertion alone would have missed it. */
input.field-scaled,
input.field-scanned,
input.field-needed,
input.field-edited {
    /* A thick left edge plus a tinted whole-field border. Both are drawn
       INSIDE the existing 1px border's box (border-width only grows
       inward here because inputs are border-box), so nothing shifts
       position when the state appears -- a jumping form reads as a glitch.
       An earlier version used only the 4px left edge and it was genuinely
       too easy to miss on a phone at arm's length: verified by looking at
       a real screenshot, not assumed from the CSS. */
    border-width: 2px;
    border-left-width: 5px;
    border-style: solid;
}

input.field-scaled { border-color: var(--color-accent-bg); }

/* A field the SOURCE left blank -- a third state, and deliberately not
   the amber "you edited this" one. Nothing is wrong and the user has done
   nothing; the food database simply had a hole. A dashed border says
   "incomplete, fill me" the way an empty form slot does, rather than
   "warning". */
input.field-missing {
    border-color: var(--color-text-muted);
    border-style: dashed;
    border-width: 2px;
}

.field-note-missing {
    /* T115, 2026-09-06. Was `var(--color-surface-2, var(--color-bg))`, the
       same dead-token bug check 14 fixed for the number-pad keys, at a
       different selector -- and it shipped before anyone caught it.
       It is WORSE than the pad-key case rather than milder, which a render
       showed and reading the CSS did not: --color-surface-2 is undefined,
       AND so is the fallback --color-bg (the real token is
       --color-page-bg). With both undefined the whole declaration is
       invalid, so this computed to rgba(0,0,0,0) -- no background at all,
       in BOTH themes -- not to the page colour. Measured, not inferred.
       Same color-mix as the pad keys, so the two tinted surfaces agree. */
    background: color-mix(in srgb, var(--color-text) 7%, var(--color-surface));
    border-left: 4px solid var(--color-text-muted);
    color: var(--color-text);
    padding: 0.5rem 0.7rem;
    border-radius: 6px;
}

/* T152: filled by a nutrition-label PHOTO, not typed and not looked up.
   Deliberately the same amber as .field-edited rather than a fourth
   colour: the palette already uses amber to mean "the app put this here,
   look at it before you trust it", and that is exactly what a scanned
   number is. It is a SEPARATE CLASS from .field-edited even though it
   renders identically, because the two say different things -- "you
   changed this" versus "a camera read this" -- and .field-edited carries
   per-field "You changed X from Y" notes that would be a lie here.
   OCR misreads digits; every one of these fields is editable on purpose. */
input.field-scanned,
input.field-needed,
input.field-edited {
    border-color: var(--color-edited-border);
    /* A wash, not a fill: the typed number stays the normal text colour on
       a barely-tinted ground. Coloured TEXT on a saturated field is how a
       form says "error, fix this", and an override is not an error -- it
       is a correction the app accepts and saves. */
    background: var(--color-edited-bg);
    color: var(--color-text);
    /* 2px, raised from the default 2026-09-06. The 1px amber border was
       reported on device as not jumping out (T39 check 13). Weight is the
       lever used rather than switching to red, which would have cost the
       amber/red distinction the palette deliberately keeps. Applies to the
       calorie field as well as the macros, on purpose -- one override
       language across the form, not a louder one for macros. */
    border-width: 2px;
}

/* The one-line explanation under a changed field. Deliberately inline and
   small: it has to be readable without being the loudest thing on a
   screen whose actual job is the number above it. */
.field-note {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    flex-wrap: wrap;
    font-size: 0.82rem;
    line-height: 1.4;
    margin: 0.35rem 0 0.75rem;
}

/* The scaling notice. Was plain muted grey text, which Nick found too
   easy to miss on device (2026-09-06) -- and a message explaining that
   every number below it just changed is exactly the wrong thing to make
   quiet. Now a real tinted callout in the app's accent, matching the
   weight of the amber "you edited this" note without borrowing its
   warning colour: scaling is arithmetic the app did correctly, not
   something to worry about. */
.field-note-scaled {
    background: var(--color-accent-soft, var(--color-bg));
    color: var(--color-text);
    border-left: 4px solid var(--color-accent-bg);
    padding: 0.5rem 0.7rem;
    border-radius: 6px;
}

.field-note-edited {
    background: var(--color-edited-bg);
    color: var(--color-edited-text);
    padding: 0.5rem 0.7rem;
    border-radius: 6px;
    /* A left rule, matching .field-note-scaled and .field-note-missing.
       Added 2026-09-06 with the macro notes: three of these can now stack
       down the form, and a bare tinted block repeated three times reads as
       striping rather than as three separate warnings. */
    border-left: 4px solid var(--color-edited-border);
}

/* The "you changed this" line on a macro. Same amber and same shape as the
   calorie note above -- an edited macro and an edited calorie figure are
   the same act and must not look like two different ones.
   The extra weight here is what answers "it doesn't jump out" (T39 check
   13): the lead phrase is bold, and the note carries an explicit
   check-the-label instruction rather than only stating what changed. */
.macro-edited-note {
    font-size: 0.85rem;
    margin-top: 0.4rem;
}

.macro-edited-note .macro-edited-lead {
    font-weight: 700;
}

/* "Reset"/"Undo" inside a field note. A real button element (not a link)
   because it performs an action rather than navigating, but styled flat
   so it does not compete with the form's actual Save. */
.field-note button {
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    color: inherit;
    font-weight: 600;
    text-decoration: underline;
    cursor: pointer;
    min-height: 0; /* overrides the global button min-height -- this is text */
}

.field-note button:hover,
.field-note button:focus-visible { opacity: 0.75; }

/* ---- The number pad ----------------------------------------------------
 *
 * Replaces the phone's full keyboard for numeric entry on this page.
 * Direct request: the system keyboard is enormous, mostly letters that
 * cannot be typed into a number field anyway, and it covers the very
 * figures the user is trying to compare against a package label.
 *
 * Reuses .modal-overlay/.modal-box (the app's existing bottom-sheet
 * convention) rather than inventing a second kind of sheet, so it slides
 * up, dims, and dismisses exactly like every other sheet here.
 */
.numpad-value {
    /* The live value, shown big, because on a phone the real field is
       behind the sheet and cannot be relied on to be visible. */
    font-size: 2rem;
    font-weight: 600;
    text-align: right;
    padding: 0.4rem 0.6rem 0.75rem;
    min-height: 2.6rem;
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}

.numpad-label {
    font-size: 0.85rem;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* ROUND KEYS, 2026-09-06 (direct request: "can we change the number pad to
   circle buttons", after Apple's own dialer/passcode keypad).
   Deliberately done in CSS rather than in Swift, which was the form the
   request was raised in. The pad is a WEB component -- one partial shared
   by Add Food and Log Weight, driven by number_pad.js -- so it renders
   inside the WKWebView on the phone and in an ordinary browser on the
   website. Rebuilding it as a native UIView would have meant a second
   implementation to keep in sync, a bridge message per keystroke, and a
   native rebuild + reinstall for every future tweak -- and the website
   would still need the web one anyway. The circles are the goal; Swift was
   only ever the assumed means. */
.numpad-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    /* justify-items centres each circle in its column, so the grid can stay
       fluid (1fr) while the keys themselves stay perfectly round. */
    justify-items: center;
    gap: 0.6rem;
}

.numpad-grid button {
    /* Round keys are sized by WIDTH with a 1:1 aspect ratio rather than by
       min-height, so the circle can never render as an oval on a narrow or
       very wide screen. clamp() keeps it past the 44px minimum touch target
       on the smallest supported phone and stops it ballooning on a tablet
       or a desktop browser, where 1fr of a wide sheet would otherwise give
       an absurdly large disc. */
    width: clamp(56px, 19vw, 76px);
    aspect-ratio: 1;
    min-height: 0; /* the global button min-height would fight aspect-ratio */
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    font-weight: 500;
    /* NOT `var(--color-surface-2, var(--color-bg))`, which is what this was.
       --color-surface-2 is never defined anywhere in this file, so that
       always resolved to the fallback -- the PAGE background -- while the
       sheet around it is --color-surface. In light those two are both white
       and nobody noticed; in dark they are #101a1e on #24211f and the keys
       nearly disappeared, which the round shape made obvious because the
       1px border became the only thing defining a key.
       Mixed against the text colour instead, so it is a real step off the
       sheet in BOTH themes and stays correct if either token moves. */
    background: color-mix(in srgb, var(--color-text) 7%, var(--color-surface));
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: 50%;
    margin: 0;
}

.numpad-grid button:active { transform: scale(0.93); }

/* Done stays a full-width pill, NOT a circle. It is a confirmation
   carrying a word, not a key -- a round button cannot hold "Done" legibly
   at this size, and making it a disc among the digits would also read as a
   fourth key in the bottom row rather than as the way out of the sheet. */
/* `.numpad-grid .numpad-done`, NOT a bare `.numpad-done`. Specificity, and
   it bit immediately: `.numpad-grid button` scores one class + one element,
   which BEATS a lone class -- so the round `width: clamp()` and
   `aspect-ratio: 1` above kept winning and Done rendered as a circle in the
   bottom-left corner with its text overflowing. Caught in a screenshot, not
   by reasoning about the cascade. */
.numpad-grid .numpad-done {
    grid-column: span 3;
    justify-self: stretch;
    width: 100%;
    aspect-ratio: auto;
    min-height: 52px;
    border-radius: 999px;
    background: var(--color-accent-bg) !important;
    color: var(--color-accent-text) !important;
    border-color: var(--color-accent-bg) !important;
    font-weight: 600;
    font-size: 1.05rem;
    margin-top: 0.35rem;
}

@media (prefers-reduced-motion: reduce) {
    .numpad-grid button:active { transform: none; }
}

.fallback-input-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 0.5rem;
    flex-wrap: wrap;
}

.fallback-input-row #fallback-amount-input {
    /* Overrides the global input's 100%-width default -- this sits in a
       row next to the unit toggle, not stacked full-width like a normal
       form field. */
    width: 100px;
    flex: 0 0 auto;
    margin-top: 0;
}

.fallback-unit-toggle {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
}

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

th, td {
    text-align: left;
    padding: 0.5rem;
    border-bottom: 1px solid var(--color-border);
    font-size: 0.9rem;
}

.meal-section h3 {
    margin-bottom: 0.25rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.add-meal-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
    text-decoration: none;
    font-size: 1rem;
    line-height: 1;
}

.add-meal-btn:hover {
    background: var(--color-graphite);
}

.meal-total {
    color: var(--color-text-muted);
    font-size: 0.9rem;
    margin-bottom: 0.5rem;
}

/* ---- Diary panels (the "By Meal" card until 2026-09-08) ---------------
 *
 * Each meal is its own panel inside the one card. The complaint being
 * fixed: four headings run together in a single block gave an EMPTY meal
 * the same visual weight as one holding three foods, and a meal's last
 * food sat flush against the next meal's heading, so a real day read as
 * one undifferentiated list.
 *
 * The gap between panels (0.5rem) is deliberately much tighter than the
 * gap between cards (~1rem+): these are parts of one card, and spacing
 * them like separate cards would break the Diary card into four. That
 * was the explicit ask -- "their own box... but still closer than the
 * usual spacing between boxes".
 */
.meal-panel {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: 10px;
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.5rem;
}

.meal-panel:last-of-type { margin-bottom: 0.9rem; }

/* An empty meal recedes rather than disappearing: still one tap from
   logging into it, but not competing with meals that have real content.
   Deliberately not hidden -- "add to lunch" has to stay reachable before
   lunch exists. */
.meal-panel.is-empty {
    background: transparent;
    border-style: dashed;
    padding-top: 0.45rem;
    padding-bottom: 0.45rem;
}

.meal-panel.is-empty .meal-panel-total { color: var(--color-text-muted); }

.meal-panel-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.meal-panel-head h3 {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 600;
}

.meal-panel-total {
    margin-left: auto;
    font-size: 0.88rem;
    font-variant-numeric: tabular-nums; /* totals line up down the column */
    color: var(--color-text);
}

.meal-entry-list {
    list-style: none;
    margin: 0.5rem 0 0;
    padding: 0;
}

.meal-entry {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0;
    border-top: 1px solid var(--color-border);
}

/* The name gets the flexible space and wraps if it must; the number and
   the buttons keep their own. Previously a table let a long name squeeze
   the calorie column into wrapping mid-value ("500" above "kcal"). */
.meal-entry-name {
    flex: 1;
    min-width: 0;
    font-size: 0.9rem;
}

.meal-entry-qty {
    color: var(--color-text-muted);
    margin-left: 0.3rem;
    font-size: 0.85rem;
}

.meal-entry-kcal {
    flex: none;
    font-size: 0.9rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* "kcal" smaller and quieter than the figure -- the number is the content,
   the unit is a label, and at this size repeating it at full weight on
   every row is most of the visual noise. */
.meal-entry-unit {
    color: var(--color-text-muted);
    font-size: 0.72rem;
    margin-left: 0.2rem;
}

.meal-entry-actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.35rem;
}

/* Condensed meal breakdown on the Dashboard's Diary card -- tighter
   than .meal-section above (no separate total line per meal beyond the
   inline one, no "nothing logged" filler for empty meals, less vertical
   margin between meals). */
.meal-section-compact {
    margin-bottom: 0.4rem;
}

.meal-section-compact h3 {
    margin: 0.4rem 0 0.15rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1rem;
}

.meal-total-inline {
    /* Darker than the old muted grey -- easier to read at a glance
       against each meal's own subtotal. */
    color: var(--color-text);
    font-weight: normal;
    font-size: 0.85rem;
    margin-left: auto;
}

/* The Diary card's own day-total, next to its <h2> -- distinct from
   .meal-total-inline above (that one's for each individual meal's own
   subtotal, inside a flex row where margin-left:auto pushes it right).
   Bigger and bold now -- it's the one number on this card that matters
   most at a glance, so it should read as more prominent than the
   per-meal subtotals underneath it. */
.section-total {
    color: var(--color-text-muted);
    font-weight: 700;
    font-size: 1.1rem;
}

.meal-section-compact table {
    margin-bottom: 0.25rem;
}

.meal-section-compact td {
    padding: 0.3rem 0.5rem;
    font-size: 0.85rem;
}

/* T31: the Dashboard row's small details link sits beside the existing
   delete control without making either action dominate the food name.
   The 28px targets stay comfortably tappable while the visual marks are
   deliberately quiet and consistent with the app's other (i) controls. */
.entry-row-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.3rem;
    white-space: nowrap;
}

.btn-info-row {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border: 1px solid var(--color-border);
    border-radius: 50%;
    color: var(--color-text-muted);
    text-decoration: none;
    font-family: Georgia, serif;
    font-size: 0.82rem;
    font-weight: 700;
    line-height: 1;
}

.btn-info-row:hover,
.btn-info-row:focus-visible {
    color: var(--color-accent);
    border-color: var(--color-accent);
}

/* Groups the calorie ring + macro rings together at the top of the
   Dashboard's rings card (see main/dashboard.html). */
.today-summary {
    text-align: center;
    margin-bottom: 0.5rem;
}

.search-results {
    border: 1px solid var(--color-border);
    border-radius: 4px;
    margin-top: 0.25rem;
    /* 240px showed about FIVE rows and was reported as too few (2026-09-06:
       "put the visual limit to 15, not the 5 it currently is").
       Expressed in vh rather than a row count, deliberately: rows are now
       two lines tall (see .search-result-name), so fifteen of them is
       roughly 900px -- taller than the whole 852px phone screen, before the
       keyboard covers half of it. A fixed 15-row height would just push the
       list off-screen. 65vh fills the space that actually exists above the
       keyboard and scrolls for the rest. */
    max-height: 65vh;
    overflow-y: auto;
}

.search-result-item {
    /* flex, not the plain block this used to be -- a row can now hold
       an optional source-link button (see search-result-source-btn
       below) alongside the food name/calorie text, needing to sit at
       the far right rather than wrap onto its own line. */
    display: flex;
    /* WRAPS as of 2026-09-06, so the name gets the row's FULL width on its
       own line and the meta + source button share the line beneath it.
       Two lines of name alone was NOT enough and this was measured, not
       assumed: with the meta sharing the line, the name had only 163px on
       a 393px screen, and "Starbucks, Double Smoked Bacon, Cheddar & Egg
       Sandwich" still truncated across both lines. Stacking takes the name
       to the full ~357px, which fits it comfortably.
       The ordering below is explicit rather than relying on source order,
       because the source button is the LAST child in the markup but has to
       render on the second line beside the meta, not after it. */
    flex-wrap: wrap;
    align-items: center;
    gap: 0.15rem 0.5rem;
    padding: 0.5rem;
    cursor: pointer;
    border-bottom: 1px solid var(--color-border);
}

.search-result-item:hover {
    background: var(--color-page-bg);
}

/* Real bug found and fixed (2026-08-31): the food name and its
   calorie/serving info used to be ONE bare flex-child span. A flex
   item's default min-width is `auto`, not 0 -- a long product name
   couldn't shrink below its own content width, which could push the
   source-link button (search-result-source-btn, flex-shrink: 0) off a
   real narrow phone screen entirely, invisible rather than un-rendered.
   Split into two elements instead of guessing at a blind CSS tweak: the
   NAME is the part that's fine to truncate if it's genuinely too long;
   the calorie/serving info -- what someone's actually deciding on -- and
   the source button both stay fully visible always. */
/* TWO LINES, not one, 2026-09-06 (direct report: "we can't really see past
   the brand name"). This was `white-space: nowrap` with an ellipsis, which
   on a real product name like "Starbucks, Double Smoked Bacon, Cheddar &
   Egg Sandwich" cut off at roughly "Starbucks, Double Smoke..." -- every
   Starbucks item truncating to the same visible text, so a list of them
   read as several copies of one thing.
   The name is still the part allowed to truncate, and the meta and source
   button still never do; that split is unchanged. It just gets a second
   line before giving up. */
.search-result-name {
    /* 100% basis == its own full-width line inside the wrapping row. */
    flex: 1 1 100%;
    min-width: 0;
    order: 1;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    /* A -webkit-box adds its own ellipsis at the clamp, so text-overflow is
       not needed -- but nowrap must GO, or the box has only one line to
       clamp to and nothing changes. */
    overflow-wrap: anywhere;
    line-height: 1.3;
}

/* Design decision, direct request ("remove the em dash... needs a
   decision on what replaces it"): nothing. The name/meta split above
   already stopped concatenating them into one string with " -- " glued
   in the middle -- restoring a literal separator character would just
   be a smaller version of the same problem. A muted, slightly smaller
   treatment (same convention as .muted elsewhere in this app) does the
   actual job a separator was standing in for -- reading as secondary
   detail next to the name, not equal-weight text -- without needing a
   character to draw a line between them at all. */
/* T229 (Nick, 2026-09-14): "I couldn't see the panera brand name from the
   search bar." The brand was in the payload already and simply never drawn.

   Its own full-width line between the name and the numbers -- order 2, with the
   meta moving to 3 -- rather than nested inside .search-result-name, which is a
   -webkit-box clamped to 2 lines and would swallow the brand on a long title.
   Muted and small, the same treatment the meta line uses, because it is
   secondary to the name rather than equal to it. */
.search-result-brand {
    flex: 1 1 100%;
    min-width: 0;
    order: 2;
    color: var(--color-text-muted);
    font-size: 0.85rem;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-result-meta {
    flex-shrink: 0;
    order: 3;
    color: var(--color-text-muted);
    font-size: 0.85rem;
}

/* Add Food page: Custom Foods/Custom Meals quick-access, direct request
   re-confirmed still wanted after the fullscreen-search restructure
   superseded the original ask without re-asking. 2 equal tiles (not
   dropdowns, per the earlier analysis in TODO.md) -- bordered/rounded to
   read as a real tappable tile, not a plain link, matching .card's own
   border/radius so it feels native to this page rather than a one-off
   shape. */
.quick-access-tiles {
    display: flex;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

.quick-access-tile {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    padding: 0.85rem 0.5rem;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    color: var(--color-text);
    text-decoration: none;
    font-size: 0.9rem;
    text-align: center;
}

.quick-access-tile:hover {
    background: var(--color-page-bg);
}

/* Real gap found and fixed, direct report ("food search results lag
   noticeably behind typing... looks visually broken"): a real search
   genuinely takes 1-2+ seconds (see the comment on showSearchLoading()
   in add_food.js for why) -- this doesn't make that faster, it replaces
   the total silence that used to sit there the whole time with a
   visible "still working on it" signal, which is what was actually
   making the delay look broken rather than just slow. An indeterminate
   bar, not a real progress bar, since there's no way to know in advance
   how long an outside API will take -- same honest signal a browser's
   own top-of-page loading bar gives for a page that's still loading. */
.search-loading-bar {
    height: 3px;
    margin: 0.5rem 0;
    border-radius: 2px;
    background: var(--color-border);
    overflow: hidden;
    position: relative;
}

.search-loading-bar::after {
    content: "";
    position: absolute;
    top: 0;
    left: -40%;
    width: 40%;
    height: 100%;
    border-radius: 2px;
    background: var(--color-accent-bg);
    animation: search-loading-sweep 1.1s ease-in-out infinite;
}

@keyframes search-loading-sweep {
    from {
        left: -40%;
    }
    to {
        left: 100%;
    }
}

/* A static, non-sweeping bar instead for anyone who's asked their OS to
   reduce motion -- still a clear "something is happening" signal
   (a filled bar reads as active/pending), just without the animation. */
@media (prefers-reduced-motion: reduce) {
    .search-loading-bar::after {
        animation: none;
        left: 0;
        width: 100%;
        opacity: 0.5;
    }
}

/* Nick, 2026-09-28: the bar alone read as a blank screen while a lookup fell
   through from Open Food Facts to USDA. Words under it, and after 3 s the first
   line swaps for a "still working" one (both stacked in one grid cell), so a
   slow fallback never looks broken. CSS-only on purpose: when the results
   replace this markup, the swap goes with it -- no timer to cancel. */
.search-loading-text {
    margin: 0 0 0.5rem;
    padding-inline: 0.5rem; /* lines up with .search-result-item's text */
    font-size: 0.9rem;
    color: var(--color-text-muted);
}

.loading-swap {
    display: grid;
}

.loading-swap span {
    grid-area: 1 / 1;
}

.loading-swap span:first-child {
    animation: search-loading-hide 0s 3s forwards;
}

.loading-swap span:last-child {
    opacity: 0;
    animation: search-loading-show 0s 3s forwards;
}

@keyframes search-loading-hide {
    to {
        opacity: 0;
    }
}

@keyframes search-loading-show {
    to {
        opacity: 1;
    }
}

/* A picked result that still has to be fetched: its calorie line becomes the
   loading line, and the rest of the list dims and ignores taps (no double
   lookups) until it lands. */
.is-picking .search-result-item {
    pointer-events: none;
}

.is-picking .search-result-item:not(.is-picked) {
    opacity: 0.5;
}

/* The small "view original source" icon button on a USDA/Open Food
   Facts search result (see openSourceLink() in add_food.js/
   meal_builder.js) -- direct request, a real link back to the source
   instead of the plain text "(USDA)"/"(Open Food Facts)" tag removed
   earlier in this project. Muted/small so it doesn't compete with the
   actual food name for attention -- this is a secondary "go verify
   this" action, not the primary one (picking the food). */
.search-result-source-btn {
    /* order 3 + margin-left:auto: sits on the meta's line, pushed right.
       Without the explicit order it would follow the meta anyway, but the
       auto margin is what keeps it hard right now that the row wraps and
       justify-content: space-between no longer applies per-line. */

    /* A real <button> (needed -- this triggers JS, not a navigation),
       unlike .navbar-icon-btn (a plain <a> tag) -- meaning it inherits
       the global `button` rule's solid accent background/padding/
       margin/min-height/border-radius, all reset back out here so this
       reads as a plain icon, not a second mini accent-colored pill
       button sitting inside the search result row. */
    all: unset;
    /* ⚠️ AFTER all:unset, not before it (2026-09-24): `all` resets every
       property, and these two used to sit above it -- so the icon lost its
       place and showed top-left on a line of its own. */
    order: 3;
    margin-left: auto;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    cursor: pointer;
    color: var(--color-text-muted);
}

.search-result-source-btn:hover {
    color: var(--color-text);
}

.muted {
    color: var(--color-text-muted);
    font-size: 0.85rem;
}

/* A plain "leave this page" link, e.g. at the top of Add Food -- styled
   muted/understated on purpose so it reads as navigation, not as
   emphasized page content the way an unstyled blue link would. */
/* Direct request (2026-09-01): "the 'go back to settings' button is too
   small, I want it bigger and more clear... so they look like app
   buttons." This was a plain 0.9rem muted text link with no real tap
   target beyond its own glyphs -- fine on a desktop, genuinely fiddly on
   a phone.

   Restyled here rather than rebuilt natively in Swift (the other option
   weighed): this one class is shared by 17 links across 16 pages --
   Settings, admin, Custom Foods/Meals, notifications, and Add Food's own
   in-page "Back to search" -- so a single CSS change covers all of them
   AND keeps working on the website and PWA, where a native nav bar
   wouldn't exist at all. A native back button would also mean
   maintaining two of them for the same pages.

   Shaped like a real iOS back control: a proper 44px tap target (Apple's
   own minimum), the chevron as a sized ::before rather than the old
   inline &larr; glyph, and a subtle pill that appears on press so a tap
   visibly registers. */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-height: 44px;
    padding: 0.35rem 0.75rem 0.35rem 0.5rem;
    margin: 0 0 0.35rem -0.5rem; /* negative left margin keeps the TEXT
        optically aligned with the page heading below it, despite the new
        padding -- without this the whole page looks indented by a hair */
    border-radius: 10px;
    color: var(--color-text);
    text-decoration: none;
    font-size: 1.05rem;
    font-weight: 500;
    line-height: 1;
}

/* The chevron, drawn here rather than typed into each template: every
   one of those links used to write its own literal "&larr;", which meant
   the arrow's size and spacing weren't controllable from CSS at all.
   They now write just the label ("Back to Settings"), and this supplies
   a properly weighted "‹" scaled to match. */
.back-link::before {
    content: "\2039";
    font-size: 1.6rem;
    line-height: 1;
    margin-top: -0.15em; /* optical centering: the glyph sits high in its
                            own em box */
}

.back-link:active {
    background: var(--color-border);
}

@media (hover: hover) {
    .back-link:hover {
        background: var(--color-border);
    }
}

.back-link:hover {
    color: var(--color-text);
}

/* Weight page's log form -- weight/body-fat/date side by side instead of
   each stacked full-width, now that this form shares the page with the
   chart and history table below it (see measurements/add_measurement.html). */
.weight-log-row {
    display: flex;
    gap: 0.75rem;
}

.weight-log-row > div {
    flex: 1;
    min-width: 0;
}

/* Reserves the same vertical space for every label in this row, whether
   its text wraps to one line ("Weight", "Date") or two ("Body fat %
   (optional)") -- without this, the shorter labels' inputs start higher
   than the wrapped one's, so the three input boxes don't line up. */
.weight-log-row label {
    min-height: 2.4em;
}

.day-nav {
    display: flex;
    /* Direct follow-up against a real screenshot: space-between spread
       the prev arrow, calendar button, and next arrow across the full
       card width -- reported as not looking good, wanted as one tight,
       centered cluster instead ("almost touching"). A small gap plus
       centering does that; the arrows still keep their own comfortable
       44px tap target (.icon-nav-btn), just with real space removed
       between them instead of flex auto-distributing it. */
    justify-content: center;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

/* A plain .btn sizes itself to its text content -- fine for the
   original "← Aug 28" text-link style, but an icon-only version needs
   an explicit square-ish footprint instead of shrink-wrapping tight
   around just the SVG (which reads as an oddly small, off-center tap
   target). Square, matches the calendar button's own comfortable size. */
.icon-nav-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    padding: 0;
}

/* Dashboard's "this week" weight widget -- same idea as .day-nav above,
   just sitting inside a card (so no bottom margin needed to separate it
   from something below) with arrow buttons instead of full prev/next
   date links. */
.week-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 1rem;
}

.week-nav button {
    margin-top: 0;
    padding: 0.4rem 0.9rem;
    min-height: 36px; /* matches .range-btn's smaller size -- these aren't
                          the primary action in the card, the same reasoning
                          as the Trends page's range buttons */
}

.week-nav button:disabled {
    opacity: 0.4;
    cursor: default;
}

#reader {
    max-width: 360px;
    margin-top: 0.75rem;
    /* Scan Barcode polish, direct request: html5-qrcode renders its own
       video feed + scanning-box overlay INSIDE this element -- not this
       app's own markup to restyle -- but the container wrapping that
       feed is fully ours. A real border/rounded corners/centering here
       is the actual "look better" this app can control without
       reaching into the library's own internals. */
    margin-left: auto;
    margin-right: auto;
    border-radius: 8px;
    overflow: hidden;
    border: 1px solid var(--color-border);
}

/* Direct request: the real back/exit control while the camera view is
   open -- bigger and full-width instead of the small default secondary
   button size every other page uses, since this is the one thing
   standing between an open camera and getting back out. */
.stop-scan-btn {
    display: block;
    width: 100%;
    max-width: 360px;
    margin-left: auto;
    margin-right: auto;
    min-height: 52px;
    font-size: 17px;
    font-weight: 600;
}

/* Add Food page header, direct request ("shrink + raise the search
   bar") -- the other real lever besides removing the redundant "Search"
   h2 (see add_food.html's own comment): this page's own <h1> used to
   get the same margin-top every other page's heading gets, which was
   real, avoidable dead space specifically on THIS page, where the goal
   was raising the search bar as high as reasonably possible. */
.food-page-header-compact h1 {
    margin-top: 0.25rem;
    margin-bottom: 0.5rem;
}

/* The Add Food search box (T200, Nick 2026-09-13): its own full-width line,
   so a long food name stays readable while typing, with a small round ✕
   inside its right edge while the search screen is open (it replaced a
   "Cancel" button that squeezed the box), and the two scanners as labelled
   buttons on the line below. */
.search-bar-row {
    position: relative;
    display: flex;
    align-items: center;
}

/* Font-size stays at the inherited 16px on purpose: smaller brings back the
   iOS auto-zoom-on-focus bug the global input rule guards against. */
.search-bar-row #food-search-input {
    flex: 1;
    margin-top: 0;
    min-height: 42px;
    padding: 0.45rem 2.6rem 0.45rem 0.7rem;   /* right padding clears the ✕ */
}

.search-clear-btn {
    position: absolute;
    right: 0.45rem;
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    min-height: 0;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--color-border);
    color: var(--color-text-muted);
}
.search-clear-btn svg { width: 14px; height: 14px; }
.search-clear-btn:hover, .search-clear-btn:active { background: var(--color-text-muted); color: var(--color-surface); }

.scan-buttons-row {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.6rem;
}

.scan-btn {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    margin-top: 0;
    min-height: 42px;
    padding: 0.45rem 0.6rem;
    white-space: nowrap;
}
.scan-btn .btn-icon-svg { width: 18px; height: 18px; flex: none; }

/* The fullscreen search screen, direct request (2026-08-31): tapping the
   search box promotes #search-card itself into this -- input pinned at
   the top (grown a little bigger, see below), camera icon and results
   right underneath, everything else on the page covered. Reuses the
   card that already holds all of this rather than a second, duplicate
   search UI to keep in sync -- see openSearchOverlay() in add_food.js. */
.search-card-overlay {
    position: fixed;
    inset: 0;
    z-index: 1000;
    margin: 0;
    border: none;
    border-radius: 0;
    overflow-y: auto;
    /* iOS safe-area insets -- this covers the whole viewport, including
       the area the native status bar/notch would otherwise sit over. */
    padding-top: max(1.25rem, env(safe-area-inset-top));
    padding-bottom: max(1.25rem, env(safe-area-inset-bottom));
}

.search-card-overlay .search-bar-row #food-search-input {
    min-height: 50px;
    padding: 0.6rem 2.9rem 0.6rem 0.75rem;
}

.search-card-overlay .scan-btn {
    min-height: 46px;
}

/* Imperial/Metric toggle switch -- a checkbox styled to look and behave
   like a sliding switch, wired up by app/static/js/units_toggle.js. */
.unit-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0.75rem 0 1.25rem;
    font-size: 0.9rem;
    color: var(--color-text-muted);
}

.switch {
    position: relative;
    display: inline-block;
    width: 42px;
    height: 24px;
    flex-shrink: 0;
}

.switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.switch .slider {
    position: absolute;
    cursor: pointer;
    inset: 0;
    background: var(--color-border);
    border-radius: 24px;
    transition: background 0.15s;
}

.switch .slider::before {
    content: "";
    position: absolute;
    height: 18px;
    width: 18px;
    left: 3px;
    bottom: 3px;
    background: #fff; /* the thumb itself -- stays plain white regardless of theme, same as most native toggle switches (iOS included) */
    border-radius: 50%;
    transition: transform 0.15s;
}

.switch input:checked + .slider {
    background: var(--color-accent-bg);
}

.switch input:checked + .slider::before {
    transform: translateX(18px);
}

/* Smaller variant of the same switch -- used on the Profile > Data page,
   where the toggle is the whole point of a small section rather than one
   control sitting above a long form (see onboarding/data.html). Same
   3px-inset/18px-travel proportions as the full-size switch above, just
   scaled down so the thumb still sits centered in the track at both ends. */
.switch.small {
    width: 32px;
    height: 18px;
}

.switch.small .slider::before {
    height: 12px;
    width: 12px;
    left: 3px;
    bottom: 3px;
}

.switch.small input:checked + .slider::before {
    transform: translateX(14px);
}

/* Standard visually-hidden pattern -- content stays in the DOM (and
   reachable/clickable via its <label>, for the profile picture file
   input) but isn't visible or in the document's visual flow. Not
   `display: none`, which would also hide it from screen readers and
   remove it from tab order. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Profile picture, at the top of the Settings hub (see profile_hub.html)
   and the small header version next to the Dashboard's settings button
   (see .navbar-avatar below). */
.settings-avatar-section {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 1.5rem;
}

.settings-avatar-section form {
    display: inline-block;
    margin: 0;
}

.settings-avatar-section .btn {
    margin-top: 0;
}

.settings-avatar,
.avatar-img {
    width: 88px;
    height: 88px;
    border-radius: 50%;
}

.settings-avatar {
    background: var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    color: var(--color-text-muted);
}

.avatar-img {
    object-fit: cover;
}

.avatar-placeholder-svg {
    width: 60%;
    height: 60%;
}

/* Small avatar next to the Dashboard's settings gear (see base.html) --
   same idea as .settings-avatar above, just sized for the header bar. */
.navbar-avatar {
    /* Sized up with the brand/gear alongside it -- see .navbar's own
       flatten-the-header comment. */
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: var(--color-avatar-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    /* Not --color-cream: the glyph has to follow the avatar chip, which
       is light in light mode and dark in dark mode. See the token. */
    color: var(--color-avatar-fg);
}

.navbar-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.navbar-avatar svg {
    width: 60%;
    height: 60%;
}

/* Profile hub's settings-style menu (see onboarding/profile_hub.html) --
   each row is a full-width tappable link to its own sub-page. */
.settings-list {
    padding: 0;
}

/* Section headings for the Settings menu (see onboarding/profile_hub.html)
   -- added 2026-09-01 with the declutter pass. Small, uppercase and muted
   on purpose: these are labels ORGANIZING the rows, not headings
   competing with them, which is exactly how iOS's own Settings groups
   read. Sits above each .settings-list card, hence the asymmetric margin
   (generous above to separate one group from the last, tight below so
   the label clearly belongs to the card under it). */
.settings-group-label {
    margin: 1.75rem 0 0.5rem;
    padding-left: 0.25rem;
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}

/* 2026-09-21 visual review, §1.1. The medical-advice line that came along
   with the legal links out of the hidden .app-footer. Sized and coloured to
   sit UNDER the About group as a footnote to it, the way iOS uses a caption
   below a grouped list -- not as another row in the list, because it is not
   tappable and a non-tappable row in a list of chevrons reads as broken. */
.settings-about-note {
    margin: 0.6rem 0.25rem 0;
    font-size: 0.78rem;
    line-height: 1.5;
}

/* The first group sits right under the profile photo block, which
   already has its own spacing below it -- no need to add a second gap. */
.settings-group-label:first-of-type {
    margin-top: 0.5rem;
}

.settings-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 1.25rem;
    color: var(--color-text);
    text-decoration: none;
    border-bottom: 1px solid var(--color-border);
    min-height: 44px;
    box-sizing: border-box;
}

.settings-row:last-child {
    border-bottom: none;
}

.settings-row:hover {
    background: var(--color-page-bg);
}

.settings-row-arrow {
    color: var(--color-text-muted);
    font-size: 1.3rem;
    line-height: 1;
}

/* Trends page charts -- a wrapper div with a fixed height, not styling the
   canvas directly: Chart.js sets its own inline width/height style on the
   canvas element every time it resizes, which would silently override a
   plain CSS rule on the canvas itself (inline styles always win). Chart.js
   reads its PARENT element's box size instead when `maintainAspectRatio:
   false` is set (see trends.js) -- giving that parent a fixed height here
   is what actually keeps the chart from growing taller as the container
   gets wider, which is what was making every chart render oversized on a
   computer-width screen. */
.chart-canvas-wrap {
    position: relative;
    height: 200px;
}

/* Dashboard's "Today vs. Average" chart -- a small widget inside a card
   shared with other content, not a deep-dive chart -- shorter than the
   Trends page's default. Went through two rounds already: first the bars
   themselves read as too big (fixed with maxBarThickness in dashboard.js,
   which caps bar WIDTH regardless of this box's size), then this box got
   shrunk to 130px to compensate -- which overshot and read as too small/
   cramped on its own. Settled on 180px: still visibly smaller than the
   Trends page's 200px deep-dive charts, but with real room to breathe.
   maxBarThickness means growing this box taller only gives the chart more
   space -- it does NOT make the bars themselves any wider. */
.chart-canvas-wrap-small {
    height: 180px;
}

/* Trends page card polish -- direct request, deliberately scoped to
   .trends-card rather than the shared .card every other page also uses
   (Add Food, Dashboard, ...), which this request was never about. Same
   border/background, just a bit more rounded and a lot more separated
   from the page than the flat 1px border alone -- .card has no shadow at
   all today. */
/* .trends-card itself now carries no rules of its own -- its whole
   original treatment (bigger radius, real shadow, more padding) became
   the app-wide .card default above instead of a one-page exception.
   Left as a marker class on Trends' own cards rather than ripping it
   out of the template -- costs nothing, and keeps this comment's own
   explanation attached to where a future reader would look for it. */

.chart-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.chart-card-header h2 {
    margin: 0;
}

/* T32: the edit screen's second Save lives in the existing Details
   heading row, so it costs no extra vertical block on a phone. It keeps
   a full 44px touch target while using compact horizontal padding. */
.details-header-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.details-top-save {
    margin: 0;
    min-height: 44px;
    padding: 0.45rem 0.85rem;
    font-size: 0.9rem;
}

/* Direct feedback: originally sat absolutely-positioned INSIDE
   .chart-canvas-wrap, bottom-right -- on a real chart with a wider date
   range, that corner is exactly where Chart.js draws its last x-axis
   label/legend value, so the button covered real numbers. Moved to its
   own quiet row right below the canvas instead -- still reads as "the
   bottom-right control for this chart" without ever overlapping chart
   content, and doesn't touch the canvas's own size (chart.js's
   `.resize()` in trends.js is keyed off the canvas, not this row). */
.chart-expand-row {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.35rem;
}
/* T214 (Nick, 2026-09-14): "make the buttons clear that they can be pressed."
   Tapping a point opens that day's foods, which nothing on screen said. The
   hint takes the free space in the row the expand button already sits in, so
   it adds no height, and it is muted because it is an affordance rather than
   content. */
.chart-tap-hint {
    margin: 0;
    margin-right: auto;
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

.chart-expand-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    min-height: auto;
    margin: 0;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: 6px;
    background: var(--color-surface);
    color: var(--color-text-muted);
    opacity: 0.85;
    cursor: pointer;
}

.chart-expand-btn:hover {
    opacity: 1;
    color: var(--color-text);
}

.chart-expand-btn .btn-icon-svg {
    width: 15px;
    height: 15px;
}

/* The expanded state itself -- a plain fixed-position takeover, not the
   browser's real Fullscreen API (unreliable inside a Capacitor WKWebView,
   see trends.js's own comment on this). Only the card matches the
   viewport; the canvas inside keeps its own CSS sizing and gets a real
   Chart.js .resize() call (trends.js) once this takes effect, so it
   actually redraws at the new size instead of just stretching a stale
   bitmap. */
/* T212 (Nick, 2026-09-13): expanded is BIGGER, not a takeover. It used to be
   `inset: 0` -- edge to edge, no radius -- which Nick found looked wrong at
   that size: "it does not actually have to be full screen, we should change it
   to 'expand' that makes it bigger, but not completely full screen."

   So it stays a card. It is still fixed and centred, so it is unmistakably the
   thing you are looking at and Esc still closes it, but it keeps its corners,
   keeps a margin, and lets the page show around the edges -- which is what
   tells you it is a temporary state rather than a new screen. The dim layer
   behind it is drawn by ::before rather than a real element, so no markup
   changed. */
.chart-card-expanded {
    position: fixed;
    inset: 5vh 4vw;
    /* width:auto is load-bearing, not tidiness: .card sets width:100%, which
       on a fixed box beats the right inset -- the card stayed exactly viewport
       width and simply shifted right, still full-bleed. Measured in a browser
       (x=12, w=393 on a 393px viewport) rather than eyeballed. */
    width: auto;
    max-width: 900px;
    margin: 0 auto;
    z-index: 1000;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    /* The first shadow IS the dimmed page: a spread of 100vmax paints
       everything outside the card and nothing inside it. The obvious version,
       a ::before overlay at z-index:-1, tinted the CARD as well -- inside the
       card's own stacking context a negative-z-index child paints above the
       parent's background, so the card came out grey. Seen in a screenshot,
       not reasoned about. No extra markup either way. */
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.45), 0 18px 50px rgba(0, 0, 0, 0.35);
}
@media (max-width: 480px) {
    /* T219 (Nick, 2026-09-14): "it still is cut off by the tab switching bar
       and the time and battery of my phone... now there is no way to close
       that box."

       Both halves are ONE bug. `3vh` is about 25px on an 852px screen, while
       the iPhone's status bar is ~54px and the app's own bottom tab bar is 66px
       plus the home-indicator inset. So the card ran underneath both -- and the
       collapse button lives in the card's BOTTOM row, which is exactly the part
       the tab bar was covering. Nothing was stuck; the way out was hidden.

       So the inset is measured from the real furniture rather than from a
       percentage that cannot know about it: the status bar via
       env(safe-area-inset-top), and the tab bar via the SAME 66px the details
       bar already uses (--details-bar-bottom, further down this file) so there
       are not two numbers for one bar to drift apart. */
    .chart-card-expanded {
        inset: calc(env(safe-area-inset-top) + 0.75rem)
               3vw
               calc(66px + env(safe-area-inset-bottom) + 0.75rem);
    }
}

.chart-card-expanded .chart-canvas-wrap {
    flex: 1;
    height: auto;
    min-height: 0;
}

/* Trends page: range buttons above the charts */
.range-buttons {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
    flex-wrap: wrap;
}

.range-btn {
    padding: 0.4rem 0.9rem;
    min-height: 36px; /* smaller than the standard 44px buttons -- these sit
                          in a tight row and aren't the primary action on
                          the page */
    border: 1px solid var(--color-border);
    border-radius: 20px;
    background: var(--color-surface);
    color: var(--color-text);
    font-size: 0.85rem;
    cursor: pointer;
}

.range-btn.active {
    background: var(--color-accent-bg);
    border-color: var(--color-accent-bg);
    color: var(--color-accent-text);
}

/* Day-detail modal (Trends page): a simple overlay + centered box, no
   external library. */
/* Bottom-sheet modals, direct request ("feels like an app, not a
   website"): every popup in this app (the calendar picker, streak info,
   chart info, day-detail, onboarding's own modals) shares these same two
   classes -- converting the shared CSS once here turns all of them into
   a native-style sheet sliding up from the bottom edge, instead of a
   centered fade-in box that reads as a desktop dialog. No template
   changes needed anywhere; every modal picks this up automatically. */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: flex-end;
    justify-content: center;
    z-index: 1000;
    padding: 0;
}

.modal-box {
    position: relative;
    background: var(--color-surface);
    /* Rounded top corners only -- flush with the screen's bottom edge,
       the real thing that reads as "a sheet", not just "rounder". */
    border-radius: 16px 16px 0 0;
    padding: 1.5rem;
    padding-bottom: calc(1.5rem + env(safe-area-inset-bottom));
    max-width: 480px;
    width: 100%;
    max-height: 85vh;
    overflow-y: auto;
}

/* A purely visual "drag handle" -- this app doesn't have a real swipe-
   to-dismiss gesture (tapping the dark backdrop already does the job),
   but the handle alone is still what reads as "a real bottom sheet" at
   a glance, the same way it does in a native app that DOES support the
   gesture. A ::before pseudo-element needs no markup change in any of
   this app's several modal templates. */
/* T283 (2026-09-21): a screen's ⓘ sheet, which is a MULTI-SECTION document
   rather than the one-paragraph modal .modal-box was built for. The base box
   already handles the sheet shape, safe area and scrolling -- this only gives
   the sections room to breathe and keeps the first heading from colliding
   with the close button. */
.info-sheet h3 {
    margin: 1.4rem 0 0.35rem;
    font-size: 1rem;
}

.info-sheet h3:first-of-type {
    margin-top: 1.1rem;
}

.info-sheet p {
    margin: 0 0 0.6rem;
    line-height: 1.55;
}

.info-sheet p:last-child {
    margin-bottom: 0;
}

.modal-box::before {
    content: "";
    display: block;
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--color-border);
    margin: -0.5rem auto 1rem;
}

/* The slide-up entrance: a CSS `animation` (not a transition someone would
   need to coordinate with JS) restarts on its own every time `display`
   flips from "none" to "flex" -- so every existing show/hide toggle
   across this app's several modals gets this for free, no JS changes
   needed anywhere. Dismissal stays an instant hide (this app's modals
   already close on backdrop tap, not a drag gesture, so there's no real
   "in-progress swipe" moment an animated close would need to match). */
@media (prefers-reduced-motion: no-preference) {
    .modal-box {
        animation: modal-sheet-slide-up 0.25s ease-out;
    }
}

@keyframes modal-sheet-slide-up {
    from {
        transform: translateY(100%);
    }
    to {
        transform: translateY(0);
    }
}

.modal-close {
    position: absolute;
    top: 0.5rem;
    right: 0.75rem;
    background: none;
    border: none;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    color: var(--color-text-muted);
    min-height: auto;
    margin: 0;
    padding: 0.25rem;
}

.modal-close:hover {
    color: var(--color-text);
}

/* Dashboard's calendar day-picker (main/dashboard.html + dashboard.js) --
   a plain 7-column grid, one cell per day. Built and redrawn entirely
   client-side; a day with anything logged (see GET /logged-days.json)
   gets a small dot under its number rather than a filled/colored cell --
   deliberately minimal, matching the direct request for "some kind of
   color coded, minimalistic, so maybe a dot" rather than a busier
   heatmap-style calendar. */
.calendar-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 0.25rem;
    margin: 1rem 0;
}

.calendar-weekday {
    text-align: center;
    font-size: 0.75rem;
    color: var(--color-text-muted);
    padding-bottom: 0.25rem;
}

.calendar-day {
    aspect-ratio: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    border: none;
    border-radius: 6px;
    background: none;
    color: var(--color-text);
    font: inherit;
    cursor: pointer;
    min-height: auto;
    margin: 0;
    padding: 0;
}

.calendar-day:hover {
    background: var(--color-border);
}

.calendar-day.calendar-day-today {
    font-weight: bold;
    border: 1px solid var(--color-border);
}

.calendar-day.calendar-day-selected {
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
}

/* T182: a required field left empty on signup/setup. Inputs get the same
   amber treatment as .field-scanned (see the input rules above); selects and
   the date-picker button draw their borders differently, so they get a ring
   instead -- same colour, same meaning. */
select.field-needed,
button.field-needed {
    box-shadow: 0 0 0 2px var(--color-edited-border);
}

/* Days outside a picker's min/max (date_picker.js disables them) -- e.g.
   birth dates under the minimum age (T176). Without this they looked
   identical to pickable days and simply did nothing when tapped. */
.calendar-day:disabled {
    opacity: 0.3;
    cursor: default;
}

.calendar-day-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--color-accent-bg);
}

/* The selected day's own dot needs to stay visible against the now-
   accent-colored background above it, not disappear into it. */
.calendar-day.calendar-day-selected .calendar-day-dot {
    background: var(--color-accent-text);
}

.calendar-key {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.8rem;
    margin: 0 0 1rem;
}

.calendar-key-dot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--color-accent-bg);
    flex-shrink: 0;
}

/* The shared date picker (date_picker.js) -- replaces native
   <input type="date"> app-wide. Reuses .calendar-grid/.calendar-day/etc.
   above as-is (same visual language as the Dashboard's own calendar,
   even though it's a separate JS component -- see date_picker.js's own
   comment for why they're not the same code). */
.date-picker-trigger {
    /* Overrides .btn-with-icon's default row order (icon first) so the
       label reads naturally before the calendar glyph, matching how a
       normal form field's value reads before its own icon/affordance. */
    flex-direction: row;
    /* Real bug found and fixed, direct report ("the date box doesn't
       line up with the other input boxes"): this button is meant to
       act as a drop-in replacement for a plain input (see date_picker.js's
       own module comment) -- a label immediately above it, just like
       any other field. But the global `button, .btn` rule's own
       `margin-top: 1rem` still applied on top of `label`'s own
       `margin-top: 0.75rem`, giving this specific field extra top
       margin no sibling plain <input> in the same row gets. Most
       visible on the Weight page's 3-field row (Weight / Body fat % /
       Date), where the mismatch pushes the date box visibly down
       against its two neighbors -- but the same inconsistency existed
       everywhere else this component appears (Add Food, onboarding's
       birth date), just less noticeable without a tight side-by-side
       row to expose it. */
    margin-top: 0;
}

/* Direct report: nothing here hinted the month/year label itself is
   tappable (to jump straight to picking a year -- see toggleDatePickerJump()
   in date_picker.js) -- easy to miss entirely, and it matters most on
   exactly the field this app most needs fast year-jumping for (a birth
   date, often decades back). A plain CSS-drawn chevron (the standard
   two-border-sides-rotated-45deg trick) rather than a 4th inline-SVG
   icon just for this -- matches this app's "no emoji, real icons"
   convention without needing a new asset for one small affordance. */
.date-picker-jump-trigger {
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    font-weight: bold;
    cursor: pointer;
    min-height: auto;
    margin: 0;
    padding: 0.25rem 0.5rem;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.date-picker-jump-trigger::after {
    content: "";
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    flex-shrink: 0;
}

.date-picker-jump {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}

.date-picker-jump select {
    flex: 1;
    margin: 0;
}

/* Profile picture crop/zoom step (profile_hub.html + profile_picture.js).
   .cropper-frame is the visible square the image gets dragged/zoomed
   inside; .cropper-circle-overlay sits on top showing exactly what's
   inside the circle vs. what gets cropped away. */
.cropper-frame {
    position: relative;
    width: 100%;
    max-width: 300px;
    aspect-ratio: 1 / 1;
    margin: 0 auto 1rem;
    overflow: hidden;
    border-radius: 8px;
    background: var(--color-graphite);
    touch-action: none; /* stops the browser's own scroll/pinch from fighting our pointer-based drag */
    cursor: grab;
}

.cropper-frame.dragging {
    cursor: grabbing;
}

#cropper-image {
    position: absolute;
    /* top/left/width/height set directly by JS on every drag/zoom --
       simpler and less error-prone than composing multiple CSS
       transform() functions whose combined effect depends on order. */
    max-width: none; /* explicit, not overriding anything specific in this app's own CSS -- just defensive, since width is set directly in pixels by JS below and shouldn't ever get capped by anything */
    user-select: none;
    -webkit-user-drag: none;
}

.cropper-circle-overlay {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    /* A huge spread box-shadow darkens everything OUTSIDE this circle,
       leaving the circle itself clear -- a standard CSS-only way to draw
       a circular crop mask with no canvas compositing needed just to
       SHOW it (only the final export, in JS, needs a canvas). */
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
    border: 2px solid rgba(255, 255, 255, 0.85);
    pointer-events: none; /* drags/clicks pass through to the image underneath */
    box-sizing: border-box;
}

#cropper-zoom-slider {
    width: 100%;
}

/* Circular progress rings (Dashboard: daily calorie total + macros). See
   the big comment in main/dashboard.html for how the fill arc works.
   NOTE: the 3 macro ring colors (protein/fat/carbs) are set inline from
   app/main/routes.py and app/food/routes.py, not here -- they're kept as
   3 distinct "data" colors separate from the neutral UI palette above, so
   each macro stays visually distinguishable regardless of the overall
   color scheme, but muted/earthy rather than vivid (see the comment on
   `macros` in main/routes.py -- the originals read as too bright against
   the rest of the app's muted palette). They're not redefined for dark
   mode -- verified they still clear the 3:1 non-text contrast minimum
   against the dark page background. */
.ring {
    display: block;
    margin: 0 auto;
}

.ring-large {
    /* Sized down a bit from an earlier version now that this ring shares a
       card with the macro rings, rather than having a whole card to
       itself -- still clearly the biggest ring on the page. */
    width: 130px;
    height: 130px;
}

.ring-small {
    width: 64px;
    height: 64px;
}

.ring-bg {
    fill: none;
    stroke: var(--color-border);
    stroke-width: 10;
}

.ring-large .ring-bg,
.ring-large .ring-fill {
    stroke-width: 10;
}

.ring-small .ring-bg,
.ring-small .ring-fill {
    stroke-width: 6;
}

.ring-fill {
    fill: none;
    stroke: var(--color-text);
    stroke-linecap: round;
    transform: rotate(-90deg);
    transform-origin: 50% 50%;
    transition: stroke-dashoffset 0.4s ease;
}

/* The 3 macro rings. These used to get an inline `stroke: <hex>` written
   by main/routes.py, which meant a server that has no idea whether the
   browser is in light or dark mode was choosing the color. Now the route
   passes a class and the color comes from the tokens, so dark mode works
   and the Dashboard rings match Trends' charts by construction. */
.ring-fill.macro-protein { stroke: var(--color-macro-protein); }
.ring-fill.macro-carbs { stroke: var(--color-macro-carbs); }
.ring-fill.macro-fat { stroke: var(--color-macro-fat); }

.ring-fill.over {
    stroke: var(--color-danger);
}

.ring-value {
    font-size: 22px;
    font-weight: 700;
    text-anchor: middle;
    fill: var(--color-text);
}

.ring-unit {
    font-size: 12px;
    text-anchor: middle;
    fill: var(--color-text-muted);
}

.ring-caption {
    text-align: center;
    margin-top: 0.5rem;
}

/* Wraps each ring SVG so tapping it jumps to Trends (see dashboard.html)
   -- inline-block so it hugs the ring's own size instead of stretching
   full-width like a normal link, and :active (not :hover, which doesn't
   fire reliably on touch) gives a quick tap-feedback dim so it's obvious
   something happened. */
.ring-link {
    display: inline-block;
    text-decoration: none;
}

.ring-link:active {
    opacity: 0.6;
}

/* Holds the two big rings (total calories + remaining) side by side --
   see the big comment in main/dashboard.html. Naturally shifts each ring
   off dead-center now that there are two of them, rather than the old
   single centered ring. */
.ring-row-large {
    display: flex;
    justify-content: center;
    gap: 1.5rem;
    flex-wrap: wrap;
}

.ring-large-item {
    text-align: center;
}

.ring-row {
    display: flex;
    justify-content: space-around;
    text-align: center;
    margin: 0.5rem 0 1rem;
}

.ring-row-item {
    flex: 1;
}

.ring-row-label {
    margin: 0.4rem 0 0.1rem;
    font-weight: 600;
    font-size: 0.85rem;
}

.ring-row-value {
    margin: 0;
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

/* A small round "(i)" explainer button -- direct request, first used by
   the Dashboard's streak widget below, then reused for each Trends chart
   (see main/trends.html). Opens a .modal-overlay/.modal-box, same
   convention as every other popup in this app -- not a hover tooltip,
   which has no reliable equivalent on a touchscreen. */
.info-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    min-height: auto;
    margin: 0;
    padding: 0;
    border: 1.5px solid var(--color-text-muted);
    border-radius: 50%;
    background: none;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-style: italic;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    flex-shrink: 0;
}

.info-btn:hover {
    color: var(--color-text);
    border-color: var(--color-text);
}

/* Logging streak as one slim line above the Diary (T193, Nick 2026-09-11:
   "make it a straight line and make it small"). Replaced the 7-bubble arc.
   Completed days keep the streak's gold; today, still open, is an outlined
   accent dot; the rest of the week is the plain track colour. */
.streak-strip {
    display: flex;
    align-items: center;
    /* T215: the row carries five fixed-size things -- label, seven dots, the
       count, the expand triangle and the info button -- and at 393px (iPhone
       14/15) they came to 384px inside a 359px card, so the info button spilled
       past the card's edge. The pixels are reclaimed from the spacing rather
       than from any of the five, so nothing Nick asked for is removed or
       shortened: a slightly tighter gap and side padding here, a slightly
       tighter gap between the dots below.

       ponytail: this buys exactly enough room for "Logging streak" + a
       two-digit count at 393px. At a three-digit streak ("365 days") the
       label truncates to "Logging str..." instead -- deliberate, because the
       alternative is a control clipped outside the card, and a label is the
       one thing here that degrades harmlessly. If that ever looks wrong, the
       real fix is one fewer control in the row: move the info button's
       explanation into the calendar it already expands into. */
    gap: 0.4rem;
    padding: 0.6rem 0.75rem;
}
/* T215 (found 2026-09-14 in a browser at 393px, iPhone 14/15 width): the row
   was 384px of content in a 359px box, so the info button was clipped off the
   right-hand edge.

   The cause is that T210's fix made .streak-dots flex:none -- correctly, so
   the count can never cover the 7th dot -- and by then EVERY item in the row
   was unshrinkable: the label was nowrap, the dots flex:none, and the count,
   the expand triangle and the info button are all fixed-size. Something has
   to be allowed to give, and the label is the only one where losing a few
   pixels costs nothing: it truncates to "Logging strea…" on the narrowest
   phones and reads in full everywhere else. The dots keep their guarantee. */
.streak-strip-label {
    font-weight: 650;
    font-size: 0.95rem;
    white-space: nowrap;
    min-width: 0;              /* without this a flex item will not shrink */
    overflow: hidden;
    text-overflow: ellipsis;
}
/* T210 fix (Nick, 2026-09-14): "the '1 day' covers the 7th day's dot".
   flex:1 + min-width:0 let this box shrink BELOW its seven dots once the new
   expand button joined the row; the dots are flex:none, so they overflowed
   underneath the count instead of the box staying their width. margin-right
   auto does the spacing job flex:1 was really there for, without ever letting
   the dots be squeezed. */
.streak-dots { display: flex; gap: 0.25rem; flex: none; margin-right: auto; }  /* T215: was 0.32rem; T227: back to 0.25, paid for by the expand button losing its stray padding */
.streak-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--color-border); flex: none; }  /* T215: was 11px */
.streak-dot-complete { background: var(--color-streak-gold); }
.streak-dot-active { background: transparent; box-shadow: inset 0 0 0 2px var(--color-accent-bg); }
.streak-strip-count {
    font-size: 0.9rem;
    color: var(--color-text-muted);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* T210 (Nick, 2026-09-13): the chevron that opens the month calendar, and the
   calendar itself. The strip above is unchanged apart from its label.

   The button is 44x44 even though the chevron drawn inside it is tiny -- the
   same rule T212 applies to the Trends chart points. A control a thumb has to
   hunt for is the complaint that produced both items, and a hit area is free
   whereas a bigger glyph costs layout. */
/* The WHOLE strip is the tap target (see initStreakCalendar), so this button
   does not need to be 44px wide -- and could not be: with the label, seven
   dots, the count and the (i) button all on one line, a 44px box pushed the
   row past the edge of a 393px phone (measured: right edge at 397). Shrinking
   it and letting the card take the tap is both smaller and a far bigger
   target than 44px ever was. The button stays a real <button> so keyboard and
   screen-reader users still have a named control. */
/* T226 (Nick, 2026-09-14): the info button "is too close to the arrow button.
   It should be in the middle of the button and the side."

   The row has no spare width at 393px -- T215 had to reclaim pixels from the
   spacing just to stop it overflowing -- so this is not added, it is MOVED: the
   gap between the dots drops 0.25rem -> 0.2rem and that width is spent here, on
   sitting the info button midway between the expand arrow and the card's edge
   instead of hard against the arrow. Net width unchanged, so the label still
   reads in full. */
.streak-strip .info-btn { margin-left: 0.5rem; }  /* T227: recentred once the arrow lost its box */

.streak-expand-btn {
    width: 24px;
    height: 44px;
    margin: -0.6rem -0.25rem -0.6rem 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: 0;
    cursor: pointer;
    flex: none;
    /* T227 (Nick, 2026-09-14): "remove the box around the arrow to the
       calendar, and then recenter it again from there."

       The box was a DROP SHADOW, not a border or a background -- inherited
       from the global `button, .btn` rule, which `background: none` and
       `border: 0` do not touch. That is why it looked like a box while every
       colour here said transparent.

       The global padding was not reset either, so this button measured 38.4px
       against the 24px its own rule asks for. Resetting it gives ~14px back to
       a row that had none, which pays for the dot gap returning to 0.25rem and
       for the info button's breathing room. */
    box-shadow: none;
    padding: 0;
    border-radius: 0;
    color: var(--color-text-muted);
}
.streak-strip { cursor: pointer; }
/* T210 fix (Nick, 2026-09-14): "make the dropdown arrow an actual arrow
   instead of a check mark so a user doesn't think that the check means they
   have logged that day."

   It was two borders on a rotated square -- a chevron, which next to seven
   logged/not-logged dots reads as a tick. A solid triangle cannot: it is a
   filled shape with no stroke, the shape every dropdown on a phone uses. */
.streak-chevron {
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 6px solid currentColor;
    transition: transform 0.18s ease;
}
.streak-expand-btn[aria-expanded="true"] .streak-chevron {
    transform: rotate(180deg);
}

.streak-calendar { padding: 0.75rem 1rem 0.9rem; margin-top: -0.35rem; }
.streak-cal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}
.streak-cal-title {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 650;
    text-align: center;
    flex: 1;
}
.streak-cal-nav {
    width: 44px;
    height: 44px;
    flex: none;
    background: none;
    border: 0;
    cursor: pointer;
    font-size: 1.4rem;
    line-height: 1;
    color: var(--color-accent-bg);
}
.streak-cal-nav[disabled] { color: var(--color-border); cursor: default; }
.streak-cal-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 0.25rem;
    text-align: center;
}
.streak-cal-dow {
    font-size: 0.7rem;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding-bottom: 0.2rem;
}
.streak-cal-day {
    aspect-ratio: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.18rem;
    font-size: 0.78rem;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}
.streak-cal-day.is-today { font-weight: 700; color: var(--color-text); }
.streak-cal-day.is-blank { visibility: hidden; }
.streak-cal-key {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
    margin: 0.6rem 0 0;
    font-size: 0.75rem;
    color: var(--color-text-muted);
}
.streak-cal-key .streak-dot { margin-left: 0.5rem; }
.streak-cal-key .streak-dot:first-child { margin-left: 0; }

/* T211 (Nick, 2026-09-13): the rest of today's micronutrients, revealed in
   place by "See more". Same rows as the three above them, so they inherit
   .entry-micros wholesale and need only the separating rule. */
.home-micros-rest { border-top: 1px solid var(--color-border); margin-top: 0.6rem; padding-top: 0.6rem; }
.home-micros-trends { display: block; margin-top: 0.6rem; }

/* Dashboard "hero" redesign (main/dashboard.html) -- direct request,
   "one hero element first" with the time-of-day greeting folded in,
   picked as "Variant 2, big number" after comparing 2 real mockups (see
   TODO.md). The old combined rings card (total + remaining + macros,
   all as rings) is gone -- total/remaining now live only as the number
   + progress bar below, and the macros moved into their own smaller
   card (still uses .ring-small/.ring-row* below, just without the two
   big .ring-large circles that used to sit above them). */
/* Large title that simply scrolls away, direct request (2026-09-01)
   after seeing the previous version on a real device: "the text shrinks
   but it looks really bad, we should make it disappear from view
   instead."

   This USED to be a sticky element that docked under the navbar and
   shrank in place (iOS's "big title collapses into the top bar"
   pattern), driven by initCollapsingHeader() in dashboard.js watching a
   #hero-title-sentinel. All of that is gone now -- the sticky
   positioning, the compact class, the JS observer and the sentinel
   element -- because plain scroll-away is BOTH the simpler thing and
   the more app-like one: iOS's own large titles don't shrink in place
   either, they slide up and clip under the navigation bar. That works
   here for free now that .navbar is opaque and takes the page's own
   background (see its own rule): this heading scrolls up behind it and
   is simply hidden, no JS involved.

   Removing the sticky positioning also removes the faint text-ghosting
   artifact that was logged against the old compact bar's top edge --
   that was a sticky-compositing quirk, and there's no longer a sticky
   element for it to happen to. */
.hero-greeting {
    /* T195 (Nick, 2026-09-11): the greeting sat "weirdly spaced away" from
       the WeighTooMuch bar -- its own 1rem top margin stacked on top of
       .container's padding-top. The container's padding is the gap now. */
    margin: 0 0 0.15rem;
    font-size: 2rem;
}

/* .day-nav's own rule (see above) centers its 3 children -- this page
   needs the opposite now that the date is real text sitting next to a
   much smaller nav cluster, not 3 roughly-equal controls. The combined
   selector (not just .hero-date-row alone) beats .day-nav on
   specificity regardless of which rule happens to sit later in this
   file, rather than relying on source order to win the override. */
.day-nav.hero-date-row {
    justify-content: space-between;
    margin-bottom: 0.75rem;
}

.hero-date-row .dashboard-date {
    margin: 0;
    font-size: 0.85rem;
    font-weight: normal;
    color: var(--color-text-muted);
}

.hero-day-nav {
    display: flex;
    align-items: center;
    gap: 0.15rem;
}

/* Quieter/smaller than the plain .icon-nav-btn these same elements use
   elsewhere (e.g. the Food page's own day-nav, which keeps its original
   full-size treatment) -- this row now sits next to the real page
   heading above it, so it should read as a small utility control, not
   compete with it. */
.hero-day-nav .icon-nav-btn {
    width: 30px;
    min-height: 30px;
    margin: 0;
}

/* Scoped off .secondary so the disabled "no later day" variant (a plain
   .btn.secondary underneath) keeps its own shaded background AND its own
   light-on-dark text color instead of both being reset here -- same
   specificity + later source order was silently winning on background
   alone. Scoping color too, not just background: --color-text-muted and
   the secondary button's fill (--color-muted-fill) are the SAME value in
   light mode, so restoring only the background would have left the icon
   exactly matching it -- still invisible, just for a new reason. See
   TODO.md. */
.hero-day-nav .icon-nav-btn:not(.secondary) {
    background: none;
    color: var(--color-text-muted);
}

/* Direct feedback once this actually rendered: .btn.secondary's normal
   treatment (a solid --color-muted-fill circle, cream icon) reads as a
   heavy, dark, out-of-place blob next to this row's otherwise very quiet
   icons-with-no-background look. Overrides it with the SAME subtle tint
   already used for :hover just below -- "disabled" now reads as a
   permanent, quiet version of that existing hover state instead of a new,
   louder color introduced just for this. Higher specificity (3 classes)
   than plain .btn.secondary (2), so this wins without needing !important. */
.hero-day-nav .icon-nav-btn.secondary {
    /* Direct feedback again: even --color-border read as a little too
       dark/solid for this quiet row. Same color-mix() approach already
       used elsewhere in this file (.native-fab-active's own glow) to
       soften it further -- lets the actual page background show through
       partway, in both themes automatically, without introducing a new
       one-off color just for this. */
    background: color-mix(in srgb, var(--color-border) 55%, transparent);
    color: var(--color-text-muted);
    box-shadow: none;
}

.hero-day-nav .icon-nav-btn:hover {
    background: var(--color-border);
}

.hero-day-nav .btn-icon-svg {
    width: 16px;
    height: 16px;
}

.hero-day-nav #calendar-btn {
    min-height: 30px;
    margin: 0;
    padding: 0 0.5rem;
    background: none;
    color: var(--color-text-muted);
    font-size: 0.8rem;
}

.hero-card {
    text-align: center;
    padding: 1.75rem 1.5rem;
    /* A LITTLE static tint -- direct request, and the agreed answer to
       "more of an existing color, not a fifth color" (see TODO.md's
       ruled-out list). This is the slate accent already in the palette,
       mixed down to 6% against the card's own surface, so it's the same
       hue as everything else rather than a new one.

       Why it's here and not on every card: the calorie number is the one
       thing this page exists to show, and a white card on a white page
       gave it nothing to sit on. Tinting only this card is what makes it
       read as the page's centerpiece instead of the first of six equal
       boxes. Every other card deliberately stays plain -- if they all got
       it, none of them would stand out and the tint would just be the new
       background.

       Static in the literal sense: a background color, painted once at
       load. No scroll listener, nothing to re-initialize on the full page
       reload that every tab switch does here. 6% keeps body text on it
       well clear of contrast limits in both themes. */
    background: color-mix(in srgb, var(--color-accent-bg) 6%, var(--color-surface));
    /* The border gets a touch more of the same, so the card's edge agrees
       with its fill instead of staying neutral grey against a tinted
       face. */
    border-color: color-mix(in srgb, var(--color-accent-bg) 18%, var(--color-border));
}

.hero-number {
    font-size: 2.75rem;
    font-weight: 700;
    /* Keeps digits a consistent width as the number itself changes
       throughout the day (e.g. "750" -> "1,200") -- without this, the
       progress bar and subline below would visibly shift horizontally
       each time this number updates. */
    font-variant-numeric: tabular-nums;
}

.hero-number-unit {
    font-size: 1rem;
    font-weight: 600;
    color: var(--color-text-muted);
    margin-left: 0.35rem;
}

.hero-progress-track {
    height: 10px;
    border-radius: 6px;
    background: var(--color-border);
    margin: 1rem auto 0;
    max-width: 280px;
    overflow: hidden;
}

/* The macro-composition bar, sitting directly under the calories bar
   (direct request 2026-09-06). Deliberately shares the track's exact
   width, radius and centring so the two read as one paired object rather
   than two unrelated widgets -- if the geometry above ever changes, change
   it here too.

   Slightly shorter (8px vs 10px) so the calories bar stays the primary
   one: this is the supporting detail, not a competing headline. */
.hero-macro-track {
    display: flex;
    height: 8px;
    border-radius: 6px;
    background: var(--color-border);
    margin: 0.4rem auto 0;
    max-width: 280px;
    overflow: hidden;
}

/* No gaps between segments: they are parts of one span and a gap would
   imply unaccounted calories.

   No min-width either, deliberately. An earlier version gave each segment
   a 2px floor so a tiny macro still showed -- but the segments now have
   to END LEVEL with the calories bar above, and a floor makes the row
   overshoot by however many near-zero macros are present. Exact alignment
   is the whole point of the pair, so a macro at zero renders as nothing,
   which is also the honest thing to show. */
.hero-macro-segment {
    height: 100%;
}

.hero-macro-segment.macro-protein { background: var(--color-macro-protein); }
.hero-macro-segment.macro-carbs   { background: var(--color-macro-carbs); }
.hero-macro-segment.macro-fat     { background: var(--color-macro-fat); }

/* The key. Needed because colour alone is not an accessible way to say
   which segment is which -- and on a 280px bar the segments are far too
   small to label in place. */
.hero-macro-key {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.15rem 0.75rem;
    margin: 0.45rem 0 0;
    font-size: 0.78rem;
    color: var(--color-text-muted);
}

.hero-macro-key-item {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    white-space: nowrap;
}

.hero-macro-key-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    flex: none;
}

.hero-macro-key-dot.macro-protein { background: var(--color-macro-protein); }
.hero-macro-key-dot.macro-carbs   { background: var(--color-macro-carbs); }
.hero-macro-key-dot.macro-fat     { background: var(--color-macro-fat); }

.hero-progress-fill {
    height: 100%;
    border-radius: 6px;
    /* --color-hero, not --color-text: direct request in the 2026-09-03
       palette redesign -- the Dashboard's main calorie tracker uses the
       LIGHTER blue, and it's the only light-mode element that does. See
       that token's own comment for the contrast check. */
    background: var(--color-hero);
}

/* Same "over goal" signal the old calorie ring used
   (.ring-fill.over) -- consistent meaning wherever it shows up. */
.hero-progress-fill.over {
    background: var(--color-danger);
}

.hero-subline {
    color: var(--color-text-muted);
    margin: 0.75rem 0 0;
    font-size: 0.95rem;
}

.hero-subline a {
    color: inherit;
    text-decoration: none;
}

.hero-subline a:hover {
    text-decoration: underline;
}

/* Small section label above a demoted/secondary card -- currently just
   "Today's Numbers" (the macro rings), which lost its old rings-card
   heading once the total/remaining rings it used to share space with
   moved into the hero card above instead. */
.secondary-label {
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    margin: 0 0 0.5rem;
}

/* Sub-tabs: a small in-page tab switcher between two closely-related pages
   (e.g. Dashboard <-> Trends, Log <-> History) that share one nav bar link
   -- see base.html's nav and each affected template's top. */
.sub-tabs {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
    border-bottom: 1px solid var(--color-border);
}

.sub-tab {
    padding: 0.5rem 1rem;
    text-decoration: none;
    color: var(--color-text-muted);
    font-weight: 600;
    font-size: 0.9rem;
    border-bottom: 2px solid transparent;
}

.sub-tab.active {
    color: var(--color-text);
    border-bottom-color: var(--color-text);
}

.sub-tab:hover {
    color: var(--color-text);
}

/* Auth pages (login/signup/forgot-password/reset-password) -- previously
   each had its own identical ad-hoc `style="max-width: 400px; margin: 2rem
   auto;"` right on the .card div, just floating near the top of an
   otherwise empty page. One shared, deliberately-designed treatment
   instead: vertically centered in the space the fixed navbar/container
   padding leaves behind, with a real icon presence on the two entry-point
   pages (login/signup) -- see auth/login.html for the fullest version,
   forgot-password/reset-password keep just the centering (a password-
   recovery screen doesn't need the same branding weight as the two pages
   someone actually starts their day on). */
.auth-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    /* 8rem approximates .container's own top+bottom padding (see above) --
       centers the card in the viewport space actually left over after
       that, not a mathematically exact figure (safe-area insets vary by
       device), which is fine for "roughly centered", the actual goal. */
    min-height: calc(100vh - 8rem);
    padding: 1.5rem 0;
}

.auth-card {
    width: 100%;
    max-width: 380px;
    text-align: center;
}

/* Wraps the logo/heading/subtitle on the two entry-point auth pages
   (login/signup -- see their templates) in a real background treatment
   instead of them sitting directly on the plain page background. Direct
   feedback/request: get rid of the icon's own tile background (see
   .auth-logo below and scripts/generate_icon.py's draw_mark()) and add
   *some* background behind this block instead of leaving it flat.
   Solid --color-onyx, not the gradient tried first -- direct feedback
   ("I dont think it should be dark [gradient]. Maybe an off white
   version of the logo against a full black background") after actually
   seeing the gradient version live. Fixed, not theme-relative
   (--color-onyx, not --color-accent-bg, which flips to near-white in
   dark mode) -- same reasoning as .navbar's own comment: this block
   stays this same solid black in both themes on purpose, so it doesn't
   need its own light/dark logic, and border-radius: 8px matches .card's
   own value for visual consistency with the rest of the app. */
.auth-hero {
    /* ⚠️ WAS var(--color-onyx), near-black. Nick, 2026-09-22, looking at it on
       a real phone: "change the top of the login page to our apps blue color
       to match the log in button".
       ⚠️ THIS REVERSES HIS OWN EARLIER REQUEST -- see the "THE BLACK HERO
       STAYS" note below, which quoted him asking for "an off white version of
       the logo against a full black background". That note is now wrong and
       says so; do not restore black on the strength of it.
       --color-slate, NOT --color-accent-bg: the Log In button uses
       --color-accent-bg, which becomes the LIGHTER slate in dark mode -- and
       the logo here is a fixed cream PNG plus cream text, both of which need
       the DEEP slate behind them to stay readable. Matching the button's
       variable would have matched the colour and broken the contrast. */
    background: var(--color-slate);
    border-radius: 8px;
    /* Less top padding than bottom -- direct request to nudge the logo up
       slightly within this block rather than centering it dead-center. */
    padding: 1.25rem 1.5rem 1.75rem;
    margin-bottom: 1.5rem;
}


/* 2026-09-21 visual review, §4.1. GET THE FORM ABOVE THE FOLD.

   ⚠️ THIS IS THE FIRST SCREEN AN APP REVIEW REVIEWER SEES, and in the
   Simulator it was the weakest in the app. Measured on iPhone 17 Pro: the
   username field started at the fold, so raising the keyboard left the
   password field AND the Log In button both off-screen. You could not
   complete the app's only entry point without scrolling a page that gave no
   sign there was anything below.

   Three things were eating the space, and none of them was the form:
     1. .auth-page vertically CENTRED the card inside 100vh, pushing it down
        past a navbar that was already there;
     2. the navbar showed the wordmark -- on login, linking to login -- while
        the hero showed the same mark again, so the logo appeared twice;
     3. the hero itself was ~190pt to carry a title and one line of subtitle
        that is IDENTICAL on both pages.

   ⚠️ ~~THE BLACK HERO STAYS.~~ ❌ OVERTURNED 2026-09-22. Nick asked for the
   black background originally ("Maybe an off white version of the logo
   against a full black background"), then asked for the app's blue instead
   after seeing it on a real phone next to the Log In button. The hero is now
   var(--color-slate) -- see .auth-hero above. Kept here rather than deleted
   so the original request is not rediscovered and "restored".
   Only its size changed in the 2026-09-21 review. */
body:has(.auth-hero) .navbar {
    display: none;
}

/* ⚠️ HIDING THE NAVBAR TOOK THE TOP SAFE AREA WITH IT, and this exact bug
   has been reported on this codebase once already -- see .container.no-navbar
   further up: "every page using this class had its own <h1> render right
   under the status bar/notch instead of below it". The base .container
   padding-top is a plain 1.5rem precisely BECAUSE the navbar sits in normal
   flow and absorbs env(safe-area-inset-top) itself. Hide the navbar and
   nothing absorbs it, so the black hero slid up under the Dynamic Island.
   Same inset the no-navbar rule adds, for the same reason. */
body:has(.auth-hero) .container {
    padding-top: calc(1.5rem + env(safe-area-inset-top));
}

@media (max-width: 480px) {
    .auth-page {
        /* Top-aligned, not centred. Centring is right on a desktop window
           with room to spare and wrong on a phone, where the space it
           "balances" is the space the keyboard is about to take. */
        justify-content: flex-start;
        min-height: 0;
        padding-top: 0.5rem;
    }

    .auth-hero {
        padding: 1rem 1.25rem 1.25rem;
        margin-bottom: 1rem;
    }

    .auth-logo {
        width: 44px;
        height: 44px;
        margin-bottom: 0.6rem;
    }

    /* ⚠️ THE LAST ~60px, AND THE REASON THEY MATTER. iOS puts a form
       accessory bar (^ v checkmark) above the keyboard, and it is about 65pt
       tall. With it there, the Log In button sat exactly underneath it --
       visible in the Simulator but unreachable without scrolling a page that
       gives no sign it scrolls.

       The honest fix is to suppress that bar, which is NATIVE: it needs
       @capacitor/keyboard and setAccessoryBarVisible(false), so a pod, an
       Xcode rebuild and a new install -- it cannot ship in a web deploy.
       Tracked as review §1.5.

       Dropping the subtitle buys the space on the web side today, and it is
       worth dropping on its own merit: it is the SAME sentence on login and
       on signup, so it distinguishes nothing, and it is the least useful
       thing on the only screen a person has to get through. It stays on
       desktop, where there is room and nothing is competing for it. */
    .auth-hero .auth-subtitle {
        display: none;
    }
}

.auth-logo {
    width: 64px;
    height: 64px;
    margin-bottom: 1rem;
    /* No border-radius/tile background here anymore -- logo-mark.png is
       already a transparent glyph (see draw_mark() in
       scripts/generate_icon.py), not a filled colored square, so it
       blends directly into .auth-hero's own gradient instead of sitting
       in a second, separate box on top of it. */
}

.auth-card h1 {
    font-size: 1.4rem;
    margin-bottom: 0.35rem;
}

/* Overrides the plain .auth-card h1 color above -- only applies inside
   .auth-hero (login/signup), not forgot-password/reset-password's bare
   h1 sitting directly on the normal page background, which still needs
   the default dark text. */
.auth-hero h1 {
    color: var(--color-cream); /* not --color-text -- .auth-hero stays this same dark gradient in both themes, same reasoning as .navbar's own text color comment */
}

.auth-subtitle {
    color: var(--color-text-muted);
    font-size: 0.9rem;
    margin: 0 0 1.5rem;
}

/* Same override pattern as .auth-hero h1 above: --color-text-muted (a
   dark grey) would be nearly unreadable against .auth-hero's dark
   gradient, so a translucent --color-cream stands in for "muted" here
   specifically, without touching the plain .auth-subtitle used elsewhere
   on the light page background. margin-bottom: 0 since .auth-hero's own
   padding-bottom already provides the spacing below this block -- the
   plain rule's 1.5rem would double up otherwise. */
.auth-hero .auth-subtitle {
    color: rgba(255, 252, 242, 0.75);
    margin-bottom: 0;
}

/* The heading/subtitle/logo above are centered (see .auth-card), but a
   form itself reads better left-aligned even inside a centered card --
   labels/inputs lining up on the left is the normal, expected shape for
   a form, centering would make every field a different width/position. */
.auth-card form {
    text-align: left;
}

.auth-card button[type="submit"] {
    width: 100%;
}

.auth-remember {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: normal; /* overrides the global bold `label` rule -- this reads as a checkbox option, not a field name */
    margin-top: 1rem;
}

/* Real bug found and fixed (2026-08-30), direct report ("too light to
   see that it is checked"): plain `accent-color` (the previous approach
   here) isn't reliable enough on its own -- confirmed directly with a
   real WebKit screenshot (Playwright's webkit engine, much closer to
   the iOS app's actual WKWebView than Chromium): in dark mode, the
   checked fill rendered as a muted GRAY, not the crisp near-white
   --color-accent-bg's own hex value would suggest -- a WebKit-specific
   internal blending behavior this app has no control over through
   accent-color alone. Real fix: a fully custom-drawn checkbox instead
   (appearance: none, hand-drawn border/fill/checkmark) -- this
   GUARANTEES the checked state uses this app's own exact colors, fully
   sidestepping whatever WebKit's own theming pipeline decides to do
   internally. --color-accent-text is used for the checkmark specifically
   because that token's whole defined purpose is "text/icons ON the
   accent color" -- correct contrast against the fill by construction,
   not a guess. Also sized up from the native default (a genuinely tiny
   12x12px, hard to see OR tap on a phone regardless of color) to a
   real 20x20px target.
   Manual step for the user: final confirmation on the actual iOS
   device is still needed (see TODO.md's on-device checklist) --
   Playwright's webkit engine is much closer to real WKWebView than
   Chromium, but isn't literally iOS. */
.auth-remember input[type="checkbox"] {
    -webkit-appearance: none;
    appearance: none;
    width: 20px;
    height: 20px;
    margin: 0;
    flex-shrink: 0;
    border: 1.5px solid var(--color-border);
    border-radius: 4px;
    background: var(--color-surface);
    position: relative;
    cursor: pointer;
}

.auth-remember input[type="checkbox"]:checked {
    background: var(--color-accent-bg);
    border-color: var(--color-accent-bg);
}

/* A hand-drawn checkmark (a rotated, partial border -- a standard CSS-
   only technique), not a Unicode character or an image -- guarantees
   it's exactly --color-accent-text, no font/emoji-rendering variance
   across platforms. */
.auth-remember input[type="checkbox"]:checked::after {
    content: "";
    position: absolute;
    left: 6px;
    top: 2px;
    width: 5px;
    height: 10px;
    border: solid var(--color-accent-text);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

.auth-footer {
    margin-top: 1.25rem;
    font-size: 0.85rem;
    color: var(--color-text-muted);
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* Add to Home Screen prompt (main/add_to_home_screen.html) -- a numbered
   list of platform-specific install steps, each with a small icon square
   echoing the step's actual gesture (share icon, plus sign, checkmark). */
.home-screen-step {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    margin-bottom: 1rem;
}

.home-screen-step-icon {
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    border-radius: 10px;
    background: var(--color-accent-bg);
    color: var(--color-accent-text);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
}

.home-screen-step-text {
    line-height: 1.4;
}

/* Notifications settings' per-day reminder times (notifications/settings.html)
   -- all 7 days' fields are really in the DOM (the form still submits
   them all together), but only the one matching the day picker's current
   selection is ever visible -- notifications.js toggles this class on
   change. Was a plain always-visible 7-row table in an earlier version of
   this page; hiding down to one day at a time reads much closer to a
   normal 2-field settings form, which is what this actually needs to look
   like day-to-day (a full week's grid is rarely what anyone's there to
   look at or edit in one sitting). */
.reminder-day-panel {
    display: none;
}

.reminder-day-panel.active {
    display: block;
}

/* The hour/minute/AM-PM dropdown time picker (see the time_picker() macro
   in notifications/settings.html) -- replaced a native <input type="time">
   reported as unreliable to actually operate on a real phone. */
.time-picker-toggle-label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 600; /* matches the bold weight the global `label` rule gives every other field label */
    margin-top: 0.75rem;
    margin-bottom: 0.25rem;
}

.time-picker-toggle-label input[type="checkbox"] {
    width: auto; /* overrides the global 100%-width input rule -- a checkbox that stretched to fill the row would be absurd */
}

.time-picker-group {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 0.25rem;
}

.time-picker-group select {
    width: auto; /* overrides the shared 100%-width select rule -- 3 short dropdowns side by side, not 3 full-width stacked ones */
    min-width: 4.5rem;
    flex: 1;
}


/* Admin dashboard figures. Right-aligned and tabular so a column of
   counts can be scanned down rather than read across -- the whole point
   of that page is noticing when a number looks wrong. */
.admin-figure {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    white-space: nowrap;
}

.admin-metric-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.admin-metric {
    border: 1px solid var(--color-border);
    border-radius: 8px;
    padding: 0.8rem;
    background: var(--color-page-bg);
}

.admin-metric span,
.admin-metric strong {
    display: block;
}

.admin-metric strong {
    margin-top: 0.2rem;
    font-size: 1.35rem;
    line-height: 1.15;
    font-variant-numeric: tabular-nums;
}

/* T39 check 16, 2026-09-07: the four Backlog figures became buttons that
   open the list of items each one counts. These rules only make a <button>
   look like the .admin-metric box it replaced -- a button carries the
   browser's own font, alignment and background, none of which match. */
.admin-metric-tab {
    font: inherit;
    color: inherit;
    text-align: left;
    width: 100%;
    cursor: pointer;
}

.admin-metric-tab:hover {
    border-color: var(--color-text-muted);
}

/* The open group. Deliberately the border rather than a fill: these sit on
   the card background, and a filled state at this size reads as "disabled"
   in the dark theme, which is the opposite of what it means. */
.admin-metric-tab.is-current {
    border-color: var(--color-text);
    box-shadow: inset 0 0 0 1px var(--color-text);
}

.backlog-group {
    margin-bottom: 1rem;
}

.backlog-group h3 {
    margin-bottom: 0.15rem;
}

/* The item's own one-line description, under its title. Blocked so a long
   entry cannot push the owner column around, and muted so the tag and title
   stay the things the eye lands on first. */
.backlog-group-blurb {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.85em;
    line-height: 1.35;
}

/* T178: a logged entry's micronutrients -- a quiet two-column list.
   "N/A" uses the same text colour as a value on purpose (Nick: don't
   highlight what's missing). */
.entry-micros {
    margin-top: 1.5rem;
    padding-top: 1rem;
    border-top: 1px solid var(--color-border);
}
.entry-micros h3 {
    margin: 0 0 0.5rem;
    font-size: 1rem;
}
.entry-micros dl {
    margin: 0;
}
.entry-micros-row {
    display: grid;
    grid-template-columns: 1fr auto;
    column-gap: 1rem;
    row-gap: 0.2rem;
    padding: 0.45rem 0;
    border-bottom: 1px solid var(--color-border);
}
.entry-micros-row dd {
    font-weight: 600;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.entry-micros-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
}
.entry-micros-sub {
    font-size: 0.8rem;
    color: var(--color-text-muted);
}
/* T183: the day's total against the daily value. */
.entry-micros-dv {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
.entry-micros-track {
    flex: 1;
    height: 6px;
    border-radius: 3px;
    background: var(--color-border);
    overflow: hidden;
}
.entry-micros-fill {
    display: block;
    height: 100%;
    border-radius: 3px;
}
.entry-micros-fill.dv-good { background: var(--color-success-text); }

/* 2026-09-21 visual review, §2.3. The one-line key that makes the red/green
   above mean something. Sized like the other .entry-micros-sub notes so it
   reads as a caption to the list, not as another row in it. */
.entry-micros-key {
    margin: 0.6rem 0 0;
    font-size: 0.72rem;
    line-height: 1.5;
    color: var(--color-text-muted);
}
.entry-micros-fill.dv-bad { background: var(--color-danger); }
.entry-micros-pct {
    font-size: 0.78rem;
    color: var(--color-text-muted);
    min-width: 5.5rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
}
.entry-micros-nodv {
    text-align: left;
}
.entry-micros-row:last-child {
    border-bottom: none;
}
.entry-micros dt,
.entry-micros dd {
    margin: 0;
}

/* ==========================================================================
   T184 (Nick, 2026-09-10): the food Details page as an app screen, from his
   notebook sketch and the approved mockup. Everything is scoped under
   #details-card so the global button/input/label rules above keep applying
   everywhere else; only the arrangement changes here, never a field.
   ========================================================================== */
#details-card {
    /* Where the fixed Log It bar sits: above the web bottom nav and its
       raised "+" button, or above the native tab bar in the app. */
    --details-bar-bottom: calc(66px + env(safe-area-inset-bottom));
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
}
.native-app #details-card {
    --details-bar-bottom: calc(var(--native-tab-bar-height) + 14px);
}
/* T227 (Nick, 2026-09-14): "the log it button is still not at the top of
   details."

   ⚠️ This REVERSES a decision he approved: T184 replaced T32's top+bottom Save
   pair with one sticky bottom bar, and he passed it in the app at the time. He
   has asked twice for it back at the top, so it moves.

   Done with flex `order`, not by moving the markup: the button stays exactly
   where it is in the form, so its id, its submit behaviour and the JS that
   writes "Log It - N cal" into it are all untouched. The summary takes -2 and
   the bar -1, which puts the bar directly under the food's name and calories
   and leaves every other child in document order.

   Still sticky, so it stays in reach while scrolling -- just pinned to the top
   now, which also means the gradient has to fade the other way.

   ⚠️ TRADE-OFF, so nobody rediscovers it: the primary action is now at the top
   of a phone screen, which is the harder end to reach with a thumb. Reverting
   is deleting this block.

   This also retires T226's fix from earlier today (`padding-bottom` on the form
   to stop the bottom-pinned bar covering the last micronutrient rows) -- with
   the bar at the top there is nothing at the bottom left to cover them. */
.food-summary { order: -2; }
.details-log-bar {
    order: -1;
    /* NOT sticky any more, and that is a finding rather than a choice.
       #details-card carries `overflow: auto` (from .card), which makes the CARD
       the sticky scrollport instead of the viewport -- so `top: 0` simply never
       engages. Proved both ways in a browser: with the card's overflow forced
       to visible, sticky-to-top pins perfectly at 0.
       That overflow is NOT safe to remove: the comment on .card records it as
       the fix for the rings card rendering narrower than every other card on
       iOS Safari, which cannot be tested from here. So the bar sits in normal
       flow at the top -- visible the moment a food is opened, which is what
       Nick asked for -- and scrolls away as you read down.
       T253 (2026-09-15): he chose the second bar. See .details-log-bar-bottom
       below -- this rule still governs the TOP one. */
    position: static;
    padding: 0.2rem 0 0.9rem;
}

#details-card label { margin-top: 0; }
#details-card > *,
#add-food-form > * { margin-top: 0; margin-bottom: 0; }
#add-food-form { display: flex; flex-direction: column; gap: 1.1rem; }

/* The way back, as a quiet pill rather than a text link. */
#details-card .details-back {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    align-self: flex-start;
    padding: 0.45rem 0.8rem 0.45rem 0.55rem;
    border-radius: 999px;
    background: var(--color-accent-soft);
    color: var(--color-text);
    font-size: 0.9rem;
    font-weight: 600;
    text-decoration: none;
}

/* ---- the summary ---------------------------------------------------------- */
.food-summary { display: flex; flex-direction: column; gap: 1rem; }
.food-title-row { display: flex; align-items: flex-start; gap: 0.6rem; }
.food-title {
    flex: 1;
    margin: 0;
    font-size: 1.65rem;
    line-height: 1.15;
    font-weight: 800;
    letter-spacing: -0.02em;
    text-wrap: balance;
    overflow-wrap: anywhere;
}
.food-title.is-placeholder { color: var(--color-text-muted); font-weight: 700; }
#details-card .details-icon-btn {
    flex: none;
    width: 40px;
    height: 40px;
    min-height: 40px;
    margin: 0;
    padding: 0;
    display: grid;
    place-items: center;
    border-radius: 12px;
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: none;
    font-size: 1.05rem;
}
#details-card[data-mode="edit"] .details-icon-btn {
    background: var(--color-edited-bg);
    border-color: var(--color-edited-border);
    color: var(--color-edited-text);
    font-weight: 800;
}
#details-card .done-icon,
#details-card[data-mode="edit"] .edit-icon { display: none; }
#details-card[data-mode="edit"] .done-icon { display: inline; }

.food-brand-row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-top: -0.55rem; }
.food-brand { font-size: 1.05rem; font-weight: 650; color: var(--color-accent-bg); }
#details-card .source-chip {
    margin: 0;
    min-height: 0;
    padding: 0.2rem 0.6rem;
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background: transparent;
    color: var(--color-text-muted);
    box-shadow: none;
    font-size: 0.78rem;
    font-weight: 650;
}
#details-card .source-chip-quiet { border-color: transparent; }

.food-energy { display: flex; flex-direction: column; gap: 0.6rem; }
.food-kcal { display: flex; align-items: baseline; gap: 0.4rem; }
/* T185: the edit button sits at the right end of the calories line. */
.food-kcal .details-icon-btn { margin-left: auto !important; align-self: center; }
.food-kcal b {
    font-size: 2.75rem;
    line-height: 1;
    font-weight: 800;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
}
.food-kcal span { color: var(--color-text-muted); font-weight: 600; }
.food-macro-bar {
    display: flex;
    gap: 2px;
    height: 22px;
    border-radius: 7px;
    overflow: hidden;
}
.food-macro-bar[hidden] { display: none; }
.food-macro-bar i { display: block; flex: 0 0 0; min-width: 0; }
.food-macro-bar .seg-protein { background: var(--color-macro-protein); }
.food-macro-bar .seg-carbs { background: var(--color-macro-carbs); }
.food-macro-bar .seg-fat { background: var(--color-macro-fat); }
.food-macro-legend { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
.food-macro-legend > div { display: flex; flex-direction: column; gap: 1px; }
.food-macro-legend .legend-key {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.8rem;
    font-weight: 650;
    color: var(--color-text-muted);
}
.food-macro-legend .legend-key::before {
    content: "";
    width: 10px;
    height: 10px;
    border-radius: 3px;
    background: var(--legend-dot);
}
.legend-protein { --legend-dot: var(--color-macro-protein); }
.legend-carbs { --legend-dot: var(--color-macro-carbs); }
.legend-fat { --legend-dot: var(--color-macro-fat); }
.food-macro-legend strong { font-size: 1.1rem; font-variant-numeric: tabular-nums; }
.food-macro-legend small { font-size: 0.75rem; color: var(--color-text-muted); }

/* ---- the fields ✏️ opens ------------------------------------------------------ */
#details-card[data-mode="view"] #details-edit-fields { display: none; }
.details-edit {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1rem;
    border-radius: 14px;
    background: var(--color-accent-soft);
}
.details-field { display: flex; flex-direction: column; gap: 0.4rem; }
.details-field > label,
.details-field-label { font-size: 0.8rem; font-weight: 650; color: var(--color-text-muted); }
.details-serving-inputs { display: grid; grid-template-columns: 6rem 1fr; gap: 0.5rem; }
.details-nutrition-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; }
.details-nutrition-grid > div { display: flex; flex-direction: column; gap: 0.25rem; }
#details-card .details-nutrition-grid input { text-align: center; font-weight: 700; padding: 0.5rem 0.2rem; min-width: 0; }
#details-card .details-nutrition-grid label {
    text-align: center;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--color-text-muted);
}
.details-edit-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
#details-card .details-edit-actions button { margin-top: 0; }
#details-card .details-edit #refresh-status:empty { display: none; }

/* ---- serving, amount, date: grouped rows ----------------------------------------- */
.details-group { border: 1px solid var(--color-border); border-radius: 14px; overflow: hidden; }
.details-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.7rem 0.9rem;
    min-height: 52px;
}
.details-row + .details-row { border-top: 1px solid var(--color-border); }
.details-row-label { color: var(--color-text-muted); font-size: 0.92rem; white-space: nowrap; }
.details-row-value { font-weight: 650; text-align: right; }
.details-row-value small { display: block; font-weight: 400; font-size: 0.8rem; color: var(--color-text-muted); }
.details-howmany { display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; justify-content: flex-end; }
.details-stepper {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--color-border);
    border-radius: 10px;
    overflow: hidden;
}
#details-card .details-stepper button {
    margin: 0;
    min-height: 38px;
    width: 38px;
    padding: 0;
    border-radius: 0;
    background: var(--color-accent-soft);
    color: var(--color-text);
    box-shadow: none;
    font-size: 1.2rem;
    font-weight: 700;
}
#details-card .details-stepper input {
    width: 3.6rem;
    min-height: 38px;
    border: 0;
    border-radius: 0;
    text-align: center;
    font-weight: 700;
    padding: 0 0.2rem;
}
#details-card .details-unit-select {
    width: auto;
    min-height: 38px;
    padding: 0.3rem 0.5rem;
    border-radius: 10px;
    color: var(--color-accent-bg);
    font-weight: 650;
    font-size: 16px;
}
.details-unit-text { color: var(--color-text-muted); font-size: 0.88rem; }
#details-card .details-date-btn {
    margin: 0;
    min-height: 0;
    padding: 0.35rem 0.1rem;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: transparent;
    color: var(--color-text);
    box-shadow: none;
    font-weight: 650;
}

/* Meal: one row of buttons over the (hidden) select. */
.meal-seg {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
    grid-auto-flow: column;
    gap: 3px;
    padding: 3px;
    border-radius: 12px;
    background: var(--color-accent-soft);
}
#details-card .meal-seg button {
    margin: 0;
    min-height: 38px;
    padding: 0.35rem 0;
    border-radius: 9px;
    background: transparent;
    color: var(--color-text-muted);
    box-shadow: none;
    font-size: 0.88rem;
    font-weight: 650;
}
#details-card .meal-seg button.is-on {
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.14);
}

/* T253: the SECOND bar, at the end of the card in normal flow. It must not
   take `order: -1`, or both would stack at the top -- so it undoes it rather
   than relying on source position, which flex ignores.
   ⚠️ NOT sticky, deliberately: T227 retired T226's padding-bottom fix because
   nothing was pinned any more, and pinning this would re-cover the
   micronutrient rows. "At the bottom of the page", not "floating above it". */
#details-card .details-log-bar-bottom {
    /* ⚠️ id + class ON PURPOSE. The plain `.details-log-bar` rule directly
       below sets `position: sticky` and comes LATER in the file, so at equal
       specificity it would win and pin this bar too. Raising specificity here
       beats source order, and beats it whichever way these rules are later
       reordered -- which a plain `.details-log-bar-bottom` would not. */
    position: static;
    order: 0;
    padding-top: 1.1rem;
}

/* The Log It bar: sticky at the bottom of the screen, above the nav. */
.details-log-bar {
    position: sticky;
    bottom: var(--details-bar-bottom);
    z-index: 5;
    padding: 0.6rem 0 0.2rem;
    background: linear-gradient(to bottom, transparent, var(--color-surface) 35%);
}
#details-card .details-log-bar button {
    width: 100%;
    margin: 0;
    min-height: 52px;
    border-radius: 14px;
    font-size: 1.02rem;
    font-weight: 750;
}
#details-card .entry-micros { margin-top: 0.25rem; }

/* T185: the Macros list -- the micronutrient row format, with the value in
   an editable box and a silhouette pencil. */
#details-card .pencil-icon { display: block; }
.macro-value { display: flex; justify-content: flex-end; }
/* Nick, 2026-09-10: plain text like the micronutrient values, NOT a box --
   the pencil beside it is the cue. The number still lives in a real input
   (so the pencil can open the app's number pad), styled to read as text;
   a thin underline appears only while it is being edited. */
.macro-box {
    display: inline-flex;
    align-items: center;
    gap: 0.1rem;
}
#details-card .macro-inline-input {
    width: 4.2ch;
    min-width: 2ch;
    min-height: 0;
    padding: 0;
    border: 0;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    background: transparent;
    color: var(--color-text);
    text-align: right;
    font-size: 1rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    field-sizing: content;
}
#details-card .macro-inline-input:focus {
    outline: none;
    border-bottom-color: var(--color-accent-bg);
}
.macro-unit { font-weight: 600; margin-left: 0.25rem; margin-right: 0.15rem; }
#details-card .macro-inline-input::-webkit-inner-spin-button,
#details-card .macro-inline-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
#details-card .macro-inline-input { -moz-appearance: textfield; appearance: textfield; }
#details-card .macro-pencil {
    margin: 0;
    min-height: 30px;
    width: 28px;
    padding: 0;
    display: grid;
    place-items: center;
    background: transparent;
    color: var(--color-text-muted);
    box-shadow: none;
}
.entry-micros-track[hidden] { display: none; }
.macro-fill-protein_g { background: var(--color-macro-protein); }
.macro-fill-carbs_g { background: var(--color-macro-carbs); }
.macro-fill-fat_g { background: var(--color-macro-fat); }
.entry-micros-fill.dv-bad { background: var(--color-danger); }

/* Nick: an edited macro (or calorie figure) must be obvious on the summary,
   not only inside the closed edit fields -- the same amber as .field-edited. */
.macro-row.is-edited .macro-box,
.food-kcal b.is-edited {
    background: var(--color-edited-bg);
    box-shadow: 0 0 0 2px var(--color-edited-border);
    border-radius: 8px;
}
.macro-row.is-edited .macro-box { padding: 0.1rem 0.1rem 0.1rem 0.45rem; }
.food-kcal b.is-edited { padding: 0 0.3rem; }
.macro-row .macro-row-note { grid-column: 1 / -1; margin: 0.2rem 0 0; }
.macro-row .macro-row-note[hidden],
#summary-kcal-note[hidden] { display: none; }
#summary-kcal-note { margin: 0; }
.food-macro-legend strong.is-edited { color: var(--color-edited-text); }

/* ==========================================================================
   T101 (Nick, 2026-09-10): Home, Trends and Weight in the Details page's
   style, from the approved mockup. Shared pieces first.
   ========================================================================== */
.screen-title { margin: 0.25rem 0 0.9rem; font-size: 1.85rem; font-weight: 800; letter-spacing: -0.02em; }

/* A two-to-five option switch -- replaces the single grey toggle buttons. */
.seg {
    display: inline-grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 3px;
    padding: 3px;
    border-radius: 11px;
    background: var(--color-accent-soft);
}
.seg button {
    margin: 0;
    min-height: 32px;
    padding: 0.3rem 0.85rem;
    border-radius: 8px;
    background: transparent;
    color: var(--color-text-muted);
    box-shadow: none;
    font-size: 0.86rem;
    font-weight: 650;
    white-space: nowrap;
}
.seg button.is-on {
    background: var(--color-surface);
    color: var(--color-text);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.14);
}
.wide-primary { width: 100%; min-height: 50px; border-radius: 14px; font-weight: 750; margin-top: 0; }
.is-down { color: var(--color-chart-good); }
.is-up { color: var(--color-danger); }

/* ---- Weight ------------------------------------------------------------- */
.weight-page .card { display: flex; flex-direction: column; gap: 0.85rem; }
.weight-page .card h2 { margin: 0; font-size: 1.1rem; }
.weight-card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.weight-now { display: flex; align-items: baseline; gap: 0.35rem; }
.weight-now b { font-size: 2.9rem; line-height: 1; font-weight: 800; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.weight-now span { color: var(--color-text-muted); font-weight: 650; font-size: 1.1rem; }
.weight-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.weight-chip { padding: 0.3rem 0.7rem; border-radius: 999px; background: var(--color-accent-soft); font-size: 0.86rem; font-weight: 600; }
.weight-chip .is-down, .weight-chip .is-up { font-weight: 750; }
.weight-last { font-size: 0.85rem; }
.weight-bignum {
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 0.35rem;
    padding: 0.8rem;
    border-radius: 14px;
    background: var(--color-accent-soft);
}
.weight-page .weight-bignum input {
    width: 6.5ch;
    min-height: 0;
    padding: 0;
    border: 0;
    background: transparent;
    text-align: right;
    font-size: 3rem;
    font-weight: 800;
    letter-spacing: -0.03em;
    color: var(--color-text);
    font-variant-numeric: tabular-nums;
}
.weight-page .weight-bignum input:focus { outline: none; }
.weight-bignum span { color: var(--color-text-muted); font-weight: 700; font-size: 1.15rem; }
.weight-log-form { display: flex; flex-direction: column; gap: 0.85rem; margin: 0; }
.weight-page .details-row label { margin: 0; font-weight: 400; font-size: 0.92rem; }
.weight-page .row-input {
    width: 7rem;
    min-height: 36px;
    text-align: right;
    font-weight: 650;
    border-radius: 9px;
}
.weight-page .row-date-btn {
    margin: 0;
    min-height: 0;
    padding: 0.35rem 0.1rem;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    background: transparent;
    color: var(--color-text);
    box-shadow: none;
    font-weight: 650;
}
.weigh-tip { margin: 0; font-size: 0.88rem; }
.weight-legend { display: flex; gap: 1rem; font-size: 0.8rem; color: var(--color-text-muted); font-weight: 600; }
.weight-legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
.weight-legend .legend-line::before { content: ""; width: 16px; height: 3px; border-radius: 2px; background: var(--color-chart-line); }
.weight-legend .legend-dot::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--color-text-muted); }

.weight-history { margin-bottom: 1rem; background: var(--color-surface); }
.weight-history-head { padding: 0.65rem 0.9rem; font-weight: 750; background: var(--color-accent-soft); }
.weight-row { border-top: 1px solid var(--color-border); }
.weight-row summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.7rem 0.9rem;
    min-height: 52px;
    cursor: pointer;
    list-style: none;
}
.weight-row summary::-webkit-details-marker { display: none; }
.weight-row-date { flex: 1; }
.weight-row-date small { display: block; color: var(--color-text-muted); font-size: 0.78rem; }
.weight-row-value { text-align: right; font-weight: 650; font-variant-numeric: tabular-nums; }
.weight-row-value small { display: block; font-size: 0.78rem; font-weight: 600; }
.weight-row-chev { color: var(--color-text-muted); transition: transform 0.15s ease-out; }
.weight-row[open] .weight-row-chev { transform: rotate(90deg); }
.weight-row-edit { padding: 0 0.9rem 0.9rem; display: flex; flex-direction: column; gap: 0.5rem; }
.weight-row-form { display: grid; grid-template-columns: 1fr 1fr auto; gap: 0.5rem; align-items: end; margin: 0; }
.weight-row-form label { margin: 0; font-size: 0.78rem; color: var(--color-text-muted); display: flex; flex-direction: column; gap: 0.25rem; }
.weight-row-form button { margin: 0; min-height: 44px; }
.weight-row-delete { margin: 0; }
.danger-link {
    margin: 0;
    min-height: 0;
    padding: 0.2rem 0;
    background: transparent;
    color: var(--color-danger);
    box-shadow: none;
    font-size: 0.88rem;
    font-weight: 650;
}
.weight-page .weight-bignum input::-webkit-inner-spin-button,
.weight-page .weight-bignum input::-webkit-outer-spin-button,
.weight-page .row-input::-webkit-inner-spin-button,
.weight-page .row-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.weight-page .weight-bignum input,
.weight-page .row-input { -moz-appearance: textfield; appearance: textfield; }

/* ---- Home (T101) ---------------------------------------------------------- */
.home-hero { display: flex; flex-direction: column; gap: 0.7rem; }
.home-kcal { display: flex; align-items: baseline; gap: 0.45rem; }
.home-kcal b { font-size: 3rem; line-height: 1; font-weight: 800; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
.home-kcal b.is-over { color: var(--color-danger); }
.home-kcal span { color: var(--color-text-muted); font-weight: 650; }
.home-bar {
    display: flex;
    gap: 2px;
    height: 22px;
    border-radius: 7px;
    overflow: hidden;
    background: var(--color-accent-soft);
}
.home-bar.is-over { box-shadow: 0 0 0 2px var(--color-danger); }
.home-bar i { display: block; height: 100%; flex: none; }
.home-bar-plain { background: var(--color-chart-line); }
.home-subline { margin: 0; font-size: 0.9rem; color: var(--color-text-muted); }
.home-subline a { color: inherit; text-decoration: none; }
.home-subline strong { color: var(--color-text); }
.home-legend { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; }
.home-legend > div { display: flex; flex-direction: column; gap: 1px; }
.home-legend-key { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.8rem; font-weight: 650; color: var(--color-text-muted); }
.home-legend strong { font-size: 1.1rem; font-variant-numeric: tabular-nums; }
.home-legend small { font-size: 0.75rem; color: var(--color-text-muted); }

.home-micros { display: flex; flex-direction: column; gap: 0.6rem; }
.home-card-head { display: flex; align-items: center; justify-content: space-between; }
.home-card-head h2 { margin: 0; }
/* T196: three micronutrients on Home; the full list is on Trends. */
.home-micros dl { margin: 0; }
.home-micros-more { font-weight: 650; font-size: 0.9rem; color: var(--color-accent-bg); text-decoration: none; }
/* T211: "See more" became a <button> (it expands in place rather than
   navigating). Reset the button chrome so it still reads as the same link
   it was, and give it a real touch height. */
button.home-micros-more {
    background: none;
    border: 0;
    padding: 0.5rem 0;
    min-height: 44px;
    cursor: pointer;
    font-family: inherit;
    text-align: left;
    /* T226 (Nick, 2026-09-14): "The see more button has a box around it, and
       see more on trends does not. Remove the box from see more."

       These two sit side by side and must look identical, but one is a <button>
       and one is an <a>, so the button quietly inherits chrome from the global
       `button, .btn` rule -- margin-top:1rem and border-radius:8px -- and, on
       iOS specifically, the platform's own button appearance, which draws a box
       that `background: none` alone does not remove. Everything that could draw
       one is reset here rather than guessing which of them it was. */
    -webkit-appearance: none;
    appearance: none;
    margin: 0;
    border-radius: 0;
    -webkit-tap-highlight-color: transparent;
    box-shadow: none;
}
button.home-micros-more:focus { outline: none; }
button.home-micros-more:focus-visible { outline: 2px solid var(--color-accent-bg); outline-offset: 2px; }

/* Tappable food rows: the whole row opens the food. */
.meal-entry-link {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    color: inherit;
    text-decoration: none;
    min-height: 44px;
}
.meal-entry-link .meal-entry-name { flex: 1; }
.meal-entry-brand { display: block; color: var(--color-text-muted); font-size: 0.78rem; font-weight: 400; }
.meal-entry-chev { color: var(--color-text-muted); flex: none; }

.home-weight-tile {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    color: inherit;
    text-decoration: none;
}
.home-weight-text { flex: 1; display: flex; flex-direction: column; gap: 0.1rem; font-size: 0.88rem; }
.home-weight-text b { font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.home-spark { width: 120px; height: 40px; flex: none; }
.home-spark polyline { fill: none; stroke: var(--color-chart-line); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
#details-card .details-delete-form { margin: 0.25rem 0 0; text-align: center; }

/* ---- Trends (T101) --------------------------------------------------------- */
.trends-seg { margin: 0.1rem 0 0.35rem; }
.trends-sub { margin: 0; font-size: 0.85rem; }
.trends-sub-inline { font-size: 0.8rem; }
.trends-rows { margin: 0.25rem 0 0; }
.trends-rows dd .muted { font-weight: 500; font-size: 0.85rem; }
#averagesChartWrap[hidden], #averagesExpandRow[hidden], #averagesRows[hidden] { display: none; }
#microsAvgCard .chart-card-header { flex-wrap: wrap; row-gap: 0.1rem; }

/* WeighTooMuch Plus (T194): the Plus page and its Settings group. */
.plus-hero { text-align: center; }
.plus-hero h1 { margin-bottom: 0.25rem; }
.plus-hero p { margin-top: 0; }
.plus-features { display: flex; flex-direction: column; gap: 0.9rem; }
.plus-feature { display: flex; flex-direction: column; gap: 0.15rem; }
.plus-note { font-size: 0.85rem; margin: 0.5rem 0 0; }
.plus-subscribe { width: 100%; margin-top: 1rem; }
.plus-subscribe:disabled { opacity: 0.55; cursor: default; }
.plus-badge {
    margin-left: auto;
    margin-right: 0.5rem;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-accent-bg);
    background: var(--color-accent-soft);
    padding: 0.15rem 0.5rem;
    border-radius: 999px;
}
.settings-row-sub { display: block; font-size: 0.8rem; color: var(--color-text-muted); font-weight: 400; }
.settings-row-soon { color: var(--color-text-muted); cursor: default; }

/* Insights (T190). */
.home-insights-line { font-size: 1rem; font-weight: 700; }
.insights-head { display: flex; align-items: baseline; justify-content: space-between; }
.insights-head h1 { margin-bottom: 0.5rem; }
.insights-lead { background: var(--color-accent-soft); display: flex; flex-direction: column; gap: 0.45rem; }
.insights-lead p { margin: 0; }
.insights-label { font-size: 0.75rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--color-accent-bg); }
.insights-figure { font-size: 2.2rem; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
.insights-figure small { font-size: 0.95rem; font-weight: 500; color: var(--color-text-muted); margin-left: 0.3rem; }
.insights-basis { font-size: 0.82rem; }
.insights-apply { align-self: flex-start; margin-top: 0.25rem; text-decoration: none; }
.insights-meter { display: block; height: 6px; border-radius: 3px; background: var(--color-border); overflow: hidden; }
.insights-meter i { display: block; height: 100%; background: var(--color-accent-bg); }
.insights-list ul, .insights-locked-rows { list-style: none; margin: 0; padding: 0; }
.insights-list li, .insights-locked-rows li { display: flex; flex-direction: column; gap: 0.2rem; padding: 0.75rem 0; border-top: 1px solid var(--color-border); }
.insights-list li:first-child, .insights-locked-rows li:first-child { border-top: 0; }
.insights-list li .muted { font-size: 0.88rem; }
.insights-badge { display: inline-block; margin: 0 0 0.25rem; }
.insights-split { display: flex; height: 8px; border-radius: 4px; overflow: hidden; margin: 0.15rem 0; }
.insights-split i { display: block; }
.insights-split-1 { background: var(--color-macro-carbs); }
.insights-split-2 { background: var(--color-macro-fat); }
.insights-split-3 { background: var(--color-macro-protein); }
.insights-split-4 { background: var(--color-border); }
.insights-locked { position: relative; overflow: hidden; }
.insights-locked-rows { filter: blur(5px); user-select: none; pointer-events: none; }
.insights-locked-rows span { color: var(--color-text-muted); font-size: 0.88rem; }
.insights-lock {
    position: absolute; inset: 0; margin: auto; width: 82%; height: fit-content;
    display: flex; flex-direction: column; align-items: center; gap: 0.4rem; text-align: center;
    background: var(--color-surface); border: 1px solid var(--color-border); border-radius: 12px; padding: 0.9rem;
    box-shadow: 0 10px 24px -14px rgba(20, 24, 26, 0.4);
}
.insights-lock .btn { text-decoration: none; }
.insights-foot { font-size: 0.85rem; }
.insights-once {
    border: 1px solid var(--color-edited-border);
    background: var(--color-edited-bg);
    color: var(--color-edited-text);
    border-radius: 10px;
    padding: 0.6rem 0.75rem;
    font-size: 0.88rem;
    line-height: 1.45;
}
.insights-once b { display: block; }


/* ===== T202: "Double-check with [brand]" and Verified foods ===== */
.verify-verified-line { display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem; margin: 0; font-size: 0.85rem; color: var(--color-text-muted); }
.verify-badge {
    display: inline-flex; align-items: center; gap: 0.2rem;
    padding: 0.15rem 0.55rem; border-radius: 999px;
    background: var(--color-success-bg); color: var(--color-success-text);
    font-size: 0.75rem; font-weight: 750;
}
.verify-banner {
    display: grid; gap: 0.6rem; padding: 0.8rem 0.9rem; border-radius: 12px;
    background: var(--color-success-bg); color: var(--color-success-text);
}
.verify-banner p { margin: 0; display: grid; gap: 0.2rem; font-size: 0.9rem; }
.verify-box { display: grid; gap: 0.5rem; }
.verify-box-unusual {
    padding: 0.75rem 0.85rem; border-radius: 12px;
    background: var(--color-edited-bg); color: var(--color-edited-text);
    border: 1px solid var(--color-edited-border);
}
.verify-unusual { margin: 0; display: grid; gap: 0.2rem; font-size: 0.85rem; }
#details-card .verify-btn {
    margin: 0; width: 100%;
    display: inline-flex; flex-direction: column; align-items: center; gap: 0.1rem;
}
.verify-btn small { font-size: 0.75rem; font-weight: 500; opacity: 0.85; }
.verify-message { margin: 0; font-size: 0.85rem; color: var(--color-text-muted); }
.verify-box-unusual .verify-message { color: inherit; }
.verify-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
#details-card .verify-actions button { margin: 0; flex: 1 1 8rem; }
.verify-result { display: grid; gap: 0.6rem; }
.verify-result-message { margin: 0; font-size: 0.9rem; color: var(--color-text-muted); }
.verify-spin { display: flex; align-items: center; gap: 0.6rem; font-size: 0.9rem; color: var(--color-text-muted); }
.verify-spin i {
    width: 1rem; height: 1rem; border-radius: 50%;
    border: 2px solid var(--color-border); border-top-color: var(--color-accent-bg);
    animation: verify-spin 0.8s linear infinite;
}
@keyframes verify-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .verify-spin i { animation-duration: 2.4s; } }
.verify-found {
    display: grid; gap: 0.55rem; padding: 0.85rem 0.9rem; border-radius: 12px;
    border: 1px solid var(--color-border); background: var(--color-surface);
}
.verify-src { margin: 0; font-size: 0.8rem; color: var(--color-text-muted); }
.verify-src a { color: var(--color-accent-bg); font-weight: 650; }
.verify-found-name { margin: 0; font-size: 1.05rem; }
.verify-servings { display: grid; gap: 0.3rem; }
.verify-serving {
    display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.5rem;
    padding: 0.4rem 0; border-bottom: 1px solid var(--color-border); font-size: 0.9rem;
}
.verify-serving span { font-variant-numeric: tabular-nums; color: var(--color-text-muted); }
.verify-macros { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.4rem; font-size: 0.8rem; }
.verify-macro { display: grid; gap: 0.1rem; }
.verify-macro span { color: var(--color-text-muted); }
.verify-macro b { font-variant-numeric: tabular-nums; }
/* T192: the app-colour picker. Each swatch is its own submit button, so the
   global button rule has to be undone here -- it sets a background, a shadow
   and a text colour that would fight the swatch. */
.accent-lede { max-width: 40rem; margin-bottom: 1.25rem; }
.accent-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.accent-option {
    display: flex; flex-direction: column; gap: 8px; align-items: stretch;
    padding: 10px; width: 100%; text-align: left;
    background: var(--color-surface); color: var(--color-text);
    border: 1px solid var(--color-border); border-radius: 12px;
    box-shadow: none; font: inherit; cursor: pointer;
}
.accent-option:hover { border-color: var(--color-text-muted); background: var(--color-surface); }
.accent-option.is-current { border-color: var(--color-accent-bg); border-width: 2px; padding: 9px; }
.accent-preview { display: flex; gap: 4px; height: 44px; }
.accent-bar { flex: 1; border-radius: 6px; }
.accent-bar-soft { flex: 0 0 22%; }
.accent-name { font-weight: 600; font-size: 0.92rem; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.accent-current-tag {
    font-size: 0.66rem; letter-spacing: 0.06em; text-transform: uppercase;
    font-weight: 600; color: var(--color-accent-bg);
}

/* T225: "Search online", offered only when a search found nothing at all. Full
   width so it cannot be missed, and the cost said in words right under it -- the
   wording must not make the expensive path feel free. */
.search-online-btn { display: block; width: 100%; margin-top: 0.75rem; }
.search-online-note { margin: 0.35rem 0 0; font-size: 0.8rem; color: var(--color-text-muted); text-align: center; }

/* T231: the maker's other servings, under the Verified line. Wraps rather than
   overflowing -- the T215 lesson, where 384px of content in a 359px card pushed
   a control off a 393px screen. */
.verify-servings { display: flex; align-items: center; flex-wrap: wrap; gap: 0.45rem; margin: 0.5rem 0 0; font-size: 0.85rem; color: var(--color-text-muted); }
.verify-servings select { min-width: 0; max-width: 100%; flex: 1 1 12rem; }

/* Their display rules above would otherwise beat the hidden attribute. */
.verify-verified-line[hidden], .verify-banner[hidden], .verify-box[hidden], .verify-unusual[hidden],
.verify-result[hidden], .verify-message[hidden], .verify-btn small[hidden],
.verify-servings[hidden] { display: none; }

/* ---- T205: the Double-checks inbox ------------------------------------
   A Double-check can take minutes, so the answer needs somewhere to go that
   is not "the page you must not leave". These styles are the inbox list, its
   unread marker, the status pills and the badge that appears on Home and in
   Settings. Everything reuses the existing card/settings-row shapes; nothing
   here invents a new kind of surface. */

/* The unread dot sits in a fixed-width slot so the eye finds it in the same
   place on every row, and the slot is reserved even when empty -- otherwise
   read and unread rows would have their text at different left edges. */
.dc-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
    margin-right: 0.6rem;
    margin-top: 0.45rem;
    align-self: flex-start;
    background: transparent;
}
.dc-dot.is-new { background: var(--color-streak-gold); }

/* .settings-row is justify-content:space-between, which is right for its
   usual two children (label, chevron). Adding the unread dot made three, and
   the label was pushed to the centre of the row. Letting the middle child
   take the free space puts it back against the dot, where every other
   settings row's label sits. */
.settings-row .dc-text { flex: 1; min-width: 0; text-align: left; }

.dc-pill {
    display: inline-block;
    margin-top: 0.3rem;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 700;
}
/* --color-success-bg / --color-success-text, NOT a made-up --color-good: the
   first version of this used a token that does not exist, so the pill had an
   invalid colour and rendered with no background at all while the two beside
   it looked right. An unknown CSS variable fails silently -- a browser was
   the only thing that showed it. */
.dc-found { background: var(--color-success-bg); color: var(--color-success-text); }
.dc-none { background: var(--color-border); color: var(--color-text-muted); }
.dc-run { background: var(--color-accent-soft); color: var(--color-accent-bg); }

/* Gold, matching the streak dot: in this app gold already means "something
   of yours happened". Reusing it beats introducing a second attention colour,
   and it is deliberately NOT the danger red -- a waiting result is good news,
   not a problem. */
.dc-badge {
    background: var(--color-streak-gold);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    margin-left: auto;
    flex: none;
    align-self: center;
    font-variant-numeric: tabular-nums;
}
.home-doublecheck-tile .dc-badge { margin-left: 0.5rem; }

.dc-serving {
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--color-border);
}
.dc-serving:last-of-type { border-bottom: 0; }
.dc-serving-head {
    display: flex;
    justify-content: space-between;
    gap: 0.75rem;
    align-items: baseline;
}
.dc-serving-head strong { font-variant-numeric: tabular-nums; white-space: nowrap; }
.dc-macros {
    font-size: 0.82rem;
    color: var(--color-text-muted);
    margin-top: 0.15rem;
    font-variant-numeric: tabular-nums;
}

/* T205: the "you can leave" note under the spinner on the food page. Muted,
   because it is reassurance rather than an instruction -- but present, since
   the inbox is useless if the page still implies you have to wait. */
.verify-leave-note {
    margin: 0.5rem 0 0;
    font-size: 0.85rem;
    color: var(--color-text-muted);
}

/* T191: Home and Trends cards are arranged by the account's own order, applied
   as a CSS `order` number on each card (app/card_layout.py). Flex is what makes
   those numbers mean anything -- this container was previously an unstyled
   block, and a block container ignores `order` entirely.
   Column flex draws the same stack it drew before, with one difference that is
   the whole feature: children come out in `order` sequence, not source order.
   Cards keep their own margin-bottom, so spacing is unchanged. */
.grid-cards {
    display: flex;
    flex-direction: column;
}

/* T191: the Home & Trends layout editor (main/card_layout.html). One row per
   card: its name, then up / down / hide. Sized for a thumb -- the buttons are
   44px, the iOS minimum tap target, which is also why the row does not try to
   fit a fourth control. */
.layout-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.layout-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--color-border);
}

.layout-row:last-child {
    border-bottom: 0;
}

.layout-name {
    flex: 1 1 auto;
    min-width: 0;
}

/* A hidden card is still listed -- that is how you get it back -- but reads as
   switched off rather than missing. */
.layout-row.is-hidden .layout-name {
    opacity: 0.45;
    text-decoration: line-through;
}

.layout-actions {
    display: flex;
    gap: 0.35rem;
    flex: 0 0 auto;
}

.layout-btn {
    min-width: 44px;
    min-height: 44px;
    padding: 0 0.5rem;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    background: var(--color-surface);
    color: inherit;
    font-size: 1rem;
    cursor: pointer;
}

.layout-btn:disabled {
    opacity: 0.35;
    cursor: default;
}

.layout-btn-eye {
    font-size: 0.85rem;
}

.layout-pinned-note {
    margin-top: -0.25rem;
}

/* T224 stage 1: the "log it now" line under the spinner. Same muted voice as
   the "you can leave this page" note it sits with -- it is guidance, not a
   second button competing with Log It, which is already on screen. */
.verify-log-now { margin-top: 0.35rem; font-weight: 600; }
/* T264: the mascot was REMOVED from every screen on 2026-09-18, after a week
   of real use. Nick: "Lets scrap the idea, and I will come back later with a
   better mascot Idea without animations."

   ⚠️ EVERY ANIMATION WENT WITH HIM rather than being left behind
   unreferenced. Dead CSS that once did something is the worst kind: it reads
   as intentional, so the next person preserves it. What is left below is
   plain layout, which a future mascot can use unchanged.

   WHAT THE WEEK TAUGHT, kept because it cost real time to learn:
     · An animation declared INSIDE an <img>-loaded SVG ignores
       prefers-reduced-motion entirely. Motion must live on the element.
     · "Ducks while scrolling" means "hidden almost always" on a phone.
     · A placement that leaves is not a placement: a run-past cameo left a
       third of sessions with no mascot at all.
     · ⚠️ AND THE ONE THAT ENDED IT: the position against the native tab bar
       was never verified on a device. "The HTML runs underneath the real
       bar" was read from a comment and built on twice; his nose was still
       clipped after both fixes. Measure on the phone BEFORE building on an
       assumption about the phone. */
.mascot {
    margin: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
}

.mascot-img {
    width: auto;
    max-width: 100%;
}

.mascot-caption {
    color: var(--color-text-muted, inherit);
    font-size: 0.9rem;
    text-align: center;
}

/* The Settings row that turns him on and off, which outlives the placements:
   the switch is the part of this feature that was right. */
.settings-row-toggle { gap: 0.6rem; }
.settings-row-main { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.settings-row-btn { margin: 0; white-space: nowrap; flex-shrink: 0; }
.mascot-preview { width: auto; flex-shrink: 0; }

/* --- T303: first-time setup, one question per screen ---------------------
   onboarding.html, goals.html (first time), reminders.html, and the bar on
   signup.html. setup_steps.js shows one .setup-step at a time. */
.setup-page {
    max-width: 480px;
    margin: 1rem auto 2rem;
}

.setup-progress {
    height: 6px;
    border-radius: 3px;
    background: var(--color-border);
    overflow: hidden;
    margin-bottom: 1.25rem;
}

.setup-progress-fill {
    display: block;
    height: 100%;
    background: var(--color-accent-bg);
    transition: width 0.25s ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .setup-progress-fill { transition: none; }
}

.setup-step h1 { margin-top: 0.5rem; }
.setup-step h1:focus { outline: none; }

/* Back on the left, the forward action on the right, both full height. */
.setup-nav {
    display: flex;
    gap: 0.75rem;
    margin-top: 1.5rem;
}
.setup-nav > * { flex: 1; margin-top: 0; text-align: center; }

.setup-footer { margin-top: 1.5rem; text-align: center; }

/* Goal and activity: big tappable cards instead of a dropdown. The radio
   stays in the DOM (keyboard, screen readers, the form POST) and the card
   around it is the visible control. */
.choice-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-top: 1rem;
}

.choice {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 0.9rem 1rem;
    min-height: 44px;
    border: 2px solid var(--color-border);
    border-radius: 12px;
    background: var(--color-surface);
    cursor: pointer;
}

.choice input[type="radio"] {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

.choice-title { font-weight: 600; }
.choice-detail { color: var(--color-text-muted); font-size: 0.9rem; }

.choice:has(input:checked) {
    border-color: var(--color-accent-bg);
    background: var(--color-accent-soft);
}

.choice:has(input:focus-visible) {
    outline: 2px solid var(--color-accent-bg);
    outline-offset: 2px;
}

.choice-list.field-needed { outline: 2px solid var(--color-danger); outline-offset: 4px; border-radius: 12px; }

/* The payoff: the number they came for, big, before any of the working. */
.setup-payoff { text-align: center; }
.setup-number { font-size: 1.1rem; margin: 0.25rem 0 0; }
.setup-number strong { display: block; font-size: 3rem; line-height: 1.1; }

/* The panda chef. Height comes from the macro; this only centres him. */
.panda {
    display: block;
    width: auto;
    max-width: 100%;
    margin: 0 auto 0.5rem;
}
.settings-row .panda { margin: 0; flex-shrink: 0; }
.hero-greeting-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.hero-greeting-row .panda { margin: 0; flex-shrink: 0; }

/* Nick, 2026-09-23: the double-check chip, beside the calorie number instead
   of a big button just under Log It. Sits left of the edit pencil. */
.food-kcal .verify-chip { margin-left: auto; align-self: center; }
.food-kcal .verify-chip:not([hidden]) + .verify-chip { margin-left: 0; }
.food-kcal .verify-chip:not([hidden]) ~ .details-icon-btn { margin-left: 0.4rem !important; }
.verify-chip {
    margin: 0;
    min-height: 36px;
    padding: 0.35rem 0.8rem;
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
    background: var(--color-accent-soft);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    box-shadow: none;
}
.verify-chip:hover { background: var(--color-accent-soft); }
.verify-chip[hidden] { display: none; }
.verify-chip:disabled { opacity: 0.5; cursor: default; }
/* The double-check count, right-aligned under the chip it belongs to. */
.verify-chip-note {
    margin: 0.15rem 0 0;
    text-align: right;
    font-size: 0.8rem;
    color: var(--color-text-muted);
}
.verify-chip-note[hidden] { display: none; }

/* --- Swipe a food off "Recently logged" (Nick, 2026-09-24) ---------------
   The red layer sits under the whole row, text at its right end, so a long
   swipe shows red all the way across; the row slides over it. */
.recent-swipe {
    position: relative;
    overflow: hidden;
}
.recent-swipe .search-result-item {
    position: relative;
    z-index: 1;
    background: var(--color-surface);
    touch-action: pan-y;   /* vertical scrolling stays native; sideways is ours */
}
.recent-swipe-delete {
    all: unset;
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    justify-content: center;
    gap: 0.15rem;
    padding-right: 1.1rem;
    background: var(--color-danger);
    color: #fff;
    font-size: 0.75rem;
    font-weight: 600;
    cursor: pointer;
}
.recent-swipe-delete svg { margin-right: 0.35rem; }
.recent-swipe.is-removing { opacity: 0.4; pointer-events: none; }

/* On a computer there is no swipe: a trash icon on hover instead. */
.recent-remove-mini { display: none; }
@media (hover: hover) {
    .recent-swipe .search-result-item:hover .recent-remove-mini {
        all: unset;
        order: 3;
        margin-left: auto;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 28px;
        height: 28px;
        color: var(--color-text-muted);
        cursor: pointer;
    }
    .recent-swipe .search-result-item:hover .recent-remove-mini:hover { color: var(--color-danger); }
}

.recent-notice h2 { margin-top: 0; }
.recent-notice-actions { display: flex; gap: 0.75rem; }
.recent-notice-actions button { flex: 1; margin-top: 0.5rem; }

/* Nick, 2026-09-24, on the phone: cards 30% closer to the screen edges
   (16px -> 11px), and inside the Food page's search card -- search bar,
   buttons and the Recently logged list -- 45% closer to the card's edges
   (24px -> 13px). Top/bottom spacing is unchanged. */
@media (max-width: 480px) {
    .container {
        padding-left: 0.7rem;
        padding-right: 0.7rem;
    }
    #search-card:not(.search-card-overlay),
    /* Claude's pass after that change, same reasoning: cards whose
       contents are boxes-within-the-box or charts. Text-only cards keep
       the roomier padding. */
    .home-diary,
    #details-card,
    .weight-log,
    .weight-trend,
    .trends-card {
        padding-left: 0.825rem;
        padding-right: 0.825rem;
    }
}
