/* ============================================================
   Hot Girl Summer — design system
   PALETTE IS LOCKED. Every colour below is lifted verbatim from
   the original app. Named here, never altered.
   ============================================================ */

/* ============================================================
   Self-hosted type. These were loaded from Google Fonts, which meant every
   single launch made a request to Google carrying the user's IP — in an app
   whose entire promise is that her measurements never leave the phone. Both
   families are SIL Open Font License 1.1, which permits bundling; the licence
   text ships beside them in ./fonts.

   Variable files, so one download per family covers every weight the app
   uses (Baloo 2 at 600/700/800, Caveat at 500/700) instead of five statics.
   latin and latin-ext only: the other subsets Google serves are Devanagari,
   Vietnamese and Cyrillic, which this app has no use for.
   ============================================================ */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('./fonts/baloo2-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('./fonts/baloo2-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('./fonts/caveat-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('./fonts/caveat-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* --- Locked palette --- */
  --plum:        #3A2E4D;  /* body text */
  --wine:        #4B1528;  /* headings on light surfaces */
  --magenta:     #8A1550;  /* section titles, badges */
  --rose:        #D1699A;  /* primary accent */
  --violet:      #8E7CC3;  /* secondary accent */
  --lilac:       #C9B6E4;  /* summary gradient start */
  --blush:       #E8A0BF;  /* summary gradient end */
  --bg-top:      #FBE4EE;  /* page gradient start */
  --bg-bottom:   #F5EEFA;  /* page gradient end */
  --muted:       #6B5A88;  /* field labels */
  --muted-soft:  #9583A8;  /* hints, empty states */
  --border:      #E6D9F0;  /* input borders */
  --field-bg:    #FBFAFD;  /* input fill */
  --divider:     #F0E4F2;  /* list separators */
  --divider-alt: #EAD9EC;  /* dashed separators */
  --danger:      #B23B5C;  /* errors */
  --ghost:       #A65C82;  /* tertiary text buttons */
  --white:       #FFFFFF;

  /* --- Motion ---
     Custom curves. The CSS built-ins are too weak to read as intentional. */
  --ease-out:    cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
  --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);

  --dur-press:  140ms;
  --dur-fast:   180ms;
  --dur-med:    240ms;
  --dur-sheet:  380ms;
  --dur-travel: 420ms;   /* something crossing the bar: the clip, and the pane it selects */

  /* --- Elevation --- */
  --shadow-card:  0 2px 10px rgba(142, 124, 195, 0.12);
  --shadow-lift:  0 8px 24px rgba(142, 124, 195, 0.18);

  /* --- Type --- */
  --font-display: 'Baloo 2', ui-rounded, 'SF Pro Rounded', system-ui, sans-serif;
  --font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
  /* The fallback stack is load-bearing, not decoration: sw.js ignores
     cross-origin requests, so offline this still reads as handwriting
     (Bradley Hand on iOS) instead of falling back to the body font. */
  --font-hand: 'Caveat', 'Bradley Hand', 'Snell Roundhand', 'Segoe Print',
               'Comic Sans MS', cursive;

  /* --- Safe areas (notch / home indicator) --- */
  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left, 0px);
  --safe-right:  env(safe-area-inset-right, 0px);

  /* --- Post-it (added, not part of the locked palette above) --- */
  --postit:      #FEF7C3;  /* a warm, soft yellow */
  --postit-deep: #FBEFA4;
}

*, *::before, *::after {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font-body);
  background: linear-gradient(180deg, var(--bg-top) 0%, var(--bg-bottom) 100%);
  background-attachment: fixed;
  min-height: 100vh;
  min-height: 100dvh;
  color: var(--plum);
  padding-bottom: calc(40px + var(--safe-bottom));
  /* `contain` not `none`: it still stops scroll chaining, but keeps the
     native rubber-band bounce. Suppressing that is most of what makes a web
     app feel like a web page rather than an app. */
  overscroll-behavior-y: contain;
}

body.sheet-open { overflow: hidden; }

.wrap {
  max-width: 480px;
  margin: 0 auto;
  padding: 0 calc(16px + var(--safe-left)) 0 calc(16px + var(--safe-right));
}

/* Numerals must never jitter as they animate or update. */
.tnum { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }

/* ---------------------------------------------------------- Topbar */

.topbar {
  background: linear-gradient(135deg, var(--violet) 0%, var(--rose) 100%);
  color: var(--white);
  padding: calc(28px + var(--safe-top)) 20px 22px;
  border-radius: 0 0 24px 24px;
  margin-bottom: 18px;
}
.topbar h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 24px;
  font-weight: 800;
  letter-spacing: 0.2px;
  line-height: 1.1;
}
.topbar p {
  margin: 5px 0 0;
  font-size: 13px;
  opacity: 0.92;
  transition: opacity var(--dur-med) var(--ease-out);
}

/* ---------------------------------------------------------- Cards */

.card {
  background: var(--white);
  border-radius: 16px;
  padding: 16px 18px;
  margin-bottom: 14px;
  box-shadow: var(--shadow-card);
}

.summary-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
  margin-top: 14px;
}
/* The stagger is decoration only. It is applied by JS, so if animation
   never runs the tiles are still on screen — content must never depend
   on an animation completing to become visible. */
.summary-grid .mini.stagger {
  animation: mini-in var(--dur-med) var(--ease-out) backwards;
}
@keyframes mini-in {
  from { opacity: 0; transform: translateY(6px); }
}
.summary-grid .mini .mv { font-size: 15px; font-weight: 700; }

/* Direction cues. Rose = moving away from goal, wine = toward it,
   plum = a metric with no "good" direction (arms, thighs, bum). */
.dir-good    { color: var(--wine); }
.dir-away    { color: var(--danger); }
.dir-neutral { color: var(--plum); }

/* ---------------------------------------------------------- Reminder banner */

.banner {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--white);
  border: 1.5px solid var(--lilac);
  border-radius: 14px;
  padding: 12px 12px 12px 16px;
  margin-bottom: 14px;
  box-shadow: var(--shadow-card);
}
/* Entrance is opt-in, added by JS. Declared on the base class it would mean
   the banner starts at opacity 0 — content waiting on an animation to exist. */
.banner.enter { animation: banner-in var(--dur-med) var(--ease-out); }
@keyframes banner-in {
  from { opacity: 0; transform: translateY(-6px); }
}
.banner-text { flex: 1; font-size: 13.5px; color: var(--wine); font-weight: 600; line-height: 1.4; }
.banner-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.banner-cta {
  background: linear-gradient(135deg, var(--violet), var(--rose));
  color: var(--white);
  border: none;
  border-radius: 10px;
  padding: 8px 14px;
  font-size: 13px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out);
}
.banner-cta:active { transform: scale(0.96); }
.banner-x {
  background: transparent;
  border: none;
  color: var(--muted-soft);
  font-size: 14px;
  width: 28px; height: 28px;
  cursor: pointer;
  border-radius: 50%;
  transition: transform var(--dur-press) var(--ease-out);
}
.banner-x:active { transform: scale(0.9); }
.banner-cta:focus-visible, .banner-x:focus-visible {
  outline: 2.5px solid var(--rose); outline-offset: 2px;
}

/* ---------------------------------------------------------- Consistency */

/* Celebration lives here — attached to showing up, which she controls,
   rather than to which direction the scale moved, which she does not. */
.consistency {
  font-size: 12.5px;
  font-weight: 600;
  margin-top: 8px;
  opacity: 0.92;
  min-height: 16px;
}

/* ---------------------------------------------------------- Recomp insight */

.insight-card {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  border-left: 3px solid var(--rose);
}
.insight-card.note { border-left-color: var(--violet); }
.insight-card.enter { animation: screen-in var(--dur-med) var(--ease-out); }
.insight-mark {
  width: 20px; height: 20px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 1px;
  background: linear-gradient(135deg, var(--violet), var(--rose));
  position: relative;
}
/* Tick for a good-news insight. */
.insight-card.good .insight-mark::after {
  content: '';
  position: absolute;
  left: 7px; top: 4px;
  width: 5px; height: 9px;
  border-right: 2px solid var(--white);
  border-bottom: 2px solid var(--white);
  transform: rotate(45deg);
}
/* Lower-case "i" for something to look at, not an alarm. */
.insight-card.note .insight-mark::after {
  content: 'i';
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--white);
  font-size: 13px;
  font-weight: 700;
  font-style: italic;
}

.insight-body { font-size: 13.5px; line-height: 1.5; color: var(--plum); }
.insight-body b { color: var(--wine); }

/* ---------------------------------------------------------- Affirmation */

/* A STICKER, and the word is doing work.
   Version one was a card: white box, 3px left border, card shadow — the exact
   parts .insight-card is built from, which made the day's line read as one more
   thing the app wanted her to do. Version two took the box away and left a bare
   sentence with nothing under it.
   A sticker is the third thing. The tells are all borrowed from real ones: a
   white die-cut edge all the way round, a warm fill rather than paper white, a
   soft shadow so it sits ON the page instead of in it, and a degree and a half
   of rotation because nobody ever put a sticker down straight. None of that
   vocabulary appears anywhere else in the app, so it cannot be misread as
   another instruction.
   The post-it on the Log page is the other paper object here and the two are
   deliberately unlike: that one is her handwriting, this one is the app's
   voice. */
.affirm {
  background: none;
  border: none;
  box-shadow: none;
  padding: 0;
  /* The air above it is doing real work: pressed up against the masthead it
     reads as a page title rather than as the day's line. The extra room below
     is the shadow's, and the rotation's. */
  margin: 14px 2px 26px;
}
.sticker {
  position: relative;
  padding: 15px 17px 16px;
  /* Four different corners. A single radius reads as a component; uneven ones
     read as something cut out by hand. */
  border-radius: 20px 15px 19px 14px;
  background:
    radial-gradient(120% 90% at 18% 0%, rgba(255,255,255,0.66) 0%, rgba(255,255,255,0) 58%),
    linear-gradient(152deg, var(--postit) 0%, #FBE2CE 46%, var(--blush) 100%);
  /* The white ring IS the die cut. Second shadow lifts it off the page. */
  box-shadow:
    0 0 0 5px var(--white),
    0 6px 16px rgba(142, 124, 195, 0.20),
    0 1px 2px rgba(75, 21, 40, 0.06);
  transform: rotate(-1.4deg);
}
.sticker-spark {
  position: absolute;
  top: -5px;
  right: 11px;
  width: 27px;
  height: 24px;
  color: var(--rose);
  opacity: 0.72;
  transform: rotate(9deg);
  pointer-events: none;
}
.affirm-text {
  margin: 0;
  /* Ragged right by a hair, so the block never squares up into a paragraph. */
  padding-right: 26px;
  font-family: var(--font-display);
  font-size: clamp(19px, 5.2vw, 23px);
  font-weight: 700;
  line-height: 1.28;
  color: var(--wine);
  letter-spacing: -0.4px;
  text-wrap: balance;       /* no orphan word hanging off a two-line sentence */
}

/* ---------------------------------------------------------- How-to-measure */

.howto {
  background: var(--field-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  margin: 2px 0 14px;
  overflow: hidden;
}
.howto summary {
  padding: 11px 14px;
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--violet);
  list-style: none;
}
.howto summary::-webkit-details-marker { display: none; }
.howto summary::before { content: '＋ '; font-weight: 700; }
.howto[open] summary::before { content: '－ '; }
.howto summary:focus-visible { outline: 2.5px solid var(--rose); outline-offset: -2px; }
.howto-body {
  padding: 0 14px 12px;
  font-size: 12.5px;
  color: var(--muted);
  line-height: 1.55;
}
.howto-body p { margin: 0 0 8px; }
.howto-body p:last-child { margin-bottom: 0; }
.howto-body ul { margin: 0 0 8px; padding-left: 18px; }
.howto-body li { margin-bottom: 4px; }

/* ---------------------------------------------------------- Chart */

.chart-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0 4px 10px;
}
.chart-head .ct { font-size: 13px; font-weight: 700; color: var(--wine); }
.chart-head .cr { font-size: 12px; color: var(--muted-soft); }
.spark { display: block; width: 100%; height: 96px; overflow: visible; }
.spark .line {
  fill: none;
  stroke: var(--rose);
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.spark .trend {
  fill: none;
  stroke: var(--violet);
  stroke-width: 1.5;
  stroke-dasharray: 3 4;
  opacity: 0.75;
}
.spark .dot { fill: var(--white); stroke: var(--rose); stroke-width: 2.5; }
/* The weight she marked, drawn at the height it sits at. Finer and quieter than
   the trend line on purpose — the readings are the subject, this is the horizon
   behind them. No text in the SVG: the chart is drawn with
   preserveAspectRatio="none", so anything lettered inside it would stretch.
   The number lives in the legend, where it renders at its true proportions. */
.spark .goal-rule {
  fill: none;
  stroke: var(--muted-soft);
  stroke-width: 1.2;
  stroke-dasharray: 1.5 4.5;
  stroke-linecap: round;
  opacity: 0.6;
}
.spark .fill { fill: url(#sparkFill); }
.chart-legend {
  display: flex;
  gap: 14px;
  padding: 8px 4px 2px;
  font-size: 11px;
  color: var(--muted-soft);
}
.chart-legend span { display: inline-flex; align-items: center; gap: 5px; }
.chart-legend i { width: 12px; height: 2.5px; border-radius: 2px; display: inline-block; }
.chart-legend i.lg-goal {
  height: 2px;
  border-radius: 0;
  background: repeating-linear-gradient(90deg,
              var(--muted-soft) 0 1.5px, transparent 1.5px 4px);
  opacity: 0.75;
}

/* ---------------------------------------------------------- Fields */

h2.section-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--magenta);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin: 18px 4px 10px;
}

.field { margin-bottom: 12px; }
.field label {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 5px;
}
.field input {
  width: 100%;
  padding: 11px 12px;
  border: 1.5px solid var(--border);
  border-radius: 10px;
  font-size: 16px; /* 16px stops iOS zooming the page on focus */
  font-family: inherit;
  background: var(--field-bg);
  color: var(--plum);
  transition: border-color var(--dur-fast) ease,
              background-color var(--dur-fast) ease,
              box-shadow var(--dur-fast) ease;
}
.field input:focus {
  outline: none;
  border-color: var(--rose);
  background: var(--white);
  box-shadow: 0 0 0 3px rgba(209, 105, 154, 0.15);
}
.field input:focus-visible { outline: none; }

/* No steppers on the baseline screen. There is no previous number to step
   from, so an arrow that lands on 0.1 is worse than no arrow at all —
   she is reading these off a scale and a tape, and will type them. */
#onboarding input[type=number]::-webkit-outer-spin-button,
#onboarding input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
#onboarding input[type=number] { -moz-appearance: textfield; appearance: textfield; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

.field textarea {
  width: 100%;
  padding: 11px 12px;
  border: 1.5px solid var(--border);
  border-radius: 10px;
  font-size: 16px;
  font-family: inherit;
  background: var(--field-bg);
  color: var(--plum);
  resize: vertical;
  min-height: 62px;
  transition: border-color var(--dur-fast) ease, background-color var(--dur-fast) ease,
              box-shadow var(--dur-fast) ease;
}
.field textarea:focus {
  outline: none;
  border-color: var(--rose);
  background: var(--white);
  box-shadow: 0 0 0 3px rgba(209, 105, 154, 0.15);
}

/* The tick was a native OS checkbox — a system control dropped into a set of
   soft, translucent, clay surfaces. It could not be softened, only recoloured,
   which is exactly why it read as foreign. It is a switch now.

   The knob is painted as two radial gradients on the input's own background,
   not as a pseudo-element: an <input> is a replaced element and will not render
   ::before/::after in most engines. Two layers so the knob carries a shadow
   under it — one blob is a dot, two is an object with weight. Sliding it is a
   background-position transition, which animates smoothly and needs no extra
   markup. */
.setrow {
  display: flex; align-items: center; gap: 12px;
  padding: 6px 2px;
}
.setrow-lbl { font-size: 13.5px; font-weight: 600; color: var(--muted); }

/* ---- the toggles ------------------------------------------------------
   Three glass cards with a pill switch on each — a preference wearing the
   weight of an action, and the page read as a stack of panels because of it.
   A disc that fills when it is on is the gesture this app already teaches on
   the Log page's period bubble. Same idea, smaller, and no card around it. */
.tog {
  display: flex; align-items: center; gap: 13px;
  /* 6px of side padding, not 2px: the lit halo is a box-shadow that reaches
     ~4px past the disc, and `#main, .pane { overflow-x: clip }` a few hundred
     lines up will shave it flat on the left otherwise. */
  padding: 6px 6px;
  cursor: pointer;
  font-size: 13.5px; font-weight: 600; line-height: 1.3;
  color: var(--plum);
}
.tog input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; }
/* Same disc as the Log page's row. The icon carries the colour at all times —
   a control whose icon greys out when off reads as DISABLED, not as off, and
   at a 22% tint neither state was legible anyway. So: white when it is off,
   and a halo of its own colour when it is on. The colour never moves, the
   light around it does. */
.tog { --tone: var(--violet); }
.tog-aff { --tone: #F2A93B; }   /* the sun, matching the Dose bubble's amber */
.tog-spl { --tone: var(--rose); }

.tog-disc {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 46px; height: 46px;
  border-radius: 50%;
  background: linear-gradient(165deg, #FFFFFF 0%, #FBF7FD 100%);
  box-shadow: var(--clay-sm);
  color: var(--tone);            /* always. on, off, either way. */
  transition: background var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.tog-disc svg { width: 21px; height: 21px; }
/* Lit: a ring of its own colour and a pool of it underneath. Both are
   box-shadows rather than a border or an outline, because those two get
   clipped by the pane's overflow-x and a glow that is flat on one side is
   worse than no glow. */
.tog input:checked + .tog-disc {
  background: linear-gradient(165deg,
              #FFFFFF 0%,
              color-mix(in srgb, var(--tone) 16%, #FFFFFF) 100%);
  box-shadow: var(--clay-sm),
              0 0 0 3px color-mix(in srgb, var(--tone) 26%, transparent),
              0 8px 20px -8px color-mix(in srgb, var(--tone) 70%, transparent);
}
.tog input:checked ~ .tog-text { color: var(--wine); }
.tog:active .tog-disc { transform: scale(.94); box-shadow: var(--clay-sm-press); }
.tog input:focus-visible + .tog-disc {
  outline: none;
  box-shadow: var(--clay-sm), 0 0 0 3px rgba(255,255,255,.9),
              0 0 0 6px rgba(209, 105, 154, .55);
}
@media (prefers-reduced-motion: reduce) { .tog-disc { transition: none; } }

/* ---------------------------------------------------------- Hero input

   The weight is the reason she opened the sheet. Giving it the same visual
   weight as "Left thigh" is what made this read as a form rather than an
   instrument. */

.hero-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
.nudge {
  width: 46px; height: 46px;
  flex-shrink: 0;
  border: 1.5px solid var(--border);
  border-radius: 50%;
  background: var(--white);
  color: var(--violet);
  font-size: 22px;
  font-weight: 700;
  font-family: inherit;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--shadow-card);
  transition: transform var(--dur-press) var(--ease-out),
              border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.nudge:active { transform: scale(0.9); border-color: var(--rose); color: var(--rose); }
.nudge:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; }

.hero-val {
  display: inline-flex;
  align-items: baseline;
  justify-content: center;
  gap: 4px;
  min-width: 0;
}
.hero-val input {
  width: 4.2ch;
  padding: 0;
  border: none;
  background: transparent;
  font-family: var(--font-display);
  font-size: 54px;
  font-weight: 800;
  line-height: 1.05;
  text-align: center;
  color: var(--wine);
  font-variant-numeric: tabular-nums;
  letter-spacing: -1px;
}
.hero-val input::placeholder { color: var(--divider-alt); }
.hero-val input:focus { outline: none; }
.hero-val input::-webkit-outer-spin-button,
.hero-val input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.hero-val input { -moz-appearance: textfield; appearance: textfield; }
.hero-unit {
  font-size: 19px;
  font-weight: 700;
  color: var(--muted-soft);
}

.chips {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 7px;
  margin-top: 13px;
}
.chips button {
  padding: 9px 4px;
  border: 1.5px solid var(--border);
  border-radius: 999px;
  background: var(--white);
  color: var(--muted);
  font-size: 13px;
  font-weight: 700;
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out),
              background-color var(--dur-fast) ease, color var(--dur-fast) ease,
              border-color var(--dur-fast) ease;
}
.chips button:active {
  transform: scale(0.94);
  background: var(--bg-top);
  border-color: var(--rose);
  color: var(--magenta);
}
.chips button:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; }

/* A number that just changed gets a beat of acknowledgement. */
.hero-val input.bumped { animation: bump 320ms var(--ease-out); }
@keyframes bump {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.07); }
  100% { transform: scale(1); }
}

.sect-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 0 12px;
}
.sect-head span {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--muted-soft);
  white-space: nowrap;
}
.sect-head::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--divider), transparent);
}

/* ---------------------------------------------------------- Photos */

/* The add/replace button and its dashed drop target are gone — tapping the
   photo bubble in .log-bubbles goes straight to the file picker, and the
   thumbnail it produces fills that same bubble. See THE BUBBLE ROW below
   for .photo-thumb and .photo-x as they exist now, scoped to .bub-photo. */

/* Thumbnail inside a history row */
.detail-photo {
  grid-column: 1 / -1;
  display: flex;
  gap: 10px;
  align-items: center;
  padding-top: 4px;
}
.detail-photo img {
  width: 56px; height: 56px;
  border-radius: 10px;
  object-fit: cover;
  cursor: pointer;
  box-shadow: var(--shadow-card);
  transition: transform var(--dur-press) var(--ease-out);
}
.detail-photo img:active { transform: scale(0.95); }
.detail-photo .cap { font-size: 12px; color: var(--muted-soft); }

.photo-viewer {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: rgba(58, 46, 77, 0.94);
  display: none;
  align-items: center;
  justify-content: center;
  padding: calc(20px + var(--safe-top)) 16px calc(20px + var(--safe-bottom));
}
.photo-viewer.open { display: flex; animation: chart-fade var(--dur-med) var(--ease-out); }
.photo-viewer img { max-width: 100%; max-height: 100%; border-radius: 14px; display: block; }
.viewer-close {
  position: absolute;
  top: calc(14px + var(--safe-top)); right: 16px;
  width: 36px; height: 36px;
  border: none; border-radius: 50%;
  background: rgba(255, 255, 255, 0.16);
  color: var(--white);
  font-size: 15px;
  cursor: pointer;
}
.viewer-cap {
  position: absolute;
  bottom: calc(18px + var(--safe-bottom));
  left: 0; right: 0;
  text-align: center;
  color: var(--white);
  font-size: 13px;
  opacity: 0.85;
}

/* ---------------------------------------------------------- Goal reached */

 /* The card gives once, the way you'd squeeze someone's arm — and that is now
   the WHOLE of the goal-reached moment. It used to be this plus a toast with a
   champagne emoji and thirty-four pieces of confetti, fired on a scale reading,
   in an app whose argument is that the scale is not the result. The confetti is
   gone. maybeGoalReached() adds .reached for 800ms against this 700ms pop, and
   guards reducedMotion() itself, so no media query is needed here. */
#summaryCard.reached { animation: card-pop 700ms var(--ease-out); }
@keyframes card-pop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.025); }
  100% { transform: scale(1); }
}

/* ---------------------------------------------------------- Buttons */

.btn {
  width: 100%;
  padding: 13px;
  border: none;
  border-radius: 12px;
  font-size: 15px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out),
              box-shadow var(--dur-press) var(--ease-out),
              opacity var(--dur-press) ease;
}
.btn:active { transform: scale(0.97); }
.btn:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; }

.btn-primary {
  background: linear-gradient(135deg, var(--violet), var(--rose));
  color: var(--white);
  box-shadow: var(--shadow-lift);
}
.btn-secondary {
  background: var(--white);
  color: var(--violet);
  border: 1.5px solid var(--lilac);
  margin-top: 8px;
}
.btn-danger {
  background: var(--white);
  color: var(--danger);
  border: 1.5px solid var(--divider-alt);
  margin-top: 8px;
}
.btn-ghost {
  background: transparent;
  border: none;
  color: var(--ghost);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  padding: 10px;
  cursor: pointer;
  border-radius: 8px;
  transition: transform var(--dur-press) var(--ease-out), opacity var(--dur-press) ease;
}
.btn-ghost:active { transform: scale(0.97); opacity: 0.7; }
.btn-ghost:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; }

.row-actions {
  display: flex;
  gap: 10px;
  margin-top: 4px;
}
.row-actions .btn-ghost { flex: 1; text-align: center; }

/* ---------------------------------------------------------- History */

/* The ledger runs from day at the top to dusk at the bottom. Each row gets a
   --dusk of 0..1 from its position in the list (set in renderHistory), which
   fades in a sunset wash over the glass. The wash is capped well below full
   strength — past about 0.55 the wine text stops holding against it, and a
   ledger you cannot read is a poor trade for a gradient. */
.week-item {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 13px 14px;
  margin: 0 0 8px;
  cursor: pointer;
  border: 1.5px solid rgba(255, 255, 255, .68);
  border-radius: 16px;
  background: rgba(255, 255, 255, .50);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
          backdrop-filter: blur(14px) saturate(150%);
  box-shadow: 0 3px 10px -5px rgba(138, 21, 80, .20),
              inset 0 1px 0 rgba(255, 255, 255, .8);
  width: 100%;
  text-align: left;
  font-family: inherit;
  color: inherit;
  transition: background-color var(--dur-fast) ease, transform var(--dur-press) var(--ease-out);
}
.week-item:last-of-type { margin-bottom: 0; }
/* the sunset wash itself — a separate layer so the glass underneath keeps its
   blur and the tint can animate independently */
.week-item::before {
  content: '';
  position: absolute; inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(148deg,
    rgba(255, 190, 96, .60) 0%,
    rgba(240, 128, 150, .58) 46%,
    rgba(150, 86, 170, .60) 100%);
  opacity: var(--dusk, 0);
  transition: opacity var(--dur-travel) var(--ease-out);
}
.week-item > * { position: relative; }
.week-item:active { transform: scale(0.985); box-shadow: inset 0 2px 6px rgba(138,21,80,.16); }
.week-item:focus-visible { outline: 2.5px solid var(--rose); outline-offset: -2px; }
.week-item[aria-expanded="true"] { background: rgba(255,255,255,.74); border-color: rgba(209,105,154,.40); }

.wk-left { min-width: 0; }
.wk-date { display: block; font-size: 14px; font-weight: 700; color: var(--wine); }
.wk-sub  { display: block; font-size: 12px; color: var(--muted-soft); margin-top: 2px; }

.wk-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.wk-badge {
  background: rgba(255, 255, 255, .82);
  color: var(--magenta);
  font-size: 13px;
  font-weight: 700;
  padding: 5px 10px;
  border-radius: 20px;
  white-space: nowrap;
}
.wk-chev {
  width: 8px; height: 8px;
  border-right: 2px solid var(--muted-soft);
  border-bottom: 2px solid var(--muted-soft);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur-fast) var(--ease-out);
}
.week-item[aria-expanded="true"] .wk-chev {
  transform: rotate(225deg) translate(-2px, -2px);
}

.detail-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-med) var(--ease-out);
}
.detail-wrap.open { grid-template-rows: 1fr; }
.detail-inner { overflow: hidden; min-height: 0; }

.detail-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 10px;
  padding: 10px 4px 4px;
  border-top: 1px dashed var(--divider-alt);
}
.detail-grid .dv { font-size: 13px; color: var(--muted); }
.detail-grid .dv b { color: var(--wine); font-weight: 700; }
.detail-grid .dv .delta { font-size: 12px; }
.detail-grid .full { grid-column: 1 / -1; }

.empty {
  text-align: center;
  padding: 30px 10px;
  color: var(--muted-soft);
  font-size: 14px;
  line-height: 1.5;
}
.empty i { font-size: 34px; display: block; margin-bottom: 10px; opacity: 0.6; font-style: normal; }

.err {
  color: var(--danger);
  font-size: 12.5px;
  margin: -4px 0 10px;
  display: none;
}
.err.show { display: block; animation: shake 320ms var(--ease-out); }
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-4px); }
  75%      { transform: translateX(4px); }
}

.warn {
  background: var(--bg-top);
  color: var(--magenta);
  font-size: 12.5px;
  border-radius: 8px;
  padding: 9px 11px;
  margin: -2px 0 12px;
  display: none;
  line-height: 1.4;
}
.warn.show { display: block; }

.hint { font-size: 12px; color: var(--muted-soft); margin: -4px 0 12px; line-height: 1.5; }

/* ---------------------------------------------------------- Settings */

h4.set-head {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--magenta);
  margin: 20px 0 8px;
}
h4.set-head:first-of-type { margin-top: 6px; }


/* A preference, not an action. This was a stacked label above a full-width
   two-column slab, which gave "which units" the same footprint on the page as
   the things you actually press. It is a pill sized to its own content now,
   sitting on its label's line — and "kg" says what "Kilograms (kg)" said,
   which is most of why it needed the width in the first place. */
.seg {
  display: grid;
  grid-template-columns: auto auto;
  gap: 4px;
  padding: 3px;
  margin-left: auto;
  /* the same glass the toggles sit on, so Settings reads as one set */
  background: rgba(255, 255, 255, .52);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
          backdrop-filter: blur(14px) saturate(150%);
  border: 1.5px solid rgba(255, 255, 255, .72);
  border-radius: 999px;
  box-shadow: inset 0 2px 6px rgba(138, 21, 80, .10);
}
.seg button {
  min-width: 48px;
  padding: 7px 15px;
  border: none;
  border-radius: 999px;
  background: transparent;
  /* transparent gradient, matching the selected state's shape (135deg, 2 stops) —
     background-image can only animate between two gradients of the same
     structure, "none" never interpolates to a gradient */
  background-image: linear-gradient(135deg, transparent, transparent);
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  transition: background-image var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out),
              color var(--dur-med) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.seg button:active { transform: scale(0.97); }
.seg button[aria-pressed="true"] {
  background: linear-gradient(135deg, var(--violet), var(--rose));
  color: var(--white);
  box-shadow: var(--shadow-card);
}
.seg button:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; }

/* ---------------------------------------------------------- Toast */

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(24px + var(--safe-bottom));
  transform: translate(-50%, 20px) scale(0.96);
  background: var(--plum);
  color: var(--white);
  font-size: 13.5px;
  font-weight: 600;
  padding: 11px 18px;
  border-radius: 999px;
  box-shadow: var(--shadow-lift);
  z-index: 40;
  opacity: 0;
  pointer-events: none;
  max-width: calc(100vw - 40px);
  text-align: center;
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.toast.show { opacity: 1; transform: translate(-50%, 0) scale(1); }

/* ---------------------------------------------------------- Splash */

.splash {
  position: fixed;
  inset: 0;
  z-index: 50;
  overflow: hidden;
  /* 180ms, not 800. The app is already up and running underneath by the time
     this starts — appReady() fires at the top of dismissSplash — so every one
     of those milliseconds was a translucent beach lying over a working screen.
     The exit swell is what makes the departure feel authored; the fade only
     has to get out of the way. */
  transition: opacity 180ms var(--ease-out), visibility 180ms;
}
.splash.hide { opacity: 0; visibility: hidden; pointer-events: none; }

/* The sky is its own layer so it can drift under everything else. */
.sky {
  position: absolute;
  inset: -20%;
  background:
    radial-gradient(60% 45% at 22% 18%, rgba(255,255,255,.75), rgba(255,255,255,0) 60%),
    linear-gradient(112deg, #FEF5AC 0%, #FDEAD2 24%, #FBE4EE 46%, #E8A0BF 74%, #D1699A 100%);
}
.splash.animate .sky { animation: sky-drift 14s var(--ease-in-out) infinite alternate; }
@keyframes sky-drift {
  from { transform: translate3d(-1.5%, -1%, 0) scale(1.04); }
  to   { transform: translate3d(1.5%, 1%, 0) scale(1.10); }
}


/* Duplicate @keyframes do not merge: the last one in the file silently wins for
   every rule that names it, which is how an edit to a dead block once appeared
   to change a live one. One definition each, in SPLASH, LIVE at the bottom. */

/* --- lettering ------------------------------------------------------- */
.word {
  position: absolute;
  left: 50%;
  top: 46%;
  width: 88%;
  max-width: 560px;
  transform: translate(-50%, -50%);
}
.word img { width: 100%; height: auto; display: block; }

/* The lettering's entrance, its shine, the margarita's draw-on and the bubble
   layer all live in SPLASH, LIVE at the foot of this file. */


/* --- chrome ----------------------------------------------------------- */
.splash-dots {
  position: absolute;
  left: 0; right: 0;
  bottom: calc(74px + var(--safe-bottom));
  text-align: center;
}
.splash-dots i {
  display: inline-block;
  width: 9px; height: 9px;
  margin: 0 6px;
  border-radius: 50%;
  background: var(--white);
  opacity: .35;
}
.splash.animate .splash-dots i { animation: dot-pulse 1500ms var(--ease-in-out) infinite; }
.splash-dots i:nth-child(2) { animation-delay: 180ms; }
.splash-dots i:nth-child(3) { animation-delay: 360ms; }
@keyframes dot-pulse {
  0%,100% { opacity: .3; transform: scale(.85); }
  50%     { opacity: .95; transform: scale(1.15); }
}

.splash-skip {
  position: absolute;
  bottom: calc(34px + var(--safe-bottom));
  left: 0; right: 0;
  text-align: center;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--wine);
  opacity: .48;
}
.splash.animate .splash-skip { animation: skip-in 700ms var(--ease-out) 2400ms backwards; }
@keyframes skip-in { from { opacity: 0; } to { opacity: .48; } }

@media (prefers-reduced-motion: reduce) {
  .sky, .word, .motes i, .splash-dots i, .splash-skip,
  /* Resting state must be the finished state, never the first frame. */
  .word { opacity: 1; }
  .word { transform: translate(-50%, -50%); }
  .motes { display: none; }
  .splash-skip { opacity: .48; }
}

/* ---------------------------------------------------------- Motion: state changes

   Every animation below marks a *change of state* — a screen swapping, a
   number moving, a row that just took an edit. None of them decorate a
   resting screen, and none of them gate whether content is visible. */

/* Screen swap: baseline ↔ main. Occasional, so it earns a transition;
   without one the whole page just teleports and reads as a page load. */
.screen-enter {
  animation: screen-in var(--dur-med) var(--ease-out);
}
@keyframes screen-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* The chart draws itself in. Seen rarely (once per app open), and it makes
   the eye travel left-to-right along the actual timeline. */
.spark .line.draw {
  animation: draw 800ms var(--ease-out) forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }

.spark .fill.fade,
.spark .trend.fade,
.spark .dot.fade {
  animation: chart-fade 420ms var(--ease-out) 380ms backwards;
}
@keyframes chart-fade { from { opacity: 0; } }
.spark .trend.fade { animation-name: chart-fade-trend; }
@keyframes chart-fade-trend { from { opacity: 0; } to { opacity: 0.75; } }

/* The row you just saved or edited identifies itself, then lets go.
   Without this a save drops you back to a list with no sign of what moved. */
.week-item.just-saved {
  animation: row-flash 1600ms var(--ease-out);
}
@keyframes row-flash {
  0%   { background: var(--bg-top); }
  60%  { background: var(--bg-top); }
  100% { background: transparent; }
}

/* A value that changed since the last render gets a single soft pulse.
   Purely to answer "which of these actually moved?" */
.mini.changed .mv {
  animation: value-pulse 900ms var(--ease-out);
}
@keyframes value-pulse {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.09); }
  100% { transform: scale(1); }
}
.mini .mv { display: inline-block; transform-origin: left center; }

/* ---------------------------------------------------------- Reduced motion */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    /* Without this, a staggered delay still hides `backwards`-filled
       content for the length of the delay. */
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ============================================================
   REDESIGN — main screen
   The purple bar ate 100px and said nothing. The number it was
   sitting above is the reason she opened the app, so the number
   gets the top of the screen and the chrome gets out of the way.
   ============================================================ */


.masthead {
  padding: calc(20px + var(--safe-top)) 0 4px;
  margin-bottom: 0;
  background: none;
}
.masthead .brand {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 800;
  letter-spacing: 0.4px;
  color: var(--magenta);
  opacity: 0.9;
}
.masthead .when {
  font-size: 12.5px;
  color: var(--muted-soft);
  margin-top: 2px;
}

/* --- hero ------------------------------------------------------------- */

.hero { padding: 22px 2px 6px; }
.hero-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.3px;
  text-transform: uppercase;
  color: var(--muted-soft);
  margin-bottom: 8px;
}
.hero-figure {
  font-family: var(--font-display);
  font-size: clamp(56px, 17vw, 82px);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -2.5px;
  color: var(--wine);
  margin-bottom: 10px;
}
.hero-line { font-size: 13px; color: var(--muted); line-height: 1.5; }
.hero-line.dim { color: var(--muted-soft); font-size: 12.5px; }

/* --- chart, edge to edge --------------------------------------------- */

.chart-bleed {
  margin: 18px calc(-16px - var(--safe-right)) 20px calc(-16px - var(--safe-left));
  padding: 0 0 6px;
  background: none;
  box-shadow: none;
  border-radius: 0;
}
.chart-bleed .chart-head { padding: 0 calc(18px + var(--safe-left)) 8px; }
.chart-bleed .chart-head .ct {
  font-size: 11px; font-weight: 700; letter-spacing: 1.2px;
  text-transform: uppercase; color: var(--muted-soft);
}
.chart-bleed .chart-head .cr { color: var(--muted); font-weight: 600; }
.chart-bleed .spark { height: 132px; }
.chart-bleed .chart-legend { padding: 6px calc(18px + var(--safe-left)) 0; }

/* --- measurement tiles, now on the page not a card -------------------- */

.summary-grid { gap: 9px; margin-top: 0; }
.summary-grid .mini {
  background: var(--white);
  border-radius: 14px;
  padding: 12px 13px;
  box-shadow: var(--shadow-card);
}
.summary-grid .mini .mv { font-size: 17px; letter-spacing: -0.3px; }
.summary-grid .mini .ml {
  font-size: 10.5px; opacity: 1; color: var(--muted-soft);
  text-transform: uppercase; letter-spacing: 0.7px; margin-top: 3px;
}

/* --- history ---------------------------------------------------------- */

.history {
  background: var(--white);
  border-radius: 18px;
  padding: 4px 16px;
  box-shadow: var(--shadow-card);
  margin-bottom: 18px;
}


/* --- insights, quieter on the open page ------------------------------- */

.insight-card { border-radius: 16px; }

/* --- onboarding card gets the same air -------------------------------- */

#onboarding .card { border-radius: 20px; padding: 20px 18px; margin-top: 8px; }

/* ============================================================
   SUNSET + MATERIALS
   The palette had drifted pastel because the app only ever used
   its two palest members. Same locked hexes — the butter and the
   rose are simply allowed back in, which is where the sunset was
   hiding all along.
   ============================================================ */

body {
  background:
    radial-gradient(120% 62% at 50% 100%, rgba(209,105,154,.34) 0%, rgba(209,105,154,0) 62%),
    radial-gradient(105% 48% at 82% 4%,  rgba(254,245,172,.85) 0%, rgba(254,245,172,0) 66%),
    linear-gradient(178deg,
      #FEF5AC 0%, #FDEBD5 12%, #FBE4EE 34%, #F5EEFA 56%, #F3DCEA 76%, #E8A0BF 100%);
  background-attachment: fixed;
  background-size: cover;
}

/* Masthead sits on the warm end, so it needs the deeper ink. */
.masthead .brand { color: var(--magenta); opacity: 1; }
.masthead .when  { color: var(--muted); }
.hero-eyebrow    { color: var(--muted); }
.hero-line       { color: var(--plum); }
.hero-line.dim   { color: var(--muted); }

/* Cards float ON the sunset rather than sitting in a pale box. */
.card, .history, .insight-card, .banner, .summary-grid .mini {
  background: rgba(255, 255, 255, 0.82);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
          backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 6px 22px rgba(138, 21, 80, 0.09);
}
.insight-card { border-left-width: 3px; }

/* Settings and the baseline editor are app surfaces, not dialogs bolted
   on the side: same masthead-style heading, same material, same rhythm. */
h4.set-head { color: var(--magenta); }

#onboarding .card {
  background: rgba(255, 255, 255, 0.86);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
          backdrop-filter: blur(20px) saturate(160%);
  border: 1px solid rgba(255, 255, 255, 0.65);
}
#onboarding .section-title {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 800;
  letter-spacing: -0.3px;
  text-transform: none;
  color: var(--wine);
}

@media (prefers-reduced-transparency: reduce) {
  .card, .history, .insight-card, .banner, .summary-grid .mini,
  #onboarding .card {
    background: var(--white);
    -webkit-backdrop-filter: none;
            backdrop-filter: none;
  }
}

/* ============================================================
   CLAY — soft-extruded surfaces, coloured tabs, raised action.
   Taken from the 3D reference: matte plastic, light from the
   top-left, a bright inner rim, and a deep soft contact shadow.
   Orange joins the set from the margarita in Bridget's poster,
   and is used sparingly — Settings only.
   ============================================================ */

:root {
  --amber:  #F2A93B;   /* the sun in her artwork */
  --tang:   #E8823C;   /* the margarita line     */

  /* Light from the top-left, a lit rim, and a soft contact shadow. */
  --clay:
    0 14px 26px -10px rgba(138, 21, 80, .30),
    0 6px 12px -6px rgba(138, 21, 80, .18),
    inset 0 2px 0 rgba(255, 255, 255, .70),
    inset 0 -6px 12px rgba(138, 21, 80, .10);
  --clay-press:
    0 5px 12px -6px rgba(138, 21, 80, .30),
    inset 0 2px 6px rgba(138, 21, 80, .18),
    inset 0 1px 0 rgba(255, 255, 255, .35);
}

/* The poster is gone — the scene is drawn live. Its rules and its 7200ms
   `poster-in` (which pinned every layer at opacity 0 through `backwards`) are
   removed rather than overridden. */

/* --- panes ------------------------------------------------------------ */

.pane { display: none; }
.pane.is-on { display: block; }
.pane-in { animation: pane-rise var(--dur-sheet) var(--ease-out); }
@keyframes pane-rise {
  from { opacity: 0; transform: translateY(10px) scale(.992); }
}

/* --- clay tab bar ----------------------------------------------------- */


.tab-ico {
  width: 42px; height: 30px;
  display: grid;
  place-items: center;
  border-radius: 15px;
  background: transparent;
  color: currentColor;
}
.tab-label {
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: .5px;
  text-transform: uppercase;
  transition: opacity var(--dur-med) var(--ease-out),
              color var(--dur-med) var(--ease-out),
              text-shadow var(--dur-med) var(--ease-out);
}
.tab:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; border-radius: 14px; }

/* Selected: the icon plate inflates into a coloured clay pill. */
.tab.is-on .tab-ico {
  color: var(--white);
  transform: translateY(-2px);
  box-shadow: var(--clay);
}
.tab.t-violet.is-on { color: var(--violet); }
.tab.t-rose.is-on   { color: var(--rose); }
.tab.t-amber.is-on  { color: var(--tang); }
/* The three .tab-ico gradient plates that used to live here are gone — the
   clip is the only coloured surface; these drew a square on top of it. */

/* --- clay lift for the primary controls ------------------------------- */

.btn-primary, .banner-cta, .seg button[aria-pressed="true"] { box-shadow: var(--clay); }
.btn-primary:active, .banner-cta:active { box-shadow: var(--clay-press); }

@media (prefers-reduced-motion: reduce) {
  .pane-in { animation: none; }
}

/* ============================================================
   MONEY CLIP — one clay ribbon, a coloured clip that slides.
   Fixes: the raised + read as permanently pressed, the four
   controls were three different sizes, and the tools row sat
   under the floating bar duplicating Settings' own buttons.
   ============================================================ */

/* Room to breathe. The old value left the last card jammed under the bar. */
body { padding-bottom: calc(150px + var(--safe-bottom)); }

.tabbar {
  position: fixed;
  left: 50%;
  bottom: calc(16px + var(--safe-bottom));
  transform: translateX(-50%);
  z-index: 40;                 /* above every pane, always hittable */
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  align-items: stretch;
  gap: 0;
  width: min(430px, calc(100vw - 24px));
  padding: 5px;
  border-radius: 26px;
  background: linear-gradient(180deg, rgba(255,255,255,.94), rgba(255,255,255,.80));
  -webkit-backdrop-filter: blur(26px) saturate(180%);
          backdrop-filter: blur(26px) saturate(180%);
  border: 1px solid rgba(255,255,255,.85);
  box-shadow:
    0 22px 44px -18px rgba(138,21,80,.40),
    0 6px 14px -8px rgba(138,21,80,.24),
    inset 0 2px 0 rgba(255,255,255,.95);
}

/* The clip itself: a clay lozenge that slides between tabs. */
.clip {
  position: absolute;
  top: 8px;
  left: 8px;
  height: calc(100% - 16px);
  width: calc((100% - 16px) / 4);
  border-radius: 20px;
  background: linear-gradient(165deg, #B9A6E4, var(--violet));
  box-shadow: var(--clay);
  /* the clip and the pane it selects are one gesture and share --dur-travel;
     the tone change is a colour change and joins the colour family at --dur-med */
  transition: transform var(--dur-travel) var(--ease-drawer),
              background var(--dur-med) var(--ease-out);
  pointer-events: none;
}

/* Every tab is now exactly the same footprint — no raised outlier. */
.tab {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  margin: 0;
  padding: 9px 2px 8px;
  min-height: 56px;
  border: none;
  background: none;
  border-radius: 20px;
  font-family: inherit;
  cursor: pointer;
  color: var(--muted-soft);
  transition: color var(--dur-med) var(--ease-out);
}
.tab-ico {
  width: 24px; height: 24px;
  display: grid; place-items: center;
  background: none;
  box-shadow: none;
  border-radius: 0;
  transform: none;
  /* the override below had dropped background-color/box-shadow/color, so a
     selected tab's clay pill changed colour instantly while its transform
     animated — restored here, all at the colour-family duration.
     background-color is gone again: it never actually changes (the gradient
     plates that used it were deleted), and filter is added since the
     is-on drop-shadow was snapping in one frame while these animated. */
  transition: transform var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out),
              color var(--dur-med) var(--ease-out),
              filter var(--dur-med) var(--ease-out);
}
.tab-ico svg { width: 21px; height: 21px; }
.tab-label {
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: .55px;
  text-transform: uppercase;
}
.tab.is-on { color: var(--white); }
.tab.is-on .tab-ico { transform: translateY(-1px) scale(1.06); }
.tab:active .tab-ico { transform: scale(.88); }
.tab:focus-visible { outline: 2.5px solid var(--wine); outline-offset: -3px; }

/* --- quick actions, matched to each other ----------------------------- */

.quick { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0 18px; }
.quick-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  padding: 14px 10px;
  border-radius: 18px;
  border: 1px solid rgba(255,255,255,.7);
  background: rgba(255,255,255,.82);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
          backdrop-filter: blur(18px) saturate(150%);
  box-shadow: 0 8px 22px -10px rgba(138,21,80,.24), inset 0 2px 0 rgba(255,255,255,.9);
  color: var(--muted);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 700;
  cursor: pointer;
  transition: transform var(--dur-press) var(--ease-out), box-shadow var(--dur-press) var(--ease-out);
}
.quick-btn:active { transform: scale(.96); box-shadow: var(--clay-press); }
.quick-ico {
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 11px;
  color: var(--white);
  box-shadow: var(--clay);
  flex-shrink: 0;
}
.quick-ico svg { width: 16px; height: 16px; }
.quick-btn.t-violet .quick-ico { background: linear-gradient(160deg, #A99AD6, var(--violet)); }
.quick-btn.t-rose   .quick-ico { background: linear-gradient(160deg, var(--blush), var(--rose)); }

/* --- panes slide, they do not merely appear --------------------------- */

.pane-in.from-right { animation: slide-from-right var(--dur-travel) var(--ease-drawer); }
.pane-in.from-left  { animation: slide-from-left  var(--dur-travel) var(--ease-drawer); }
/* The pane slide must not be able to widen the document.
   `slide-from-right` translates the incoming pane +38px, which overflows the
   RIGHT edge and creates real scrollable width; `slide-from-left` translates
   -38px, and browsers do not make leftward overflow scrollable in LTR. That
   asymmetry is the whole bug — going forward through the tabs pushed the bar
   off screen, going backward never did.
   `clip`, not `hidden`: hidden would make this a scroll container and break
   the position:sticky element inside it. */
#main, .pane { overflow-x: clip; }

@keyframes slide-from-right {
  from { opacity: 0; transform: translateX(38px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes slide-from-left {
  from { opacity: 0; transform: translateX(-38px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* The old `bubble-out` exit is gone. It could never play: seedMotes() wrote
   animation-duration and animation-delay as INLINE styles, and an inline value
   beats a stylesheet one — so swapping the animation-name to bubble-out left it
   running at the mote's own 5–8s rise duration behind a 2.6s delay, while the
   splash was already fading at 620ms. The bubbles are driven by the Web
   Animations API now, which takes its timing from the call and cannot be
   overridden by a leftover inline property. */

@media (prefers-reduced-motion: reduce) {
  .clip { transition: none; }
  .pane-in.from-right, .pane-in.from-left { animation: none; }
}

/* ============================================================
   iPHONE SIZING
   `inset: 0` on a fixed element resolves against iOS Safari's
   LAYOUT viewport, which is taller than the visible area while
   the browser chrome is showing — so the artwork was being
   centred against a box that extended below the screen and
   landed low. Pin to the small/dynamic viewport instead.
   ============================================================ */

.splash {
  top: 0; left: 0; right: 0; bottom: auto;
  width: 100vw;
  height: 100vh;                      /* fallback */
  height: 100svh;                     /* the visible area, chrome included */
  height: 100dvh;                     /* follows the chrome as it hides */
  min-height: -webkit-fill-available; /* older iOS Safari */
}


/* Chrome sits against the true bottom of the visible viewport. */
.splash-dots { bottom: calc(78px + var(--safe-bottom)); }
.splash-skip { bottom: calc(38px + var(--safe-bottom)); }

/* ============================================================
   LOG IS A PANE, NOT A POPUP
   A modal over the top made sense when the app was one screen.
   With a tab bar it read as an interruption of the app rather
   than a part of it — and it covered the very bar you navigate
   with. Logging is now the fourth pane and behaves like the
   other three.
   ============================================================ */

.pane-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 20px 2px 14px;
}
.pane-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 27px;
  font-weight: 800;
  letter-spacing: -0.6px;
  line-height: 1.05;
  color: var(--wine);
}
.pane-back {
  border: none;
  background: none;
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ghost);
  cursor: pointer;
  padding: 6px 2px;
  flex-shrink: 0;
  transition: transform var(--dur-press) var(--ease-out), opacity var(--dur-press) ease;
}
.pane-back:active { transform: scale(.95); opacity: .65; }
.pane-back:focus-visible { outline: 2.5px solid var(--rose); outline-offset: 2px; border-radius: 6px; }

/* The log pane's controls sit on the sunset like every other pane's do. */
/* `> input`, not ` input`. The period switch lives inside a .field, wrapped in
   its label, and this is the `background` SHORTHAND — which resets
   background-image to none. The switch paints its knob as two radial gradients
   on background-image, so this rule was quietly deleting the knob and leaving
   a bare pill. The child combinator reaches the text fields and not the
   switch. */
#pane-log .field > input,
#pane-log .field > textarea { background: rgba(255,255,255,.86); }
#pane-log .howto         { background: rgba(255,255,255,.72); }
.btn-primary { margin-top: 4px; }
#pane-log .btn-danger  { margin-bottom: 4px; }

/* Settings gets the same heading, so the two read as siblings. */
#pane-settings h4.set-head:first-of-type { margin-top: 2px; }

/* ============================================================
   THE CLIP CURLS OVER THE BAR
   In the reference the coloured ribbon wraps the edge of the
   white card and casts onto it. The clip now rises above the
   bar's top edge, takes a highlight along that lip, and drops
   a shadow back down onto the white — which is what reads as
   depth rather than a coloured rectangle sitting inside.
   ============================================================ */


.clip {
  border-radius: 20px;
  background: linear-gradient(178deg, #C6B4EC 0%, #A996DC 26%, var(--violet) 100%);
  box-shadow:
    0 14px 22px -10px rgba(138, 21, 80, .42),   /* cast onto the white bar */
    0 3px 6px -2px rgba(138, 21, 80, .28),
    inset 0 3px 0 rgba(255, 255, 255, .78),     /* light along the lip     */
    inset 0 -10px 16px rgba(88, 20, 60, .20);   /* the underside rolls off */
}
.clip.to-history  { background: linear-gradient(178deg, #F3BDD3 0%, var(--blush) 28%, var(--rose) 100%); }
.clip.to-settings { background: linear-gradient(178deg, #FFD3AE 0%, #FFA97E 34%, #FB8A63 100%); }

/* Every tab now has real depth of its own, lit and shadowed. */
.tab .tab-ico {
  filter: drop-shadow(0 2px 2px rgba(138, 21, 80, .22));
}
.tab.is-on .tab-ico {
  filter: drop-shadow(0 2px 3px rgba(60, 10, 40, .38));
}
.tab .tab-label {
  text-shadow: 0 1px 0 rgba(255, 255, 255, .65);
}
/* the label needs clearing of the lip so the curl reads */
.tab { padding-top: 12px; }

/* The clip is the coloured element now, so everything riding on it is white.
   The older per-tone colour rules were written when the icon plate carried
   the colour, and they were still outranking the selected state. */
.tab.t-violet.is-on, .tab.t-rose.is-on, .tab.t-amber.is-on,
.tab.t-violet.is-on .tab-label, .tab.t-rose.is-on .tab-label,
.tab.t-amber.is-on .tab-label { color: var(--white); }


/* ============================================================
   SPLASH, IN THE APP'S OWN MATERIAL

   The splash used to be its own visual language: an inflatable
   balloon font raster, glossy line-art drawings, a neon halo.
   The app it opens is matte clay — rounded display type, soft
   extrusion, light from the top-left, and a deep contact shadow
   in magenta. Two different products, one after the other.

   Everything below is built from the same primitives as the tab
   bar and the segmented pills: --clay for the extrusion, --amber
   and --tang for the warm end of the locked palette, and the
   display face the rest of the app already sets its headings in.
   ============================================================ */

.splash-scene { position: absolute; inset: 0; display: block; overflow: hidden; }

.sky {
  inset: 0;
  /* The ramp had drifted to yellow-into-pink and lost both ends of the set.
     A sunset runs violet at the top through pink and butter to tang at the
     horizon — all four are in the locked palette and all four belong here. */
  background:
    radial-gradient(86% 52% at 24% 20%, rgba(255,255,255,.42), rgba(255,255,255,0) 62%),
    linear-gradient(168deg,
      #9B84C9   0%,
      var(--lilac)  14%,
      #E9A8D0  32%,
      var(--blush)  50%,
      #F6D5B2  70%,
      #F2DCA6  86%,
      #EFB388 100%);
}

/* --- sun: a clay disc, and clay rays ---------------------------------- */


@keyframes sun-turn    { to { transform: rotate(360deg); } }
@keyframes sun-breathe { 0%,100% { transform: scale(1); } 50% { transform: scale(1.045); } }
@keyframes clay-in {
  0%   { opacity: 0; transform: translate(-50%,-50%) scale(.74) translateY(14px); }
  70%  { opacity: 1; transform: translate(-50%,-50%) scale(1.035); }
  100% { opacity: 1; transform: translate(-50%,-50%) scale(1); }
}

/* --- lettering: her artwork, per-letter lip, matte -----------------------
   Back to her own letterforms. The Titan One pass was rounder as a shape but
   rendered flat — a geometric face with one gradient across it has no bulge,
   and no amount of shading on the outside fixes a letterform that is not
   itself inflated.

   The per-letter lip is cut from the artwork itself. Her lettering is a single
   flat raster whose letters physically touch, so it cannot be segmented — I
   tried, and the whole of "summer" is one connected blob. But her own shading
   darkens both the outer edge of every letter AND every join where two letters
   meet, so thresholding the luminance inside the glyphs yields exactly the
   outline the lip needs. That is splash-l*-lip.png; rebuild with
   tools/build-word-mask.py.

   Two lines, two pairs of images, so `summer` can carry more curve and its own
   motion than the line above it.
   ---------------------------------------------------------------------- */
.word {
  position: absolute; left: 50%; top: 50%;
  width: 100%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.2vh;
}


/* The shadows sit on the LINE, not on the masked spans: CSS applies filter
   BEFORE mask, so a filter on a masked element is drawn around its unmasked
   rectangle and then clipped away entirely. */
.word-line {
  filter:
    drop-shadow(0 3px 3px rgba(168, 66, 58, .24))
    drop-shadow(0 9px 12px rgba(150, 40, 92, .24))
    drop-shadow(0 18px 26px rgba(138, 21, 80, .26));
}

/* Each line arrives on its own beat, then keeps a slow easy roll — the joy is
   in the second line moving a little more than the first. */
.splash.animate .wl1 { animation: wl-in 860ms var(--ease-out) 340ms backwards,
                                  wl-roll-a 6.4s var(--ease-in-out) 1500ms infinite; }
.splash.animate .wl2 { animation: wl-in 860ms var(--ease-out) 560ms backwards,
                                  wl-roll-b 5.6s var(--ease-in-out) 1700ms infinite; }
@keyframes wl-in {
  0%   { opacity: 0; transform: scale(.66) translateY(20px) rotate(-5deg); }
  58%  { opacity: 1; transform: scale(1.06) translateY(-6px) rotate(1.5deg); }
  80%  { transform: scale(.98) translateY(2px) rotate(-.6deg); }
  100% { opacity: 1; transform: scale(1) translateY(0) rotate(0deg); }
}
@keyframes wl-roll-a {
  0%, 100% { transform: rotate(-1.4deg) translateY(0); }
  50%      { transform: rotate(.6deg) translateY(-5px); }
}
@keyframes wl-roll-b {
  0%, 100% { transform: rotate(1.6deg) translateY(0) scale(1); }
  50%      { transform: rotate(-1.9deg) translateY(5px) scale(1.022); }
}
@media (prefers-reduced-motion: reduce) {
  .splash.animate .wl1, .splash.animate .wl2 { animation: none; opacity: 1; }
}

/* ============================================================
   TRIAL — the living line drawing.
   Remove this block and the .splash-lines div in index.html to
   take it out; nothing else depends on either.
   ============================================================ */
.splash-lines {
  position: absolute; inset: 0;
  --lines-strength: .55;
  pointer-events: none;
  opacity: 0;
}
/* Near-white on `overlay` — this layer is only the scene now, and the scene
   sits high on the violet where that reads. The figure that needed a second,
   darker ink has been replaced by artwork (see .drifter). */
.sl-scene {
  color: rgba(255, 252, 246, .95);
  mix-blend-mode: overlay;
}
.splash-lines svg { position: absolute; inset: 0; width: 100%; height: 100%; }

.splash.animate .splash-lines { animation: lines-in 1200ms var(--ease-out) 600ms forwards; }
@keyframes lines-in { to { opacity: var(--lines-strength); } }

/* --- alive: every line draws itself on, holds, and leaves ---------------
   One shared cycle with staggered delays. `stroke-dasharray` is set per
   element from its own length via pathLength, so a short path and a long one
   travel at the same apparent speed instead of the long one crawling. */
/* `pathLength` is an SVG ATTRIBUTE, not a CSS property — set in CSS it is
   silently dropped, and `stroke-dasharray: 1` then means a one-pixel dash
   rather than "the whole path", so every line rendered as a faint dotted
   trail that never drew on. It is on the elements in index.html now, which
   normalises each path to length 1 so a short line and a long one draw at the
   same apparent speed. */
.sl-draw {
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
.splash.animate .sl-draw { animation: line-life 9s var(--ease-in-out) infinite backwards; }
@keyframes line-life {
  0%        { stroke-dashoffset: 1;  opacity: 0; }
  8%        { opacity: 1; }
  26%, 62%  { stroke-dashoffset: 0;  opacity: 1; }
  86%       { stroke-dashoffset: -1; opacity: 0; }
  100%      { stroke-dashoffset: -1; opacity: 0; }
}
/* The horizon, then the shoreline, then the foam behind it — 300ms apart, in
   the order the eye reads them. .d5 had no rule at all, so the foam fell back
   to 0ms and led the other two instead of trailing them. */
.splash.animate .d3  { animation-delay: 320ms; }
.splash.animate .d4  { animation-delay: 620ms; }
.splash.animate .d5  { animation-delay: 920ms; }

/* --- the water actually moves ------------------------------------------
   Each band is drawn far wider than the viewport and translated by exactly
   one wavelength, so the loop is seamless — a shorter path would visibly
   snap back at the wrap. */
.splash.animate .wv { animation: wave-drift linear infinite; }
.splash.animate .w1 { animation-duration: 9s;  }
.splash.animate .w2 { animation-duration: 12s; animation-direction: reverse; }
.splash.animate .w3 { animation-duration: 15s; }
.splash.animate .w4 { animation-duration: 11s; animation-direction: reverse; }
@keyframes wave-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(136px); }   /* one full q-curve pair */
}

@media (prefers-reduced-motion: reduce) {
  .splash-lines { opacity: var(--lines-strength); animation: none; }
  .sl-draw { stroke-dasharray: none; stroke-dashoffset: 0; animation: none !important; }
  .wv { animation: none !important; }
}

@keyframes glass-sway {
  0%,100% { transform: translate(-50%,-50%) rotate(0deg); }
  32%     { transform: translate(-50%,-50%) rotate(1.8deg); }
  68%     { transform: translate(-50%,-50%) rotate(-1.4deg); }
}

.fizz { position: absolute; left: 50%; top: 22%; width: 56%; height: 24%;
        transform: translateX(-50%); pointer-events: none; }
.fizz i { position: absolute; bottom: 0; width: 6px; height: 6px; border-radius: 50%;
          background: rgba(255,255,255,.9); opacity: 0; }
.splash.animate .fizz i { animation: fizz-up 2800ms var(--ease-out) infinite; }
@keyframes fizz-up {
  0%   { opacity: 0;   transform: translateY(0) scale(.4); }
  18%  { opacity: .85; }
  100% { opacity: 0;   transform: translateY(-104px) scale(1.15); }
}

@media (prefers-reduced-motion: reduce) {
  .fizz { display: none; }
}

/* ============================================================
   BUBBLES — a cascade running backwards
   They rise from the bottom edge and, part way up, each one
   fans into two or three smaller ones, which fan again. What
   starts as four bubbles is a screenful by the time the splash
   leaves, and they are what carries it away.

   Position, size and timing are set per bubble by app.js
   through the Web Animations API. Nothing here may set an
   animation on `.motes i`: the version before this one did, and
   the inline duration app.js was writing then overrode the
   exit's own timing, so the pop ran for eight seconds behind a
   fade that had already finished.
   ============================================================ */

.motes { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.motes i {
  position: absolute;
  left: 0; top: 0;
  border-radius: 50%;
  opacity: 0;                       /* the animation owns opacity */
  will-change: transform, opacity;
  background:
    /* the specular pin, up and to the left, as on the clay */
    radial-gradient(circle at 30% 26%,
      rgba(255,255,255,.98) 0 6%,
      rgba(255,255,255,.52) 13%,
      rgba(255,255,255,0)   34%),
    /* warm iridescence pooling in the belly */
    radial-gradient(circle at 70% 74%,
      rgba(255,190,226,.58) 0%,
      rgba(255,228,168,.32) 44%,
      rgba(255,255,255,0)   72%),
    /* the soap rim: a bubble is mostly edge */
    radial-gradient(circle at 50% 50%,
      rgba(255,255,255,0)   54%,
      rgba(255,255,255,.56) 82%,
      rgba(255,208,236,.50) 93%,
      rgba(255,255,255,0)   100%);
  box-shadow:
    inset -5px -7px 16px rgba(138, 21, 80, .18),
    inset  4px  5px 14px rgba(255, 255, 255, .52),
    0 3px 12px rgba(138, 21, 80, .12);
}

/* --- the exit ---------------------------------------------------------
   The bubbles' own pop is driven from app.js so each starts from where it
   actually is. Here the artwork gets out of their way and the sky thins,
   so the app is already showing through the gaps. */

/* The scene owns no entrance — the strokes, the lines and the lettering each
   arrive on their own — so nothing may animate it in. The :not(.popping) is
   load-bearing: this rule carries !important, and both classes are on the
   element at once during the exit, so without it nothing the exit declares
   below can ever take effect. */
.splash.animate:not(.popping) .splash-scene { animation: none !important; opacity: 1 !important; }

/* But it has to LEAVE. This selector used to carry `.splash.popping` too, which
   pinned the artwork fully opaque for the whole exit while sky-out spent 760ms
   removing the only thing behind it — so the horizon, the water and the
   lettering hung over the Log screen for most of a second and then cut out when
   `hide` landed 160ms later, which is what read as the line work outliving the
   fade and then disappearing rather than going. The rule that was supposed to
   take the artwork away was written without a declaration block, swallowed its
   own @keyframes with it, and had therefore never once run.

   It goes first now, a beat ahead of the sky, and the bubbles outlive them
   both — which is the transition. */
.splash.popping .splash-scene { animation: scene-out 560ms var(--ease-in-out) 40ms forwards; }
@keyframes scene-out { to { opacity: 0; transform: scale(1.05); } }

.splash.popping .sky { animation: sky-out 760ms var(--ease-in-out) 60ms forwards; }
@keyframes sky-out { to { opacity: 0; transform: scale(1.08); } }
.splash.popping .splash-dots,
.splash.popping .splash-skip { animation: chrome-out 240ms linear forwards; }
@keyframes chrome-out { to { opacity: 0; } }

/* The bubble layer must outlive the artwork — it is the transition. */
.splash.popping .motes { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .motes { display: none; }
  .splash.popping .sky { animation: none; }
  .splash.popping .splash-scene { animation: none; }
}

/* ============================================================
   TAB BAR v2 — Log first, warm at both ends, icons that move
   ============================================================ */

/* the purple circle is gone — the clip is the only coloured thing */
.tab.t-butter .tab-ico { background: none; box-shadow: none; width: 28px; height: 28px; }

/* Bigger icons, more presence. */
.tab { padding: 13px 2px 9px; min-height: 62px; gap: 5px; }

/* --- icons that actually do something on selection -------------------- */

@keyframes ico-pop {
  0%   { transform: translateY(0) scale(1); }
  38%  { transform: translateY(-5px) scale(1.22); }
  100% { transform: translateY(-1px) scale(1.06); }
}

/* The last survivor of an even older set — spin-quarter spun the whole Log
   sprite a quarter turn on selection. It had been buried under a later block
   for so long that removing that block brought it back, which is the standing
   hazard in this file: nothing was ever deleted, only shadowed. Deleted now,
   and its @keyframes went with it. */

/* --- the icons move as line drawings, not as wobbling sprites ---------
   Every one of these used to animate the WHOLE <svg> — climb translated it,
   leaf flipped it in 3D, turn-plus spun it, slide-sliders rotated it +/-7deg,
   which is exactly the seesaw it looked like. Nothing inside the drawing ever
   moved. Now the sprite holds still and its parts do the work.
   ---------------------------------------------------------------------- */

/* Progress: a short segment slithers along the trend line. The full line stays
   faintly under it so the icon still reads when nothing is moving. The dash
   pattern sums to the path's own length (~23), so the snake wraps seamlessly
   instead of vanishing at the end of a cycle. */
.trend-ghost { opacity: .30; }
.trend-snake { stroke-dasharray: 6 17; stroke-dashoffset: 0; }
.tab[data-pane="progress"].is-on .trend-snake {
  animation: snake 2.4s linear infinite;
}
@keyframes snake { to { stroke-dashoffset: -23; } }
.tab[data-pane="progress"].is-on .trend-head {
  animation: head-glint 2.4s var(--ease-out) infinite;
}
@keyframes head-glint {
  0%, 62%, 100% { opacity: .55; }
  78%           { opacity: 1; }
}

/* Weeks: the days scan horizontally like a reel, decelerate, overshoot a hair
   and snap back — the jackpot stop, laid on its side. The per-keyframe timing
   functions are what make it read as momentum rather than a slide. */
.cal-reel { transform: translateX(0); }
.tab[data-pane="history"].is-on .cal-reel {
  animation: reel 4.4s infinite;
}
@keyframes reel {
  0%   { transform: translateX(0);      animation-timing-function: cubic-bezier(.4,0,.9,.35); }
  30%  { transform: translateX(-17.6px); animation-timing-function: linear; }
  52%  { transform: translateX(-35.2px); animation-timing-function: cubic-bezier(.1,.8,.3,1); }
  70%  { transform: translateX(-53.6px); animation-timing-function: var(--ease-out); }
  76%  { transform: translateX(-54.6px); animation-timing-function: var(--ease-out); }
  82%  { transform: translateX(-52.8px); }
  100% { transform: translateX(-52.8px); }
}

/* Log: the ruled lines write themselves in, one after the next, then the page
   clears and it starts again. */
.jrnl-rule { stroke-dasharray: 6; stroke-dashoffset: 0; }
.tab[data-pane="log"].is-on .jrnl-rule { animation: write 4.6s var(--ease-out) infinite; }
.tab[data-pane="log"].is-on .r1 { animation-delay: 0ms; }
.tab[data-pane="log"].is-on .r2 { animation-delay: 260ms; }
.tab[data-pane="log"].is-on .r3 { animation-delay: 520ms; }
@keyframes write {
  0%       { stroke-dashoffset: 6; }
  22%, 78% { stroke-dashoffset: 0; }
  96%, 100% { stroke-dashoffset: 6; }
}

/* Settings: each knob runs its own way, at its own moment, and settles with a
   small overshoot. They must not share a direction or a phase — moving as a
   pair on one axis is what made it a seesaw. */
.sl-knob { transform-box: fill-box; transform-origin: center; }
.tab[data-pane="settings"].is-on .knob-a { animation: knob-a 5.2s var(--ease-in-out) infinite alternate; }
.tab[data-pane="settings"].is-on .knob-b { animation: knob-b 5.2s var(--ease-in-out) 420ms infinite alternate; }
@keyframes knob-a {
  0%        { transform: translateX(0); }
  26%       { transform: translateX(-6.4px); animation-timing-function: var(--ease-out); }
  34%       { transform: translateX(-5.2px); }
  100%      { transform: translateX(-5.2px); }
}
@keyframes knob-b {
  0%        { transform: translateX(0); }
  30%       { transform: translateX(6.8px); animation-timing-function: var(--ease-out); }
  38%       { transform: translateX(5.6px); }
  100%      { transform: translateX(5.6px); }
}

@media (prefers-reduced-motion: reduce) {
  .tab.is-on .tab-ico, .tab.is-on .tab-ico svg,
  .tabbar:not(:hover) .tab.is-on .tab-ico { animation: none; }
}

/* Anything riding on the clip is white, full stop. This has now been fought
   over by three generations of per-tone rules; ending the argument. */
.tab.is-on .tab-ico,
.tab.is-on .tab-label { color: #FFFFFF !important; }
.tab.is-on .tab-label { text-shadow: 0 1px 3px rgba(60,10,40,.45); font-weight: 800; }
/* except Log, whose butter clip is too light to carry white */

/* ============================================================
   LOG SITS BETWEEN THE PINK AND THE ORANGE
   Order is violet, rose, yellow, orange — a warm run into the
   right-hand end. Log's problem was never the colour, it was
   the position plus an orange idle state that made the whole
   bar read yellow. Its resting state is now the same neutral
   as every other tab; only the clip is warm.
   ============================================================ */

.tab.t-butter { color: var(--muted-soft); }

/* --- dock: more of the sunset through it ------------------------------ */
.tabbar {
  background: linear-gradient(180deg, rgba(255,255,255,.44), rgba(255,255,255,.26));
  -webkit-backdrop-filter: blur(34px) saturate(200%);
          backdrop-filter: blur(34px) saturate(200%);
  border: 1px solid rgba(255,255,255,.5);
  box-shadow:
    0 24px 46px -20px rgba(138,21,80,.32),
    0 6px 14px -8px rgba(138,21,80,.14),
    inset 0 2px 0 rgba(255,255,255,.7);
}

/* ============================================================
   ICONS WITH A FULL RANGE OF MOTION
   A single jump on selection is a punctuation mark, not motion.
   The live tab's mark now keeps working: it travels, turns and
   returns on its own loop, and the loop suits the thing it is.
   ============================================================ */

.tab.is-on .tab-ico { animation: ico-settle var(--dur-travel) var(--ease-out); }
@keyframes ico-settle {
  0%   { transform: translateY(2px) scale(.86); }
  46%  { transform: translateY(-6px) scale(1.24); }
  100% { transform: translateY(-1px) scale(1.06); }
}
.tabbar:not(:hover) .tab.is-on .tab-ico { animation: ico-settle var(--dur-travel) var(--ease-out); }

/* The whole-sprite loops that used to live here are gone: climb translated the
   svg, leaf flipped it in 3D, turn-plus spun it a full turn, slide-sliders
   rotated it +/-7deg. They moved the drawing as an object and never moved a
   line inside it. The per-part animations above replace all four. */

@media (prefers-reduced-motion: reduce) {
  .tab.is-on .tab-ico, .tab.is-on .tab-ico svg,
  .tabbar:not(:hover) .tab.is-on .tab-ico { animation: none !important; }
}

/* Log's clip is deep enough to carry white, so every label matches. It was
   wine-on-butter before, which read as the one tab that had gone grey. */
/* The bar ramps purple → pink → yellow → orange, which puts butter directly
   beside amber. This gradient used to run #FBD971 → #E79A22 — golden at both
   ends, the same family as Settings, and side by side the two tabs read as one
   colour repeated. Butter now stays a true yellow and never reaches orange. */
/* Deep enough to carry white, so the selected tab reads the same as every
   other one. The pale yellow this replaces needed dark text, and one tab with
   dark labels among three with white was the inconsistency. Still clearly gold
   rather than the tang orange next to it. */
/* Butter yellow, and it stays butter. The deepened gold this replaces was an
   attempt to make white text pass on it, which only made the tab dark and
   still left it fighting the orange. White holds here on a shadow instead —
   the label is 10px bold uppercase, which carries a shadow well. */
/* Pale butter, not gold. The label sits low in the tab, so the ramp holds the
   pale cream across the top two-thirds — where the eye reads the colour — and
   deepens to honey only at the bottom edge, which is where the white label
   actually lands. That is what buys legibility without darkening the tab. */
/* The one and only butter ramp. There were three of these from three eras and
   the last one won, so two rounds of "soften the yellow" landed on copies that
   were already dead. Softened to the sand in the reference: the old top stop
   was #FFF8CE, an electric yellow that sat outside the sunset rather than in
   it and looked lit from a different source than the peach beside it. */
.clip.to-butter {
  background: linear-gradient(178deg,
    #F9E6B4 0%, #F2D492 38%, #EAC176 72%, #E0B364 100%);
}
.tab.t-butter.is-on .tab-ico,
.tab.t-butter.is-on .tab-label { color: #FFFFFF !important; }
.tab.t-butter.is-on .tab-label { text-shadow: 0 1px 3px rgba(122, 74, 4, .70) !important; }
.tab.t-butter.is-on .tab-ico   { filter: drop-shadow(0 1px 2px rgba(122, 74, 4, .55)); }

/* Thinner. These are the app's only navigation, but at 66px tall over a 9px
   pad the dock was ~84px of fixed furniture — and on the Log page it closed on
   the bubble row hard enough that the two read as one crowded band. 52px over
   a 6px pad gives the page back roughly 24px, which is the difference between
   the discs sitting above the dock and fighting it. */
.tab { min-height: 52px; padding: 9px 2px 7px; }
.tab .tab-ico { width: 28px; height: 28px; }
.tab .tab-ico svg { width: 26px; height: 26px; stroke-width: 2.1; }
.tab-label { font-size: 9.5px; letter-spacing: .5px; }
/* The clip lies OVER the lip of the dock — the band draped over an edge, which
   is the whole idea. It was flattened into the bar for one pass while I was
   chasing what looked like the pill escaping its container; that turned out to
   be the pane-slide overflow bug instead (see #main overflow-x), so the
   overhang comes back.
   It is deliberate and symmetric now: 12px proud of the top, and the extra
   height added back so it still meets the bottom inset, rather than the old
   version which hung 11px above and stopped 17px short. */
.clip {
  /* The strip proud of the dock is TRANSLUCENT, so the sky reads through it
     while it keeps its colour — that is what makes it look like a band lying
     over the lip rather than a tile stuck on top of it. A mask with alpha
     stops does it: half-opaque at the very top, full by the time it reaches
     the bar's edge. The mask takes the pseudo-elements with it, which is what
     we want — the fold highlight should fade out up there too. */
  -webkit-mask-image: linear-gradient(180deg,
    rgba(0,0,0,.42) 0px, rgba(0,0,0,.58) 7px, rgba(0,0,0,.86) 14px, #000 21px);
          mask-image: linear-gradient(180deg,
    rgba(0,0,0,.42) 0px, rgba(0,0,0,.58) 7px, rgba(0,0,0,.86) 14px, #000 21px);
  /* the lip casting onto the face, just below the fold */
  box-shadow:
    inset 0 7px 9px -5px rgba(84, 18, 52, .42),
    0 16px 24px -12px rgba(138, 21, 80, .40),
    0 4px 8px -3px rgba(138, 21, 80, .26);
  top: -9px;   /* proportional to the slimmer bar */
  /* `100%` here is the bar's PADDING box, not its border box, so the old
     `100% - 16px + 12px` came out 87px against a 93px bar — proud by 11 at the
     top and 17 short at the bottom. It needs to be taller than 100%, not
     shorter, to overhang the lip and still meet the bottom inset. */
  height: calc(100% + 5px);
  border-radius: 22px 22px 20px 20px;
}
/* Sold as a CLIP, not a tile sitting proud.
   Straight-on, there is no perspective to work with — the vanishing point is
   behind the viewer — so the fold has to be implied entirely by light. Three
   layers do it, and they have to be in this order:
     ::before  the strip above the dock's lip turns away from the light, so it
               darkens toward the very top and fades into the face
     ::after   the fold itself catches a bright line, right on the lip
     inset     a soft shadow immediately under the fold, cast by the lip onto
               the clip's own face — this is the part that reads as curve
               rather than as a bevel */
.clip::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 22px;
  border-radius: 22px 22px 0 0;
  background: linear-gradient(180deg,
    rgba(84, 18, 52, .20) 0%,
    rgba(84, 18, 52, .10) 40%,
    rgba(84, 18, 52, .02) 74%,
    rgba(84, 18, 52, 0)  100%);
  pointer-events: none;
}
.clip::after {
  content: '';
  position: absolute;
  left: 5%; right: 5%; top: 9.5px;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,.18));
  filter: blur(.4px);
  pointer-events: none;
}
.tabbar { overflow: visible; }

.btn-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ============================================================
   YOUR DATA — three bands, stood up like cocktails
   Two ghost links and a full-width danger button read as three
   different kinds of thing. They are three actions on the same
   data, so they get one shape, stood vertically, each with its
   own summer tone and its icon above the word.
   ============================================================ */

.cans {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 4px 0 12px;
}
.can {
  font-family: var(--font-display);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  padding: 18px 8px 16px;
  min-height: 146px;
  border: 1.5px solid rgba(255, 255, 255, .70);
  border-radius: 22px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  letter-spacing: .2px;
  color: var(--white);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
          backdrop-filter: blur(12px) saturate(150%);
  box-shadow: var(--clay);
  transition: transform var(--dur-press) var(--ease-out),
              box-shadow var(--dur-press) var(--ease-out),
              filter var(--dur-med) var(--ease-out);
}
.can:active { transform: translateY(2px) scale(.97); box-shadow: var(--clay-press); }
.can:focus-visible { outline: 2.5px solid var(--magenta); outline-offset: 3px; }


/* No tile behind the icon. A rounded square underlay boxed each drawing into
   about 22px inside a 42px plate, which is most of why they read as small and
   inert — the band is already a surface, the icon does not need a second one.
   The drawing is the whole thing now, at more than twice the size, and it
   carries its own shadow so it still sits on the colour. */
.can-ico {
  display: grid;
  place-items: center;
  width: 58px; height: 58px;
}
.can-ico svg {
  width: 54px; height: 54px;
  overflow: visible;
  filter: drop-shadow(0 2px 2px rgba(96, 22, 58, .34))
          drop-shadow(0 6px 10px rgba(96, 22, 58, .26));
}
.ico-art { transform-box: fill-box; transform-origin: 50% 75%; }
.can:active .can-ico svg { transform: translateY(2px) scale(.94); }
.can-ico svg { transition: transform var(--dur-press) var(--ease-out); }

/* It spills, and then the tide comes in and takes it — which is the delete.
   The wash is drawn far wider than the icon and slides across, so it reads as
   water arriving rather than a line growing. */
@keyframes spill-pool {
  0%      { opacity: 0; transform: scaleX(.3); }
  22%     { opacity: .9; transform: scaleX(1); }
  62%     { opacity: .9; transform: scaleX(1); }
  100%    { opacity: 0; transform: scaleX(1.25) translateX(6px); }
}
@keyframes tide-in {
  0%   { opacity: 0; transform: translateX(-6px); }
  30%  { opacity: .85; }
  70%  { opacity: .85; }
  100% { opacity: 0; transform: translateX(22px); }
}
/* Reserve two lines on every band so "Restore" sits on the same baseline as
   the labels that wrap. */
.can-label {
  text-shadow: 0 1px 2px rgba(96, 22, 58, .38);
  min-height: 2.5em;
}

@media (max-width: 340px) {
  .can { min-height: 112px; padding: 14px 6px 13px; font-size: 11.5px; }
  .can-ico { width: 36px; height: 36px; }
}

/* ============================================================
   The wordmark reads as marshmallow only with a lip of white
   around it. A soft glow is not enough — it needs an actual
   edge, the way a sugared sweet or an inflatable seam catches
   light all the way round.
   Chained zero-blur drop-shadows in four directions synthesise
   that outline: each one shadows the result of the last, so
   four passes close the ring. A single shadow only lights one
   side, and mask-based outlines would need the glyphs scaled,
   which distorts a wide wordmark unevenly.
   ============================================================ */
/* and a brighter wash across the top so the surface curves toward the light */

/* ============================================================
   DELETE CONFIRMATION
   The one irreversible action in the app, so it gets a real
   dialog rather than a native confirm() the browser is free to
   suppress. Cancel is the prominent button; the destructive one
   is clearly marked and deliberately second.
   ============================================================ */

.wipe-scrim {
  position: fixed; inset: 0; z-index: 60;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(58, 46, 77, .48);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}
.wipe-scrim.open { display: flex; animation: wipe-scrim-in var(--dur-med) var(--ease-out); }
@keyframes wipe-scrim-in { from { opacity: 0; } }

.wipe-card {
  width: 100%; max-width: 360px;
  background: linear-gradient(168deg, #FFF7FB 0%, #FDEDF5 100%);
  border: 1.5px solid rgba(255, 255, 255, .8);
  border-radius: 26px;
  padding: 24px 22px 20px;
  text-align: center;
  box-shadow: 0 26px 50px -18px rgba(96, 22, 58, .52),
              inset 0 2px 0 rgba(255, 255, 255, .9);
  animation: wipe-card-in var(--dur-sheet) var(--ease-drawer);
}
@keyframes wipe-card-in {
  from { opacity: 0; transform: translateY(18px) scale(.94); }
}

.wipe-ico {
  width: 62px; height: 62px;
  margin: 0 auto 12px;
  display: grid; place-items: center;
  color: var(--danger);
}
.wipe-ico svg {
  width: 54px; height: 54px;
  filter: drop-shadow(0 3px 4px rgba(178, 59, 92, .34));
}
.wipe-title {
  font-family: var(--font-display);
  font-weight: 800; font-size: 23px;
  color: var(--wine);
  margin: 0 0 8px;
}
.wipe-body {
  font-size: 13.5px; line-height: 1.5;
  color: var(--muted);
  margin: 0 0 20px;
}
.wipe-actions { display: grid; gap: 9px; }
.wipe-keep {
  color: var(--white);
  background: linear-gradient(158deg, #B9A6E4 0%, var(--violet) 48%, var(--rose) 100%);
  box-shadow: var(--clay);
}
.wipe-go {
  color: var(--danger);
  background: rgba(255, 255, 255, .8);
  border: 1.5px solid rgba(178, 59, 92, .42);
}
.wipe-keep:active, .wipe-go:active { transform: scale(.97); box-shadow: var(--clay-press); }

@media (prefers-reduced-motion: reduce) {
  .wipe-scrim.open, .wipe-card { animation: none; }
}

/* ==================================================================== body map

   The measurement diagram on the Log page. Every dot writes into the same
   `w_<key>` input the typed list uses, so prefill, validation, unit
   conversion and save all stay exactly where they were — the map is a second
   way to reach those fields, never a second copy of the data. */

.bm-wrap{position:relative;margin:2px 0 10px}
.bm-fig{display:block;width:100%;height:auto}

#bmMarks .lead{stroke-width:1.4;fill:none;opacity:.75}
#bmMarks .mark{cursor:pointer}
/* No fill in these rules. Each pip carries its own sunset colour as a fill
   ATTRIBUTE, and a CSS declaration beats a presentation attribute. */
#bmMarks .halo{opacity:.18;transition:opacity 200ms var(--ease-out)}
#bmMarks .pip{stroke:#fff;stroke-width:2.4;
  transition:transform 200ms var(--ease-out),opacity 200ms var(--ease-out)}
#bmMarks .mark:hover .halo,#bmMarks .mark.on .halo{opacity:.34}
/* scale, not `r`: the dots are deliberately different sizes and a fixed
   radius made the larger ones shrink on selection. */
#bmMarks .mark.on .pip{transform:scale(1.18);transform-origin:center;transform-box:fill-box}
#bmMarks .mark:focus-visible{outline:none}
#bmMarks .mark:focus-visible .halo{opacity:.5}
#bmMarks .anchor{fill:#A85E8E;opacity:.8}
/* Done is opacity, not colour. Repainting a filled dot violet threw away the
   sunset palette entirely — the log prefills from last week, so every dot is
   filled the moment the pane opens and none of the colours were ever seen.
   Faded means empty, solid means filled, and each dot keeps its own hue. */
#bmMarks .pip{opacity:.42}
#bmMarks .mark.done .pip{opacity:1}
#bmMarks .mark.done .halo{opacity:.30}
#bmMarks .tag{font-size:11.5px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;
  fill:var(--muted);pointer-events:none}
#bmMarks .val{font-size:12px;font-weight:800;fill:var(--magenta);pointer-events:none}

/* the tape line */
.bm-tape{opacity:0;transition:opacity 180ms var(--ease-out)}
.bm-tape line{stroke:var(--magenta);stroke-width:2;stroke-linecap:round;opacity:.85}
.bm-tape #bmTapeL{stroke-dasharray:6 5}
.bm-tape.on{opacity:1}
.bm-tape.on #bmTapeL{animation:bm-draw 460ms var(--ease-out) both}
@keyframes bm-draw{from{stroke-dashoffset:var(--tape-len,200)}to{stroke-dashoffset:0}}

/* The input is pinned to the body part, above or below the connector.
   Along the bottom of the pane it read as page furniture and you lost the
   connection to whatever you had just tapped. */
.bm-entry{position:absolute;left:0;top:0;width:min(300px,calc(100% - 20px));
  display:flex;align-items:center;gap:8px;z-index:3;
  background:#fff;border:1px solid var(--border);border-radius:16px;
  padding:8px 8px 8px 14px;
  box-shadow:0 12px 30px -10px rgba(138,21,80,.34);opacity:0;pointer-events:none;
  transition:opacity 200ms var(--ease-out),transform 260ms var(--ease-out)}
.bm-entry.below{transform:translateY(-8px) scale(.97);transform-origin:top center}
.bm-entry.above{transform:translateY(8px) scale(.97);transform-origin:bottom center}
.bm-entry.show{opacity:1;transform:none;pointer-events:auto}
/* the tail: a rotated square with two borders hidden, so only the outer
   corner draws — flips with the popover */
.bm-entry::after{content:'';position:absolute;left:var(--caret,50%);width:13px;height:13px;
  background:#fff;transform:translateX(-50%) rotate(45deg)}
.bm-entry.below::after{top:-7px;border-left:1px solid var(--border);border-top:1px solid var(--border)}
.bm-entry.above::after{bottom:-7px;border-right:1px solid var(--border);border-bottom:1px solid var(--border)}
.bm-elbl{font-size:11px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;
  color:var(--muted);white-space:nowrap}
.bm-entry input{flex:1;min-width:0;border:none;background:var(--field-bg);border-radius:10px;
  padding:8px 10px;font:700 16px/1 inherit;color:var(--wine);text-align:right;
  font-variant-numeric:tabular-nums}
.bm-entry input:focus{outline:2px solid var(--rose);outline-offset:-2px}
.bm-eunit{font-size:12px;font-weight:700;color:var(--muted-soft)}
.bm-esave{border:none;border-radius:11px;padding:9px 14px;font:700 13px/1 inherit;cursor:pointer;
  background:linear-gradient(135deg,var(--violet),var(--rose));color:#fff;
  transition:transform 140ms var(--ease-out)}
.bm-esave:active{transform:scale(.94)}
.bm-ex{background:none;border:none;color:var(--muted-soft);padding:9px 6px;font-size:15px;cursor:pointer}

/* The counter tucks up into the dead space below her feet — 34px of empty
   canvas sits inside the SVG under the figure, and a thin bar is exactly
   what belongs in it. The bubble row used to claim that space with its own
   negative margin; now that the bar sits between the map and the row, the
   tuck lives here or the bubbles paint straight over the count. */
.bm-progress{display:flex;align-items:center;gap:10px;margin:-24px 0 10px}

/* Lifted off the counter line and sitting just above it, at the right end —
   on the line it was competing with "0 of 8" for the same few pixels. The row
   is the positioning context; the pill floats in the dead space under her feet
   where nothing else is. */
.bm-progress { position: relative; }
.tape {
  position: absolute;
  right: 0;
  bottom: calc(100% + 7px);
  /* Load-bearing. .bm-progress is pulled up 24px into the body map's box, so
     this pill sits INSIDE .bm-wrap — and `.bm-wrap > svg` carries z-index 1,
     which was painting and, worse, hit-testing over the button. It rendered
     perfectly and could not be pressed. Above the bubble row's 2 as well, so
     the order along that band stays deliberate rather than incidental. */
  z-index: 3;
  display: inline-flex; align-items: center; justify-content: center;
  height: 26px;
  padding: 0 10px;
  border: 1px solid rgba(255,255,255,.6);
  border-radius: 999px;
  /* Transparent enough that the sunset carries straight through it — this is
     a hint, and a solid chip would announce itself louder than the numbers. */
  background: rgba(255,255,255,.42);
  -webkit-backdrop-filter: blur(12px) saturate(150%);
          backdrop-filter: blur(12px) saturate(150%);
  color: var(--muted);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.tape svg { width: 21px; height: 21px; }
.tape:active { transform: scale(.92); }
.tape[aria-expanded="true"] {
  color: var(--violet);
  background: rgba(255,255,255,.78);
  box-shadow: var(--clay-sm);
}
.tape:focus-visible {
  outline: none;
  color: var(--violet);
  box-shadow: 0 0 0 3px rgba(209,105,154,.45);
}
/* Opens where it was asked for, under the counter line, rather than as one
   more full-width panel in the stack. */
.tape-body {
  margin: -2px 0 14px;
  padding: 12px 15px 13px;
  border-radius: 16px;
  background: rgba(255,255,255,.72);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  box-shadow: 0 6px 18px -10px rgba(138,21,80,.28);
}
.tape-body[hidden] { display: none; }
.bm-bar{flex:1;height:6px;border-radius:99px;background:var(--border);overflow:hidden}
.bm-bar i{display:block;height:100%;width:0;border-radius:99px;
  background:linear-gradient(90deg,var(--violet),var(--rose));
  transition:width 500ms var(--ease-out)}
.bm-count{font-size:12px;font-weight:700;color:var(--muted);font-variant-numeric:tabular-nums}

.bm-typed{margin:0 0 18px}
.bm-typed>summary{cursor:pointer;font-size:13px;font-weight:700;color:var(--ghost);
  padding:8px 2px;list-style:none}
.bm-typed>summary::-webkit-details-marker{display:none}
.bm-typed>summary::before{content:'▸ ';font-size:11px}
.bm-typed[open]>summary::before{content:'▾ '}

@media (prefers-reduced-motion: reduce){
  #bmMarks .halo,#bmMarks .pip,.bm-entry,.bm-bar i,.bm-tape{transition:none}
  .bm-tape.on #bmTapeL{animation:none;stroke-dashoffset:0}
}

/* ------------------------------------------------- log page: layout revisions

   Three problems, all of proportion rather than function:

   - The date was a full-width boxed input with its label stacked above, which
     gave a piece of context the same visual weight as the numbers she came to
     enter. It is one compact row now.
   - The weight card was a 54px display numeral with 46px nudge circles above
     the diagram. It read as the page's centrepiece and dwarfed everything
     around it. It is now a single row, and it sits UNDER the diagram — the
     measurements are the body of the task, weight is one more number. */

/* The date lives in the pane head now, as one object rather than three. The
   real <input type="date"> sits transparent across the whole pill, so tapping
   anywhere on it opens the OS picker while what she reads is our own text. */
.log-head { align-items: center; padding: 12px 2px 10px; gap: 10px; }
.datepill {
  position: relative;
  display: inline-flex; align-items: center; gap: 7px;
  margin-right: auto;
  padding: 7px 14px 7px 11px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,.8);
  background: linear-gradient(165deg, rgba(255,255,255,.78) 0%, rgba(246,236,250,.62) 100%);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  box-shadow: var(--clay-sm);
  color: var(--violet);
  transition: box-shadow var(--dur-press) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.datepill:active { transform: scale(.97); box-shadow: var(--clay-sm-press); }
.datepill-ico { width: 15px; height: 15px; flex: 0 0 auto; }
.datepill-text {
  font-family: var(--font-display);
  font-size: 13.5px; font-weight: 700; letter-spacing: -.1px;
  color: var(--wine);
  white-space: nowrap;
}
/* Transparent, not hidden: display:none or visibility:hidden would take the
   native picker and the label association with it. */
.datepill > input[type="date"] {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0; padding: 0; border: none;
  opacity: 0;
  background: none;
  cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.datepill:focus-within {
  box-shadow: var(--clay-sm), 0 0 0 3px rgba(209,105,154,.28);
}

/* Weight, reined in. Same control, a third of the footprint. */
.hero-row{gap:9px}
.hero-val input{font-size:30px;width:3.6ch;letter-spacing:-.4px}
.hero-unit{font-size:14px}
.chips{margin-top:10px;gap:6px}

/* ============================================================
   CONTROL MATERIALS

   The log page was two design languages at once: clay buttons in
   the dock and the primary action, and 1.5px outlined boxes for
   everything else. Outlined boxes are what a form looks like
   before anyone has designed it, which is why these controls
   read as borrowed from another app.

   Two materials from here, used consistently:

     CLAY   — anything you PRESS. Bright inset top edge, a
              magenta-tinted drop shadow, a soft inset underside.
     CARVED — anything you TYPE INTO. An inset well, no outline.
              Raised things invite a press, hollow things invite
              a fill, and the two never get confused.
   ============================================================ */

:root{
  /* --clay is tuned for a 46px puck; on a 30px chip its 26px blur
     reads as a smudge. Same light, smaller source. */
  --clay-sm-press:
    0 2px 5px -3px rgba(138, 21, 80, .28),
    inset 0 2px 5px rgba(138, 21, 80, .17),
    inset 0 1px 0 rgba(255, 255, 255, .30);
}

/* ---- CARVED: fields you type into ------------------------------------- */
.field input,
.field textarea,
.bm-entry input,
#bubPopDose input {
  border: none;
  border-radius: 13px;
  background: linear-gradient(180deg, #F2E9F7 0%, #FCF9FE 62%);
  box-shadow: var(--carve);
}
.field input:focus,
.field textarea:focus,
.bm-entry input:focus,
#bubPopDose input:focus {
  outline: none;
  background: linear-gradient(180deg, #FBF4FC 0%, #FFFFFF 62%);
  box-shadow: var(--carve-focus);
}
.field textarea { border-radius: 15px; }

/* ---- CLAY: things you press ------------------------------------------- */
.nudge {
  width: 38px; height: 38px;
  border: none;
  background: linear-gradient(165deg, #FFFFFF 0%, #EFE1F7 100%);
  color: var(--violet);
  font-size: 19px;
  box-shadow: var(--clay-sm);
}
.nudge:active {
  transform: scale(.92);
  border: none;
  color: var(--magenta);
  box-shadow: var(--clay-sm-press);
}

.chips button {
  border: none;
  border-radius: 13px;
  padding: 9px 4px;
  background: linear-gradient(165deg, #FFFFFF 0%, #F1E4F8 100%);
  color: var(--plum);
  font-size: 12.5px;
  box-shadow: var(--clay-sm);
}
.chips button:active {
  transform: scale(.94);
  background: linear-gradient(165deg, #F6ECFA 0%, #E9D9F3 100%);
  border: none;
  color: var(--magenta);
  box-shadow: var(--clay-sm-press);
}

/* The weight card holds clay controls, so it stops being a bordered box and
   becomes the surface they sit on. */

.bm-esave { box-shadow: var(--clay-sm); }
.bm-esave:active { box-shadow: var(--clay-sm-press); }

/* "Type them in a list instead" was a bare text link floating between two
   cards. It is a control, so it looks like one. */
.bm-typed > summary {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 15px;
  border-radius: 12px;
  background: linear-gradient(165deg, #FFFFFF 0%, #F1E4F8 100%);
  box-shadow: var(--clay-sm);
  color: var(--plum);
  transition: box-shadow var(--dur-press) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.bm-typed > summary:active { transform: scale(.97); box-shadow: var(--clay-sm-press); }
.bm-typed > summary::before { color: var(--violet); font-size: 12px; }
.bm-typed[open] > summary { margin-bottom: 12px; }


/* ---- the switch -------------------------------------------------------- */
/* Off, this was a white knob on a pale lilac track — two light tones with
   almost no contrast, so it read as unfinished rather than as off. The track
   is deeper now and the knob carries the same clay as every other control. */

@media (prefers-reduced-motion: reduce) {
  .bm-typed > summary { transition: none; }
}


/* The carve was too shy to register: a .13 inset over an almost-white
   gradient just looks like a flat box. Deepen the well and darken its lip. */
:root{
  --carve:
    inset 0 3px 6px rgba(138, 21, 80, .20),
    inset 0 1px 2px rgba(138, 21, 80, .12),
    inset 0 -1.5px 0 rgba(255, 255, 255, 1);
  --carve-focus:
    inset 0 3px 6px rgba(138, 21, 80, .15),
    inset 0 -1.5px 0 rgba(255, 255, 255, 1),
    0 0 0 3px rgba(209, 105, 154, .26);
}

/* and a touch more lift under the pressable things */
:root{
  --clay-sm:
    0 7px 14px -6px rgba(138, 21, 80, .34),
    0 2px 4px -1px rgba(138, 21, 80, .20),
    inset 0 1.5px 0 rgba(255, 255, 255, .95),
    inset 0 -4px 8px rgba(138, 21, 80, .09);
}

/* The last flat outlined box on the page, sitting between a carved well and
   a clay button. A disclosure is something you press. */
.howto {
  border: none;
  border-radius: 14px;
  background: linear-gradient(165deg, #FFFFFF 0%, #F3E7F9 100%);
  box-shadow: var(--clay-sm);
  transition: box-shadow var(--dur-press) var(--ease-out);
}
.howto summary { color: var(--plum); padding: 12px 15px; }
.howto summary::before { color: var(--violet); }
.howto:not([open]):active { box-shadow: var(--clay-sm-press); }

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

/* ============================================================
   DOODLES — traced from her icon sheet.

   These replace the palm and the sun-ring that were six and two
   hand-authored strokes approximating a shape. They are real
   single-stroke paths lifted from the artwork's own centrelines
   (prototypes/trace-icons.py), which is what lets them DRAW
   rather than merely appear: a raster can only fade in.

   Each one draws itself on stroke by stroke, holds, then fades
   away — the drawing survives the fade, so it reads as a hand
   finishing a sketch and the page turning, not as an undo.
   ============================================================ */
.splash-doodles { position: absolute; inset: 0; pointer-events: none; }
.dd {
  position: absolute;
  color: rgba(255, 252, 246, .95);
  mix-blend-mode: overlay;      /* same ink as the horizon and the water */
  opacity: .92;                 /* the strokes own appearing and leaving now */
  transform: rotate(var(--rot, 0deg));
  overflow: visible;
}
.dd path { stroke-width: 2.2; stroke-dasharray: 1; stroke-dashoffset: 0; }

/* The base state above is the FINISHED drawing on purpose — both triggers are
   JS-added classes, so a base of dashoffset:1 would mean no JS, no ink, and
   tests/css.js guards that. The cost was a cold load showing the completed
   picture for the frames between first paint and .animate landing, and then
   erasing it to draw it again, which is the opposite of the effect.

   So JS arms it instead: app.js is the last element in <body> and adds
   .dd-armed at parse time, which is proof a script ran and the drawing is
   coming. Scoped to no-preference because the reduced-motion block below
   deliberately puts the ink straight back. */
@media (prefers-reduced-motion: no-preference) {
  .splash.dd-armed .dd path { stroke-dashoffset: 1; opacity: 0; }
}

/* Varied sizes and a scatter, so it reads as a page of sketches rather than
   a grid. Sizes are in vh for the same reason the float is: how much room
   there is depends on viewport height. */
/* Placed around the opaque things a white line must not cross — the clay
   sun, the margarita, and her. Faint lines BEHIND the wordmark are fine and
   deliberate; the horizon and the water already run straight through it. */
/* Airy: nothing crowds anything. She is the only solid thing on the screen
   now, and she drifts across the bottom, so the sketches stay above her. */
/* Scattered over the whole screen, not banked in the top two thirds. Nothing
   shares a size, an angle, a margin, a hold or a pen speed. Sizes run 7vh to
   27vh, nearly a factor of four.

   --n is the icon's stroke count and --dwell is about 1.4s / --n, so every
   icon takes roughly the same time to draw regardless of how it is built. */
.dd-sun      { left:  4%; top:  2%; width: 21vh;   --w: 2.3; --rot: -16deg; --n: 3;  --dwell: 460ms; --hold: 1500ms; }
.dd-palm     { right: 2%; top: 11%; width: 27vh;   --w: 1.8; --rot:   8deg; --n: 64; --dwell: 22ms;  --hold: 1900ms; }
.dd-shades   { left: 20%; top: 34%; width:  9vh;   --w: 3.6; --rot: -27deg; --n: 21; --dwell: 67ms;  --hold: 1300ms; }
.dd-cocktail { right: 4%; top: 44%; width: 17vh;   --w: 2.4; --rot:  13deg; --n: 49; --dwell: 29ms;  --hold: 1700ms; }
.dd-melon    { left:  2%; top: 58%; width: 14.5vh; --w: 2.7; --rot:  -6deg; --n: 12; --dwell: 117ms; --hold: 1200ms; }
.dd-shell    { right:22%; top: 79%; width:  7vh;   --w: 4.0; --rot:  31deg; --n: 22; --dwell: 64ms;  --hold: 1450ms; }
/* thin strokes vanish at small sizes, so the smaller the icon the heavier
   the pen — the line has to stay the same weight on screen */
.dd path { stroke-width: var(--w, 2.2); }

.splash.animate .dd {
  animation: dd-life 1500ms var(--ease-in-out) var(--start, 0ms) both;
}
/* Movement while the strokes land: a drawing that arrives perfectly still
   reads as a shape appearing rather than as something being drawn. */
/* --rot is each icon's own resting angle. Baking it into the keyframes is
   the only way to have both a per-icon angle and a shared animation: two
   rules cannot each own `transform` on one element. */
@keyframes dd-life {
  0%   { transform: rotate(calc(var(--rot) - 7deg)) scale(.9); }
  9%   { transform: rotate(calc(var(--rot) + 3deg)) scale(1.03); }
  26%  { transform: rotate(calc(var(--rot) - 2deg)) scale(1); }
  52%  { transform: rotate(calc(var(--rot) + 1deg)) scale(1); }
  76%  { transform: rotate(var(--rot)) scale(1); }
  100% { transform: rotate(calc(var(--rot) - 3deg)) scale(.97); }
}
/* ONE PEN.

   This used to stagger the strokes by ~7ms while each stroke took about 1.6s,
   so all sixty-four of the palm's strokes were drawing at once. That is a
   reveal, not a drawing — the shape simply materialises. A pen draws stroke
   one, finishes it, then starts stroke two.

   So the lag between strokes IS the time one stroke takes: --dwell. Each icon
   sets --dwell from its own stroke count, so they all take about 1.4s to draw
   however many strokes they happen to have — the sun is three long continuous
   lines, the palm is sixty-four short ones.

   Two animations on the same property. dd-off is listed second and wins once
   it starts; it has `forwards` rather than `both`, so during its delay it does
   not apply its own from-state and dd-on stays in charge. */
/* --at and --dur are per-stroke overrides, set in the markup, and default to
   the uniform ladder above — every icon that does not set them behaves exactly
   as before. They exist because one --dwell for every stroke only reads as one
   pen while the strokes are a similar length. The sun is three strokes of 48,
   476 and 164 units: at a flat 460ms each, the middle one whipped out ten times
   faster than the little curl before it, which is what made it look like it
   loaded strangely. Its three are timed from their own lengths now, so the pen
   moves at one speed and the icon still finishes on the same frame.

   The erase delay is written off --at rather than off --i, which is the same
   arithmetic for a uniform icon and keeps the strokes lifting in the order they
   were laid down for a metered one. */
.splash.animate .dd path {
  --at: calc(var(--start, 0ms) + var(--i, 0) * var(--dwell, 40ms));
  animation:
    dd-on  var(--dur, var(--dwell, 40ms)) linear var(--at) both,
    dd-off var(--dur, var(--dwell, 40ms)) linear
           calc(var(--at) + var(--n, 20) * var(--dwell, 40ms)
                + var(--hold, 1600ms)) forwards;
}
/* opacity, not just dashoffset. stroke-linecap is round, and a round cap gets
   painted even when the dash has zero length showing — so every stroke that
   had not started yet sat on the scene as a dot, and 64 of them on the palm
   read as a constellation hanging there before the drawing arrived. Carrying
   opacity through the keyframes is what actually makes "not drawn yet" mean
   nothing on screen. dd-on holds its from-state through its delay (`both`),
   which is where those dots were living. */
@keyframes dd-on  { from { stroke-dashoffset: 1; opacity: 0; }
                    to   { stroke-dashoffset: 0; opacity: 1; } }
/* Erased the way it was drawn: the pen runs back along the same line rather
   than the whole drawing fading out. */
@keyframes dd-off { from { stroke-dashoffset: 0;  opacity: 1; }
                    to   { stroke-dashoffset: -1; opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .dd { animation: none !important; }
  .dd path { animation: none !important; stroke-dashoffset: 0; opacity: 1; }
}

/* ---- the Your Data icons ------------------------------------------------
   Traced props from the same sheet the splash uses, so Settings and the
   splash are drawn by the same hand. They DRAW themselves rather than
   perform: a shell closing over your backup, a ball coming back, and a
   watermelon that already has a bite out of it for the one that deletes.

   pathLength="1" is on every path in the markup. It is an SVG ATTRIBUTE and
   is silently ignored if set from CSS, and without it `stroke-dasharray: 1`
   means a one-user-unit dash rather than the whole path — the strokes render
   as fine dots and nothing ever draws. */
.ico-art path { stroke-dasharray: 1; stroke-dashoffset: 0; }
@keyframes ico-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }

/* Triggered by the pane opening, not by :hover — there is no hover on the
   phone this is built for. `.is-on` goes on when Settings is shown and off
   when it is left, so the run restarts each visit and never loops. */
/* One pen here too. These had the same fault as the splash doodles: a 13ms
   stagger while each stroke took 820ms, so every stroke drew at once and the
   icon materialised instead of being drawn. The lag between strokes IS the
   time one stroke takes, and each icon sets it from its own stroke count so
   all three finish together at about 900ms. */
#pane-settings.is-on .can .ico-art path {
  animation: ico-draw var(--dwell) linear
             calc(var(--lead) + var(--i, 0) * var(--dwell)) backwards;
}
.can-backup  .ico-art { --dwell: 41ms; --lead: 200ms; }   /* shell, 22 strokes */
.can-restore .ico-art { --dwell: 60ms; --lead: 320ms; }   /* beach ball, 15 */
.can-delete  .ico-art { --dwell: 75ms; --lead: 440ms; }   /* melon, 12 */
@media (hover: hover) {
  .can:hover .ico-art path {
    animation: ico-draw var(--dwell) linear calc(var(--i, 0) * var(--dwell));
  }
}
/* A 100-unit box at 54px needs a heavier pen than a 24-unit one did. */
.can-ico svg { stroke-width: 5; }

/* ============================================================
   THE DRIFTER — she stays for the session.

   One element at body level, outside .splash and outside #main,
   so no screen change can restart her. Four nested layers
   because four animations cannot share `transform`: x, y, tilt,
   and the arrival.

   X and Y run at 61s and 47s. Those are coprime, so the combined
   path does not repeat for about 48 minutes — it reads as
   wandering rather than as a loop, which one shared duration
   never can. Both `alternate`, so she eases at each turn and
   never snaps back to a start position.
   ============================================================ */
/* Explicit width, not `width: auto`. A fixed element shrink-to-fits, and
   here the fit would have to resolve through three nested divs to an <img>
   sized `height:100%` — that chain collapses to zero width and she simply
   does not render. Her aspect is 640/944, so the width is derived. */
.drifter {
  position: fixed;
  left: -1%; bottom: 6%;
  --dh: 30vh;
  height: var(--dh);
  width: calc(var(--dh) * 0.678);
  z-index: 11;                 /* over the splash while it is up */
  pointer-events: none;
  opacity: .88;
  transition: opacity 700ms var(--ease-out);
}
.dr-x, .dr-y, .dr-tilt { width: 100%; height: 100%; }
.drifter .sf-img { display: block; width: 100%; height: auto; }


:root{ --ease-drift: cubic-bezier(.45,0,.55,1); }
.dr-x    { animation: dr-x 61s var(--ease-drift) infinite alternate; }
.dr-y    { animation: dr-y 47s var(--ease-drift) infinite alternate; }
.dr-tilt { animation: dr-spin 84s linear infinite; }
@keyframes dr-x    { from { transform: translateX(-5vw); } to { transform: translateX(56vw); } }
@keyframes dr-y    { from { transform: translateY(0); }    to { transform: translateY(-54vh); } }
/* A full turn, not a tilt: a float in a pool has nothing keeping it
   upright, and going right round is what sells the water. 84s means
   she is never obviously turning, only ever having turned. */
@keyframes dr-spin { to { transform: rotate(360deg); } }

/* She arrives once, with the splash, and never again. */
.splash.animate ~ .drifter .sf-img,
.drifter .sf-img { animation: float-arrive 1100ms var(--ease-out) 900ms backwards; }
@keyframes float-arrive {
  from { opacity: 0; transform: scale(.82) translateY(26px) rotate(-6deg); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .dr-x, .dr-y, .dr-tilt, .drifter .sf-img, .wk, .wk-side { animation: none !important; }
  .drifter { transition: none; }
}

/* ---- the icons move as they are drawn ----------------------------------
   A drawn line that arrives perfectly still reads as a shape appearing. The
   sketch layer tips and settles while the strokes land, the way a hand does;
   the idle layer keeps a slow life afterwards. Separate layers, because both
   want `transform` and the press feedback on the <svg> wants it too. */
.ico-idle, .ico-art { transform-box: fill-box; transform-origin: 50% 60%; }
@keyframes ico-sketch {
  0%   { transform: rotate(-5deg) scale(.88); }
  50%  { transform: rotate(2.5deg) scale(1.04); }
  78%  { transform: rotate(-1deg) scale(.99); }
  100% { transform: rotate(0) scale(1); }
}
#pane-settings.is-on .can .ico-art { animation: ico-sketch 1150ms var(--ease-out) 160ms backwards; }
@media (hover: hover) { .can:hover .ico-art { animation: ico-sketch 1150ms var(--ease-out); } }

/* each one moves the way its own object would, and on its own clock so the
   three never fall into step */
@keyframes idle-rock { 0%,100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
@keyframes idle-roll { to { transform: rotate(360deg); } }
@keyframes idle-tip  { 0%,100% { transform: rotate(3deg) translateY(0); } 50% { transform: rotate(-3deg) translateY(-2px); } }
#pane-settings.is-on .can-backup  .ico-idle { animation: idle-rock 6.5s var(--ease-in-out) 1.5s infinite; }
#pane-settings.is-on .can-restore .ico-idle { animation: idle-roll 22s linear 1.7s infinite; }
#pane-settings.is-on .can-delete  .ico-idle { animation: idle-tip  7.5s var(--ease-in-out) 1.9s infinite; }

@media (prefers-reduced-motion: reduce) {
  .ico-idle, .ico-art { animation: none !important; }
}

/* ============================================================
   FROSTED SURFACES

   She drifts behind the whole app now, and an opaque control
   simply deletes her wherever it sits. Every surface you press
   or type into is glass: enough to let the layer behind read
   through, not so much that it performs as glass.

   One level: --glass for surfaces that carry text, which need
   the extra body to keep it legible.

   backdrop-filter is the whole effect, so there is a fallback:
   without it the same rules would be flat washes with no
   separation at all, and the alpha has to come up.
   ============================================================ */
:root{
  --glass:       rgba(255,255,255,.62);
  --glass-blur:  blur(16px) saturate(170%);
  --glass-well:  linear-gradient(180deg, rgba(226,208,240,.55) 0%, rgba(252,246,253,.55) 55%);
}

/* --- carved: the things you type into ----------------------------------
   No backdrop-filter here, deliberately. Every one of these sits on a surface
   that is ALREADY frosted — a card, a popover, or the page's own gradient —
   so blurring again re-blurs a blur and changes nothing you can see. It was
   costing 21 compositor layers out of 54 on the Log page for a difference
   that does not survive a screenshot comparison, and stacked backdrop layers
   are exactly what makes a phone's compositor produce ghosting artifacts.
   The 55% fill does the work on its own. */
.field input, .field textarea, .bm-entry input, #bubPopDose input {
  background: var(--glass-well);
}
.field input:focus, .field textarea:focus, .bm-entry input:focus, #bubPopDose input:focus {
  background: linear-gradient(180deg, rgba(251,244,252,.78) 0%, rgba(255,255,255,.78) 62%);
}

/* --- clay: the things you press ---------------------------------------- */
.nudge, .chips button, .bm-typed > summary, .howto {
  background: linear-gradient(165deg, rgba(255,255,255,.72) 0%, rgba(241,228,248,.62) 100%);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
}
.chips button:active {
  background: linear-gradient(165deg, rgba(246,236,250,.80) 0%, rgba(233,217,243,.72) 100%);
}

/* --- cards ------------------------------------------------------------- */
.bm-entry::after {
  background: rgba(255,255,255,.72);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
}

.bm-entry {
  background: linear-gradient(168deg, var(--glass) 0%, rgba(251,242,250,.58) 100%);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
}

/* --- the dock ---------------------------------------------------------- */
.tabbar { background: rgba(255,255,255,.60); }

/* --- the coloured actions ---------------------------------------------- */
/* These keep their colour but stop being solid slabs. Back up / Restore /
   Delete all are big enough that fully opaque they were three walls.
   .82 alpha was not enough to tell — it read as a flat tint rather than as
   glass. The tone now runs top to bottom, thin at the head and deep at the
   foot, like liquid standing in a tall glass: the airy part is where nothing
   sits, and the label at the bottom keeps a saturated ground to read against.
   That bottom stop is set by contrast, not by taste — white on the old Restore
   measured 3.81:1, under AA. The three now measure 5.25 / 4.75 / 6.20.
   A white rim inside the top edge is what actually sells it as lit. */
.can-backup {
  --glow: rgba(126,106,190,.60);
  background:
    linear-gradient(180deg, rgba(255,255,255,.40) 0%, rgba(255,255,255,0) 40%),
    linear-gradient(178deg, rgba(160,142,214,.44) 0%,
                            rgba(142,124,195,.74) 58%,
                            rgba(108,88,168,.94) 100%);
}
.can-restore {
  --glow: rgba(209,105,154,.58);
  background:
    linear-gradient(180deg, rgba(255,255,255,.42) 0%, rgba(255,255,255,0) 40%),
    linear-gradient(178deg, rgba(236,150,190,.44) 0%,
                            rgba(214,101,158,.74) 58%,
                            rgba(182,66,126,.94) 100%);
}
.can-delete {
  --glow: rgba(178,59,92,.55);
  background:
    linear-gradient(180deg, rgba(255,255,255,.38) 0%, rgba(255,255,255,0) 40%),
    linear-gradient(178deg, rgba(226,124,140,.44) 0%,
                            rgba(198,74,100,.74) 58%,
                            rgba(166,46,80,.95) 100%);
}
/* Each tile sits in a pool of its own colour. That is the whole of the oomph:
   a shadow that is merely dark reads as weight, one that carries the tile's
   own hue reads as light coming off it. */
.can {
  --glow: rgba(138,21,80,.34);
  -webkit-backdrop-filter: blur(18px) saturate(190%);
          backdrop-filter: blur(18px) saturate(190%);
  box-shadow: var(--clay),
              0 16px 26px -16px var(--glow),
              inset 0 1px 0 rgba(255,255,255,.72);
}
.can:active {
  box-shadow: var(--clay-press),
              0 7px 13px -12px var(--glow),
              inset 0 1px 0 rgba(255,255,255,.55);
}

@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  /* No blur means no separation, so the alpha has to carry it alone. */
  :root { --glass: rgba(255,255,255,.90);
          --glass-well: linear-gradient(180deg,rgba(226,208,240,.92) 0%,rgba(252,246,253,.92) 55%); }
  .nudge, .chips button, .bm-typed > summary, .howto {
    background: linear-gradient(165deg, rgba(255,255,255,.95) 0%, rgba(241,228,248,.92) 100%);
  }
  .tabbar { background: rgba(255,255,255,.92); }
}

@media (prefers-reduced-transparency: reduce) {
  .field input, .field textarea, .bm-entry input,
  .nudge, .chips button, .bm-typed > summary, .howto,
  .tog-disc, .bm-entry, .tabbar, .can,
  .bub, .bub-pop, #bubPopDose input {
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  .bm-entry, .tabbar, .bub { background: var(--white); }
  .drifter { opacity: .05; }
}

/* ---- she is on the splash again --------------------------------------- */
/* .splash is z-index 50. At 11 she was behind it, which is why she vanished
   from the one screen she is the star of. */
.drifter { z-index: 51; }

/* Full render in the app. She was a .13 ghost; the frosted surfaces are what
   soften her where she passes under them, not her own opacity.

   No --dh change here. Custom properties do not transition, so dropping her
   from 30vh to 26vh at the handover resized her by 13% in a single frame —
   half of what read as her "resetting" when the splash cleared. She is one
   size for the whole session now. */
body.app-ready .drifter { z-index: -1; opacity: 1; }

/* Nothing sits above her while the splash is up — that IS her on that screen —
   so there is no cover for the frame she drops behind the app, and z-index
   cannot be transitioned. She was crisp and on top of the Log screen for the
   better part of a second (the sky is gone by 820ms; app-ready did not land
   until the splash element's own fade ended at ~1160ms) and then snapped
   underneath it.

   So the layer change hides inside a fade of her own: she goes out with the
   artwork, changes layer while she is invisible, and comes back through the
   frosted surfaces as texture on the base 700ms transition. Quick out, slow
   in. Must stay after the app-ready rule above — same specificity, so source
   order is what decides the frame they overlap on. */
/* z-index does not transition, and she must not go dark on the way over — she
   is the one thing on screen for the whole handover.

   Fading her out and back was a disappearance. Swapping her layer under the
   still-opaque sky was the same disappearance with the fade taken off, because
   BEHIND THE APP is also behind the splash: a copy at -1 cannot be seen until
   the sky has gone, and the sky does not go for another 760ms.

   So both layers exist at once. A second copy of her is stacked where she is
   going, with its clocks set to the original's so it is the same figure in the
   same place, and the copy on top fades out on exactly sky-out's curve and
   timing. The two are complementary: the top one is worth (1 - sky) less each
   frame, the bottom one is revealed by precisely that much. Their sum is her,
   continuously, and it ramps .88 to 1 across the handover, which is the
   difference between her splash value and her app value. */
.drifter.drifter-ghost { z-index: -1; opacity: 1; }
.drifter.is-going { animation: drifter-hand-off 760ms var(--ease-in-out) 60ms forwards; }
@keyframes drifter-hand-off { to { opacity: 0; } }

/* The frame the real one replaces the copy: same layer, same opacity, so the
   substitution cannot be seen — but only if her own 700ms transition is not
   allowed to animate it. */
body.drifter-snap .drifter { transition: none; }

/* ---- keep her off the measurement diagram ------------------------------ */
/* The body map is the one place on the Log page where a second figure behind
   the artwork is genuinely confusing. It gets its own frosted panel, so she
   passes BEHIND the diagram rather than through it. */
/* No frame. The rectangle cut a hard edge straight across her as she floated
   behind it, which is the one place a straight line has no business being.
   The frost lives on the disc instead — round, and masked so even its own
   edge dissolves rather than stopping. */
.bm-wrap { position: relative; }
.bm-frost {
  position: absolute;
  left: 50%; top: 48.4%;          /* the disc: cx 250 cy 300 in a 500x620 box */
  width: 90%; aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  -webkit-backdrop-filter: blur(16px) saturate(160%);
          backdrop-filter: blur(16px) saturate(160%);
  -webkit-mask-image: radial-gradient(circle, #000 54%, rgba(0,0,0,.6) 80%, transparent 100%);
          mask-image: radial-gradient(circle, #000 54%, rgba(0,0,0,.6) 80%, transparent 100%);
  pointer-events: none;
  z-index: 0;
}
.bm-wrap > svg { position: relative; z-index: 1; }

/* ---- her wake ----------------------------------------------------------
   Not a trail. Four short arcs that surface for a moment and go, on a 3s
   beat with different offsets, so the water reads as disturbed rather than
   as a drawn path following her. Outside the spin layer, because water does
   not turn with the float. */
/* Square and centred on her ring. `inset` plus width/height fought each
   other and put the box down and to the right of her, and a non-square box
   would have turned every ripple into an ellipse. */
.dr-wake {
  position: absolute;
  left: 50%; top: 46%;
  width: 175%; aspect-ratio: 1; height: auto;
  transform: translate(-50%, -50%);
  color: rgba(255,255,255,.9);
  overflow: visible;
  pointer-events: none;
}
.wk { stroke-width: 2.1; opacity: 0;
      transform-box: view-box; transform-origin: 50% 50%; }
.wk-side { transform-box: view-box; transform-origin: 50% 50%; }

/* Only the trailing set. dr-x is 61s `alternate`, so a full there-and-back is
   122s: this runs once over that, showing the left set while she travels
   right and the right set while she comes back. */
.splash.animate .wk-left,  body.app-ready .wk-left  { animation: wake-lead 122s linear infinite; }
.splash.animate .wk-right, body.app-ready .wk-right { animation: wake-trail 122s linear infinite; }
@keyframes wake-lead  { 0%, 46% { opacity: 1; } 54%, 100% { opacity: 0; } }
@keyframes wake-trail { 0%, 46% { opacity: 0; } 54%, 100% { opacity: 1; } }

/* Each ring grows out of her and thins as it goes, staggered so there is
   always one arriving and one leaving. */
/* Four different clocks. One shared 3s period gave every ring the same beat
   and the whole thing read as a metronome; these do not share a common
   multiple worth noticing, so the surface never repeats visibly. */
.splash.animate .wk, body.app-ready .wk {
  animation: ripple var(--dur, 3.6s) var(--ease-out) var(--lag, 0s) infinite;
}
.wk.k1 { --dur: 3.1s; --lag: 0s;    --to: 1.34; }
.wk.k2 { --dur: 4.3s; --lag: .9s;   --to: 1.20; }
.wk.k3 { --dur: 3.7s; --lag: 2.1s;  --to: 1.42; }
.wk.k4 { --dur: 5.1s; --lag: 1.45s; --to: 1.26; }

@keyframes ripple {
  0%   { opacity: 0;   transform: scale(.42); stroke-width: 3.2; }
  14%  { opacity: .55; }
  55%  { opacity: .26; }
  100% { opacity: 0;   transform: scale(var(--to, 1.28)); stroke-width: 1.2; }
}

/* It was still on screen when the app arrived. It lands, holds, and is gone
   well before the splash is. */
.splash.animate .word { animation: word-life 5200ms var(--ease-in-out) 300ms both; }
@keyframes word-life {
  0%       { opacity: 0; transform: translate(-50%, -50%) scale(.92); }
  9%       { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  62%      { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100%     { opacity: 0; transform: translate(-50%, -50%) scale(1.06); }
}

@media (prefers-reduced-motion: reduce) {
  .wk { animation: none !important; opacity: .3; }
  .splash.animate .word { animation: none !important; opacity: 1; }
}

/* ============================================================
   THE WORDMARK — SVG text with a real gradient.

   It was HTML text tinted through background-clip:text. That is a paint path
   the offline renderer will not draw, so its colour could never be checked —
   and this splash has already lost its title once to exactly that class of
   thing. An SVG <linearGradient> paints everywhere.

   paint-order="stroke" is what makes it bubble lettering: the fat white
   stroke is laid down FIRST and the gradient fill lands on top, so the rim
   sits behind the letters instead of eating into them. Without it a stroke
   this heavy swallows the glyphs, which is exactly what a 13px HTML
   text-stroke did.
   ============================================================ */
/* aspect-ratio explicitly, matching the viewBox. An <svg> with a width and
   no height does NOT reliably take its height from the viewBox — here it
   collapsed to nothing and the title vanished a second time. */
.word-line { display: block; overflow: visible; height: auto; aspect-ratio: 640 / 210; }
.word-line text {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 132px;
  letter-spacing: -2px;
  text-anchor: middle;
  paint-order: stroke fill;
  stroke: #FFFFFF;
  stroke-width: 15;
  stroke-linejoin: round;
}
.wl1 text { fill: url(#wgA); }
.wl2 text { fill: url(#wgB); }
/* Bigger. It is the name of the app, on the one screen that is only about
   the name. */
/* Bigger again, and the two lines overlap slightly because the curve
   leaves air under the middle of the line above. */
.word-line.wl1 { width: 104%; max-width: 620px; }
.word-line.wl2 { width: 94%;  max-width: 560px; margin-top: -13%; }
.word { gap: 0; }

/* ============================================================
   THE BUBBLE ROW — weight, period, dose, the photo

   Weight was the only thing under the map speaking the body map's
   language — tap the small thing, a popover opens anchored to it.
   Dose and period were boxed form fields and the photo was a
   dashed drop target: three vocabularies on one page pretending to
   be one screen. All four are bubbles now, one rule set wearing
   four tints, distinguished only by what tapping one does — a
   popover, a toggle in place, or a straight trip to the picker.
   ============================================================ */

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

/* Pulled up into the dead space between her feet and the progress bar, same
   as the disc it replaces. The z-index is load-bearing, not decorative:
   .bm-wrap > svg carries z-index:1 of its own a few rules up, and without a
   higher one here it paints straight over the row.

   The negative margin-inline borrows a little of .wrap's own 16px padding
   (styles.css:90) rather than the row's — at 320px the four 68px-minimum
   bubbles plus three gaps need 288px and .wrap only offers 288px inside its
   padding, zero to spare. Reaching 6px into the padding on each side buys
   the gaps room without the row itself overflowing it. */
.log-bubbles {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: clamp(6px, 2vw, 16px);
  margin: 0 0 16px;
  margin-inline: -6px;
  position: relative;
  z-index: 2;
}

/* One custom property carries the tint through the icon, the ring and the
   gradient, so the four bubbles are a single rule set wearing four colours
   rather than four near-duplicate blocks. Weight keeps the disc's own
   violet as the base case; dose borrows the body map's right-arm pip
   rather than inventing a fifth colour for the row to carry. */
.bub { --bub: var(--violet); }
.bub-cycle { --bub: var(--rose); }
.bub-dose  { --bub: #F2A93B; }
.bub-photo { --bub: var(--blush); }

.bub {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px;
  /* Floor is 68px, not a rounder-looking 62px — that's the disc this row
     replaced, and "slightly bigger" than it was the brief. 19.5vw alone only
     clears 68px past a 348.7px viewport, so the floor is what actually holds
     the line on an SE-width phone; vw only matters above 349px, when it
     starts pushing bubbles past the 68px floor toward the 76px ceiling. */
  width: clamp(68px, 19.5vw, 76px); aspect-ratio: 1;
  border: none; border-radius: 50%;
  background: linear-gradient(165deg,
              rgba(255,255,255,.78) 0%,
              color-mix(in srgb, var(--bub) 9%, rgba(255,255,255,.62)) 100%);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  box-shadow: var(--clay-sm);
  color: var(--bub);
  font-family: inherit; cursor: pointer;
  transition: box-shadow var(--dur-press) var(--ease-out),
              transform var(--dur-press) var(--ease-out),
              background var(--dur-press) var(--ease-out);
}
.bub:active { transform: scale(.95); box-shadow: var(--clay-sm-press); }
.bub.open {
  box-shadow: var(--clay-sm-press),
              0 0 0 2px color-mix(in srgb, var(--bub) 55%, transparent);
}
/* Set reads as a deeper wash of the same tint, not a different state —
   this is still resting, just resting on a number instead of on empty. */
.bub.is-set {
  background: linear-gradient(165deg,
              rgba(255,255,255,.78) 0%,
              color-mix(in srgb, var(--bub) 24%, rgba(255,255,255,.62)) 100%);
}
.bub.is-set .bub-v { color: var(--wine); }

.bub-ico { width: 22px; height: 22px; margin-bottom: -1px; }
.bub-v {
  font-family: var(--font-display);
  font-size: 15px; font-weight: 800; line-height: 1;
  color: var(--divider-alt); font-variant-numeric: tabular-nums; letter-spacing: -.3px;
}
.bub-n {
  font-size: 9px; font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--muted-soft); line-height: 1;
}

/* Kept verbatim from the disc it replaces: a balloon, not a scale and not
   an anchor. A scale states the obvious and an anchor is unkind; a balloon
   means lightness, which is the direction of travel, and it draws in three
   strokes so it still reads at 22px. It drifts, because of course it does. */
.bub-ico-weight { animation: balloon-bob 5.5s var(--ease-in-out) infinite; }
@keyframes balloon-bob {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-2.5px) rotate(3deg); }
}

/* A boolean doesn't earn a popover — it fills instead, so "on" reads at a
   glance and is the single most saturated thing in the row when lit. */
.bub-cycle.is-on {
  background: linear-gradient(165deg, var(--rose) 0%, #B45781 100%);
}
.bub-cycle.is-on .bub-ico,
.bub-cycle.is-on .bub-v,
.bub-cycle.is-on .bub-n { color: #fff; }

/* The thumbnail takes the whole circle over; the icon and the value line
   have nothing left to say once there is a photo to look at instead. The
   name line survives, over a scrim, so the row still reads left to right
   even mid-way through a week with a photo already attached. */
.bub-photo-wrap { position: relative; width: clamp(68px, 19.5vw, 76px); aspect-ratio: 1; }
.bub-photo { width: 100%; height: 100%; position: relative; overflow: hidden; }
.bub-photo .photo-thumb { position: absolute; inset: 0; border-radius: 50%; overflow: hidden; }
.bub-photo .photo-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.bub-photo.has-photo .bub-ico,
.bub-photo.has-photo .bub-v { display: none; }
/* Pinned to the bottom rather than left in the flex column's centre — a
   caption over a bottom scrim reads as a caption only if it actually sits
   where the scrim is darkest. */
.bub-photo.has-photo .bub-n {
  position: absolute; left: 0; right: 0; bottom: 7px; z-index: 1; color: #fff;
}
.bub-photo.has-photo::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: linear-gradient(to top, rgba(58,46,77,.55), transparent 60%);
}
/* Nothing behind a filled bubble needs blurring, and stacking backdrop-filter
   under overflow:hidden + border-radius + an image layer is exactly the
   combination that renders as a solid block on some compositors. */
.bub-photo.has-photo {
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
/* Offset outside the wrap's own box so it overlaps the rim rather than
   sitting flush inside it — the same "torn corner" read a photo gets when
   it's actually stuck to something. A sibling of the button, not a child of
   it: a <button> cannot legally contain another <button>, and letting the
   parser fix that by closing the outer tag early is what would have pushed
   this off the bubble entirely. */
.photo-x {
  position: absolute; top: -4px; right: -4px; z-index: 2;
  width: 20px; height: 20px;
  border: none; border-radius: 50%;
  background: rgba(58, 46, 77, 0.72);
  color: var(--white);
  font-size: 10px;
  cursor: pointer;
  line-height: 1;
}

/* ---- the popovers -------------------------------------------------------
   Anchored to the ROW, centred on it, never to the individual bubble — a
   320px card hung off the rightmost bubble runs off a phone screen. The
   caret slides to sit under whichever bubble opened it instead, via
   --caret, exactly the technique .bm-entry already uses for the same
   reason. Below is the default; JS measures against the tab bar before
   showing and flips to .above when a downward popover would land under it. */
.bub-pop {
  position: absolute; left: 50%; z-index: 6;
  width: min(320px, calc(100vw - 44px));
  padding: 12px 13px 13px;
  border-radius: 20px;
  /* Heavier glass than anything else on purpose: everything else is a surface
     you look AT, this one sits OVER live content while you type into it. */
  background: linear-gradient(168deg, rgba(255,255,255,.95) 0%, rgba(252,246,253,.93) 100%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
          backdrop-filter: blur(24px) saturate(180%);
  box-shadow: 0 20px 44px -14px rgba(138,21,80,.42),
              0 4px 12px -6px rgba(138,21,80,.24),
              inset 0 1px 0 rgba(255,255,255,1);
  opacity: 0; pointer-events: none;
  transition: opacity 180ms var(--ease-out), transform 240ms var(--ease-out);
}
.bub-pop.below { top: calc(100% + 9px); transform: translateX(-50%) translateY(-8px) scale(.96); transform-origin: 50% 0; }
.bub-pop.above { bottom: calc(100% + 9px); transform: translateX(-50%) translateY(8px) scale(.96); transform-origin: 50% 100%; }
.bub-pop.show { opacity: 1; transform: translateX(-50%); pointer-events: auto; }
.bub-pop::after {
  content: ''; position: absolute; left: var(--caret, 50%); width: 13px; height: 13px; border-radius: 2px;
  background: rgba(255,255,255,.97);
  transform: translateX(-50%) rotate(45deg);
}
.bub-pop.below::after { top: -6.5px; }
.bub-pop.above::after { bottom: -6.5px; }

.bub-pop .hero-row { gap: 10px; justify-content: space-between; }
.bub-pop .hero-val input { font-size: 34px; width: 4ch; }
.bub-pop .hero-unit { font-size: 15px; }
.bub-pop .chips { margin-top: 11px; }
#bubPopDose label { display: block; font-size: 12.5px; font-weight: 600; color: var(--muted); margin: 0 0 6px; }
#bubPopDose input { width: 100%; padding: 11px 12px; font-size: 16px; font-family: inherit; color: var(--plum); }
.bub-pop-done {
  width: 100%; margin-top: 11px;
  border: none; border-radius: 13px; padding: 10px;
  font: 700 13.5px/1 inherit; color: #fff; cursor: pointer;
  background: linear-gradient(135deg, var(--violet), var(--rose));
  box-shadow: var(--clay-sm);
  transition: transform var(--dur-press) var(--ease-out);
}
.bub-pop-done:active { transform: scale(.97); box-shadow: var(--clay-sm-press); }

@media (prefers-reduced-motion: reduce) {
  .bub, .bub-pop, .bub-ico-weight { transition: none; animation: none !important; }
}

/* ============================================================
   THE POST-IT — notes, restyled as paper rather than another
   carved field.

   Everything else on this page is a number with a home to go to.
   Notes is the one field that is just whatever she wants to say,
   so it gets to look like the thing it actually is: a note stuck
   on afterwards, tilted, buttery yellow, not glass.
   ============================================================ */
/* A note stuck to the page reads as a note only if it doesn't span the
   column it's stuck to — full-width at -1.1deg the tilt was too shallow to
   see and it just read as one more rounded card in the stack. Capped width,
   a tilt you can actually clock, and a shorter reach so it sits closer to
   square than to a banner. The 8px side allowance below is the tilt's own
   bounding-box growth at -2deg over a ~110px-tall note (h*sin(2deg) ≈ 4px a
   side) plus a couple of px of slack — #main/.pane already clip overflow-x
   (styles.css:1963) so a rotated corner could never force page scroll, but
   the allowance keeps the corner from visibly clipping at 320px too. */
/* ---- the note ----------------------------------------------------------
   It was a yellow rectangle with a rule under a label, which is a form field
   wearing paper's colours. A page actually torn out of a notebook has four
   things a rectangle hasn't: rules under the writing, a ragged edge where it
   left the pad, tape holding it down, and something its owner stuck on it.

   Restrained on purpose — one strip of tape and one sprig. This sits on a
   page whose whole job is to stay quiet. */
.postit {
  position: relative;
  width: min(300px, calc(100% - 16px));
  margin: 0 auto 22px;
  transform: rotate(-2deg);
}

.postit-paper {
  position: relative;
  padding: 13px 15px 21px;      /* the bottom pad is deep because the tear eats it */
  background:
    /* paper tooth — barely there, but it stops the fill reading as flat ink */
    radial-gradient(rgba(255,255,255,.5) .5px, rgba(255,255,255,0) .5px) 0 0 / 4px 4px,
    linear-gradient(170deg, var(--postit) 0%, var(--postit-deep) 100%);
  /* Torn, not guillotined. clip-path takes box-shadow with it, so the lift is
     a drop-shadow filter — which has the bonus of following the ragged
     silhouette instead of a rectangle the eye can still see. */
  clip-path: polygon(
    0 0, 100% 0,
    100% calc(100% - 8px), 94% calc(100% - 2px), 88% calc(100% - 7px),
    82% calc(100% - 3px),  76% calc(100% - 8px), 70% calc(100% - 1px),
    64% calc(100% - 6px),  58% calc(100% - 2px), 52% calc(100% - 7px),
    46% calc(100% - 3px),  40% calc(100% - 8px), 34% calc(100% - 2px),
    28% calc(100% - 6px),  22% calc(100% - 1px), 16% calc(100% - 7px),
    10% calc(100% - 3px),   4% calc(100% - 8px), 0 calc(100% - 4px));
  filter: drop-shadow(0 9px 14px rgba(138,21,80,.26))
          drop-shadow(0 2px 3px rgba(138,21,80,.16));
  transition: filter var(--dur-fast) ease;
}
/* Torn paper goes pale along the tear where the fibres pull apart. */
.postit-paper::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0; height: 11px;
  background: linear-gradient(to top, rgba(255,255,255,.62), rgba(255,255,255,0));
  pointer-events: none;
}

/* No rule under it any more — the ruled lines below do that job, and two
   horizontals stacked is what made this read as a box with a line in it. */
.postit-lbl {
  display: block;
  margin-bottom: 5px;
  font-size: 10.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
  color: #9A8340;
}

/* Transparent to the paper underneath — this is ink on the note, not a field
   on top of it. The rules live on the textarea rather than the paper so they
   land exactly on the writing: the gradient's period IS the line-height, which
   is why that is a round 28px and not a ratio. font-hand's fallback stack
   matters here more than almost anywhere else in the app — sw.js ignores
   cross-origin requests, so offline this reads Bradley Hand on iOS and still
   looks handwritten. */
.postit textarea {
  display: block;
  width: 100%;
  min-height: 84px;                     /* three rules show even when empty */
  border: none; outline: none; resize: none;
  padding: 0;
  box-shadow: none;
  background: repeating-linear-gradient(180deg,
              rgba(255,255,255,0) 0 27px,
              rgba(154,131,64,.22) 27px 28px);
  font-family: var(--font-hand);
  font-size: 19px; line-height: 28px;   /* Caveat runs small for its point size */
  color: #5A4A2E;                       /* var(--plum) on yellow goes muddy */
}
.postit textarea::placeholder { color: #A89257; }

/* Washi, torn off by hand rather than cut — the ragged ends are the whole
   tell. Pale enough to see the paper through it, because tape you cannot see
   through is a sticker, and a saturated stripe reads as gift wrap. */
.postit-tape {
  position: absolute; z-index: 2;
  top: -11px; left: 50%;
  width: 96px; height: 23px;
  transform: translateX(-62%) rotate(-5.5deg);
  background:
    /* fibre, not stripe — at any more contrast than this it reads as gift wrap */
    repeating-linear-gradient(90deg,
      rgba(255,255,255,.12) 0 5px, rgba(255,255,255,0) 5px 11px),
    linear-gradient(160deg, rgba(201,182,228,.50), rgba(232,160,191,.38));
  clip-path: polygon(4% 8%, 96% 0, 100% 46%, 95% 100%, 5% 92%, 0 50%);
  box-shadow: 0 1px 2px rgba(138,21,80,.10);
}

/* Faded and behind the writing, because it is pressed into the page rather
   than stuck onto it — and because a botanical someone kept is an adult's
   journal, where a die-cut sticker sitting proud of the paper is a child's. */
.postit-sprig {
  position: absolute; z-index: 0;
  right: 5px; bottom: 11px;
  width: 40px; height: 49px;
  color: var(--rose);
  opacity: .42;
  transform: rotate(11deg);
  pointer-events: none;
}
.postit-sprig .sprig-stem {
  stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; fill: none;
}
.postit-sprig .sprig-leaf path {
  fill: currentColor; fill-opacity: .45;
  stroke: currentColor; stroke-width: 1; stroke-linejoin: round;
}
/* The writing sits over the sprig, never under it. */
.postit-lbl, .postit textarea { position: relative; z-index: 1; }

/* Ring the paper, not the ink — a focus ring on a transparent textarea would
   draw a hard rectangle across a rotated, torn note and break it outright. */
.postit:focus-within .postit-paper {
  filter: drop-shadow(0 9px 14px rgba(138,21,80,.26))
          drop-shadow(0 2px 3px rgba(138,21,80,.16))
          drop-shadow(0 0 3px rgba(198,160,60,.85));
}

@media (prefers-contrast: more) {
  .postit { transform: none; }
  .postit textarea {
    color: #3E3220;
    background: repeating-linear-gradient(180deg,
                rgba(255,255,255,0) 0 27px,
                rgba(120,100,45,.45) 27px 28px);
  }
  /* Decoration is the first thing to go when someone has asked for contrast. */
  .postit-tape, .postit-sprig { display: none; }
}

/* ---- quieten the two disclosures --------------------------------------
   They were full-width slabs with the same weight as the things you act on,
   which is what made this page read as a stack of detached panels. They are
   secondary, so they look secondary: pills, sized to their own text. */
.bm-typed > summary, .howto summary {
  display: inline-flex; align-items: center; gap: 7px;
  width: auto; border-radius: 999px; padding: 8px 15px;
  font-size: 12.5px; font-weight: 700; color: var(--ghost);
}
.howto {
  background: none; box-shadow: none;
  border-radius: 16px;
}
.howto:not([open]) { background: none; }
.howto[open] {
  background: linear-gradient(180deg, rgba(255,255,255,.55) 0%, rgba(255,255,255,.45) 100%);
  -webkit-backdrop-filter: var(--glass-blur); backdrop-filter: var(--glass-blur);
  box-shadow: var(--clay-sm);
}
.bm-typed > summary, .howto:not([open]) summary {
  background: linear-gradient(165deg, rgba(255,255,255,.66) 0%, rgba(241,228,248,.56) 100%);
  box-shadow: var(--clay-sm);
}

