/* ============================================================
   Jotara — a daily habit log
   Built on kd-design-system v1.0. Tokens mirror css/tokens.css
   (Night Studio); motion uses the house curve and the
   rise-and-unblur primitive.
   ============================================================ */

/* ---------- Instrument Sans, self-hosted ---------- */
@font-face{
  font-family:'Instrument Sans'; font-style:normal; font-weight:400 700;
  font-display:swap; src:url(../fonts/instrument-sans-0.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:'Instrument Sans'; font-style:normal; font-weight:400 700;
  font-display:swap; src:url(../fonts/instrument-sans-1.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;
}

/* ============================================================
   Theme

   Three axes, set independently on <html>:
     data-theme   dark | light      the surface palette
     data-accent  blue | …          the one colour that carries meaning
     (no attribute)                 defaults to dark / blue

   Every colour below resolves through a token, and the two that get
   mixed with alpha carry an -rgb triplet alongside, so a theme only
   ever has to restate the triplet — never hunt down an rgba() call.
   ============================================================ */
:root{
  --kd-ink:#0a0b0e;
  --kd-paper-white:#f4f4f6;
  --kd-error:#c62f2f;
  --kd-warm:#ff7a59;

  /* dark is the default, so it lives on the bare :root — a page with
     no data-theme still paints correctly before any script runs */
  /* Dusk, not night. The reference sits mid-tone — a dusty plum you
     could still call dark — and that is what lets glass read as glass:
     a panel over true black has nothing to be translucent against. */
  --kd-bg:#241a22;
  --kd-glass:62%;
  --kd-text:#f6eef0;
  --kd-text-rgb:246 238 240;
  --kd-muted:#b9a3aa;
  --kd-line:rgb(var(--kd-text-rgb) / 0.16);
  --kd-surface:#31232c;
  --kd-surface-2:#3d2c36;
  --kd-instrument:var(--kd-paper-white);
  --kd-on-accent:#fff;
  --kd-sky:#3a2733;   /* the mesh supplies the colour; this is only its floor */
  --kd-shadow:none;
  /* The unlit state of every instrument — gauge segments, the dial
     track, empty tally bars. On glass a whisper of the text colour is
     enough; on paper the same value disappears, so light restates it. */
  --kd-track:rgb(var(--kd-text-rgb) / 0.12);
  /* Accent bloom. Glow is a dark-room effect; on paper it reads as
     smudge, so light turns it almost all the way down. */
  --kd-glow:0.7;
  /* Strength of the three ambient washes. On glass they carry the
     whole mood; on paper the same values read as a stain, so light
     restates them far lower rather than turning the art off. */
  --kd-wash-1:0.30;
  --kd-wash-2:0.55;
  --kd-wash-3:0.10;
  --kd-grain:0.05;

  --kd-font:'Instrument Sans','Helvetica Neue',Arial,sans-serif;
  --kd-fs-label:0.72rem;
  --kd-fs-tag:0.62rem;
  --kd-track-display:-0.02em;
  --kd-track-label:0.14em;

  --kd-radius:18px;
  --kd-radius-sm:14px;
  --kd-radius-xs:10px;
  --kd-pill:100px;

  --kd-ease:cubic-bezier(0.22,0.7,0.2,1);
  --kd-t-fast:0.25s;
  --kd-t-move:0.35s;
  --kd-t-reveal:0.9s;

  --kd-pad:clamp(1.25rem,5vw,2.5rem);
}

/* ---------- light ----------
   Not the dark palette inverted. Paper needs less contrast than glass
   to read as calm, so the surfaces sit close to the background and the
   line does the separating. Muted darkens well past a simple flip so
   it clears 4.5:1 on the card surface. */
:root[data-theme="light"]{
  /* The page is a shade deeper than the cards, not the same white, so
     a card is an object sitting on a surface rather than an outline
     drawn on the void. That separation is what light mode was missing. */
  --kd-bg:#f4e9e8;   /* blush paper */
  --kd-glass:72%;
  --kd-text:#2a1d24;
  --kd-text-rgb:42 29 36;
  --kd-muted:#7c6069;
  --kd-line:rgb(var(--kd-text-rgb) / 0.14);
  --kd-surface:#fffafa;
  /* Well clear of the background — this is the empty state of every
     bar and gauge, and at 2% away it read as nothing at all. */
  --kd-surface-2:#e6d6d6;
  --kd-track:rgb(var(--kd-text-rgb) / 0.16);
  --kd-glow:0.18;
  --kd-instrument:#4a3540;
  --kd-sky:#fbeef0;
  --kd-shadow:0 1px 2px rgb(var(--kd-text-rgb) / 0.06),0 10px 28px rgb(var(--kd-text-rgb) / 0.08);
  --kd-wash-1:0.07;
  --kd-wash-2:0.05;
  --kd-wash-3:0.04;
  --kd-grain:0.025;
}

/* ---------- accents ----------
   Softer and warmer than the originals, which were pure screen hues
   and fought the dusty ground. Coral leads because it is the one
   colour the reference uses for anything that matters. Each has a
   deep partner that feeds the mesh rather than any UI surface. */
:root{
  --kd-accent:#f4785c;
  --kd-accent-rgb:244 120 92;
  --kd-accent-deep:#7a3244;
  --kd-accent-deep-rgb:122 50 68;
}
:root[data-accent="rose"]{
  --kd-accent:#e8698c;   --kd-accent-rgb:232 105 140;
  --kd-accent-deep:#6d2646; --kd-accent-deep-rgb:109 38 70;
}
:root[data-accent="violet"]{
  --kd-accent:#a274d4;   --kd-accent-rgb:162 116 212;
  --kd-accent-deep:#432a63; --kd-accent-deep-rgb:67 42 99;
}
:root[data-accent="amber"]{
  --kd-accent:#e8a34e;   --kd-accent-rgb:232 163 78;
  --kd-accent-deep:#6b3d1f; --kd-accent-deep-rgb:107 61 31;
}
:root[data-accent="green"]{
  --kd-accent:#5fae8d;   --kd-accent-rgb:95 174 141;
  --kd-accent-deep:#254a3e; --kd-accent-deep-rgb:37 74 62;
}
:root[data-accent="teal"]{
  --kd-accent:#5aa5b8;   --kd-accent-rgb:90 165 184;
  --kd-accent-deep:#23484f; --kd-accent-deep-rgb:35 72 79;
}

/* A colour change should feel like a change of light, not a repaint. */
:root[data-theme-ready] body,
:root[data-theme-ready] .card,
:root[data-theme-ready] .ambient{
  transition:background-color var(--kd-t-reveal) var(--kd-ease),
             color var(--kd-t-reveal) var(--kd-ease);
}

/* ============================================================
   Glass

   One recipe, applied to every surface that floats over the mesh.

   The thing that actually sells glass is not the blur — it's the edge.
   A real pane catches light along its top lip and drops a faint shadow
   along its bottom, so each panel gets an inset highlight above and an
   inset darkening below. Without those two lines a blurred panel just
   looks like fog; with them it looks like a sheet of something.

   Saturation is boosted on the backdrop because blurring desaturates,
   and a washed-out mesh behind glass is the whole effect lost.
   ============================================================ */
:root{
  --glass-bg:rgb(255 255 255 / 0.07);
  --glass-edge:rgb(255 255 255 / 0.22);
  --glass-under:rgb(0 0 0 / 0.22);
  --glass-hair:rgb(255 255 255 / 0.10);
  --glass-lift:0 12px 36px rgb(0 0 0 / 0.30);
  --glass-blur:30px;
}
:root[data-theme="light"]{
  --glass-bg:rgb(255 255 255 / 0.46);
  --glass-edge:rgb(255 255 255 / 0.85);
  --glass-under:rgb(90 60 50 / 0.07);
  --glass-hair:rgb(255 255 255 / 0.55);
  --glass-lift:0 12px 34px rgb(90 60 50 / 0.10);
}

.glass{
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  border:1px solid var(--glass-hair);
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    var(--glass-lift);
}

*,*::before,*::after{box-sizing:border-box;}

html{background:var(--kd-bg);height:100%;}
body{
  margin:0;
  min-height:100%;
  background:var(--kd-bg);
  color:var(--kd-text);
  font-family:var(--kd-font);
  font-size:1.0625rem;
  line-height:1.65;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  -webkit-tap-highlight-color:transparent;
  overscroll-behavior-y:none;
}
.shell{
  position:relative;
  z-index:1;
  max-width:520px;
  margin:0 auto;
  padding:
    calc(1.5rem + env(safe-area-inset-top))
    calc(var(--kd-pad) + env(safe-area-inset-right))
    calc(3rem + env(safe-area-inset-bottom))
    calc(var(--kd-pad) + env(safe-area-inset-left));
}
::selection{background:var(--kd-accent);color:var(--kd-on-accent);}

/* ============================================================
   The mesh

   Five heavily-blurred blobs, each on its own long drift, over a base
   wash. Two of the five are always warm — peach and rose — regardless
   of the chosen accent, which is what keeps every theme in the same
   family instead of turning the green accent into a cold screen. The
   accent supplies the other two, so your choice still leads.

   Blur is the whole effect: at 90px nothing reads as a circle, it
   reads as light. Everything animates on transform only, so it runs
   on the compositor and never triggers layout.
   ============================================================ */
:root{
  /* Deep, saturated members of the plum family. The first pass used
     pale warm tones — dusty rose, mauve, warm sand — and under `screen`
     those lift every channel by roughly the same amount, which is the
     definition of washing out: measured against the #241A22 ground they
     pushed luminance from 29 to 84 while dropping saturation from 0.28
     to 0.22. These are darker and far more saturated, so screen lifts
     the reds and magentas and leaves the ground reading as plum. */
  --mesh-warm-1:138 46 74;     /* wine     */
  --mesh-warm-2:104 44 96;     /* deep plum */
  --mesh-warm-3:156 66 44;     /* burnt amber */
  --mesh-alpha:0.42;
  --mesh-blur:76px;
  /* Screen on a dark ground makes the blobs behave like coloured
     light rather than paint — overlaps get brighter instead of
     muddier, which is exactly what a five-colour mesh needs. */
  --mesh-blend:screen;
}
:root[data-theme="light"]{
  /* On paper the same alphas turn to poster paint — light needs about
     half the strength and a slightly wider blur to stay atmospheric. */
  /* On paper the same blobs must behave like pigment, not light —
     multiply keeps the overlaps rich instead of blowing them out. And
     the pale set stays here: under multiply the deep plums that dark
     needs would go straight to mud. */
  --mesh-warm-1:214 150 148;   /* dusty rose */
  --mesh-warm-2:176 138 168;   /* mauve      */
  --mesh-warm-3:236 170 140;   /* warm sand  */
  --mesh-alpha:0.62;
  --mesh-blur:88px;
  --mesh-blend:multiply;
}

.ambient{
  position:fixed;
  inset:0;
  z-index:0;
  pointer-events:none;
  overflow:hidden;
  background:linear-gradient(175deg,var(--kd-sky) 0%,var(--kd-bg) 58%);
  transition:opacity 1.2s var(--kd-ease);
}
/* The blur is back. I had swapped it for multi-stop gradients on a
   performance hunch, and the hunch cost the look — the gradients read
   as flat bands where the blur reads as light. The real device report
   was about the gauge bloom, not this, so the trade was never needed.

   will-change stays off: five permanently promoted full-screen layers
   is real memory for no visual gain, and transforms composite anyway. */
.blob{
  position:absolute;
  border-radius:50%;
  filter:blur(var(--mesh-blur));
  opacity:var(--mesh-alpha);
  mix-blend-mode:var(--mesh-blend);
  transform:translate3d(0,0,0);
}
@supports not (mix-blend-mode: screen){
  .blob{ mix-blend-mode:normal; opacity:calc(var(--mesh-alpha) * 0.8); }
}
.blob-1{
  width:78vw; height:78vw;
  top:-24vw; left:-18vw;
  background:radial-gradient(circle at 50% 50%, rgb(var(--kd-accent-rgb)) 0%, transparent 68%);
  animation:drift-1 34s var(--kd-ease) infinite alternate;
}
.blob-2{
  width:66vw; height:66vw;
  top:-8vw; right:-20vw;
  background:radial-gradient(circle at 50% 50%, rgb(var(--mesh-warm-1)) 0%, transparent 68%);
  animation:drift-2 41s var(--kd-ease) infinite alternate;
}
.blob-3{
  width:84vw; height:84vw;
  top:32vh; left:-30vw;
  background:radial-gradient(circle at 50% 50%, rgb(var(--mesh-warm-2)) 0%, transparent 66%);
  animation:drift-3 47s var(--kd-ease) infinite alternate;
}
.blob-4{
  width:70vw; height:70vw;
  bottom:-22vw; right:-14vw;
  background:radial-gradient(circle at 50% 50%, rgb(var(--mesh-warm-3)) 0%, transparent 68%);
  animation:drift-4 38s var(--kd-ease) infinite alternate;
}
.blob-5{
  width:56vw; height:56vw;
  bottom:6vh; left:8vw;
  background:radial-gradient(circle at 50% 50%, rgb(var(--kd-accent-deep-rgb)) 0%, transparent 70%);
  opacity:calc(var(--mesh-alpha) * 0.7);
  animation:drift-5 53s var(--kd-ease) infinite alternate;
}

/* Long, prime-ish durations so the five never resynchronise into an
   obvious loop. */
@keyframes drift-1{ to{ transform:translate3d(14vw,10vh,0) scale(1.18); } }
@keyframes drift-2{ to{ transform:translate3d(-16vw,14vh,0) scale(1.12); } }
@keyframes drift-3{ to{ transform:translate3d(18vw,-12vh,0) scale(0.88); } }
@keyframes drift-4{ to{ transform:translate3d(-12vw,-16vh,0) scale(1.22); } }
@keyframes drift-5{ to{ transform:translate3d(10vw,-8vh,0) scale(1.3); } }

/* Grain over the whole mesh — it stops the blur banding on wide gamut
   screens, which is exactly where a five-colour mesh shows seams. */
.ambient::after{
  content:'';
  position:absolute;
  inset:0;
  opacity:var(--kd-grain);
  mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* Closing the day warms and lifts the whole mesh. */
body.is-complete .ambient{opacity:1.25;}
body.is-complete .blob{animation-duration:22s;}

/* ---------- type ---------- */
.label{
  font-size:var(--kd-fs-label);
  font-weight:500;
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-muted);
  line-height:1.4;
}
.num{font-variant-numeric:tabular-nums;letter-spacing:var(--kd-track-display);}

/* ---------- masthead ---------- */
.masthead{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:0.75rem;
  margin-bottom:2.5rem;
}
.brand{display:flex;align-items:center;gap:0.7rem;min-width:0;}
.brand svg{height:30px;width:auto;display:block;flex:none;color:var(--kd-accent);}
/* The wordmark stays on the loader, where there is room for it, and
   comes off the app itself — the mark alone is enough once you are in,
   and the masthead was competing with the dial for attention. */
.visually-hidden{
  position:absolute;
  width:1px;height:1px;
  margin:-1px;padding:0;border:0;
  clip-path:inset(50%);
  overflow:hidden;
  white-space:nowrap;
}
.brand .label{color:var(--kd-text);}
.masthead-right{display:flex;align-items:center;gap:0.6rem;}
.icon-btn{
  width:34px;height:34px;flex:none;
  display:grid;place-items:center;
  border-radius:50%;
  border:1px solid var(--glass-hair);
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  box-shadow:inset 0 1px 0 var(--glass-edge),inset 0 -1px 0 var(--glass-under);
  color:var(--kd-text);
  cursor:pointer;
  padding:0;
  transition:color var(--kd-t-fast) var(--kd-ease),border-color var(--kd-t-fast) var(--kd-ease),transform var(--kd-t-move) var(--kd-ease);
}
.icon-btn:active{transform:scale(0.94);}
.icon-btn svg{width:15px;height:15px;display:block;}

/* ---------- hero dial ---------- */
.hero{
  position:relative;
  display:grid;
  place-items:center;
  margin-bottom:2.25rem;
}
.dial{
  position:relative;
  width:min(74vw,300px);
  aspect-ratio:1;
  display:grid;
  place-items:center;
}
/* Direct child only — this rotates the ring so it starts at twelve
   o'clock, and must not reach the mark's SVG inside .dial-seal. */
.dial > svg{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  transform:rotate(-90deg);
  overflow:visible;
}
.dial-track{fill:none;stroke:var(--kd-track);stroke-width:2;}
.dial-prog{
  fill:none;
  stroke:var(--kd-accent);
  stroke-width:5;
  stroke-linecap:round;
  transition:stroke-dashoffset 0.75s var(--kd-ease);
}
.dial-target{
  stroke:var(--kd-muted);
  stroke-width:2;
  stroke-linecap:round;
  opacity:0.75;
  transition:stroke var(--kd-t-fast) var(--kd-ease);
}
body.is-complete .dial-target{stroke:var(--kd-accent);}
.dial-glow{
  fill:none;
  stroke:var(--kd-accent);
  stroke-width:9;
  stroke-linecap:round;
  /* Widened stroke instead of a CSS blur: same halo, and it survives
     WKWebView, which quietly ignores filters on SVG children. */
  stroke-width:16;
  opacity:calc(var(--kd-glow) * 0.30);
  transition:stroke-dashoffset 0.75s var(--kd-ease),opacity var(--kd-t-move) var(--kd-ease);
}
.dial-face{
  position:relative;
  text-align:center;
  display:grid;
  gap:0.1rem;
  padding:0 1rem;
  transition:opacity 0.5s var(--kd-ease),transform 0.6s var(--kd-ease),filter 0.5s var(--kd-ease);
}
.dial-seal{
  position:absolute;
  width:74%;
  aspect-ratio:1;
  display:grid;
  place-items:center;
  opacity:0;
  transform:scale(0.86);
  pointer-events:none;
  transition:opacity 0.5s var(--kd-ease),transform 0.6s var(--kd-ease);
}
.dial-seal svg{width:100%;height:100%;display:block;overflow:visible;}
/* A soft disc of accent behind the stroke, so the check sits on light
   rather than on the mesh. */
/* Blurred, so it reads as light behind the stroke rather than a disc
   drawn on top of the mesh — a hard circle edge here looked like a
   mistake. */
.seal-bloom{
  fill:rgb(var(--kd-accent-rgb) / 0.20);
  filter:blur(14px);
  transform-origin:50% 50%;
  transform:scale(0.55);
  transition:transform 0.7s var(--kd-ease);
}
.seal-check{
  fill:none;
  stroke:var(--kd-accent);
  stroke-width:7;
  stroke-linecap:round;
  stroke-linejoin:round;
  filter:drop-shadow(0 0 10px rgb(var(--kd-accent-rgb) / calc(var(--kd-glow) * 0.8)));
}
/* Drawn, not faded in — the length is the path's own, measured once by
   hand so no JS is needed to set it. */
.seal-check.draw{
  stroke-dasharray:78;
  stroke-dashoffset:78;
  animation:seal-draw 0.62s var(--kd-ease) 0.12s forwards;
}
@keyframes seal-draw{ to{ stroke-dashoffset:0; } }
.dial.show-seal .dial-face{opacity:0;transform:scale(0.92);filter:blur(4px);}
.dial.show-seal .dial-seal{opacity:1;transform:scale(1);}
.dial.show-seal .seal-bloom{transform:scale(1);}
body.is-complete .dial{cursor:pointer;}
.dial-num{font-size:clamp(4.25rem,19vw,5.75rem);font-weight:500;line-height:0.92;}
.dial-unit{font-size:1.05rem;font-weight:500;letter-spacing:-0.01em;}
.dial-unit em{font-style:normal;color:var(--kd-accent);}
.dial-date{
  font-size:var(--kd-fs-tag);
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-muted);
  margin-top:0.45rem;
}
.dial-status{
  margin-top:0.5rem;
  font-size:var(--kd-fs-label);
  font-weight:500;
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-muted);
  transition:color var(--kd-t-fast) ease;
}
body.is-complete .dial-status{color:var(--kd-accent);}
body.is-complete .dial-glow{opacity:calc(var(--kd-glow) * 1.21);}

/* ---------- stats ---------- */
.stats{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  border-top:1px solid var(--kd-line);
  border-bottom:1px solid var(--kd-line);
  margin-bottom:2rem;
}
.stat{text-align:center;padding:1.15rem 0.5rem;position:relative;}
.stat + .stat::before{
  content:'';
  position:absolute;
  left:0;top:22%;bottom:22%;
  width:1px;
  background:var(--kd-line);
}
.stat-num{font-size:1.75rem;font-weight:500;line-height:1.1;}
.stat-num small{font-size:0.9rem;font-weight:500;color:var(--kd-muted);margin-left:0.05em;}
.stat-cap{
  font-size:var(--kd-fs-tag);
  letter-spacing:0.1em;
  text-transform:uppercase;
  color:var(--kd-muted);
  margin-top:0.25rem;
}

/* ---------- ledger ---------- */
.ledger{margin-bottom:2rem;}
.ledger-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
  margin-bottom:0.85rem;
}
.seg{display:flex;gap:0.3rem;}
.seg button{
  font-family:inherit;
  font-size:var(--kd-fs-tag);
  font-weight:500;
  letter-spacing:0.1em;
  text-transform:uppercase;
  color:var(--kd-muted);
  background:transparent;
  border:1px solid transparent;
  border-radius:var(--kd-pill);
  padding:0.3rem 0.7rem;
  cursor:pointer;
  transition:color var(--kd-t-fast) var(--kd-ease),border-color var(--kd-t-fast) var(--kd-ease);
}
.seg button.is-active{
  color:var(--kd-text);
  border-color:var(--glass-hair);
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  box-shadow:inset 0 1px 0 var(--glass-edge);
}

/* ---------- week ----------
   Seven named columns. No scrolling, no axis to read — the shape of a
   week is already in your head, so the view just has to fill it in. */
.week-row{
  display:grid;
  grid-template-columns:repeat(7,1fr);
  gap:6px;
  align-items:end;
}
.week-cell{display:grid;justify-items:center;gap:0.4rem;grid-template-rows:auto 44px auto;}
.week-name{
  font-size:var(--kd-fs-tag);
  font-weight:600;
  letter-spacing:0.06em;
  color:var(--kd-muted);
}
.week-date{font-size:var(--kd-fs-tag);color:var(--kd-muted);font-variant-numeric:tabular-nums;}
.week-row .tally{width:100%;align-self:end;border-radius:5px;}

/* ---------- month ----------
   A calendar, because a month has dates and people look for them. */
.month-head{
  display:grid;
  grid-template-columns:repeat(7,1fr);
  gap:5px;
  margin-bottom:0.5rem;
}
.month-head span{
  text-align:center;
  font-size:var(--kd-fs-tag);
  font-weight:600;
  color:var(--kd-muted);
}
.month-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:5px;}
.mcell{
  aspect-ratio:1;
  height:auto !important;
  border-radius:8px;
  display:grid;
  place-items:center;
  font-size:0.7rem;
  font-variant-numeric:tabular-nums;
  color:var(--kd-muted);
  background:var(--kd-surface-2);
}
.mcell.void{background:transparent;}
.mcell.full{background:var(--kd-accent);color:var(--kd-on-accent);}
.mcell.partial{background:rgb(var(--kd-accent-rgb) / 0.38);color:var(--kd-text);}
.mcell.ahead{background:transparent;border:1px solid var(--kd-line);opacity:0.6;}
.mcell.today{box-shadow:0 0 0 2px var(--kd-bg),0 0 0 3.5px var(--kd-accent);}
.mcell.grace{background:transparent;border:1.5px dashed rgb(var(--kd-accent-rgb) / 0.65);}
/* Which way time runs. Cheap to add, and it stops the flipped order
   from being a puzzle. */
.tally-axis{
  display:flex;
  justify-content:space-between;
  margin-top:0.55rem;
  font-size:var(--kd-fs-tag);
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-muted);
}
.tally{
  align-self:end;
  width:100%;
  height:38%;
  border-radius:3px;
  background:var(--kd-surface-2);
  transition:height 0.5s var(--kd-ease),background var(--kd-t-fast) var(--kd-ease);
}
.tally.partial{background:rgb(var(--kd-accent-rgb) / 0.38);}
.tally.full{background:var(--kd-accent);}
.tally.today{box-shadow:0 0 0 1.5px var(--kd-bg),0 0 0 3px var(--kd-accent);}

/* year grid — weeks as columns, scrolled to today */
.year-wrap{overflow-x:auto;overscroll-behavior-x:contain;padding-bottom:0.4rem;}
.year-wrap::-webkit-scrollbar{height:3px;}
.year-wrap::-webkit-scrollbar-thumb{background:var(--kd-line);border-radius:99px;}
.year{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,10px);gap:3px;width:max-content;}
.ycell{
  width:10px;height:10px;
  border-radius:2px;
  background:var(--kd-surface-2);
}
.ycell.l1{background:rgb(var(--kd-accent-rgb) / 0.25);}
.ycell.l2{background:rgb(var(--kd-accent-rgb) / 0.5);}
.ycell.l3{background:rgb(var(--kd-accent-rgb) / 0.75);}
.ycell.l4{background:var(--kd-accent);}
.ycell.today{box-shadow:0 0 0 1.5px var(--kd-bg),0 0 0 3px var(--kd-accent);}
.ycell.void{background:transparent;}
.year-months{
  display:flex;
  gap:0;
  margin-top:0.45rem;
  font-size:var(--kd-fs-tag);
  letter-spacing:0.1em;
  text-transform:uppercase;
  color:var(--kd-muted);
  width:max-content;
}

/* ---------- cards ---------- */
.card{
  /* Glass, not a solid panel — the light has to move underneath it. */
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  border:1px solid var(--glass-hair);
  border-radius:var(--kd-radius);
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    var(--glass-lift);
  padding:1.5rem 1.5rem 0.65rem;
  margin-bottom:1rem;
  overflow:hidden;
}
.card.pad-b{padding-bottom:1.35rem;}
.card-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
  padding-bottom:1rem;
  border-bottom:1px solid var(--kd-line);
  margin-bottom:0.15rem;
}
.card-headings{min-width:0;}
.card-title{font-size:1.1rem;font-weight:600;letter-spacing:-0.01em;line-height:1.3;}
.card-count{
  font-size:0.95rem;
  font-weight:500;
  letter-spacing:0.01em;
  color:var(--kd-muted);
  font-variant-numeric:tabular-nums;
  white-space:nowrap;
  margin-top:0.15rem;
}
.card-count b{color:var(--kd-text);font-weight:500;}

/* ============================================================
   Section instruments — four mechanisms, one family.
   ============================================================ */
.gauge{width:88px;height:88px;flex:none;}
.g{width:100%;height:100%;display:block;overflow:visible;}

/* The unlit track has to read on its own — an instrument you can only
   see once it's lit looks broken rather than restrained. */
.g .seg{
  fill:none;
  stroke:var(--kd-track);
  stroke-width:5;
  stroke-linecap:round;
  transition:stroke var(--kd-t-fast) var(--kd-ease),opacity var(--kd-t-fast) var(--kd-ease);
}
.g .seg.on{
  stroke:var(--kd-accent);
}
/* Animates opacity and glow rather than stroke-width, so the variants
   keep their own weights (rings 2.4, clock 8) while it plays. */
.g .seg.pop{animation:seg-pop 0.5s var(--kd-ease);}
@keyframes seg-pop{
  0%{opacity:0.3;filter:none;}
  35%{opacity:1;}
  100%{opacity:1;}
}

/* needle: the part that reads as a real instrument */
.g .needle{
  transform-origin:34px 34px;
  transform:rotate(-130deg);
  transition:transform 0.75s var(--kd-ease);
}
.g .needle line{
  stroke:var(--kd-instrument);
  stroke-width:2.6;
  stroke-linecap:round;
}
.g .hub{fill:var(--kd-instrument);}
.g-heat .needle line{
  stroke:var(--needle,var(--kd-warm));
  filter:drop-shadow(0 0 5px currentColor);
  transition:stroke var(--kd-t-move) var(--kd-ease);
}
.g-heat .hub{fill:var(--needle,var(--kd-warm));transition:fill var(--kd-t-move) var(--kd-ease);}
.g-heat .heat-track{fill:none;stroke-width:6;stroke-linecap:round;opacity:0.65;}
.g-heat .tick{
  stroke:rgb(var(--kd-text-rgb) / 0.18);
  stroke-width:2;
  stroke-linecap:round;
  transition:stroke var(--kd-t-fast) var(--kd-ease);
}
.g-heat .tick.on{stroke:var(--kd-text);}

.g-ladder .seg{stroke-width:3;}
.g-rings .seg{stroke-width:3.4;}
.g-rings .core{fill:rgb(var(--kd-text-rgb) / 0.12);transition:fill var(--kd-t-fast) var(--kd-ease);}
.g-clock .seg{stroke-width:11;stroke-linecap:butt;}

/* completing a section locks the instrument — it should be obvious
   from across the room which sections are shut */
.g.full .seg.on{stroke-width:6;}
.g-rings.full .core{fill:var(--kd-accent);}
.g.full .needle line{stroke:var(--kd-accent);}
.g.full .hub{fill:var(--kd-accent);}
.g-heat.full .heat-track{opacity:0.9;}
.g.full{animation:g-lock 0.62s var(--kd-ease);}
@keyframes g-lock{
  0%{transform:scale(1);}
  38%{transform:scale(1.07);}
  100%{transform:scale(1);}
}

/* ---------- checklist rows ---------- */
.item{
  display:flex;
  align-items:center;
  gap:0.9rem;
  padding:0.85rem 0.1rem;
  border-top:1px solid var(--kd-line);
  cursor:pointer;
  user-select:none;
  -webkit-user-select:none;
  touch-action:manipulation;
  transition:opacity var(--kd-t-fast) ease;
}
.item:first-of-type{border-top:none;}
.item:active{opacity:0.65;}
.box{
  position:relative;
  width:24px;height:24px;flex:none;
  border:1.5px solid var(--kd-muted);
  border-radius:8px;
  display:grid;place-items:center;
  transition:
    background var(--kd-t-fast) var(--kd-ease),
    border-color var(--kd-t-fast) var(--kd-ease),
    transform var(--kd-t-move) var(--kd-ease);
}
.box svg{width:13px;height:13px;display:block;}
.box path{
  stroke:var(--kd-instrument);
  stroke-width:2.2;
  stroke-linecap:round;
  stroke-linejoin:round;
  fill:none;
  stroke-dasharray:16;
  stroke-dashoffset:16;
  transition:stroke-dashoffset 0.32s var(--kd-ease);
}
.item.done .box,.train.done .box{background:var(--kd-accent);border-color:var(--kd-accent);}
.item.done .box path,.train.done .box path{stroke-dashoffset:0;}
.item.just-done .box,.train.just-done .box{animation:kd-press 0.4s var(--kd-ease);}
@keyframes kd-press{
  0%{transform:scale(1);}
  38%{transform:scale(0.86);}
  100%{transform:scale(1);}
}
.item-label{font-size:0.97rem;line-height:1.4;transition:color var(--kd-t-fast) ease;}
.item.done .item-label{color:var(--kd-muted);}

/* ---------- weekly training ---------- */
.train{
  display:flex;
  align-items:center;
  gap:0.9rem;
  padding:0.8rem 0.1rem;
  border-top:1px solid var(--kd-line);
  cursor:pointer;
  user-select:none;
  -webkit-user-select:none;
  touch-action:manipulation;
  transition:opacity var(--kd-t-fast) ease;
}
.train:first-of-type{border-top:none;}
.train:active{opacity:0.65;}
.train-body{flex:1;min-width:0;}
.train-label{font-size:0.97rem;line-height:1.35;}
.train-sub{font-size:var(--kd-fs-tag);color:var(--kd-muted);letter-spacing:0.04em;margin-top:0.05rem;}
.train.hit .train-sub{color:var(--kd-accent);}
.pips{display:flex;gap:4px;flex:none;}
.pip{
  width:7px;height:7px;
  border-radius:50%;
  background:var(--kd-surface-2);
  border:1px solid var(--kd-line);
  transition:background var(--kd-t-fast) var(--kd-ease),border-color var(--kd-t-fast) var(--kd-ease);
}
.pip.on{background:var(--kd-accent);border-color:var(--kd-accent);}

/* ---------- note ---------- */
.note-field{
  width:100%;
  min-height:3.2rem;
  resize:none;
  font-family:inherit;
  font-size:0.97rem;
  line-height:1.5;
  color:var(--kd-text);
  background:transparent;
  border:none;
  outline:none;
  padding:0.85rem 0 0.2rem;
}
.note-field::placeholder{color:var(--kd-muted);}

/* ---------- reminders ---------- */
.remind{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
  padding:1.1rem 1.35rem;
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  border:1px solid var(--glass-hair);
  border-radius:var(--kd-radius);
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    var(--glass-lift);
  margin-bottom:1rem;
}
.remind-copy{min-width:0;}
.remind-title{font-size:0.97rem;font-weight:600;letter-spacing:-0.01em;}
.remind-status{font-size:var(--kd-fs-tag);color:var(--kd-muted);letter-spacing:0.04em;margin-top:0.1rem;}

.btn{
  flex:none;
  padding:0.62rem 1.15rem;
  border-radius:var(--kd-pill);
  border:1px solid var(--glass-hair);
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  box-shadow:inset 0 1px 0 var(--glass-edge),inset 0 -1px 0 var(--glass-under);
  color:var(--kd-text);
  font-family:inherit;
  font-size:0.78rem;
  font-weight:600;
  cursor:pointer;
  touch-action:manipulation;
  transition:border-color var(--kd-t-fast) var(--kd-ease),color var(--kd-t-fast) var(--kd-ease),opacity var(--kd-t-fast) ease;
}
.btn:active{opacity:0.7;}
.btn.is-on{border-color:var(--kd-accent);color:var(--kd-accent);}
.btn.primary{background:var(--kd-accent);border-color:var(--kd-accent);color:var(--kd-on-accent);}
.btn.danger{color:var(--kd-error);border-color:rgba(198,47,47,0.4);}
.btn:disabled{opacity:0.35;pointer-events:none;}
.btn.block{width:100%;display:block;padding:0.85rem 1.2rem;}
.btn.ghost{border-color:transparent;color:var(--kd-muted);}

/* ============================================================
   Onboarding

   Full-screen, above everything, and escapable at every step. It sits
   on the plain background rather than a card so the first thing the
   app does isn't shouting.
   ============================================================ */
.onboard{
  position:fixed;
  inset:0;
  z-index:60;
  background:var(--kd-bg);
  overflow-y:auto;
  overscroll-behavior:contain;
  animation:sheet-in 0.5s var(--kd-ease) both;
}
.onboard[hidden]{display:none;}

/* ---------- the ground ----------
   Onboarding carries the same mesh as the app rather than photography.
   It is painted as static radials instead of the drifting blobs: this
   sits above the app's own mesh and must be opaque enough to hide it,
   and a second set of animating layers over the first is more motion
   than a setup screen needs. */
.onboard::before{
  content:'';
  position:fixed;
  inset:0;
  pointer-events:none;
  background:
    radial-gradient(ellipse 70% 55% at 12% 8%,  rgb(var(--kd-accent-rgb) / 0.30), transparent 66%),
    radial-gradient(ellipse 65% 50% at 88% 18%, rgb(var(--mesh-warm-1) / 0.34), transparent 68%),
    radial-gradient(ellipse 85% 60% at 78% 92%, rgb(var(--mesh-warm-2) / 0.32), transparent 70%),
    radial-gradient(ellipse 70% 55% at 8% 88%,  rgb(var(--mesh-warm-3) / 0.26), transparent 68%),
    linear-gradient(170deg, var(--kd-sky) 0%, var(--kd-bg) 62%);
}

.ob-inner{
  position:relative;   /* above the two art layers */
  z-index:1;
  max-width:520px;
  margin:0 auto;
  min-height:100%;
  display:flex;
  flex-direction:column;
  /* No bottom padding — the sticky actions bar owns the bottom edge and
     carries the safe-area inset itself. Padding here would leave a strip
     of bare artwork under the bar. */
  padding:
    calc(2rem + env(safe-area-inset-top))
    calc(var(--kd-pad) + env(safe-area-inset-right))
    0
    calc(var(--kd-pad) + env(safe-area-inset-left));
}
.ob-progress{display:flex;gap:0.35rem;margin-bottom:2.5rem;}
.ob-progress i{
  height:3px;
  flex:1;
  border-radius:3px;
  background:var(--kd-line);
  transition:background var(--kd-t-move) var(--kd-ease);
}
.ob-progress i.on{background:var(--kd-accent);}
.ob-step{flex:1;}
.ob-kicker{
  font-size:var(--kd-fs-tag);
  font-weight:600;
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-accent);
  margin-bottom:0.7rem;
}
.ob-title{
  font-size:clamp(1.6rem,7vw,2.1rem);
  font-weight:600;
  letter-spacing:var(--kd-track-display);
  line-height:1.15;
  margin:0 0 0.7rem;
}
.ob-sub{color:var(--kd-muted);font-size:0.94rem;line-height:1.6;margin:0 0 2rem;}
.ob-sub b{color:var(--kd-text);font-variant-numeric:tabular-nums;}
.ob-group{margin-bottom:1.9rem;}
.ob-group > .label{margin-bottom:0.7rem;}
.ob-group .input{margin-bottom:0.6rem;}

.ob-choices{display:grid;gap:0.6rem;}
/* Frosted rather than solid — a pane of the art showing through, which
   is what stops the controls reading as a form pasted onto a photo. */
.ob-choice{
  width:100%;
  text-align:left;
  padding:1.05rem 1.2rem;
  border-radius:var(--kd-radius-sm);
  border:1px solid var(--kd-line);
  background:color-mix(in srgb, var(--kd-surface) 62%, transparent);
  -webkit-backdrop-filter:blur(18px) saturate(1.4);
  backdrop-filter:blur(18px) saturate(1.4);
  color:var(--kd-text);
  font-family:inherit;
  font-size:1rem;
  font-weight:500;
  cursor:pointer;
  transition:border-color var(--kd-t-fast) var(--kd-ease),background var(--kd-t-fast) var(--kd-ease);
}
.ob-choice.is-active{
  border-color:var(--kd-accent);
  background:rgb(var(--kd-accent-rgb) / 0.08);
}

.ob-pills{display:flex;flex-wrap:wrap;gap:0.45rem;}
.ob-pill{
  padding:0.55rem 0.9rem;
  border-radius:var(--kd-pill);
  border:1px solid var(--kd-line);
  background:color-mix(in srgb, var(--kd-surface) 45%, transparent);
  -webkit-backdrop-filter:blur(14px) saturate(1.3);
  backdrop-filter:blur(14px) saturate(1.3);
  color:var(--kd-muted);
  font-family:inherit;
  font-size:0.82rem;
  font-weight:500;
  cursor:pointer;
  text-align:left;
  transition:border-color var(--kd-t-fast) var(--kd-ease),color var(--kd-t-fast) var(--kd-ease),background var(--kd-t-fast) var(--kd-ease);
}
.ob-pill.is-active{
  border-color:var(--kd-accent);
  background:var(--kd-accent);
  color:var(--kd-on-accent);
}

/* The actions bar sticks to the bottom of the viewport and frosts what
   scrolls under it, so a long step never buries Continue. */
.ob-actions{
  display:flex;
  align-items:center;
  gap:0.6rem;
  flex-wrap:wrap;
  position:sticky;
  bottom:0;
  margin-top:auto;
  /* Bleeds past the inner padding to the screen edges, so it reads as
     the bottom of the picture rather than a grey card laid on top of
     it, and fades in from transparent instead of drawing a hard line
     across the water. */
  margin-inline:calc(-1 * var(--kd-pad));
  padding:2.5rem var(--kd-pad) calc(1rem + env(safe-area-inset-bottom));
}
/* The wash sits on its own layer behind the buttons. A mask on the bar
   itself would fade the buttons with it — masks clip the whole subtree. */
.ob-actions::before{
  content:'';
  position:absolute;
  inset:0;
  z-index:-1;
  pointer-events:none;
  background:linear-gradient(180deg,
    transparent 0%,
    color-mix(in srgb, var(--kd-bg) 62%, transparent) 45%,
    color-mix(in srgb, var(--kd-bg) 92%, transparent) 100%);
  -webkit-backdrop-filter:blur(14px);
  backdrop-filter:blur(14px);
  -webkit-mask-image:linear-gradient(180deg,transparent 0%,#000 40%);
  mask-image:linear-gradient(180deg,transparent 0%,#000 40%);
}
.ob-actions .btn.primary{flex:1;padding:0.85rem 1.2rem;}

/* ---------- footer ---------- */
.footer{
  text-align:center;
  margin-top:2.5rem;
  padding-top:1.75rem;
  border-top:1px solid var(--kd-line);
}
.footer .label{letter-spacing:0.16em;}
.footer svg{height:24px;width:auto;margin:0 auto 0.85rem;display:block;opacity:0.5;color:var(--kd-muted);}

:focus-visible{outline:3px solid var(--kd-accent);outline-offset:3px;border-radius:6px;}

/* ============================================================
   Settings sheet
   ============================================================ */
.sheet{
  position:fixed;
  inset:0;
  z-index:50;
  background:var(--kd-bg);
  overflow-y:auto;
  /* WebKit gives input[type=date] an intrinsic width wider than its
     container, which pushed the sheet past the viewport and left it
     swiping sideways a few pixels. Clamp the control, and stop the
     scroll at the container as a backstop. */
  overflow-x:hidden;
  overscroll-behavior:contain;
  animation:sheet-in 0.4s var(--kd-ease) both;
}
.sheet[hidden]{display:none;}
@keyframes sheet-in{from{opacity:0;transform:translateY(2rem);}}
.sheet-inner{
  max-width:520px;
  margin:0 auto;
  padding:
    calc(1.5rem + env(safe-area-inset-top))
    calc(var(--kd-pad) + env(safe-area-inset-right))
    calc(3rem + env(safe-area-inset-bottom))
    calc(var(--kd-pad) + env(safe-area-inset-left));
}
.sheet-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
  margin-bottom:2rem;
}
.sheet-title{font-size:1.35rem;font-weight:600;letter-spacing:var(--kd-track-display);}
.group{margin-bottom:2.25rem;}
.group > .label{margin-bottom:0.85rem;}

/* ---------- appearance pickers ---------- */
.theme-seg{margin-bottom:0.9rem;}
.theme-seg button{flex:1;text-align:center;}
.swatches{display:flex;gap:0.55rem;flex-wrap:wrap;}
.swatch{
  width:34px;height:34px;
  flex:none;
  border-radius:50%;
  border:2px solid transparent;
  background:var(--sw);
  cursor:pointer;
  padding:0;
  position:relative;
  transition:transform var(--kd-t-fast) var(--kd-ease),box-shadow var(--kd-t-fast) var(--kd-ease);
}
/* The ring sits outside the colour so it reads on any of the six. */
.swatch.is-active{
  box-shadow:0 0 0 2px var(--kd-bg),0 0 0 4px var(--kd-text);
}
.swatch:focus-visible{outline:2px solid var(--kd-text);outline-offset:3px;}

/* ---------- day-of-week chips ----------
   Collapsed to a summary by default. Seven chips on every habit made a
   long list unreadable, and the answer is "every day" for most of them
   anyway — so the chips are opt-in and the summary carries the state. */
.field-group{margin-bottom:0.85rem;}
.field-group .field{margin-bottom:0.25rem;}
.days-wrap{padding-left:0.1rem;}
.days-summary{
  font-family:inherit;
  font-size:var(--kd-fs-tag);
  letter-spacing:0.04em;
  color:var(--kd-muted);
  background:transparent;
  border:none;
  border-radius:var(--kd-pill);
  padding:0.15rem 0;
  cursor:pointer;
  display:inline-flex;
  align-items:center;
  gap:0.35rem;
  transition:color var(--kd-t-fast) var(--kd-ease);
}
.days-summary::after{
  content:'';
  width:5px;height:5px;
  border-right:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;
  transform:rotate(45deg) translate(-1px,-1px);
  transition:transform var(--kd-t-fast) var(--kd-ease);
}
.days-wrap.is-open .days-summary::after{transform:rotate(-135deg) translate(-2px,-2px);}
/* A habit that isn't every-day is worth spotting in a long list. */
.days-wrap.is-custom .days-summary{color:var(--kd-accent);}
.days{
  display:flex;
  align-items:center;
  gap:0.28rem;
  flex-wrap:wrap;
  padding:0.4rem 0 0.2rem;
}
.days[hidden]{display:none;}
.day{
  width:26px;height:26px;
  flex:none;
  border-radius:50%;
  border:1px solid var(--kd-line);
  background:transparent;
  color:var(--kd-muted);
  font-family:inherit;
  font-size:0.66rem;
  font-weight:600;
  cursor:pointer;
  padding:0;
  transition:background var(--kd-t-fast) var(--kd-ease),color var(--kd-t-fast) var(--kd-ease),border-color var(--kd-t-fast) var(--kd-ease);
}
.day.on{
  background:var(--kd-accent);
  border-color:var(--kd-accent);
  color:var(--kd-on-accent);
}
.day:focus-visible{outline:2px solid var(--kd-text);outline-offset:2px;}

/* Shown on a card when some of its habits aren't scheduled today. */
.card-aside{
  font-size:var(--kd-fs-tag);
  color:var(--kd-muted);
  padding:0.15rem 0 0.9rem;
}
.group-note{font-size:var(--kd-fs-tag);color:var(--kd-muted);line-height:1.6;margin-top:0.7rem;}
.group-note code{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:0.95em;
  color:var(--kd-text);
  background:var(--kd-surface-2);
  padding:0.1rem 0.35rem;
  border-radius:5px;
}

.field{
  display:flex;
  align-items:center;
  gap:0.6rem;
  padding:0.6rem 0;
  border-top:1px solid var(--kd-line);
}
.field:first-of-type{border-top:none;}
.input{
  flex:1;
  min-width:0;
  font-family:inherit;
  font-size:0.95rem;
  color:var(--kd-text);
  background:var(--kd-surface);
  border:1px solid var(--kd-line);
  border-radius:var(--kd-radius-xs);
  padding:0.55rem 0.8rem;
  transition:border-color var(--kd-t-fast) var(--kd-ease);
}
.input:focus{outline:none;border-color:var(--kd-accent);}
.input.narrow{flex:none;width:64px;text-align:center;}
.input[type="date"],.input[type="time"]{
  width:100%;
  max-width:100%;
  min-width:0;
  -webkit-appearance:none;
  appearance:none;
}
textarea.input{resize:vertical;min-height:5.5rem;line-height:1.5;font-size:0.8rem;}
.remove{
  flex:none;
  width:30px;height:30px;
  display:grid;place-items:center;
  border-radius:50%;
  border:1px solid var(--kd-line);
  background:transparent;
  color:var(--kd-muted);
  font-size:1rem;
  line-height:1;
  cursor:pointer;
  transition:color var(--kd-t-fast) var(--kd-ease),border-color var(--kd-t-fast) var(--kd-ease);
}
.sub-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:1rem;
  margin:2rem 0 0.6rem;
}
/* The group title was styled identically to the habit rows beneath it,
   so a list read as one flat run of inputs. It now reads as a heading:
   larger, heavier, no box until you touch it. */
.input.title-input{
  font-size:1.1rem;
  font-weight:600;
  letter-spacing:var(--kd-track-display);
  background:transparent;
  border-color:transparent;
  padding-left:0;
  padding-right:0;
}
.input.title-input:focus{
  background:var(--kd-surface);
  border-color:var(--kd-line);
  padding-left:0.8rem;
  padding-right:0.8rem;
}
/* Habits sit indented under their title, so the hierarchy is spatial
   as well as typographic. */
.field-group{padding-left:0.9rem;border-left:1px solid var(--kd-line);}
.sub-head:first-child{margin-top:0;}
.sub-title{font-size:0.97rem;font-weight:600;}
.add{
  font-family:inherit;
  font-size:var(--kd-fs-tag);
  font-weight:600;
  letter-spacing:0.08em;
  text-transform:uppercase;
  color:var(--kd-muted);
  background:transparent;
  border:none;
  cursor:pointer;
  padding:0.2rem 0;
  transition:color var(--kd-t-fast) var(--kd-ease);
}
.row-btns{display:flex;gap:0.6rem;flex-wrap:wrap;}
.row-btns .btn{flex:1;min-width:120px;text-align:center;}
.toast{
  position:fixed;
  left:50%;
  bottom:calc(2rem + env(safe-area-inset-bottom));
  transform:translateX(-50%) translateY(1rem);
  z-index:200;
  background:var(--kd-surface-2);
  border:1px solid var(--kd-line);
  color:var(--kd-text);
  font-size:0.85rem;
  padding:0.7rem 1.2rem;
  border-radius:var(--kd-pill);
  opacity:0;
  pointer-events:none;
  transition:opacity var(--kd-t-fast) var(--kd-ease),transform var(--kd-t-move) var(--kd-ease);
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}

/* ---------- motion ---------- */
@keyframes kd-rise{
  from{opacity:0;transform:translateY(1.75rem);filter:blur(6px);}
  to{opacity:1;transform:none;filter:blur(0);}
}
.rise{animation:kd-rise var(--kd-t-reveal) var(--kd-ease) both;}
@keyframes kd-tally-in{from{height:0;opacity:0;}}

/* ============================================================
   Motion

   Three additions, all tied to something the user did — nothing here
   animates on its own except the mesh. Ambient motion you didn't cause
   is decoration; motion that answers a tap is feedback.
   ============================================================ */

/* A row gives under the finger and comes back. */
.item,.train{
  transition:transform 0.18s var(--kd-ease),color var(--kd-t-fast) ease;
  transform-origin:left center;
}
.item:active,.train:active{transform:scale(0.985);}

/* Ticking sends one ring of accent out from the box and fades it. The
   ripple lives on a pseudo-element so it can't affect layout. */
.item .box,.train .box{position:relative;}
.item.just-done .box::after,
.train.just-done .box::after{
  content:'';
  position:absolute;
  inset:0;
  border-radius:inherit;
  border:2px solid var(--kd-accent);
  animation:kd-ripple 0.62s var(--kd-ease) forwards;
  pointer-events:none;
}
@keyframes kd-ripple{
  from{ transform:scale(1); opacity:0.85; }
  to{ transform:scale(2.1); opacity:0; }
}

/* The dial's number lifts and settles whenever the count changes, so
   the headline figure acknowledges the tap rather than snapping. */
.dial-num.bumped{animation:kd-bump 0.5s var(--kd-ease);}
@keyframes kd-bump{
  0%{ transform:translateY(0.22em) scale(0.94); opacity:0; filter:blur(5px); }
  60%{ opacity:1; filter:blur(0); }
  100%{ transform:none; opacity:1; }
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:0.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:0.01ms !important;
  }
  .rise{animation:none;opacity:1;transform:none;filter:none;}
  /* Freeze the mesh where it starts — still a mesh, just a still one. */
  .blob{animation:none !important;}
  .item:active,.train:active{transform:none;}
}

/* ============================================================
   Hover, for things that can actually hover

   A touch tap leaves :hover latched on the last element touched, so
   ungated hover styles read as a button that stays lit — or, with a
   transform, one that stays zoomed. That was the wiggle. Gating the
   whole set behind a hover-capable pointer keeps the desktop polish
   and gives the phone none of the artefacts.
   ============================================================ */
@media (hover:hover) and (pointer:fine){
  .icon-btn:hover{color:var(--kd-text);border-color:var(--kd-muted);}
  .btn:hover{border-color:var(--kd-muted);}
  .btn.ghost:hover{color:var(--kd-text);border-color:transparent;}
  .ob-choice:hover{border-color:var(--kd-muted);}
  .ob-pill:hover{color:var(--kd-text);border-color:var(--kd-muted);}
  .swatch:hover{transform:scale(1.08);}
  .day:hover{border-color:var(--kd-muted);color:var(--kd-text);}
  .remove:hover{color:var(--kd-error);border-color:rgba(198,47,47,0.4);}
  .add:hover{color:var(--kd-accent);}
}

/* No double-tap-to-zoom on anything meant to be tapped. Without this
   iOS waits to see if a second tap is coming, which both delays the
   response and zooms the page when it decides one was. */
.item,.train,.day,.swatch,.ob-pill,.ob-choice,.seg button,.icon-btn,
.remove,.add,.btn,.tally,.ycell{
  touch-action:manipulation;
}

/* ============================================================
   The completion overlay

   A tick on the dial was the wrong reward for the wrong place: the
   dial lives at the top of the page and you are almost always at the
   bottom, ticking the last habit off, when the day actually closes.
   The moment has to travel to you.

   So the whole screen frosts, a pane of glass rises through it, a ring
   sweeps and a check draws — then it clears itself after a couple of
   seconds. Tap anywhere to dismiss early.
   ============================================================ */
.celebrate{
  position:fixed;
  inset:0;
  z-index:80;
  display:grid;
  place-items:center;
  background:rgb(var(--kd-accent-rgb) / 0.10);
  -webkit-backdrop-filter:blur(26px) saturate(1.5);
  backdrop-filter:blur(26px) saturate(1.5);
  animation:cel-in 0.42s var(--kd-ease) both;
}
.celebrate[hidden]{display:none;}
.celebrate.out{animation:cel-out 0.5s var(--kd-ease) both;}
@keyframes cel-in{from{opacity:0;}}
@keyframes cel-out{to{opacity:0;}}

/* The pane itself — the same glass as everything else, one size up. */
.celebrate-pane{
  display:grid;
  justify-items:center;
  gap:0.2rem;
  padding:2.6rem 2.9rem 2.3rem;
  border-radius:32px;
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.8);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.8);
  border:1px solid var(--glass-hair);
  box-shadow:
    inset 0 1px 0 var(--glass-edge),
    inset 0 -1px 0 var(--glass-under),
    0 30px 70px rgb(0 0 0 / 0.34);
  animation:cel-pane 0.66s var(--kd-ease) both;
}
@keyframes cel-pane{
  from{ opacity:0; transform:translateY(18px) scale(0.92); filter:blur(8px); }
  to{ opacity:1; transform:none; filter:blur(0); }
}

.celebrate-mark{width:132px;height:132px;display:block;overflow:visible;}

/* The whole day, faint — so a day that closed on the target rather than
   on every last box still reads as partly filled. */
.cel-track{
  fill:none;
  stroke:var(--kd-track);
  stroke-width:3;
}
/* One segment per habit closed, igniting in sequence. The stagger is
   what makes it feel counted rather than announced. */
.cel-seg{
  fill:none;
  stroke:var(--kd-accent);
  stroke-width:7;
  stroke-linecap:round;
  transform-origin:50% 50%;
  opacity:0;
  animation:cel-seg 0.5s var(--kd-ease) calc(0.18s + var(--i) * 0.075s) both;
  filter:drop-shadow(0 0 8px rgb(var(--kd-accent-rgb) / calc(var(--kd-glow) * 0.85)));
}
@keyframes cel-seg{
  from{ opacity:0; transform:scale(0.72); }
  60%{ opacity:1; transform:scale(1.06); }
  to{ opacity:1; transform:scale(1); }
}
/* Lands once the last segment is lit — the delay is derived from the
   segment count so it always arrives just after them. */
.cel-count{
  fill:var(--kd-text);
  font-family:var(--kd-font);
  font-size:34px;
  font-weight:600;
  letter-spacing:var(--kd-track-display);
  opacity:0;
  transform-origin:50% 50%;
  animation:cel-count 0.55s var(--kd-ease) calc(0.28s + var(--n) * 0.075s) both;
}
@keyframes cel-count{
  from{ opacity:0; transform:scale(0.6); filter:blur(6px); }
  to{ opacity:1; transform:scale(1); filter:blur(0); }
}

.celebrate-title{
  font-size:1.5rem;
  font-weight:600;
  letter-spacing:var(--kd-track-display);
  color:var(--kd-text);
  margin-top:0.9rem;
  animation:cel-rise 0.6s var(--kd-ease) 0.86s both;
}
.celebrate-sub{
  font-size:var(--kd-fs-tag);
  font-weight:500;
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-muted);
  animation:cel-rise 0.6s var(--kd-ease) 0.98s both;
}
@keyframes cel-rise{
  from{ opacity:0; transform:translateY(9px); filter:blur(5px); }
  to{ opacity:1; transform:none; filter:blur(0); }
}

/* A perfect day gets a second, wider halo behind the pane. */
.celebrate.perfect .celebrate-pane::after{
  content:'';
  position:absolute;
  inset:-40%;
  border-radius:50%;
  background:radial-gradient(circle, rgb(var(--kd-accent-rgb) / 0.22), transparent 62%);
  z-index:-1;
  animation:cel-halo 1.6s var(--kd-ease) 0.3s both;
}
.celebrate-pane{position:relative;}
@keyframes cel-halo{
  from{ opacity:0; transform:scale(0.7); }
  60%{ opacity:1; }
  to{ opacity:0.75; transform:scale(1); }
}

@media (prefers-reduced-motion:reduce){
  .celebrate-pane,.celebrate-title,.celebrate-sub{animation:none;opacity:1;transform:none;filter:none;}
  .cel-seg,.cel-count{animation:none;opacity:1;transform:none;filter:none;}
}

/* A day the streak was carried through. Marked rather than hidden — a
   streak that survives a miss should say so, or the number stops
   meaning anything. */
.tally.grace{
  background:transparent;
  border:1.5px dashed rgb(var(--kd-accent-rgb) / 0.65);
}

/* ============================================================
   The day editor
   ============================================================ */
.dayedit{
  position:fixed;
  inset:0;
  z-index:55;
  background:var(--kd-bg);
  overflow-y:auto;
  overflow-x:hidden;
  overscroll-behavior:contain;
  animation:sheet-in 0.4s var(--kd-ease) both;
}
.dayedit[hidden]{display:none;}
.de-inner{
  max-width:520px;
  margin:0 auto;
  padding:
    calc(1.5rem + env(safe-area-inset-top))
    calc(var(--kd-pad) + env(safe-area-inset-right))
    calc(3rem + env(safe-area-inset-bottom))
    calc(var(--kd-pad) + env(safe-area-inset-left));
}
.de-inner .sheet-head{align-items:flex-start;}
.de-inner .sheet-head .label{margin-top:0.35rem;}
.de-group{margin-bottom:1.9rem;}
.de-group > .label{margin-bottom:0.35rem;}
/* Rows reuse the main screen's item styling so a past day looks and
   behaves exactly like today — same box, same tick, same feedback. */
.de-item{border-top:1px solid var(--kd-line);}
.de-group .de-item:first-of-type{border-top:none;}

/* The ledger is now interactive, so it should say so. */
.tally,.ycell{cursor:pointer;}

/* ============================================================
   The cover

   Step -1 of onboarding: the mark, a face, and the promise. It carries
   a photograph where the rest of the app does not, because this is the
   one screen you see once — the objection to imagery was always that
   you can't dismiss it on a screen you open daily, and this isn't one.
   ============================================================ */
.onboard.is-cover::before{
  background:
    linear-gradient(180deg,
      rgb(20 12 16 / 0.55) 0%,
      rgb(20 12 16 / 0.10) 34%,
      rgb(20 12 16 / 0.30) 68%,
      rgb(20 12 16 / 0.86) 100%),
    url('../img/intro.jpg') center/cover no-repeat;
}
.ob-cover{
  display:flex;
  flex-direction:column;
  justify-content:space-between;
  min-height:64vh;
  padding-top:1rem;
}
.ob-cover-mark{
  align-self:center;
  width:94px;
  color:#fff;
  filter:drop-shadow(0 2px 18px rgb(0 0 0 / 0.45));
}
.ob-cover-mark svg{width:100%;height:auto;display:block;}
/* The tagline sits low, over the darkest part of the gradient. */
.ob-cover-line{
  font-size:clamp(1.5rem,6.4vw,1.9rem);
  font-weight:600;
  letter-spacing:var(--kd-track-display);
  color:#fff;
  text-align:center;
  margin-bottom:0.5rem;
}
.ob-cover-line em{font-style:normal;color:var(--kd-accent);}
/* On the cover the copy is always light — it sits on a photograph, not
   on the theme's surface. */
.onboard.is-cover .ob-actions .btn.primary{width:100%;}

/* The category header carries its own tools, so adding a habit and
   removing the whole group don't compete for the same slot. */
.sub-head-tools{display:flex;align-items:center;gap:0.5rem;flex:none;}

/* Multi-select focus: a tick that appears when chosen, so "more than
   one" is legible rather than implied by colour alone. */
.ob-choice{display:flex;align-items:center;justify-content:space-between;gap:1rem;}
.ob-check{
  width:20px;height:20px;flex:none;
  border-radius:50%;
  border:1.5px solid var(--kd-line);
  position:relative;
  transition:background var(--kd-t-fast) var(--kd-ease),border-color var(--kd-t-fast) var(--kd-ease);
}
.ob-choice.is-active .ob-check{background:var(--kd-accent);border-color:var(--kd-accent);}
.ob-choice.is-active .ob-check::after{
  content:'';
  position:absolute;
  left:6px;top:2.5px;
  width:5px;height:10px;
  border:solid var(--kd-on-accent);
  border-width:0 2px 2px 0;
  transform:rotate(45deg);
}
.ob-focus-note{
  margin-top:0.9rem;
  font-size:var(--kd-fs-tag);
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-accent);
}

/* ---------- demo badge ----------
   Only present when ?demo=1. It has to be unmissable: a sandbox that
   looks exactly like the real app is a good test and a bad surprise. */
.demo-bar{
  /* Top, not bottom: the bottom of every screen already belongs to a
     primary action — Get started, Continue, Done — and the badge was
     landing on top of them. */
  position:fixed;
  left:50%;
  top:calc(0.5rem + env(safe-area-inset-top));
  transform:translateX(-50%);
  z-index:90;
  display:flex;
  align-items:center;
  gap:0.85rem;
  padding:0.55rem 0.85rem 0.55rem 1.1rem;
  border-radius:var(--kd-pill);
  border:1px solid var(--glass-hair);
  background:var(--glass-bg);
  -webkit-backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  backdrop-filter:blur(var(--glass-blur)) saturate(1.7);
  box-shadow:inset 0 1px 0 var(--glass-edge),var(--glass-lift);
  color:var(--kd-text);
  font-family:inherit;
  font-size:var(--kd-fs-tag);
  letter-spacing:0.04em;
  cursor:pointer;
  touch-action:manipulation;
}
.demo-bar b{
  color:var(--kd-accent);
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:var(--kd-track-label);
}
/* Keep the footer clear of the badge. */
body.is-demo .shell{padding-top:calc(3.6rem + env(safe-area-inset-top));}
body.is-demo .ob-inner{padding-top:calc(3.8rem + env(safe-area-inset-top));}

/* ============================================================
   Tablet

   A phone column stranded in the middle of a 13-inch screen is not an
   iPad app, it is an iPhone app someone let grow. So above a genuine
   tablet width the page splits: the instruments — dial, streak, ledger —
   settle into a column on the left and stay put, while the things you
   actually tick move to the right and scroll past them. The summary
   stops scrolling away from the work.

   Below that width the two wrappers are display:contents, which means
   they are not there at all as far as layout is concerned and the phone
   renders exactly as it did before this block existed.
   ============================================================ */
.col-left,
.col-right{ display:contents; }

@media (min-width:820px){
  .shell{
    max-width:1180px;
    display:grid;
    /* The left column is proportional, not greedy. A fixed 380px took
       more than half of an 11-inch portrait screen and left the habit
       list — the part you actually use — narrower than the instruments
       beside it. */
    grid-template-columns:clamp(260px,34%,380px) minmax(0,1fr);
    column-gap:clamp(2rem,5vw,4.5rem);
    align-items:start;
  }
  .masthead{ grid-column:1 / -1; }

  .col-left,
  .col-right{ display:block; }

  /* The dial is the anchor of the whole screen, so it earns the extra
     room the wider layout gives it. */
  .hero .dial{ max-width:320px; }

  /* Nothing in the left column is the last thing on the page any more,
     so its trailing margin would just push the column off-centre. */
  .col-left > :last-child{ margin-bottom:0; }
}

/* Pin the instruments only when the window is tall enough for them to
   have somewhere to stay. In a short landscape window a panel that
   sticks reads as something jammed rather than something anchored. */
@media (min-width:820px) and (min-height:720px){
  .col-left{
    position:sticky;
    top:calc(1.5rem + env(safe-area-inset-top));
  }
}

/* Wider still, and the habit cards can sit two abreast rather than
   running to an uncomfortable measure. The note field and the reminder
   stay full width — they are single things, not a set. */
@media (min-width:1180px){
  .col-right{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    column-gap:1.25rem;
    align-items:start;
  }
  .col-right > .card.pad-b,
  .col-right > .remind,
  .col-right > .footer{ grid-column:1 / -1; }
}

/* ============================================================
   Tablet: the full-screen surfaces

   The main screen already splits in two above 820px, but onboarding,
   Settings and the day editor were still 520px columns sitting in the
   middle of a 13-inch screen, which reads as a phone app someone
   propped up rather than something built for the device.

   Each one gets the treatment that suits what it is: onboarding asks on
   the left and answers on the right, Settings and the day editor lay
   their groups out two abreast. Below 820px every rule here is inert and
   the phone is untouched.
   ============================================================ */
.ob-ask,
.ob-do{ display:contents; }

@media (min-width:820px){
  /* ---------- onboarding ---------- */
  .ob-inner{ max-width:1000px; }

  /* :not(.ob-cover) — the cover is a flex column that spaces the mark,
     the tagline and the button down the picture. Turning it into a grid
     threw all three into the wrong places. */
  .ob-step:not(.ob-cover){
    display:grid;
    grid-template-columns:minmax(240px,32%) minmax(0,1fr);
    column-gap:clamp(2rem,5vw,4rem);
    align-items:start;
  }
  .ob-ask,
  .ob-do{ display:block; }

  /* The question holds still while the answers scroll, so you can always
     see what you are answering. */
  .ob-ask{ position:sticky; top:0; z-index:1; }
  .ob-title{ font-size:clamp(2rem,3.4vw,2.9rem); }
  .ob-sub{ margin-bottom:0; }

  /* Focus and habit chips have room to sit in a proper field rather than
     a single stack. */
  .ob-choices{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    gap:0.6rem;
  }

  /* The cover is a photograph, not a form — it keeps its narrow measure
     so the tagline stays a caption rather than stretching across the
     whole picture. */
  .onboard.is-cover .ob-inner{ max-width:560px; }

  /* The actions bar bled to the phone's screen edges. At this width that
     would leave it floating far from the content it belongs to, so it
     tucks back to the column. */
  .ob-actions{ margin-inline:0; }

  /* ---------- settings, and the day editor ---------- */
  .sheet-inner,
  .de-inner{
    max-width:1000px;
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
    column-gap:clamp(2rem,4vw,3.5rem);
    align-items:start;
  }
  .sheet-head{ grid-column:1 / -1; }

  /* Save applies to the whole sheet, so it spans the whole sheet rather
     than sitting at the foot of whichever column it happened to land in.
     :has() picks it out without needing a new class in the markup. */
  .sheet-inner > .group:has(> [data-save]){
    grid-column:1 / -1;
    max-width:22rem;
    margin-inline:auto;
    width:100%;
  }

  /* The note is a single wide thing, not one of a pair. */
  .de-inner > .de-group:last-of-type{ grid-column:1 / -1; }
}

/* A 13-inch screen can carry three columns of settings without the rows
   becoming uncomfortably long. */
@media (min-width:1180px){
  .sheet-inner{ grid-template-columns:repeat(3,minmax(0,1fr)); }
  .ob-choices{ grid-template-columns:repeat(3,minmax(0,1fr)); }
}

/* ============================================================
   Tablet, second pass

   Four things the device showed that the browser did not.
   ============================================================ */

/* ---------- 1. the cover is film now ----------
   The still was a portrait 1180x2354 built for a phone, so every tablet
   shape cropped hard into it. The film is the opposite — 16:9 — so on a
   phone it fills the frame and on a tablet it stops trying to and takes
   a pane of its own instead. */
.ob-cover-video{
  position:fixed;
  inset:0;
  width:100%;
  height:100%;
  object-fit:cover;
  z-index:-1;
  pointer-events:none;
}
.ob-cover-body{
  display:flex;
  flex-direction:column;
  justify-content:space-between;
  min-height:inherit;
  flex:1;
  position:relative;   /* above the film and its scrim */
  z-index:1;
}

/* The scrim was painted by is-cover::before as a background image. The
   film sits behind it now, so the layer keeps only its gradient. */
.onboard.is-cover::before{
  background:linear-gradient(180deg,
    rgb(20 12 16 / 0.55) 0%,
    rgb(20 12 16 / 0.10) 34%,
    rgb(20 12 16 / 0.30) 68%,
    rgb(20 12 16 / 0.86) 100%);
}

@media (min-width:820px){
  /* ---------- 3. the actions wash spans the screen ----------
     It bleeds to the screen edges with a negative margin, which is
     correct on a phone where the column is the screen. In a centred
     1000px column it stopped mid-screen and drew two hard edges. This
     is the full-bleed trick: out to the viewport, then padded back. */
  .ob-actions{
    margin-inline:calc(50% - 50vw);
    padding-inline:calc(50vw - 50% + var(--kd-pad));
    /* Above the sticky question, so a long list of habits can never
       scroll it underneath something. */
    z-index:3;
    /* And clear of the bottom edge. A control sitting 16px from the
       bottom of an iPad is inside the home-indicator gesture strip,
       where the first tap tends to go to the system rather than the
       button — which feels exactly like a dead button. */
    padding-bottom:calc(2.25rem + env(safe-area-inset-bottom));
  }
}

/* The cover's actions never bleed: on a phone the picture runs to the
   edges, and on a tablet the bar sits inside the column either way. */
@media (min-width:820px){
  /* The wash under the cover's buttons has to reach both screen edges.
     Capping this bar at the reading column drew the gradient as a
     520px-wide band with a hard edge down each side, sitting on top of
     a full-screen film. The bar spans; only its contents are centred. */
  .onboard.is-cover .ob-actions{
    margin-inline:calc(50% - 50vw);
    padding-inline:calc(50vw - 50% + var(--kd-pad));
    justify-content:center;
  }
  .onboard.is-cover .ob-actions > *{ max-width:520px; }
  .onboard.is-cover .ob-actions .btn.primary{ flex:0 1 520px; }
}

/* ---------- 4. the mesh was tuned for a phone ----------
   The blobs are sized in vw but the blur was a fixed 76px. At 430px a
   78vw blob is 335px across and the blur swallows it; at 1366px the same
   blob is 1066px and the blur barely touches the edge, so what reads as
   soft light on a phone reads as five bright discs on a tablet — which
   is why dark mode stopped looking dark. The blur now scales with the
   blobs, and the whole layer sits back a little. */
@media (min-width:820px){
  :root{
    --mesh-blur:clamp(76px, 9vw, 118px);
    --mesh-alpha:0.36;
  }
  :root[data-theme="light"]{
    --mesh-blur:clamp(88px, 10vw, 132px);
    --mesh-alpha:0.42;
  }
}

/* ---------- 5. landscape was leaving space at the edges ---------- */
@media (min-width:1180px){
  .shell{ max-width:1400px; }
  /* Cards fill the room they are given rather than always pairing up,
     so a 13-inch in landscape gets three and nothing is stranded. */
  .col-right{
    grid-template-columns:repeat(auto-fill,minmax(300px,1fr));
  }
  .sheet-inner,
  .de-inner{ max-width:1200px; }
}


/* ============================================================
   Onboarding is opaque, so stop paying for the mesh underneath it.

   The blobs drift continuously and each one carries a large blur, so
   every frame re-rasterises five full-screen layers. Behind onboarding
   none of that is visible: the panel paints its own ground over the
   top. Pausing the drift there took the habit screen from stalling on
   each tap to responding immediately.
   ============================================================ */
html.is-onboarding .blob{ animation-play-state:paused; }
html.is-onboarding .ambient{ opacity:0; }

/* A date input with no value renders empty in the iOS web view — no
   dd/mm/yyyy, nothing — so it needs to say what it is from the outside. */
.field-label{
  display:block;
  font-size:var(--kd-fs-label);
  font-weight:500;
  letter-spacing:var(--kd-track-label);
  text-transform:uppercase;
  color:var(--kd-muted);
  margin:0.9rem 0 0.4rem;
}
/* The control itself should look occupied even while it is empty. */
.input[type="date"]{ min-height:3rem; }
.input[type="date"]:not(:focus):invalid,
.input[type="date"][value=""]{ color:var(--kd-muted); }

/* The page never pans sideways. overflow-x:clip already says so for
   layout; this says the same thing to touch, which is the layer that
   was still allowing a horizontal drag. The year strip is the one
   thing that genuinely scrolls across, so it opts back in. */
body{ touch-action:pan-y; }
.year-wrap{ touch-action:pan-x pan-y; }
