/* ==========================================================================
   Baby Tracker — design system

   One stylesheet, no build step, no framework. Everything below is written
   against the class vocabulary in the build spec (§6b); nothing here styles a
   class the JavaScript will not emit, and nothing the JavaScript emits is left
   unstyled.

   Two people use this app. It is opened one-handed, in the dark, by somebody
   who has been awake since 3am and is holding a baby in the other arm. Every
   decision below follows from that: 44px targets, 16px type, huge numbers,
   and a dark theme that is genuinely dark rather than a grey theme with the
   lights turned down.

   Table of contents
     1.  Tokens                    every literal colour, size and easing
     2.  Themes                    dark by preference or by attribute
     2b. Palettes                  neutral / boy / girl, across both themes
     3.  Reset and base
     4.  Layout                    container, stack, row, state helpers
     5.  App header
     6.  Views
     7.  Tab bar
     8.  Buttons
     9.  Tracker accents           feeding / diaper / sleep / medicine /
                                   temperature / growth / vaccine
     10. Home cards                .tcard, the grid, the pulsing dot, flagged
     11. Stats, bars, rings, meters
     12. Entries, separators, empty states
     13. Forms and controls
     14. Overlays                  backdrop, sheet, toast
     15. Skeletons
     16. Sign-in card
     17. Wide screens
     18. Keyframes
     19. Reduced motion
     20. Standing banners          the open episode and the nappy rash, above
                                   the card grid — plus the emergency state the
                                   first of them takes on the server's verdict
     21. Guidance panels           .guide, the frame around the fever wording
     22. Growth chart              the inline-SVG weight line
     23. PIN pad                   the four dots and the keypad under them
     24. Visit summary             .summary, the sheet a pediatrician reads
     25. Print                     what that sheet comes out of a printer as
     26. Settings list             the rows Settings collapses into
     27. Vaccine reference         the record card and the photographs of it
     28. Contraction timer         the mother's screen, in plum
     29. Expecting home            the due panel and the two doors
     30. Hospital bag              two packing checklists
     31. Babies                    the pager, the page dots and the header
                                   switcher — drawn only above one baby

   20 onwards are appended rather than slotted in beside their kin, because
   renumbering every section under them would rewrite the whole file to move a
   dozen blocks. All of them except 25 are ordinary components and §19's
   blanket rule reaches them like everything else; 25 is a media block that
   overrides the lot, and it still does from where it sits: nothing below it
   names a selector §25 sets, and the tokens §25 remaps are read at use time.
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. Tokens

   Every colour literal in the app lives in this one block, including the dark
   ones. The theme blocks in §2 only re-point the semantic names at a different
   literal — they never introduce a colour of their own.

   That is deliberate. A colour defined inside @media (prefers-color-scheme)
   exists only for people whose phone happens to be in dark mode, so the
   html[data-theme="dark"] override cannot reach it and the two ways of turning
   the app dark drift apart. Keeping the literals here means the two theme
   blocks are pure remapping and can be read side by side.
   -------------------------------------------------------------------------- */
:root {
    /* ---- Light literals -------------------------------------------------
       Warm off-white rather than pure white. A white app at 6am is a torch,
       and the warmth is what keeps a screen full of pastel accents from
       looking clinical. */
    --lt-paper:      #FBFAF9;
    --lt-paper-2:    #F2EFEB;
    --lt-surface:    #FFFFFF;
    --lt-surface-2:  #F4F2EF;
    --lt-ink:        #16182A;
    --lt-ink-soft:   #4C5068;
    --lt-ink-dim:    #7C8199;
    --lt-line:       rgba(22, 24, 42, 0.10);
    --lt-line-2:     rgba(22, 24, 42, 0.17);
    --lt-track:      rgba(22, 24, 42, 0.08);
    --lt-skel-a:     rgba(22, 24, 42, 0.055);
    --lt-skel-b:     rgba(22, 24, 42, 0.115);
    --lt-toast-bg:   #1B1D2E;
    --lt-toast-ink:  #F6F5FB;
    --lt-scrim:      rgba(18, 18, 30, 0.40);
    --lt-header-bg:  rgba(251, 250, 249, 0.82);
    --lt-glass:      rgba(255, 255, 255, 0.72);

    /* ---- Dark literals --------------------------------------------------
       The theme that matters. #101018 is near-black with just enough blue in
       it to sit under navy ink without looking brown, and the surfaces step up
       in small increments — on an OLED phone at minimum brightness anything
       larger reads as a lit panel floating in a black room.

       The text is #E7E6EE, never #FFFFFF. Pure white on near-black is what
       makes a dark theme feel like it is shouting. */
    --dk-paper:      #101018;
    --dk-paper-2:    #14141E;
    --dk-surface:    #1A1A24;
    --dk-surface-2:  #23232F;
    --dk-ink:        #E7E6EE;
    --dk-ink-soft:   #A7A7BC;
    --dk-ink-dim:    #767690;
    --dk-line:       rgba(255, 255, 255, 0.075);
    --dk-line-2:     rgba(255, 255, 255, 0.145);
    --dk-track:      rgba(255, 255, 255, 0.09);
    --dk-skel-a:     rgba(255, 255, 255, 0.05);
    --dk-skel-b:     rgba(255, 255, 255, 0.11);
    --dk-toast-bg:   #2A2A38;
    --dk-toast-ink:  #F1F0F7;
    --dk-scrim:      rgba(4, 4, 10, 0.62);
    --dk-header-bg:  rgba(16, 16, 24, 0.78);
    --dk-glass:      rgba(26, 26, 36, 0.72);

    /* ---- Tracker accents ------------------------------------------------
       Amber, teal, indigo, rose. Four hues far enough apart that a card is
       identifiable from across a dark room by colour alone, before anybody
       reads the label.

       The dark set is the same four hues pulled down in chroma and lightness.
       Full-saturation #F59E42 on #101018 glares; #E29456 does not, and at 3am
       that is the difference between glancing at the screen and squinting. */
    --lt-feeding:    #F59E42;
    --lt-diaper:     #3FBF9F;
    --lt-sleep:      #6C6BE8;
    --lt-medicine:   #EC6A8C;
    --lt-feeding-s:  rgba(245, 158, 66, 0.14);
    --lt-diaper-s:   rgba(63, 191, 159, 0.14);
    --lt-sleep-s:    rgba(108, 107, 232, 0.13);
    --lt-medicine-s: rgba(236, 106, 140, 0.13);

    --dk-feeding:    #E29456;
    --dk-diaper:     #45AE97;
    --dk-sleep:      #7B7AD8;
    --dk-medicine:   #DD7691;
    --dk-feeding-s:  rgba(226, 148, 86, 0.18);
    --dk-diaper-s:   rgba(69, 174, 151, 0.18);
    --dk-sleep-s:    rgba(123, 122, 216, 0.19);
    --dk-medicine-s: rgba(221, 118, 145, 0.18);

    /* Red and green, added with the temperature and growth trackers.

       The red is not a fifth decorative hue picked for variety. It is the
       fever colour, and a thermometer card painted anything else would be
       lying about what it is for. It lands one point from --lt-danger so a
       flagged reading and the tracker it came from are the same red, and in
       the dark theme it is --dk-danger exactly — there is no argument for two
       reds a shade apart on a screen that only ever shows one of them.

       The green is far enough round the wheel from the diaper teal to be told
       apart at arm's length in a dark room, which is the only test that
       matters: these two cards sit in the same grid. */
    --lt-temperature:   #DC4C4C;
    --lt-growth:        #57A773;
    --lt-temperature-s: rgba(220, 76, 76, 0.12);
    --lt-growth-s:      rgba(87, 167, 115, 0.14);

    --dk-temperature:   #E8706F;
    --dk-growth:        #68B382;
    --dk-temperature-s: rgba(232, 112, 111, 0.16);
    --dk-growth-s:      rgba(104, 179, 130, 0.18);

    /* Blue, added with the vaccine record.

       It is a plain blue on purpose. Everything else in the app's warm end of
       the palette already means something — amber is a feed, rose is a dose,
       red is a fever — and a vaccine record must not borrow any of those. In
       particular it must not be red or amber: this app records what happened
       and has no opinion about it, and a screen full of amber rows would be an
       opinion whatever the words on it said.

       Far enough round from the sleep indigo and the diaper teal to be told
       from either at arm's length, which is the only test that matters, and the
       dark variant is the same hue lifted and desaturated like the rest. */
    --lt-vaccine:       #4A90D9;
    --lt-vaccine-s:     rgba(74, 144, 217, 0.13);

    --dk-vaccine:       #609BD6;
    --dk-vaccine-s:     rgba(96, 155, 214, 0.18);

    /* Cyan, added with the bath tracker. It reads as water, which is the
       whole of the reasoning — this is the one card whose subject has an
       obvious colour, and spending it on anything else would be perverse.

       It sits between two accents it has to be told from at a glance, and both
       gaps were the deciding constraint rather than an afterthought. The diaper
       teal (#3FBF9F) is 26° round the wheel and green-leaning where this is
       blue-leaning; the vaccine blue (#4A90D9) is 20° the other way and much
       deeper. The diaper gap is the one that matters, because those two cards
       sit in the same grid — the vaccine colour has no card and is only ever
       seen on its own screen.

       Dark is the same hue lifted and desaturated like every other accent
       here: full-chroma cyan on #101018 glares exactly the way the amber
       does. */
    --lt-bath:          #2AA9C4;
    --lt-bath-s:        rgba(42, 169, 196, 0.13);

    --dk-bath:          #51B2C8;
    --dk-bath-s:        rgba(81, 178, 200, 0.18);

    /* Plum, added with the contraction timer — the ninth accent, and the first
       one that is about the mother rather than the baby, which is exactly why
       it is a colour nothing else in the app has ever used. Two neighbours
       matter: the sleep indigo (#6C6BE8, ~40° away and much bluer) because the
       labor banner and the sleep card can share a 3am screen, and the medicine
       rose (#EC6A8C, ~50° the other way and much warmer) because both lean
       red-ish. Plum sits between them and reads as neither at arm's length.

       It is deliberately NOT red and never may be: red is the fever
       emergency's, and a contraction being timed is not an emergency state —
       an app that paints the timer red is an app announcing something it has
       no business announcing.

       Dark is the same hue lifted and desaturated like every other accent
       here; full-chroma plum on #101018 glares the way the amber does. */
    --lt-labor:         #A15CC4;
    --lt-labor-s:       rgba(161, 92, 196, 0.13);

    --dk-labor:         #B37ECE;
    --dk-labor-s:       rgba(179, 126, 206, 0.18);

    /* The fill behind the timer button while a contraction is being timed —
       one value in both themes, for the same reason --alert is: it is a solid
       slab carrying white type, and the slab has to hold in light and in dark
       alike. Neither theme's accent does — the light plum carries white at
       4.3:1 and the lifted dark one at barely 3:1 — so the fill gets its own
       deeper plum, which carries white at 5.5:1 and still reads as the same
       colour family as the accent it stands in for. Plum and never red: red
       is the fever emergency's, and a contraction being timed is not an
       emergency state. */
    --labor-fill:       #8F4BB2;
    --labor-fill-ink:   #FFFFFF;

    /* ---- Palette literals -----------------------------------------------
       The boy and girl palettes (§2b). A palette is a second axis on top of
       light/dark: it re-points --brand and lays a tint over the five grounds,
       and it touches nothing else. It is never a third theme.

       The tints are deliberately almost invisible one at a time — a couple of
       points of hue on an off-white, a couple on a near-black. Held next to
       the neutral screen they read as a blue-ish or a pink-ish app; held on
       their own at 3am they read as the app. Anything stronger turns a tool
       into a nursery wall, and the ground of every screen is the last place
       to spend contrast.

       Every value below was measured rather than picked, against the neutral
       set as the bar (see §2b for the numbers). The lightness of each ground
       is within a point of its neutral twin, so --ink, --ink-soft and
       --ink-dim carry exactly as far as they always did; the hue is all that
       moved. The two --brand values are darker than a first pick would be
       because white type sits on them in .btn--primary: the pink especially
       had to come down from a bright rose, which fails, to one that holds. */
    --lt-boy-brand:     #0A66CC;
    --lt-boy-brand-s:   rgba(10, 102, 204, 0.16);
    --lt-boy-paper:     #E6F0FC;
    --lt-boy-paper-2:   #D5E5F9;
    --lt-boy-surface:   #F4F9FF;
    --lt-boy-surface-2: #E0EBFA;
    --lt-boy-header-bg: rgba(230, 240, 252, 0.85);

    --dk-boy-brand:     #5AA8F5;
    --dk-boy-brand-s:   rgba(90, 168, 245, 0.22);
    --dk-boy-paper:     #081428;
    --dk-boy-paper-2:   #0C1B33;
    --dk-boy-surface:   #11223D;
    --dk-boy-surface-2: #1A2F4F;
    --dk-boy-header-bg: rgba(8, 20, 40, 0.82);

    --lt-girl-brand:     #C93266;
    --lt-girl-brand-s:   rgba(201, 50, 102, 0.18);
    --lt-girl-paper:     #FFE8F0;
    --lt-girl-paper-2:   #FFD6E6;
    --lt-girl-surface:   #FFF6FA;
    --lt-girl-surface-2: #FFE3EE;
    --lt-girl-header-bg: rgba(255, 232, 240, 0.86);

    --dk-girl-brand:     #FF9DBF;
    --dk-girl-brand-s:   rgba(255, 157, 191, 0.24);
    --dk-girl-paper:     #2E101B;
    --dk-girl-paper-2:   #3A1423;
    --dk-girl-surface:   #431827;
    --dk-girl-surface-2: #56202F;
    --dk-girl-header-bg: rgba(46, 16, 27, 0.84);

    /* ---- Status ---------------------------------------------------------
       Warm, not surgical. Nothing in this app is an emergency; the reddest
       thing it ever says is "this medicine is overdue". */
    --lt-ok:         #1E9E6A;
    --lt-warn:       #D9862A;
    --lt-danger:     #DC4B4B;
    --lt-ok-s:       rgba(30, 158, 106, 0.13);
    --lt-warn-s:     rgba(217, 134, 42, 0.14);
    --lt-danger-s:   rgba(220, 75, 75, 0.12);

    --dk-ok:         #3BB584;
    --dk-warn:       #DFA351;
    --dk-danger:     #E8706F;
    --dk-ok-s:       rgba(59, 181, 132, 0.16);
    --dk-warn-s:     rgba(223, 163, 81, 0.16);
    --dk-danger-s:   rgba(232, 112, 111, 0.15);

    /* ---- The emergency fill --------------------------------------------
       One value in both themes, and the only colour in this block that is not
       remapped below. It fills the panel that says to call a pediatrician now,
       which is the single solid slab of colour in the app, and a slab carrying
       white body text has to hold 4.5:1 in light and in dark alike. Neither
       --lt-danger nor --dk-danger does: the dark one is a coral that white
       simply falls off. #C4302B carries white at 5.5:1 and still reads as a
       warning rather than as a button.

       Anything that only tints — a border, a figure, a soft background — keeps
       using --danger and stays theme-aware. This is for the fill alone. */
    --alert:         #C4302B;
    --alert-ink:     #FFFFFF;

    /* ---- The ink on a filled brand button -------------------------------
       The same question as --alert-ink above, asked about .btn--primary, and
       it has a different answer in each theme.

       White holds comfortably on all three LIGHT brands (4.6-4.9:1, measured
       with the button's own gradient laid over them — see §2b). It does not
       hold on the dark ones: they are lifted and desaturated so they do not
       glare on a near-black screen, and that lift is exactly what takes white
       type off them. A 15px 700-weight label is not WCAG "large text", so the
       bar is 4.5:1 and all three dark brands miss it — the neutral indigo by
       the same margin it always has, which makes this a shortfall the palettes
       inherited rather than one they introduced.

       So in dark the ink inverts instead. Darkening the brands would have been
       the other way out and it is the wrong one: the accent IS the palette,
       and dulling it to carry white would spend the whole point of the feature
       on a contrast bug that predates it.

       #0B0B12 rather than --dk-paper (#101018): the page ground is a hair too
       light and lands at 4.46:1 against the neutral indigo at the darkest end
       of the button's gradient. Two points down is a colour nobody can tell
       apart from it and clears the bar everywhere. */
    --lt-brand-ink:  #FFFFFF;
    --dk-brand-ink:  #0B0B12;

    /* ---- Semantic names — light by default -----------------------------
       Everything from here down uses these, never the literals above. */
    --paper:        var(--lt-paper);
    --paper-2:      var(--lt-paper-2);
    --surface:      var(--lt-surface);
    --surface-2:    var(--lt-surface-2);
    --ink:          var(--lt-ink);
    --ink-soft:     var(--lt-ink-soft);
    --ink-dim:      var(--lt-ink-dim);
    --line:         var(--lt-line);
    --line-2:       var(--lt-line-2);
    --track:        var(--lt-track);
    --skel-a:       var(--lt-skel-a);
    --skel-b:       var(--lt-skel-b);
    --toast-bg:     var(--lt-toast-bg);
    --toast-ink:    var(--lt-toast-ink);
    --scrim:        var(--lt-scrim);
    --header-bg:    var(--lt-header-bg);
    --glass:        var(--lt-glass);

    --c-feeding:    var(--lt-feeding);
    --c-diaper:     var(--lt-diaper);
    --c-sleep:      var(--lt-sleep);
    --c-medicine:   var(--lt-medicine);
    --c-feeding-s:  var(--lt-feeding-s);
    --c-diaper-s:   var(--lt-diaper-s);
    --c-sleep-s:    var(--lt-sleep-s);
    --c-medicine-s: var(--lt-medicine-s);

    --c-temperature:   var(--lt-temperature);
    --c-growth:        var(--lt-growth);
    --c-temperature-s: var(--lt-temperature-s);
    --c-growth-s:      var(--lt-growth-s);
    --c-vaccine:       var(--lt-vaccine);
    --c-vaccine-s:     var(--lt-vaccine-s);
    --c-bath:          var(--lt-bath);
    --c-bath-s:        var(--lt-bath-s);
    --c-labor:         var(--lt-labor);
    --c-labor-s:       var(--lt-labor-s);

    --ok:           var(--lt-ok);
    --warn:         var(--lt-warn);
    --danger:       var(--lt-danger);
    --ok-s:         var(--lt-ok-s);
    --warn-s:       var(--lt-warn-s);
    --danger-s:     var(--lt-danger-s);

    /* The app's own colour, used anywhere that is not about one tracker: the
       tab bar, focus rings, the sign-in card. It is the sleep indigo on
       purpose — this is a night-time app and indigo is its face.

       A SHADE DARKER THAN --c-sleep, and not var(--c-sleep) as it once was.
       White on the sleep indigo measures 4.28:1, and the primary button's label
       is 15px bold, which WCAG does not count as large text — so the default
       theme was failing the 4.5:1 bar that both the boy and girl palettes pass.
       It cannot be fixed by darkening --c-sleep itself: that is the sleep
       tracker's accent, and the whole rule about palettes is that the seven
       tracker colours never move. So the brand gets its own value, close enough
       that nothing looks different and dark enough to read. Measures 4.58:1. */
    --brand:        #6867E0;
    --brand-soft:   var(--c-sleep-s);

    /* Set here rather than in §2b, because it follows the theme and not the
       palette: all three dark brands need the same near-black and all three
       light ones the same white. */
    --brand-ink:    var(--lt-brand-ink);

    /* Components inherit --accent from a .tcard--*, .entry--* or a
       [data-tracker] ancestor (§9). Away from all three they fall back to the
       brand, which is why every use below is written var(--accent, var(--brand)). */

    /* ---- Space — 4px scale --------------------------------------------- */
    --s1: 0.25rem;
    --s2: 0.5rem;
    --s3: 0.75rem;
    --s4: 1rem;
    --s5: 1.5rem;
    --s6: 2rem;
    --s7: 3rem;

    /* The gutter. 16px on a phone is what stops a full-width card looking
       like a browser default. */
    --gutter: 1rem;

    /* ---- Radii ----------------------------------------------------------
       Big and soft. 18-22px is the range where a card stops reading as a box
       and starts reading as an object — below it the app looks like a form,
       above it like a toy. */
    --r-xs:   10px;
    --r-sm:   14px;
    --r:      18px;
    --r-lg:   22px;
    --r-xl:   26px;
    --r-pill: 999px;

    /* ---- Shadows --------------------------------------------------------
       Two stops each: a tight contact shadow that glues the card to the page,
       and a wide, very soft one that gives it height. One stop alone always
       looks either like a border or like a smudge. */
    --shadow-1:
        0 1px 2px rgba(16, 18, 34, 0.05),
        0 6px 16px -8px rgba(16, 18, 34, 0.14);
    --shadow-2:
        0 2px 5px rgba(16, 18, 34, 0.06),
        0 18px 40px -18px rgba(16, 18, 34, 0.24);
    --shadow-3:
        0 -1px 3px rgba(16, 18, 34, 0.05),
        0 -22px 60px -20px rgba(16, 18, 34, 0.32);

    /* ---- Motion ---------------------------------------------------------
       --ease-out is the workhorse: fast start, long soft landing, which is
       what makes a transition feel like a thing settling rather than a thing
       fading. --ease-spring overshoots slightly and is reserved for the sheet,
       because that is the one movement the thumb "throws". */
    --ease-out:    cubic-bezier(0.22, 1, 0.36, 1);
    --ease-spring: cubic-bezier(0.20, 1.24, 0.34, 1);
    --t-fast: 0.14s;
    --t:      0.22s;
    --t-slow: 0.38s;

    /* ---- Frame ---------------------------------------------------------- */
    --header-h: 56px;
    --tabbar-h: 58px;

    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto,
            "Helvetica Neue", Arial, sans-serif;
    --font-num: -apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui,
                sans-serif;

    /* Native controls — the datetime-local picker, scrollbars, the caret —
       are painted by the OS, and this is the only way to tell it which set to
       use. Without it a dark app opens a white date picker. */
    color-scheme: light dark;
}


/* --------------------------------------------------------------------------
   2. Themes

   Dark applies when the phone asks for it, unless the user has explicitly
   chosen Light in Settings; and always when they have explicitly chosen Dark.
   Both blocks say exactly the same thing, which is why neither contains a
   colour — a divergence between them can only ever be a missing line, not a
   wrong shade.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
    html:not([data-theme="light"]) {
        --paper:        var(--dk-paper);
        --paper-2:      var(--dk-paper-2);
        --surface:      var(--dk-surface);
        --surface-2:    var(--dk-surface-2);
        --ink:          var(--dk-ink);
        --ink-soft:     var(--dk-ink-soft);
        --ink-dim:      var(--dk-ink-dim);
        --line:         var(--dk-line);
        --line-2:       var(--dk-line-2);
        --track:        var(--dk-track);
        --skel-a:       var(--dk-skel-a);
        --skel-b:       var(--dk-skel-b);
        --toast-bg:     var(--dk-toast-bg);
        --toast-ink:    var(--dk-toast-ink);
        --scrim:        var(--dk-scrim);
        --header-bg:    var(--dk-header-bg);
        --glass:        var(--dk-glass);

        --c-feeding:    var(--dk-feeding);
        --c-diaper:     var(--dk-diaper);
        --c-sleep:      var(--dk-sleep);
        --c-medicine:   var(--dk-medicine);
        --c-feeding-s:  var(--dk-feeding-s);
        --c-diaper-s:   var(--dk-diaper-s);
        --c-sleep-s:    var(--dk-sleep-s);
        --c-medicine-s: var(--dk-medicine-s);

        --c-temperature:   var(--dk-temperature);
        --c-growth:        var(--dk-growth);
        --c-temperature-s: var(--dk-temperature-s);
        --c-growth-s:      var(--dk-growth-s);
        --c-vaccine:       var(--dk-vaccine);
        --c-vaccine-s:     var(--dk-vaccine-s);
        --c-bath:          var(--dk-bath);
        --c-bath-s:        var(--dk-bath-s);
        --c-labor:         var(--dk-labor);
        --c-labor-s:       var(--dk-labor-s);

        --ok:           var(--dk-ok);
        --warn:         var(--dk-warn);
        --danger:       var(--dk-danger);
        --ok-s:         var(--dk-ok-s);
        --warn-s:       var(--dk-warn-s);
        --danger-s:     var(--dk-danger-s);

        /* The label on a filled brand button inverts to near-black here, on
           every palette. §1 has the measurements and §8 the rule. */
        --brand-ink:    var(--dk-brand-ink);

        /* Shadows go black rather than navy, and get deeper. A navy shadow on
           a near-black page is invisible, which leaves every card looking
           like a flat patch of slightly lighter paint. */
        --shadow-1:
            0 1px 2px rgba(0, 0, 0, 0.45),
            0 8px 20px -10px rgba(0, 0, 0, 0.65);
        --shadow-2:
            0 2px 6px rgba(0, 0, 0, 0.50),
            0 22px 46px -20px rgba(0, 0, 0, 0.80);
        --shadow-3:
            0 -1px 3px rgba(0, 0, 0, 0.40),
            0 -26px 64px -22px rgba(0, 0, 0, 0.85);

        color-scheme: dark;
    }
}

html[data-theme="dark"] {
    --paper:        var(--dk-paper);
    --paper-2:      var(--dk-paper-2);
    --surface:      var(--dk-surface);
    --surface-2:    var(--dk-surface-2);
    --ink:          var(--dk-ink);
    --ink-soft:     var(--dk-ink-soft);
    --ink-dim:      var(--dk-ink-dim);
    --line:         var(--dk-line);
    --line-2:       var(--dk-line-2);
    --track:        var(--dk-track);
    --skel-a:       var(--dk-skel-a);
    --skel-b:       var(--dk-skel-b);
    --toast-bg:     var(--dk-toast-bg);
    --toast-ink:    var(--dk-toast-ink);
    --scrim:        var(--dk-scrim);
    --header-bg:    var(--dk-header-bg);
    --glass:        var(--dk-glass);

    --c-feeding:    var(--dk-feeding);
    --c-diaper:     var(--dk-diaper);
    --c-sleep:      var(--dk-sleep);
    --c-medicine:   var(--dk-medicine);
    --c-feeding-s:  var(--dk-feeding-s);
    --c-diaper-s:   var(--dk-diaper-s);
    --c-sleep-s:    var(--dk-sleep-s);
    --c-medicine-s: var(--dk-medicine-s);

    --c-temperature:   var(--dk-temperature);
    --c-growth:        var(--dk-growth);
    --c-temperature-s: var(--dk-temperature-s);
    --c-growth-s:      var(--dk-growth-s);
    --c-vaccine:       var(--dk-vaccine);
    --c-vaccine-s:     var(--dk-vaccine-s);
    --c-bath:          var(--dk-bath);
    --c-bath-s:        var(--dk-bath-s);
    --c-labor:         var(--dk-labor);
    --c-labor-s:       var(--dk-labor-s);

    --ok:           var(--dk-ok);
    --warn:         var(--dk-warn);
    --danger:       var(--dk-danger);
    --ok-s:         var(--dk-ok-s);
    --warn-s:       var(--dk-warn-s);
    --danger-s:     var(--dk-danger-s);

    --brand-ink:    var(--dk-brand-ink);

    --shadow-1:
        0 1px 2px rgba(0, 0, 0, 0.45),
        0 8px 20px -10px rgba(0, 0, 0, 0.65);
    --shadow-2:
        0 2px 6px rgba(0, 0, 0, 0.50),
        0 22px 46px -20px rgba(0, 0, 0, 0.80);
    --shadow-3:
        0 -1px 3px rgba(0, 0, 0, 0.40),
        0 -26px 64px -22px rgba(0, 0, 0, 0.85);

    color-scheme: dark;
}

/* Light chosen explicitly. The tokens are already light in :root, so this only
   has to tell the OS to stop painting native controls dark. */
html[data-theme="light"] {
    color-scheme: light;
}


/* --------------------------------------------------------------------------
   2b. Palettes

   data-palette is a second, independent axis: data-theme says how bright the
   app is, data-palette says which hue it is. Six combinations, and all six are
   written out below.

   WHAT A PALETTE MAY TOUCH
   Seven tokens: --brand, --brand-soft, and a tint on --paper, --paper-2,
   --surface, --surface-2, --header-bg. That is the whole list.

   WHAT A PALETTE MUST NEVER TOUCH — the rule to read before making it pinker
   The eight tracker accents (feeding, diaper, sleep, medicine, temperature,
   growth, vaccine, bath) and --ok / --warn / --danger are identical on all
   three palettes, and they stay that way.

   The accents are not decoration. They are how a card is identified from
   across a dark room before anybody reads its label, and a screen where
   everything is pink is a screen where nothing is findable — the feeding card
   and the temperature card would differ only in the words on them, which is
   exactly the reading nobody does at 3am. And --danger, plus the temperature
   red that lands on it, is what the fever panel is drawn in. Red has to mean
   fever on every palette; a fever panel that came out pink on the girl theme
   because it inherited the brand would be a safety bug wearing a stylesheet.

   So: a palette changes the app's own colour and the ground it sits on. It
   never changes what a colour means.

   WHY EACH PALETTE APPEARS THREE TIMES
   Same reason §2's two blocks say the same thing twice, one step worse. A
   palette's light rule is html[data-palette="x"] — one attribute, and so more
   specific than the plain html:not([data-theme="light"]) that §2 turns the app
   dark with. Written once, a palette's light grounds would therefore beat the
   dark theme, and a phone left on Auto would paint a bright pink screen at
   midnight. Every palette, including neutral, needs its own rule inside the
   prefers-color-scheme block and its own under html[data-theme="dark"], each
   carrying two attributes so it outranks the light one. The neutral blocks are
   not ceremony either: without them an explicit Neutral choice would be the
   one setting that breaks dark mode.

   CONTRAST — checked, not assumed
   Every one of the six was measured against the neutral screen as the bar:
   --ink on --paper and on --surface, --ink-soft and --ink-dim on --surface and
   --surface-2, --brand as a figure on both grounds, --brand behind the white
   type of .btn--primary (including the white the button's gradient lays over
   it), and --brand on its own --brand-soft wash, which is how a selected chip
   is drawn.

   The grounds all land within 0.05 of their neutral twin — body ink ~16.7:1 on
   paper, muted ink ~3.8:1 on surface — because the tints move hue and not
   lightness. The brand figures improve on neutral (4.6-5.2:1 against 4.1:1).

   The pink light theme was the one at risk and it is where the starting values
   changed. White on the suggested #D94E86 is 3.9:1 and fails under the button
   gradient; #C93C74 holds 4.8:1, 4.6:1 with the gradient's white on top. The
   blue moved for the same reason, #2E7FD4 (4.1:1) to #2A72BE (4.9:1). Both
   dark brands came down from their starting values as well, to sit where the
   neutral dark brand already sits under white type. The ink was never the
   thing to adjust — it is shared with the neutral theme and moving it here
   would have made three inks to maintain.

   ONE LINE ABOVE IS NO LONGER THE WHOLE STORY, and it is the line about the
   dark brands sitting "where the neutral dark brand already sits under white
   type". They do — and that place was 3.64-3.74:1, which is under the 4.5:1 a
   15px bold button label needs. All three dark brands were there, the neutral
   indigo included, since long before these palettes existed. The fix is in §8
   and it inverts the LABEL rather than the brand, for the reason given there:
   these three colours are the entire point of the feature and dulling them to
   carry white would spend it. The measured numbers, before and after, all six
   combinations, are with that rule.
   -------------------------------------------------------------------------- */

/* ---- Light ---- */
html[data-palette="neutral"] {
    --paper:        var(--lt-paper);
    --paper-2:      var(--lt-paper-2);
    --surface:      var(--lt-surface);
    --surface-2:    var(--lt-surface-2);
    --header-bg:    var(--lt-header-bg);
    /* The same shade as the :root default above, and darker than --c-sleep for
       the contrast reason explained there. Explicitly re-stated rather than
       left to inherit, because this block exists to undo boy/girl and has to
       name every token they set. */
    --brand:        #6867E0;
    --brand-soft:   var(--c-sleep-s);
}

html[data-palette="boy"] {
    --paper:        var(--lt-boy-paper);
    --paper-2:      var(--lt-boy-paper-2);
    --surface:      var(--lt-boy-surface);
    --surface-2:    var(--lt-boy-surface-2);
    --header-bg:    var(--lt-boy-header-bg);
    --brand:        var(--lt-boy-brand);
    --brand-soft:   var(--lt-boy-brand-s);
}

html[data-palette="girl"] {
    --paper:        var(--lt-girl-paper);
    --paper-2:      var(--lt-girl-paper-2);
    --surface:      var(--lt-girl-surface);
    --surface-2:    var(--lt-girl-surface-2);
    --header-bg:    var(--lt-girl-header-bg);
    --brand:        var(--lt-girl-brand);
    --brand-soft:   var(--lt-girl-brand-s);
}

/* ---- Dark by preference. The Auto path, and the one that is easy to leave
       out: it is the combination nobody sees while building in daylight. ---- */
@media (prefers-color-scheme: dark) {
    html[data-palette="neutral"]:not([data-theme="light"]) {
        --paper:        var(--dk-paper);
        --paper-2:      var(--dk-paper-2);
        --surface:      var(--dk-surface);
        --surface-2:    var(--dk-surface-2);
        --header-bg:    var(--dk-header-bg);
        --brand:        var(--c-sleep);
        --brand-soft:   var(--c-sleep-s);
    }

    html[data-palette="boy"]:not([data-theme="light"]) {
        --paper:        var(--dk-boy-paper);
        --paper-2:      var(--dk-boy-paper-2);
        --surface:      var(--dk-boy-surface);
        --surface-2:    var(--dk-boy-surface-2);
        --header-bg:    var(--dk-boy-header-bg);
        --brand:        var(--dk-boy-brand);
        --brand-soft:   var(--dk-boy-brand-s);
    }

    html[data-palette="girl"]:not([data-theme="light"]) {
        --paper:        var(--dk-girl-paper);
        --paper-2:      var(--dk-girl-paper-2);
        --surface:      var(--dk-girl-surface);
        --surface-2:    var(--dk-girl-surface-2);
        --header-bg:    var(--dk-girl-header-bg);
        --brand:        var(--dk-girl-brand);
        --brand-soft:   var(--dk-girl-brand-s);
    }
}

/* ---- Dark chosen explicitly ---- */
html[data-theme="dark"][data-palette="neutral"] {
    --paper:        var(--dk-paper);
    --paper-2:      var(--dk-paper-2);
    --surface:      var(--dk-surface);
    --surface-2:    var(--dk-surface-2);
    --header-bg:    var(--dk-header-bg);
    --brand:        var(--c-sleep);
    --brand-soft:   var(--c-sleep-s);
}

html[data-theme="dark"][data-palette="boy"] {
    --paper:        var(--dk-boy-paper);
    --paper-2:      var(--dk-boy-paper-2);
    --surface:      var(--dk-boy-surface);
    --surface-2:    var(--dk-boy-surface-2);
    --header-bg:    var(--dk-boy-header-bg);
    --brand:        var(--dk-boy-brand);
    --brand-soft:   var(--dk-boy-brand-s);
}

html[data-theme="dark"][data-palette="girl"] {
    --paper:        var(--dk-girl-paper);
    --paper-2:      var(--dk-girl-paper-2);
    --surface:      var(--dk-girl-surface);
    --surface-2:    var(--dk-girl-surface-2);
    --header-bg:    var(--dk-girl-header-bg);
    --brand:        var(--dk-girl-brand);
    --brand-soft:   var(--dk-girl-brand-s);
}


/* --------------------------------------------------------------------------
   3. Reset and base
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* The page colour goes on <html>, not only on <body>. Installed on a Home
   Screen, everything outside the page box samples this: the strip under the
   home indicator, the rubber-band area when a list is overscrolled, and the
   backdrop behind a modal sheet. Left unset, all three flash white while the
   rest of the app is near-black. */
html {
    background: var(--paper);
    min-height: 100%;
    -webkit-text-size-adjust: 100%;
    /* Kills double-tap zoom while leaving scrolling and panning alone. The
       viewport meta tag alone does not stop it in the installed app, and a
       stray double tap on a card lands the whole app at 2x with no visible
       way back. */
    touch-action: manipulation;
}

body {
    margin: 0;
    min-height: 100%;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--font);
    /* 16px floor, everywhere, forever. Below it iOS zooms the page on input
       focus and the Save button walks off the bottom of the screen. */
    font-size: 16px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
    overscroll-behavior-y: none;
}

h1, h2, h3, h4 {
    margin: 0;
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.015em;
}
h1 { font-size: 1.5rem; }
h2 { font-size: 1.1875rem; }
h3 { font-size: 1rem; }
p  { margin: 0 0 var(--s3); }

button, input, select, textarea {
    font: inherit;
    color: inherit;
}
button {
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}
button:disabled { cursor: default; }

a { color: var(--brand); text-decoration: none; }

[hidden] { display: none !important; }

/* Visible only for keyboard users. A ring drawn on every tap would be noise on
   a device with no keyboard at all. */
:focus { outline: none; }
/* No border-radius here on purpose: the outline already follows whatever
   rounding the element has, and setting one would square off a pill chip the
   moment it took focus. */
:focus-visible {
    outline: 2px solid var(--accent, var(--brand));
    outline-offset: 2px;
}

::selection {
    background: var(--brand-soft);
    color: var(--ink);
}


/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */
/* Caps the reading width without owning the gutters — a .container nested in a
   .view (which already pads) drops its own padding below, so the two never
   stack up into 32px of margin. */
.container {
    width: 100%;
    max-width: 32rem;
    margin-inline: auto;
    padding-inline: max(var(--gutter), env(safe-area-inset-left, 0px));
}

.stack { display: grid; gap: var(--s4); }

.row {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex-wrap: wrap;
}

/* Eats the slack in a .row so whatever follows it is pushed to the far end. */
.spacer { flex: 1 1 auto; min-width: 0; }

.muted { color: var(--ink-dim); }

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

/* ---- State helpers ------------------------------------------------------
   .is-on is intentionally not styled on its own: "on" means a filled chip in
   one place and a slid toggle in another, so it is defined next to each of
   them. The colour helpers below are global because they always mean the
   same thing. */
.is-hidden { display: none !important; }

.is-loading {
    pointer-events: none;
    opacity: 0.55;
    transition: opacity var(--t) ease;
}

.is-ok     { color: var(--ok); }
.is-warn   { color: var(--warn); }
.is-danger { color: var(--danger); }

/* A medicine past its due time. Colour alone is not enough on a screen glanced
   at from three feet away, so the badge behind it breathes as well. */
.is-overdue { color: var(--danger); }


/* --------------------------------------------------------------------------
   5. App header

   Sticky, translucent, and it absorbs the top safe-area inset itself. With
   viewport-fit=cover the web view runs under the clock and the notch; without
   the inset the baby's name renders behind the carrier label.

   The blur is what makes the app feel native — content scrolling under a
   frosted bar reads as depth, and it is the cheapest depth cue there is.
   -------------------------------------------------------------------------- */
.app-header {
    position: sticky;
    top: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: var(--s3);
    min-height: calc(var(--header-h) + env(safe-area-inset-top, 0px));
    padding-top: env(safe-area-inset-top, 0px);
    padding-right: max(var(--gutter), env(safe-area-inset-right, 0px));
    padding-left: max(var(--gutter), env(safe-area-inset-left, 0px));
    background: var(--header-bg);
    -webkit-backdrop-filter: saturate(180%) blur(18px);
    backdrop-filter: saturate(180%) blur(18px);
    border-bottom: 1px solid var(--line);
}

.app-header__title {
    min-width: 0;
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.015em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.app-header__sub {
    min-width: 0;
    color: var(--ink-dim);
    font-size: 0.8125rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* margin-left:auto rather than a flex-grow on the title, so the actions sit
   right whether the title and subtitle are siblings on one line or stacked
   inside a wrapper. */
.app-header__actions {
    display: flex;
    align-items: center;
    gap: var(--s1);
    margin-left: auto;
    flex: none;
}

.icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--ink-soft);
    font-size: 1.125rem;
    line-height: 1;
    transition: background-color var(--t) ease, color var(--t) ease,
                transform var(--t-fast) var(--ease-out);
}
.icon-btn:hover { background: var(--surface-2); color: var(--ink); }
.icon-btn:active { transform: scale(0.9); }

.icon-btn--badge { overflow: visible; }

.badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--r-pill);
    background: var(--danger);
    color: #fff;
    font-size: 0.6875rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* The ring is the header's own background, so the count reads as sitting on
   top of the bell rather than welded to it. */
.icon-btn--badge .badge {
    position: absolute;
    top: 4px;
    right: 3px;
    box-shadow: 0 0 0 2px var(--header-bg);
    animation: badge-pop 0.34s var(--ease-spring) both;
}


/* --------------------------------------------------------------------------
   6. Views

   #view is the scroller. Its bottom padding is the tab bar's height plus the
   home-indicator inset plus a breath, because the tab bar is fixed and the
   last entry in a long list would otherwise sit permanently underneath it.
   -------------------------------------------------------------------------- */
#view {
    display: block;
    padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--s5));
    min-height: 60vh;
}

.view {
    display: grid;
    gap: var(--s5);
    padding: var(--s4) max(var(--gutter), env(safe-area-inset-right, 0px))
             var(--s5) max(var(--gutter), env(safe-area-inset-left, 0px));
    max-width: 32rem;
    margin-inline: auto;
}

/* The view already provides the gutters; a container inside it only caps the
   width. */
.view .container { padding-inline: 0; }

/* Added by Baby.views.show() for exactly one frame's worth of animation. The
   movement is small and quick on purpose: switching tabs must not feel like
   waiting for a page. */
.view--enter {
    animation: view-in 0.32s var(--ease-out) both;
}

.view__head {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
    flex-wrap: wrap;
}

.view__title {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 1.5rem;
    font-weight: 750;
    letter-spacing: -0.02em;
}

.view__sub {
    flex-basis: 100%;
    margin: calc(var(--s1) * -1) 0 0;
    color: var(--ink-dim);
    font-size: 0.875rem;
}


/* --------------------------------------------------------------------------
   7. Tab bar

   Fixed to the bottom, because the four destinations have to be reachable by a
   thumb that is already holding the phone. It pads itself with the bottom
   inset so the labels clear the home indicator instead of being cut in half
   by it.
   -------------------------------------------------------------------------- */
.tabbar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 40;
    display: flex;
    align-items: stretch;
    height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    padding-inline: max(var(--s2), env(safe-area-inset-left, 0px));
    background: var(--header-bg);
    -webkit-backdrop-filter: saturate(180%) blur(18px);
    backdrop-filter: saturate(180%) blur(18px);
    border-top: 1px solid var(--line);
}

.tabbar__item {
    position: relative;
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-width: 0;
    min-height: 44px;
    padding: 0 2px;
    border: 0;
    border-radius: var(--r-sm);
    background: none;
    color: var(--ink-dim);
    overflow: hidden;
    transition: color var(--t) ease;
}

.tabbar__icon {
    font-size: 1.1875rem;
    line-height: 1;
    transition: transform var(--t) var(--ease-spring);
}

.tabbar__label {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

.tabbar__item--active { color: var(--brand); }

/* The icon lifts a fraction when its tab is chosen. Small enough that nobody
   notices it happening and large enough that the bar feels responsive. */
.tabbar__item--active .tabbar__icon { transform: translateY(-1px) scale(1.08); }

/* A short bar above the active tab. Drawn on the item itself rather than as a
   sliding element, because a slider needs JS to know where it is going and the
   tab bar is the one component that must never wait for anything. */
.tabbar__item--active::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    width: 22px;
    height: 3px;
    border-radius: 0 0 3px 3px;
    background: var(--brand);
    transform: translateX(-50%);
    animation: tab-mark 0.3s var(--ease-out) both;
}


/* --------------------------------------------------------------------------
   8. Buttons

   Every pressable surface in the app gets the same press: a tinted wash that
   expands from the middle under the thumb. It is drawn with a pseudo-element
   rather than a JS ripple because it has to fire on the very first touch,
   before any handler has run, on a page that may still be fetching.
   -------------------------------------------------------------------------- */
.btn,
.tcard,
.tabbar__item,
.chip,
.segmented__item,
.stepper__btn,
.entry {
    position: relative;
    overflow: hidden;
    -webkit-tap-highlight-color: transparent;
}

.btn::after,
.tcard::after,
.tabbar__item::after,
.chip::after,
.segmented__item::after,
.stepper__btn::after,
.entry::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: currentColor;
    opacity: 0;
    transform: scale(0.55);
    pointer-events: none;
    /* Slow on release, instant on press: the wash appears the moment the
       finger lands and then dissolves on its own. */
    transition: opacity 0.45s ease, transform 0.45s var(--ease-out);
}

.btn:active::after,
.tcard:active::after,
.tabbar__item:active::after,
.chip:active::after,
.segmented__item:active::after,
.stepper__btn:active::after,
.entry:active::after {
    opacity: 0.13;
    transform: scale(1);
    transition-duration: 0s;
}

/* The base button is the quiet one — used for Cancel, for "Log a past nap",
   for anything that is not the point of the screen. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    min-height: 44px;
    padding: 0 var(--s4);
    border: 1px solid var(--line-2);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    font-size: 0.9375rem;
    font-weight: 650;
    letter-spacing: -0.005em;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color var(--t) ease, border-color var(--t) ease,
                transform var(--t-fast) var(--ease-out), box-shadow var(--t) ease,
                opacity var(--t) ease;
}
.btn:hover { background: var(--surface-2); }
.btn:active { transform: scale(0.975); }
.btn:disabled { opacity: 0.45; transform: none; }
.btn:disabled::after { display: none; }

/* Primary takes the accent of whatever it is inside — amber in the feeding
   sheet, indigo in the sleep sheet — so the Save button always belongs to the
   thing being saved. The gradient is a white-to-black overlay rather than a
   second colour, which means it works for all four accents and for a
   medicine's own colour out of the database.

   THE LABEL COLOUR, AND WHY IT IS TWO TOKENS DEEP

   `var(--accent-ink, var(--brand-ink))` reads as: the ink that goes with
   whatever colour this button ended up wearing. A button inside a tracker's
   subtree gets --accent from §9 and --accent-ink with it, which is white in
   both themes and unchanged by everything below. A button anywhere else — a
   settings sheet, the sign-in card, the "See everything" row — has neither, so
   it falls through to the app's own brand and the app's own ink. The two
   fallbacks are written as one chain because they have to move together: a
   button cannot take the brand's colour and the accent's ink.

   That chain is the whole scoping mechanism for the dark-mode fix. It reaches
   --brand buttons and nothing else: not --danger (outlined, --danger on
   --danger-s), not --warn or --ok, and not the emergency fever panel, whose
   own rule in §21 is more specific than this one and still paints the button
   white-on-red.

   MEASURED, all six theme x palette combinations, 15px/700 label so the bar is
   4.5:1. Sampled at the darkest point of the button's own gradient (its bottom
   edge, 7% black), which is the worst case for dark ink and the mirror of how
   §2b sampled the top edge for white:

     LIGHT — unchanged, white ink, and all three already held:
       neutral #6C6BE8  4.1  |  boy #2A72BE  4.9  |  girl #C93C74  4.8

     DARK — white ink BEFORE this change, and every one of them failed:
       neutral #7B7AD8  3.74 |  boy #4489D2  3.65 |  girl #D9598D  3.64

     DARK — #0B0B12 ink AFTER it:
       neutral 4.62 | boy 4.73 | girl 4.73   (gradient's darkest edge)
       neutral 5.25 | boy 5.37 | girl 5.38   (flat fill, where the label sits)
       neutral 6.92 | boy 6.99 | girl 6.83   (gradient's lightest edge)

   The neutral light 4.1 is the app's original indigo and is left exactly as it
   was: it is over the bar, this change is about the dark themes, and moving a
   colour nobody reported a problem with is not a contrast fix. */
.btn--primary {
    border-color: transparent;
    background-color: var(--accent, var(--brand));
    background-image: linear-gradient(180deg,
        rgba(255, 255, 255, 0.18) 0%,
        rgba(255, 255, 255, 0.03) 48%,
        rgba(0, 0, 0, 0.07) 100%);
    color: var(--accent-ink, var(--brand-ink));
    font-weight: 700;
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.20),
        0 8px 18px -10px rgba(16, 18, 34, 0.55);
}
.btn--primary:hover { background-color: var(--accent, var(--brand)); filter: brightness(1.04); }
.btn--primary:active { filter: brightness(0.97); }

.btn--ghost {
    border-color: transparent;
    background: transparent;
    color: var(--ink-soft);
}
.btn--ghost:hover { background: var(--surface-2); color: var(--ink); }

/* Delete. Outlined rather than filled: a red slab in a sheet reads as the
   thing you are meant to press, and it is not. */
.btn--danger {
    border-color: transparent;
    background: var(--danger-s);
    color: var(--danger);
}
.btn--danger:hover { background: var(--danger-s); filter: brightness(1.06); }

.btn--block { width: 100%; }

.btn--sm {
    min-height: 36px;
    padding: 0 var(--s3);
    font-size: 0.875rem;
    border-radius: var(--r-xs);
}

.btn--icon {
    width: 44px;
    min-height: 44px;
    padding: 0;
    font-size: 1.0625rem;
}
.btn--icon.btn--sm { width: 36px; min-height: 36px; }

/* A save in flight. Drawn entirely from the button's own text colour, so it
   works on the primary fill and on a ghost, and it needs no extra element —
   which means the JavaScript only ever toggles one class. The .is-loading rule
   in §4 has already dimmed the button and made it inert. */
.btn.is-loading::before {
    content: "";
    width: 16px;
    height: 16px;
    flex: none;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}


/* --------------------------------------------------------------------------
   9. Tracker accents

   The single place any tracker colour is named. Three hooks, one meaning:
   a home card, an entry row, or a whole subtree marked with data-tracker —
   which is how a sheet or a detail view picks up its colour without the
   JavaScript ever knowing a hex value.

   Each block names three things: the colour, the wash behind it, and the ink
   that reads on top of it when something is FILLED with it — which today is
   .btn--primary and nothing else. --accent-ink is white on all eight accents
   in both themes, which is what they have always been and what the dark-mode
   brand-ink fix in §8 deliberately leaves alone. It is written out here rather
   than left to a fallback so that a button wearing a tracker's colour and a
   button wearing the app's own can never end up sharing an ink: they are
   different colours with different contrast, and §8's chain resolves both from
   whichever of these blocks it is standing in.
   -------------------------------------------------------------------------- */
.tcard--feeding,
.entry--feeding,
[data-tracker="feeding"] {
    --accent: var(--c-feeding);
    --accent-soft: var(--c-feeding-s);
    --accent-ink: #FFFFFF;
}

.tcard--diaper,
.entry--diaper,
[data-tracker="diaper"] {
    --accent: var(--c-diaper);
    --accent-soft: var(--c-diaper-s);
    --accent-ink: #FFFFFF;
}

.tcard--sleep,
.entry--sleep,
[data-tracker="sleep"] {
    --accent: var(--c-sleep);
    --accent-soft: var(--c-sleep-s);
    --accent-ink: #FFFFFF;
}

.tcard--medicine,
.entry--medicine,
[data-tracker="medicine"] {
    --accent: var(--c-medicine);
    --accent-soft: var(--c-medicine-s);
    --accent-ink: #FFFFFF;
}

/* Temperature carries the fever red. The sick banner and the sick view hang
   off the same hook — both mark themselves data-tracker="temperature" and
   inherit the colour here, rather than naming a red of their own. */
.tcard--temperature,
.entry--temperature,
[data-tracker="temperature"] {
    --accent: var(--c-temperature);
    --accent-soft: var(--c-temperature-s);
    --accent-ink: #FFFFFF;
}

.tcard--growth,
.entry--growth,
[data-tracker="growth"] {
    --accent: var(--c-growth);
    --accent-soft: var(--c-growth-s);
    --accent-ink: #FFFFFF;
}

/* Two hooks rather than three, and the missing one is the point: there is no
   .tcard--vaccine because there is no vaccine card on the home screen. That
   screen answers "when was the last feed" at 3am; a vaccine is a thing that
   happens once a month in a clinic, and giving it a tile there would push a
   tracker somebody actually needs at that hour into the second row.

   The vaccines view and its sheet mark themselves data-tracker="vaccine" and
   take the colour from here; the rows inside it use .entry--vaccine. */
.entry--vaccine,
[data-tracker="vaccine"] {
    --accent: var(--c-vaccine);
    --accent-soft: var(--c-vaccine-s);
    --accent-ink: #FFFFFF;
}

/* Bath gets all three hooks back: it has a card on the home grid, rows in the
   timeline, and a view and a sheet of its own. Nothing here is conditional on
   how often the card's number changes — a tracker that fills up twice a week
   is styled exactly like one that fills up twelve times a day. */
.tcard--bath,
.entry--bath,
[data-tracker="bath"] {
    --accent: var(--c-bath);
    --accent-soft: var(--c-bath-s);
    --accent-ink: #FFFFFF;
}

/* Plum: the contraction timer, the ninth accent and the first about the
   MOTHER. Two hooks like vaccine, and the missing one is the point again —
   there is no .tcard--labor because there is no labor card on the home grid:
   while contractions are being timed the home screen grows a banner ABOVE
   everything, and the rest of the time it shows nothing about this feature
   at all.

   The entry hook is .entry--contraction, not .entry--labor, because the rows
   are contractions — 'contraction' is the entry type the server serialises —
   while the VIEW is named 'labor'. The banner and the labor view both mark
   themselves data-tracker="labor" and take the colour from here.

   NOT OVERRIDABLE BY THE BOY/GIRL PALETTES, like the eight above it — §2b's
   whole contract is that a palette re-points --brand and tints the grounds and
   never touches a --c-* token. That contract is what this line leans on, and
   it matters more here than anywhere: the palettes are the BABY's, and the one
   screen about the mother must not turn pink or blue because of a setting
   about somebody else. */
.entry--contraction,
[data-tracker="labor"] {
    --accent: var(--c-labor);
    --accent-soft: var(--c-labor-s);
    --accent-ink: #FFFFFF;
}


/* --------------------------------------------------------------------------
   10. Home cards

   The six things this app is for. Two across, because one column means
   scrolling to find out whether the baby has been fed and this screen has to
   answer that without moving.

   SIX IS THE COUNT EVERY BREAKPOINT BELOW IS SIZED FOR, and it is a kinder
   number than the five it replaced. Six divides by one, two and three, so the
   grid is three tidy rows on a phone, two rows of three on a desk, and never
   leaves a single card sitting alone at the end of a row looking like a
   mistake. The one width that does not divide it is the 48rem tablet case at
   four across, and a second row of two is a pair rather than an orphan.
   -------------------------------------------------------------------------- */
.card-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s3);
}

/* Under 380px two columns leaves about 150px per card, which is not enough for
   "2h 14m" at the size that figure has to be. */
@media (max-width: 380px) {
    .card-grid { grid-template-columns: minmax(0, 1fr); }
}

.tcard {
    display: flex;
    flex-direction: column;
    width: 100%;
    min-height: 172px;
    padding: var(--s3) var(--s3) 0;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: inherit;
    font: inherit;
    text-align: left;
    box-shadow: var(--shadow-1);
    transition: transform var(--t) var(--ease-out), box-shadow var(--t) ease,
                border-color var(--t) ease;
}
.tcard:active { transform: scale(0.985); }

/* A wash of the tracker's colour bleeding out of the top-left corner, under
   the icon. Tinting the whole card would make four cards of four colours and
   no white space; a corner wash tells you which tracker you are looking at
   while leaving the numbers on a plain surface. */
.tcard::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(120% 86% at 0% 0%,
        var(--accent-soft) 0%, transparent 68%);
    pointer-events: none;
}

.tcard__top {
    display: flex;
    align-items: center;
    gap: var(--s2);
    position: relative;
}

.tcard__badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: 13px;
    background: var(--accent-soft);
    color: var(--accent, var(--brand));
    font-size: 1.0625rem;
    line-height: 1;
}

.tcard__name {
    min-width: 0;
    font-size: 0.9375rem;
    font-weight: 650;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The one number somebody actually came to read. It wraps rather than
   clipping: on a 320px screen "5h 02m" and "ago" do not fit on one line at any
   size the figure deserves, and a half-eaten "ag" would look like a bug. */
.tcard__since {
    position: relative;
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0 3px;
    margin-top: var(--s3);
    min-height: 2.25rem;
}

/* Tabular figures so a ticking "2h 09m" does not jitter sideways once a
   second, and a transition on colour and transform so a value that changes
   under a re-render settles into place instead of snapping.

   Held at 2rem rather than made fluid. Fluid type here only shrinks the one
   figure the whole screen exists to show, and the wrap above already handles
   the narrowest case gracefully — a big number with "ago" tucked under it
   reads deliberately, a 24px number does not. */
.tcard__since-num {
    font-family: var(--font-num);
    font-size: 2rem;
    font-weight: 750;
    line-height: 1;
    letter-spacing: -0.035em;
    font-variant-numeric: tabular-nums;
    transition: color var(--t-slow) ease, transform var(--t-slow) var(--ease-out);
}

.tcard__since-unit {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--ink-dim);
}

.tcard__last {
    position: relative;
    margin-top: 2px;
    color: var(--ink-dim);
    font-size: 0.75rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* margin-top:auto pins today's summary to the bottom of the card whatever the
   lines above it come to, so the four cards line up as a grid rather than as
   four independently sized boxes. */
.tcard__today {
    position: relative;
    margin-top: auto;
    padding: var(--s2) 0;
    color: var(--ink-soft);
    font-size: 0.8125rem;
    font-weight: 550;
    line-height: 1.35;
}

.tcard__foot {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    margin-inline: calc(var(--s3) * -1);
    padding: 0 var(--s2) 0 var(--s3);
    border-top: 1px solid var(--line);
}

.tcard__foot-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s1);
    min-height: 40px;
    padding: 0 var(--s2);
    border: 0;
    border-radius: var(--r-xs);
    background: none;
    color: var(--ink-dim);
    font-size: 0.75rem;
    font-weight: 650;
    transition: color var(--t) ease;
}
.tcard__foot-btn:hover { color: var(--accent, var(--brand)); }

/* Sleep, in progress. The card takes the accent as its border and keeps a slow
   breath going in the wash behind it — this is the only state in the app that
   is happening right now rather than having happened. */
.tcard--active {
    border-color: var(--accent, var(--brand));
    box-shadow: var(--shadow-1), 0 0 0 3px var(--accent-soft);
}
.tcard--active::before { animation: breathe 4.4s ease-in-out infinite; }
.tcard--active .tcard__since-num { color: var(--accent, var(--brand)); }
.tcard--active .tcard__badge { background: var(--accent, var(--brand)); color: #fff; }

/* A card whose latest reading has been flagged — .is-warn for a fever,
   .is-danger for a level that says call now.

   The card is a summary and not the guidance: it says "look at this", and the
   panel that opens behind it says what to do about it. So it takes a coloured
   edge and a coloured figure and stops there, and unlike the sleeping card
   nothing on it moves. A home screen with a red card pulsing on it is an
   alarm, and an alarm on a screen somebody opens forty times a day is a thing
   they learn to stop seeing. */
.tcard.is-warn,
.tcard.is-danger {
    /* .is-warn and .is-danger in §4 set colour on whatever they land on. Left
       alone on a card that repaints the tracker's name along with its figure,
       which is a lot of red for a line that only says "Temperature". */
    color: inherit;
}

.tcard.is-warn {
    border-color: var(--warn);
    box-shadow: var(--shadow-1), 0 0 0 3px var(--warn-s);
}
.tcard.is-warn .tcard__badge { background: var(--warn-s); color: var(--warn); }
.tcard.is-warn .tcard__since-num,
.tcard.is-warn .tcard__today { color: var(--warn); }

.tcard.is-danger {
    border-color: var(--danger);
    box-shadow: var(--shadow-1), 0 0 0 3px var(--danger-s);
}
/* The only filled badge in the grid, in the same red as the panel it belongs
   to — and --alert rather than --danger because a white glyph has to survive
   it in the dark theme as well as the light one. */
.tcard.is-danger .tcard__badge { background: var(--alert); color: var(--alert-ink); }
.tcard.is-danger .tcard__since-num,
.tcard.is-danger .tcard__today { color: var(--danger); }

/* Sits next to "Sleeping" wherever that word appears — the card, the Right Now
   strip, the sleep view's header. */
.pulse-dot {
    position: relative;
    display: inline-block;
    width: 8px;
    height: 8px;
    flex: none;
    border-radius: 50%;
    background: var(--accent, var(--brand));
    vertical-align: 1px;
}
.pulse-dot::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: inherit;
    animation: pulse 1.9s ease-out infinite;
}

/* Entrance. The cards arrive one after another rather than together, which is
   what makes a first paint feel authored instead of merely fast. nth-child
   delays, so no JavaScript is involved and the animation survives a re-render
   of the grid. */
.card-grid .tcard { animation: card-in 0.46s var(--ease-out) both; }
.card-grid .tcard:nth-child(1) { animation-delay: 0.02s; }
.card-grid .tcard:nth-child(2) { animation-delay: 0.08s; }
.card-grid .tcard:nth-child(3) { animation-delay: 0.14s; }
.card-grid .tcard:nth-child(4) { animation-delay: 0.20s; }
.card-grid .tcard:nth-child(5) { animation-delay: 0.26s; }
.card-grid .tcard:nth-child(6) { animation-delay: 0.32s; }


/* --------------------------------------------------------------------------
   11. Stats, bars, rings, meters
   -------------------------------------------------------------------------- */
/* Flex rather than grid, and the reason is the last row. A day of feeds has
   four tiles and a day of diapers has five; a grid of fixed tracks leaves the
   fifth stranded in a column with a hole beside it, while a flex row lets it
   grow into the space and read as a summary line. Nobody has to tell either
   layout how wide the screen is. */
.stats {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
}

.stat {
    display: grid;
    gap: 2px;
    flex: 1 1 4.5rem;
    padding: var(--s3);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    min-width: 0;
}

.stat__num {
    font-family: var(--font-num);
    font-size: 1.375rem;
    font-weight: 750;
    line-height: 1.1;
    letter-spacing: -0.03em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    transition: color var(--t-slow) ease;
}

/* THIS LABEL IS NEVER TRUNCATED. It is the only thing on the tile that says
   which figure the number above it is, and on the contraction row two of them
   differ only in their opening word: "Apart · last hour" against "Lasting ·
   last hour", and in Spanish "Entre una y otra · última hora" against
   "Duración · última hora". Clipped to the width of the tile those become the
   same string — "APART · LAST H…" and "LASTING · LAST…" — and the figure a
   labouring woman reads down the phone to a midwife becomes a coin toss.

   So it wraps instead, and a taller tile is always the better trade. Nothing
   here caps the line count: a longer translation, a narrower phone or a bigger
   accessibility size makes the tile grow, never the words disappear.

   `balance` evens the lines out rather than leaving one word stranded on the
   last one — "ENTRE UNA / Y OTRA · / ÚLTIMA HORA" instead of "ENTRE UNA Y /
   OTRA · / ÚLTIMA HORA". Engines without it simply wrap normally.

   `anywhere` is the last resort, for a word wider than the whole tile. The
   tiles are min-width:0 and so never grow to fit one, and a word that hangs
   out of its tile is painted OVER by the next tile's background — which is
   the ellipsis again by another route. Breaking inside the word keeps every
   letter on screen. It only ever fires past the sizes measured below. */
.stat__label {
    color: var(--ink-dim);
    font-size: 0.6875rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    overflow-wrap: anywhere;
    text-wrap: balance;
}

.stat.is-ok     .stat__num { color: var(--ok); }
.stat.is-warn   .stat__num { color: var(--warn); }
.stat.is-danger .stat__num { color: var(--danger); }

/* Under 25.5rem, two tiles to a row instead of three — and both numbers in
   here were measured on the real tiles in both engines, not chosen.

   THE FIGURE IS THE HALF THAT CANNOT WRAP. The label above wraps on purpose
   and a taller tile is the right trade for it; a FIGURE on two lines is a
   different failure, because the row it is in changes height the moment an em
   dash becomes a value. On the contraction screen that row is under a thumb
   during labour, refreshing every few seconds as the numbers fill in, and it
   jumped 24.19px each time — the tile grew a line the instant "—" became
   "12m 45s".

   Three across leave each tile (width - 32px of gutters - 16px of gaps) / 3,
   less var(--s3) either side. That is 85px of figure at 375px, 90px at 390px
   and 91px at 393px. What has to fit in it, on one line, measured at
   1.375rem/750 with tabular figures:

     94.27px  "12 lb 4 oz" and "+3 lb 2 oz", the growth weights
     90.52px  "10h 30m" / "14h 20m", the sleep and nursing totals
     89.56px  "1180 mL", a day's bottles
     89.03px  "12m 45s", start-to-start at an early-labour interval
     81.72px  "101.2°F", the widest figure with no space in it to wrap at

   So three across does not fit the widest figure on ANY phone: the first
   viewport at which it does is 403px, where the box finally reaches 94.33px.
   375, 390 and 393 are the three widths nearly every phone in use is one of,
   and all three wrap. This is why the breakpoint is not at the 375px reference
   width but above every phone — 25.5rem (408px) clears the measured 403px with
   a little room, and the large phones past it (412px, 430px) get boxes of
   97.33px and 103.33px, which fit.

   8rem is what makes it two: 3 x 128 + 16 = 400 does not fit the 376px row an
   408px phone has, so the third tile wraps; 2 x 128 + 8 = 264 fits the 288px
   row a 320px phone has, so it never collapses to one. Two across hands the
   figure 143.5px at 375px against the 94.27px it needs.

   This is a whole-app change and was checked as one: feeding, diapers, sleep,
   temperature, growth, baths, medicine and contractions, both languages, both
   engines, 320px and 375px — no label truncated, no figure wrapped, no row
   that changes height between an em dash and a value. */
@media (max-width: 25.5rem) {
    .stat { flex-basis: 8rem; }
}

/* ---- 14-day bars --------------------------------------------------------
   Pure CSS: the JavaScript sets each bar's height as a percentage and nothing
   else. No chart library for fourteen rectangles. */
.bars {
    display: flex;
    align-items: stretch;
    gap: 5px;
    height: 132px;
    padding: var(--s2) 0 0;
}

.bars__col {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    gap: 5px;
    min-width: 0;
    border: 0;
    background: none;
    padding: 0;
    color: inherit;
}

.bars__bar {
    width: 100%;
    max-width: 20px;
    /* A day with nothing logged still draws a sliver. An empty column with no
       bar at all looks like the chart failed rather than like a quiet day. */
    min-height: 3px;
    border-radius: 6px 6px 3px 3px;
    background: linear-gradient(180deg,
        var(--accent, var(--brand)) 0%,
        var(--accent-soft) 190%);
    transform-origin: bottom center;
    animation: bar-grow 0.5s var(--ease-out) both;
    transition: filter var(--t) ease, height var(--t-slow) var(--ease-out);
}

/* Fourteen columns, fourteen small delays: the chart wipes in from the left
   like something being drawn. */
.bars__col:nth-child(1)  .bars__bar { animation-delay: 0.00s; }
.bars__col:nth-child(2)  .bars__bar { animation-delay: 0.03s; }
.bars__col:nth-child(3)  .bars__bar { animation-delay: 0.06s; }
.bars__col:nth-child(4)  .bars__bar { animation-delay: 0.09s; }
.bars__col:nth-child(5)  .bars__bar { animation-delay: 0.12s; }
.bars__col:nth-child(6)  .bars__bar { animation-delay: 0.15s; }
.bars__col:nth-child(7)  .bars__bar { animation-delay: 0.18s; }
.bars__col:nth-child(8)  .bars__bar { animation-delay: 0.21s; }
.bars__col:nth-child(9)  .bars__bar { animation-delay: 0.24s; }
.bars__col:nth-child(10) .bars__bar { animation-delay: 0.27s; }
.bars__col:nth-child(11) .bars__bar { animation-delay: 0.30s; }
.bars__col:nth-child(12) .bars__bar { animation-delay: 0.33s; }
.bars__col:nth-child(13) .bars__bar { animation-delay: 0.36s; }
.bars__col:nth-child(14) .bars__bar { animation-delay: 0.39s; }

.bars__label {
    color: var(--ink-dim);
    font-size: 0.625rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
}

/* The day being shown below the chart. */
.bars__col.is-on .bars__bar { box-shadow: 0 0 0 2px var(--accent-soft); }
.bars__col.is-on .bars__label { color: var(--accent, var(--brand)); }

/* ---- Countdown ring -----------------------------------------------------
   A conic gradient with a hole punched in it by a pseudo-element, which is
   cheaper and sharper than an SVG arc and needs no viewBox arithmetic.

   --pct is registered as a real number so the fill can be *transitioned*.
   Without the registration the browser treats it as an opaque string and the
   arc snaps from one second's value to the next; with it, the countdown
   sweeps. Nothing else depends on the registration, so an engine that ignores
   @property still draws a correct ring.

   The fill is deliberately driven by a transition and not by a keyframe. A
   keyframe animation holding the property with fill-mode forwards wins over
   every later change to it, which would freeze the ring at whatever it was
   worth on first paint — and this ring is redrawn once a second by the
   ticker. The entrance below is therefore a plain scale-and-fade. */
@property --pct {
    syntax: "<number>";
    inherits: true;
    initial-value: 0;
}

.ring {
    --pct: 0;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    flex: none;
    border-radius: 50%;
    background: conic-gradient(
        var(--accent, var(--brand)) calc(var(--pct) * 1%),
        var(--track) 0);
    transition: --pct 0.9s var(--ease-out), background-color var(--t) ease;
    animation: ring-in 0.42s var(--ease-out) both;
}

.ring::before {
    content: "";
    position: absolute;
    inset: 4px;
    border-radius: 50%;
    background: var(--surface);
}

.ring__value {
    position: relative;
    font-size: 0.6875rem;
    font-weight: 750;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    text-align: center;
}

/* Full circle and red, with a slow flash. A dose that is late is the only
   thing in this app allowed to ask for attention. */
.ring.is-overdue {
    background: conic-gradient(var(--danger) 100%, var(--danger) 0);
    animation: overdue-flash 2.4s ease-in-out infinite;
}
.ring.is-overdue .ring__value { color: var(--danger); }

/* ---- Meter --------------------------------------------------------------
   Doses used out of the daily maximum. */
.meter {
    height: 8px;
    border-radius: var(--r-pill);
    background: var(--track);
    overflow: hidden;
}

.meter__fill {
    height: 100%;
    width: 0;
    border-radius: inherit;
    background: var(--accent, var(--brand));
    transition: width 0.6s var(--ease-out), background-color var(--t) ease;
}
.meter__fill.is-warn   { background: var(--warn); }
.meter__fill.is-danger { background: var(--danger); }


/* --------------------------------------------------------------------------
   12. Entries, separators, empty states

   .entry-list is the app's one grouped surface: the timeline uses it, so do
   the activity log, the users list and every block in Settings. One shape for
   "rows that belong together" means somebody who has learnt to tap a row on
   the home screen already knows what a row does in Settings.
   -------------------------------------------------------------------------- */
.entry-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    /* Clips the rows' corners and the press wash to the list's own rounding. */
    overflow: hidden;
}

.entry {
    display: flex;
    align-items: center;
    gap: var(--s3);
    width: 100%;
    min-height: 62px;
    padding: var(--s3) var(--s3);
    border: 0;
    border-top: 1px solid var(--line);
    border-radius: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    animation: row-in 0.34s var(--ease-out) both;
}
.entry:first-child { border-top: 0; }

/* Only the first handful are worth staggering. Past that the delays add up to
   a list that is still assembling itself when somebody is already scrolling. */
.entry:nth-child(1) { animation-delay: 0.00s; }
.entry:nth-child(2) { animation-delay: 0.03s; }
.entry:nth-child(3) { animation-delay: 0.06s; }
.entry:nth-child(4) { animation-delay: 0.09s; }
.entry:nth-child(5) { animation-delay: 0.12s; }
.entry:nth-child(6) { animation-delay: 0.15s; }
.entry:nth-child(7) { animation-delay: 0.18s; }
.entry:nth-child(8) { animation-delay: 0.21s; }

.entry__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: 12px;
    background: var(--accent-soft, var(--surface-2));
    color: var(--accent, var(--ink-soft));
    font-size: 0.9375rem;
    line-height: 1;
}

.entry__main {
    flex: 1 1 auto;
    min-width: 0;
    display: grid;
    gap: 1px;
}

.entry__title {
    font-size: 0.9375rem;
    font-weight: 650;
    letter-spacing: -0.01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.entry__detail {
    color: var(--ink-soft);
    font-size: 0.8125rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Who logged it and when. The quietest line in the row — useful when you go
   looking for it, invisible when you are not. */
.entry__meta {
    color: var(--ink-dim);
    font-size: 0.75rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.entry__value {
    flex: none;
    margin-left: auto;
    color: var(--accent, var(--ink));
    font-family: var(--font-num);
    font-size: 0.9375rem;
    font-weight: 750;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* The kebab. Sits outside .entry__main so a long title never pushes it off the
   row, and keeps a full 44px target even though the glyph is tiny. */
.entry__menu {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex: none;
    margin-right: calc(var(--s2) * -1);
    border: 0;
    border-radius: var(--r-xs);
    background: none;
    color: var(--ink-dim);
    font-size: 0.9375rem;
    transition: background-color var(--t) ease, color var(--t) ease;
}
.entry__menu:hover { background: var(--surface-2); color: var(--ink); }
.entry__menu:active { transform: scale(0.9); }

.entry.is-overdue .entry__icon { background: var(--danger-s); color: var(--danger); }
.entry.is-overdue .entry__value { color: var(--danger); }

/* A flagged reading in the sick view's timeline, where a row's colour is how
   the eye finds the worst night in three days of them.

   Only the figure changes, and the class goes on the figure rather than on the
   row: .entry.is-danger is already spoken for — the activity log marks a
   deleted entry with it — and a row-level rule here would repaint history.
   Written at two classes so it also outranks .entry__value's own colour, which
   the bare §4 helper would lose to. */
.entry__value.is-warn   { color: var(--warn); }
.entry__value.is-danger { color: var(--danger); }

/* ---- Day separator ------------------------------------------------------
   Sticks under the header while its day scrolls past, so a long history never
   leaves you wondering which day you are looking at. */
.day-sep {
    position: sticky;
    top: calc(var(--header-h) + env(safe-area-inset-top, 0px));
    z-index: 5;
    display: flex;
    align-items: center;
    gap: var(--s2);
    width: max-content;
    max-width: 100%;
    margin: var(--s3) 0 calc(var(--s2) * -1);
    padding: 5px 11px;
    border-radius: var(--r-pill);
    background: var(--glass);
    -webkit-backdrop-filter: blur(14px) saturate(180%);
    backdrop-filter: blur(14px) saturate(180%);
    color: var(--ink-dim);
    font-size: 0.6875rem;
    font-weight: 750;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* ---- Nothing here yet ---------------------------------------------------
   An empty screen is the first thing a new family sees, so it gets a shape of
   its own rather than a blank panel. */
.empty {
    display: grid;
    justify-items: center;
    gap: var(--s3);
    padding: var(--s6) var(--s4);
    text-align: center;
    animation: view-in 0.4s var(--ease-out) both;
}

.empty__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    height: 64px;
    border-radius: 22px;
    background: var(--accent-soft, var(--surface-2));
    color: var(--accent, var(--ink-dim));
    font-size: 1.5rem;
}

.empty__text {
    max-width: 22rem;
    color: var(--ink-dim);
    font-size: 0.9375rem;
    line-height: 1.5;
}

.load-more {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    width: 100%;
    min-height: 46px;
    padding: 0 var(--s4);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    color: var(--ink-soft);
    font-size: 0.875rem;
    font-weight: 650;
    transition: background-color var(--t) ease, color var(--t) ease;
}
.load-more:hover { background: var(--surface); color: var(--ink); }
.load-more:active { transform: scale(0.99); }


/* --------------------------------------------------------------------------
   13. Forms and controls

   These are filled in one-handed, in the dark, by somebody who wants to be
   done. Everything is oversized, nothing is smaller than 16px, and the
   controls that matter most (type, amount, when) are chips and steppers rather
   than text fields — because a keyboard at 3am is a punishment.
   -------------------------------------------------------------------------- */
/* align-content:start keeps a label pinned to its input when this field sits
   in a row beside a taller one. */
.field {
    display: grid;
    gap: 6px;
    align-content: start;
    min-width: 0;
}

.field__label {
    color: var(--ink-soft);
    font-size: 0.8125rem;
    font-weight: 650;
    letter-spacing: 0.005em;
}

.field__hint {
    color: var(--ink-dim);
    font-size: 0.75rem;
    line-height: 1.4;
}

.field__error {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--danger);
    font-size: 0.75rem;
    font-weight: 600;
    animation: nudge 0.34s ease both;
}

.input,
.textarea,
.select {
    display: block;
    width: 100%;
    min-height: 48px;
    padding: 12px 14px;
    border: 1px solid var(--line-2);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--ink);
    /* 16px, never less. iOS zooms the viewport on focus below it. */
    font-size: 16px;
    line-height: 1.35;
    -webkit-appearance: none;
    appearance: none;
    transition: border-color var(--t) ease, box-shadow var(--t) ease,
                background-color var(--t) ease;
}

.input::placeholder,
.textarea::placeholder { color: var(--ink-dim); }

.input:focus,
.textarea:focus,
.select:focus {
    outline: none;
    border-color: var(--accent, var(--brand));
    box-shadow: 0 0 0 4px var(--accent-soft, var(--brand-soft));
}

.input.is-danger,
.textarea.is-danger,
.select.is-danger { border-color: var(--danger); }

.textarea {
    min-height: 88px;
    line-height: 1.45;
    resize: vertical;
}

/* The chevron is a data URI so it costs no request and no icon font, and it is
   drawn in currentColor's stand-in — a mid grey that reads on both themes. */
.select {
    padding-right: 40px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237C8199' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 13px center;
    background-size: 16px 16px;
}

/* Number fields are for amounts, and the spinner arrows are a 12px target next
   to a stepper with two 48px ones. */
.input[type="number"] { -moz-appearance: textfield; }
.input[type="number"]::-webkit-outer-spin-button,
.input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* iOS lays a datetime-local out as inline text, which leaves it a different
   height from every other field unless it is told to fill the box. */
.input[type="datetime-local"],
.input[type="date"],
.input[type="time"] {
    min-height: 48px;
    line-height: 22px;
}

/* ---- Segmented picker ---------------------------------------------------
   Used for the six feed types and for the three nursing sides. Tiles rather
   than a sliding track: six labels in a track are unreadable on a 320px
   screen, and the icon is what actually gets aimed at.

   auto-fit means three items make three columns and six make two rows of
   three, without the JavaScript stating a layout. */
.segmented {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(5.25rem, 1fr));
    gap: var(--s2);
}

.segmented__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    min-height: 62px;
    padding: var(--s2) 6px;
    border: 1.5px solid transparent;
    border-radius: var(--r-sm);
    background: var(--surface-2);
    color: var(--ink-soft);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.15;
    text-align: center;
    transition: background-color var(--t) ease, color var(--t) ease,
                border-color var(--t) ease, transform var(--t-fast) var(--ease-out);
}
.segmented__item i { font-size: 1.125rem; line-height: 1; }
.segmented__item:active { transform: scale(0.96); }

.segmented__item--on {
    border-color: var(--accent, var(--brand));
    background: var(--accent-soft, var(--brand-soft));
    color: var(--accent, var(--brand));
    font-weight: 700;
}

/* ---- Chips --------------------------------------------------------------
   Colour, consistency, place, medicine. Optional detail, one tap each. */
.chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    min-height: 44px;
    padding: 0 var(--s4);
    border: 1.5px solid transparent;
    border-radius: var(--r-pill);
    background: var(--surface-2);
    color: var(--ink-soft);
    font-size: 0.9375rem;
    font-weight: 600;
    white-space: nowrap;
    transition: background-color var(--t) ease, color var(--t) ease,
                border-color var(--t) ease, transform var(--t-fast) var(--ease-out);
}
.chip:active { transform: scale(0.96); }

.chip--on {
    border-color: var(--accent, var(--brand));
    background: var(--accent-soft, var(--brand-soft));
    color: var(--accent, var(--brand));
    font-weight: 700;
}

/* A chip that stands for a colour — a diaper's, or a medicine's own colour out
   of the database, which the JavaScript passes as an inline --accent. The dot
   is the swatch; the label still reads in the app's ink. */
.chip--color::before {
    content: "";
    width: 12px;
    height: 12px;
    flex: none;
    border-radius: 50%;
    background: var(--accent, var(--brand));
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}

/* The -15m / -30m / -1h row, and the quick amount chips from Settings. One
   line that scrolls sideways rather than four that wrap: these are shortcuts,
   and shortcuts must never push the Save button off the screen. */
.quick-chips {
    display: flex;
    gap: var(--s2);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Room for the focus ring, and so the last chip is not clipped flat
       against the sheet's edge. */
    padding-bottom: 2px;
}
.quick-chips::-webkit-scrollbar { display: none; }
.quick-chips .chip { flex: 0 0 auto; }

/* ---- Stepper ------------------------------------------------------------
   Amounts and doses. Two enormous targets and a number between them, which is
   the whole reason a bottle can be logged without looking at the screen. */
.stepper {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: 5px;
    border-radius: var(--r-sm);
    background: var(--surface-2);
}

.stepper__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    flex: none;
    border: 0;
    border-radius: 12px;
    background: var(--surface);
    color: var(--ink);
    font-size: 1.0625rem;
    box-shadow: var(--shadow-1);
    transition: transform var(--t-fast) var(--ease-out), opacity var(--t) ease;
}
.stepper__btn:active { transform: scale(0.9); }
.stepper__btn:disabled { opacity: 0.35; }

/* The number and its unit, centred as a pair.
   The unit is a sibling rather than part of the field's text: with "4 oz" as
   the value, every edit would start by deleting a unit nobody meant to change. */
.stepper__field {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: baseline;
    justify-content: center;
    gap: 0.3rem;
}

/* An <input>, not a label — the value is typed as often as it is stepped, for
   the 1.1 oz feed and the 100.4 reading that no run of taps can reach.
   Everything below is undoing the browser's own field chrome so it still reads
   as the big number it always was. */
.stepper__value {
    flex: 0 1 auto;
    /* Wide enough for "100.4" in tabular figures; the pair stays centred as
       digits come and go, so the number does not jump while it is typed. */
    width: 5ch;
    max-width: 100%;
    min-width: 0;
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: 0;
    border-radius: 0;
    padding: 0;
    margin: 0;
    color: inherit;
    text-align: center;
    font-family: var(--font-num);
    font-size: 1.5rem;
    font-weight: 750;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    transition: color var(--t) ease;
}

.stepper__value:focus {
    outline: none;
}

/* The ring goes on the pair, not the field, so the unit is inside it and the
   whole control reads as one focused thing. */
.stepper__field:focus-within {
    outline: 2px solid var(--accent, var(--brand));
    outline-offset: 4px;
    border-radius: var(--r-sm);
}

.stepper__suffix {
    flex: none;
    font-size: 0.95rem;
    font-weight: 650;
    color: var(--ink-soft);
}

/* ---- Paired stepper -----------------------------------------------------
   Pounds and ounces, side by side. A US scale reads "7 lb 4 oz" and every
   paediatrician asks for it that way; making somebody turn that into a decimal
   before they can enter it is how a weight gets typed in wrong.

   The modifier carries no surface of its own. Each half is an ordinary
   .stepper with its own background and its own suffix, and the pair is only
   the two-column track they sit in — so it works whether the class lands on a
   bare wrapper or on a .stepper being re-purposed, and either way the halves
   come out of Baby.form.stepper() with nothing added to it. */
.stepper--pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s2);
    padding: 0;
    background: none;
}

/* Two 48px buttons and a figure between them, in half the width of a sheet on
   a 320px screen, comes to a handful of pixels of slack. 44px is the floor
   this app holds everywhere and it buys back enough for the number. */
.stepper--pair .stepper__btn {
    width: 44px;
    height: 44px;
}

.stepper--pair .stepper__value { font-size: 1.25rem; }

/* ---- Toggle -------------------------------------------------------------
   The whole control is the label, so the words are part of the target. */
.toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    min-height: 44px;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.toggle__track {
    position: relative;
    display: inline-block;
    width: 52px;
    height: 32px;
    flex: none;
    border-radius: var(--r-pill);
    background: var(--track);
    transition: background-color var(--t) ease;
}

.toggle__knob {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(16, 18, 34, 0.35);
    transition: transform var(--t) var(--ease-spring);
}

/* The class may land on the control or on the track depending on which is
   convenient where it is built; both work. */
.toggle.is-on .toggle__track,
.toggle__track.is-on { background: var(--accent, var(--brand)); }

.toggle.is-on .toggle__knob,
.toggle__track.is-on .toggle__knob { transform: translateX(20px); }


/* --------------------------------------------------------------------------
   14. Overlays
   -------------------------------------------------------------------------- */
#modal-root { position: relative; z-index: 60; }

.backdrop {
    position: fixed;
    inset: 0;
    z-index: 60;
    background: var(--scrim);
    /* The blur is what stops a bright home screen showing through a sheet at
       3am and makes the sheet the only thing in focus. */
    -webkit-backdrop-filter: blur(10px) saturate(120%);
    backdrop-filter: blur(10px) saturate(120%);
    animation: fade-in var(--t) ease both;
}

/* ---- Bottom sheet -------------------------------------------------------
   On a phone every form is a sheet thrown up from the bottom edge, because
   that is where the thumb is. The spring easing overshoots by a hair on the
   way up — the difference between a panel appearing and a panel being tossed
   into place. */
.sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 70;
    display: flex;
    flex-direction: column;
    max-height: 88vh;
    /* dvh where it exists: on iOS Safari, 88vh is measured against the tallest
       possible viewport, so with the address bar showing the sheet's footer
       sits below the fold. */
    max-height: 88dvh;
    background: var(--surface);
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: var(--shadow-3);
    animation: sheet-up 0.42s var(--ease-spring) both;
    overflow: hidden;
}

/* The grab handle. Nothing drags it — it is there because a sheet without one
   does not read as a sheet, and people try to swipe it away either way. */
.sheet__handle {
    width: 38px;
    height: 4px;
    flex: none;
    margin: 9px auto 3px;
    border-radius: var(--r-pill);
    background: var(--line-2);
}

.sheet__head {
    display: flex;
    align-items: center;
    gap: var(--s3);
    flex: none;
    padding: var(--s2) var(--s4) var(--s3);
}

.sheet__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: 13px;
    background: var(--accent-soft, var(--brand-soft));
    color: var(--accent, var(--brand));
    font-size: 1.0625rem;
}

.sheet__title {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 1.1875rem;
    font-weight: 750;
    letter-spacing: -0.02em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sheet__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex: none;
    border: 0;
    border-radius: var(--r-pill);
    background: var(--surface-2);
    color: var(--ink-soft);
    font-size: 0.9375rem;
    transition: background-color var(--t) ease, color var(--t) ease,
                transform var(--t-fast) var(--ease-out);
}
.sheet__close:hover { color: var(--ink); }
.sheet__close:active { transform: scale(0.9); }

/* The only scroller in the sheet. Head and foot stay put so Save is always
   reachable however long the form is. */
.sheet__body {
    flex: 1 1 auto;
    display: grid;
    align-content: start;
    gap: var(--s4);
    padding: var(--s1) var(--s4) var(--s4);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

.sheet__foot {
    display: flex;
    align-items: center;
    gap: var(--s2);
    flex: none;
    padding: var(--s3) var(--s4);
    padding-bottom: calc(var(--s3) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    background: var(--surface);
}

/* Buttons share the footer evenly, except an icon-only Delete which keeps its
   square. */
.sheet__foot .btn { flex: 1 1 auto; }
.sheet__foot .btn--icon { flex: 0 0 auto; }

/* ---- Toasts -------------------------------------------------------------
   Above the tab bar, never over it: a confirmation that covers the way out of
   the screen is worse than no confirmation. */
#toast-root {
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px) + var(--s3));
    z-index: 90;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s2);
    padding-inline: var(--gutter);
    pointer-events: none;
}

.toast {
    display: flex;
    align-items: center;
    gap: var(--s3);
    width: 100%;
    max-width: 26rem;
    padding: var(--s3) var(--s4);
    border-radius: var(--r-sm);
    background: var(--toast-bg);
    color: var(--toast-ink);
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.35;
    box-shadow: var(--shadow-2);
    pointer-events: auto;
    animation: toast-in 0.34s var(--ease-out) both;
}

/* A colour bar rather than a coloured slab: the message stays as readable as
   any other text and the status is carried by 4px of edge. */
.toast::before {
    content: "";
    width: 4px;
    align-self: stretch;
    flex: none;
    margin: -2px 0;
    border-radius: var(--r-pill);
    background: currentColor;
    opacity: 0.5;
}

.toast--ok::before    { background: var(--ok); opacity: 1; }
.toast--error::before { background: var(--danger); opacity: 1; }
.toast--info::before  { background: var(--brand); opacity: 1; }


/* --------------------------------------------------------------------------
   15. Skeletons

   Shown for the fraction of a second before the first dashboard lands. They
   are shaped like the thing that is coming, so the layout does not jump when
   the numbers arrive.
   -------------------------------------------------------------------------- */
/* A highlight that travels across a flat base, rather than a three-stop
   gradient stretched to 300%: the stretched version leaves a hard edge where
   its last stop runs out, which on a card-sized block reads as a seam rather
   than a sheen. */
.skeleton {
    position: relative;
    border-radius: var(--r-xs);
    background-color: var(--skel-a);
    background-image: linear-gradient(100deg,
        transparent 26%,
        var(--skel-b) 50%,
        transparent 74%);
    background-size: 220% 100%;
    background-repeat: no-repeat;
    animation: shimmer 1.5s ease-in-out infinite;
}

.skeleton--line {
    height: 12px;
    border-radius: var(--r-pill);
}

/* Three lines of the same width read as a wall. Descending widths read as
   text. */
.skeleton--line:nth-of-type(2) { width: 78%; }
.skeleton--line:nth-of-type(3) { width: 56%; }

.skeleton--card {
    height: 172px;
    border-radius: var(--r-lg);
}


/* --------------------------------------------------------------------------
   16. Sign-in card

   index.php carries its own styling for the screen somebody sees once per
   phone. These rules are the app's version of the same pieces, so any other
   sign-in surface (a session that came back expired, the installer's
   first-admin form, and the invite onboarding form a new household sets
   itself up on — SPEC-14 §4) can be built out of .login-card / .login-mark /
   .login-title / .login-sub plus the ordinary .field, .input and .btn.
   -------------------------------------------------------------------------- */
.login-card {
    width: 100%;
    max-width: 23.75rem;
    /* Centres itself vertically without needing a class on <body>, which this
       stylesheet cannot count on being there. */
    margin: 8vh auto;
    padding: var(--s6) var(--s5) var(--s5);
    border: 1px solid var(--line);
    border-radius: var(--r-xl);
    background: var(--surface);
    box-shadow: var(--shadow-2);
    animation: card-in 0.5s var(--ease-out) both;
}

.login-card .field { margin-bottom: var(--s3); }
.login-card .btn { margin-top: var(--s3); }

.login-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 74px;
    height: 74px;
    margin: 0 auto var(--s4);
    border-radius: 24px;
    background: linear-gradient(140deg,
        var(--c-sleep) 0%, #8C63E6 52%, var(--c-medicine) 100%);
    color: #fff;
    font-size: 1.9375rem;
    box-shadow:
        inset 0 0 0 1px rgba(255, 255, 255, 0.16),
        0 16px 30px -12px rgba(108, 107, 232, 0.75);
    animation: float 4.6s ease-in-out infinite;
}

.login-title {
    margin: 0 0 var(--s5);
    text-align: center;
    font-size: 1.5625rem;
    font-weight: 700;
    letter-spacing: -0.015em;
}

/* The quiet line under the title — "Set up your family's app" on the invite
   onboarding form, or whatever a sign-in surface needs to say once. When it is
   present it takes over the title's bottom margin, so a card with one does not
   open a double gap. */
.login-title:has(+ .login-sub) { margin-bottom: var(--s1); }

.login-sub {
    margin: 0 0 var(--s5);
    text-align: center;
    font-size: 0.9375rem;
    color: var(--ink-dim);
}


/* --------------------------------------------------------------------------
   17. Wide screens

   Everything above is written for a phone held in one hand. From 768px there
   is room for the four cards in a row, and the tab bar stops being furniture
   at the bottom of a phone and becomes navigation in the header — which is
   where a pointer expects to find it, and which gives the content the whole
   height of the window back.
   -------------------------------------------------------------------------- */
/* The first thing that changes is the sheet. A bottom sheet is a shape for a
   thumb reaching up from the bottom edge of a phone; on anything wider than
   that it is a panel stuck to the floor of the window. From 640px it becomes a
   centred dialog that scales in — the same content, the same order, a
   different entrance. */
@media (min-width: 40rem) {
    .sheet {
        inset: 0;
        margin: auto;
        width: calc(100% - var(--s6));
        max-width: 30rem;
        max-height: 84vh;
        max-height: 84dvh;
        border-radius: var(--r-xl);
        box-shadow: var(--shadow-2);
        animation: dialog-in 0.3s var(--ease-out) both;
    }

    .sheet--wide { max-width: 40rem; }

    /* The handle is a "drag me down" affordance for a thumb. A dialog has a
       close button instead. */
    .sheet__handle { display: none; }
    .sheet__head { padding-top: var(--s4); }
    .sheet__foot { padding-bottom: var(--s3); }
}

/* A phone held sideways is 640-780px wide and about 380px tall. A dialog
   claiming 84% of that has room for a title and one field, so in landscape it
   takes almost the whole height and gives the padding back. */
@media (min-width: 40rem) and (max-height: 30rem) {
    .sheet { max-height: 96vh; width: calc(100% - var(--s4)); }
    .sheet__head { padding-top: var(--s3); padding-bottom: var(--s2); }
    .sheet__body { gap: var(--s3); }
}

@media (min-width: 48rem) {
    :root {
        --header-h: 64px;
        --gutter: 1.5rem;
    }

    /* Wide enough that a quarter of it is still a card and not a column:
       four cards across 46rem leaves each one narrower than it is on a phone,
       which is the wrong way round. */
    .container,
    .view { max-width: 52rem; }

    .view { gap: var(--s6); padding-top: var(--s5); }

    .card-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s4); }

    .tcard { min-height: 188px; padding: var(--s4) var(--s4) 0; }

    /* A quarter of the row is still narrower than a phone's half, so the
       figure comes down with it. */
    .tcard__since-num { font-size: 1.75rem; }
    .tcard__since-unit { font-size: 0.8125rem; }
    .tcard__foot { margin-inline: calc(var(--s4) * -1); padding-left: var(--s4); }
    .tcard:hover {
        transform: translateY(-2px);
        box-shadow: var(--shadow-2);
    }
    .tcard--active:hover { box-shadow: var(--shadow-2), 0 0 0 3px var(--accent-soft); }
    .tcard.is-warn:hover { box-shadow: var(--shadow-2), 0 0 0 3px var(--warn-s); }
    .tcard.is-danger:hover { box-shadow: var(--shadow-2), 0 0 0 3px var(--danger-s); }

    /* Lifted out of the bottom edge and dropped into the middle of the header
       band. Fixed rather than sticky because it is a sibling that follows
       <main> in the document — the only way it can appear above the content it
       switches is to be taken out of flow. */
    .tabbar {
        top: env(safe-area-inset-top, 0px);
        bottom: auto;
        left: 50%;
        right: auto;
        z-index: 45;
        width: auto;
        height: var(--header-h);
        transform: translateX(-50%);
        padding: 0;
        gap: var(--s1);
        background: none;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        border-top: 0;
    }

    /* Row instead of column, and wide enough to be aimed at with a mouse. */
    .tabbar__item {
        flex: 0 0 auto;
        flex-direction: row;
        gap: var(--s2);
        min-height: 40px;
        padding: 0 var(--s4);
        border-radius: var(--r-pill);
    }

    .tabbar__icon { font-size: 1rem; }
    .tabbar__label { font-size: 0.875rem; font-weight: 650; }

    .tabbar__item:hover { color: var(--ink); background: var(--surface-2); }

    .tabbar__item--active {
        background: var(--brand-soft);
        color: var(--brand);
    }

    /* The little bar over the tab belongs to a bottom bar. Up here the pill
       behind the label is the indicator. */
    .tabbar__item--active::before { display: none; }
    .tabbar__item--active .tabbar__icon { transform: none; }

    /* Nothing is fixed to the bottom any more, so the view stops reserving
       room for it. */
    #view { padding-bottom: var(--s6); }

    #toast-root { bottom: var(--s5); }

    .stats { grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); }
    .bars { height: 168px; gap: 8px; }
    .bars__bar { max-width: 28px; }

    .entry:hover { background: var(--surface-2); }
}

/* A desk. Give the cards a little more room, but never let the timeline's text
   column grow past what is comfortable to read in one pass. */
@media (min-width: 64rem) {
    .container,
    .view { max-width: 58rem; }

    /* THREE ACROSS, TWO ROWS — and it is the sixth card that decides it.

       The row is 880px here (58rem of view, less two 24px gutters) with a 16px
       gap, and the arithmetic is the whole argument:

         six across  → 133px a card. Too thin by a wide margin. That leaves
                       101px inside the padding, and a 30px badge and its gap
                       eat 38 of them, so "Temperature" — the longest name in
                       the app — has 63px to arrive in and does not.
         five across → 163px, which is what this was until there was a sixth
                       card to strand on a row of its own underneath it.
         three across → 283px a card, two rows of three, nothing orphaned.

       So three, and the cards come out wider here than at any other width
       rather than narrower. That is the right way round for a desk: the same
       figure, read from further away.

       The badge and name step-down that used to live here went with the
       five-across layout it was written for. At 283px there is room for the
       full-size icon and the full-size label, and shrinking them would have
       left the widest cards in the app wearing the smallest type in it. */
    .card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}


/* --------------------------------------------------------------------------
   18. Keyframes

   Nothing here animates layout — every one of these moves opacity, transform,
   background-position or a registered custom property, so they run on the
   compositor and a mid-range phone holds 60fps while a list is still being
   built behind them.
   -------------------------------------------------------------------------- */
@keyframes card-in {
    from { opacity: 0; transform: translateY(14px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

@keyframes view-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

@keyframes row-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

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

@keyframes dialog-in {
    from { opacity: 0; transform: scale(0.94) translateY(10px); }
    to   { opacity: 1; transform: none; }
}

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

@keyframes toast-in {
    from { opacity: 0; transform: translateY(16px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}

@keyframes tab-mark {
    from { opacity: 0; transform: translateX(-50%) scaleX(0.2); }
    to   { opacity: 1; transform: translateX(-50%) scaleX(1); }
}

@keyframes badge-pop {
    from { transform: scale(0.4); opacity: 0; }
    to   { transform: none; opacity: 1; }
}

/* The ring the pulsing dot throws off. It expands and dies rather than
   flashing, which reads as a heartbeat instead of an alarm. */
@keyframes pulse {
    0%   { transform: scale(1); opacity: 0.55; }
    70%  { transform: scale(3.2); opacity: 0; }
    100% { transform: scale(3.2); opacity: 0; }
}

/* The wash behind a sleeping card. Very slow and very shallow — it should be
   noticeable only if you look at it. */
@keyframes breathe {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}

/* The sick banner's edge. Slower and deeper than the sleeping card's wash,
   because this one has to be findable on a screen somebody is scrolling past,
   and it never goes out entirely — an edge that disappeared for a moment
   would be an edge that got missed. Five seconds reads as breathing; anything
   under two reads as blinking, and a blinking banner about a sick baby is
   something a parent turns off. */
@keyframes edge-breathe {
    0%, 100% { opacity: 0.75; }
    50%      { opacity: 0.28; }
}

/* The emergency panel arriving. It comes up from just under full size on the
   spring, which is the difference between a panel appearing and a panel
   landing. It runs once: the panel is loud in what it is, not in what it does. */
@keyframes alert-in {
    from { opacity: 0; transform: scale(0.92); }
    to   { opacity: 1; transform: none; }
}

/* The one thing in this app that keeps moving to be noticed: the ring around
   the sick banner when the server has called the latest reading an emergency.

   1.4 SECONDS PER CYCLE, AND THAT NUMBER IS A SAFETY LIMIT, NOT A TASTE.
   WCAG 2.3.1 forbids anything flashing more than three times a second, because
   a fast strobe can trigger a photosensitive seizure. This is 0.71 Hz — a fifth
   of that ceiling — and it is a swell rather than a flash: the ring breathes
   between bright and faint, and never reaches either end of the scale.

   It is also the ONLY thing that moves. The fill, the border and every word are
   static, so nothing anybody has to read is ever caught mid-fade, and there is
   no frame of this cycle in which the panel is not obviously and entirely red.
   §19 stops the movement and keeps all of that, which is the whole point of
   putting the emergency in the colour and reserving the animation for finding
   it on a screen somebody is scrolling past. */
@keyframes alert-pulse {
    0%, 100% { opacity: 0.92; }
    50%      { opacity: 0.30; }
}

/* The weight line drawing itself in from the left. The dash offset it clears
   is set on the element from the path's own length. */
@keyframes draw-line {
    to { stroke-dashoffset: 0; }
}

@keyframes shimmer {
    from { background-position: 150% 0; }
    to   { background-position: -50% 0; }
}

@keyframes bar-grow {
    from { transform: scaleY(0); opacity: 0; }
    to   { transform: scaleY(1); opacity: 1; }
}

@keyframes ring-in {
    from { opacity: 0; transform: scale(0.86); }
    to   { opacity: 1; transform: none; }
}

@keyframes overdue-flash {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

@keyframes nudge {
    0%, 100% { transform: translateX(0); }
    25%      { transform: translateX(-4px); }
    75%      { transform: translateX(4px); }
}

@keyframes float {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-7px); }
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* A rejected PIN. Harder and faster than --nudge, which is the "look at this"
   of a form error: this one is a head shake, it decays instead of swinging
   evenly, and it is the whole of the answer — the server will not say which of
   wrong, unknown or malformed it was, so the movement carries the "no" and the
   sentence underneath only says to try again. */
@keyframes shake {
    0%, 100% { transform: translateX(0); }
    14%      { transform: translateX(-9px); }
    28%      { transform: translateX(8px); }
    43%      { transform: translateX(-6px); }
    57%      { transform: translateX(4px); }
    72%      { transform: translateX(-2px); }
    86%      { transform: translateX(1px); }
}


/* --------------------------------------------------------------------------
   19. Reduced motion

   One rule, at the end, with the widest possible reach. Everything above is
   decoration on top of a layout that is already correct without it, so
   switching all of it off costs nothing but the flourish — and somebody who
   has asked their phone to stop moving things has usually asked for a reason
   that matters more than a flourish does.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation: none !important;
        transition: none !important;
        scroll-behavior: auto !important;
    }

    /* The blanket rule above reaches the sections below it as well — the sick
       banner's breathing edge, the guidance panel's entrance, the chart's
       fades, the visit summary's sections arriving one after another — and
       switching every one of them off costs nothing, because each decorates a
       layout that is already correct standing still. The summary in particular
       is a page of figures that has to be readable the instant it is drawn,
       which is what it does here.

       The weight line is the single exception, and it has to be repaired by
       hand. Its shape is hidden by a dash offset that only the keyframe ever
       clears, so with the animation gone the offset stands and the chart draws
       its grid, its band and a dot per measurement around a line that is not
       there. This is the static end state that animation would have reached.

       STILL THE ONLY EXCEPTION, and the WHO reference curves added under §22
       are the reason that is worth restating. They fade rather than draw, and
       a fade switched off leaves a thing that is simply visible — which is why
       there is no .chart__ref line below. Any future path that hides itself
       behind a dash offset needs one here, and the test for whether it does is
       exactly this: switch motion off and see whether the shape is still on
       the screen. */
    .chart__line {
        stroke-dashoffset: 0 !important;
    }

    /* THE EMERGENCY BANNER KEEPS EVERY BIT OF ITS EMPHASIS AND LOSES ONLY THE
       MOVEMENT, and this is the one place in the file where that distinction
       has to be made out loud.

       "Reduced motion" is a request to stop things moving. It is not a request
       to be told less, and reading it as one here would be the worst bug in the
       app: somebody who cannot tolerate animation would get a panel that says
       to go to an emergency room dressed exactly like every other panel on the
       screen. So nothing below removes emphasis. The solid red fill, the 2px
       edge, the outer glow, the white ring and every word inside the panel are
       plain static properties that the blanket rule above never touched — the
       animation was only ever moving that ring's opacity.

       Pinned to its brightest rather than left where the base rule puts it, so
       the ring reads as a heavy white border standing perfectly still instead
       of as an animation that has been paused somewhere in the middle. */
    .sick-banner--emergency::after {
        opacity: 1 !important;
    }

    /* The contraction timer's breathing ring (§28), by the same reasoning as
       the emergency ring above at a much lower temperature: the ring is how a
       glance tells "timing right now" from "idle", so it keeps its full
       strength and stands still rather than fading out with the movement. The
       fill, the elapsed figure and every word on the button are static
       properties the blanket rule never touched — the animation was only ever
       moving this ring's opacity. */
    .labor-btn.is-on::after {
        opacity: 1 !important;
    }

    /* The keypad keeps its press state, because that state is not decoration:
       it is the only confirmation that a digit landed on a screen with no
       keyboard, no caret and, half the time, no light in the room. The blanket
       rule above has already taken the easing off it, which leaves the press
       reading instantly instead of settling; the scale goes too, so nothing
       moves, and the fill under the finger carries it on its own.

       The rejected-PIN shake stops with everything else. The dots still turn
       red and still empty themselves, so the answer survives the animation. */
    .pinpad__key:active {
        transform: none;
    }

    /* §26's chevron leans a couple of pixels towards the sheet it is about to
       open. With the easing stripped by the rule above, that lean arrives as a
       jump rather than as a movement, which is the one thing this block exists
       to prevent — so it goes entirely, and the wash under the finger answers
       the press on its own.

       !important because §26 is appended below this block: the two selectors
       match at the same weight, and source order alone would hand it to the
       one further down the file. */
    .setlist__row:active .setlist__chev {
        transform: none !important;
    }

    /* §27's photo tiles lift very slightly under a finger, which is how a
       square picture with no chrome around it says it is a button at all. The
       blanket rule at the top of this block takes the easing off that lift but
       not the lift itself, so it would arrive as a jump — the one thing this
       block exists to prevent. It goes entirely; the tap still opens the photo
       full size, which is the confirmation that actually matters.

       !important for the same reason as the rule above it: §27 is appended
       below this block, the two selectors match at equal weight, and source
       order alone would hand it to the one further down the file. */
    .photos__tile:active .photos__img {
        transform: none !important;
    }

    /* The page cross-fade (View Transitions) is already skipped in core.js for
       these readers; this is the belt that braces it.

       The blanket rule at the top of this block does not reach it — these are
       pseudo-elements on the root's own transition tree and * has never matched
       them — so any path that reaches startViewTransition without going through
       withTransition() would still cross-fade the whole screen. */
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation: none !important;
    }

    /* §31's baby pager, and this one is a REPAIR rather than a switch-off — the
       same kind the weight line above needs, for the same reason.

       dashboard.js already takes the instant-swap path for these readers: it
       asks prefers-reduced-motion itself, builds no incoming pane and writes no
       transform, so the switch is a redraw and nothing crosses the screen. This
       rule is the floor under that. The blanket rule at the top of this block
       kills the transition but not the transformed position it was easing
       towards, so if any path ever did reach setShift() here — a media query
       that flipped after the answer was cached, a future caller that forgets to
       ask — the pane would be left parked one screen-width off to the side with
       nothing left to animate it back. Pinning it home means the worst case is
       a screen that swaps without moving, which is exactly what was asked for.

       The pane is never a permanent position, only a transient one, so there is
       nothing here to lose by nailing it down. */
    .baby-pager__pane {
        transform: none !important;
    }
}


/* --------------------------------------------------------------------------
   20. Standing banners

   Full width, directly above the card grid, and drawn only on the days they
   are true. Two of them use this block. They are the only components allowed
   to push the trackers down the screen, and they earn it by carrying the
   things that are true all day rather than the things that happened today.

   ONE BLOCK, TWO SUBJECTS. The rash banner is a --rash modifier rather than a
   component of its own, and the block name is the banner's SHAPE — a standing
   full-width panel above the grid — rather than its subject. A rash is not an
   illness and nothing here says it is. A second near-identical block would
   have drifted from this one within a month, and the two would then disagree
   about the padding of a panel sitting directly on top of the other.

   THE EPISODE BANNER answers the three questions somebody asks at 3am in the
   order they ask them: how long has this been going on, what was the last
   reading, and when am I meant to take the next one.

   Prominent, and deliberately not an alarm. The edge breathes on a five-second
   cycle instead of flashing and the type stays in the app's ordinary ink. This
   thing may be on screen for three days; a banner that shouts for three days
   is a banner somebody ends the episode early just to be rid of.

   That holds for every level except one. When the server flags the latest
   reading as an emergency the banner stops holding back entirely — see the
   --emergency block below, and note that the trigger is the server's own
   verdict and never a number compared anywhere in the client.

   THE RASH BANNER is the quiet one, and stays quiet: the diaper accent, no
   breathing edge, no figures, nothing to tap. It states a fact and says how to
   end it.

   Everything inside is laid out as one wrapping flex row so it survives 375px:
   the head, the note and the actions each claim a full line, and however many
   stats there are share the line between them and wrap when they run out.
   -------------------------------------------------------------------------- */
.sick-banner {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s3);
    padding: var(--s4);
    border: 1px solid var(--accent, var(--brand));
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--shadow-1);
}

/* The same corner wash the tracker cards carry, so the banner reads as one of
   the family rather than as something that has been dropped in over the top. */
.sick-banner::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(120% 100% at 0% 0%,
        var(--accent-soft) 0%, transparent 72%);
    pointer-events: none;
}

/* The breathing edge. Drawn as an inset ring rather than an outer glow: an
   outer one would have to spread past the element's box to be seen, and this
   banner sits a hair above the card grid, so a glow would bleed onto the first
   row of cards. Opacity is the only thing animating, which keeps it on the
   compositor while the timeline re-renders behind it. */
.sick-banner::after {
    content: "";
    position: absolute;
    inset: 3px;
    border-radius: calc(var(--r-lg) - 3px);
    box-shadow: inset 0 0 0 1.5px var(--accent, var(--brand));
    opacity: 0.75;
    pointer-events: none;
    animation: edge-breathe 5.2s ease-in-out infinite;
}

/* The children take position:relative for the same reason the card's do: an
   absolutely positioned ::before paints over a static sibling, and without
   this the wash would sit on top of the words. */
.sick-banner__head {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1) var(--s2);
    flex: 1 1 100%;
    min-width: 0;
    font-size: 1.0625rem;
    font-weight: 750;
    letter-spacing: -0.015em;
    line-height: 1.25;
}
.sick-banner__head i {
    flex: none;
    color: var(--accent, var(--brand));
}

/* A figure and its label, the same shape as a .stat tile at the size a banner
   can afford. The two lines are addressed as children rather than given class
   names of their own — the banner already carries three, and a fourth for
   every line of text inside it is a vocabulary nobody can hold in their head.
   The first child is the figure; anything after it is a label. */
.sick-banner__stat {
    position: relative;
    display: grid;
    gap: 1px;
    flex: 1 1 7rem;
    min-width: 0;
    /* The labels below wrap, so one tile in the row can be two lines tall
       while its neighbours are one. The banner is `align-items: center`, which
       would leave the short tiles floating in the middle of the tall one's
       line with their backgrounds ending at different heights — a row of
       ragged slabs. Stretching only the tiles keeps that row square without
       disturbing the head, the guidance panel, the note or the action row,
       every one of which is `flex: 1 1 100%` and therefore alone on its own
       flex line where `stretch` and `center` come to the same thing. */
    align-self: stretch;
    padding: var(--s2) var(--s3);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    font-size: 0.8125rem;
    color: var(--ink-soft);
}

.sick-banner__stat > :first-child {
    font-family: var(--font-num);
    font-size: 1.125rem;
    font-weight: 750;
    line-height: 1.15;
    letter-spacing: -0.02em;
    /* A recheck countdown lives in one of these and ticks once a second. */
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The next-check countdown once it has passed the time the parent set.
   Written at three simple selectors so it outranks the tile's own --ink above,
   which the bare .is-warn helper in §4 would lose to.

   Amber and never red, and there is a rule behind that rather than a taste:
   the interval is the parent's own choice, this app has no clinical opinion
   about how often a temperature should be taken, and turning somebody's own
   kitchen timer red at them would be the app pretending it does. The word in
   the tile is "Check now" for the same reason — never "overdue". */
.sick-banner__stat > :first-child.is-warn { color: var(--warn); }

/* THIS LABEL IS NEVER TRUNCATED EITHER, and for the same reason .stat__label
   is not (§20): it is the only thing on the tile that says which figure is
   above it, and this is the tile a fever is read off at 3am.

   It used to be `nowrap; overflow: hidden; text-overflow: ellipsis` and the
   cost was measured, not imagined. The last-dose caption is "<medicine> ·
   <how long ago>" and clipped in EVERY configuration — 183px of text into a
   125px tile at 375px in English, 190px into 97px at 320px in Spanish — so
   "Acetaminofén · hace 1h 30m" arrived as "ACETAMINOFÉN · HAC…" and the one
   number a pediatrician asks for down the phone, when the dose was given, was
   the half that got cut. "Volver a tomar" clipped to "VOLVER A TO…" at 320px
   beside an intact "PRÓXIMA TOMA", which is the two captions that must not be
   confusable being made confusable.

   So it wraps, exactly as .stat__label does, and the tile grows taller. The
   banner is already `flex-wrap: wrap` over `flex: 1 1 7rem` tiles, so a taller
   tile costs a few pixels of a panel that is full width anyway, and
   `align-self: stretch` above keeps the row square while it happens.

   `balance` evens the lines rather than stranding one word; `anywhere` is the
   last resort for a medicine name wider than the tile, which cannot be reached
   by shortening the word because the word is the medicine. */
.sick-banner__stat > :first-child ~ * {
    color: var(--ink-dim);
    font-size: 0.6875rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    overflow-wrap: anywhere;
    text-wrap: balance;
}

.sick-banner__actions {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    flex: 1 1 100%;
}

/* The buttons split the line evenly, and wrap to a line each rather than
   shrinking below their labels — "Log temperature" clipped to "Log temp…" on a
   narrow phone is exactly the wrong word to lose. */
.sick-banner__actions .btn {
    flex: 1 1 auto;
    min-width: 0;
}

/* A full-width sentence under the head. The rash banner's only other line, and
   the reason it is a paragraph rather than a fourth kind of stat tile: it is
   read once, it has no figure in it, and what it says is how to make the
   banner go away. */
.sick-banner__note {
    position: relative;
    flex: 1 1 100%;
    min-width: 0;
    margin: 0;
    color: var(--ink-dim);
    font-size: 0.8125rem;
    line-height: 1.45;
}


/* ---- The nappy rash -----------------------------------------------------
   A fact, and dressed as one. The diaper accent arrives on its own through
   data-tracker, so all this modifier does is take the movement off.

   The breathing edge is right for an illness and wrong for this. A rash runs
   for a week; an edge pulsing for a week stops being a signal and becomes
   something in the corner of the eye that the room has to learn to ignore —
   and the banner beside it, on the days there is one, is the thing that must
   not be ignored. It goes static, and quieter than the episode banner's,
   because a rash never outranks an illness on this screen. */
.sick-banner--rash::after {
    opacity: 0.42;
    animation: none;
}

/* ---- The rash, on the day there is also an emergency --------------------
   Two standing banners is the only arrangement on this screen that can push
   the whole card grid off a 375x667 phone, and the second of them is the one
   that has to give way: an illness outranks a rash, and on this particular
   day the panel above has told somebody to telephone a pediatrician.

   NOTHING IT SAYS CHANGES. The head, the day count, the "since", and the
   sentence that is the only way to make the banner go away are all still
   here at every width — this rule spends padding, gap and one step of head
   size, and stops there. The head drops to the caption size for the same
   reason the emergency banner's own does: it is not the headline on this
   screen, the sentence above it is.

   The negative margin is the 24px `.view` grid gap being spent back. The
   banner is a grid item, so its margin box is what sizes the row, and eight
   of those pixels are kept rather than all of them — the rash then reads as
   something stacked under the emergency panel instead of a second panel
   arriving in its own right, which is exactly the ranking above.

   Worth 39px altogether, and see the budget in the --emergency block below
   for what that buys and what it does not. */
.sick-banner--emergency + .sick-banner--rash {
    margin-top: calc(var(--s2) - var(--s5));
    gap: var(--s1) var(--s3);
    padding: var(--s2) var(--s3);
}

.sick-banner--emergency + .sick-banner--rash .sick-banner__head {
    font-size: 0.9375rem;
}


/* ---- An emergency reading -----------------------------------------------
   THE TRIGGER IS THE SERVER'S VERDICT AND NOTHING ELSE. dashboard.js reads
   cards.temperature.latest.guidance.level and puts this class on when it says
   'emergency'. No number is compared in the client and no threshold appears in
   this file: TemperatureService::guidance() decided once what a reading means,
   and this is only the colour that decision wears. The level below it —
   'fever' — keeps the calm banner above and does not pulse, because that is
   the severity the server encoded and the client does not get a second opinion.

   Everything the ordinary banner does to stay calm is undone here. The tracker
   accent gives way to --alert, the corner wash goes, the five-second breathing
   edge becomes the 1.4s pulse in §18, and the panel stops being something on
   the home screen and becomes the home screen.

   --alert rather than --danger for the same reason §21 gives: this is white
   body text on a fill, and the dark theme's danger red is a coral that white
   falls off. The two panels are the same red because they are the same
   message. */
.sick-banner--emergency {
    border-width: 2px;
    border-color: var(--alert);
    background: var(--alert);
    color: var(--alert-ink);
    box-shadow: var(--shadow-2), 0 0 0 4px var(--danger-s);

    /* Tighter than the calm banner, and it is vertical space rather than taste.
       This version carries a title, a four-line paragraph, a footnote and
       usually a call button on top of everything the ordinary one holds.

       RE-MEASURED at 375px, because the 481px this comment used to quote was a
       best case dressed up as a budget. It was taken with no method caveat and
       no rash banner underneath, and neither of those is a rare day.
       TemperatureService::guidance() appends " This was a forehead reading; a
       rectal one is what your pediatrician will ask for." to rules 1 and 2
       whenever the reading was not rectal, which is most thermometers in most
       houses; and the rash banner draws directly under this one whenever both
       states are true. All three figures below are with four stat tiles and a
       number saved, and the fold is the viewport less the 58px tab bar:

         emergency alone                481px    first card top 592, peeks 17px
         + the method caveat            526px    first card top 637
         + the rash banner under it     —        first card top 723

       So the honest line is the last one and not the first: on the worst day
       the card grid starts below the fold of a 375x667 phone. That is accepted
       here and nowhere else in the app, for two reasons. The panel has just
       told somebody to telephone a pediatrician, and the trackers are what
       gets read after that call rather than instead of it. And the one thing
       the fold rule exists to protect does not move: this banner carries its
       own "Log temperature" button, at 488-532 in the worst case above, which
       is over the fold on a 667-tall phone and on an 812-tall one. Nobody
       scrolls to log the next reading.

       WHAT WAS BOUGHT BACK IS FURNITURE ONLY. The rash banner goes compact and
       tucks up under this one (the `--emergency + --rash` rule above) for 39px,
       which is what takes an 812-tall phone from showing nothing of the grid to
       showing 31px of the first card. The padding, the gap, the caption-sized
       head and the flattened tiles below are worth about eighty more, and every
       one of those is furniture too. NOTHING EITHER PANEL SAYS IS REMOVED TO
       FIT: not a figure, not the footnote, not the method caveat, not the rash
       sentence, not a word of the wording, at any width.

       526 is this block's budget and 723 is what the home screen costs on the
       worst day. Anything added here spends both, and what it is spent on is
       how far somebody has to scroll to reach the rest of the screen. */
    gap: var(--s2) var(--s3);
    padding: var(--s3);
}

/* The corner wash is the temperature accent, and over this fill it reads as a
   dirty patch rather than as depth. */
.sick-banner--emergency::before { background: none; }

/* The pulse — see @keyframes alert-pulse in §18 for the 1.4s cycle and the
   reason it is that number. Only this ring moves; the fill, the border and
   every word underneath hold still. Two pixels of inset and a 2.5px white ring
   rather than the 1.5px hairline the calm banner wears, so that with the
   movement switched off in §19 what is left still reads as a heavy edge. */
.sick-banner--emergency::after {
    inset: 2px;
    border-radius: calc(var(--r-lg) - 2px);
    box-shadow: inset 0 0 0 2.5px var(--alert-ink);
    opacity: 0.92;
    animation: alert-pulse 1.4s ease-in-out infinite;
}

/* The head's icon takes the tracker accent, which is a red on a red here. */
.sick-banner--emergency .sick-banner__head i { color: var(--alert-ink); }

/* The episode name drops to a caption, and it is hierarchy rather than space.
   On every other day this line IS the banner's headline. Here the guidance
   title sits directly under it saying to call a pediatrician now, and two
   17px/750 lines stacked on top of each other is a panel with two headlines
   and therefore none. "Fever · day 2" is the context; the sentence below it is
   the message. It also buys back a line's worth of height, which the block
   above is already fighting for. */
.sick-banner--emergency .sick-banner__head {
    font-size: 0.9375rem;
    font-weight: 700;
}

/* "Day 3 · since Yesterday", quieted against the fill. 88% white and not the
   82% §21 uses for its footnote, and the six points are measured rather than
   picked: at 15px it is ordinary body text however heavy it is set, nowhere
   near the 18.66px that would earn the WCAG large-text exemption, so it is
   held to the full 4.5:1. Against #C4302B, 82% lands at 4.17 and 88% at 4.59. */
.sick-banner--emergency .muted { color: rgba(255, 255, 255, 0.88); }

/* THE GUIDANCE BLOCK INSIDE IT IS THE SAME COMPONENT the reading sheet shows,
   markup for markup — .guide.guide--emergency, same three children, same
   footnote — so the wording cannot drift between the two screens and the note
   cannot be dropped from one without being dropped from both. All that changes
   is that it stops supplying its own fill: the banner around it is already
   that colour, and a red slab inside a red slab is two panels where there is
   one thing to say. The type, the white title and §21's inverted button all
   come through untouched.

   TWO CLASSES ON THE RIGHT-HAND SIDE, and it is not decoration: §21 sits below
   this section, so a single `.guide` here would tie with `.guide--emergency`
   on specificity and lose the tie on source order — putting the fill, the
   inset edge and the entrance animation straight back. */
.sick-banner--emergency .guide.guide--emergency {
    position: relative;
    flex: 1 1 100%;
    min-width: 0;
    padding: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
    animation: none;
}

/* Flattened to about two thirds of their height, and they can afford it: once
   the panel is carrying the sentence that says to call, these tiles are the
   supporting facts a pediatrician asks for down the phone rather than the
   thing being read first.

   A DARKENING SCRIM, NOT A LIGHTENING ONE, and this is the one decision in the
   block that was made with a calculator rather than an eye. The obvious way to
   lift a tile off a fill is a wash of white, and here it is the wrong way
   round: white over #C4302B lands on #CC4A45, which drags the tile's own white
   figure down to 4.4:1 and its 11px uppercase label to 3.5:1 — a caption
   nobody can read, on the one panel in the app that has to be readable at
   arm's length. 16% black goes the other way to #A52824 and takes the same two
   to 7.2:1 and 5.3:1. It also simply looks right: a well pressed into the
   slab rather than a smear laid over it.

   Sized to their contents rather than to a fixed 7rem, too. The calm banner
   gives every tile the same width because a column of equal tiles is easier to
   read down; here there is no room for that, and a basis of `auto` lets three
   short figures share one line instead of clipping "Check now" to "Check n…"
   on a panel where a clipped word is the last thing anybody needs to puzzle
   over. */
.sick-banner--emergency .sick-banner__stat {
    flex: 1 1 auto;
    padding: var(--s1) var(--s2);
    background: rgba(0, 0, 0, 0.16);
    color: rgba(255, 255, 255, 0.82);
}

.sick-banner--emergency .sick-banner__stat > :first-child {
    font-size: 1rem;
    color: var(--alert-ink);
}

/* The amber that marks a check the parent is late for is meant to stand out
   against the app's own surfaces and all but disappears against this one. It
   goes white with the rest: a panel that is already the loudest thing in the
   app does not need a second signal inside it, and losing the amber loses
   nothing — the word in the tile still says "Check now". */
.sick-banner--emergency .sick-banner__stat > :first-child.is-warn {
    color: var(--alert-ink);
}

.sick-banner--emergency .sick-banner__stat > :first-child ~ * {
    color: rgba(255, 255, 255, 0.78);
}

/* EVERY BUTTON ON THE PANEL GOES TO THE OUTLINE, INCLUDING THE PRIMARY, and
   that is the whole rule — there is deliberately no --primary override under
   it.

   "Log temperature" is the banner's primary on every other day and gives it up
   here. A white slab on a red panel is the panel saying "this is the answer",
   and two of them side by side is two answers; the one that dials has to be
   the only one. This selector is (0,2,0) and `.btn--primary` is (0,1,0), so
   the outline simply wins and no second rule is needed to demote it.

   The tel: button keeps its slab without being named here either: §21's
   `.guide--emergency .btn--primary` ties this on specificity and sits below it
   in the file, so it takes the tie. That is the same source-order fact the
   nested-guide rule above has to work around, used the other way up — and it
   is why an override for the filled state would be dead code if it were
   written. When no number is saved there is no filled button at all, which is
   correct rather than a gap: the instruction on the panel is to call, and this
   app is not the thing that can do that part. */
.sick-banner--emergency .btn {
    border-color: rgba(255, 255, 255, 0.55);
    background-color: transparent;
    background-image: none;
    color: var(--alert-ink);
}
.sick-banner--emergency .btn:hover { background-color: rgba(255, 255, 255, 0.14); }


/* --------------------------------------------------------------------------
   21. Guidance panels

   The frame around the fever wording. The words are fixed, come from the
   server and are not this file's business; all that is decided here is how
   loud they are.

   Each level repoints four local names and nothing else, which is what keeps
   the four variants honest — they differ in colour and in one case in fill,
   never in what they leave out. Note in particular that the level colours do
   NOT come from --accent, even though a guidance panel always renders inside a
   data-tracker="temperature" subtree and could have it for free. A panel that
   said "worth confirming" in the same red as one that said "call now" would
   flatten the only distinction on the screen that matters.
   -------------------------------------------------------------------------- */
.guide {
    /* The default is the fever amber. A panel that somehow arrived without a
       level would then read as something to pay attention to, which is the
       safe direction to fail in — this app never talks anybody down. */
    --guide:      var(--warn);
    --guide-soft: var(--warn-s);
    --guide-ink:  var(--ink);
    --guide-dim:  var(--ink-dim);

    display: grid;
    gap: var(--s2);
    padding: var(--s4);
    /* The edge is an inset shadow rather than a border-left so it follows the
       corner radius instead of squaring the left side off; the extra padding
       is the text clearing it. */
    padding-left: calc(var(--s4) + 4px);
    border-radius: var(--r);
    background: var(--guide-soft);
    color: var(--guide-ink);
    box-shadow: inset 4px 0 0 var(--guide);
    animation: card-in 0.36s var(--ease-out) both;
}

.guide__title {
    display: flex;
    align-items: center;
    gap: var(--s2);
    color: var(--guide);
    font-size: 1.0625rem;
    font-weight: 750;
    letter-spacing: -0.015em;
    line-height: 1.25;
}
.guide__title i { flex: none; }

/* Left at 15px and 1.5 line height. This is the one paragraph in the app that
   gets read rather than glanced at, sometimes out loud to somebody on the
   phone, and it is short enough that nothing is gained by setting it smaller.
   The margin reset is because the grid gap above already spaces it. */
.guide__body {
    margin: 0;
    font-size: 0.9375rem;
    line-height: 1.5;
}

/* The footnote every panel carries, without exception: the sentence that says
   this is general guidance and not a diagnosis. Quiet, never quieter than
   12px, and never dimmer than the variant's own dim ink — a footnote that
   cannot be read is a footnote that is not there, and this is the one that
   keeps the panel honest about what it is. */
.guide__note {
    margin: 0;
    padding-top: var(--s1);
    color: var(--guide-dim);
    font-size: 0.75rem;
    line-height: 1.45;
}

/* ---- Call now -----------------------------------------------------------
   The loudest thing in this app and the only solid slab of colour in it: the
   panel stops being a note on the screen and becomes the screen.

   The fill is --alert rather than --danger because this is white body text on
   colour, and the dark theme's danger red is a coral that white falls off.
   The edge inverts to white with it, and the footnote goes to 82% white so it
   stays a footnote without dropping under contrast. */
.guide--emergency {
    --guide:      #FFFFFF;
    --guide-soft: var(--alert);
    --guide-ink:  var(--alert-ink);
    --guide-dim:  rgba(255, 255, 255, 0.82);

    box-shadow:
        inset 4px 0 0 var(--guide),
        var(--shadow-2),
        0 0 0 4px var(--danger-s);
    animation: alert-in 0.4s var(--ease-spring) both;
}

/* A primary button cannot be the panel's own colour, so on the filled variant
   it inverts: white slab, red label. Everything else in there becomes an
   outline in white, which is the only treatment that survives the fill. */
.guide--emergency .btn {
    border-color: rgba(255, 255, 255, 0.55);
    background-color: transparent;
    background-image: none;
    color: var(--alert-ink);
}
.guide--emergency .btn:hover { background-color: rgba(255, 255, 255, 0.14); }

.guide--emergency .btn--primary {
    border-color: transparent;
    background-color: var(--alert-ink);
    color: var(--alert);
}

/* ---- A fever ------------------------------------------------------------
   Amber, not red. Red is where "call now" lives and it must not be spent on
   the level below it, or the level above has nothing left to say. */
.guide--fever {
    --guide:      var(--warn);
    --guide-soft: var(--warn-s);
}

/* ---- On the low side ----------------------------------------------------
   Also a level that says to call, so it takes the call-now hue — tinted, not
   filled. Only the emergency level earns the fill. Cool colours were the
   obvious idea for a low reading and the wrong one: the colour here tracks how
   urgent the panel is, not which way the number went. */
.guide--low {
    --guide:      var(--danger);
    --guide-soft: var(--danger-s);
}

/* ---- Worth confirming ---------------------------------------------------
   Calm and quiet, on purpose. This appears when a forehead reading is a little
   high, and the whole of what it says is "take a rectal one before you decide".
   Dressing that in fever colours would decide it for them. */
.guide--confirm {
    --guide:      var(--ink-dim);
    --guide-soft: var(--surface-2);
}
.guide--confirm .guide__title { color: var(--ink); }


/* --------------------------------------------------------------------------
   22. Growth chart

   One inline SVG, no library. The shape is genuinely simple — a filled band, a
   line over it, a dot per measurement, a couple of grid rules, a label at each
   end — so this is six rules and no arithmetic.

   Responsiveness comes from the viewBox and from nothing else. The SVG keeps
   its default preserveAspectRatio, so the chart scales uniformly: a dot stays
   round and a date stays the shape of type at every width. The obvious
   alternative, a fixed pixel height with preserveAspectRatio="none", makes the
   drawing fill any box at all — and squashes every circle into an ellipse and
   every label into a condensed face to do it. Only the band could survive
   that, and a band is not worth the rest.

   The consequence, and it belongs to growth.js rather than to this file: the
   viewBox's units are what the type below is sized in. Keep the viewBox near
   the width the chart actually renders at — around 360 on a phone — or the
   labels arrive scaled by whatever the difference is.
   -------------------------------------------------------------------------- */
.chart {
    display: block;
    width: 100%;
    height: auto;
    /* A dot sitting exactly on the top or the bottom of the plot would
       otherwise lose half of itself to the viewport's edge. */
    overflow: visible;
}

.chart__grid {
    stroke: var(--line);
    stroke-width: 1;
    /* A hairline that stays a hairline however far the viewBox is scaled up,
       and that lands on a device pixel rather than between two of them. */
    vector-effect: non-scaling-stroke;
    shape-rendering: crispEdges;
}

/* The band under the line. --accent-soft is already a low-alpha colour in both
   themes, which is why this needs no gradient and therefore no <defs> — one
   fill, and it works on paper and on near-black. */
.chart__area {
    fill: var(--accent-soft, var(--brand-soft));
    stroke: none;
    animation: fade-in 0.6s var(--ease-out) 0.15s both;
}

/* The line draws itself on. --len is the path's own length, set inline by
   growth.js from getTotalLength(). The fallback is longer than any path this
   chart will ever hold, which fails in the safe direction: the line still
   reveals, just from a dash pattern bigger than it needed to be. A fallback
   that was too short would leave a repeating gap in the finished line. */
.chart__line {
    fill: none;
    stroke: var(--accent, var(--brand));
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    stroke-dasharray: var(--len, 2000);
    stroke-dashoffset: var(--len, 2000);
    animation: draw-line 0.9s var(--ease-out) both;
}

/* Hollow, so the line reads through it and two measurements taken a day apart
   do not merge into a blob.

   It fades rather than pops. A scale on an SVG child needs transform-box to
   put the origin on the shape itself, and where that is missing the dot scales
   about the SVG's own origin and flies in from the corner. Opacity cannot go
   wrong, and the delay is simply the line finishing first. */
.chart__dot {
    fill: var(--surface);
    stroke: var(--accent, var(--brand));
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
    animation: fade-in 0.3s var(--ease-out) 0.55s both;
}

.chart__label {
    fill: var(--ink-dim);
    font-family: var(--font);
    font-size: 11px;
    font-weight: 600;
    /* Nothing in the chart is worth selecting, and a stray long-press on a date
       should not drop a blue highlight across the graph. */
    -webkit-user-select: none;
    user-select: none;
}

/* ---- The WHO reference curves -------------------------------------------
   Five z-lines behind the measurements — −2, −1, 0, +1, +2 — and the rule that
   governs every value below is a safety rule rather than a taste one:

   ALL FIVE ARE ONE NEUTRAL INK. Not a ramp, not green in the middle and red at
   the edges, not a warmer line at −2 than at 0. A percentile is a factual
   position on a distribution; deciding that one position is better than
   another is interpretation, and interpretation is the thing this app does not
   do. Drawing that opinion instead of writing it does not make it not an
   opinion — it makes it one nobody can quote back, which is worse. A
   measurement below the 3rd centile has a dozen ordinary explanations and one
   worrying one, and this stylesheet cannot tell them apart any more than the
   rest of the app can.

   So: --ink-dim, the same slate grey the axis labels are drawn in, in both
   themes. It is a neutral in each — #7C8199 on paper, #767690 on near-black —
   and it is deliberately NOT var(--accent): the growth accent is green, and a
   green band across the middle of a percentile chart is the reassurance §0
   forbids, wearing a stroke.

   The only difference between the five is weight, and it is a reading aid: the
   median is the line the eye needs to find first to make sense of the other
   four, so it is a little more present. Nothing about that says it is where a
   baby ought to be.

   They fade in rather than drawing themselves on like .chart__line. These are
   the paper the measurements are drawn on and they should already be there
   when the eye arrives; five lines racing across the chart would also be five
   times the motion for something nobody is meant to watch. That choice is why
   §19 needs no repair rule for them — a fade switched off is simply a thing
   that is visible, where the line's dash reveal switched off is a line that
   never appears. */
.chart__ref {
    fill: none;
    stroke: var(--ink-dim);
    /* Measured on paper rather than guessed: --ink-dim at this alpha lands at
       about 1.5:1 against the light ground, which is a shade more present than
       .chart__grid's hairline and a long way behind the measurements. Lower and
       the curves disappear on a phone in daylight, which is where a percentile
       gets looked up; higher and they start competing with the line they exist
       to give a position to. */
    stroke-opacity: 0.35;
    stroke-width: 1;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    animation: fade-in 0.5s var(--ease-out) both;
}

.chart__ref--median {
    stroke-opacity: 0.6;
    stroke-width: 1.5;
}

/* Sized under .chart__label, because these sit at the right edge among the
   plotted dots rather than along an axis with room of their own — and because
   "−2" and "+2" are two characters that have to be legible without competing
   with the measurement they are standing next to. */
.chart__ref-label {
    fill: var(--ink-dim);
    fill-opacity: 0.75;
    font-family: var(--font);
    font-size: 9px;
    font-weight: 600;
    -webkit-user-select: none;
    user-select: none;
}


/* --------------------------------------------------------------------------
   23. PIN pad

   Four digits and you are in. It is the sign-in screen (index.php) and the
   installer's first-admin form, so it lives here rather than in either page's
   own <style> — a keypad that exists twice is a keypad that stops matching
   itself the first time one of the two is touched.

   Everything about it is written for the same moment: 3am, one hand, no light,
   a baby in the other arm. Hence keys that clear 64px with room to spare, a
   press state that is instant rather than eased, dots big enough to count
   without focusing, and a grid that survives a 375px screen with the whole pad
   above the fold. Nothing here is smaller than a thumb.

   The colour comes from var(--accent, var(--brand)) like every other component
   in this file, so a page that sets --accent on an ancestor gets a pad in its
   own colour and a page that sets nothing gets the app's indigo.
   -------------------------------------------------------------------------- */
.pinpad {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--s3);
    width: 100%;
    /* Wide enough for 64px keys on the narrowest phone, capped so the pad does
       not sprawl into a target field on a tablet — a hand does not get bigger
       when the screen does. */
    max-width: 320px;
    margin-inline: auto;
    /* A long press on a key is a mistyped digit, not an attempt to select the
       character or to summon the iOS callout menu. */
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

/* The dots ride in the pad's own grid rather than in a wrapper of their own,
   which is what keeps them centred on the keys below however wide the pad
   ends up being. */
.pinpad__dots {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s3);
    min-height: 44px;
}

.pinpad__dot {
    width: 14px;
    height: 14px;
    border: 1.5px solid var(--line-2);
    border-radius: var(--r-pill);
    background: transparent;
    transition:
        transform var(--t-fast) var(--ease-out),
        background-color var(--t-fast) ease,
        border-color var(--t-fast) ease,
        box-shadow var(--t-fast) ease;
}

/* Filled, and slightly larger than the empty ones. Size is the part that
   registers in peripheral vision — the count has to be readable while looking
   at the keys, not at the dots. */
.pinpad__dot--on {
    border-color: var(--accent, var(--brand));
    background: var(--accent, var(--brand));
    transform: scale(1.3);
    box-shadow: 0 0 12px -2px var(--accent, var(--brand));
}

/* Rejected. Red, then a shake, then the JavaScript empties them — in that
   order, so the dots are still full while they move. Emptying them the instant
   the answer arrives throws away the one moment that reads as "no". */
.pinpad__dots--error {
    animation: shake 0.45s var(--ease-out);
}

.pinpad__dots--error .pinpad__dot {
    border-color: var(--danger);
}

.pinpad__dots--error .pinpad__dot--on {
    background: var(--danger);
    box-shadow: none;
}

.pinpad__key {
    display: flex;
    align-items: center;
    justify-content: center;
    /* The floor, not the target. On a 375px screen the columns work out at
       about 93px wide, and the row height follows the content box below. */
    min-height: 64px;
    padding: 0;
    color: var(--ink);
    font-family: var(--font-num);
    font-size: 1.75rem;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    border: 1px solid var(--line-2);
    border-radius: var(--r);
    background: var(--track);
    -webkit-appearance: none;
    appearance: none;
    /* Only the release eases; :active below takes the transition off so the
       press itself lands on the same frame as the finger. */
    transition:
        background-color var(--t) ease,
        border-color var(--t) ease,
        transform var(--t) var(--ease-out);
}

/* A solid fill, white type and a real shrink, arriving with no easing at all.
   Anything gentler than this is a state somebody has to look at the screen to
   confirm, and looking at the screen is what this pad exists to avoid — the
   key has to be felt to have gone down. It lasts only as long as the finger is
   there, so a full-strength flash is a hundred milliseconds, not a glare.

   iOS only honours :active on an element the browser considers clickable,
   which the cursor declared in §3's reset is what settles. */
.pinpad__key:active {
    transform: scale(0.94);
    color: #fff;
    border-color: var(--accent, var(--brand));
    background: var(--accent, var(--brand));
    transition: none;
}

/* The 0. Two columns wide with delete beside it, which is the bottom row of
   the number pad iOS raises for every numeric field on the phone — the one
   layout a thumb already knows in the dark. */
.pinpad__key--wide {
    grid-column: span 2;
}

/* Backspace. Quieter at rest than a digit, because it is the key nobody is
   aiming for, and identical to one the moment it is pressed. */
.pinpad__key--action {
    color: var(--ink-soft);
    font-size: 1.5rem;
    border-color: transparent;
    background: transparent;
}

/* Short phones. The keys do not give up their height — they are the point —
   so the room comes out of the gaps and the dots instead. */
@media (max-height: 680px) {
    .pinpad { gap: var(--s2); }
    .pinpad__dots { min-height: 36px; }
}


/* --------------------------------------------------------------------------
   24. Visit summary

   A data sheet, not a dashboard. Everything else in this file is designed to
   be glanced at one-handed in the dark; this one thing is designed to be read
   across a desk by somebody who is not the person holding the phone, and
   possibly off a sheet of paper rather than off a screen at all.

   So it is deliberately plain. No accent washes, no tinted figures, no state
   colours: a number that has gone amber is the app saying something about it,
   and this screen says nothing about anything. It lists what happened and
   stops, because the person reading it is the one qualified to draw the
   conclusion and a colour would get there before they did.

   The shape is label-left, figure-right, one fact per line — the shape of every
   lab report and discharge summary ever printed, chosen because it is already
   familiar to the reader who matters and it survives being read upside down
   across a table.
   -------------------------------------------------------------------------- */
.summary {
    display: grid;
    gap: var(--s4);
}

.summary__section {
    display: grid;
    gap: 0;
    padding: var(--s2) var(--s4) var(--s3);
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    animation: card-in 0.4s var(--ease-out) both;
}

/* The heading, written twice because a section can carry either.
   .day-sep is what the rest of the app labels a block with and it is already
   the right thing here; a plain <h3> is what somebody writing a table of
   figures reaches for first. Both are styled so the sheet reads the same
   whichever the view was built with, and neither is a class this file invented
   for the occasion. */
.summary__section > h3 {
    padding: var(--s2) 0 var(--s1);
    color: var(--ink-dim);
    font-size: 0.6875rem;
    font-weight: 750;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}

.summary__section > .day-sep {
    position: static;
    margin: 0;
    padding-inline: 0;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}

.summary__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s2) 0;
}

/* Between rows only, never above the first one or under the last. A rule under
   the heading would cut the section's title off from its own figures, and a
   rule at the foot would read as a total. */
.summary__row + .summary__row {
    border-top: 1px solid var(--line);
}

.summary__label {
    flex: 1 1 auto;
    min-width: 0;
    color: var(--ink-soft);
    font-size: 0.9375rem;
    line-height: 1.35;
}

/* Tabular figures, so a column of numbers lines up on the decimal point and a
   difference between two of them is visible as a shape before it is read. The
   right edge is the one the eye runs down. */
.summary__value {
    flex: 0 1 auto;
    max-width: 62%;
    color: var(--ink);
    font-family: var(--font-num);
    font-size: 1.0625rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
    line-height: 1.35;
    text-align: right;
}

/* The quiet line under a block. It carries the two sentences this feature is
   not allowed to leave out — how many days a figure was averaged over, and
   "Routine US schedule — your pediatrician sets the actual dates." — so it is
   never smaller than 12px and never dimmer than --ink-dim. A footnote that
   cannot be read is a footnote that is not there. */
.summary__note {
    margin: 0;
    padding-top: var(--s2);
    color: var(--ink-dim);
    font-size: 0.75rem;
    line-height: 1.45;
}

/* The same staggered entrance the card grid and the entry lists use, so the
   sheet assembles rather than appearing. Nine steps covers the nine sections
   the visit summary has; anything past them arrives with the last delay, which
   is the right way for that to degrade. */
.summary__section:nth-child(1) { animation-delay: 0.00s; }
.summary__section:nth-child(2) { animation-delay: 0.03s; }
.summary__section:nth-child(3) { animation-delay: 0.06s; }
.summary__section:nth-child(4) { animation-delay: 0.09s; }
.summary__section:nth-child(5) { animation-delay: 0.12s; }
.summary__section:nth-child(6) { animation-delay: 0.15s; }
.summary__section:nth-child(7) { animation-delay: 0.18s; }
.summary__section:nth-child(8) { animation-delay: 0.21s; }
.summary__section:nth-child(9) { animation-delay: 0.24s; }


/* --------------------------------------------------------------------------
   25. Print

   The visit summary is the first thing anybody will ever print out of this app
   and probably the only one, so this block is written for that page and judged
   by one question: does it come out on one or two sheets, legible, with nothing
   on it that belongs to a phone.

   Everything here follows from paper being a different medium rather than a
   smaller screen. Paper has no tab bar to navigate with, no buttons to press,
   no dark mode, and no second chance — what comes out is what somebody carries
   into a room. So the chrome goes, the controls go, the colour goes, and what
   is left is the figures.

   The tokens are remapped rather than the components overridden. Every rule in
   the 3,000 lines above is written against --ink, --surface, --line and
   --accent, so pointing those at black and white converts the entire stylesheet
   in one block — including the eight tracker accents, which is why a printed
   row comes out in black ink instead of in a colour that costs money and reads
   as grey. Overriding components one at a time would mean finding all of them,
   and the one that got missed would be found by a printer.
   -------------------------------------------------------------------------- */
@media print {
    /* Seven selectors for one element, and every one of them is load-bearing.
       A phone in dark mode must not print white type on a black page, and the
       two rules that would do that — §2's html:not([data-theme="light"]) and
       html[data-theme="dark"] — both carry an attribute selector, which outranks
       a bare :root. A media query changes nothing about specificity, so the only
       way to beat them is to match their shape and come later in the file, which
       is exactly what this block does. Printing from a phone whose OS is in dark
       mode is not an edge case; it is the likelier of the two.

       The last three are §2b's palettes, which carry two attributes and would
       otherwise outrank the first four the same way — a pink page and a pink
       --brand on paper, from a phone that only wanted the sheet in its hand.
       They match a palette of any value, so a palette added later needs nothing
       here; and one of the three always matches, whatever data-theme says, so
       the four above are still what a palette-less phone prints by. */
    :root,
    html:not([data-theme="light"]),
    html[data-theme="dark"],
    html[data-theme="light"],
    html[data-palette]:not([data-theme="light"]),
    html[data-theme="dark"][data-palette],
    html[data-theme="light"][data-palette] {
        --paper:        #FFFFFF;
        --paper-2:      #FFFFFF;
        --surface:      #FFFFFF;
        --surface-2:    #FFFFFF;
        --glass:        #FFFFFF;
        --header-bg:    #FFFFFF;
        --scrim:        transparent;
        --ink:          #000000;
        --ink-soft:     #1C1C1C;
        --ink-dim:      #4A4A4A;
        --line:         #B4B4B4;
        --line-2:       #8C8C8C;
        --track:        #FFFFFF;

        /* Every accent to black and every soft wash to white. --brand is named
           here rather than left to follow --c-sleep, because on a palette it no
           longer does — §2b points it at a blue or a pink of its own, and a
           token that is only sometimes derived has to be reset either way.
           Status colours go too: an amber figure on a laser printer is a grey
           figure, and a grey figure on a data sheet reads as one that matters
           less rather than as one that matters more. */
        --brand:          #000000;
        --brand-soft:     #FFFFFF;
        --c-feeding:      #000000;
        --c-diaper:       #000000;
        --c-sleep:        #000000;
        --c-medicine:     #000000;
        --c-temperature:  #000000;
        --c-growth:       #000000;
        --c-vaccine:      #000000;
        --c-bath:         #000000;
        --c-labor:        #000000;
        --c-feeding-s:    #FFFFFF;
        --c-diaper-s:     #FFFFFF;
        --c-sleep-s:      #FFFFFF;
        --c-medicine-s:   #FFFFFF;
        --c-temperature-s: #FFFFFF;
        --c-growth-s:     #FFFFFF;
        --c-vaccine-s:    #FFFFFF;
        --c-bath-s:       #FFFFFF;

        /* The timer's fill goes the same way --alert does below: white ground,
           black type, so the state survives a printer that drops backgrounds. */
        --labor-fill:     #FFFFFF;
        --labor-fill-ink: #000000;

        --ok:           #000000;
        --warn:         #000000;
        --danger:       #000000;
        --ok-s:         #FFFFFF;
        --warn-s:       #FFFFFF;
        --danger-s:     #FFFFFF;

        /* Inverted rather than blacked out. This pair is the emergency panel's
           fill and the type on it, and it is the one place in the app where the
           ink is lighter than the ground. Browsers do not print background
           colours, so a black fill would come out as white paper carrying white
           letters — the loudest sentence in the app, printed as a blank space.
           White ground and black type says the same thing on paper. */
        --alert:        #FFFFFF;
        --alert-ink:    #000000;

        --shadow-1:     none;
        --shadow-2:     none;
        --shadow-3:     none;

        color-scheme: light;
    }

    /* The two values .guide--emergency hardcodes rather than taking from a
       token, and which the block above therefore cannot reach: the white edge
       and the 82%-white footnote. Both are invisible on paper. The panel is not
       part of the visit summary, but somebody printing the sick view to take to
       a doctor must not find a gap where the wording was. */
    .guide--emergency {
        --guide:     #000000;
        --guide-dim: #4A4A4A;
    }

    @page {
        /* Works out the same on A4 and on Letter, which is the only reason it
           is a margin rather than a page size: this app has no idea which of
           the two is in the tray. */
        margin: 12mm;
    }

    /* The root sizes are in points from here down, so every rem-based figure in
       the file scales to paper in one step. 11pt is the size a table of numbers
       is set at in print and it is what makes nine sections fit on a sheet. */
    html,
    body {
        background: #FFFFFF;
        color: #000000;
        font-size: 11pt;
    }

    /* Nothing on paper is an interface. The header names the baby and the sheet
       names her again; the tab bar navigates a page that cannot be navigated;
       the buttons, chips, steppers and fields are all things to press. The
       overlays are here because printing while a sheet is open is exactly the
       sort of thing that happens, and a backdrop prints as a grey rectangle
       over the whole page. */
    .app-header,
    .tabbar,
    #toast-root,
    .toast,
    .backdrop,
    .sheet,
    .btn,
    .icon-btn,
    .entry__menu,
    .load-more,
    .skeleton,
    .chips,
    .segmented,
    .stepper,
    .toggle,
    .field {
        display: none !important;
    }

    /* Nothing moves, nothing is blurred, nothing floats. An entrance animation
       is a real hazard here rather than a cosmetic one: the sections are drawn
       with animation-fill-mode both, so a browser that rasterises the page
       before the delays have elapsed prints them at zero opacity — a blank
       sheet with a heading on it. */
    *,
    *::before,
    *::after {
        animation: none !important;
        transition: none !important;
        box-shadow: none !important;
        text-shadow: none !important;
        -webkit-backdrop-filter: none !important;
        backdrop-filter: none !important;
    }

    /* The scroller's bottom padding was clearing a tab bar that is no longer
       there, and the 32rem cap was a reading width for a phone. On a page it
       would leave a third of the sheet empty and push the summary onto a second
       one for no reason. */
    #view {
        padding: 0;
        min-height: 0;
    }

    .view {
        display: block;
        max-width: none;
        margin: 0;
        padding: 0;
    }

    .view__title {
        font-size: 1.4rem;
        letter-spacing: -0.01em;
    }

    /* Two columns, and this is what gets the sheet count down from three to
       one. Reading order is preserved — down the left column, then down the
       right — which is the order §7 puts the sections in, the order a
       pediatrician asks for them. With the rule below, a section is a block
       that lands whole in one column or moves to the next. */
    .summary {
        display: block;
        column-count: 2;
        column-gap: 10mm;
    }

    /* A section split across a column break or a page break is the failure this
       whole block exists to avoid: half the feeding figures at the bottom of
       one sheet and half at the top of the next is how a number gets read
       against the wrong label. page-break-inside is the older spelling and is
       still what some print engines act on. */
    .summary__section {
        break-inside: avoid;
        page-break-inside: avoid;
        margin: 0 0 5mm;
        padding: 0 0 2mm;
        border: 0;
        border-bottom: 1px solid var(--line);
        border-radius: 0;
        background: none;
    }

    /* The headings stay, and they stay in small caps. They are what makes a
       printed sheet skimmable — somebody looking for the temperature figures
       finds them by shape before reading a word — and they are also the only
       structure left once the cards, the shadows and the colour have gone. */
    .summary__section > h3,
    .summary__section > .day-sep,
    .day-sep {
        position: static;
        color: #000000;
        background: none;
        break-after: avoid;
        page-break-after: avoid;
    }

    .summary__row {
        padding: 1.2mm 0;
    }

    .summary__label { color: #000000; }

    .summary__value {
        color: #000000;
        font-weight: 700;
    }

    /* The footnotes print. The one that says how many days a figure was
       averaged over, and the one that says a pediatrician sets the actual
       dates, are the two lines on this sheet that stop a figure being read as
       more than it is — and this is the copy that leaves the house. */
    .summary__note {
        color: #1C1C1C;
        break-inside: avoid;
        page-break-inside: avoid;
    }

    /* Entry rows appear on the sheet wherever a section lists things rather
       than counts them — the vaccines given, the illnesses. Flattened to a line
       of type: the tinted icon tile is a phone affordance and on paper it is a
       square of toner. */
    .entry {
        padding: 1.2mm 0;
        border-top: 0;
        background: none;
    }

    .entry__icon {
        width: auto;
        height: auto;
        background: none;
        color: #000000;
    }
}


/* --------------------------------------------------------------------------
   26. Settings list

   Settings was five open sections stacked down one page: every control the app
   owns, all of it on screen at once, growing by a block each time a feature
   landed. This is what it collapses to — one row per section, with the section
   itself moved into a sheet or a view behind the row.

   The value on the right is the point of the component, not decoration on it. A
   list of bare labels would have hidden the controls without answering one of
   the questions somebody opens Settings to ask — is the temperature in °C, are
   notifications on, how many people can see this baby — and would have traded a
   crowded screen for a screen you have to open four sheets to read. The row
   answers; the sheet behind it is only there to change the answer.

   The shape is .entry-list's on purpose: a rounded surface card, hairlines
   between the rows, nothing above the first or below the last. §12 already
   spends the app's one grouped-list idea on the timeline, the activity log and
   the users list, and somebody who has learnt what a row does in any of those
   knows what one does here. It is written out rather than shared because only
   the card is the same — everything from .setlist__row down differs.
   -------------------------------------------------------------------------- */
/* min-width:0 on the list and on every card in it, and both are load-bearing.
   A row's value is nowrap, so its minimum content width is the whole string —
   and a box's minimum size travels up through its ancestors by default. Dropped
   into the .stack that Settings is built out of, the grid column would size
   itself to the longest value in the list, the page would grow a horizontal
   scrollbar, and the ellipsis below would never once be reached. This is the
   floor those boxes have to lose for the truncation to happen instead. */
.setlist {
    display: flex;
    flex-direction: column;
    gap: var(--s2);
    min-width: 0;
}

/* Space between one card and the next. The title lives INSIDE its card (see
   .setlist__group-title), so there is no sibling-title case to write for —
   an earlier version of this rule also matched `.setlist__group +
   .setlist__group-title`, which could never fire and quietly implied the
   opposite structure to anybody reading it. */
.setlist__group + .setlist__group {
    margin-top: var(--s4);
}

.setlist__group {
    display: flex;
    flex-direction: column;
    min-width: 0;
    border: 1px solid var(--line);
    border-radius: var(--r);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    /* Clips the end rows' corners and the press wash to the card's rounding. */
    overflow: hidden;
}

/* The small caps the day separator and the summary headings are already set in,
   indented to the card's own padding so it reads as a label belonging to that
   card rather than as a heading on the page.

   It sits INSIDE the card, which is why it carries its own vertical padding.
   With none it rendered hard against the card's top edge — close enough to the
   rounding to read as a rendering fault rather than a heading. More room above
   than below, so it binds to the rows it names instead of floating midway
   between them and the edge. */
.setlist__group-title {
    padding: var(--s4) var(--s3) var(--s2);
    color: var(--ink-dim);
    font-size: 0.6875rem;
    font-weight: 750;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}

.setlist__row {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--s3);
    width: 100%;
    min-height: 56px;
    padding: var(--s2) var(--s3);
    border: 0;
    border-radius: 0;
    background: none;
    color: inherit;
    font: inherit;
    text-align: left;
    overflow: hidden;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--t) ease;
}

/* Between rows only, written as an adjacency rather than as a border on every
   row with the first one cleared afterwards. The hairline is then right whatever
   else the card happens to open with. */
.setlist__row + .setlist__row { border-top: 1px solid var(--line); }

/* Hover, and only where a pointer exists. The rest of the file states :hover
   flat and gets away with it; this row would not. It opens a sheet over itself,
   and on a touch screen the hover a tap leaves behind would sit lit under that
   sheet and still be lit when it closes. */
@media (hover: hover) {
    .setlist__row:hover { background: var(--surface-2); }
}

/* The press wash every pressable surface in the app carries. Repeated here
   rather than added to §8's selector list, which is left exactly as it was
   found; the numbers are copied from it unchanged, because a settings row that
   pressed differently from a timeline row would be a wrongness nobody could
   name and everybody would feel. */
.setlist__row::after {
    content: "";
    position: absolute;
    inset: 0;
    background: currentColor;
    opacity: 0;
    transform: scale(0.55);
    pointer-events: none;
    transition: opacity 0.45s ease, transform 0.45s var(--ease-out);
}
.setlist__row:active::after {
    opacity: 0.13;
    transform: scale(1);
    transition-duration: 0s;
}

/* The group clips its rows to its own rounded corners, and an outline drawn
   outside a row's box is clipped along with them — §3's ring would disappear
   down the left and right edges, which is most of it. Drawn inside instead, at
   the same width and colour. */
.setlist__row:focus-visible {
    outline: 2px solid var(--accent, var(--brand));
    outline-offset: -2px;
}

/* A circle rather than the timeline's squircle: nothing in this list was logged,
   and the round tile is what keeps a settings row from reading as an entry. The
   rows that do belong to a tracker mark themselves data-tracker and take their
   colour from §9; the rest fall back to the app's own indigo, which is what
   --brand is for. */
.setlist__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: none;
    border-radius: 50%;
    background: var(--accent-soft, var(--brand-soft));
    color: var(--accent, var(--brand));
    font-size: 0.8125rem;
    line-height: 1;
}

.setlist__main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: var(--s3);
}

/* The label never shrinks. It is the name of the thing, and half a name is not
   one — when a row runs out of room on a 375px screen it gives up the tail of
   the value instead, which is still legible as a value. */
.setlist__label {
    flex: none;
    font-size: 0.9375rem;
    font-weight: 650;
    letter-spacing: -0.01em;
}

/* Set in --ink-soft, not the --ink-dim the file mutes its quietest lines with.
   This is the line the screen exists to show — it is what somebody reads instead
   of opening the sheet — and --ink-dim is reserved for text you only read when
   you have gone looking for it. Truncates rather than wraps: a wrapped value
   would grow the row, and eight rows that each might be one line or two is the
   wall of controls this component was built to end. */
.setlist__value {
    flex: 0 1 auto;
    min-width: 0;
    margin-left: auto;
    color: var(--ink-soft);
    font-size: 0.8125rem;
    text-align: right;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Small, dim, and it leans into the press. The lean is the row's only way of
   saying that the tap goes somewhere rather than toggling something in place;
   §19 switches it off for anybody who has asked their phone to stop moving. */
.setlist__chev {
    flex: none;
    color: var(--ink-dim);
    font-size: 0.75rem;
    line-height: 1;
    transition: transform var(--t) var(--ease-out);
}
.setlist__row:active .setlist__chev { transform: translateX(2px); }


/* --------------------------------------------------------------------------
   27. Vaccine reference material, and photographs of the card

   Two things live here, and they arrived together because they answer the same
   question from opposite ends: what is this shot, and what does the paper in
   the drawer actually say.

   NOTHING IN THIS SECTION IS AN ALERT, and every choice below is downstream of
   that. Published reference material — the routine US schedule, and what the
   CDC's Vaccine Information Statement lists — may be shown; a colour, a badge
   or an icon that made it read as a warning would turn a page out of a
   document into an opinion the app is not allowed to hold. So the panel takes
   §21's shape and the vaccine blue rather than the fever amber, the headings
   are ink rather than a shouted colour, and no state class anywhere below
   changes with what has or has not been recorded.

   It is built out of what is already here: .guide for the panel, .chips for
   the vaccines inside it, .entry for the rows, .meter for the upload bar,
   .btn for the link out. What is genuinely new is the sheet's typography and
   the photo grid, and that is all that is written from scratch.
   -------------------------------------------------------------------------- */

/* ---- The "around this age" panel ----------------------------------------
   §21's guidance panel, in the vaccines blue. Reusing it rather than building
   a second panel keeps one shape in the app for "a bordered thing with a
   heading and a footnote", and .guide__note is already the slot for the
   sentence that keeps a panel honest about what it is — which is exactly what
   this panel's footnote does. */
.guide--reference {
    --guide:      var(--c-vaccine);
    --guide-soft: var(--c-vaccine-s);
}

/* The heading stays in ink, and this is not a style preference.

   Every other .guide variant colours its title because the title is the
   warning — amber for a fever, red for the emergency panel. This panel warns
   about nothing, so the colour would be borrowed emphasis it has not earned;
   and #4A90D9 on the page reaches 3.4:1, which is under the bar for 17px text
   however pretty it looks. Ink clears it comfortably, and the icon carries the
   blue where contrast is not being asked to hold a sentence up. */
.guide--reference .guide__title { color: var(--ink); }
.guide--reference .guide__title i { color: var(--guide); }

/* This panel carries two footnotes — whose schedule it is, and what the whole
   panel is — and they are two sentences rather than one because they are two
   different rules. The grid gap already separates them, so the second drops
   the top padding it would otherwise stack on top of it. */
.guide__note + .guide__note { padding-top: 0; }

/* A chip here opens a reference sheet; it is not one of a set being chosen
   from, so it never takes the .chip--on treatment and never looks picked. The
   surface lifts it off the panel's tint and the icon says where it goes. */
.vinfo__chip {
    border-color: var(--line-2);
    background: var(--surface);
}
.vinfo__chip i {
    color: var(--accent, var(--brand));
    font-size: 0.875rem;
}

/* ---- The reference sheet ------------------------------------------------
   A page about a vaccine and never about this baby, so there is nothing here
   for a value, a figure or a status to style — every block below is a heading
   and a paragraph, which is what a transcription of a document should look
   like. */
.vinfo { display: grid; gap: var(--s4); }

/* The full name under the title. Quiet, because it is the same thing said
   longer, and it is not drawn at all where the label is already the full
   name. */
.vinfo__sub {
    margin: 0;
    color: var(--ink-soft);
    font-size: 0.9375rem;
    line-height: 1.4;
}

/* Heading and body, spaced like a .field so a sheet full of these reads like
   the rest of the app's sheets rather than like a document dropped into one. */
.vinfo__section {
    display: grid;
    gap: 6px;
}

/* 15px at 1.5, the same as .guide__body. These are sentences somebody reads
   rather than glances at — sometimes out loud, in a room, to somebody else —
   and nothing is gained by setting them smaller. */
.vinfo__text {
    margin: 0;
    font-size: 0.9375rem;
    line-height: 1.5;
}

/* A plain bulleted list, and plain is the specification rather than the
   default: the app adds no order, no emphasis and no colour to what the
   document enumerates, so there is nothing here but markers and text. */
.vinfo__list {
    margin: 0;
    padding-left: 1.15rem;
    display: grid;
    gap: var(--s1);
    font-size: 0.9375rem;
    line-height: 1.45;
}

/* The way out to the whole document, given a full-width target because it is
   the answer to everything this app deliberately does not say. */
.vinfo__link { margin-top: var(--s1); }

/* The footnote every reference panel and sheet carries once. Ruled off above,
   because on a sheet it is the last thing on the page rather than a line
   inside a tinted box — the rule is what stops it reading as one more
   paragraph of the document. */
.vinfo__note {
    margin: 0;
    padding-top: var(--s3);
    border-top: 1px solid var(--line);
    color: var(--ink-dim);
    font-size: 0.75rem;
    line-height: 1.45;
}

/* The same body, opened in place inside the record sheet rather than in a
   second overlay that would take the half-filled form with it. It reads as a
   panel so it is obvious where the reference material stops and the form
   starts again.

   :empty is doing real work — the container is built once and filled on a tap,
   and an empty bordered box sitting under the button would look like something
   that had failed to load. */
.vinfo--inline {
    padding: var(--s3);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
}
.vinfo--inline:empty { display: none; }

/* ---- A row that opens one ----------------------------------------------
   The name on a row becomes a button when the app holds published material for
   it. It keeps .entry__title's typography exactly — the two classes are on the
   same element, so nothing here may set a font property or it would win — and
   only unsets what a <button> brings with it. */
.entry__title--info {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    max-width: 100%;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    text-align: left;
}
.entry__title--info > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Small, dim, and the only mark on the row that says the name goes somewhere.
   Not the accent: a coloured glyph beside a vaccine's name on a record row is
   one step from looking like a status, and no icon in this feature is allowed
   to mean anything about her. */
.entry__title--info i {
    flex: none;
    color: var(--ink-dim);
    font-size: 0.75rem;
}

/* ---- Record photos ------------------------------------------------------
   The card, on the phone, in the waiting room. The tiles are square and
   cropped because a drawer full of paper photographs at every angle would
   otherwise make a ragged grid; the whole picture is one tap away, uncropped,
   which is where it is actually read. */
.photos { margin-top: var(--s2); }

/* The grid gap already spaces these, so the base <p> margin would only add a
   second gap under every line. */
.photos p { margin: 0; }

.photos__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: var(--s3);
}

/* The "no photos yet" line is a child of the grid, so it takes the full width
   rather than sitting in a 96px column on its own. */
.photos__grid > .field__hint { grid-column: 1 / -1; }

.photos__item {
    display: grid;
    gap: 6px;
    min-width: 0;
}

.photos__tile {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    overflow: hidden;
}

.photos__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--t) var(--ease-out);
}
.photos__tile:active .photos__img { transform: scale(1.03); }

/* Two lines, then an ellipsis. A caption is a note to yourself about which
   page this is, and a long one must not push the tile below it down the
   screen. */
.photos__caption {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    color: var(--ink-dim);
    font-size: 0.75rem;
    line-height: 1.35;
}

/* The camera, the caption and the bar. Spaced off the grid above it so it
   never looks like another tile. */
.photos__add { margin-top: var(--s2); }

/* A file input is the one control the app cannot draw itself. It keeps .input's
   box so it lines up with the caption field above it, and its own button is
   dressed as a small ghost button so the row does not arrive in the browser's
   default grey. */
.photos__file {
    padding: 8px 10px;
    line-height: 1.6;
}
.photos__file::-webkit-file-upload-button {
    margin-right: var(--s3);
    padding: 6px 12px;
    border: 1px solid var(--line-2);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    color: var(--ink);
    font-size: 0.875rem;
    font-weight: 650;
}

/* §11's meter, doing the one job it was built for on another screen: a bar
   that fills. It is hidden until something is actually in flight, because a
   bar sitting at zero reads as an upload that has stalled. */
.photos__progress { margin-top: var(--s1); }

/* Uncropped, and capped so the sheet's own buttons stay on screen underneath
   it. A photograph of a record is read rather than admired — the point is the
   lot number in the corner, so nothing here crops or covers one. */
.photo-full {
    display: block;
    width: 100%;
    max-height: 68vh;
    object-fit: contain;
    border-radius: var(--r-sm);
    background: var(--surface-2);
}


/* --------------------------------------------------------------------------
   28. The contraction timer

   Built for one hand at 3am between contractions, which is a harder brief
   than the rest of the app's: the person tapping this is the one having the
   contraction, or a partner watching her have it. So one control, most of a
   375px screen, and two states a glance can tell apart across a dark room.

   Appended like §20-27 rather than slotted in beside §8's buttons, and §19's
   blanket rule reaches everything here except the one ring it pins on
   purpose. The accent arrives through [data-tracker="labor"] (§9) on the view
   the button sits in; nothing below names the plum directly except the
   timing fill, which has its own token for the same reason --alert does.
   -------------------------------------------------------------------------- */

/* ---- The button ----------------------------------------------------------
   THE LARGEST TAP TARGET IN THE APP, and that is a specification rather than
   a flourish: 45vh of a 375x667 phone is ~300px of button, which can be hit
   with a thumb, without looking, mid-contraction. Nothing else is allowed to
   be bigger, because nothing else in the app is tapped by somebody in labor.

   Two declarations for the height: vh everywhere, then svh where the browser
   knows it — on an iPhone's collapsing toolbars vh is the LARGER viewport, so
   45vh alone could push the stats below the button off the first screen. */
.labor-btn {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    width: 100%;
    min-height: 45vh;
    min-height: 45svh;
    padding: var(--s5) var(--s4);
    border: 2px solid var(--accent, var(--brand));
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--ink);
    box-shadow: var(--shadow-1);
    text-align: center;
    /* A press anywhere on it must never select the label or zoom the page —
       this button is tapped repeatedly, hard, by somebody not looking at it. */
    user-select: none;
    -webkit-user-select: none;
}

/* The same corner wash the home cards carry, so the idle button reads as one
   of the family — an invitation in the tracker's own colour, not a slab. */
.labor-btn::before {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: radial-gradient(120% 100% at 0% 0%,
        var(--accent-soft, var(--brand-soft)) 0%, transparent 72%);
    pointer-events: none;
}

/* Everything on the button sits above the wash. */
.labor-btn > * { position: relative; }

/* The state line — the whole label while idle ("Contraction starting"), the
   second line while timing ("It's easing — tap"). Big both times: it is read
   mid-contraction, and mid-contraction is not a moment for 14px type. */
.labor-btn__label {
    font-size: 1.375rem;
    font-weight: 750;
    letter-spacing: -0.015em;
    line-height: 1.2;
}

/* The live elapsed seconds while timing — the biggest figure in the app,
   readable at arm's length with the lights off. Tabular, because it changes
   once a second and must not sway side to side while it does. */
.labor-btn__num {
    font-family: var(--font-num);
    font-size: 4rem;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

/* The pressed state is a fill change rather than a scale: it survives §19
   untouched, and a thumb pressing this at 3am needs the confirmation, not the
   choreography. */
.labor-btn:active {
    background: var(--accent-soft, var(--brand-soft));
}

/* A request in flight. The tap has already been answered — the flip is
   optimistic and the haptic fired on the thumb — so this is a whisper, not a
   spinner: contractions.js ignores taps while it is set, and the button must
   not dress a half-second of wifi as something happening to the mother. */
.labor-btn.is-loading {
    opacity: 0.88;
}

/* ---- Timing --------------------------------------------------------------
   The state a glance across a dark room has to catch: the button fills with
   the deep plum and everything on it goes white. Spelt with the shared is-on
   helper, the way contractions.js spells every state on this screen.

   PLUM, NEVER RED — red is the fever emergency's colour in this app, and a
   contraction being timed is not an emergency state; a timer that dressed
   like the panel that says to call a pediatrician now would be the app
   editorialising once a minute. */
.labor-btn.is-on {
    border-color: var(--labor-fill);
    background: var(--labor-fill);
    color: var(--labor-fill-ink);
}

/* The wash is the idle state's furniture; on the fill it would grey a corner. */
.labor-btn.is-on::before { background: none; }

/* The breathing ring: the sick banner's cadence (5.2s, §18's edge-breathe),
   nothing like the emergency's 1.4s pulse. It exists so "timing right now" is
   findable on a screen somebody else's phone is showing across the room, and
   it is the one moving thing on the button. §19 pins it bright and still. */
.labor-btn.is-on::after {
    content: "";
    position: absolute;
    inset: 4px;
    border-radius: calc(var(--r-lg) - 4px);
    box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.85);
    opacity: 0.75;
    pointer-events: none;
    animation: edge-breathe 5.2s ease-in-out infinite;
}

.labor-btn.is-on:active {
    background: var(--accent, var(--labor-fill));
}

/* ---- The home banner -----------------------------------------------------
   §20's furniture with the labor accent, drawn by dashboard.js ABOVE the sick
   banner — the mother outranks everything on this screen. The whole banner is
   one door to the labor view, so it takes a pointer and the pressed wash the
   home cards use; the buttons row of the sick banner has no equivalent here,
   because there is exactly one thing to do with this banner and it is all of
   it. */
.sick-banner--labor {
    cursor: pointer;
}

.sick-banner--labor:active {
    background: var(--surface-2);
}

/* The one line of figures: "5m apart · lasting 50s · 6 in the last hour".
   One line rather than §20's stat tiles because it is read as a sentence down
   a phone to a provider, and because the banner above the grid has to stay
   shallow — there may be a sick banner under it needing the same screen.
   Tabular, since half of it ticks once a second while a contraction is being
   timed. */
.sick-banner__line {
    position: relative;
    flex: 1 1 100%;
    min-width: 0;
    margin: 0;
    font-family: var(--font-num);
    font-size: 1.0625rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.3;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}

/* The chevron at the head's far end — the only mark that says this banner is
   a door. Dim rather than accented, like the settings rows' chevron: it is a
   direction, not a figure. The head is a wrapping flex row, so auto margin
   walks it to the right edge of the first line. Written with the head in the
   selector because §20's `.sick-banner__head i` colours every icon in the
   head with the accent at class-plus-element weight, and this one has to say
   something different from where it sits. */
.sick-banner__head i.sick-banner__chev {
    margin-left: auto;
    color: var(--ink-dim);
    font-size: 0.8125rem;
}


/* --------------------------------------------------------------------------
   29. The expecting home

   SPEC-13 §3: while the payload's mode is 'expecting' the six tracker cards
   are not drawn, and the home screen becomes three things — the due-date
   panel, two door cards in the grid's own furniture, and one quiet line.
   Everything here is built from tokens, so both themes and the boy/girl
   palettes come for free, and NOTHING here animates beyond what .tcard
   already does — §19's blanket rule needs no new exception for this section.

   The doors wear the accent of what they OPEN: the contraction-timer card
   marks itself data-tracker="labor" and takes the plum from §9, and the
   hospital-bag card marks nothing and falls back to --brand — a checklist is
   not a tracker and gets no accent of its own (SPEC-12 §5). No colour is
   named anywhere below.
   -------------------------------------------------------------------------- */

/* The due-date panel: one composed line off the payload ("Due Wed, Oct 28 ·
   31 weeks along") and no other content — no advice, no countdown ring, no
   progress toward anything. Card furniture rather than a plain heading so the
   screen's first rectangle is visibly the same app the born home is. */
.due-panel {
    padding: var(--s3) var(--s4);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    box-shadow: var(--shadow-1);
}

.due-panel__line {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 650;
    letter-spacing: -0.01em;
    line-height: 1.35;
    color: var(--ink);
}

/* The second line in the same panel — "Average: 3 lb 6 oz · Cabbage"
   (SPEC-17 §4). Deliberately the panel's quiet half: --ink-dim at caption size
   against the due date's 650 weight above it, so a glance lands on the date and
   this is only ever read on purpose. It carries a published population average
   and a size analogy, never a statement about this baby, and nothing about it
   should look like a figure worth measuring against. Tokens only, so both
   themes and the boy/girl palettes come for free; it wraps at 375px rather than
   truncating, because half of "3 lb 6 oz" is worse than two lines. */
.due-panel__average {
    margin: var(--s1) 0 0;
    color: var(--ink-dim);
    font-size: 0.8125rem;
    line-height: 1.4;
}

/* A door card: a .tcard with no "time since" figure, so the top row and the
   foot carry it alone. The name is allowed to wrap — "Contraction timer" in a
   two-across grid at 375px does not fit the base card's single ellipsised
   line, and a door whose label reads "Contractio…" is a door nobody can read
   the front of. min-height keeps the pair of doors as tall as a tracker card,
   so the grid does not visibly deflate the day the mode flips. */
.tcard--door {
    min-height: 148px;
}

.tcard--door .tcard__name {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    font-size: 1rem;
    line-height: 1.25;
}

/* The count line ("12 of 24 packed") sits where every card's summary sits,
   pinned to the bottom by the base rule's margin-top:auto. Reserved height
   even when empty, so the labor door and the bag door stay the same shape
   whether or not the bag migration has run. */
.tcard--door .tcard__today {
    min-height: 2rem;
}

/* The one quiet line under the doors. Muted and centred: it explains the
   screen's emptiness and must never compete with the panel above it. Pulled
   back toward the cards it belongs to — the view's own grid gap is sized for
   separating sections, and this line is a caption, not a section. */
.expecting-note {
    margin: calc(var(--s3) * -1) 0 0;
    text-align: center;
    color: var(--ink-dim);
    font-size: 0.8125rem;
    line-height: 1.4;
}

/* The growth screen's §4 sentence (SPEC-13), rendered once ABOVE the chart:
   "Born N weeks early — plotted at corrected age…". Quiet like the WHO
   attribution under the chart, but not interchangeable with it — this line
   says how the reference is plotted, that one says who reads it. */
.corrected-note {
    margin: 0;
    color: var(--ink-dim);
    font-size: 0.8125rem;
    line-height: 1.4;
}

/* The sentence's host is a standing child of the view's gapped grid; while
   there is nothing to say it collapses entirely, so a full-term baby's chart
   never carries a blank row where an explanation would have been. */
.corrected-host:empty {
    display: none;
}


/* --------------------------------------------------------------------------
   30. The hospital bag

   SPEC-12 §5: the rows are §12's .entry furniture and the two classes below
   are the only things this feature adds — a checked-state treatment and the
   add row. NO ACCENT, deliberately: a checklist is not a tracker and gets no
   ninth colour (the view carries no data-tracker, so everything here falls
   back to --brand through §9's chain). Both themes come from tokens, nothing
   animates beyond the transitions .entry already has, so §19's blanket rule
   needs no new exception.
   -------------------------------------------------------------------------- */

/* A packed row. The strike and the dim land on the LABEL ONLY — the meta
   ("Vanessa · 3h ago") keeps full contrast, because who packed it and when is
   the question this list answers from the other phone, and the one thing a
   done-mark must never dim is the answer. The row itself stays where it was
   and keeps its full hit area: position and tappability mean nothing about
   done-ness here (SPEC-12 §4), so nothing below touches layout. */
.entry--checked .entry__title {
    color: var(--ink-dim);
    text-decoration: line-through;
    text-decoration-thickness: 1px;
}

/* The check mark itself reads quieter than an unpacked row's icon — packed
   rows should recede as a group so the still-to-pack ones stand forward, and
   the mark is part of the label's statement, not the meta's. */
.entry--checked .entry__icon {
    color: var(--ink-dim);
}

/* The add row: one .input beside one .btn--icon, per list. The input takes
   every pixel the button leaves so "Phone charger (long cable)" is readable
   while it is being typed at 375px; both controls carry their own 44px-class
   heights from §13 and §8, so there is nothing to re-win here. */
.bag-add {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.bag-add .input {
    flex: 1 1 auto;
    min-width: 0;
}


/* --------------------------------------------------------------------------
   31. Babies — the pager, the dots, the header switcher

   SPEC-16 §3. Everything in this section is drawn ONLY when the account has
   more than one unarchived baby: dashboard.js builds no pager, no dots and no
   chevron below that, so on a household with one baby not one selector here
   matches anything. That is the parity promise, and it is kept in the JS
   rather than by hiding things with CSS — an element that is display:none is
   still an element somebody's screen reader can find.

   Nothing here names a colour. The dots and the tag take --brand, which §9
   already resolves per palette and per theme, so both themes and all three
   palettes arrive for free.
   -------------------------------------------------------------------------- */

/* The viewport the panes slide inside.
 *
 * NO overflow at rest, and that is deliberate: the cards under it carry soft
 * shadows that a clipping box would shear off along the edges of every screen
 * in the app. .is-swiping puts the clip on for the length of a gesture and the
 * slide that follows it, which is the only time anything is outside the box. */
.baby-pager {
    position: relative;
}

.baby-pager.is-swiping {
    overflow: hidden;
}

/* The pane restates .view's own grid and gap. The home screen's children —
   the banners, the card grid, "Right now", the timeline — are direct children
   of the view's grid when there is one baby and children of this when there is
   more than one, and they have to lay out identically either way. */
.baby-pager__pane {
    display: grid;
    gap: var(--s5);
}

/* The incoming pane, parked off to one side. Absolute so it takes no height of
   its own: the pager keeps the height of the screen being left, which is what
   stops the page under the finger jumping as the two cross. */
.baby-pager__pane--ghost {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
}

/* Linear travel, one easing, no bounce at the end. The rubber-band at the ends
   of the list is a fraction applied to the finger's own distance in
   dashboard.js, not a spring here — resistance while the finger is down reads
   as "nothing further this way"; a bounce after it lifts reads as an error. */
.baby-pager__pane--settle {
    transition: transform 0.26s var(--ease-out);
}

/* The page dots, sitting directly under the header — the standard signifier
   that sideways exists.
 *
 * The negative margins pull them out of the view grid's own spacing: 24px of
 * gap above the cards is right between two blocks of content and far too much
 * around a 7px indicator, which belongs against the header it describes rather
 * than floating between it and the first card.
 *
 * Each dot is a real button carrying a 24x32 target around a 7px mark. Smaller
 * than this app's 44px floor, deliberately and only here: the dots are the
 * third way across (the gesture and the header chevron are the other two), and
 * a row of 44px squares under the header would take a card's worth of screen
 * for an indicator. They are buttons at all because once dots are on screen
 * somebody taps one, and a dot that does nothing teaches that the row is a
 * picture — on the very screen where the gesture also lives. */
.baby-dots {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    margin: calc(var(--s2) * -1) 0 calc(var(--s3) * -1);
}

.baby-dots__dot {
    width: 24px;
    height: 32px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.baby-dots__dot::before {
    content: "";
    display: block;
    width: 7px;
    height: 7px;
    margin: 0 auto;
    border-radius: var(--r-pill);
    background: var(--ink-dim);
    opacity: 0.4;
    transition: background-color var(--t) ease, opacity var(--t) ease,
                transform var(--t) var(--ease-out);
}

.baby-dots__dot--on::before {
    background: var(--brand);
    opacity: 1;
    transform: scale(1.25);
}

/* Inset, because the dot is a mark in the middle of a target much larger than
   itself and a ring around the whole button would float in space. */
.baby-dots__dot:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: -6px;
    border-radius: var(--r-sm);
}

/* ---- The header switcher ----------------------------------------------
   The name and the age already sit in a plain <div> inside .app-header;
   dashboard.js adds the class, the role and the chevron to that same wrapper
   rather than rebuilding markup app.php owns. With one baby the wrapper is
   bare and none of this applies. */
.baby-switch {
    min-width: 0;
    border-radius: var(--r-sm);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.baby-switch:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 3px;
}

/* The wash under a press, in the same key as .icon-btn's. It is the only
   feedback a name in a header can give, and without it the chevron looks
   painted on. */
.baby-switch:active {
    background: var(--surface-2);
}

/* The title becomes a row: the name, then the chevron. The ellipsis moves
   from the h1 to the name span so a long name is trimmed and the chevron
   stays visible — a switcher whose affordance is the first thing to be cut
   off is a switcher nobody finds. */
.app-header__title--switch {
    display: flex;
    align-items: center;
    gap: var(--s2);
}

.baby-switch__name {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.baby-switch__chev {
    flex: none;
    color: var(--ink-dim);
    font-size: 0.75rem;
}

/* The marker on the baby the session is showing, in the switcher sheet and in
   the Settings list. A quiet pill rather than a tick: it states which child
   this phone is on, which is a fact about the device and not an approval. */
.baby-tag {
    flex: none;
    align-self: center;
    padding: 2px var(--s2);
    border-radius: var(--r-pill);
    background: var(--brand-soft);
    color: var(--brand);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    white-space: nowrap;
}
