/* LUMA - resets, orbs, splash, bottom nav, tab views, pull-to-refresh, cards
   Sheet 2 of 9. Source order decides the cascade - see the link tags in luma.html. */

/* ============================================================
   GLOBAL RESETS & BASE
   ============================================================ */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: transparent;

  /* An app, not a page. Two separate giveaways, and they need two properties:
     `user-select` kills the blue selection and iOS's magnifying loupe on a
     long-press, and `-webkit-touch-callout` kills the "Save Image / Copy" sheet
     that a long-press raises over the mascots and the 42 movement photos.
     Setting one without the other leaves half the tell in place.
     Applied on `*` rather than `body` because both inherit, but a UA stylesheet
     sets its own value on inputs and buttons — the override below is what puts
     selection back exactly where she needs it. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* The exception, and the whole of it: anywhere she types. A field she cannot
   select inside is a field she cannot correct — on WebKit, `user-select: none`
   on an ancestor reaches into the input and breaks the caret, so this is
   load-bearing, not tidiness. Keep `input` and `textarea` here if either is
   ever restyled. */
input, textarea {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

html, body {
  height: 100%;
  overscroll-behavior: none;
  /* No pinch-zoom and no double-tap-zoom. `pan-x pan-y` still allows scrolling in
     BOTH directions, which the calendar strip, the story rings and the workout
     history strip all need — `pan-y` alone would kill them. This is the layer that
     works in Safari 13+; the viewport meta and the gesture guard cover the rest.
     See "The app is not zoomable" in CLAUDE.md. */
  touch-action: pan-x pan-y;
}

/* Stops iOS enlarging text on its own when the phone is turned to landscape. That
   is a zoom Maiko never asked for and cannot undo from inside the app. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

/* No scrollbars, anywhere. This is a phone app, and a phone does not show one:
   iOS and Android draw an overlay bar that fades on its own — measured
   2026-09-16, 0px of gutter on a 390px screen. A DESKTOP browser is what the
   rule is for. Chrome on Windows takes a permanent 15px strip down the
   right-hand edge of something meant to read as a phone, and it lands in every
   screenshot of this app taken on the PC.

   Both syntaxes are load-bearing: Firefox only understands scrollbar-width,
   Chrome and Safari only the ::-webkit pseudo-element. The horizontal strips —
   the calendar, the story rings, the 14-day workout scroller — fall under the
   same rule; a bar under a row of tiles meant to be swiped is worse than the
   one down the page.

   Nothing here stops anything SCROLLING. Only the indicator goes, and on a
   touch screen the content moving under the finger is the indicator. */
* {
  scrollbar-width: none;            /* Firefox */
  -ms-overflow-style: none;         /* old Edge */
}
*::-webkit-scrollbar {              /* Chrome, Safari */
  width: 0;
  height: 0;
  display: none;
}

body {
  font-family: var(--font-ka);
  background: var(--bg);
  color: var(--text-1);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.5;
}

/* Chrome draws a light spinner box inside number inputs — never right here. */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type=number] { -moz-appearance: textfield; appearance: textfield; }

::selection { background: var(--lime-dim); color: var(--text-1); }

/* Number spans — always Inter */
.num {
  font-family: var(--font-num);
  font-variant-numeric: tabular-nums;
}

/* App shell */
#app {
  max-width: 480px;
  margin: 0 auto;
  min-height: 100dvh;
  position: relative;
  /* `overflow: hidden` here made #app a SCROLL CONTAINER, which is what silently
     killed `position: sticky` on the day-tab header — the header stuck to #app's
     own box, and #app scrolls away with the page, so it behaved exactly like a
     static element. Measured: at scrollY 420 the bar sat at y = -400.

     `clip` clips identically but does NOT create a scroll container, so sticky
     resolves against the viewport as intended. The `hidden` line above it is the
     fallback for anything that predates `overflow: clip` (Safari < 16): those
     browsers keep today's behaviour — no sticky header, but no stray horizontal
     scroll from the drifting orbs either, which is what this rule is for. */
  overflow: hidden;
  overflow: clip;
  background: var(--bg);
}

/* ============================================================
   ANIMATED GRADIENT BACKGROUND (orbs)
   ============================================================ */
#orb-layer {
  position: fixed;
  inset: 0;
  max-width: 480px;
  margin: 0 auto;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

.orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(100px);
  opacity: 0;
  animation: orbDrift var(--orb-dur) ease-in-out infinite;
  animation-delay: var(--orb-delay);
  will-change: transform, opacity;
  /* InBody day recolours the orbs; the class flip needs a transition to animate. */
  transition: background 800ms var(--ease-in-out), opacity 800ms var(--ease-in-out);
}

.orb-1 {
  width: 380px;
  height: 380px;
  background: var(--orb-1);
  opacity: 0.13;
  top: -80px;
  right: -100px;
  --orb-dur: 18s;
  --orb-delay: 0s;
  --dx: -40px;
  --dy: 60px;
}

.orb-2 {
  width: 320px;
  height: 320px;
  background: var(--orb-2);
  opacity: 0.10;
  bottom: 200px;
  left: -120px;
  --orb-dur: 22s;
  --orb-delay: -7s;
  --dx: 50px;
  --dy: -40px;
}

.orb-3 {
  width: 280px;
  height: 280px;
  background: var(--orb-3);
  opacity: 0.08;
  bottom: -60px;
  right: -60px;
  --orb-dur: 26s;
  --orb-delay: -14s;
  --dx: -30px;
  --dy: -50px;
}

@keyframes orbDrift {
  0%   { transform: translate(0, 0) scale(1); }
  33%  { transform: translate(var(--dx), var(--dy)) scale(1.08); }
  66%  { transform: translate(calc(var(--dx) * -0.5), calc(var(--dy) * 0.8)) scale(0.96); }
  100% { transform: translate(0, 0) scale(1); }
}

body.inbody-day .orb-1 { background: var(--amber); opacity: 0.15; }
body.inbody-day .orb-2 { background: var(--red);   opacity: 0.10; }
body.inbody-day .orb-3 { background: #FF8C42;      opacity: 0.08; }

/* ============================================================
   SPLASH SCREEN
   ============================================================ */
#splash {
  position: fixed;
  inset: 0;
  z-index: 100;
  /* Black from the very first painted frame, and it stays black until the orb
     has drawn. This is step 0 of the sequence, not a fallback — it is also the
     colour the orb swells up OUT OF. */
  background: #000;
  overflow: hidden;
  /* The orb and everything around it live in .splash-shell, which is the app's own
     480px column — so this only ever centres that column and paints the black
     either side of it on a desktop window. */
  display: flex;
  justify-content: center;
  transition: opacity 0.85s var(--ease-out);
}

#splash.hidden {
  opacity: 0;
  pointer-events: none;
}

/* ---- The phone-shaped column ------------------------------------
   The splash used to be full-bleed while every screen behind it is a 480px
   column (rule 6), so on a desktop the photo ran the whole window and cropped to
   a band across the middle of the runner — her head gone, the headline landing on
   her hip — and then the app snapped into a narrow column the moment it opened.

   Same shell as the app now: at any phone width this is the full screen and
   nothing changes, and on a desktop it is a phone-shaped window on black, which
   is what the rest of LUMA already looks like there.

   It owns the `overflow: hidden` that clips the orb when it swells on the way
   out, so it floods the column and never the black gutters. */
.splash-shell {
  position: relative;
  width: 100%;
  max-width: 480px;
  height: 100%;
  overflow: hidden;
  /* 2026-09-23 — the photo is gone; the ground is the app's own --bg with the
     faintest lime warmth where the orb sits, so the orb's glow has somewhere to
     land rather than stopping dead at black. It is a ground, not a light: it
     must never be read as a second glowing thing. */
  background: radial-gradient(120% 60% at 50% 34%, #11130E 0%, var(--bg) 62%);
}

/* ---- The gate ---------------------------------------------------
   Every animated piece carries .sp-anim, and the whole sequence is held at its
   from-state until #splash gains `ready` — which the boot script adds once the
   orb has drawn its first frame, not when the page loaded (2026-09-23 — until
   then this waited on the photo decoding).

   Without this the orb's swell runs against an empty box on a cold load and
   the ball snaps in somewhere near the end of it, out of step with everything
   timed to follow it. A paused animation does not advance its delay either, so
   the whole timeline genuinely starts from zero at `ready`.

   It cannot deadlock: the script adds `ready` after the first frame, on a phone
   with no WebGL (the CSS orb), and behind a timeout — the button is the only
   door into the app. */
#splash:not(.ready) .sp-anim { animation-play-state: paused; }

/* ---- The orb (2026-09-23) --------------------------------------
   Replaced the runner photo, from the reel Giorgi sent: one glowing, liquid ball
   in LUMA's colours. It is a WebGL drawing (createOrb() in js/12-init.js), and on
   the way out it flares and swells until it covers the screen.

   THREE BOXES, and each owns one transform, because an animation's transform
   beats an inline one and the two would fight:
     .splash-stage    takes the free height between the wordmark and the headline
     .splash-orb      the entrance (spOrbIn), held by the .sp-anim gate
     .splash-orb-grow the exit — js writes its scale() inline, measured

   The canvas is ~1.5x the ball: the rest is room for the glow and the wobble.
   The shader fades the glow to nothing before the canvas edge, so there is no
   box to see on a dark ground. */
.splash-stage {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.splash-orb {
  /* the vh cap keeps the bubble clear of the headline on a short phone
     (SE: 667px). Round 2 is ~20% bigger than round 1's 360px / 46vh */
  /* +56px: the box may run into the 28px side gutters. Only the bloom lives
     out there, and the shader fades it to nothing before the canvas edge */
  width: min(calc(100% + 56px), 420px, 50vh);
  flex-shrink: 0;            /* or the stage squeezes it back inside the gutters */
  aspect-ratio: 1;
  position: relative;
  z-index: 3;                /* over the text when it swells */
  animation: spOrbIn var(--sp-dur-orb) var(--ease-out) var(--sp-in-orb) both;
}
.splash-orb-grow {
  width: 100%;
  height: 100%;
  will-change: transform;
}
.splash-orb canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* No WebGL: the same bubble in gradients — a glowing lime/teal rim round a
   see-through middle, one soft wisp inside. Still next to the real one, but it
   breathes, glows, and still flares and swells on the way out. */
.splash-orb-css {
  position: absolute;
  inset: 15%;
  border-radius: 50%;
  border: 2px solid rgba(201, 245, 59, 0.75);
  border-bottom-color: rgba(0, 221, 179, 0.8);
  border-left-color: rgba(0, 221, 179, 0.55);
  background:
    radial-gradient(ellipse 55% 22% at 48% 34%, rgba(201, 245, 59, 0.34), rgba(201, 245, 59, 0) 70%),
    radial-gradient(ellipse 30% 40% at 60% 64%, rgba(0, 221, 179, 0.26), rgba(0, 221, 179, 0) 70%),
    radial-gradient(circle, rgba(10, 10, 12, 0.2) 55%, rgba(0, 221, 179, 0.14) 100%);
  box-shadow: 0 0 46px rgba(201, 245, 59, 0.22), 0 0 90px rgba(0, 221, 179, 0.12),
              inset 0 0 30px rgba(0, 221, 179, 0.22);
  animation: spBreathe var(--sp-dur-breathe) var(--ease-in-out) infinite;
}
.splash-orb-css::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #E3FA8F;
  opacity: 0;
  transition: opacity var(--sp-dur-ignite) var(--ease-out);
}
.splash-orb.is-ignited .splash-orb-css::after { opacity: 1; }
@keyframes spBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.025); }
}

/* On the way out: everything but the orb steps back.
   filter, NOT opacity: every one of these carries an entrance animation with
   fill-mode both, and an animation's held opacity beats any rule — an opacity
   fade here would be dead CSS. None of the entrances touches filter. */
.splash-inner > :not(.splash-stage) {
  transition: filter var(--dur-mid) var(--ease-out);
}
.splash-inner.is-leaving > :not(.splash-stage) {
  filter: opacity(0);
}

/* ---- The content column -----------------------------------------
   Fills .splash-shell, which is already the 480px column — so the text and the
   photo are cropped to the same box and cannot drift apart from each other. */
.splash-inner {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  padding: calc(28px + env(safe-area-inset-top, 0px)) 28px
           calc(28px + env(safe-area-inset-bottom, 0px));
}

/* The wordmark. Wide tracking leaves a trailing space after the A that pulls the
   centred word off its optical centre; the matching padding-left puts it back —
   the same pairing the old 32px logo used, and for the same reason. */
.splash-mark {
  align-self: center;
  font-family: var(--font-num);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 4px;
  padding-left: 4px;
  color: #FFF;
  opacity: 0.5;
  animation: spDrop var(--sp-dur-step) var(--ease-out) var(--sp-in-mark) both;
}

/* ---- The headline -----------------------------------------------
   The one place in this app that uses a serif, and the one place --font-ka-serif
   may be referenced. See the note on that variable before reusing it. */
.splash-line {
  font-family: var(--font-ka-serif);
  font-size: 46px;
  font-weight: 500;
  line-height: 1.06;
  letter-spacing: -0.01em;
  color: #FFF;
}

/* 2026-09-23 — the two lines arrive from opposite edges (CargoX), not rising in
   place. Same start times, so the opening is exactly as long as it was; only the
   travel is longer, and --ease-out lands most of it in the first ~300ms. */
.splash-line-1 {
  animation: slideInL var(--dur-slide) var(--ease-out) var(--sp-in-line1) both;
}

/* 150ms behind the line above — the stagger IS the difference between these two
   delays, so move them together. */
.splash-line-2 {
  animation: slideInR var(--dur-slide) var(--ease-out) var(--sp-in-line2) both;
}

/* Fade only, no slide: a third thing rising in the same column reads as a queue
   rather than as a sentence arriving. */
/* ---- The wipe out of the splash (2026-09-22) ----
   One lime plane, clip-path only. It is 101 so it sits over #splash (100) and can
   still be on screen after the splash is gone; it only ever runs on the path that
   lands straight on the day screen, so nothing else is up to compete with it.

   clip-path rather than a transform or a width: inset(... round r) interpolates,
   so one property takes a pill at the bottom of the screen all the way to a
   36px circle in the header without ever detaching the shape from its own edges.
   The steps are set inline by playSplashWipe() because the last one is MEASURED
   from where the header button actually is. */
/* 2026-09-23 — the plane is the APP COLUMN, not the window. It was inset: 0, and on
   a desktop the lime flooded a 2000px-wide screen around a 480px app (Giorgi's
   screenshot). Every full-screen motion in this app is phone-shaped: the same
   480px column as #app, centred. On a phone this is exactly the old full screen. */
#splash-wipe {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 50%;
  width: min(100%, 480px);
  transform: translateX(-50%);
  z-index: 101;
  background: var(--lime);
  clip-path: inset(100% 0 0 0);
  pointer-events: none;
  will-change: clip-path;
}
#splash-wipe.gone { display: none; }

.splash-squiggle {
  display: block;
  width: 132px;
  height: 14px;
  margin-top: 12px;
  overflow: visible;        /* the round caps sit just outside the box */
}
.splash-squiggle path {
  fill: none;
  stroke: var(--lime);
  stroke-width: 3.5;
  stroke-linecap: round;
  /* 190 is comfortably longer than the path, so one value hides all of it. */
  stroke-dasharray: 190;
  stroke-dashoffset: 190;
  animation: spSquig var(--sp-dur-squig) var(--ease-out) var(--sp-in-squig) both;
}
@keyframes spSquig { to { stroke-dashoffset: 0; } }

.splash-tag {
  font-family: var(--font-ka);
  font-size: 14px;
  font-weight: 400;
  line-height: 1.5;
  margin-top: 14px;
  color: rgba(255, 255, 255, 0.45);
  animation: spFade var(--sp-dur-step) var(--ease-out) var(--sp-in-tag) both;
}

/* ---- The launch CTA ---------------------------------------------
   Full width inside the 28px gutters. It is still the ONLY way off the splash —
   there is no auto-dismiss and no tap-anywhere.

   The selector repeats .btn-primary on purpose: that class sets its own
   `box-shadow: 0 0 0 0` further down the file, so a bare .splash-btn would lose
   on source order at equal specificity and the pulse would be dead CSS. */
.btn-primary.splash-btn {
  width: 100%;
  margin-top: 30px;
  padding: 17px 24px;
  font-size: 16px;
  /* Two animations, not one: the spring entrance, then the single glow pulse
     behind it. They touch different properties (transform/opacity vs box-shadow)
     so neither overwrites the other. */
  animation: spPop var(--sp-dur-btn) var(--ease-spring) var(--sp-in-btn) both,
             spPulse var(--sp-dur-pulse) var(--ease-in-out) var(--sp-in-pulse) both;
}

.btn-primary.splash-btn:active {
  transform: scale(0.96);
}

@keyframes spFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The orb rises out of the dark: a swell from 0.86, so it arrives as a thing
   growing rather than a picture fading in. */
@keyframes spOrbIn {
  from { opacity: 0; transform: scale(0.86); }
  to   { opacity: 1; transform: scale(1); }
}

/* Enters from 8px ABOVE its resting place and drops into it. */
@keyframes spDrop {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* The side entry, shared by the splash, the streak page and the finish screen.
   Here in the base sheet so every later sheet can use it. */
@keyframes slideInL {
  from { opacity: 0; transform: translateX(calc(-1 * var(--slide-dist))); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes slideInR {
  from { opacity: 0; transform: translateX(var(--slide-dist)); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes spRise {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes spPop {
  from { opacity: 0; transform: scale(0.88); }
  to   { opacity: 1; transform: scale(1); }
}

/* Up and back down once. Both ends are the resting state, so the button settles
   exactly where it started rather than keeping a halo. */
@keyframes spPulse {
  0%   { box-shadow: 0 0 0 0 var(--lime-pulse); }
  45%  { box-shadow: 0 0 20px 0 var(--lime-pulse); }
  100% { box-shadow: 0 0 0 0 var(--lime-pulse); }
}

/* ============================================================
   BOTTOM NAVIGATION
   ============================================================ */
#bottom-nav {
  position: fixed;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 480px;
  z-index: 50;
  background: rgba(14, 14, 18, 0.85);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-top: 1px solid var(--border);
  padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  justify-content: space-around;
}

.nav-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  cursor: pointer;
  /* Equal-width tabs. The indicator width is measured from the active button, so a
     label-sized button gave the lime bar a different width on every tab. */
  flex: 1 1 0;
  min-width: 0;
  padding: 6px 4px;
  position: relative;
  transition: transform var(--dur-fast) var(--ease-spring);
}

.nav-btn:active {
  transform: scale(0.92);
}

.nav-icon {
  width: 22px;
  height: 22px;
  stroke: var(--text-3);
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke var(--dur-mid) var(--ease-out),
              transform var(--dur-mid) var(--ease-spring);
}

.nav-btn.active .nav-icon {
  stroke: var(--lime);
}

.nav-label {
  font-family: var(--font-ka);
  font-size: 10px;
  font-weight: 500;
  color: var(--text-3);
  transition: color var(--dur-mid) var(--ease-out);
  white-space: nowrap;
}

.nav-btn.active .nav-label {
  color: var(--lime);
  font-weight: 600;
}

/* Sliding lime underline — left/width set by JS from getBoundingClientRect */
#nav-indicator {
  position: absolute;
  top: 0;
  height: 2px;
  background: var(--lime);
  border-radius: 0 0 2px 2px;
  transition: left var(--dur-mid) var(--ease-in-out),
              width var(--dur-mid) var(--ease-in-out);
  box-shadow: 0 0 8px var(--lime-glow);
}

/* ============================================================
   TAB VIEWS
   ============================================================ */
#main-content {
  position: relative;
  z-index: 1;
  padding-bottom: calc(72px + env(safe-area-inset-bottom));
  min-height: 100dvh;
}

.tab-view {
  display: none;
  padding: var(--pad);
  animation: tabIn var(--dur-mid) var(--ease-out) both;
}

.tab-view.active-view {
  display: block;
}

/* A tab fading out must not keep contributing its height, or for those 280ms
   the document is BOTH tabs tall and the scrollbar visibly shrinks and jumps on
   every single tab tap. Measured before this: day 2528px, settings 1895px,
   mid-switch 4287px.
   #app is already position:relative, so absolute anchors correctly. If she is
   scrolled down when she switches, the leaving tab lands above the viewport and
   simply disappears instead of fading — no worse than hiding it outright, and
   the common case (at the top) keeps the crossfade exactly as it was. */
.tab-view.tab-leaving {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  pointer-events: none;
}

/* The page behind a sheet or a full-screen overlay must not scroll. Without it
   the scroll indicator on the right reports the hidden page rather than what
   she is actually looking at, and a drag near the edge scrolls the wrong thing.
   Applied to both elements: either alone leaves iOS a way through. */
html.scroll-locked,
html.scroll-locked body {
  overflow: hidden;
}

/* ...and it must also HOLD ITS PLACE. `overflow: hidden` alone makes the
   document non-scrollable, and the browser then clamps its scroll offset to
   zero — measured 2026-09-16: scrolled to 545px, opened the calories sheet,
   scrollY read 0 while it was open and stayed 0 after it closed. The page flew
   to the top behind the sheet (the backdrop is only 60% opaque, so she sees it)
   and she was back at the top of the day tab when she came out.

   Pinning the body at its own negative offset is the fix: nothing moves on the
   way in, and unlockPageScroll() puts the scroll back on the way out.
   `--lock-y` is written by lockPageScroll() and is already negative. */
html.scroll-locked body {
  position: fixed;
  top: var(--lock-y, 0px);
  left: 0;
  right: 0;
  width: 100%;
}

/* ============================================================
   PULL TO REFRESH (2026-09-16)
   ============================================================
   An installed PWA has NO browser chrome — no address bar, no reload button,
   no native pull-to-refresh (this file sets `overscroll-behavior: none`, which
   kills it, and iOS gives a standalone app none of the three anyway). So when
   sw.js announced a new version there was literally no way to take it from
   inside the app: S.pwaUpdate had to tell her to close and reopen it.

   This is that missing gesture. It is drawn as one indicator that rides down
   from under the top edge as she pulls; past the threshold it spins and the
   page reloads.

   No text, deliberately — an arrow needs no words, and no words means no new
   Georgian to proofread. */
#ptr {
  position: fixed;
  top: 0;
  left: 50%;
  /* 120: over the splash (100), because the pull works THERE too since
     2026-09-16 and an indicator hidden behind the screen it belongs to is no
     indicator. Still under the sheets (150) and the toasts (200) — pulling is
     blocked whenever one of those is up, so it has no business over them. */
  z-index: 120;
  width: 34px;
  height: 34px;
  margin-left: -17px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  /* Parked just off the top edge. --ptr-y is written by the drag, in px. */
  transform: translate3d(0, calc(var(--ptr-y, 0px) - 46px), 0)
             rotate(var(--ptr-rot, 0deg));
  opacity: var(--ptr-op, 0);
}

/* While a finger is down the indicator must track it 1:1 with no transition, or
   it lags behind the pull and the gesture stops feeling direct. The transition
   is only for the release — the spring back, or the settle before the reload. */
#ptr.ptr-release {
  transition: transform var(--dur-mid) var(--ease-out),
              opacity var(--dur-mid) var(--ease-out);
}

#ptr svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: var(--lime);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Armed: past the threshold, so letting go will reload. The lime ring is the
   only signal she gets that the gesture has taken. */
#ptr.ptr-armed {
  border-color: var(--lime);
  background: var(--lime-dim);
}

#ptr.ptr-spin svg { animation: ptrSpin 0.7s linear infinite; }

@keyframes ptrSpin {
  to { transform: rotate(360deg); }
}

@keyframes tabIn {
  from { opacity: 0; transform: translateX(12px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes tabOut {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(-12px); }
}

/* ============================================================
   REUSABLE UI COMPONENTS — Cards
   ============================================================ */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  position: relative;
  overflow: hidden;
}

.card-sm { padding: 12px; border-radius: var(--radius-sm); }
.card-lg { padding: 20px; border-radius: var(--radius-lg); }

/* Lime accent left border variant */
.card-accent {
  border-left: 2px solid var(--lime);
}

/* Glass variant — used for coaching/modal overlays */
.card-glass {
  background: rgba(28, 28, 34, 0.72);
  backdrop-filter: blur(24px);
  -webkit-backdrop-filter: blur(24px);
  border: 1px solid rgba(255, 255, 255, 0.10);
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-ka);
  font-size: 15px;
  font-weight: 600;
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring),
              box-shadow var(--dur-fast) var(--ease-out),
              background var(--dur-mid) var(--ease-out);
  -webkit-tap-highlight-color: transparent;
}

.btn:active { transform: scale(0.96); }

.btn-primary {
  background: var(--lime);
  color: #0A0A0C;
  padding: 14px 24px;
  box-shadow: 0 0 0 0 var(--lime-glow);
}

.btn-primary:active {
  transform: scale(0.96);
  box-shadow: 0 0 20px var(--lime-glow);
}

/* Ghost — border only */
.btn-ghost {
  background: transparent;
  color: var(--text-1);
  border: 1px solid var(--border-2);
  padding: 13px 24px;
}

.btn-ghost:active {
  background: var(--surface-2);
}

/* Icon button */
.btn-icon {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 2026-09-24: every button shows the hand on a desktop. The back arrows, the
   close x's and .btn-icon never set it. :where() carries ZERO specificity, so any
   class that says cursor: default (.pd-mini, .settings-row.is-static) still wins. */
:where(button:not(:disabled), [role="button"]) { cursor: pointer; }

/* The მეტი door, lime, with a drawn "+" (2026-09-24, from Giorgi's "+ More Info"
   reference). One class for all three: the score card, the weight card on the
   progress tab, and the workout history. Right-aligned, sized to its label. */
.more-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;              /* full width, 48px tall — Giorgi: "the click should feel sensible" */
  min-height: 48px;
  margin: 14px 0 0;
  padding: 12px 18px;
  background: var(--lime);
  color: #0A0A0C;
  border: 0;
  border-radius: 14px;
  font-family: var(--font-ka);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 16px var(--lime-glow);
  transition: transform var(--dur-fast) var(--ease-spring),
              box-shadow var(--dur-fast) var(--ease-out);
}
.more-pill:active { transform: scale(0.95); box-shadow: 0 0 20px var(--lime-glow-2); }
.more-pill svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linecap: round;
}

/* The solid soft button (2026-09-24). What used to be coloured text — the InBody
   card's full-analysis link, its cycle-summary door — keeps its colour but gets a
   body: a tinted fill, a rim of the same colour, a real height. --btn-c is a
   triplet so one class serves lime and teal. */
.btn-soft {
  --btn-c: var(--lime-rgb);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  min-height: 46px;
  padding: 11px 16px;
  background: rgba(var(--btn-c), 0.14);
  border: 1px solid rgba(var(--btn-c), 0.32);
  border-radius: 14px;
  color: rgb(var(--btn-c));
  font-family: var(--font-ka);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring),
              background var(--dur-fast) var(--ease-out);
}
.btn-soft:active { transform: scale(0.97); background: rgba(var(--btn-c), 0.24); }
.btn-soft.is-teal { --btn-c: var(--teal-rgb); }

/* A full page slides in from the right and back out to the right (2026-09-24,
   Giorgi's pick). The sheets used to rise 30px and fade; closing removed the node
   in one frame. .page-leaving is set by pageOut() in js/06-progress.js. */
@keyframes pageIn {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
@keyframes pageOut {
  from { transform: translateX(0); }
  to   { transform: translateX(100%); }
}
.modal-bg.page-leaving {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-page-out) var(--ease-in-out);
}

/* Full width */
.btn-full { width: 100%; }

/* Small */
.btn-sm {
  font-size: 13px;
  padding: 10px 18px;
}

/* ---------- Progress bars ---------- */
.progress-bar {
  height: 6px;
  background: var(--surface-3);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--teal);
  transition: width 600ms var(--ease-spring);
  position: relative;
}

/* Lime variant */
.progress-fill-lime { background: var(--lime); }

/* Shimmer on progress bars */
.progress-fill::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 40px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
  animation: shimmer 2s ease-in-out infinite;
}

@keyframes shimmer {
  from { transform: translateX(-40px); }
  to   { transform: translateX(40px); }
}

/* ---------- Section headers ---------- */
.section-header > .widget-head-icon { margin-right: 8px; }

.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.section-title {
  font-family: var(--font-ka);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.section-action {
  font-family: var(--font-ka);
  font-size: 12px;
  color: var(--lime);
  background: none;
  border: none;
  cursor: pointer;
  font-weight: 500;
}

/* ---------- Tags / pills ---------- */
.tag {
  display: inline-flex;
  /* Bug fix: a flex container drops the whitespace between its items, so a
     label followed by a <span class="num"> rendered with no space between the
     word and the number. The gap restores it, and baseline alignment sits the
     Inter digits on the Georgian baseline instead of centring their boxes. */
  gap: 0.3em;
  align-items: baseline;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
}

.tag-lime  { background: var(--lime-dim);  color: var(--lime); }
.tag-teal  { background: var(--teal-dim);  color: var(--teal); }
.tag-amber { background: var(--amber-dim); color: var(--amber); }
.tag-red   { background: var(--red-dim);   color: var(--red); }
.tag-muted { background: var(--surface-3); color: var(--text-2); }

/* ---------- Toast notifications ---------- */
#toast-container {
  position: fixed;
  top: calc(16px + env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 200;
  width: calc(100% - 40px);
  max-width: 440px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}

.toast {
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--radius);
  padding: 12px 16px;
  font-size: 14px;
  color: var(--text-1);
  animation: toastIn 0.3s var(--ease-spring) both,
             toastOut 0.3s var(--ease-out) 2.7s both;
  display: flex;
  align-items: center;
  gap: 10px;
  backdrop-filter: blur(16px);
  /* The CONTAINER ignores taps so the app underneath stays usable; each toast
     takes them back. Without this the tap-to-dismiss on .toast-hold never
     fired — the click died on the container. */
  pointer-events: auto;
  /* The swipe is ours: without this the browser claims the vertical drag and
     the toast does not move. */
  touch-action: none;
  cursor: pointer;
}

.toast-lime  { border-left: 3px solid var(--lime); }
.toast-teal  { border-left: 3px solid var(--teal); }
.toast-amber { border-left: 3px solid var(--amber); }

@keyframes toastIn {
  from { opacity: 0; transform: translateY(-8px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes toastOut {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(-8px); }
}


/* ============================================================
   LAYER 1 PLACEHOLDERS
   ============================================================ */
.placeholder-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  text-align: center;
}

.placeholder-title {
  font-family: var(--font-ka);
  font-size: 18px;
  font-weight: 600;
  color: var(--text-1);
}

/* ---- The account choice (2026-09-24) -------------------------------
   Signed out, #splash carries .is-guest from boot and the two account buttons
   stand where the start button stands, arriving on its own entrance (spPop at
   --sp-in-btn, held by the same `#splash:not(.ready) .sp-anim` gate). Signed in,
   the screen is exactly the old one: the start button alone.
   It used to be a swap AFTER a tap on Start, which read as the screen having
   changed — Giorgi sent that back the same day. */
#splash.is-guest .btn-primary.splash-btn { display: none; }
.splash-acc {
  width: 100%;
  margin-top: 30px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  animation: spPop var(--sp-dur-btn) var(--ease-spring) var(--sp-in-btn) both;
}
.splash-acc[hidden] { display: none; }
.splash-acc.is-busy { opacity: 0.5; pointer-events: none; }
.splash-acc-btn { width: 100%; padding: 17px 24px; font-size: 16px; }
.splash-acc-btn:active { transform: scale(0.96); }
