* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: var(--font);
  font-size: var(--size-body);
  line-height: 1.5;
  color: var(--text);
  background: var(--bg) radial-gradient(120% 60% at 50% 0%, var(--bg-glow) 0%, var(--bg) 62%);
  min-height: 100vh;          /* fallback: older iOS Safari has no dvh unit */
  min-height: 100dvh;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  -webkit-user-select: none;
  /* Allow vertical scroll, hide only horizontal. If the layout ever computes taller than the
     viewport (engine differences), it scrolls — it must never clip content to nothing. */
  overflow-x: hidden;
}

:focus-visible { outline: 2px solid #5ef0c0aa; outline-offset: 2px; }

/* Flex column, not a fixed-height nested grid: nested minmax(0,1fr) grids resolve their height
   differently across engines and collapse to zero on WebKit. Flex column with min-height:0 on the
   growable middle is the cross-engine-safe full-height shell. */
.app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;          /* min-, not fixed: a collapsed shell would clip to nothing */
  min-height: 100dvh;
  /* ...but it must not grow past the screen either, or Home scrolls as a page and the sticky tab
     bar rides over the quest list. Capped here, the list is what scrolls, which is what a list of
     habits should do once there are more than a screenful. */
  max-height: 100vh;
  max-height: 100dvh;
  max-width: 480px;
  margin: 0 auto;
  padding: max(12px, env(safe-area-inset-top)) 16px 0;
  gap: 12px;
}
.topbar, .xp { flex: none; }

/* Screens swap, they never slide — navigation must feel instant. */
.screen {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  animation: screen-in var(--t-press) linear;
}
.screen[hidden] { display: none; }
#screen-journey, #screen-you {
  overflow-y: auto;
  padding-bottom: 8px;
}
.tabs { flex: none; position: sticky; bottom: 0; }
@keyframes screen-in { from { opacity: 0; } to { opacity: 1; } }

.screen__title { font-size: var(--size-title); letter-spacing: -0.02em; }
.screen__sub { font-size: var(--size-body); color: var(--text-muted); margin-top: 4px; }
.screen__note { font-size: var(--size-small); color: var(--text-muted); }

.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.stat {
  display: grid; gap: 2px;
  padding: 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.stat__value { font-size: 1.75rem; font-weight: 800; line-height: 1.1; }
.stat__unit { font-size: var(--size-small); color: var(--text-muted); }
.stat__label { font-size: var(--size-small); color: var(--text-muted); }

.habit-stats { list-style: none; display: grid; gap: 8px; }
.habit-stat {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center; gap: 10px;
  padding: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.habit-stat__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.habit-stat__num { font-weight: 700; }
.habit-stat__num small { font-weight: 400; color: var(--text-muted); }

.card {
  display: grid; gap: 6px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.card__label { font-size: var(--size-small); color: var(--text-muted); }
.card__value { font-size: 1.125rem; font-weight: 700; }
.card__meta { font-size: var(--size-small); color: var(--text-muted); }
.plain-list { list-style: none; display: grid; gap: 4px; padding: 4px 0; }

/* ---- sound opt-in, asked once ---- */
.ask {
  position: fixed; inset: auto 16px calc(76px + env(safe-area-inset-bottom));
  z-index: var(--z-banner);
  max-width: 448px; margin: 0 auto;
  display: grid; gap: 10px;
  padding: 14px 16px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.ask__text { font-size: var(--size-small); }
.ask__actions { display: flex; gap: 8px; justify-content: flex-end; }

/* ---- add-quest button ---- */
.add-quest {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: none;
  color: var(--text-muted);
  font: inherit; font-size: var(--size-small); font-weight: 700;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out), border-color var(--t-ui) var(--ease-out);
}
.add-quest:active { transform: scale(0.98); }
.add-quest__plus { position: relative; width: 14px; height: 14px; }
.add-quest__plus::before, .add-quest__plus::after {
  content: ''; position: absolute; inset: 50% 0 auto;
  height: 2px; background: currentColor; border-radius: 2px;
}
.add-quest__plus::after { transform: rotate(90deg); }

/* ---- sheet (the one gesture surface) ---- */
.scrim {
  position: fixed; inset: 0;
  z-index: var(--z-sheet);
  background: #05071adb;
  opacity: 0;
  transition: opacity var(--t-ui) var(--ease-out);
}
.scrim.on { opacity: 1; }
.scrim[hidden] { display: none; }

.sheet {
  position: fixed; inset: auto 0 0;
  z-index: var(--z-sheet);
  max-width: 480px; margin: 0 auto;
  max-height: 88vh;
  max-height: 88dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: grid; gap: 10px;
  padding: 8px 16px calc(20px + env(safe-area-inset-bottom));
  background: var(--sheet-bg);
  -webkit-backdrop-filter: blur(20px) saturate(160%);   /* iOS needs the prefix or the glass is clear */
  backdrop-filter: blur(20px) saturate(160%);
  border-top: 1px solid var(--sheet-edge);   /* bright top edge, Apple materials */
  border-radius: 24px 24px 0 0;
  /* pan-y, NOT none: the body must scroll natively or a tall sheet hides its submit button below
     the screen. The drag-to-dismiss lives on the handle only (which sets touch-action:none). */
  touch-action: pan-y;
  -webkit-overflow-scrolling: touch;
  will-change: transform;
}
.sheet[hidden] { display: none; }

/* Full-width grab zone so drag-to-dismiss has a real target; the visible pill is centered in it. */
.sheet__handle {
  height: 24px;
  margin: 0 auto 4px;
  display: flex; align-items: center; justify-content: center;
  cursor: grab;
  touch-action: none;
  position: sticky; top: 0;
}
.sheet__handle::before {
  content: '';
  width: 40px; height: 4px;
  border-radius: 999px;
  background: var(--text-muted);
  opacity: 0.5;
}
.sheet__title { font-size: var(--size-title); letter-spacing: -0.02em; }
.sheet__note { font-size: var(--size-small); color: var(--text-muted); }

.field { display: grid; gap: 6px; }
.field__label { font-size: var(--size-small); color: var(--text-muted); }
.field__input {
  min-height: var(--tap);
  padding: 0 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
}
.field__input::placeholder { color: var(--text-muted); }

.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip-btn {
  min-height: var(--tap);
  padding: 0 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit; font-size: var(--size-small);
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out), border-color var(--t-ui) var(--ease-out);
}
.chip-btn:active { transform: scale(0.96); }

.glyph-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.glyph {
  min-height: var(--tap);
  display: grid; place-items: center;
  font-size: 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out), border-color var(--t-ui) var(--ease-out);
}
.glyph:active { transform: scale(0.95); }
.glyph.on { background: var(--accent-soft); border-color: var(--accent-line); }

.segmented { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.segment {
  min-height: var(--tap);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font: inherit; font-weight: 700;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out),
              border-color var(--t-ui) var(--ease-out),
              color var(--t-ui) var(--ease-out);
}
.segment:active { transform: scale(0.97); }
.segment.on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-ink); }

/* ---- hold to delete ---- */
.habit-row {
  position: relative;
  display: flex; align-items: center;
  min-height: var(--tap);
  padding: 0 12px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  touch-action: none;
  cursor: pointer;
}
.habit-row__fill {
  position: absolute; inset: 0;
  background: linear-gradient(90deg, #ff8fb133, #ff8fb166);
  clip-path: inset(0 100% 0 0);
  transition: clip-path 200ms var(--ease-out);   /* snap-back on release is fast */
}
.habit-row.holding .habit-row__fill {
  clip-path: inset(0);
  transition: clip-path 1200ms linear;           /* deliberate on the way in */
}
.habit-row__text { position: relative; }

/* ---- onboarding (first run only) ---- */
.overlay:empty { display: none; }
.overlay {
  position: fixed; inset: 0;
  z-index: var(--z-sheet);
  background: var(--bg) radial-gradient(120% 60% at 50% 0%, var(--bg-glow) 0%, var(--bg) 62%);
  overflow-y: auto;
  transition: opacity var(--t-ui) var(--ease-out);
}
.overlay.leaving { opacity: 0; }

.onboard {
  display: grid; gap: 20px; align-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  max-width: 480px; margin: 0 auto;
  padding: 24px 16px calc(24px + env(safe-area-inset-bottom));
}
.onboard__title { font-size: var(--size-title); letter-spacing: -0.02em; text-align: center; }
.onboard__cards { display: grid; gap: 12px; }

.starter {
  display: grid;
  grid-template-columns: 84px 1fr;
  grid-template-areas: 'art name' 'art line';
  align-items: center;
  gap: 2px 14px;
  min-height: 96px;
  padding: 12px 16px 12px 12px;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  /* ponytail: no entry animation. The staggered fade fought CSS specificity, was invisible on
     throttled tabs, and users see this screen exactly once. Press feedback stays. */
  transition: transform var(--t-press) var(--ease-out),
              border-color var(--t-ui) var(--ease-out),
              opacity var(--t-ui) var(--ease-out);
}
.starter:active { transform: scale(0.98); }
.starter__art { grid-area: art; width: 84px; height: 84px; }
.starter__art svg, .starter__art img { width: 100%; height: 100%; object-fit: contain; }
.starter__name { grid-area: name; font-size: 1.125rem; font-weight: 800; align-self: end; }
.starter__line { grid-area: line; font-size: var(--size-small); color: var(--text-muted); align-self: start; }

.starter.picked { border-color: var(--mint); }
.onboard__cards:has(.picked) .starter:not(.picked) { opacity: 0.3; }


/* ---- top bar ---- */
.topbar { display: flex; align-items: center; justify-content: space-between; }

.chip {
  display: flex; align-items: center; gap: 6px;
  min-height: var(--tap);
  padding: 0 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: var(--size-small);
  font-weight: 700;
}
.chip svg { width: 18px; height: 18px; }
.chip--level svg { color: var(--mint); }
.chip--streak svg { color: var(--flame); }

.flame-icon { display: inline-flex; transform-origin: 50% 80%; }

/* ---- xp bar ---- */
.xp {
  height: 8px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  overflow: hidden;
}
.xp__fill {
  height: 100%;
  width: 100%;
  background: var(--brand);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--t-beat) linear;
}

/* ---- scene ---- */
/* Sized by its contents, never by the leftover space. As a shrinkable, centred box it did both
   wrong things at once: with a short list it absorbed 88px of dead air, and with a full one it
   compressed until the creature's bob crossed the XP bar. Fixed height means the slack goes to the
   quest list, which is the part that runs out of room. */
.scene {
  position: relative;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  gap: 2px;
  margin-top: 12px;  /* real clearance: the bob lifts 7px past the box, and at 6px the
                        sprout on the creature's head was grazing the XP bar */
}
/* The quest list is the part that gives, so Home fits a short phone instead of growing past it and
   sliding its last card under the sticky tab bar. `flex: none` here meant the page grew instead. */
.today, .add-quest { flex: none; }
.quests { flex: 1 1 auto; min-height: 0; }
/* The ground belongs under the creature's feet, not behind the name text — so the glade is
   anchored to the creature's own box, and the island's top arc lands just above its base. */
/* The plot carries the size, not the creature inside it: an explicitly sized box cannot collapse
   the way a content-sized nested grid can on WebKit (the bug that once blanked Home). Scales down
   on short screens so Home fits on small phones, full size on tall ones. */
.creature-plot {
  position: relative;
  width: clamp(96px, 15vh, 128px);
  height: clamp(96px, 15vh, 128px);
}
.glade {
  position: absolute;
  left: 50%; bottom: 0;
  width: 210%;
  /* Height comes from the SVG's own viewBox ratio, and the offset is a share of the glade's own
     height — so neither depends on the parent resolving a percentage height. `top: 73%` here
     silently became `top: 0` on any engine that sized the plot to zero, which threw the ground up
     behind the creature. */
  transform: translate(-50%, 58%);
  pointer-events: none;
}
.glade svg { display: block; width: 100%; height: auto; }
.creature-stage {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid; place-items: center;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out);
}
.creature-stage svg { width: 100%; height: 100%; overflow: visible; }
.creature-art {
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
  -webkit-user-drag: none; user-select: none;
  animation: bob 3.6s var(--ease-in-out) infinite;
  will-change: transform;
}
.creature-stage:active { transform: scale(0.98); }

/* One clock: body and shadow share duration and easing, so they can never drift apart. */
#body-group { animation: bob 3.6s var(--ease-in-out) infinite; transform-origin: 100px 150px; }
#shadow { animation: bob-shadow 3.6s var(--ease-in-out) infinite; transform-origin: 100px 164px; }
#eyes { transform-origin: 100px 102px; }

/* Not just a bob: a gentle rock and breathe so a still creature reads as alive, not asleep. */
@keyframes bob {
  0%, 100% { transform: translateY(0) rotate(-1.3deg) scale(1); }
  25%      { transform: translateY(-4px) rotate(0deg) scale(1.012); }
  50%      { transform: translateY(-7px) rotate(1.3deg) scale(1.02); }
  75%      { transform: translateY(-4px) rotate(0deg) scale(1.012); }
}
@keyframes bob-shadow {
  0%, 100% { transform: scale(1); opacity: 0.55; }
  50%      { transform: scale(0.88); opacity: 0.4; }
}
@keyframes blink {
  0%, 92%, 100% { transform: scaleY(1); }
  96%           { transform: scaleY(0.1); }
}
#eyes { animation: blink 5s linear infinite; }

.creature-name { font-size: 1.25rem; font-weight: 800; letter-spacing: -0.02em; }
.creature-stage-tag { font-size: var(--size-small); color: var(--text-muted); }

/* ---- today row ---- */
.today { display: flex; align-items: center; justify-content: space-between; }
.today__label { font-size: var(--size-small); color: var(--text-muted); }
.today__dots { display: flex; gap: 6px; }
/* Same size whether done or not: a row where half the dots are bigger reads as a rendering
   glitch, not as progress. Fill is the signal. */
.dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: transparent; border: 1.5px solid var(--border);
  transition: background-color var(--t-ui) var(--ease-out), border-color var(--t-ui) var(--ease-out);
}
.dot.on { background: var(--accent-line); border-color: var(--accent-line); }

/* ---- quests ---- */
.quests {
  display: grid; gap: 8px; align-content: start;
  padding-bottom: 4px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.quest {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 12px;
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: transform var(--t-press) var(--ease-out), opacity var(--t-ui) var(--ease-out),
              border-color var(--t-ui) var(--ease-out);
}
.quest__text { min-width: 0; }
.quest__name { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.quest__glyph {
  width: 36px; height: 36px; flex: none;
  display: grid; place-items: center;
  font-size: 1.25rem;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.quest__text { flex: 1; min-width: 0; }
.quest__name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.quest__meta { font-size: var(--size-small); color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
.quest__meta svg { width: 14px; height: 14px; color: var(--flame); }

.check {
  position: relative;
  flex: none;
  width: 46px; height: 46px;
  padding: 0;
  background: none; border: none;
  color: var(--text-muted);
  cursor: pointer;
}
/* Hit padding beyond the visible circle, without pushing the card layout around. */
.check::after { content: ''; position: absolute; inset: -8px; }
.check__ring {
  display: grid; place-items: center;
  width: 46px; height: 46px;
  border: 2px solid var(--border);
  border-radius: 50%;
  background: transparent;
  transition: transform var(--t-fast) var(--ease-out),
              border-color var(--t-fast) var(--ease-out),
              background-color var(--t-fast) var(--ease-out);
}
.check:active .check__ring { transform: scale(0.95); }
.check svg { width: 22px; height: 22px; opacity: 0; transition: opacity var(--t-press) var(--ease-out); }

/* A finished quest is settled, not louder than the ones still waiting. Six filled gradient discs
   read as decoration; a soft accent disc still says "done" without taking over the screen. */
.check.on .check__ring {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  transform: scale(1);
  transition-timing-function: var(--spring-pop);
}
.check.on svg { opacity: 1; color: var(--accent-ink); }
.quest.done { background: var(--surface); }
.quest.done .quest__name { color: var(--text-muted); }
.quest.done .quest__glyph { opacity: 0.65; }

/* ---- floating xp (fire-and-forget decoration) ---- */
.xp-float {
  position: fixed;
  z-index: var(--z-widget-fx);
  font-weight: 800;
  color: var(--mint);
  pointer-events: none;
  animation: xp-rise var(--t-beat) var(--ease-out) forwards;
}
@keyframes xp-rise {
  from { transform: translate(-50%, 0) scale(0.9); opacity: 0; }
  25%  { opacity: 1; }
  to   { transform: translate(-50%, -56px) scale(1.1); opacity: 0; }
}

/* ---- tabs ---- */
.tabs {
  display: flex;
  border-top: 1px solid var(--border);
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--bg);   /* opaque: it is sticky, content scrolls under it */
}
.tab {
  flex: 1;
  min-height: var(--tap);
  display: grid; place-items: center; gap: 2px;
  padding: 8px 0;
  background: none; border: none;
  color: var(--text-muted);
  font: inherit; font-size: 0.75rem;
  cursor: pointer;
  transition: color var(--t-press) var(--ease-out), transform var(--t-press) var(--ease-out);
}
.tab:active { transform: scale(0.96); }
.tab[aria-selected='true'] { color: var(--text); }
.tab svg { width: 22px; height: 22px; }

@media (prefers-reduced-motion: reduce) {
  #body-group, #shadow, #eyes, .creature-art { animation: none; }
  .screen { animation: none; }
  .xp-float { animation: xp-fade var(--t-ui) linear forwards; }
  @keyframes xp-fade { from { opacity: 1; } to { opacity: 0; } }
  .dot, .check__ring, .quest, .tab, .xp__fill {
    transition-duration: var(--t-press);
    transition-timing-function: linear;
  }
  .dot.on, .check.on .check__ring { transform: none; }
}

@media (prefers-reduced-transparency: reduce) {
  .sheet { background: var(--surface); -webkit-backdrop-filter: none; backdrop-filter: none; }
}

.badge {
  justify-self: start;
  margin-top: 4px;
  padding: 4px 12px;
  border-radius: 999px;
  background: #ff8fb122;
  border: 1px solid var(--danger-line);
  color: var(--blush);
  font-size: var(--size-small);
  font-weight: 700;
}

.habit-row__time {
  position: relative;
  margin-left: auto;
  font-size: var(--size-small);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* ---- attunement blend (You screen) ---- */
.attune { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 6px 10px; padding: 4px 0; }
.attune__label { font-size: var(--size-small); color: var(--text-muted); }
.attune__track { height: 8px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); overflow: hidden; }
.attune__fill {
  display: block; height: 100%; width: 100%; border-radius: 999px;
  transform-origin: left center;
  transition: transform var(--t-ui) var(--ease-out);
}
.attune__val { font-size: var(--size-small); font-variant-numeric: tabular-nums; color: var(--text-muted); min-width: 2ch; text-align: right; }

/* ---- journey: heatmap + trends ---- */
.heatmap {
  display: grid;
  grid-template-rows: repeat(7, 1fr);
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 3px;
  padding: 4px 0;
}
.heat { aspect-ratio: 1; border-radius: 3px; background: var(--surface-raised); }
.heat--0 { background: #1a1f3d; }
.heat--1 { background: #21506b; }
.heat--2 { background: #2f8a86; }
.heat--3 { background: #46c69f; }
.heat--4 { background: var(--mint); }

.habit-stat__trend { font-weight: 800; font-size: 1.1rem; }
.trend--up { color: var(--mint); }
.trend--down { color: var(--blush); }
.trend--flat { color: var(--text-muted); }

/* ---- toast (errors, blocked actions) ---- */
.toast {
  position: fixed; left: 50%; bottom: calc(84px + env(safe-area-inset-bottom));
  transform: translate(-50%, 12px);
  z-index: var(--z-toast);
  max-width: 88%;
  padding: 10px 16px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text);
  font-size: var(--size-small); font-weight: 600;
  opacity: 0; pointer-events: none;
  transition: opacity var(--t-ui) var(--ease-out), transform var(--t-ui) var(--ease-out);
}
.toast.on { opacity: 1; transform: translate(-50%, 0); }

.onboard__sub { text-align: center; color: var(--text-muted); font-size: var(--size-small); margin-top: -8px; }

/* ---- home guest sign-in banner ---- */
.signin-banner {
  flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text-muted);
  font: inherit; font-size: var(--size-small);
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out);
}
.signin-banner[hidden] { display: none; }
.signin-banner:active { transform: scale(0.99); }
.signin-banner__cta {
  flex: none; font-weight: 800;
  color: var(--brand-ink);
}
.card__id { font-size: 0.75rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* ---- welcome / login-first screen ---- */
.welcome { align-content: center; text-align: center; gap: 14px; }
.welcome__mark { width: 120px; height: 120px; margin: 0 auto; }
.welcome__mark svg, .welcome__mark img { width: 100%; height: 100%; object-fit: contain; }
.welcome__actions { display: grid; gap: 10px; margin-top: 12px; }
.welcome .ask__btn { justify-self: center; }

/* ---- destructive: delete account ---- */

/* ---- weekly letter card ---- */
.letter { border-color: #9d7bff44; background: linear-gradient(180deg, #9d7bff11, var(--surface)); }
.letter__line { font-size: var(--size-body); line-height: 1.5; }
.letter__line + .letter__line { margin-top: 6px; color: var(--text-muted); }

/* ---- button row + rename input ---- */
.btn-row { display: flex; gap: 8px; margin-top: 4px; }
.btn-row > .btn { flex: 1 1 0; }
.rename-input { font-size: 1.125rem; font-weight: 700; }

/* ---- habit goal line ---- */
.habit-row__text { display: flex; flex-direction: column; gap: 1px; }
.habit-row__goal { font-size: 0.75rem; color: var(--text-muted); }

/* ---- webhook code block ---- */
.code {
  font: 12px/1.5 ui-monospace, monospace;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 10px; margin: 4px 0; overflow-x: auto; white-space: pre; color: var(--text-muted);
}

/* ---- share button ---- */
.share-btn { width: 100%; margin-top: 8px; }

/* ---- world fireflies (perfect-day garnish, §3.3). Transform/opacity only; motion off on request. */
.firefly {
  animation: firefly-drift 4.2s var(--ease-in-out, ease-in-out) infinite;
  animation-delay: var(--fd, 0s);
  transform-box: fill-box;
  transform-origin: center;
  will-change: transform, opacity;
}
@keyframes firefly-drift {
  0%, 100% { transform: translate(0, 0); opacity: 0.35; }
  50%      { transform: translate(4px, -6px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .firefly { animation: none; opacity: 0.8; }
}


}

/* An anchor styled as a button still needs to sit on the same baseline as real buttons. */
a.ask__btn { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; }

/* ---- weekday picker ---- */
.days { display: flex; gap: 6px; }
.day {
  flex: 1 1 0;
  min-height: var(--tap);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font: inherit; font-size: var(--size-small); font-weight: 600;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out), color var(--t-ui) var(--ease-out),
              background-color var(--t-ui) var(--ease-out), border-color var(--t-ui) var(--ease-out);
}
.day:active { transform: scale(0.96); }
.day.on {
  color: var(--accent-ink);
  background: var(--accent-soft);
  border-color: var(--accent-line);
  font-weight: 800;              /* weight carries it too, so colour is not the only signal */
}
.days-summary { font-size: var(--size-small); color: var(--text-muted); margin-top: 6px; }
.habit-row__goal + .habit-row__days, .habit-row__days { display: block; }

/* A secondary call to action: reads as a real button, without competing with the primary one. */

/* ---- fourteen-day chart ---- */
.chart { gap: 8px; }
.chart svg { display: block; width: 100%; overflow: visible; }
.chart__axis {
  display: flex; justify-content: space-between;
  font-size: var(--size-small); color: var(--text-muted);
}

/* ---- habit glyphs as drawn icons ---- */
.glyph-icon { display: inline-flex; color: var(--mint); }
.glyph-icon svg { width: 22px; height: 22px; }
.glyph-emoji { font-size: 1.25rem; line-height: 1; }
.quest__glyph .glyph-icon svg, .habit-stat__glyph .glyph-icon svg { width: 24px; height: 24px; }
.glyph { display: inline-flex; align-items: center; justify-content: center; }
.glyph.on .glyph-icon { color: var(--accent-ink); }

/* ---- buttons ----
   One family, three jobs. Before this the screen mixed 999px pills, 12px segments and 18px cards,
   and the secondary button set its label to --text-muted, which is the colour disabled controls
   use: a button that reads as unavailable is the "naive" tell, not the shape. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap);
  padding: 0 18px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font: inherit; font-size: var(--size-body); font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out),
              background-color var(--t-ui) var(--ease-out),
              border-color var(--t-ui) var(--ease-out);
}
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.45; cursor: default; }
.btn:disabled:active { transform: none; }

.btn--primary { background: var(--brand); color: #fff; }
.btn--primary:hover { background: var(--brand-strong); }

/* Full text colour, not muted: this is an available action, it just is not the main one. */
.btn--secondary {
  background: var(--surface-raised);
  border-color: var(--border);
  color: var(--text);
  font-size: var(--size-small);
}
.btn--secondary:hover { border-color: var(--brand-line); }

.btn--danger {
  background: none;
  border-color: var(--danger-line);
  color: var(--danger);
  font-size: var(--size-small);
}
.btn--danger--armed { background: var(--danger-soft); border-color: var(--danger); }

.btn--primary.share-btn { width: 100%; margin-top: 8px; }

/* ---- folded section ----
   The webhook is a real feature and a rare one. As a full card it cost the same weight as the
   account, for something most people never open. */
.card--fold { cursor: pointer; }
.card--fold > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; justify-content: space-between;
  min-height: 24px;
}
.card--fold > summary::-webkit-details-marker { display: none; }
.card--fold > summary::after {
  content: ''; width: 8px; height: 8px; flex: none;
  border-right: 2px solid var(--text-muted); border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--t-ui) var(--ease-out);
}
.card--fold[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.card--fold[open] { gap: 8px; }

/* ---- buttons ----
   One family, four jobs, on the system radius. The app previously ran three at once: 999px pills
   (.cta, .ask__btn, .chip-btn), 12px controls and 18px cards, all on screen together. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  min-height: var(--tap);
  padding: 0 18px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: none;
  font: inherit; font-size: var(--size-body); font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--t-press) var(--ease-out),
              background-color var(--t-ui) var(--ease-out),
              border-color var(--t-ui) var(--ease-out);
}
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.45; cursor: default; }
.btn:disabled:active { transform: none; }
.btn--block { width: 100%; }

/* One filled button per group. Two side by side is two primaries, which is none. */
.btn--primary { background: var(--brand); color: #fff; }
.btn--primary:hover { background: var(--brand-strong); }

/* Available, just not the main thing. Full text colour: --text-muted is what disabled controls
   use, and a live action wearing it reads as switched off. */
.btn--secondary {
  background: var(--surface-raised);
  border-color: var(--border);
  color: var(--text);
  font-size: var(--size-small);
}
.btn--secondary:hover { border-color: var(--brand-line); }

.btn--quiet { color: var(--text-muted); font-size: var(--size-small); }
.btn--quiet:hover { color: var(--text); }

/* Destructive is deliberately the quietest control here. A full-width outlined red button under
   the primary reads as its peer, and the one irreversible action should not compete for the tap.
   It fills only once armed, when the fill is the warning. */
.btn--danger { color: var(--danger); border-color: transparent; font-size: var(--size-small); }
.btn--danger:hover { border-color: var(--danger-line); }
.btn--danger--armed { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

/* ---- account / Google sign-in ---- */
.btn--google {
  background: #ffffff;
  color: #1f1f1f;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
}
.btn--google::before {
  content: '';
  width: 18px; height: 18px;
  /* Google 'G', inlined as a data URI so the CSP-safe bundle needs no external asset. */
  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="%234285F4" d="M45 24c0-1.6-.1-3.1-.4-4.6H24v9.1h11.8c-.5 2.7-2 5-4.3 6.6v5.5h7C42.6 36.7 45 30.9 45 24z"/><path fill="%2334A853" d="M24 46c5.8 0 10.6-1.9 14.2-5.2l-7-5.5c-1.9 1.3-4.4 2.1-7.2 2.1-5.5 0-10.2-3.7-11.9-8.7H5v5.6C8.6 41.1 15.7 46 24 46z"/><path fill="%23FBBC05" d="M12.1 28.7c-.4-1.3-.7-2.7-.7-4.7s.3-3.4.7-4.7v-5.6H5C3.1 17.1 2 20.4 2 24s1.1 6.9 3 9.3l7.1-4.6z"/><path fill="%23EA4335" d="M24 11.4c3.1 0 5.9 1.1 8.1 3.2l6-6C34.5 5.1 29.8 3 24 3 15.7 3 8.6 7.9 5 15.3l7.1 5.6c1.7-5 6.4-8.7 11.9-8.7z"/></svg>') center/contain no-repeat;
}

/* ---- habit grid chart ----
   Rows are habits, columns are days. Empty cells read as an unfilled grid rather than as misses,
   which is the difference between a chart with no data yet and a chart full of failures. */
.grid-chart {
  display: grid;
  grid-template-columns: auto repeat(var(--cols), 1fr);
  gap: 3px;
  align-items: center;
}
.grid-chart__name { display: inline-flex; padding-right: 4px; }
.grid-chart__name .glyph-icon svg { width: 16px; height: 16px; }
.grid-cell {
  aspect-ratio: 1;
  border-radius: 3px;
  background: var(--surface-raised);
}
.grid-cell.is-done { background: var(--mint); }
/* A day the habit was never due is not a gap in the record. */
.grid-cell.is-rest { background: none; box-shadow: inset 0 0 0 1px var(--border); }

/* What counts as done, in the user's own words. Sits with the name because that is the question
   it answers; nothing verifies it, so it never wears the tone of a measured value. */
.quest__target { font-weight: 400; color: var(--text-muted); font-size: var(--size-small); }

.empty { gap: 8px; }
.empty .card__value { font-size: var(--size-body); }

/* ---- scrollbars ----
   Gone. A bar drawn beside the content is the desktop answer to "is there more?", and on a phone it
   is a grey streak that appears mid-gesture and vanishes. The edge fades below answer the same
   question without drawing anything that is not content. */
.quests,
#screen-journey,
#screen-you,
.sheet {
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;   /* momentum, so a flick coasts instead of stopping dead */
}
.quests::-webkit-scrollbar,
#screen-journey::-webkit-scrollbar,
#screen-you::-webkit-scrollbar,
.sheet::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ---- scroll edges ----
   Without a scrollbar, a row cut in half at the container edge is the only thing saying "there is
   more", and a hard cut reads as a rendering fault. The mask dissolves it instead, and each side is
   only applied when there is actually something past it — `is-top`/`is-bot` are set from the scroll
   position, so a list that fits or is scrolled to the end has no fade at all. */
/* Registered so they interpolate — an unregistered custom property jumps between values, which is
   the popping the fade exists to remove. */
@property --fade-top { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --fade-bot { syntax: '<length>'; inherits: false; initial-value: 0px; }
.scroll-fade { --fade-top: 0px; --fade-bot: 0px; }
.scroll-fade.is-top { --fade-top: 22px; }
.scroll-fade.is-bot { --fade-bot: 26px; }
.scroll-fade {
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bot)), transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bot)), transparent 100%);
  transition: --fade-top var(--t-ui) linear, --fade-bot var(--t-ui) linear;
}

img.welcome__mark { border-radius: 22px; display: block; margin: 0 auto; }

/* ---- awards ----
   Earned ones are shown as a filled mark; the ladder rungs below show how near the next is, which
   is the part that brings someone back. Nothing here renders an award you have not earned as a
   greyed-out medal: a wall of locked things is a list of failures. */
.awards { gap: 12px; }
.awards__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(74px, 1fr));
  gap: 14px 6px;
}
/* Earned awards are the only place in the app that gets to be colourful, and each disc wears the
   colour that already means its thing: the streak flame for streaks, mint for kept, violet for
   mind, indigo for the plain count. Seven greyish pills said nothing; seven coloured discs are a
   shelf. */
.award {
  display: grid; justify-items: center; gap: 6px;
  text-align: center;
  --award-hue: var(--award-brand);
}
.award[data-tier='streak'], .award[data-tier='return'] { --award-hue: var(--flame); }
.award[data-tier='growth'], .award[data-tier='glade']  { --award-hue: var(--mint); }
.award[data-tier='perfect']                            { --award-hue: var(--violet); }
.award__mark {
  width: 54px; height: 54px;
  display: grid; place-items: center;
  border-radius: 50%;
  color: var(--award-ink);
  /* One light source, top-left, so the disc reads as a struck medal rather than a flat circle. */
  background:
    radial-gradient(circle at 32% 26%, rgba(255, 255, 255, 0.34), transparent 58%),
    var(--award-hue);
  box-shadow: 0 3px 10px -2px color-mix(in srgb, var(--award-hue) 55%, transparent);
}
.award__mark svg { width: 26px; height: 26px; }
.award__name {
  font-size: 0.8125rem; font-weight: 700; line-height: 1.25;
  color: var(--text);
}

.award-next { display: grid; gap: 4px; padding: 4px 0; }
.award-next__row { display: flex; justify-content: space-between; gap: 8px; font-size: var(--size-small); }
.award-next__name { color: var(--text); font-weight: 700; }
.award-next__count { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.award-next__track {
  height: 6px; border-radius: 999px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  overflow: hidden;
}
.award-next__fill {
  display: block; height: 100%; width: 100%;
  transform-origin: left center;
  background: var(--brand);
  transition: transform var(--t-ui) var(--ease-out);
}

/* Offline is a statement of fact, not a warning: the work is saved either way. */
.offline {
  flex: none;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  color: var(--text-muted);
  font-size: var(--size-small);
}
.offline[hidden] { display: none; }
