  /* Self-hosted fonts (P4) - previously loaded from fonts.googleapis.com /
     fonts.gstatic.com at runtime. Vendoring these removes a render-blocking
     cross-origin round trip and lets font-src/style-src drop those hosts
     from CSP. Files are the exact ones Google's own css2 API serves for
     these weights/styles (v32 Nunito / v40 Playfair Display, latin subset) -
     Nunito and Playfair's "normal" style are variable fonts, so one physical
     file legitimately backs multiple font-weight declarations below. */
  @font-face {
    font-family: 'Nunito';
    font-style: normal;
    font-weight: 400 800;
    font-display: swap;
    src: url('/fonts/nunito-latin.woff2') format('woff2');
  }
  @font-face {
    font-family: 'Playfair Display';
    font-style: normal;
    font-weight: 500 600;
    font-display: swap;
    src: url('/fonts/playfair-normal-latin.woff2') format('woff2');
  }
  @font-face {
    font-family: 'Playfair Display';
    font-style: italic;
    font-weight: 500;
    font-display: swap;
    src: url('/fonts/playfair-italic-500-latin.woff2') format('woff2');
  }

  :root {
    /* Force native form controls (inputs, date pickers, scrollbars) to follow
       our own light/dark toggle instead of the phone's system setting —
       otherwise a device in system dark mode renders input fields with dark
       native chrome while the rest of the app stays in our light theme. */
    color-scheme: light;
    --rose: #e8819a;
    /* rose-deep and ink-soft were darkened from #c75b77/#9a8090 - the
       originals measured 3.82:1 and 3.38:1 against --cream, both failing
       WCAG AA (needs 4.5:1 for normal text). New values pass at ~4.9:1 and
       ~5.0:1. Dark mode's own values already passed and are untouched. */
    --rose-deep: #b04a66;
    --rose-soft: rgba(232,129,154,0.18);
    --rose-glow: rgba(199,91,119,0.25);
    --cream: #fff7f3;
    --card: rgba(255,255,255,0.55);
    --glass: rgba(255,255,255,0.45);
    --glass-border: rgba(255,255,255,0.7);
    --ink: #3a2a34;
    --ink-soft: #7d6472;
    --line: rgba(199,91,119,0.15);
    --gold: #e8b04b;
    --shadow: 0 8px 32px -8px rgba(160,80,110,0.22);
    --glass-shadow: 0 4px 24px rgba(160,80,110,0.12), inset 0 1px 0 rgba(255,255,255,0.8);
  }

  [data-dark] {
    color-scheme: dark;
    --cream: #140e12;
    --card: rgba(40,22,32,0.6);
    --glass: rgba(40,22,32,0.5);
    --glass-border: rgba(255,180,200,0.12);
    --ink: #f0e0e8;
    --ink-soft: #b89aaa;
    --line: rgba(199,91,119,0.18);
    --rose-soft: rgba(232,129,154,0.15);
    --rose-glow: rgba(199,91,119,0.2);
    --shadow: 0 8px 32px -8px rgba(0,0,0,0.4);
    --glass-shadow: 0 4px 24px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,180,200,0.1);
  }
  [data-dark] html {
    background: #12080f;
  }
  [data-dark] body {
    background: radial-gradient(ellipse at 20% 10%, #2a0d1a 0%, #12080f 40%, #0e0a14 100%) fixed !important;
  }
  [data-dark] .craving {
    background: rgba(40,16,28,0.5);
  }
  [data-dark] .count-hero::before,
  [data-dark] .tl::before {
    background: linear-gradient(var(--rose-deep), #5a2a6a);
  }
  [data-dark] .tl-item::before { box-shadow: 0 0 0 4px #140e12; }
  [data-dark] .ic-grid .ph { background: rgba(40,22,32,0.6); }
  [data-dark] header.app{
      background: rgba(18,12,18,0.42);
      border-bottom-color: rgba(255,220,230,0.08);
    }

    [data-dark] .brand-mark{
      background: rgba(255,255,255,0.06);
      border-color: rgba(255,220,230,0.12);
      box-shadow:
        0 8px 24px rgba(0,0,0,0.22),
        inset 0 1px 0 rgba(255,255,255,0.08);
    }

    [data-dark] .brand-mark path{
      fill: #f3d7e2;
    }

    [data-dark] .gear{
      background: rgba(255,255,255,0.06);
      border-color: rgba(255,220,230,0.12);
      color: #f0e0e8;
      box-shadow:
        0 8px 24px rgba(0,0,0,0.22),
        inset 0 1px 0 rgba(255,255,255,0.08);
    }
  [data-dark] nav.tabs{
    background: rgba(26,16,24,0.16);
    border-color: rgba(255,210,225,0.14);
    box-shadow:
      0 14px 40px rgba(0,0,0,0.28),
      inset 0 1px 0 rgba(255,255,255,0.1),
      inset 0 -1px 1px rgba(0,0,0,0.06);
  }

  /* Same low-opacity/faint-tint glass treatment as the light-mode pill
     (see the base nav.tabs .tab-pill rule) - just a solid gradient before,
     now a translucent tinted layer over the bar's own blur. */
  [data-dark] nav.tabs .tab-pill{
    background: linear-gradient(135deg, rgba(232,129,154,0.22), rgba(255,255,255,0.06));
    border-color: rgba(255,255,255,0.10);
    box-shadow:
      0 8px 24px rgba(0,0,0,0.24),
      inset 0 1px 0 rgba(255,255,255,0.12),
      inset 0 -1px 1px rgba(0,0,0,0.06);
  }
  [data-dark] .sheet { background: rgba(22,12,18,0.96); backdrop-filter: blur(40px); -webkit-backdrop-filter: blur(40px); }
  [data-dark] .field { background: rgba(40,22,32,0.7); border-color: var(--line); color: var(--ink); }
  [data-dark] .mood-cell, [data-dark] .note, [data-dark] .seg { background: rgba(40,22,32,0.6); border-color: var(--line); }
  [data-dark] .emoji-pick button { background: rgba(40,22,32,0.6); border-color: var(--line); }
  [data-dark] .gear { background: rgba(40,22,32,0.7); }
  [data-dark] .chip { background: rgba(40,22,32,0.6); border-color: var(--line); }
  [data-dark] .btn.ghost { background: rgba(40,22,32,0.5); border-color: rgba(199,91,119,0.3); }
  [data-dark] .acc .acc-chev{
      color: rgba(240,224,232,0.58);
    }

    [data-dark] .acc[open] .acc-chev{
      color: rgba(255,255,255,0.92);
    }
  
  /* Note: dark styling is driven exclusively by the [data-dark] attribute
     above (set by the in-app toggle, seeded from prefers-color-scheme only
     on first-ever load). There used to also be a @media
     (prefers-color-scheme: dark) block here duplicating a subset of these
     rules — it reacted to the phone's system setting independently of the
     in-app toggle, so the two could disagree (e.g. phone in system dark
     mode, app explicitly set to light) and produce a half-dark/half-light
     mix, since only some elements had a matching [data-dark] rule. Removed
     so there's a single source of truth. */
  /* Disables pinch-zoom and double-tap-to-zoom app-wide (panning/scrolling
     stays allowed - pan-x/pan-y are the only gestures granted, pinch-zoom
     and double-tap-zoom are simply not in this list). Paired with
     maximum-scale=1/user-scalable=no on the viewport meta tag in
     index.html for browsers that still honor it; touch-action is the more
     reliable of the two since some browsers (notably iOS Safari) ignore
     user-scalable=no for accessibility reasons but still respect this.

     This used to be set via the universal `*` selector, applying an
     explicit touch-action to every single element in the DOM. That turned
     out to be the actual cause of a real "completely unscrollable, 100% of
     the time, from the very first paint" report (Chrome on a Nothing Fold)
     - taps still worked, no swipe ever produced a scroll. That fingerprint
     doesn't match the scroll-lock race this file's overscroll-behavior
     comment below is about (those are intermittent, not present on every
     fresh load); it matches a known Chromium caveat instead - giving every
     element its own explicit touch-action value can knock gesture handling
     off the fast compositor-thread path onto a slower/inconsistent
     main-thread path on some engine builds, and it can fail silently rather
     than just being slower. touch-action is inherited via ancestor-chain
     intersection (an element's used value is the intersection of its own
     value with every ancestor's), so setting it once on <html> below is
     exactly as effective at blocking pinch/double-tap-zoom app-wide as
     setting it on every element was - it just no longer forces that
     per-element resolution. */
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
  html,body{margin:0;padding:0}
  html {
    background: #fff0f8;
    /* overscroll-behavior-y history: 'none' originally (blocks pull-to-
       refresh/bounce chaining entirely) -> 'contain' in v1.13.4 on a wrong
       theory that 'none' caused a Nothing Fold "completely unscrollable"
       report (v1.13.5 found the actual cause: an app-wide touch-action
       rule, see above) -> back to 'none' in v1.13.6 to fix a white flash
       during iOS bounce that 'contain' had re-enabled. Removed entirely
       here: a real, isolated report (physical mouse, Incognito - so
       extensions and the specific browser profile are ruled out - other
       sites scroll fine with the same mouse in the same window) showed the
       *wheel* not scrolling this page at all, while dragging the scrollbar
       still worked - meaning the document itself was never actually
       overflow-locked, only wheel-driven scrolling into it was failing.
       That fingerprint matches this property specifically: this file
       already carried a comment (see above) that 'none' has a known
       history of "disabling scrolling outright, not just the bounce
       effect" on some engine builds - what's new here is evidence that
       'contain' can trigger the same failure mode, not just 'none', and
       that it isn't limited to Android/WebView. There's no way left to
       keep any restrictive value here without risking this, so the
       pull-to-refresh/bounce-chaining prevention this property existed for
       is being given up entirely in favor of scrolling actually working. */
    overflow-x: hidden;
    touch-action: pan-x pan-y;
  }
  /* iOS Safari-only bounce/rubber-band containment (v1.14.2): removing
     overscroll-behavior-y entirely above fixed a real desktop-Chrome wheel-
     scroll bug, but it also undid the earlier iOS bounce/white-flash fix -
     with nothing set, iOS Safari's default rubber-banding at the very top/
     bottom reveals blank background past the content edge. The desktop bug
     was only ever observed on desktop Chrome with a physical mouse, never on
     iOS, so scoping the restriction to iOS Safari specifically (via the
     standard -webkit-touch-callout feature-detect, which is only truthy on
     iOS WebKit) restores bounce containment there without reopening the
     wheel-scroll bug anywhere else. */
  @supports (-webkit-touch-callout: none) {
    html, body {
      overscroll-behavior-y: none;
    }
  }
  body {
    font-family: 'Nunito', system-ui, sans-serif;
    color: var(--ink);
    background: radial-gradient(ellipse at 15% 0%, #ffe8f0 0%, #fff0f8 35%, #f3eaff 70%, #ffe8f5 100%) fixed;
    min-height: 100vh;
    min-height: 100dvh;
    overflow-x: hidden;
    width: 100%;
  }
  h1,h2,h3,.serif{font-family:'Playfair Display',serif;font-weight:600}
  #app{
    max-width:560px;margin:0 auto;padding:0 16px 130px;
    min-height: 100vh;
    min-height: 100dvh;
  }

  /* Boot splash (v1.15.0): a static overlay painted the instant index.html's
     CSS loads - before app.js has even finished fetching/parsing/mounting
     Preact - so there's no blank gap between the native iOS "Add to Home
     Screen" splash (which uses the same gradient/heart, see the
     apple-touch-startup-image links in index.html) and the web app's own
     first paint. app.js removes/animates this out once the real content
     behind it is actually ready (see the boot-splash useEffect in App()) -
     this file only owns the static look and the reveal animation itself. */
  #boot-splash{
    position: fixed;
    inset: 0;
    z-index: 9999;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #733c4e;
  }
  @media (prefers-color-scheme: dark){
    #boot-splash{
      background: #1a0a13;
    }
    #boot-splash .boot-heart-stop-1,
    #boot-splash .boot-heart-stop-2{ stop-color: #412235; }
    #boot-splash .boot-heart{ filter: drop-shadow(0 10px 32px rgba(214,90,130,0.55)); }
    #boot-splash .boot-kicker{ color: rgba(240,224,232,0.6); }
    #boot-splash .boot-word{ color: #f0e0e8; text-shadow: 0 8px 22px rgba(0,0,0,0.4); }
  }
  #boot-splash .boot-wrap{
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
  #boot-splash .boot-heart{
    width: 108px;
    height: 108px;
    filter: drop-shadow(0 10px 28px rgba(110,20,55,0.35));
  }
  #boot-splash .boot-kicker{
    margin-top: 18px;
    font-family: 'Nunito', sans-serif;
    font-weight: 800;
    font-size: 12px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: rgba(255,255,255,0.78);
  }
  #boot-splash .boot-word{
    margin-top: 4px;
    font-family: 'Playfair Display', serif;
    font-weight: 600;
    font-size: 38px;
    letter-spacing: -0.01em;
    color: #ffffff;
    text-shadow: 0 8px 22px rgba(110,20,55,0.28);
  }
  /* Reveal: a "step through the heart" handoff, not a flat fade or a plain
     circle. A heart-shaped window, centered exactly where the heart sits
     (measured in app.js so it lines up regardless of device height), grows
     from the heart's own position out past every screen corner - the home
     screen blooms out of the heart itself and swallows the whole splash,
     like walking through a doorway shaped like the heart, rather than
     everything fading to white or growing through a generic circle.
     Driven from app.js via requestAnimationFrame, writing a fresh
     clip-path: path(evenodd, "...") value to this element every frame -
     the path string is a big rectangle plus the heart shape as a second
     evenodd subpath, i.e. "everywhere except the heart", so the heart
     itself is the hole. Two earlier approaches were tried and dropped:
     first a CSS-custom-property-driven radial-gradient mask, then an SVG
     <mask> element referenced via mask-image: url(#id) - neither animated
     reliably, and the mask-image version specifically produced a solid
     black screen (or no visible reveal at all) on real iOS Safari despite
     working correctly in every desktop/simulator test. clip-path: path()
     needs no cross-element ID reference and no luminance/alpha color
     semantics at all - it's pure geometry - which is why it was chosen
     over both mask-based approaches for this specific effect. Text
     disappears first and fast, since it shouldn't visually travel with the
     heart - it gets consumed by the growing window instead. */
  #boot-splash.boot-reveal{
    pointer-events: none;
  }
  #boot-splash.boot-reveal .boot-kicker,
  #boot-splash.boot-reveal .boot-word{
    animation: bootTextFade .22s ease forwards;
  }
  @keyframes bootTextFade{
    to{ opacity: 0; transform: translateY(-6px) scale(.94); }
  }
  .muted{color:var(--ink-soft)}
  .center{text-align:center}
  button{font-family:inherit;cursor:pointer;border:none}
  input,textarea,select{font-family:inherit;font-size:16px;color:var(--ink)}

  /* Header
     Previously position:sticky. Sticky ties the header's stacking to its
     spot in #app's own document flow - fine while always visible, but
     fragile once JS starts sliding it off/on with a transform: several
     rounds of tuning the show/hide *logic* never fixed reveal-on-scroll-up
     reliably, which pointed at the positioning model itself rather than the
     thresholds. Switched to position:fixed so the header is a genuinely
     independent layer over the page (not "stacked" between #app's own
     content, which is what made it look like it could permanently scroll
     away instead of being able to reappear anywhere, including near the
     bottom of a tall tab like Home's love-burst/love-note-wall sections).
     left/right:0 + max-width/margin:auto centers it exactly like #app does,
     since it's no longer inside #app's own padding/max-width box for layout
     purposes. A .header-spacer right after it (see app.js) reserves the
     same height in normal flow so content doesn't jump up underneath it. */
  header.app{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:14px;
    padding: calc(env(safe-area-inset-top) + 10px) 18px 14px;
    position: fixed;
    top: var(--banner-h, 0px);
    left: 0;
    right: 0;
    width: 100%;
    max-width: 560px;
    margin: 0 auto;
    box-sizing: border-box;
    z-index: 20;
    background: rgba(255,247,243,0.52);
    backdrop-filter: blur(24px) saturate(1.8);
    -webkit-backdrop-filter: blur(24px) saturate(1.8);
    border-bottom: 1px solid rgba(255,255,255,0.42);
    transform: translateZ(0);
    will-change: transform;
    /* backdrop-filter deliberately isn't in this list (it was, originally -
       see below). header.app's *own* backdrop-filter only ever toggles
       between blur(24px) at the top and none while floating (the actual
       visible floating blur lives entirely on the ::before layer below,
       with its own opacity transition) - animating both at once meant two
       separate blur transitions overlapping with different timing, which
       read as the floating blur taking an extra beat to catch up after the
       header had already finished sliding into place. header.app's own
       backdrop-filter now just snaps instantly with the class change;
       ::before's opacity fade is the only blur animation left, so there's
       nothing left for it to race against. */
    transition: transform .28s cubic-bezier(.2,.8,.2,1), top .28s cubic-bezier(.2,.8,.2,1),
      background .28s ease, border-color .28s ease;
  }
  header.app.header-hidden{
    transform: translate3d(0, -115%, 0);
  }
  /* Third state, distinct from both the normal (at-top) header above and
     the fully-hidden one above: reappearing mid-scroll - scrolled down past
     the top, then scrolled back up far enough to trigger a reveal without
     returning all the way to y<40 - drops the frosted-glass background/
     blur/border entirely instead of showing the same solid bar again, the
     way Instagram's own hide-on-scroll-down/reveal-on-scroll-up header
     does: just the notification bell, brand mark/title, and settings gear
     floating directly over whatever content is underneath. The three
     elements keep their own individual pill/circle backgrounds (.brand-mark,
     .gear below) so they stay legible on their own; only the header bar's
     own rectangular background goes away. The brand text has no background
     of its own to fall back on here, so it gets a soft halo shadow instead
     for the same legibility reason - light-mode text is dark, so it gets a
     light halo; dark-mode text is light, so it gets a dark one. */
  header.app.header-floating{
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-bottom-color: transparent;
  }
  /* The blur itself lives on a ::before layer instead of directly on
     header.app.header-floating, because a plain flat blur() applied to the
     whole bar (tried first) either looked like a visible glass panel with
     a hard edge, or - if lightened enough to avoid that - wasn't doing
     much for legibility right at the top where it's needed most. A
     mask-image gradient fixes that, but masking header.app itself would
     also fade out the actual icons/text (mask affects the whole element,
     children included) - not just the blur - which isn't wanted; the
     bell/brand/gear need to stay fully visible all the way down. Splitting
     the blur onto its own absolutely-positioned, pointer-events:none layer
     behind the real content (z-index:-1, inside header.app's own stacking
     context from its position:fixed + z-index above - see the base
     header.app rule - so this can't escape below the rest of the page)
     means the mask only ever fades the *blur*, while .brand/.gear/h1 above
     it stay exactly as visible at the bottom of the header as at the top.

     Defined on the *base* header.app selector (always present in the box),
     not scoped to .header-floating, specifically so it can transition.
     A ::before only exists in the render tree at all while its selector
     matches - scoping it to .header-floating meant the whole blur+gradient
     effect popped in at full strength the instant that class was added and
     vanished instantly when removed, no matter what transition was
     declared on it, since there was no "before" state for the browser to
     interpolate from. Keeping the pseudo-element permanently in the box
     and animating its opacity between 0 and 1 instead gives an actual
     value to transition, so the blur genuinely fades in/out over time
     rather than snapping. */
  header.app::before{
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 96%);
    mask-image: linear-gradient(to bottom, #000 0%, #000 30%, transparent 96%);
    /* Matches header.app's own .28s transform duration above (was .4s) -
       the blur now fades in step with the header sliding into place
       instead of visibly trailing behind it. */
    transition: opacity .28s ease;
  }
  header.app.header-floating::before{
    opacity: 1;
  }
  header.app.header-floating .brand-kicker,
  header.app.header-floating h1{
    text-shadow: 0 1px 3px rgba(255,255,255,0.9), 0 2px 14px rgba(255,255,255,0.65);
  }
  [data-dark] header.app.header-floating .brand-kicker,
  [data-dark] header.app.header-floating h1{
    text-shadow: 0 1px 3px rgba(0,0,0,0.7), 0 2px 14px rgba(0,0,0,0.45);
  }
  /* Reserves the combined height of the fixed header (and the re-add
     banner above it, while shown) in normal flow so the tab content
     underneath doesn't render hidden behind them. The + 10px restores the
     breathing room the old sticky header used to get for free from its own
     `margin: 0 -16px 10px` - that margin doesn't mean anything for a
     position:fixed element (nothing sits "after" it in flow to be pushed
     down), so without adding it back here explicitly, content directly
     underneath (e.g. the countdown card) rendered flush against/under the
     header instead of with its old gap. */
  .header-spacer{
    height: calc(var(--header-h, 76px) + var(--banner-h, 0px) + 10px);
    flex: none;
  }
  .brand{
    display:flex;
    align-items:center;
    gap:12px;
    min-width:0;
  }

  .brand-mark{
    width:40px;
    height:40px;
    border-radius:14px;
    display:flex;
    align-items:center;
    justify-content:center;
    background: rgba(255,255,255,0.28);
    border: 1px solid rgba(255,255,255,0.5);
    box-shadow:
      0 6px 20px rgba(160,80,110,0.10),
      inset 0 1px 0 rgba(255,255,255,0.55);
    backdrop-filter: blur(16px) saturate(1.6);
    -webkit-backdrop-filter: blur(16px) saturate(1.6);
    flex:none;
    position: relative;
    padding: 0;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    touch-action: manipulation;
  }

  .brand-mark .notif-dot{
    position: absolute;
    top: -2px; right: -2px;
    width: 11px; height: 11px;
    border-radius: 50%;
    background: #e0355a;
    border: 2px solid var(--cream, #fff0f8);
    box-shadow: 0 1px 4px rgba(200,20,60,0.4);
  }
  [data-dark] .brand-mark .notif-dot{ border-color: #140e12; }

  .brand-mark svg{
    width:18px;
    height:18px;
    fill: url(#none);
    stroke: none;
  }

  .brand-mark path{
    fill: var(--rose-deep);
    opacity: .92;
  }

  .brand-copy{
    min-width:0;
  }

  .brand-kicker{
    font-size:11px;
    line-height:1.1;
    letter-spacing:.08em;
    text-transform:uppercase;
    color: var(--ink-soft);
    font-weight:800;
    margin-bottom:4px;
  }
  header.app h1{
    margin:0;
    font-family:'Playfair Display', serif;
    font-size:24px;
    line-height:1;
    letter-spacing:-0.02em;
    color: var(--ink);
  }

  .gear svg{
    width:19px;
    height:19px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity:.92;
    transition: transform .3s cubic-bezier(.2,.8,.2,1);
  }
  .gear:active svg{ transform: rotate(35deg); }
  /* Dark-mode toggle icon (sun/moon) — the swap between icons is paired with
     a slow 180° spin of this wrapper so flipping themes feels like turning a
     dial rather than an abrupt icon swap. The button's own .gear:active svg
     tap-bounce still layers on top of this since it targets the inner svg. */
  .theme-icon{
    display: inline-flex;
    transition: transform .5s cubic-bezier(.2,.8,.2,1);
  }
  .gear {
    width: 44px; height: 44px; border-radius: 50%;
    background: var(--glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    font-size: 18px; display: flex; align-items: center; justify-content: center;
    color: var(--rose-deep); flex: none;
  }
  /* Small circular icon-only action button (matches .gear's glass treatment) */
  .icon-btn{
    width: 44px; height: 44px; border-radius: 50%; flex: none;
    background: var(--glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    display: flex; align-items: center; justify-content: center;
    color: var(--rose-deep);
  }
  .icon-btn svg{ width: 19px; height: 19px; }
  .icon-btn:active{ transform: scale(0.94); }
  /* iOS "please re-add to Home Screen" update banner. Used to be
     position:relative + a full-bleed negative-margin trick, staying in
     #app's normal flow so it pushed the (position:sticky) header down and
     scrolled away as you scrolled past it. Now that header.app is
     position:fixed (see header.app below for why), it no longer has a
     flow position for anything to "push down" - so this is fixed too,
     pinned above the header (higher z-index), spanning the viewport
     directly instead of needing the negative-margin hack. It's dismissed
     via its own "Got it" button rather than by scrolling past it; header.app
     reads its measured height back via --banner-h (set in app.js) to sit
     right below it instead of underneath/behind it. */
  .readd-banner{
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 25;
    box-sizing: border-box;
    padding: 14px 20px 16px;
    padding-top: calc(14px + env(safe-area-inset-top));
    background:
      linear-gradient(180deg, rgba(255,255,255,0.16) 0%, rgba(255,255,255,0) 40%),
      linear-gradient(135deg, var(--rose) 0%, var(--rose-deep) 100%);
    color: #fff;
    border-radius: 0 0 22px 22px;
    box-shadow: 0 10px 28px rgba(160,60,90,0.35);
  }
  .readd-banner-inner{
    display: flex;
    align-items: center;
    gap: 12px;
    max-width: 560px;
    margin: 0 auto;
  }
  .readd-banner-icon{
    flex: none;
    width: 30px; height: 30px;
    display: flex; align-items: center; justify-content: center;
    background: rgba(255,255,255,0.24);
    border: 1px solid rgba(255,255,255,0.3);
    border-radius: 50%;
  }
  .readd-banner-icon svg{ width: 16px; height: 16px; }
  .readd-banner p{
    flex: 1;
    margin: 0;
    font-size: 13px;
    line-height: 1.45;
    font-weight: 600;
    letter-spacing: 0.1px;
  }
  .readd-banner button{
    flex: none;
    background: rgba(255,255,255,0.24);
    border: 1px solid rgba(255,255,255,0.3);
    color: #fff;
    border-radius: 999px;
    padding: 9px 16px;
    font-size: 12px;
    font-weight: 800;
    transition: transform .12s ease, background .12s ease;
  }
  .readd-banner button:active{ transform: scale(0.94); background: rgba(255,255,255,0.32); }
  /* Live strength meter for the "change our key" field */
  .key-strength{
    display: flex;
    align-items: center;
    gap: 9px;
    margin-top: 9px;
  }
  .key-strength-bars{
    display: flex;
    gap: 4px;
    flex: 1;
  }
  .key-strength-bar{
    height: 4px;
    flex: 1;
    border-radius: 999px;
    background: rgba(154,128,144,0.25);
    transition: background .18s ease;
  }
  .key-strength-bar.on.weak{ background: #d1495b; }
  .key-strength-bar.on.medium{ background: #dd9c2e; }
  .key-strength-bar.on.strong{ background: #4f9d69; }
  .key-strength-label{
    flex: none;
    font-size: 11.5px;
    font-weight: 800;
    text-align: right;
  }
  .key-strength-label.weak{ color: #c23a4f; }
  .key-strength-label.medium{ color: #b07a1c; }
  .key-strength-label.strong{ color: #3d8557; }
  .key-strength-label.invalid{ color: var(--ink-soft); font-weight: 700; }
  /* "What's new" patch notes sheet */
  .wn-badge{
    width: 36px; height: 36px; border-radius: 50%; flex: none;
    display: flex; align-items: center; justify-content: center;
    background: linear-gradient(135deg, var(--rose) 0%, var(--rose-deep) 100%);
    color: #fff;
    font-size: 17px;
    box-shadow: 0 3px 12px var(--rose-glow);
  }
  .wn-entry{
    background: var(--card);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    border: 1px solid var(--glass-border);
    border-radius: 18px;
    padding: 14px 16px;
    margin-bottom: 12px;
  }
  .wn-entry-head{
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 10px;
  }
  .wn-version{
    font-family: 'Playfair Display', serif;
    font-weight: 700;
    font-size: 16px;
    color: var(--ink);
  }
  .wn-date{
    font-size: 11.5px;
    color: var(--ink-soft);
    font-weight: 700;
  }
  .wn-items{ display: flex; flex-direction: column; gap: 9px; }
  .wn-item{
    display: flex;
    gap: 10px;
    align-items: flex-start;
    font-size: 13.5px;
    line-height: 1.45;
    color: var(--ink);
  }
  .wn-item-icon{ flex: none; font-size: 14px; margin-top: 1px; }
  .wn-footer-link{
    background: none;
    border: none;
    color: var(--rose-deep);
    font-weight: 700;
    font-size: 12.5px;
    padding: 0;
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  /* Cards */
  .card {
    background: var(--card);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-radius: 24px;
    box-shadow: var(--glass-shadow);
    padding: 20px;
    margin-bottom: 16px;
    border: 1px solid var(--glass-border);
  }
  .card h2 { font-size: 19px; margin: 0 0 4px; }
  .eyebrow{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--rose-deep);font-weight:800;margin-bottom:8px;font-family:'Nunito'}

  /* Buttons */
  .btn {
    /* Was a --rose -> --rose-deep gradient; the lighter --rose end measured
       2.61:1 for the white button text, failing WCAG AA even for large/bold
       text (needs 3:1). Solid --rose-deep (now ~5.2:1 against white) fixes
       it without losing the color. */
    background: var(--rose-deep);
    color: #fff; border-radius: 16px; padding: 13px 18px;
    font-weight: 800; font-size: 15px;
    box-shadow: 0 4px 20px var(--rose-glow), inset 0 1px 0 rgba(255,255,255,0.2);
    border: none;
    transition: transform 0.1s, box-shadow 0.1s;
    touch-action: manipulation;
  }
  .btn:active { transform: scale(0.97); box-shadow: 0 2px 10px var(--rose-glow); }
  .btn.ghost {
    background: var(--glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    color: var(--rose-deep);
    border: 1px solid rgba(199,91,119,0.25);
    box-shadow: var(--glass-shadow);
  }
  .btn.block { width: 100%; }
  .btn.sm { padding: 8px 13px; font-size: 13px; border-radius: 12px; }
  /* Inputs */
  .field {
    width: 100%;
    background: var(--glass);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--glass-border);
    border-radius: 14px; padding: 13px 14px; margin-top: 8px; outline: none;
    color: var(--ink);
    transition: border-color 0.2s, box-shadow 0.2s;
  }
  .field:focus {
    border-color: var(--rose);
    box-shadow: 0 0 0 3px var(--rose-glow);
  }
  /* Keyboard focus was nearly invisible app-wide (only .field had its own
     style) - this covers every other interactive element without adding a
     ring on mouse/touch clicks, since :focus-visible only fires for
     keyboard/programmatic focus in modern browsers. */
  :focus-visible {
    outline: 2px solid var(--rose-deep);
    outline-offset: 2px;
  }
  .field:focus-visible {
    outline: none; /* .field already has its own focus treatment above */
  }
  /* D1: utility classes for the most-repeated inline style="..." patterns
     found in app.js (256 occurrences audited 2026-07-03). Converting these
     moves them under the [data-dark] token system and out of harm's way of
     the "255 inline styles bypass theming" issue the report flagged - this
     is a deliberate first slice (the safe, unambiguous, single-attribute
     cases), not a full elimination; see CLAUDE.md for the rest. */
  .fw-800 { font-weight: 800; }
  .m-0 { margin: 0; }
  .min-w-0 { min-width: 0; }
  .mt-6 { margin-top: 6px; }
  .mt-8 { margin-top: 8px; }
  .mt-10 { margin-top: 10px; }
  .mt-12 { margin-top: 12px; }
  .mt-14 { margin-top: 14px; }
  .mt-16 { margin-top: 16px; }
  .mt-20 { margin-top: 20px; }
  .flex-1 { flex: 1; }
  .d-block { display: block; }
  .field-emoji { width: 56px; text-align: center; padding: 10px 6px; }
  .muted-lead { margin: 2px 0 12px; }
  .muted-small { font-size: 12px; margin: 2px 0 8px; }
  .mt-6-0 { margin: 6px 0 0; }
  .mb-6 { margin-bottom: 6px; }
  /* "danger" outline variant for destructive ghost buttons (delete/remove) */
  .btn.ghost.danger { color: #b13; border-color: #f3c2c2; }
  .fs-12 { font-size: 12px; }
  .flex-none { flex: none; }
  .mb-10 { margin-bottom: 10px; }
  /* freeform user-entered note/story text - preserves line breaks the user typed */
  .note-text { margin: 6px 0 0; white-space: pre-wrap; }
  .mb-12 { margin-bottom: 12px; }
  .mb-14 { margin-bottom: 14px; }
  /* input + button side-by-side row, used for "add a new item" fields */
  .add-row { display: flex; gap: 8px; align-items: center; margin-top: 14px; }
  .fw-600 { font-weight: 600; }
  .fs-11 { font-size: 11px; }
  .row-between { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
  .add-row-tight { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
  .row-gap8 { display: flex; gap: 8px; }
  /* Toggleable meal-time tags, used both when editing an eating-out idea and
     when picking the pool for a brand-new one. */
  .mealtag-picker { display: flex; flex-wrap: wrap; gap: 6px; }
  .mealtag {
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    border-radius: 999px; padding: 6px 12px;
    font-size: 12.5px; font-weight: 700; color: var(--ink-soft);
    transition: background .15s, border-color .15s, color .15s;
  }
  .mealtag.on {
    background: linear-gradient(135deg, var(--rose) 0%, var(--rose-deep) 100%);
    border-color: transparent;
    color: #fff;
  }
  /* One meal period's start/end editor, grouped into its own card (instead
     of a flat stack of label+input pairs) so each period reads as a single
     unit at a glance. */
  .meal-period-card {
    background: var(--glass);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--glass-border);
    border-radius: 16px;
    padding: 12px 14px;
    margin-top: 10px;
  }
  .meal-period-head { display: flex; align-items: center; gap: 8px; font-weight: 800; }
  .meal-period-icon { font-size: 18px; }
  .meal-period-sep { color: var(--ink-soft); font-weight: 800; }
  .poster-thumb { width: 54px; height: 78px; border-radius: 11px; object-fit: cover; }
  /* clamp long text (e.g. movie/show overviews) to 2 lines with an ellipsis */
  .clamp-2 { font-size: 11px; margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  /* monospace display of a couple key (settings / rotate-key screens) */
  .key-display { font-weight: 800; font-family: monospace; font-size: 15px; margin-top: 6px; word-break: break-all; }
  .error-text { color: var(--rose-deep); font-size: 13px; margin: 8px 0 0; }

  /* A6: respect prefers-reduced-motion - zero out animation/transition
     durations app-wide instead of trying to special-case each one. */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.001ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.001ms !important;
      scroll-behavior: auto !important;
    }
  }
  textarea.field{resize:vertical;min-height:64px}
  label.lbl{font-size:13px;font-weight:800;color:var(--ink-soft)}
  /* Custom date field (native date inputs render inconsistently on iOS) */
  .date-field{position:relative;display:flex;align-items:center;justify-content:space-between;cursor:pointer}
  .date-field span.dv{font-size:15px}
  .date-field .date-chev{color:var(--rose-deep);flex:none;display:flex;align-items:center;opacity:.85}
  .date-field input[type="date"]{
    position:absolute; inset:0; width:100%; height:100%;
    margin:0; padding:0; border:none; background:transparent;
    opacity:0; cursor:pointer;
  }
  .row{display:flex;gap:10px}
  .row>*{flex:1;min-width:0}

  /* Countdown */
  .count-hero {
    background: linear-gradient(160deg, #3d2233 0%, #5c2740 45%, #7a3350 100%);
    color: #fbeef1; border-radius: 28px; padding: 34px 28px; text-align: center;
    box-shadow: 0 20px 48px rgba(50,20,35,0.35), inset 0 1px 0 rgba(255,255,255,0.08);
    margin-bottom: 16px; position: relative; overflow: hidden;
    border: 1px solid rgba(255,255,255,0.09);
  }
  .count-hero::after {
    content: '';
    position: absolute; inset: 0;
    background: radial-gradient(circle at 82% -12%, rgba(240,205,150,0.22) 0%, transparent 55%);
    border-radius: inherit; pointer-events: none;
  }
  .count-hero .kicker {
    position: relative;
    font-size: 11.5px;
    letter-spacing: .22em;
    text-transform: uppercase;
    font-weight: 800;
    color: rgba(251,238,241,0.62);
  }
  .count-hero .big{
    position: relative;
    font-family:'Playfair Display'; font-weight:600; font-size:56px; line-height:1; margin:14px 0 2px;
    background: linear-gradient(135deg, #fff 0%, #f0cf9a 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    letter-spacing: -0.01em;
  }
  .count-hero .unit{
    position: relative;
    font-size: 11.5px; letter-spacing: .16em; text-transform: uppercase;
    font-weight: 800; color: rgba(251,238,241,0.62);
  }
  .count-hero .divider{
    position: relative;
    width: 34px; height: 1px; margin: 18px auto 16px;
    background: linear-gradient(90deg, transparent, rgba(240,205,150,0.65), transparent);
  }
  .count-hero .lbl{opacity:.92;font-weight:700}
  .count-hero .ev{
    position: relative;
    font-family:'Playfair Display'; font-weight:500; font-size:20px; letter-spacing:-0.01em;
  }
  .count-hero .ev-date{
    display: block; margin-top: 5px;
    font-family: 'Nunito'; font-size: 11.5px; letter-spacing: .06em; text-transform: uppercase;
    font-weight: 700; color: rgba(251,238,241,0.6);
  }

  /* Moods */
  .mood-grid{display:flex;gap:12px}
  .mood-cell {
    flex: 1;
    min-width: 0;
    background: var(--glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    border-radius: 18px; padding: 14px; text-align: center;
    transition: transform 0.15s, box-shadow 0.15s;
    cursor: pointer;
  }
  .mood-cell:active { transform: scale(0.96); }
  .mood-cell.readonly { cursor: default; opacity: 0.72; }
  .mood-cell.readonly:active { transform: none; }
  .mood-cell .emoji{font-size:38px;line-height:1}
  .mood-cell .who{font-weight:800;font-size:13px;margin-top:6px}
  .mood-cell .lab{font-size:12px;color:var(--ink-soft)}
  .emoji-pick{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
  .emoji-pick button {
    font-size: 26px;
    background: var(--glass);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--glass-border);
    border-radius: 13px; width: 46px; height: 46px;
    transition: transform 0.1s;
  }
  .emoji-pick button:active { transform: scale(0.9); }
  .emoji-pick button.on { border-color: var(--rose); background: var(--rose-soft); box-shadow: 0 0 0 2px var(--rose-glow); }
  .chip {
    display: inline-flex; align-items: center; gap: 5px;
    background: var(--glass);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--glass-border);
    border-radius: 999px; padding: 4px 10px; font-size: 12px; margin: 3px 3px 0 0;
  }
  .pill {
    display: inline-flex; align-items: center; gap: 6px;
    background: var(--rose-soft);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: var(--rose-deep); border-radius: 999px; padding: 5px 12px;
    font-size: 12.5px; font-weight: 800;
    border: 1px solid rgba(199,91,119,0.2);
  }
  /* Craving */
  .craving {
    background: var(--glass);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    text-align: center;
    border: 1px solid var(--glass-border);
  }
  .craving .result{font-size:54px;margin:6px 0}
  .craving .name{font-family:'Playfair Display';font-size:23px}

  /* Notes feed */
  .note {
    background: var(--glass);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-radius: 16px; padding: 13px 14px; margin-top: 10px;
    border: 1px solid var(--glass-border);
  }
  .note .meta{font-size:11.5px;color:var(--ink-soft);font-weight:700;margin-top:6px}
  /* Connected-account status row (e.g. Apple Calendar in Settings) — an
     inline row rather than a card, since this sits directly under its own
     section label/description and doesn't need a second boxed container. */
  .caldav-status{
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 2px 0 4px;
  }
  .caldav-status-info{
    display: flex;
    align-items: center;
    gap: 11px;
    min-width: 0;
  }
  .caldav-dot{
    width: 9px; height: 9px; border-radius: 50%; flex: none;
    background: #4f9d69;
    box-shadow: 0 0 0 4px rgba(79,157,105,0.16);
  }
  .caldav-status-name{
    font-weight: 800;
    font-size: 15px;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .caldav-status-sub{
    font-size: 11.5px;
    font-weight: 700;
    color: var(--ink-soft);
    margin-top: 1px;
  }
  .link-row{position:relative}
  .reminder-row{position:relative}

  /* Coming up — collapsible extra events */
  .more-events{
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .34s cubic-bezier(.4,0,.2,1);
  }
  .more-events.open{
    grid-template-rows: 1fr;
  }
  .more-events > div{
    overflow: hidden;
  }
  .more-toggle{
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
    margin-top: 12px;
    padding: 9px 12px;
    background: var(--glass);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--glass-border);
    border-radius: 999px;
    font-size: 12px;
    font-weight: 800;
    letter-spacing: .03em;
    color: var(--rose-deep);
    transition: background .15s ease;
  }
  .more-toggle:active{ transform: scale(0.97); }
  .more-toggle .chev{
    width: 13px; height: 13px;
    display: inline-flex;
    transition: transform .28s cubic-bezier(.4,0,.2,1);
  }
  .more-toggle.open .chev{
    transform: rotate(180deg);
  }

  /* Watch */
  .watch-item{display:flex;gap:12px;align-items:flex-start;padding:14px 0;border-top:1px solid var(--line)}
  .watch-item:first-child{border-top:none}
  .poster {
    width: 54px; height: 78px; border-radius: 12px; flex: none;
    display: flex; align-items: center; justify-content: center; font-size: 26px;
    background: linear-gradient(135deg, var(--rose-soft), rgba(180,140,220,0.2));
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
  }
  .stars{display:inline-flex;gap:2px}
  .stars span{font-size:18px;color:#e7d3da;cursor:pointer}
  .stars span.on{color:var(--gold)}
  .seg {
    display: inline-flex;
    flex-wrap: wrap;
    max-width: 100%;
    background: var(--glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    border-radius: 13px; overflow: hidden;
  }
  .seg button { padding: 7px 11px; font-size: 12.5px; font-weight: 800; background: transparent; color: var(--ink-soft); }
  .seg button.on {
    background: linear-gradient(135deg, var(--rose) 0%, var(--rose-deep) 100%);
    color: #fff;
    box-shadow: 0 2px 10px var(--rose-glow);
  }

  /* Timeline */
  .tl{position:relative;padding-left:26px}
  .tl::before{content:"";position:absolute;left:8px;top:6px;bottom:6px;width:2px;background:linear-gradient(var(--rose-soft),#e7d3fb)}
  .tl-item{position:relative;margin-bottom:18px}
  .tl-item::before{content:"";position:absolute;left:-22px;top:6px;width:11px;height:11px;border-radius:50%;background:var(--rose-deep);box-shadow:0 0 0 4px #fff}
  .tl-photo{width:100%;display:block;max-height:300px;object-fit:cover;cursor:zoom-in}
  .tl-photo-wrap{border-radius:16px;margin-top:10px;min-height:120px;max-height:300px}
  .tl-gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:10px}
  .tl-gallery-item{aspect-ratio:1;border-radius:12px;cursor:zoom-in}
  .tl-gallery-item img{width:100%;height:100%;object-fit:cover;display:block}

  /* Shimmering placeholder for photos while they're still loading (D-style
     lazy image), used by the LazyPhoto component - reserves space right away
     instead of the browser's default alt-text + broken-image icon. */
  .lazy-photo{position:relative;overflow:hidden}
  .lazy-photo img{opacity:0;transition:opacity .25s ease}
  .lazy-photo.loaded img{opacity:1}
  .lazy-photo.error img{display:none}
  .lazy-photo.loading,.lazy-photo.error{background:#f3e7ec}
  [data-dark] .lazy-photo.loading,[data-dark] .lazy-photo.error{background:rgba(40,22,32,0.6)}
  .lazy-photo-skel{
    position:absolute;inset:0;
    background:linear-gradient(100deg, rgba(0,0,0,.05) 25%, rgba(0,0,0,.11) 45%, rgba(0,0,0,.05) 65%);
    background-size:200% 100%;
    animation:lazyPhotoSweep 1.4s ease-in-out infinite;
  }
  [data-dark] .lazy-photo-skel{
    background:linear-gradient(100deg, rgba(255,255,255,.05) 25%, rgba(255,255,255,.12) 45%, rgba(255,255,255,.05) 65%);
    background-size:200% 100%;
  }
  @keyframes lazyPhotoSweep{0%{background-position:200% 0}100%{background-position:-200% 0}}
  .lazy-photo-fallback{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;opacity:.5}
  @media (prefers-reduced-motion: reduce){
    .lazy-photo-skel{animation:none}
  }

  /* Jump-to-date calendar (Story tab) */
  .cal-zoom{
    display: flex; align-items: center; gap: 6px;
    background: var(--glass);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    border-radius: 999px;
    padding: 8px 16px 8px 18px;
    font-family: 'Playfair Display', serif; font-weight: 600; font-size: 17px;
    color: var(--ink);
    cursor: pointer;
    transition: transform .12s ease, box-shadow .12s ease;
  }
  .cal-zoom:active{ transform: scale(0.96); }
  .cal-zoom:disabled{ cursor: default; box-shadow: none; background: transparent; border-color: transparent; padding-left: 0; }
  .cal-zoom-chev{
    width: 15px; height: 15px; flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--rose-deep);
  }
  .cal-zoom-chev svg{ width: 100%; height: 100%; }
  .cal-nav{
    width: 32px; height: 32px; border-radius: 50%;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    display: flex; align-items: center; justify-content: center;
    font-size: 17px; font-weight: 800; color: var(--rose-deep);
    line-height: 1;
  }
  .cal-nav:active{ transform: scale(0.92); }
  .cal-grid{
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 5px;
  }
  .cal-dow{
    text-align: center;
    font-size: 11px;
    font-weight: 800;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--ink-soft);
    padding: 2px 0 8px;
  }
  .cal-day{
    position: relative;
    aspect-ratio: 1;
    display: flex; align-items: center; justify-content: center;
    border-radius: 13px;
    font-size: 13.5px;
    font-weight: 700;
    background: transparent;
    border: none;
    color: var(--ink-soft);
    opacity: .35;
  }
  .cal-day.empty{ opacity: 0; pointer-events: none; }
  .cal-day:disabled{ cursor: default; }
  .cal-day.has{
    opacity: 1;
    color: #fff;
    background: linear-gradient(135deg, var(--rose) 0%, var(--rose-deep) 100%);
    box-shadow: 0 3px 12px var(--rose-glow);
    cursor: pointer;
  }
  .cal-day.has:active{ transform: scale(0.92); }
  .cal-day.today{ box-shadow: 0 0 0 2px var(--rose-deep) inset; }
  .cal-day.has.today{ box-shadow: 0 3px 12px var(--rose-glow), 0 0 0 2px #fff inset; }
  /* Selected-day marker for read-only history calendars (e.g. mood history) -
     distinct from .today since a selected day and today can differ. */
  .cal-day.sel{ outline: 2px solid var(--rose-deep); outline-offset: 2px; }
  .cal-day .cal-dot{
    position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%);
    width: 4px; height: 4px; border-radius: 50%;
    background: #fff; opacity: .9;
  }
  /* Month/year picker grids (zoomed-out calendar views) */
  .cal-picker-grid{
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 8px;
  }
  .cal-cell{
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px;
    padding: 16px 8px;
    border-radius: 14px;
    font-size: 14px;
    font-weight: 800;
    background: transparent;
    border: none;
    color: var(--ink-soft);
    opacity: .35;
  }
  .cal-cell:disabled{ cursor: default; }
  .cal-cell.has{
    opacity: 1;
    color: #fff;
    background: linear-gradient(135deg, var(--rose) 0%, var(--rose-deep) 100%);
    box-shadow: 0 3px 12px var(--rose-glow);
    cursor: pointer;
  }
  .cal-cell.has:active{ transform: scale(0.95); }
  .cal-cell.today{ box-shadow: 0 0 0 2px var(--rose-deep) inset; }
  .cal-cell.has.today{ box-shadow: 0 3px 12px var(--rose-glow), 0 0 0 2px #fff inset; }
  .cal-cell-count{
    font-size: 10px;
    font-weight: 700;
    opacity: .85;
  }

  /* Lightbox */
  .lightbox-scrim{
    position: fixed; inset: 0; z-index: 85;
    background: rgba(10,6,10,0.94);
    display: flex; align-items: center; justify-content: center;
    padding: calc(env(safe-area-inset-top) + 14px) 16px calc(env(safe-area-inset-bottom) + 14px);
  }
  .lightbox-img{
    max-width: 100%; max-height: 100%;
    object-fit: contain;
    border-radius: 14px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  }
  .lightbox-close{
    position: absolute; top: calc(env(safe-area-inset-top) + 14px); right: 16px;
    width: 40px; height: 40px; border-radius: 50%;
    background: rgba(255,255,255,0.12);
    backdrop-filter: blur(8px);
    color: #fff; display: flex; align-items: center; justify-content: center;
    font-size: 18px; border: 1px solid rgba(255,255,255,0.2);
    z-index: 2;
  }
  .lightbox-nav{
    position: absolute; top: 50%; transform: translateY(-50%);
    width: 44px; height: 44px; border-radius: 50%;
    background: rgba(255,255,255,0.12);
    backdrop-filter: blur(8px);
    color: #fff; display: flex; align-items: center; justify-content: center;
    font-size: 22px; border: 1px solid rgba(255,255,255,0.2);
    z-index: 2;
  }
  .lightbox-nav.prev{ left: 14px; }
  .lightbox-nav.next{ right: 14px; }
  .lightbox-counter{
    position: absolute; bottom: calc(env(safe-area-inset-bottom) + 18px); left: 50%; transform: translateX(-50%);
    font-size: 11.5px; font-weight: 800; letter-spacing: .04em; color: rgba(255,255,255,0.8);
    background: rgba(255,255,255,0.12);
    backdrop-filter: blur(8px);
    padding: 5px 13px; border-radius: 999px;
    border: 1px solid rgba(255,255,255,0.15);
  }
  .tl-date{font-size:12px;font-weight:800;color:var(--rose-deep)}
  .tl-attribution{
    margin-top: 12px;
    padding-top: 9px;
    border-top: 1px solid var(--line);
    font-size: 10px;
    letter-spacing: .05em;
    text-transform: uppercase;
    font-weight: 700;
    color: var(--ink-soft);
    opacity: .7;
  }

  /* Memory card menu */
  .tl-card{position:relative}
  .tl-menu-btn{
    position:absolute; top:10px; right:10px;
    width:24px; height:24px; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:transparent; border:none; cursor:pointer;
    color:var(--ink-soft); opacity:.55;
    font-size:15px; letter-spacing:1px; line-height:1;
    transition:opacity .15s ease, background .15s ease;
  }
  .tl-menu-btn:hover, .tl-menu-btn.open{opacity:1; background:rgba(0,0,0,0.05)}
  .tl-menu-pop{
    position:absolute; top:36px; right:10px; z-index:20;
    min-width:132px;
    background:var(--card);
    backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    border:1px solid var(--glass-border);
    border-radius:14px;
    box-shadow: var(--glass-shadow);
    padding:6px;
    display:flex; flex-direction:column; gap:2px;
    animation: tlMenuIn .12s ease;
  }
  @keyframes tlMenuIn{ from{opacity:0; transform:translateY(-4px) scale(.97)} to{opacity:1; transform:translateY(0) scale(1)} }
  .tl-menu-pop button{
    all:unset; box-sizing:border-box;
    display:block; width:100%; padding:8px 10px;
    font-size:13px; font-weight:600; border-radius:9px;
    cursor:pointer; color:var(--ink);
  }
  .tl-menu-pop button:hover{background:rgba(0,0,0,0.05)}
  .tl-menu-pop button.danger{color:#b13}
  .tl-menu-pop button.danger:hover{background:rgba(187,17,51,0.08)}

  /* Event row menu (inline, not corner-absolute like .tl-menu-btn — event
     rows are single-line and have no reserved top-right corner space).
     The dropdown itself is rendered via a portal to <body> (position:fixed,
     inline top/right), not nested in the row, so it always paints above
     sibling rows regardless of their own backdrop-filter stacking contexts. */
  .ev-menu-btn{
    width:24px; height:24px; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    background:transparent; border:none; cursor:pointer;
    color:var(--ink-soft); opacity:.55;
    font-size:15px; letter-spacing:1px; line-height:1;
    transition:opacity .15s ease, background .15s ease;
  }
  .ev-menu-btn:hover, .ev-menu-btn.open{opacity:1; background:rgba(0,0,0,0.05)}
  .ev-menu-pop{
    position:absolute; top:calc(100% + 6px); right:0; z-index:20;
    min-width:132px;
    background:var(--card);
    backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    border:1px solid var(--glass-border);
    border-radius:14px;
    box-shadow: var(--glass-shadow);
    padding:6px;
    display:flex; flex-direction:column; gap:2px;
    animation: tlMenuIn .12s ease;
  }
  .ev-menu-pop button{
    all:unset; box-sizing:border-box;
    display:block; width:100%; padding:8px 10px;
    font-size:13px; font-weight:600; border-radius:9px;
    cursor:pointer; color:var(--ink);
  }
  .ev-menu-pop button:hover{background:rgba(0,0,0,0.05)}
  .ev-menu-pop button.danger{color:#b13}
  .ev-menu-pop button.danger:hover{background:rgba(187,17,51,0.08)}

  /* iCloud grid */
  .ic-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px}
  .ic-grid .ph{position:relative;aspect-ratio:1;border-radius:12px;overflow:hidden;background:#f3e7ec}
  .ic-grid .ph img{width:100%;height:100%;object-fit:cover;display:block}
  .ic-grid .ph button{position:absolute;right:5px;bottom:5px;background:rgba(199,91,119,.92);color:#fff;border-radius:9px;font-size:11px;font-weight:800;padding:4px 7px}

  /* Bottom nav */
  nav.tabs{
    position: fixed;
    left: 50%;
    bottom: calc(14px + env(safe-area-inset-bottom));
    /* translateZ(0) forces its own GPU compositing layer - without it, iOS
       Safari (especially in home-screen/standalone PWA mode) can let a fixed
       + backdrop-filter element fall a frame behind during fast scrolling,
       so it visibly detaches and floats over the content mid-scroll instead
       of staying pinned to the bottom of the viewport. will-change reinforces
       the same layer promotion ahead of time rather than on first scroll. */
    transform: translate(-50%, 0) translateZ(0);
    -webkit-transform: translate(-50%, 0) translateZ(0);
    transition: transform .28s cubic-bezier(.2,.8,.2,1), opacity .18s ease;
    will-change: transform;
    width: calc(100% - 28px);
    max-width: 420px;
    height: 68px;
    padding: 8px;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    align-items: center;
    border-radius: 999px;
    background: rgba(255,255,255,0.08);
    backdrop-filter: blur(16px) saturate(2.2);
    -webkit-backdrop-filter: blur(16px) saturate(2.2);
    border: 1px solid rgba(255,255,255,0.32);
    box-shadow:
      0 14px 40px rgba(160,80,110,0.14),
      inset 0 1px 0 rgba(255,255,255,0.35),
      inset 0 -1px 1px rgba(0,0,0,0.06);
    z-index: 30;
    overflow: hidden;
  }

  /* Shrinks slightly while scrolling down, back to full size while
     scrolling up - driven by the same headerHidden state/detection as the
     header itself (app.js), so it shares one already-proven-reliable
     scroll-direction signal instead of adding a second one. transform-origin
     stays centered (the default) so it shrinks evenly in place rather than
     drifting, since the base rule already centers the bar via
     translate(-50%, 0). */
  nav.tabs.nav-shrunk{
    transform: translate(-50%, 0) translateZ(0) scale(0.78);
    -webkit-transform: translate(-50%, 0) translateZ(0) scale(0.78);
  }

  /* iOS can visibly misposition position:fixed elements while the on-screen
     keyboard is opening/open (see app.js's keyboardOpen effect for the full
     explanation) - rather than fighting that with viewport math, the nav
     bar is just hidden whenever a text input has focus, and fades back in
     on blur. pointer-events:none so it can't be tapped while invisible. */
  nav.tabs.nav-hidden{
    opacity: 0;
    pointer-events: none;
  }

  /* Active pill: a second, denser glass layer stacked on the bar's own
     blur, not a solid color card sitting underneath it. Opacity is low and
     pink is only a faint gradient tint (never the dominant fill) so it
     reads as tinted glass rather than a painted-on chip. The glow/shadow
     matches the bar's own "thickness" logic (neutral outer shadow + top
     and bottom inset highlights) instead of a colored glow, so the pill
     looks like a physical layer of the same material as the bar, not a
     sticker on top of it. */
  nav.tabs .tab-pill{
    position: absolute;
    top: 8px;
    left: 8px;
    width: calc((100% - 16px) / 5);
    height: calc(100% - 16px);
    border-radius: 999px;
    background: linear-gradient(135deg, rgba(232,129,154,0.16), rgba(255,255,255,0.12));
    backdrop-filter: blur(10px) saturate(2.4);
    -webkit-backdrop-filter: blur(10px) saturate(2.4);
    border: 1px solid rgba(255,255,255,0.32);
    box-shadow:
      0 8px 24px rgba(0,0,0,0.16),
      inset 0 1px 0 rgba(255,255,255,0.35),
      inset 0 -1px 1px rgba(0,0,0,0.06);
    transition: transform .28s cubic-bezier(.2,.8,.2,1);
    pointer-events: none;
  }

  /* Progressive enhancement: where the browser actually supports feeding an
     SVG filter (feTurbulence + feDisplacementMap, defined once in app.js as
     #liquid-distort) through backdrop-filter, the pill distorts the pixels
     behind it - a genuine refraction ripple - rather than just blurring
     them. Support for filter-driven backdrop-filter is inconsistent across
     browsers, so this only applies on top of the plain blur+saturate above
     when @supports confirms the browser will accept the value; everywhere
     else the plain blur/saturate glass from above is what's shown. */
  @supports (backdrop-filter: blur(1px) url(#liquid-distort)) or (-webkit-backdrop-filter: blur(1px) url(#liquid-distort)) {
    nav.tabs .tab-pill{
      backdrop-filter: blur(10px) saturate(2.4) url(#liquid-distort);
      -webkit-backdrop-filter: blur(10px) saturate(2.4) url(#liquid-distort);
    }
  }

  nav.tabs .tab-pill.watch{
    transform: translateX(100%);
  }

  nav.tabs .tab-pill.story{
    transform: translateX(200%);
  }

  nav.tabs .tab-pill.finds{
    transform: translateX(300%);
  }

  nav.tabs .tab-pill.wheel{
    transform: translateX(400%);
  }

  nav.tabs button{
    position: relative;
    z-index: 1;
    height: 100%;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--ink-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
  }

  nav.tabs button .ic{
    width: 22px;
    height: 22px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  nav.tabs button .ic svg{
    width: 22px;
    height: 22px;
    overflow: visible;
  }

  nav.tabs button.on{
    /* A flat #fff read as pasted onto a now-translucent glass pill instead
       of lifted off it. A deeper rose (chosen after live-testing against
       the actual pill) plus a subtle drop-shadow (works on the SVG icon
       itself, unlike box-shadow) gives it a bit of lift instead of looking
       like a flat sticker. */
    color: rgba(160, 80, 110, 0.88);
    filter: drop-shadow(0 1px 1.5px rgba(0,0,0,0.22));
  }

  nav.tabs button:active{
    transform: scale(0.96);
  }

  /* Modal */
  /* --vvh (set in app.js from window.visualViewport.height, kept in sync on
     resize/orientation change) is used here instead of 100dvh - both
     100vh and 100dvh left a real gap between the scrim/sheet and the true
     bottom of the screen in the installed iOS PWA (standalone mode), which
     a dvh-only attempt at fixing this didn't resolve. Measuring the actual
     visible viewport in JS sidesteps CSS viewport-unit quirks entirely.
     100dvh stays as the fallback for the (very unlikely) case JS hasn't
     run yet. */
  .scrim{position:fixed;inset:0;height:var(--vvh, 100dvh);background:rgba(60,30,45,.42);display:flex;align-items:flex-end;justify-content:center;z-index:50;padding:0}
  .sheet {
    background: rgba(255,250,252,0.85);
    backdrop-filter: blur(40px) saturate(2);
    -webkit-backdrop-filter: blur(40px) saturate(2);
    border-radius: 28px 28px 0 0; width: 100%; max-width: 560px;
    padding: 22px 18px calc(22px + env(safe-area-inset-bottom));
    /* Previously a flat 90vh, which doesn't reserve any room for the status
       bar/Dynamic Island - a long sheet (e.g. Settings) would stretch up
       until it was nearly flush against it. This caps it so there's always
       at least ~16px of clearance below the safe area at the very top
       (outside the sheet, in the scrim), regardless of how much content the
       sheet holds - the sheet's own padding doesn't need to double up on
       safe-area-inset-top on top of that. */
    max-height: min(90vh, calc(var(--vvh, 100dvh) - env(safe-area-inset-top) - 16px));
    overflow: auto;
    box-shadow: 0 -12px 60px rgba(160,80,110,0.15);
    border-top: 1px solid rgba(255,255,255,0.8);
  }
  .grab{
    width: 44px;
    height: 5px;
    border-radius: 999px;
    background: rgba(199,91,119,0.22);
    margin: 0 auto 14px;
    touch-action: none;
  }
  /* Sub-sheets (opened on top of another sheet) get this instead of .grab -
     see the isSub check in the Sheet component. */
  .sheet-back{
    display: flex; align-items: center; gap: 2px;
    background: none; border: none;
    color: var(--rose-deep);
    font-size: 14.5px; font-weight: 800;
    padding: 2px 6px 16px 0;
    margin: -6px 0 0 -6px;
    cursor: pointer;
  }
  .sheet-back svg{ width: 20px; height: 20px; flex: none; }
  .sheet-back:active{ opacity: .6; }

  /* Unlock */
  .unlock{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:calc(env(safe-area-inset-top) + 30px) 30px calc(env(safe-area-inset-bottom) + 30px)}
  /* Brand mark (v1.15.1): the actual app icon (gradient rounded square +
     heart, same design as icon-v2.svg/apple-touch-icon-v2.png) instead of
     the generic 💞 emoji this screen used before - so the first thing you
     see when opening the app is the same mark as its Home Screen icon, not
     a mismatched system emoji. drop-shadow (not box-shadow) so the shadow
     follows the rounded-square shape itself, since box-shadow would just
     shadow the <svg>'s square bounding box regardless of the rx-rounded
     rect drawn inside it. */
  .unlock-mark{
    display: block;
    flex: none;
    filter: drop-shadow(0 14px 30px rgba(160,50,90,0.4));
  }
  .unlock h1{font-size:30px;color:var(--rose-deep);margin:10px 0 4px}
  .spin{width:34px;height:34px;border:3px solid var(--rose-soft);border-top-color:var(--rose-deep);border-radius:50%;animation:sp 1s linear infinite;margin:24px auto}
  @keyframes sp{to{transform:rotate(360deg)}}
  .empty{text-align:center;color:var(--ink-soft);padding:18px 6px;font-size:14px}
  .toast {
    position: fixed; bottom: 88px; left: 50%; transform: translateX(-50%);
    background: rgba(40,20,30,0.75);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    color: #fff; padding: 10px 18px; border-radius: 999px;
    font-size: 13.5px; font-weight: 700; z-index: 60;
    border: 1px solid rgba(255,255,255,0.15);
    box-shadow: 0 4px 20px rgba(0,0,0,0.2);
  }
  a{color:var(--rose-deep)}
  .loading-overlay {
    position: fixed; inset: 0;
    background: rgba(20,10,16,0.3);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    display: flex; align-items: center; justify-content: center; z-index: 100;
  }
  .loading-ring {
    width: 48px; height: 48px;
    border: 3px solid rgba(199,91,119,0.25);
    border-top-color: var(--rose-deep);
    border-radius: 50%;
    animation: sp 0.85s linear infinite;
    box-shadow: 0 0 20px var(--rose-glow);
  }
  .tab-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40vh;
  }

  /* Pull to refresh */
  .ptr-slot {
    display: flex; align-items: center; justify-content: center;
    overflow: hidden;
    height: 0;
  }
  .ptr-slot.snap { transition: height .22s cubic-bezier(.22,1,.36,1); }
  .ptr-ring {
    width: 30px; height: 30px;
    filter: drop-shadow(0 2px 8px var(--rose-glow));
  }
  .ptr-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); display: block; }

  /* Accordion */
    .acc{
    background: var(--card);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid var(--glass-border);
    border-radius: 22px;
    box-shadow: var(--glass-shadow);
    margin-bottom: 14px;
    overflow: hidden;
  }

  .acc summary{
    list-style: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    cursor: pointer;
    padding: 18px 18px;
    font-weight: 800;
    color: var(--rose-deep);
  }

  .acc summary::-webkit-details-marker{
    display:none;
  }

  .acc .acc-chev{
    width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: rgba(58,42,52,0.55);
    transition: transform .22s ease, color .18s ease, opacity .18s ease;
    opacity: .9;
  }

  .acc .acc-chev svg{
    width: 18px;
    height: 18px;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .acc[open] .acc-chev{
    transform: rotate(180deg);
    color: var(--rose-deep);
  }

  .acc-body{
    padding: 0 18px 16px;
  }

  /* Lightweight variant for accordions nested inside another .card (e.g. the
     Movies/TV rows under the origin filter) - no card chrome of its own, just
     a hairline divider, so the group reads as one unit instead of stacked boxes. */
  .acc.nested{
    background: none; border: none; box-shadow: none; border-radius: 0;
    margin-bottom: 0; border-top: 1px solid var(--line);
  }
  .acc.nested summary{ padding: 14px 0; }
  .acc.nested .acc-body{ padding: 0 0 14px; }

  /* Love button */
  .love-card {
    position: relative;
    overflow: hidden;
  }

  .love-launcher {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-top: 10px;
  }

  .love-launcher .btn.lg {
    padding: 20px 18px;
    font-size: 18px;
    border-radius: 20px;
  }
  /* Rapid alternating taps (e.g. two fingers tapping one-after-another to
     send a burst of love quickly) could otherwise be misread by the
     browser as a pan/scroll gesture starting on this button, since pan-x/
     pan-y are still allowed app-wide (see the global touch-action rule) -
     touch-action:none here means every touch on this specific button is
     treated purely as a tap, never a gesture. */
  .love-send-btn { touch-action: none; }

  .love-counts {
    display: flex;
    gap: 10px;
  }

  .love-count {
    flex: 1;
    min-width: 62px;
    text-align: center;
    padding: 8px 8px;
    border-radius: 16px;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
  }

  .love-count .num {
    font-size: 20px;
    font-weight: 800;
    line-height: 1;
    color: var(--rose-deep);
  }

  .love-count .lbl {
    display: block;
    margin-top: 4px;
    font-size: 9.5px;
    color: var(--ink-soft);
    font-weight: 800;
    letter-spacing: .02em;
    text-transform: uppercase;
    white-space: normal;
    line-height: 1.2;
  }

  .love-burst-layer {
    position: fixed;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 90;
  }

  .love-particle {
    position: absolute;
    left: 50%;
    top: 58%;
    font-size: 24px;
    line-height: 1;
    opacity: 0;
    animation: love-burst 1.9s ease-out forwards;
    filter: drop-shadow(0 8px 18px rgba(199,91,119,0.28));
  }

  @keyframes love-burst {
    0% {
      transform: translate(-50%, -50%) translate(0, 0) scale(0.4) rotate(0deg);
      opacity: 0;
    }
    10% {
      opacity: 1;
    }
    70% {
      opacity: 0.95;
    }
    100% {
      transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(var(--scale)) rotate(var(--rot));
      opacity: 0;
    }
  }
