/* ============================================================
   Hale — self-hosted Inter + Baloo 2 (no third-party font CDN; privacy-first)
   ============================================================ */
/* Baloo 2 — the rounded, bubbly display face that matches the H mark (variable: 400–800). */
@font-face {
  font-family: 'Baloo 2';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(fonts/baloo2-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-greek-ext.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/inter-400-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-greek-ext.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/inter-500-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-greek-ext.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/inter-600-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-cyrillic-ext.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-cyrillic.woff2) format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-greek-ext.woff2) format('woff2');
  unicode-range: U+1F00-1FFF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-greek.woff2) format('woff2');
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-vietnamese.woff2) format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/inter-700-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   Hale — design system
   ============================================================ */
:root {
  --font: 'Inter', system-ui, -apple-system, sans-serif;
  --font-display: 'Baloo 2', 'Inter', system-ui, sans-serif;   /* bubbly display face — echoes the rounded H mark */
  --bg:        #0a0c0b;
  --bg-2:      #0e1110;
  --panel:     #11150f00;
  --panel-solid:#10140f;
  --elev:      #141813;
  --elev-2:    #19201a;

  --green:     #4ade80;
  --green-2:   #3ecf8e;
  --green-3:   #86efac;
  --green-deep:#16a34a;
  --accent-rgb: 74 222 128;
  --on-accent: #04130a;
  --green-glow: rgb(var(--accent-rgb) / 0.35);
  --outline:   rgb(var(--accent-rgb) / 0.22);
  --outline-strong: rgb(var(--accent-rgb) / 0.42);

  --text:      #e8ece8;
  --text-dim:  #9aa69c;
  --text-faint:#828e84;   /* was #69756b: 3.11-3.97:1 on the dark surfaces it sits on, under the 4.5 AA floor */

  --radius:    18px;
  --radius-sm: 12px;
  --radius-lg: 26px;

  --sidebar-w: 272px;
  --sidebar-collapsed: 76px;

  --hairline: rgba(255,255,255,0.05);
  --shadow: 0 12px 40px rgba(0,0,0,0.45);
  --ease: cubic-bezier(.22,.61,.36,1);
}

* { box-sizing: border-box; }
/* Form controls don't inherit font-family by default — force every one onto the single app font. */
button, input, textarea, select, optgroup { font-family: var(--font); }
html, body { height: 100%; overflow-x: hidden; overscroll-behavior-x: none; }   /* clip the off-canvas drawer at the viewport — no side-to-side scroll */
body {
  margin: 0;
  background:
    radial-gradient(1200px 600px at 70% -10%, rgb(var(--accent-rgb) / 0.06), transparent 60%),
    radial-gradient(900px 500px at -10% 110%, rgb(var(--accent-rgb) / 0.05), transparent 55%),
    var(--bg);
  color: var(--text);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

svg.ic { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ============================================================ LAYOUT */
.app { display: flex; height: 100vh; padding: 10px; gap: 10px; }

/* ============================================================ SIDEBAR */
.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 auto;
  background: linear-gradient(180deg, var(--elev), var(--bg-2));
  border: 1px solid var(--outline);
  border-radius: var(--radius-lg);
  padding: 14px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  box-shadow: var(--shadow);
  transition: width .28s var(--ease), padding .28s var(--ease), opacity .2s var(--ease);
  position: relative;
  z-index: 30;
}
/* open ↔ closed sidebar — docked, pushes the main pane aside (no overlay) */
.app.closed { gap: 0; }
.app.closed .sidebar { width: 0; min-width: 0; padding-left: 0; padding-right: 0; border-width: 0; opacity: 0; overflow: hidden; pointer-events: none; }
#openSidebar { display: none; }
.app.closed #openSidebar { display: grid; }

.sidebar-top { display: flex; align-items: center; gap: 8px; padding: 4px 6px 8px; }
.brand { display: flex; align-items: center; gap: 10px; overflow: hidden; }
.brand-mark { display: flex; flex: 0 0 auto; }
.brand-mark svg { width: 32px; height: 32px; display: block; }
.brand-name {
  font-family: var(--font);
  font-weight: 700; font-size: 1.3125rem; letter-spacing: .5px;
  background: linear-gradient(90deg, var(--green-3), var(--green)); -webkit-background-clip: text; background-clip: text; color: transparent;
}

.icon-btn {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 11px;
  background: transparent; border: 1px solid transparent; color: var(--text-dim);
  cursor: pointer; transition: .18s var(--ease); flex: 0 0 auto;
}
.icon-btn:hover { background: var(--elev-2); color: var(--text); border-color: var(--outline); }
.icon-btn.listening { color: var(--green); border-color: var(--green); background: rgb(var(--accent-rgb) / 0.10); animation: micPulse 1.2s ease-in-out infinite; }
@keyframes micPulse { 0%,100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.35); } 50% { box-shadow: 0 0 0 5px rgb(var(--accent-rgb) / 0); } }

.new-chat {
  display: flex; align-items: center; gap: 12px;
  margin: 6px 2px 10px;
  padding: 11px 14px; border-radius: 14px;
  background: linear-gradient(180deg, rgb(var(--accent-rgb) / 0.14), rgb(var(--accent-rgb) / 0.06));
  border: 1px solid var(--outline-strong);
  color: var(--text); font-weight: 600; font-size: 0.875rem; cursor: pointer;
  transition: .18s var(--ease);
}
.new-chat:hover { box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.10); border-color: var(--green); transform: translateY(-1px); }
.new-chat .ic { width: 18px; height: 18px; color: var(--green); }

.nav { display: flex; flex-direction: column; gap: 3px; }
.nav-item {
  display: flex; align-items: center; gap: 13px;
  padding: 10px 13px; border-radius: 12px;
  background: transparent; border: 1px solid transparent;
  color: var(--text-dim); font-size: 0.875rem; font-weight: 500;
  cursor: pointer; text-align: left; width: 100%;
  transition: .15s var(--ease);
}
.nav-item:hover { background: var(--elev-2); color: var(--text); }
.nav-item.active {
  background: linear-gradient(90deg, rgb(var(--accent-rgb) / 0.12), transparent);
  border-color: var(--outline); color: var(--text);
}
.nav-item.active .ic { color: var(--green); }
.nav-item .ic { flex: 0 0 auto; }

.nav-section-label { color: var(--text-faint); font-size: 0.6875rem; font-weight: 600; letter-spacing: .8px; text-transform: uppercase; padding: 14px 14px 6px; transition: .2s var(--ease); }

.recents { flex: 1 1 auto; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; min-height: 0; }
.recents::-webkit-scrollbar { width: 8px; }
.recents::-webkit-scrollbar-thumb { background: var(--elev-2); border-radius: 8px; }
.recent {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 13px; border-radius: 10px; cursor: pointer;
  color: var(--text-dim); font-size: 0.8125rem; border: 1px solid transparent;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: .15s var(--ease);
}
.recent:hover { background: var(--elev-2); color: var(--text); }
.recent .ic { width: 15px; height: 15px; flex: 0 0 auto; opacity: .7; }

.sidebar-bottom { border-top: 1px solid var(--outline); padding-top: 8px; margin-top: 4px; display: flex; flex-direction: column; gap: 3px; }
.user { display: flex; align-items: center; gap: 11px; padding: 8px 10px; border-radius: 12px; background: transparent; border: 1px solid transparent; cursor: pointer; transition: .15s var(--ease); }
.user:hover { background: var(--elev-2); border-color: var(--outline); }
.user-meta { display: flex; flex-direction: column; line-height: 1.25; text-align: left; }
.user-name { font-size: 0.8125rem; font-weight: 600; color: var(--text); }
.user-plan { font-size: 0.6875rem; color: var(--green-2); }
.avatar { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; flex: 0 0 auto;
  background: linear-gradient(135deg, var(--green), var(--green-deep)); color: var(--on-accent); font-weight: 700; font-size: 0.875rem; border: none; cursor: pointer; }
.avatar.lg { width: 36px; height: 36px; }

/* ============================================================ MAIN */
.main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; background: linear-gradient(180deg, var(--bg-2), var(--bg)); border: 1px solid var(--hairline); border-radius: var(--radius-lg); overflow: hidden; }

.topbar { display: flex; align-items: center; gap: 12px; padding: 14px 20px; border-bottom: 1px solid var(--hairline); }
.model-pill { display: flex; align-items: center; gap: 9px; padding: 8px 14px; border-radius: 999px; background: var(--elev); border: 1px solid var(--outline); color: var(--text); font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: .15s var(--ease); }
.model-pill:hover { border-color: var(--outline-strong); }
.model-pill .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green-glow); }
.model-pill .chev { width: 16px; height: 16px; color: var(--text-dim); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 14px; }
.badge-privacy { display: flex; align-items: center; gap: 6px; font-size: 0.78125rem; font-weight: 600; color: var(--green-2); padding: 6px 12px; border-radius: 999px; border: 1px solid var(--outline); background: rgb(var(--accent-rgb) / 0.05); }
.badge-privacy .ic { width: 15px; height: 15px; }

/* overflow-x MUST be stated. Setting only overflow-y makes the other axis compute from
   `visible` to `auto` (CSS overflow spec), so this container quietly became sideways-scrollable:
   the landing's cinematic sections lean out on rotateY/translateX by design, and that turned into
   ~120px of real horizontal scroll on a phone. The html/body guard above doesn't help — this is
   the element that actually scrolls. */
.view { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; min-height: 0; }
.view::-webkit-scrollbar { width: 10px; }
.view::-webkit-scrollbar-thumb { background: var(--elev-2); border-radius: 8px; }

/* ============================================================ CHAT / HERO */
.chat-wrap { max-width: 820px; margin: 0 auto; padding: 24px 24px 0; height: 100%; display: flex; flex-direction: column; }
.hero { flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 30px 0 10px; }
.hero-tag {
  font-family: var(--font); font-weight: 600;
  font-size: clamp(28px, 4vw, 40px); line-height: 1.12; letter-spacing: -.4px; margin: 0;
  background: linear-gradient(100deg, var(--green-3) 0%, var(--green) 45%, var(--green-deep) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero-sub { color: var(--text-dim); font-size: 1rem; margin: 14px auto 0; max-width: 500px; line-height: 1.6; }

.suggestions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-top: 28px; }
.suggestion {
  text-align: left; padding: 16px; border-radius: var(--radius); cursor: pointer;
  background: var(--panel-solid); border: 1px solid var(--hairline);
  color: var(--text); transition: .18s var(--ease); display: flex; flex-direction: column; gap: 10px; min-height: 104px;
}
.suggestion:hover { border-color: var(--outline-strong); transform: translateY(-2px); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.06); }

/* messages */
/* overflow-x stated for the same reason as .view — declaring only overflow-y makes the other
   axis compute to `auto`, which made the whole chat pane swipeable sideways. */
.messages { display: flex; flex-direction: column; gap: 22px; padding: 16px 0 12px; flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; min-height: 0; }
.messages::-webkit-scrollbar { width: 10px; }
.messages::-webkit-scrollbar-thumb { background: var(--elev-2); border-radius: 8px; }
.msg { display: flex; gap: 14px; align-items: flex-start; }
.msg .m-avatar { width: 32px; height: 32px; border-radius: 9px; flex: 0 0 auto; display: grid; place-items: center; font-size: 0.8125rem; font-weight: 700; }
.msg.user .m-avatar { background: var(--elev-2); color: var(--text-dim); }
.msg.bot .m-avatar { background: linear-gradient(135deg, var(--green), var(--green-deep)); color: var(--on-accent); }
.msg.bot .m-avatar svg { width: 18px; height: 18px; stroke: var(--on-accent); fill: none; stroke-width: 1.8; }
/* A URL with no spaces in it cannot wrap on its own, and Research mode ends every answer with a
   Sources list of full URLs — so without this a single citation blew the chat pane out to twice
   its width. .m-col already carries min-width:0, so breaking the word is all that was missing. */
.m-body { padding-top: 4px; font-size: 0.9375rem; line-height: 1.65; color: var(--text); overflow-wrap: break-word; }
/* Same reasoning for the other surfaces that render model or user text. Now that .view clips
   horizontally, an unbreakable token would be silently cut off instead of escaping — wrapping is
   strictly better. Every selector here was checked against the markup before being added. */
.twin-list li, .card-sub, .stat-name, .mk-val, .reach-steps li, .cp-name { overflow-wrap: break-word; }
/* A flex child defaults to min-width:auto, which refuses to shrink below its longest word —
   so overflow-wrap alone does nothing inside a flex row. (It worked for .m-body only because
   .m-col already set min-width:0.) These two hold user- and model-supplied text: a memory can
   be 240 chars and may contain a URL, a marker name 40. Without this they were silently cut off
   rather than wrapped. `anywhere` is the variant that also lowers the min-content contribution. */
.mem-item > span { min-width: 0; overflow-wrap: anywhere; }
.mk-name, .mk-val { min-width: 0; overflow-wrap: anywhere; }
.m-body p { margin: 0 0 10px; }
.m-body p:last-child { margin-bottom: 0; }
.m-body strong { color: var(--green-3); font-weight: 600; }
.m-body em { color: var(--text); font-style: italic; }
.m-body ul { margin: 0 0 10px; padding-left: 20px; }
.m-body li { margin: 3px 0; }
.typing { display: inline-flex; gap: 5px; align-items: center; padding: 6px 0; }
.typing span { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: tdot 1.1s ease-in-out infinite; }
.typing span:nth-child(2){ animation-delay: .15s } .typing span:nth-child(3){ animation-delay: .3s }
@keyframes tdot { 0%,60%,100%{ transform: translateY(0) scale(.8); opacity: .4 } 30%{ transform: translateY(-5px) scale(1); opacity: 1 } }
.caret { display: inline-block; width: 2px; height: 1.05em; margin-left: 1px; vertical-align: -3px; border-radius: 1px; background: var(--green); box-shadow: 0 0 6px var(--green-glow); animation: caretBlink .9s steps(1) infinite; }
@keyframes caretBlink { 0%,50%{ opacity: 1 } 50.01%,100%{ opacity: 0 } }

/* composer */
.composer-wrap { flex: 0 0 auto; padding: 26px 0 18px; position: relative; }
.composer { display: flex; align-items: flex-end; gap: 8px; background: var(--elev); border: 1px solid var(--outline); border-radius: 26px; padding: 8px 8px 8px 8px; transition: .18s var(--ease); }
.composer textarea { flex: 1 1 auto; resize: none; border: none; outline: none; background: transparent; color: var(--text); font: inherit; font-size: 0.9375rem; line-height: 1.5; padding: 9px 6px; max-height: 160px; }
.composer textarea::placeholder { color: var(--text-faint); }
.composer .icon-btn { width: 40px; height: 40px; }
.composer .send { background: linear-gradient(135deg, var(--green), var(--green-deep)); color: var(--on-accent); border: none; }
.composer .send:hover { box-shadow: 0 0 14px var(--green-glow); }
.composer .send .ic { stroke: var(--on-accent); }
.composer-hint { text-align: center; font-size: 0.71875rem; color: var(--text-faint); margin-top: 9px; }
.composer-hint b { color: var(--green-2); font-weight: 600; }
.attach-chip { display: inline-flex; align-items: center; gap: 8px; margin: 0 0 8px; padding: 7px 12px; border-radius: 10px; background: rgb(var(--accent-rgb) / 0.08); border: 1px solid var(--outline); color: var(--green-3); font-size: 0.78125rem; }
.attach-chip .x { cursor: pointer; opacity: .7; } .attach-chip .x:hover { opacity: 1; }

/* ============================================================ GENERIC PAGE */
.page { max-width: 1080px; margin: 0 auto; padding: 30px 28px 60px; }
.page-head { margin-bottom: 26px; }
.page-eyebrow { display: inline-flex; align-items: center; gap: 8px; color: var(--green-2); font-size: 0.78125rem; font-weight: 600; letter-spacing: .6px; text-transform: uppercase; }
.page-eyebrow::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: heartbeat 2.4s var(--ease) infinite; }
@keyframes heartbeat {
  0% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0.45); transform: scale(1); }
  14% { transform: scale(1.25); }
  28% { transform: scale(1); }
  70% { box-shadow: 0 0 0 10px rgb(var(--accent-rgb) / 0); }
  100% { box-shadow: 0 0 0 0 rgb(var(--accent-rgb) / 0); transform: scale(1); }
}
.page-title { font-family: var(--font); font-size: 1.875rem; font-weight: 700; margin: 8px 0 8px; letter-spacing: -.4px; }
.page-desc { color: var(--text-dim); font-size: 0.9375rem; max-width: 640px; line-height: 1.6; }

.grid { display: grid; gap: 16px; }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }

.card { background: var(--panel-solid); border: 1px solid var(--hairline); border-radius: var(--radius); padding: 20px; transition: .18s var(--ease); }
.card.outline { border-color: var(--outline); }
.card-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.card-ic { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; background: rgb(var(--accent-rgb) / 0.10); color: var(--green); flex: 0 0 auto; }
.card-title { font-weight: 600; font-size: 1rem; }
.card-sub { font-size: 0.78125rem; color: var(--text-faint); }

.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 600; padding: 5px 11px; border-radius: 999px; }
.pill.green { color: var(--green-2); background: rgb(var(--accent-rgb) / 0.10); border: 1px solid var(--outline); }
.pill.amber { color: #fbbf6c; background: rgba(251,191,108,0.10); border: 1px solid rgba(251,191,108,0.25); }
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }

.metric { display: flex; flex-direction: column; gap: 4px; }
.metric .m-lab { font-size: 0.78125rem; color: var(--text-dim); }

.bar { height: 8px; border-radius: 99px; background: var(--elev-2); overflow: hidden; margin-top: 10px; }
.bar > i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--green-deep), var(--green)); }

.list-row { display: flex; align-items: center; gap: 14px; padding: 14px 0; border-bottom: 1px solid var(--hairline); }
.list-row:last-child { border-bottom: none; }
.list-row .grow { flex: 1 1 auto; min-width: 0; }
.list-row .r-title { font-weight: 600; font-size: 0.90625rem; }
.list-row .r-sub { font-size: 0.78125rem; color: var(--text-dim); margin-top: 2px; }

.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; border-radius: 999px; font-family: var(--font-display); font-size: 0.875rem; font-weight: 600; cursor: pointer; border: 1px solid transparent; transition: .15s var(--ease); }
.btn.ghost { background: transparent; border-color: var(--outline); color: var(--text); }
.btn.ghost:hover { border-color: var(--green); }
.btn .ic { width: 17px; height: 17px; }

.upload-zone { border: 1.5px dashed var(--outline-strong); border-radius: var(--radius); padding: 34px; text-align: center; color: var(--text-dim); cursor: pointer; transition: .18s var(--ease); }
.upload-zone:hover { background: rgb(var(--accent-rgb) / 0.04); border-color: var(--green); color: var(--text); }
.upload-zone .ic { width: 30px; height: 30px; color: var(--green); margin-bottom: 8px; }

/* sparkline */
.spark { width: 100%; height: 46px; margin-top: 12px; }
.spark path.line { fill: none; stroke: var(--green); stroke-width: 2; }
.spark path.area { fill: url(#sparkGrad); opacity: .25; stroke: none; }

/* Digital Twin — compact confidence gauge (in the markers card header) */
#twinModelCard { --cc: var(--green); }
#twinModelCard.conf-low { --cc: #fbbf6c; }
#twinModelCard.conf-moderate { --cc: var(--green-3); }
#twinModelCard.conf-high { --cc: var(--green); }
.twin-markers-head { display: flex; align-items: center; justify-content: space-between; gap: 18px; flex-wrap: wrap; margin-bottom: 14px; }
.twin-markers-intro { min-width: 200px; flex: 1 1 auto; }
.twin-gauge { display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 0 0 auto; }
.twin-gauge svg { width: 88px; height: 88px; display: block; }
.twin-gauge .track { fill: none; stroke: var(--elev-2); stroke-width: 8; }
.twin-gauge .prog { fill: none; stroke: var(--cc); stroke-width: 8; stroke-linecap: round;
  stroke-dasharray: 100; stroke-dashoffset: 100; transition: stroke-dashoffset .6s var(--ease), stroke .3s; }
.twin-gauge .conf-pct { fill: var(--cc); font: 700 30px var(--font); transition: fill .3s; }
.twin-conf-cap { font-size: 0.75rem; color: var(--text-dim); }
.twin-conf-cap b { color: var(--cc); text-transform: capitalize; font-weight: 600; }

/* Digital Twin — what-if simulator */
.twin-inputs { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.twin-field { display: flex; flex-direction: column; gap: 5px; }
.twin-field > span { font-size: 0.75rem; color: var(--text-dim); }
.twin-field .tf-unit { color: var(--text-faint); }
.twin-field input, #twinScenario {
  font: inherit; font-size: 0.875rem; color: var(--text);
  background: var(--elev); border: 1px solid var(--hairline); border-radius: 10px; padding: 9px 11px;
}
.twin-field input:focus, #twinScenario:focus { outline: none; border-color: var(--green); }
.scenario-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.scenario-chip { font: inherit; font-size: 0.8125rem; color: var(--text-dim); background: transparent; cursor: pointer;
  border: 1px solid var(--hairline); border-radius: 999px; padding: 7px 12px; transition: .15s var(--ease); }
.scenario-chip:hover { border-color: var(--green); color: var(--text); }
.twin-run-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
#twinScenario { flex: 1; min-width: 220px; resize: vertical; }
.twin-weeks { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: var(--radius-sm);
  border: 1px solid var(--outline); background: var(--panel-solid); color: var(--text-dim); white-space: nowrap; }
.twin-weeks input { width: 2.4em; padding: 0; background: transparent; border: none; outline: none; color: var(--text);
  font: inherit; font-size: 1rem; font-weight: 700; text-align: center; -moz-appearance: textfield; appearance: textfield; }
.twin-weeks input::-webkit-outer-spin-button, .twin-weeks input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }   /* hide native spinner — custom steppers below */
.twin-weeks .wk-unit { font-size: 0.78125rem; color: var(--text-dim); }
.wk-step { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--outline);
  background: var(--elev-2); color: var(--text); font-size: 1.1rem; line-height: 1; cursor: pointer; flex: 0 0 auto; transition: .15s var(--ease); }
.wk-step:hover { border-color: var(--green); color: var(--green); }
.twin-loading { color: var(--text-dim); padding: 4px 0; }

.verify-panel { display: flex; flex-direction: column; gap: 8px; margin: 14px 0 20px;
  padding: 14px 16px; border-radius: var(--radius-sm); border: 1px solid var(--outline);
  background: rgb(var(--accent-rgb) / 0.06); }
.verify-panel.revised { border-color: rgba(251,191,108,0.4); background: rgba(251,191,108,0.07); }
.verify-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.verify-badge { font-size: 0.75rem; font-weight: 700; padding: 4px 10px; border-radius: 999px; }
.verify-badge.verified { color: var(--green-2); background: rgb(var(--accent-rgb) / 0.16); }
.verify-badge.revised { color: #fbbf6c; background: rgba(251,191,108,0.16); }
.twin-list { margin: 4px 0 0; padding-left: 18px; color: var(--text-dim); font-size: 0.8125rem; line-height: 1.55; }
.twin-list li { margin: 2px 0; }

.proj-vals { display: flex; align-items: center; gap: 8px; margin: 8px 0 4px; font-family: var(--font); }
.proj-cur { color: var(--text-faint); font-size: 0.875rem; }
.proj-new { color: var(--text); font-weight: 600; font-size: 1rem; }
.proj-vals .arrow { color: var(--text-faint); }
.proj-trend { font-size: 0.78125rem; font-weight: 600; color: var(--green-2); margin-bottom: 8px; }
.conf { display: inline-block; font-size: 0.6875rem; padding: 2px 9px; border-radius: 999px; text-transform: capitalize; }
.conf.moderate { color: #fbbf6c; background: rgba(251,191,108,0.14); }

.disclaimer-banner { display: flex; gap: 12px; align-items: flex-start; padding: 14px 16px; border-radius: var(--radius-sm); background: rgb(var(--accent-rgb) / 0.04); border: 1px solid var(--outline); color: var(--text-dim); font-size: 0.78125rem; line-height: 1.55; margin-top: 22px; }
.disclaimer-banner .ic { color: var(--green); flex: 0 0 auto; margin-top: 1px; }

/* scrim for mobile */
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.5); opacity: 0; pointer-events: none; transition: .2s; z-index: 25; }

/* ============================================================ RESPONSIVE */
@media (max-width: 860px) {
  .suggestions { grid-template-columns: 1fr; }
  .grid.cols-3, .grid.cols-2 { grid-template-columns: 1fr; }
}

/* ---- phones: the sidebar becomes an off-canvas drawer over full-width content ---- */
@media (max-width: 768px) {
  .app { padding: 0; gap: 0; }
  .main { border-radius: 0; border-width: 0; }
  .topbar { padding: 12px 14px; }

  .sidebar {                                   /* fixed drawer, slides in from the left */
    position: fixed; top: 0; left: 0; bottom: 0;
    width: 84vw; max-width: 320px; min-width: 0;
    border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
    transform: translateX(-100%);
    transition: transform .26s var(--ease);
    opacity: 1; overflow-y: auto;
  }
  .app:not(.closed) .sidebar { transform: translateX(0); }
  /* override the desktop "collapse to 0 width" — on phones we slide off-screen instead */
  .app.closed .sidebar {
    width: 84vw; max-width: 320px;
    padding: 14px 12px; border-width: 1px;
    opacity: 1; pointer-events: auto; overflow-y: auto;
    transform: translateX(-100%);
  }
  #openSidebar { display: grid; }              /* hamburger always available in the top bar */
  .app:not(.closed) .scrim { opacity: 1; pointer-events: auto; }   /* backdrop when the drawer is open */

  .page { padding: 18px 16px 0; }
  .chat-wrap, .chat-home { padding-left: 16px; padding-right: 16px; }
  .icon-btn { width: 40px; height: 40px; }

  /* compact, scannable intro on phones — short tiles, no long descriptions, so the home doesn't scroll forever */
  .intro-head { padding: 12px 0 0; }
  .hero-sub { font-size: 0.875rem; margin-top: 10px; }
  .intro { margin: 12px auto 16px; }
  .intro-note { font-size: 0.75rem; padding: 10px 12px; margin-top: 10px; }
}

@media (max-width: 480px) {
  .badge-privacy { display: none; }            /* declutter the top bar on small phones */
  .topbar { gap: 8px; }
}

/* ============================================================ MOTION POLISH */
@keyframes msgIn  { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes viewIn { from { opacity: 0; transform: translateY(8px);  } to { opacity: 1; transform: none; } }
@keyframes popIn  { from { opacity: 0; transform: translateY(12px) scale(.98); } to { opacity: 1; transform: none; } }

.msg { animation: msgIn .4s var(--ease) both; }
.chat-wrap, .page { animation: viewIn .32s var(--ease) both; }
.hero-tag { animation: viewIn .55s var(--ease) both; }
.hero-sub { animation: viewIn .55s var(--ease) .08s both; }

.suggestion { animation: popIn .45s var(--ease) both; }
.suggestion:nth-child(1){ animation-delay: .05s }
.suggestion:nth-child(2){ animation-delay: .11s }
.suggestion:nth-child(3){ animation-delay: .17s }
.suggestion:nth-child(4){ animation-delay: .23s }

/* respect users who prefer less motion */
@media (prefers-reduced-motion: reduce) {
  .caret, .typing span { animation-duration: 1s !important; }
}

/* ============================================================ RESEARCH MODE */
.composer-tools { position: absolute; top: 26px; left: 16px; z-index: 3; display: flex; gap: 8px; margin: 0; transform: translateY(-72%); }
.tool-chip { display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: 999px; background: var(--elev); border: 1px solid var(--outline); color: var(--text-dim); font-size: 0.8125rem; font-weight: 600; cursor: pointer; transition: .16s var(--ease); }
.tool-chip:hover { color: var(--text); border-color: var(--outline-strong); }
.tool-chip .ic { width: 16px; height: 16px; }
.tool-chip.active { background: linear-gradient(180deg, rgb(var(--accent-rgb) / 0.16), rgb(var(--accent-rgb) / 0.06)); border-color: var(--green); color: var(--green-3); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.08); }
.tool-chip.active .ic { color: var(--green); }
.composer.research { border-color: var(--outline-strong); }
.composer.research:focus-within { border-color: var(--green); box-shadow: 0 0 0 4px rgb(var(--accent-rgb) / 0.10); }

.typing-label { font-size: 0.8125rem; font-weight: 500; margin-right: 4px; background: linear-gradient(90deg, var(--text-faint), var(--green-3), var(--text-faint)); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: shimmer 1.6s linear infinite; }
@keyframes shimmer { from { background-position: 200% 0 } to { background-position: -200% 0 } }

.m-body a { color: var(--green-2); text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--outline-strong); transition: color .15s var(--ease); }
.m-body a:hover { color: var(--green-3); text-decoration-color: var(--green); }

.model-pill.research { border-color: var(--green); }
.model-pill.research .dot { background: var(--green-3); box-shadow: 0 0 10px var(--green-glow); }

/* ============================================================ MESSAGE ACTIONS */
.m-col { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.msg-actions { display: flex; gap: 4px; margin-top: 4px; opacity: 0; transform: translateY(-2px); transition: .15s var(--ease); }
.msg:hover .msg-actions { opacity: 1; transform: none; }
.msg-action { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; background: transparent; border: 1px solid transparent; color: var(--text-faint); cursor: pointer; transition: .15s var(--ease); }
.msg-action:hover { background: var(--elev-2); color: var(--text); border-color: var(--outline); }
.msg-action .ic { width: 15px; height: 15px; }
.msg-action.ok { color: var(--green); border-color: var(--outline); }
.msg-action.speaking { color: var(--green); border-color: var(--green); background: rgb(var(--accent-rgb) / 0.10); }

/* inline message editing */
.edit-box { display: flex; flex-direction: column; gap: 8px; }
.edit-area { width: 100%; resize: none; background: var(--elev); border: 1px solid var(--outline); border-radius: 12px; color: var(--text); font: inherit; font-size: 0.9375rem; line-height: 1.5; padding: 10px 12px; outline: none; }
.edit-area:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.08); }
.edit-actions { display: flex; gap: 8px; justify-content: flex-end; }
.btn-sm { padding: 7px 14px; font-size: 0.78125rem; }

/* stop button (replaces send while generating) — green, with a filled square */
.icon-btn.send.stop { background: linear-gradient(135deg, var(--green), var(--green-deep)); }
.icon-btn.send.stop:hover { box-shadow: 0 0 14px var(--green-glow); }
.icon-btn.send.stop .ic { stroke: var(--on-accent); }
.icon-btn.send.stop .ic rect { fill: var(--on-accent); stroke: none; }

/* ============================================================ CHAT LIST */
.recent { display: flex; align-items: center; gap: 2px; padding: 0; border-radius: 10px; border: 1px solid transparent; transition: .15s var(--ease); overflow: hidden; }
.recent:hover { background: var(--elev-2); }
.recent.active { background: linear-gradient(90deg, rgb(var(--accent-rgb) / 0.10), transparent); border-color: var(--outline); }
.recent-main { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 11px; padding: 9px 11px; background: transparent; border: none; color: var(--text-dim); font-size: 0.8125rem; cursor: pointer; text-align: left; }
.recent:hover .recent-main, .recent.active .recent-main, .recent.pinned .recent-main { color: var(--text); }
.recent-main .ic { width: 15px; height: 15px; flex: 0 0 auto; opacity: .7; }
.recent-main .label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.recent-actions { display: flex; gap: 2px; padding-right: 6px; flex: 0 0 auto; opacity: 0; transition: .15s var(--ease); }
.recent:hover .recent-actions, .recent.pinned .recent-actions { opacity: 1; }
.r-act { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 7px; background: transparent; border: none; color: var(--text-faint); cursor: pointer; transition: .15s var(--ease); }
.r-act:hover { background: var(--bg-2); color: var(--text); }
.r-act.active { color: var(--green); }
.r-act .ic { width: 14px; height: 14px; }
.rename-input { flex: 1 1 auto; min-width: 0; margin: 1px 4px; padding: 7px 10px; border-radius: 9px; border: 1px solid var(--green); background: var(--elev); color: var(--text); font: inherit; font-size: 0.8125rem; }
.rename-input:focus { outline: none; box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.12); }

/* "What Hale remembers" — memory list on the Privacy page */
.mem-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.mem-list { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.mem-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 12px; border-radius: 10px; background: var(--elev); border: 1px solid var(--hairline); }
.mem-item span { font-size: 0.875rem; color: var(--text); line-height: 1.45; }
.mem-item .r-act { flex: 0 0 auto; opacity: .7; }
.mem-item .r-act:hover { opacity: 1; color: #f87171; background: var(--bg-2); }

/* My Data — trends & insights */
.data-sec-head { display: flex; align-items: center; justify-content: space-between; margin: 28px 0 12px; }
.mk-card { padding: 14px; }
.mk-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.mk-name { font-size: 0.8125rem; color: var(--text-dim); }
.mk-top .r-act { opacity: .55; flex: 0 0 auto; }
.mk-top .r-act:hover { opacity: 1; color: #f87171; }
.mk-val { font-family: var(--font); font-weight: 700; font-size: 1.4rem; color: var(--text); margin: 3px 0 8px; }
.mk-spark { width: 100%; height: 36px; display: block; }
.mk-change { font-size: 0.75rem; color: var(--green-2); margin-top: 6px; }
.mk-trend-empty { font-size: 0.75rem; color: var(--text-faint); margin-top: 4px; }

/* Protocols generator */
.proto-gen { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
#protoGoal { flex: 1; min-width: 240px; font: inherit; font-size: 0.875rem; color: var(--text); background: var(--elev); border: 1px solid var(--hairline); border-radius: 10px; padding: 10px 12px; }
#protoGoal:focus { outline: none; border-color: var(--green); }
.proto-items { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 7px; }
.proto-items label { display: flex; align-items: flex-start; gap: 9px; cursor: pointer; font-size: 0.875rem; color: var(--text); line-height: 1.45; }
.proto-items input { margin-top: 3px; flex: 0 0 auto; width: 15px; height: 15px; accent-color: var(--green); cursor: pointer; }
.proto-items input:checked + span { color: var(--text-faint); text-decoration: line-through; }
.proto-track { display: flex; align-items: flex-start; gap: 7px; margin-top: 10px; font-size: 0.8rem; color: var(--green-2); }
.proto-track .ic { width: 14px; height: 14px; flex: 0 0 auto; margin-top: 1px; }
.proto-progress { display: flex; align-items: center; gap: 12px; margin: 6px 0 14px; }
.proto-bar { flex: 1; height: 7px; border-radius: 999px; background: var(--elev-2); overflow: hidden; }
.proto-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--green-3), var(--green)); border-radius: 999px; transition: width .4s var(--ease); }
.proto-progress span { font-size: 0.78rem; color: var(--text-dim); flex: 0 0 auto; }
.proto-complete { border: 1px solid var(--outline); border-radius: var(--radius-sm); background: rgb(var(--accent-rgb) / 0.08); padding: 14px 16px; margin-bottom: 4px; }
.pc-head { font-weight: 600; color: var(--text); }

/* Legal documents */
.legal-body { text-align: left; max-height: 58vh; overflow-y: auto; color: var(--text-dim); font-size: 0.875rem; line-height: 1.6; margin: 6px 0 16px; }
.legal-body p { margin: 0 0 10px; }
.legal-body b { color: var(--text); }
.legal-note { font-size: 0.75rem; color: var(--text-faint); font-style: italic; }

/* "Verify your email" banner */
.verify-bar { display: flex; align-items: center; gap: 10px; margin: 10px 16px 0; padding: 9px 14px; border: 1px solid var(--outline); background: var(--elev-2); border-radius: 10px; font-size: 0.82rem; color: var(--text-dim); }
.verify-bar[hidden] { display: none; }
.vb-ic { display: inline-flex; color: var(--green-2); }
.vb-ic .ic { width: 16px; height: 16px; }
.vb-text { flex: 1; }
.vb-x { background: transparent; border: none; color: var(--text-faint); cursor: pointer; font-size: 0.8rem; padding: 2px 6px; line-height: 1; border-radius: 6px; }
.vb-x:hover { color: var(--text); background: var(--elev); }

/* Public landing page */
body[data-view="landing"] .sidebar,
body[data-view="landing"] .topbar,
body[data-view="landing"] .scrim,
body[data-view="landing"] #verifyBar,
body[data-view="landing"] #installBar { display: none !important; }
body[data-view="landing"] .main { margin: 0; width: 100%; }
body[data-view="landing"] .view { padding: 0; max-width: none; overflow-y: auto; }
.lp { max-width: 1000px; margin: 0 auto; padding: 0 20px 60px; }
.lp-top { display: flex; align-items: center; justify-content: space-between; padding: 18px 2px; }
.lp-brand { display: flex; align-items: center; gap: 8px; font-family: var(--font); font-weight: 700; font-size: 1.25rem; color: var(--text); }
.lp-brand svg { width: 26px; height: 26px; color: var(--green); }
.lp-top-cta { display: flex; gap: 8px; }
.lp-hero { text-align: center; padding: 44px 0 36px; }
.lp-eyebrow { display: inline-flex; align-items: center; gap: 6px; font-size: 0.78rem; color: var(--green-2); border: 1px solid var(--outline); border-radius: 999px; padding: 5px 12px; margin-bottom: 18px; }
.lp-eyebrow svg { width: 14px; height: 14px; }
.lp-hero h1 { font-family: var(--font); font-size: clamp(2.2rem, 6vw, 3.6rem); font-weight: 800; line-height: 1.05; margin: 0 0 16px; background: linear-gradient(90deg, var(--green-3), var(--green)); -webkit-background-clip: text; background-clip: text; color: transparent; }
.lp-sub { max-width: 620px; margin: 0 auto 24px; color: var(--text-dim); font-size: 1.05rem; line-height: 1.6; }
.lp-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.lp-btn { padding: 12px 24px; font-size: 0.95rem; }
.lp-fine { margin-top: 14px; font-size: 0.8rem; color: var(--text-faint); }
.lp-pricing { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; max-width: 640px; margin: 0 auto 48px; }
.lp-plan { background: var(--elev); border: 1px solid var(--outline); border-radius: 18px; padding: 26px; }
.lp-plan.featured { border-color: var(--green); }
.lp-plan-name { font-weight: 650; color: var(--text); margin-bottom: 6px; }
.lp-plan-price { font-family: var(--font); font-size: 2.4rem; font-weight: 800; color: var(--text); margin-bottom: 16px; }
.lp-plan-price span { font-size: 1rem; font-weight: 400; color: var(--text-dim); }
.lp-plan ul { list-style: none; padding: 0; margin: 0 0 18px; }
.lp-plan li { display: flex; align-items: flex-start; gap: 8px; color: var(--text-dim); font-size: 0.9rem; padding: 6px 0; }
.lp-plan li svg { width: 16px; height: 16px; color: var(--green); flex: 0 0 16px; margin-top: 2px; }
.lp-plan li b { color: var(--text); }
.lp-plan .lp-btn { width: 100%; }
.lp-foot { text-align: center; color: var(--text-faint); font-size: 0.8rem; border-top: 1px solid var(--outline); padding-top: 24px; }
.lp-foot p { margin: 6px 0; display: inline-flex; align-items: center; gap: 6px; justify-content: center; flex-wrap: wrap; }
.lp-foot svg { width: 14px; height: 14px; flex: 0 0 14px; }
.lp-foot-links a { color: var(--green-2); }
.lp-copy { color: var(--text-faint); display: block; }

/* My Data — reference-range flags + per-marker goals */
.mk-range { margin-left: 7px; vertical-align: middle; }
.mk-goal { margin-top: 8px; font-size: 0.8rem; color: var(--green-2); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.mk-goal-edit { background: none; border: none; font-size: 0.78rem; cursor: pointer; padding: 0; text-decoration: underline; color: var(--text-faint); }
.mk-goal-edit:hover { color: var(--text); }
.mk-goal-set { margin-top: 10px; background: none; font-size: 0.78rem; cursor: pointer; color: var(--green-2); border: 1px dashed var(--outline); border-radius: 999px; padding: 5px 12px; }
.mk-goal-set:hover { border-color: var(--green); color: var(--text); }
.mk-ranges-note { margin-top: 12px; font-size: 0.75rem; color: var(--text-faint); }

/* Digital Twin — reach-a-goal (reverse mode) */
.reach-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.reach-select, .reach-target { background: var(--bg-2); border: 1px solid var(--outline); border-radius: 10px; color: var(--text); font: inherit; font-size: 0.9rem; padding: 10px 12px; outline: none; }
.reach-select:focus, .reach-target:focus { border-color: var(--green); }
.reach-target { width: 96px; }
.reach-to { color: var(--text-dim); font-size: 0.9rem; }
.reach-steps { margin: 8px 0 0; padding-left: 20px; color: var(--text-dim); font-size: 0.9rem; line-height: 1.6; }
.reach-steps li { margin: 4px 0; }

/* Follow-up suggestion chips under a reply */
.followups { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0 16px; padding-left: 50px; }
.followup-chip { background: var(--elev); border: 1px solid var(--outline); border-radius: 999px; padding: 7px 14px; font-size: 0.82rem; color: var(--text-dim); cursor: pointer; transition: .15s var(--ease); text-align: left; line-height: 1.3; }
.followup-chip:hover { border-color: var(--green); color: var(--text); }
@media (max-width: 640px) { .followups { padding-left: 0; } }
.intro-h { font-family: var(--font); font-size: 1.3rem; font-weight: 700; margin: 0 0 4px; color: var(--text); }

/* Accessibility polish */
:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
}

/* ============================================================
   Bubbly display type — headings & key numbers get the rounded
   Baloo 2 face so the whole app echoes the H mark.
   ============================================================ */
.brand-name, .page-title, .hero-tag, .modal-title, .locked-title, .pd-name, .pd-h,
.card-title, .intro-h, .help-row h4, .metric .m-val, .mk-val, .proj-vals, .p-price, .pd-price,
.lp-brand, .lp-hero h1, .lp-plan-name, .lp-plan-price, .lp-sec h2, .lp-how h2,
.new-chat .label, .nav-item .label, .tool-chip, .scenario-chip, .acc-name, .lp-step b {
  font-family: var(--font-display); letter-spacing: 0;
}

/* In-app hero heartbeat — a living EKG under the greeting */
.hero-ekg { width: min(300px, 72%); height: 40px; margin: 6px auto 0; display: block; overflow: visible; animation: viewIn .55s var(--ease) .12s both; }
.hero-ekg path, .lp-ekg path { fill: none; stroke: var(--green); stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 620; stroke-dashoffset: 620; animation: ekgDraw 3.8s linear infinite; filter: drop-shadow(0 0 7px rgb(var(--accent-rgb) / 0.55)); }
@keyframes ekgDraw { 0% { stroke-dashoffset: 620; opacity: 1; } 60% { stroke-dashoffset: 0; opacity: 1; } 82% { opacity: 1; } 100% { stroke-dashoffset: 0; opacity: 0; } }
.lp-ekg { width: min(520px, 86%); height: 54px; margin: 18px auto 6px; display: block; overflow: visible; }

/* ============================================================
   Landing — immersive animated feature tour
   ============================================================ */
.lp-how { text-align: center; margin: 44px 0 8px; }
.lp-how h2, .lp-sec h2 { font-size: clamp(1.55rem, 3.6vw, 2.15rem); font-weight: 700; color: var(--text); margin: 0 0 10px; line-height: 1.15; }
.lp-how-sub { color: var(--text-dim); font-size: 0.95rem; max-width: 560px; margin: 0 auto 20px; line-height: 1.6; }
.lp-how-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; text-align: left; }
.lp-step { background: var(--elev); border: 1px solid var(--outline); border-radius: 20px; padding: 22px; }
.lp-step-n { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; font-family: var(--font-display); font-weight: 700; font-size: 1.15rem; color: #06130a; background: linear-gradient(135deg, var(--green-3), var(--green)); margin-bottom: 12px; animation: heartbeat 2.4s var(--ease) infinite; }
.lp-step:nth-child(2) .lp-step-n { animation-delay: .4s; }
.lp-step:nth-child(3) .lp-step-n { animation-delay: .8s; }
.lp-step b { display: block; color: var(--text); font-size: 1.05rem; margin-bottom: 4px; }
.lp-step p { margin: 0; color: var(--text-dim); font-size: 0.875rem; line-height: 1.55; }

.lp-sec { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; align-items: center; margin: 64px 0; opacity: 0; transform: translateY(28px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.lp-sec.seen { opacity: 1; transform: none; }
.lp-sec.rev .lp-viz { order: 2; }
@media (max-width: 760px) { .lp-sec { grid-template-columns: 1fr; gap: 18px; margin: 44px 0; } .lp-sec.rev .lp-viz { order: 0; } }
.lp-viz svg { width: 100%; height: auto; display: block; }
.lp-copy h2 { margin-top: 10px; }
.lp-copy > p { color: var(--text-dim); font-size: 0.95rem; line-height: 1.65; margin: 0; }

/* Shared viz vocabulary */
.vz-axis { stroke: rgba(255,255,255,0.14); stroke-width: 1.5; fill: none; }
.vz-tag { fill: var(--text-faint); font: 600 11px var(--font); }
.vz-txt { fill: var(--text); font: 600 11px var(--font); }
.vz-txt.dim { fill: var(--text-dim); font-weight: 400; }
.vz-bub-u { fill: rgb(var(--accent-rgb) / 0.16); }
.vz-bub-b { fill: rgba(255,255,255,0.06); }
@keyframes vzDraw { 0%, 6% { stroke-dashoffset: 340; opacity: 1; } 55%, 88% { stroke-dashoffset: 0; opacity: 1; } 98%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vzPop { 0%, 10% { transform: scale(0); } 16% { transform: scale(1.35); } 20%, 90% { transform: scale(1); } 98%, 100% { transform: scale(0); } }
@keyframes vzU { 0%, 4% { opacity: 0; transform: translateY(7px); } 10%, 90% { opacity: 1; transform: none; } 97%, 100% { opacity: 0; } }
@keyframes vzT { 0%, 13% { opacity: 0; } 17%, 40% { opacity: 1; } 45%, 100% { opacity: 0; } }
.vz-tdot { fill: var(--text-dim); animation: vzTdot 1s infinite; }
.vz-tdot.t2 { animation-delay: .16s; } .vz-tdot.t3 { animation-delay: .32s; }
@keyframes vzTdot { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
@keyframes vzB { 0%, 44% { opacity: 0; transform: translateY(7px); } 51%, 90% { opacity: 1; transform: none; } 97%, 100% { opacity: 0; } }
@keyframes vzCur { 0%, 8% { stroke-dashoffset: 200; } 40%, 92% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vzWhat { 0%, 18% { stroke-dashoffset: 235; opacity: 1; } 60%, 92% { stroke-dashoffset: 0; opacity: 1; } 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vzWave { 0%, 8% { transform: scale(1); opacity: .8; } 40% { transform: scale(2.6); opacity: 0; } 100% { transform: scale(2.6); opacity: 0; } }
@keyframes vzChk { 0%, 12% { stroke-dashoffset: 26; } 20%, 92% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes vzSpin { 0%, 48% { transform: rotate(0deg); opacity: .35; } 60% { opacity: 1; } 80%, 92% { transform: rotate(360deg); opacity: 1; } 100% { transform: rotate(360deg); opacity: .35; } }

/* 5 · Modes — research sweeps sources; training loads the week */
.vz-doc { fill: rgba(255,255,255,0.08); }
@keyframes vzScan { 0% { transform: translateY(0); } 50% { transform: translateY(58px); } 100% { transform: translateY(0); } }
.vz-bar { fill: var(--green); opacity: .9; transform-box: fill-box; transform-origin: bottom; animation: vzBar 5s var(--ease) infinite; }
@keyframes vzBar { 0%, 8% { transform: scaleY(0.08); } 45%, 88% { transform: scaleY(1); } 100% { transform: scaleY(0.08); } }
@keyframes vzCard { 0%, 8% { opacity: 0; transform: translateY(16px); } 18%, 88% { opacity: 1; transform: none; } 97%, 100% { opacity: 0; } }
.vz-card rect.face { fill: rgba(255,255,255,0.06); stroke: rgba(255,255,255,0.1); }

/* 7 · Privacy — the shield seals shut */
.vz-shield { fill: none; stroke: var(--green); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 470; stroke-dashoffset: 470; animation: vzShield 7s var(--ease) infinite; }
@keyframes vzShield { 0%, 6% { stroke-dashoffset: 470; } 42%, 90% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; opacity: 0; } }
.vz-lockchk { fill: none; stroke: var(--green-3); stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 80; stroke-dashoffset: 80; animation: vzChk 7s var(--ease) infinite; animation-delay: 2.4s; }

/* ============================================================
   Bottom tab bar — app-style primary navigation
   ============================================================ */
.tabbar { display: flex; gap: 4px; flex: 0 0 auto; padding: 7px 10px calc(7px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--hairline); background: var(--bg-2); }
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 7px 2px 6px;
  border-radius: 16px; background: none; border: none; color: var(--text-dim); cursor: pointer;
  font-family: var(--font-display); font-size: 0.72rem; font-weight: 600; line-height: 1; transition: .15s var(--ease); }
.tab-item .ic { width: 21px; height: 21px; }
.tab-item:hover { color: var(--text); }
.tab-item.active { color: var(--green-2); background: rgb(var(--accent-rgb) / 0.12); }
.tab-item.locked { opacity: .55; }
body[data-view="landing"] .tabbar { display: none !important; }
/* Responsive split: desktop navigates with the sidebar; phones/tablets with the bottom tabs. */
@media (min-width: 901px) { .tabbar { display: none; } }
@media (max-width: 900px) { .sidebar .nav { display: none; } }

/* Quick "new chat" in the topbar — only where it makes sense (the Chat tab) */
#topNewChat { display: none; }
body[data-view="chat"] #topNewChat { display: inline-flex; }

/* Home tab — the dashboard */
.home-page { max-width: 1000px; margin: 0 auto; }
.home-page .intro-head { text-align: center; padding: 12px 0 4px; }
.home-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 10px; }
.daily-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px; margin: 0 -10px; border-radius: 12px; cursor: pointer; color: var(--text); font-size: 0.9rem; transition: background .15s var(--ease); }
.daily-item:hover { background: var(--elev); }
.daily-item input { display: none; }
.daily-item .dbox { width: 22px; height: 22px; flex: 0 0 22px; border-radius: 8px; border: 1.5px solid var(--outline-strong, rgba(255,255,255,0.25)); display: grid; place-items: center; color: transparent; transition: .15s var(--ease); }
.daily-item .dbox .ic { width: 14px; height: 14px; }
.daily-item.done .dbox { background: var(--green); border-color: var(--green); color: #06130a; }
.daily-item.done .dlab { color: var(--text-faint); text-decoration: line-through; }
.stat-row { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-top: 1px solid var(--hairline); }
.stat-row:first-child { border-top: none; }
.stat-name { flex: 1; color: var(--text-dim); font-size: 0.875rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stat-val { font-family: var(--font-display); font-weight: 700; color: var(--text); }
.stat-val small { color: var(--text-faint); font-weight: 400; }

/* Composer focus row — organized panel above the chat box (not fused to it) */
.composer-tools { position: static; transform: none; z-index: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 10px; padding: 10px 12px; border: 1px solid var(--hairline); border-radius: 18px; background: var(--bg-2); }
.mode-label { color: var(--text-faint); font-size: 0.72rem; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; margin-right: 4px; }

/* Digital Twin — collapsed data drawer (auto-filled, zero stress) */
.twin-data { margin: 14px 0 0; }
.twin-data summary { cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; padding: 12px 16px; border: 1px solid var(--outline); border-radius: 14px; background: var(--elev); color: var(--text-dim); font-size: 0.875rem; transition: border-color .15s var(--ease); }
.twin-data summary:hover { border-color: var(--outline-strong, rgba(255,255,255,0.25)); }
.twin-data summary::-webkit-details-marker { display: none; }
.twin-data summary .ic { width: 16px; height: 16px; color: var(--green-2); flex: 0 0 16px; }
.twin-data summary b { color: var(--text); }
.twin-data-sub { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.twin-edit { color: var(--green-2); text-decoration: underline; font-size: 0.8rem; flex: 0 0 auto; }
.twin-data[open] summary { border-color: var(--green); }
.twin-data > .card { margin-top: 10px; }

/* Help & Guide modal */
.help-modal { max-width: 460px; }
.help-body { text-align: left; max-height: 64vh; overflow-y: auto; margin: 8px 0 16px; }
.help-intro { color: var(--text-dim); font-size: 0.875rem; line-height: 1.55; margin: 0 0 8px; }
.help-row { display: flex; gap: 12px; align-items: flex-start; padding: 12px 0; border-top: 1px solid var(--outline); }
.help-ic { flex: 0 0 30px; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px; background: var(--elev-2); color: var(--green-2); }
.help-ic .ic { width: 17px; height: 17px; }
.help-row h4 { margin: 1px 0 3px; font-size: 0.9rem; font-weight: 650; color: var(--text); }
.help-row p { margin: 0; font-size: 0.82rem; line-height: 1.5; color: var(--text-dim); }
.pro-tag { font-size: 0.62rem; font-weight: 700; letter-spacing: .04em; color: var(--green-2); border: 1px solid var(--green-2); border-radius: 6px; padding: 1px 5px; margin-left: 7px; vertical-align: middle; }
.help-foot { display: flex; gap: 9px; align-items: flex-start; margin-top: 14px; padding: 11px 12px; border-radius: 10px; background: var(--elev-2); color: var(--text-dim); font-size: 0.78rem; line-height: 1.5; }
.help-foot .ic { flex: 0 0 16px; width: 16px; height: 16px; color: var(--green-2); margin-top: 1px; }
.help-foot b { color: var(--text); }
.legal-link { color: var(--green-2); font-size: 0.875rem; cursor: pointer; }
.legal-link:hover { text-decoration: underline; }
.legal-scrollhint { font-size: 0.75rem; color: var(--text-faint); text-align: center; margin: 8px 0 0; }

/* ============================================================ MARKETPLACE */
.mkt-search { display: flex; align-items: center; gap: 10px; background: var(--elev); border: 1px solid var(--outline); border-radius: 999px; padding: 11px 16px; margin-bottom: 14px; }
.mkt-search:focus-within { border-color: var(--green); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.08); }
.mkt-search input { flex: 1 1 auto; background: transparent; border: none; outline: none; color: var(--text); font: inherit; font-size: 0.90625rem; }
.mkt-search input::placeholder { color: var(--text-faint); }
.mkt-search .ic { color: var(--text-dim); flex: 0 0 auto; }

.mkt-row { display: flex; gap: 16px; align-items: center; justify-content: space-between; flex-wrap: wrap; margin-bottom: 20px; }
.mkt-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.mkt-chip { padding: 8px 14px; border-radius: 999px; background: var(--elev); border: 1px solid var(--outline); color: var(--text-dim); font-size: 0.8125rem; font-weight: 600; cursor: pointer; transition: .15s var(--ease); }
.mkt-chip:hover { color: var(--text); border-color: var(--outline-strong); }
.mkt-chip.active { background: linear-gradient(180deg, rgb(var(--accent-rgb) / 0.16), rgb(var(--accent-rgb) / 0.06)); border-color: var(--green); color: var(--green-3); }

.product { width: 100%; text-align: left; font: inherit; cursor: pointer; display: flex; flex-direction: column; gap: 5px; }
.product:hover { transform: translateY(-2px); border-color: var(--outline-strong); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.05); }
.p-tile { width: 46px; height: 46px; border-radius: 12px; display: grid; place-items: center; background: rgb(var(--accent-rgb) / 0.10); color: var(--green); margin-bottom: 6px; }
.p-tile .ic { width: 24px; height: 24px; }
.p-tile.lg { width: 64px; height: 64px; border-radius: 16px; flex: 0 0 auto; }
.p-tile.lg .ic { width: 32px; height: 32px; }
.p-name { font-weight: 600; font-size: 0.9375rem; color: var(--text); line-height: 1.3; }
.p-brand { font-size: 0.78125rem; color: var(--text-faint); }
.p-meta { display: flex; align-items: center; gap: 8px; font-size: 0.78125rem; margin-top: 2px; }
.stars { color: #fbbf6c; font-weight: 600; }
.dim { color: var(--text-dim); }
.p-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 10px; }
.p-price { font-family: var(--font); font-weight: 700; font-size: 1.0625rem; color: var(--text); }
.p-badge { font-size: 0.6875rem; color: var(--green-2); background: rgb(var(--accent-rgb) / 0.08); border: 1px solid var(--outline); padding: 3px 9px; border-radius: 999px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 58%; }

/* product detail */
.back-btn { margin-bottom: 16px; }
.product-detail { padding: 26px; }
.pd-head { display: flex; gap: 18px; align-items: flex-start; }
.pd-headinfo { min-width: 0; }
.pd-name { font-family: var(--font); font-size: 1.625rem; font-weight: 700; margin: 8px 0 2px; letter-spacing: -.3px; }
.pd-brand { color: var(--text-dim); font-size: 0.875rem; }
.pd-meta { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; margin-top: 12px; font-size: 0.84375rem; }
.pd-price { font-family: var(--font); font-weight: 700; font-size: 1.25rem; color: var(--text); }
.pd-badges { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.pd-desc { color: var(--text-dim); line-height: 1.65; margin: 20px 0; font-size: 0.9375rem; }
.pd-h { font-family: var(--font); font-size: 1.0625rem; margin: 6px 0 2px; }
.buy-row { display: flex; gap: 10px; flex-wrap: wrap; }
.buy { text-decoration: none; }
.buy .ic { width: 16px; height: 16px; }
@media (max-width: 860px) { .pd-head { flex-direction: column; } }

/* ============================================================ AUTH / SUBSCRIPTION */
.nav-item.locked { color: var(--text-faint); }
.nav-item .nav-lock { margin-left: auto; display: inline-grid; place-items: center; }
.nav-item .nav-lock .ic { width: 14px; height: 14px; opacity: .65; }
.app.closed .nav-lock { display: none; }
.tool-chip.locked { opacity: .72; }   /* .5 dropped the label to ~2.6:1 in the signed-out first-run state */
.icon-btn.locked { opacity: .68; }    /* the settings gear was near-invisible when signed out */

.locked-card { max-width: 460px; margin: 7vh auto 0; text-align: center; padding: 36px 28px; display: flex; flex-direction: column; align-items: center; }
.locked-ic { width: 62px; height: 62px; border-radius: 18px; display: grid; place-items: center; background: rgb(var(--accent-rgb) / 0.10); color: var(--green); margin-bottom: 14px; }
.locked-ic .ic { width: 30px; height: 30px; }
.locked-title { font-family: var(--font); font-size: 1.625rem; font-weight: 700; margin: 0 0 6px; }
.locked-sub { color: var(--text-dim); font-size: 0.9375rem; line-height: 1.6; max-width: 360px; margin: 0 0 18px; }

.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); backdrop-filter: blur(3px); display: grid; place-items: center; z-index: 100; padding: 20px; animation: viewIn .18s var(--ease) both; }
.modal { position: relative; width: 100%; max-width: 400px; background: linear-gradient(180deg, var(--elev), var(--bg-2)); border: 1px solid var(--outline); border-radius: var(--radius-lg); padding: 30px 26px 24px; box-shadow: var(--shadow); text-align: center; animation: popIn .24s var(--ease) both; }
.modal-x { position: absolute; top: 14px; right: 14px; width: 30px; height: 30px; border-radius: 9px; background: transparent; border: none; color: var(--text-dim); font-size: 0.875rem; cursor: pointer; transition: .15s var(--ease); }
.modal-x:hover { background: var(--elev-2); color: var(--text); }
.modal-mark { width: 56px; height: 56px; border-radius: 16px; display: grid; place-items: center; margin: 0 auto 14px; background: rgb(var(--accent-rgb) / 0.10); color: var(--green); }
.modal-mark svg { width: 28px; height: 28px; fill: none; stroke: var(--green); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.modal-mark.pro { background: linear-gradient(135deg, rgb(var(--accent-rgb) / 0.22), rgba(22,163,74,0.12)); }
.modal-title { font-family: var(--font); font-size: 1.375rem; font-weight: 700; margin: 0 0 6px; }
.modal-sub { color: var(--text-dim); font-size: 0.875rem; line-height: 1.55; margin: 0 0 18px; }
.modal-input { width: 100%; background: var(--bg-2); border: 1px solid var(--outline); border-radius: 12px; color: var(--text); font: inherit; font-size: 0.9375rem; padding: 12px 14px; margin-bottom: 10px; outline: none; }
.modal-input:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.08); }
.modal-label { display: block; text-align: left; color: var(--text-dim); font-size: 0.78rem; margin: 2px 2px 6px; }
#agegate { z-index: 200; }   /* the age gate outranks every other modal — nothing may cover it */
.modal-input[type="date"] { color-scheme: dark; min-height: 46px; }   /* native picker chrome matches the dark modal */
html.light .modal-input[type="date"] { color-scheme: light; }
.modal-btn { width: 100%; justify-content: center; margin-top: 6px; }
.modal-btn.danger { border-color: rgba(248,113,113,0.4); color: #f87171; }
.modal-fine { color: var(--text-faint); font-size: 0.71875rem; margin: 12px 0 0; }
.modal-fine a, .agree-row a { color: var(--green-2); }
.agree-row { display: flex; align-items: flex-start; gap: 9px; text-align: left; font-size: 0.78rem; color: var(--text-dim); line-height: 1.45; margin: 4px 2px 2px; cursor: pointer; }
.agree-row input { margin-top: 2px; flex: 0 0 auto; width: 16px; height: 16px; accent-color: var(--green); cursor: pointer; }
.oauth-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 11px 14px; border-radius: 12px; background: var(--elev); border: 1px solid var(--outline); color: var(--text); font: inherit; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: .15s var(--ease); }
.oauth-btn svg { width: 18px; height: 18px; flex: 0 0 auto; }
.modal-err { color: #f87171; font-size: 0.78125rem; text-align: left; margin: 2px 2px 6px; }
.modal-err:empty { display: none; }
.modal-input.invalid { border-color: rgba(248,113,113,0.6); box-shadow: 0 0 0 3px rgba(248,113,113,0.12); }
.modal-feats { list-style: none; padding: 0; margin: 0 0 16px; text-align: left; display: flex; flex-direction: column; gap: 11px; }
.modal-feats li { display: flex; gap: 10px; align-items: flex-start; font-size: 0.84375rem; color: var(--text); line-height: 1.45; }
.modal-feats .ic { width: 18px; height: 18px; color: var(--green); flex: 0 0 auto; margin-top: 1px; }
.modal-feats b { color: var(--green-3); font-weight: 600; }
.modal-price { display: flex; align-items: baseline; justify-content: center; gap: 5px; margin: 4px 0 14px; }
.mp-amt { font-family: var(--font); font-size: 2.125rem; font-weight: 700; }
.mp-per { color: var(--text-dim); font-size: 0.875rem; }

/* ============================================================ SETTINGS */
.user-row { display: flex; align-items: center; gap: 4px; }
.user-row .user { flex: 1 1 auto; min-width: 0; }
.user-row #settingsBtn { flex: 0 0 auto; }

.modal { max-height: 88vh; overflow-y: auto; }

/* Paged settings: left section nav + scrolling content pane */
.modal:has(.settings-paged) { max-width: 660px; }
.settings-paged { display: flex; gap: 0; text-align: left; min-height: 380px; max-height: 70vh; }
.set-nav { flex: 0 0 150px; display: flex; flex-direction: column; gap: 2px; padding-right: 14px; border-right: 1px solid var(--hairline); }
.set-nav button { text-align: left; padding: 9px 12px; border-radius: 9px; background: transparent; border: none; color: var(--text-dim); font: inherit; font-size: 0.875rem; cursor: pointer; transition: .14s var(--ease); }
.set-nav button:hover { background: var(--elev-2); color: var(--text); }
.set-nav button.active { background: rgb(var(--accent-rgb) / 0.12); color: var(--text); font-weight: 600; }
.set-pane { flex: 1 1 auto; min-width: 0; overflow-y: auto; padding: 0 4px 4px 18px; }
.set-pane h2 { margin: 0 0 16px; font-size: 1.15rem; font-weight: 600; }
@media (max-width: 560px) {
  .settings-paged { flex-direction: column; max-height: 76vh; }
  .set-nav { flex: 0 0 auto; flex-direction: row; flex-wrap: wrap; gap: 6px; padding: 0 0 12px; border-right: none; border-bottom: 1px solid var(--hairline); }
  .set-pane { padding: 14px 2px 4px; }
}

.set-group { margin-bottom: 20px; }
.set-group:last-child { margin-bottom: 0; }
.set-label { color: var(--text-faint); font-size: 0.6875rem; font-weight: 600; letter-spacing: .6px; text-transform: uppercase; margin-bottom: 4px; }
.set-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--hairline); cursor: pointer; }
.set-row:last-child { border-bottom: none; }
.set-row > span:first-child { font-size: 0.875rem; font-weight: 500; color: var(--text); }
.set-row small { display: block; color: var(--text-faint); font-size: 0.71875rem; font-weight: 400; margin-top: 3px; }
.set-action { display: block; width: 100%; text-align: left; padding: 11px 13px; border-radius: 11px; background: var(--elev); border: 1px solid var(--outline); color: var(--text); font: inherit; font-size: 0.84375rem; font-weight: 500; cursor: pointer; margin-top: 8px; transition: .15s var(--ease); }
.set-action:hover { border-color: var(--outline-strong); }
.set-action.danger { color: #f87171; border-color: rgba(248,113,113,0.3); }
.set-action.danger:hover { border-color: rgba(248,113,113,0.6); }

.toggle { position: relative; display: inline-block; width: 42px; height: 24px; flex: 0 0 auto; }
.toggle input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; margin: 0; cursor: pointer; z-index: 1; }
.toggle .track { position: absolute; inset: 0; background: var(--elev-2); border: 1px solid var(--outline); border-radius: 999px; transition: .18s var(--ease); }
.toggle .track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--text-dim); transition: .18s var(--ease); }
.toggle input:checked + .track { background: linear-gradient(135deg, var(--green), var(--green-deep)); border-color: var(--green); }
.toggle input:checked + .track::after { transform: translateX(18px); background: var(--on-accent); }

/* animation-iteration-count is the half that was missing. Shortening the DURATION of an
   `infinite` animation doesn't stop it — it restarts it ~60x a second, so Reduce Motion was
   making the EKG labels strobe between opacity 0 and .9 on every frame instead of holding still.
   That is the opposite of the setting's purpose and a WCAG 2.3.1 flash risk. Run once, then stop. */
.reduce-motion *, .reduce-motion *::before, .reduce-motion *::after {
  animation-duration: .001s !important; animation-iteration-count: 1 !important;
  transition-duration: .001s !important; }
/* the caret and the typing dots are functional indicators — they keep looping, gently */
.reduce-motion .caret, .reduce-motion .typing span {
  animation-duration: 1s !important; animation-iteration-count: infinite !important; }
/* Every vlab keyframe ends at opacity 0, so merely stopping the cycle leaves the EKG unlabelled.
   Drop the animation outright rather than relying on the post-animation revert — a running
   animation still wins over the cascade, and it stays "running" whenever the tab is backgrounded. */
.reduce-motion .vital-lab { animation: none !important; }
.reduce-motion .vital-lab.l1 { opacity: .9; }

/* personalization + appearance controls */
.set-hint { color: var(--text-faint); font-size: 0.75rem; margin: 0 0 10px; }
.set-area { width: 100%; resize: vertical; min-height: 64px; background: var(--bg-2); border: 1px solid var(--outline); border-radius: 12px; color: var(--text); font: inherit; font-size: 0.84375rem; line-height: 1.5; padding: 10px 12px; margin-bottom: 10px; outline: none; }
.set-area:focus { border-color: var(--green); box-shadow: 0 0 0 3px var(--green-glow); }
.acc-name { text-align: center; font-weight: 600; }
.swatches { display: flex; gap: 8px; }
.swatch { width: 22px; height: 22px; border-radius: 50%; background: var(--sw); border: 2px solid transparent; cursor: pointer; padding: 0; transition: .15s var(--ease); }
.swatch:hover { transform: scale(1.1); }
.swatch.active { border-color: var(--bg); box-shadow: 0 0 0 2px var(--sw); }
.seg { display: flex; border: 1px solid var(--outline); border-radius: 10px; overflow: hidden; }
.seg button { padding: 6px 12px; background: transparent; border: none; color: var(--text-dim); font: inherit; font-size: 0.78125rem; cursor: pointer; transition: .15s var(--ease); }
.seg button + button { border-left: 1px solid var(--outline); }
.seg button.active { background: var(--elev-2); color: var(--text); }

/* text size (scales the chat reading/writing surfaces) */
html { font-size: 16px; } /* text-scale */
html.text-small { font-size: 14.5px; } /* text-scale */
html.text-large { font-size: 18px; } /* text-scale */

/* accent themes — recolor the --green family */
/* The accent picker's own swatches are user CHOICES, not the theme — the blanket green purge
   caught this one and left it half teal, half green. Restored to a coherent teal. */
html.accent-teal  { --green: #2dd4bf; --green-2: #5eead4; --green-3: #99f6e4; --green-deep: #0d9488; --accent-rgb: 45 212 191; }
html.accent-blue  { --green: #60a5fa; --green-2: #93c5fd; --green-3: #bfdbfe; --green-deep: #2563eb; --accent-rgb: 96 165 250; }
html.accent-tan   { --green: #d6b98c; --green-2: #e7d2ad; --green-3: #f0e2c8; --green-deep: #a8855c; --accent-rgb: 214 185 140; }
html.accent-clay  { --green: #9c6a3c; --green-2: #c89a6a; --green-3: #d9b189; --green-deep: #6e4827; --accent-rgb: 156 106 60; --on-accent: #fff; }
html.accent-stone { --green: #e8e6e1; --green-2: #cfcabf; --green-3: #f4f2ed; --green-deep: #b8b3a8; --accent-rgb: 232 230 225; }

/* ============================================================ LIGHT THEME */
html.light {
  --bg: #f5f4ef;
  --bg-2: #eceae3;
  --panel-solid: #ffffff;
  --elev: #ffffff;
  --elev-2: #ecebe4;
  --text: #1c241e;
  --text-dim: #586159;
  --text-faint: #6a726a;   /* was #8b938b — 2.87:1 on this bg, under the 4.5:1 AA floor. Now 4.51:1 */
  --hairline: rgba(0,0,0,0.10);
  --shadow: 0 14px 40px rgba(0,0,0,0.12);
  /* The accent greens are tuned for a near-black background. Left as-is they render at
     1.6-1.9:1 on cream — "Private", "Free preview" and the "create a free account" link were
     all effectively unreadable in light mode. Deriving from each accent's own --green-deep
     keeps every theme on-brand instead of inventing six new colours. Measured on BOTH light
     surfaces (the cream page and the white cards), 76% clears AA everywhere: emerald 4.81/5.30,
     teal 5.41/5.96, blue 7.05/7.76, tan 4.93/5.43, clay 9.82/10.81. */
  /* BASE: no color-mix(). It landed in Safari 16.2 but IPHONEOS_DEPLOYMENT_TARGET is 16.0, and a
     custom property holding an unsupported function is only invalid at SUBSTITUTION time — so on
     iOS 16.0/16.1 `background: linear-gradient(..., var(--green-3), ...)` became invalid-at-
     computed-value-time and .btn.primary lost its fill entirely. --green-deep alone is
     accent-aware and already ~3:1 on cream, far better than the 1.6:1 these tokens started at. */
  --green:   var(--green-deep);
  --green-2: var(--green-deep);
  --green-3: var(--green-deep);
}
/* ...and the darker, AA-passing mix wherever color-mix() actually exists. */
@supports (color: color-mix(in srgb, red 50%, #000)) {
  html.light {
    --green:   color-mix(in srgb, var(--green-deep) 76%, #000);
    --green-2: color-mix(in srgb, var(--green-deep) 76%, #000);
    --green-3: color-mix(in srgb, var(--green-deep) 76%, #000);
  }
  /* stone is a neutral, so its --green-deep is a light beige and 76% only reaches ~3:1. */
  html.light.accent-stone {
    --green:   color-mix(in srgb, var(--green-deep) 55%, #000);
    --green-2: color-mix(in srgb, var(--green-deep) 55%, #000);
    --green-3: color-mix(in srgb, var(--green-deep) 55%, #000);
  }
}
/* accent-colored text needs to be darker on a light background */
html.light .m-body strong,
html.light .attach-chip,
html.light .tool-chip.active,
html.light .mkt-chip.active { color: var(--green-deep); }
html.light .brand-name { background-image: linear-gradient(90deg, var(--green-deep), var(--green)); }
html.light .hero-tag { background-image: linear-gradient(100deg, var(--green-deep), var(--green-2)); }
html.light .stars { color: #c8860a; }

/* ============================================================ ACCOUNT / SETTINGS EXTRAS */
.acct-head { display: flex; gap: 14px; align-items: center; margin-bottom: 20px; }
.acct-head .modal-mark { margin: 0; flex: 0 0 auto; }
.acct-id { flex: 1 1 auto; min-width: 0; }
.acct-head .acc-name { text-align: left; margin: 0 0 4px; }
.acct-head .modal-sub { margin: 0; }
.set-action small { display: block; color: var(--text-faint); font-size: 0.71875rem; font-weight: 400; margin-top: 3px; }
.set-action.hosted { display: flex; align-items: center; justify-content: space-between; gap: 10px; }

/* ============================================================ MODEL PICKER */
.model-wrap { position: relative; }
.model-menu { position: absolute; top: calc(100% + 8px); left: 0; min-width: 244px; background: var(--elev); border: 1px solid var(--outline); border-radius: 14px; box-shadow: var(--shadow); padding: 6px; z-index: 60; display: flex; flex-direction: column; gap: 2px; animation: popIn .18s var(--ease) both; }
.model-opt { text-align: left; background: transparent; border: none; border-radius: 10px; padding: 9px 11px; cursor: pointer; color: var(--text); font: inherit; transition: .15s var(--ease); }
.model-opt:hover { background: var(--elev-2); }
.model-opt.active { background: linear-gradient(90deg, rgb(var(--accent-rgb) / 0.12), transparent); }
.mo-name { font-size: 0.84375rem; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.mo-name .ic { width: 15px; height: 15px; color: var(--green); margin-left: auto; }
.mo-desc { font-size: 0.71875rem; color: var(--text-dim); margin-top: 2px; }

/* ============================================================
   Usability: manual marker entry · cross-tool handoff · get-started
   ============================================================ */
.manual-add { margin-top: 12px; }
.manual-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.manual-form[hidden] { display: none; }   /* the hidden attribute must beat display:flex */
.manual-in { flex: 1 1 140px; min-width: 120px; padding: 9px 12px; border-radius: var(--radius-sm);
  border: 1px solid var(--outline); background: var(--panel-solid); color: var(--text); font: inherit; font-size: 0.84375rem; }
.manual-in:focus { outline: none; border-color: var(--green); }
.manual-msg { font-size: 0.78125rem; color: var(--text-faint); margin-top: 8px; min-height: 1em; }

.next-steps { margin: 28px 0 4px; }
.ns-label { font-size: 0.78125rem; color: var(--text-faint); text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px; }
.ns-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ns-btn { display: flex; align-items: center; gap: 12px; text-align: left; padding: 14px 16px; border-radius: var(--radius);
  border: 1px solid var(--outline); background: var(--panel-solid); color: var(--text); cursor: pointer; transition: .15s var(--ease); }
.ns-btn:hover { border-color: var(--green); background: rgb(var(--accent-rgb) / 0.04); }
.ns-btn svg { width: 22px; height: 22px; color: var(--green); flex-shrink: 0; }
.ns-btn span { display: flex; flex-direction: column; }
.ns-btn b { font-size: 0.875rem; }
.ns-btn small { color: var(--text-faint); font-size: 0.75rem; }
.intro-head { text-align: center; padding: 20px 0 4px; }
.intro { max-width: 720px; margin: 18px auto 24px; }
.intro-note { margin-top: 14px; padding: 12px 16px; border-radius: var(--radius-sm); text-align: center;
  border: 1px solid var(--outline); background: rgb(var(--accent-rgb) / 0.05); font-size: 0.8125rem; color: var(--text-dim); }
.intro-link { background: none; border: none; padding: 0; font: inherit; color: var(--green); font-weight: 600; cursor: pointer; text-decoration: underline; }

@media (max-width: 560px) {
  .ns-row { grid-template-columns: 1fr; }
}

/* OAuth sign-in buttons */
.oauth-col { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 14px; }
.oauth-btn {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  width: 100%; padding: 11px 16px; border-radius: 12px; cursor: pointer;
  background: var(--elev); border: 1px solid var(--hairline);
  color: var(--text); font: 600 14px/1 var(--font); transition: border-color .15s, background .15s;
}
.oauth-btn:hover { border-color: var(--green); background: rgb(var(--accent-rgb) / 0.06); }
.oauth-div { display: flex; align-items: center; gap: 10px; color: var(--dim); font-size: 12px; margin-top: 2px; }
.oauth-div::before, .oauth-div::after { content: ""; flex: 1; height: 1px; background: var(--hairline); }

/* ============================================================ VISUAL IDENTITY v36 — depth, glow & atmosphere.
   Same calm, same green — but alive: breathing auras, glass depth, film grain, glowing touchpoints. */

/* breathing aurora in the outer frame */
body::before { content: ""; position: fixed; inset: -10%; z-index: -1; pointer-events: none;
  background:
    radial-gradient(560px 420px at 78% -4%, rgb(var(--accent-rgb) / 0.14), transparent 62%),
    radial-gradient(520px 420px at -6% 104%, rgb(var(--accent-rgb) / 0.10), transparent 60%);
  filter: blur(46px); animation: aura 26s ease-in-out infinite alternate; }
@keyframes aura {
  0%   { transform: translate3d(0, 0, 0) scale(1); }
  100% { transform: translate3d(2%, 3%, 0) scale(1.06); }
}

/* the same atmosphere inside the main panel */
.main { position: relative; z-index: 0; }
.main::before { content: ""; position: absolute; inset: -12%; z-index: -1; pointer-events: none;
  background:
    radial-gradient(720px 460px at 82% -6%, rgb(var(--accent-rgb) / 0.10), transparent 60%),
    radial-gradient(560px 430px at 4% 108%, rgb(var(--accent-rgb) / 0.07), transparent 58%);
  filter: blur(38px); animation: aura 32s ease-in-out infinite alternate; }

/* film grain — barely there, but everything feels more physical */
body::after { content: ""; position: fixed; inset: 0; z-index: 9999; pointer-events: none; opacity: .045;
  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='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }

/* page titles — the brand voice on every page */
.page-title { font-family: var(--font-display); font-size: 2.05rem; font-weight: 700; letter-spacing: .1px;
  background: linear-gradient(105deg, var(--text) 55%, var(--green-3) 115%);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }

/* cards — glass depth + lift on touch */
.card { background: linear-gradient(180deg, rgba(255,255,255,.025), rgba(255,255,255,0) 46%), var(--panel-solid);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.045); }

/* primary buttons — glow + a sheen that sweeps on hover */
.btn.primary { position: relative; overflow: hidden; box-shadow: 0 8px 24px -10px rgb(var(--accent-rgb) / 0.5); }
.btn.primary::after { content: ""; position: absolute; top: -4px; bottom: -4px; left: -70%; width: 44%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.32), transparent);
  transform: skewX(-18deg); transition: left .5s var(--ease); pointer-events: none; }
.btn.primary:hover::after { left: 130%; }
.tab-item.active { text-shadow: 0 0 14px rgb(var(--accent-rgb) / 0.55); }

/* vital numbers carry a faint glow of life */
.mk-val, .m-val { text-shadow: 0 0 22px rgb(var(--accent-rgb) / 0.16); }
@keyframes heroSheen {
  0%, 100% { background-position: 0% 0; }
  50%      { background-position: 100% 0; }
}

/* modals — deeper glass, green edge-light */
.modal-overlay { backdrop-filter: blur(10px) saturate(1.15); -webkit-backdrop-filter: blur(10px) saturate(1.15); }
.modal { box-shadow: 0 30px 80px -30px rgba(0,0,0,.75), 0 0 60px -28px rgb(var(--accent-rgb) / 0.4), inset 0 1px 0 rgba(255,255,255,.05); }

/* the small touches that read "crafted" */
::selection { background: rgb(var(--accent-rgb) / 0.3); }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 8px; border: 3px solid transparent; background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: rgb(var(--accent-rgb) / 0.35); border: 3px solid transparent; background-clip: padding-box; }

/* light mode — the same soul, daylight version */
html.light body::before { opacity: .55; }
html.light body::after { opacity: .028; }
html.light .main::before { opacity: .5; }
html.light .card { background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0) 46%), var(--panel-solid); box-shadow: inset 0 1px 0 rgba(255,255,255,.7); }
html.light .card:hover { box-shadow: 0 18px 40px -24px rgb(var(--accent-rgb) / 0.35), inset 0 1px 0 rgba(255,255,255,.8); }
html.light *::-webkit-scrollbar-thumb { background: rgba(0,0,0,.14); border: 3px solid transparent; background-clip: padding-box; }

/* stillness for those who ask for it */
@media (prefers-reduced-motion: reduce) {
  body::before, .main::before, .lp-hero h1 { animation: none; }
  .card:hover, .btn.primary:hover { transform: none; }
  .btn.primary::after { display: none; }
}

/* ============================================================ SIGNATURE LAYER v37 — structure, not just glow.
   Topographic body-contours, gradient-framed glass, floating dock, flourished titles. */

/* topographic contours — the terrain of a body, faint in two corners of every page */
.main::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; opacity: .085;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600' fill='none' stroke='%237ce89e' stroke-width='1.1' stroke-opacity='0.55'%3E%3Cpath d='M367 300L371 307L368 314L362 319L353 322L347 325L342 328L339 332L335 335L330 337L325 338L321 340L319 345L316 353L313 363L307 371L300 374L293 369L288 358L286 346L285 336L283 332L278 333L269 337L260 340L252 339L247 336L244 330L243 324L242 318L241 312L241 306L244 300L250 295L257 291L263 289L264 285L260 279L253 269L248 258L248 248L253 243L262 244L273 249L281 255L288 259L292 259L296 255L300 251L305 248L311 247L316 247L322 246L329 245L337 245L343 247L347 253L346 262L341 273L335 281L333 287L336 289L345 291L357 294L367 300Z'/%3E%3Cpath d='M388 300L389 309L390 318L392 328L393 339L392 349L385 357L374 361L360 360L347 357L337 356L332 360L329 370L326 385L320 399L311 407L300 407L290 401L282 392L275 383L268 377L261 373L254 368L249 363L244 356L239 350L233 345L222 341L210 337L198 331L192 322L194 310L204 300L217 292L229 286L235 280L235 273L230 263L226 251L225 239L229 229L237 223L246 219L255 216L264 212L273 210L282 211L292 215L300 222L307 229L314 231L322 227L334 219L347 211L361 209L371 214L375 225L373 240L369 254L367 264L368 272L373 278L380 284L385 292L388 300Z'/%3E%3Cpath d='M402 300L406 310L416 323L427 339L434 355L431 370L420 380L402 384L384 384L369 384L358 387L351 395L343 404L334 414L324 420L312 424L300 426L287 429L274 433L260 433L247 428L238 417L233 400L232 383L230 370L224 362L212 359L194 357L176 351L163 342L158 328L162 314L170 300L179 288L185 277L190 267L194 256L199 246L206 237L214 229L220 220L224 207L227 191L233 176L244 164L258 162L274 169L288 181L300 194L310 202L320 201L332 195L346 188L362 185L375 187L386 195L394 206L401 217L408 228L413 239L416 252L415 265L410 278L404 290L402 300Z'/%3E%3Cpath d='M430 300L438 314L449 330L457 348L458 365L452 381L441 394L428 405L416 416L403 426L389 433L373 437L356 435L340 432L326 431L313 437L300 449L284 464L265 476L246 478L230 468L220 450L214 429L210 410L203 397L193 388L180 380L167 371L156 360L148 346L142 331L137 316L131 300L127 283L129 266L138 251L155 240L174 233L191 227L201 218L204 204L205 184L207 161L216 142L230 132L249 131L267 136L285 144L300 150L314 153L329 155L343 159L356 165L368 172L381 178L397 182L415 185L435 189L452 199L461 214L459 234L449 255L437 273L430 287L430 300Z'/%3E%3Cpath d='M480 300L480 318L476 335L471 352L465 369L463 387L462 409L461 432L454 454L439 470L417 475L391 471L367 462L347 456L331 457L317 468L300 483L281 496L260 502L239 500L221 490L205 478L190 465L175 452L162 438L153 420L149 401L148 381L146 364L139 349L124 335L104 319L87 300L79 278L84 257L100 239L123 227L144 217L160 206L170 193L176 176L184 158L194 142L209 129L225 119L242 110L260 101L280 93L300 90L320 95L338 110L352 129L363 148L375 159L392 163L414 161L440 160L465 165L482 178L490 199L489 222L484 244L481 264L480 282L480 300Z'/%3E%3Cpath d='M531 300L514 321L495 339L483 355L480 374L485 399L491 427L491 457L481 481L461 496L435 502L408 502L384 502L362 504L341 508L321 512L300 513L279 511L259 508L237 508L213 510L186 513L160 510L137 498L124 476L121 447L124 418L127 392L124 373L111 357L91 342L72 322L59 300L56 276L62 253L73 231L84 211L96 191L110 173L127 158L147 147L169 140L189 134L206 124L220 106L234 83L252 59L275 43L300 41L324 53L345 75L361 97L377 115L394 124L414 129L436 134L458 142L476 155L491 172L505 190L519 209L532 230L541 252L541 276L531 300Z'/%3E%3Cpath d='M568 300L547 324L529 346L520 367L518 390L517 416L513 442L503 466L487 487L470 507L451 525L430 544L407 559L381 566L352 563L325 550L300 534L278 522L256 520L231 528L201 540L168 547L138 543L115 525L101 499L94 469L90 441L83 416L74 393L65 371L58 348L56 324L54 300L52 276L46 249L38 221L35 190L42 162L61 140L91 128L124 124L154 122L177 116L193 100L207 76L224 51L246 30L272 20L300 20L327 27L352 37L377 46L401 57L422 72L440 91L453 114L465 135L479 153L499 167L527 179L557 194L581 215L591 242L586 272L568 300Z'/%3E%3Cpath d='M604 300L598 329L591 358L580 385L564 410L544 430L522 448L503 467L491 491L482 522L472 557L454 589L428 609L395 612L360 601L328 583L300 567L274 560L248 560L220 565L190 566L160 561L133 550L108 534L84 516L60 497L38 475L23 448L18 417L24 384L36 353L45 325L46 300L35 274L17 244L1 209L-2 175L9 145L34 122L65 107L96 96L123 84L147 70L169 55L193 42L219 33L246 26L272 19L300 9L330 -4L362 -14L395 -14L424 -0L446 27L459 62L468 95L479 121L498 138L525 150L557 163L585 182L604 208L611 238L610 270L604 300Z'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600' fill='none' stroke='%237ce89e' stroke-width='1.1' stroke-opacity='0.55'%3E%3Cpath d='M367 300L371 307L368 314L362 319L353 322L347 325L342 328L339 332L335 335L330 337L325 338L321 340L319 345L316 353L313 363L307 371L300 374L293 369L288 358L286 346L285 336L283 332L278 333L269 337L260 340L252 339L247 336L244 330L243 324L242 318L241 312L241 306L244 300L250 295L257 291L263 289L264 285L260 279L253 269L248 258L248 248L253 243L262 244L273 249L281 255L288 259L292 259L296 255L300 251L305 248L311 247L316 247L322 246L329 245L337 245L343 247L347 253L346 262L341 273L335 281L333 287L336 289L345 291L357 294L367 300Z'/%3E%3Cpath d='M388 300L389 309L390 318L392 328L393 339L392 349L385 357L374 361L360 360L347 357L337 356L332 360L329 370L326 385L320 399L311 407L300 407L290 401L282 392L275 383L268 377L261 373L254 368L249 363L244 356L239 350L233 345L222 341L210 337L198 331L192 322L194 310L204 300L217 292L229 286L235 280L235 273L230 263L226 251L225 239L229 229L237 223L246 219L255 216L264 212L273 210L282 211L292 215L300 222L307 229L314 231L322 227L334 219L347 211L361 209L371 214L375 225L373 240L369 254L367 264L368 272L373 278L380 284L385 292L388 300Z'/%3E%3Cpath d='M402 300L406 310L416 323L427 339L434 355L431 370L420 380L402 384L384 384L369 384L358 387L351 395L343 404L334 414L324 420L312 424L300 426L287 429L274 433L260 433L247 428L238 417L233 400L232 383L230 370L224 362L212 359L194 357L176 351L163 342L158 328L162 314L170 300L179 288L185 277L190 267L194 256L199 246L206 237L214 229L220 220L224 207L227 191L233 176L244 164L258 162L274 169L288 181L300 194L310 202L320 201L332 195L346 188L362 185L375 187L386 195L394 206L401 217L408 228L413 239L416 252L415 265L410 278L404 290L402 300Z'/%3E%3Cpath d='M430 300L438 314L449 330L457 348L458 365L452 381L441 394L428 405L416 416L403 426L389 433L373 437L356 435L340 432L326 431L313 437L300 449L284 464L265 476L246 478L230 468L220 450L214 429L210 410L203 397L193 388L180 380L167 371L156 360L148 346L142 331L137 316L131 300L127 283L129 266L138 251L155 240L174 233L191 227L201 218L204 204L205 184L207 161L216 142L230 132L249 131L267 136L285 144L300 150L314 153L329 155L343 159L356 165L368 172L381 178L397 182L415 185L435 189L452 199L461 214L459 234L449 255L437 273L430 287L430 300Z'/%3E%3Cpath d='M480 300L480 318L476 335L471 352L465 369L463 387L462 409L461 432L454 454L439 470L417 475L391 471L367 462L347 456L331 457L317 468L300 483L281 496L260 502L239 500L221 490L205 478L190 465L175 452L162 438L153 420L149 401L148 381L146 364L139 349L124 335L104 319L87 300L79 278L84 257L100 239L123 227L144 217L160 206L170 193L176 176L184 158L194 142L209 129L225 119L242 110L260 101L280 93L300 90L320 95L338 110L352 129L363 148L375 159L392 163L414 161L440 160L465 165L482 178L490 199L489 222L484 244L481 264L480 282L480 300Z'/%3E%3Cpath d='M531 300L514 321L495 339L483 355L480 374L485 399L491 427L491 457L481 481L461 496L435 502L408 502L384 502L362 504L341 508L321 512L300 513L279 511L259 508L237 508L213 510L186 513L160 510L137 498L124 476L121 447L124 418L127 392L124 373L111 357L91 342L72 322L59 300L56 276L62 253L73 231L84 211L96 191L110 173L127 158L147 147L169 140L189 134L206 124L220 106L234 83L252 59L275 43L300 41L324 53L345 75L361 97L377 115L394 124L414 129L436 134L458 142L476 155L491 172L505 190L519 209L532 230L541 252L541 276L531 300Z'/%3E%3Cpath d='M568 300L547 324L529 346L520 367L518 390L517 416L513 442L503 466L487 487L470 507L451 525L430 544L407 559L381 566L352 563L325 550L300 534L278 522L256 520L231 528L201 540L168 547L138 543L115 525L101 499L94 469L90 441L83 416L74 393L65 371L58 348L56 324L54 300L52 276L46 249L38 221L35 190L42 162L61 140L91 128L124 124L154 122L177 116L193 100L207 76L224 51L246 30L272 20L300 20L327 27L352 37L377 46L401 57L422 72L440 91L453 114L465 135L479 153L499 167L527 179L557 194L581 215L591 242L586 272L568 300Z'/%3E%3Cpath d='M604 300L598 329L591 358L580 385L564 410L544 430L522 448L503 467L491 491L482 522L472 557L454 589L428 609L395 612L360 601L328 583L300 567L274 560L248 560L220 565L190 566L160 561L133 550L108 534L84 516L60 497L38 475L23 448L18 417L24 384L36 353L45 325L46 300L35 274L17 244L1 209L-2 175L9 145L34 122L65 107L96 96L123 84L147 70L169 55L193 42L219 33L246 26L272 19L300 9L330 -4L362 -14L395 -14L424 -0L446 27L459 62L468 95L479 121L498 138L525 150L557 163L585 182L604 208L611 238L610 270L604 300Z'/%3E%3C/svg%3E");
  background-repeat: no-repeat, no-repeat;
  background-position: right -190px top -190px, left -240px bottom -260px;
  background-size: 560px 560px, 660px 660px; }
html.light .main::after { opacity: .06; }

/* cards — lit gradient frames (the border itself carries light) */
.card { border: 1px solid transparent;
  background:
    linear-gradient(180deg, rgba(255,255,255,.028), rgba(255,255,255,0) 46%) padding-box,
    linear-gradient(var(--panel-solid), var(--panel-solid)) padding-box,
    linear-gradient(160deg, rgb(var(--accent-rgb) / 0.34), rgba(255,255,255,.06) 34%, rgba(255,255,255,.05) 62%, rgb(var(--accent-rgb) / 0.26)) border-box; }
html.light .card {
  background:
    linear-gradient(180deg, rgba(255,255,255,.6), rgba(255,255,255,0) 46%) padding-box,
    linear-gradient(var(--panel-solid), var(--panel-solid)) padding-box,
    linear-gradient(160deg, rgb(var(--accent-rgb) / 0.35), rgba(0,0,0,.07) 36%, rgba(0,0,0,.06) 62%, rgb(var(--accent-rgb) / 0.28)) border-box; }
html.light .card:hover { border-color: transparent; }
/* .set-group and .twin-list share the v84 dark surface rule with .card, but only .card ever got
   a light-mode counterpart — so in light mode every Settings panel rendered as a near-black slab
   inside a white modal, with its --text row labels ("Theme", "Accent color", "Text size",
   "Reduce motion", "Press Enter to send") at 1.19:1, i.e. invisible. Insight lists on My Data
   and the Watch-outs lists went black inside white cards at 3.03:1. */
html.light .set-group, html.light .twin-list {
  background: linear-gradient(180deg, rgba(255,255,255,.6), rgba(255,255,255,0) 46%), var(--panel-solid);
  border-color: rgba(0,0,0,.10);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.7);
}

/* every page title carries the pulse — a glowing vital underline */
.page-title { position: relative; padding-bottom: 12px; margin-bottom: 4px; }
.page-title::after { content: ""; position: absolute; left: 2px; bottom: 0; height: 3px; width: 64px; border-radius: 99px;
  background: linear-gradient(90deg, var(--green), rgb(var(--accent-rgb) / 0)); box-shadow: 0 0 14px rgb(var(--accent-rgb) / 0.55); }
.nav-item.active .ic { filter: drop-shadow(0 0 6px rgb(var(--accent-rgb) / 0.6)); }
.user .avatar { box-shadow: 0 0 0 2px var(--bg-2), 0 0 0 3.5px rgb(var(--accent-rgb) / 0.5); }

/* mobile tab bar — a floating glass dock */
.tabbar { margin: 6px 10px calc(8px + env(safe-area-inset-bottom, 0px)); padding: 7px 10px; border: 1px solid var(--hairline);
  border-radius: 22px; background: rgba(16, 20, 15, .74); backdrop-filter: blur(16px) saturate(1.2); -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: 0 18px 44px -18px rgba(0,0,0,.65), inset 0 1px 0 rgba(255,255,255,.05); }
html.light .tabbar { background: rgba(255,255,255,.72); box-shadow: 0 18px 44px -20px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.7); }

/* composer — a glass capsule that lights up when you speak to it */
.composer { background: rgba(22, 27, 21, .7); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
.composer:focus-within { border-color: var(--green);
  box-shadow: 0 0 0 4px rgb(var(--accent-rgb) / 0.10), 0 10px 34px -14px rgb(var(--accent-rgb) / 0.4); }
html.light .composer { background: rgba(255,255,255,.8); }

/* Hale's replies carry a vital thread down their left edge */
.msg.bot .m-col { border-left: 2px solid transparent; padding-left: 14px; margin-left: -16px;
  border-image: linear-gradient(180deg, rgb(var(--accent-rgb) / 0.55), rgb(var(--accent-rgb) / 0.06)) 1; }

/* home stats read like instruments */
.stat-val { font-family: var(--font-display); font-size: 1.16rem; letter-spacing: .2px; }

/* landing: ghost numerals watermark the three steps */
.lp-step { position: relative; overflow: hidden; }
.lp-step::after { position: absolute; right: -4px; top: -26px; font: 800 110px/1 var(--font-display);
  color: rgb(var(--accent-rgb) / 0.08); pointer-events: none; }
.lp-step:nth-of-type(1)::after { content: "1"; }
.lp-step:nth-of-type(2)::after { content: "2"; }
.lp-step:nth-of-type(3)::after { content: "3"; }

/* landing hero — a soft halo breathes behind the headline */
.lp-hero { position: relative; }
.lp-hero::before { content: ""; position: absolute; left: 50%; top: 8%; width: min(720px, 90%); height: 300px; z-index: -1;
  transform: translateX(-50%); border-radius: 50%; filter: blur(60px);
  background: radial-gradient(closest-side, rgb(var(--accent-rgb) / 0.16), transparent 70%);
  animation: haloBreathe 7s ease-in-out infinite; }
@keyframes haloBreathe { 0%, 100% { opacity: .7; } 50% { opacity: 1; } }

@media (prefers-reduced-motion: reduce) { .lp-hero::before { animation: none; } }

/* ============================================================ GRADIENT IDENTITY v39 — the accent is a blend, not a color.
   Everything that used to be flat green now flows teal → green, like light moving through water. */

/* the flow, as reusable strokes of paint */
:root {
}

/* greetings & page titles speak in the gradient */
.hero-tag { background: linear-gradient(100deg, var(--green-2), var(--green) 36%, var(--green-3) 100%);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
html.light .hero-tag { background: linear-gradient(100deg, #0d9488, #16a34a 55%, #4ade80);
  -webkit-background-clip: text; background-clip: text; }
html.light .page-title { background: linear-gradient(105deg, #14271c 42%, #16a34a 82%, #0d9488 112%);
  -webkit-background-clip: text; background-clip: text; }

/* landing headline — the same water, slowly moving */
.lp-hero h1 { background: linear-gradient(92deg, #3ecf8e, #4ade80 24%, #6ee7a0 50%, #4ade80 76%, #3ecf8e);
  background-size: 240% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; }

/* the EKG lines are drawn in the gradient itself */
.hero-ekg path, .lp-ekg path { stroke: url(#haleFlow); }

/* active nav pill — teal enters from the left */
.nav-item.active { background: linear-gradient(90deg, rgb(var(--accent-rgb) / 0.16), rgb(var(--accent-rgb) / 0.07) 55%, transparent); }

/* small badges join the family */
.pro-tag { border-color: transparent; background: linear-gradient(115deg, rgb(var(--accent-rgb) / 0.18), rgb(var(--accent-rgb) / 0.14));
  color: var(--green-2); }

/* ============================================================ LANDING SHOW v42 — the ad page performs the product. */

/* --- hero: live metric chips floating in the space around the headline --- */
.lp-float { position: absolute; inset: 0; pointer-events: none; }
.lp-chip { position: absolute; display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 14px; border-radius: 999px; font: 600 0.8rem var(--font); color: var(--text-dim);
  background: rgba(20, 26, 21, .72); border: 1px solid var(--hairline);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 14px 34px -16px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.05);
  opacity: 0; animation: chipIn 1s var(--ease) forwards, chipFloat 7s ease-in-out infinite; }
.lp-chip b { color: var(--text); }
.lp-chip i { width: 8px; height: 8px; border-radius: 50%; }
.lp-chip .up { background: var(--green); box-shadow: 0 0 10px rgb(var(--accent-rgb) / .8); }
.lp-chip .dn { background: #3ecf8e; box-shadow: 0 0 10px rgb(62 207 142 / .8); }
.lp-chip .ok { background: var(--green-3); box-shadow: 0 0 10px rgb(var(--accent-rgb) / .6); }
.lp-chip.c1 { left: 4%;  top: 26%; animation-delay: .5s, .5s; }
.lp-chip.c2 { right: 5%; top: 22%; animation-delay: .9s, 1.6s; }
.lp-chip.c3 { left: 7%;  top: 62%; animation-delay: 1.3s, 3.1s; }
.lp-chip.c4 { right: 8%; top: 58%; animation-delay: 1.7s, 4.4s; }
@keyframes chipIn { from { opacity: 0; transform: translateY(14px) scale(.92); } to { opacity: 1; transform: none; } }
@keyframes chipFloat { 0%,100% { translate: 0 0; } 50% { translate: 0 -9px; } }
@media (max-width: 900px) { .lp-float { display: none; } }
html.light .lp-chip { background: rgba(255,255,255,.75); }

/* --- shared spring for the show --- */
.lp-sec.seen .lp-viz > svg { animation: vizEnter .7s cubic-bezier(.34,1.56,.64,1) both; }
@keyframes vizEnter { from { opacity: 0; transform: scale(.94) translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes vzValPop { 0%, 40% { opacity: 0; transform: scale(.4); } 50%, 88% { opacity: 1; transform: scale(1); } 96%, 100% { opacity: 0; } }
@keyframes vzMem { 0%, 62% { opacity: 0; transform: translateY(6px); } 70%, 92% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes vzDelta { 0%, 52% { opacity: 0; transform: scale(.6); } 60%, 94% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }

/* --- protocols: the week fills itself in --- */
.vz-prog-bg { fill: rgba(255,255,255,0.07); }
.vz-prog { fill: var(--green); transform-box: fill-box; transform-origin: left center; animation: vzProg 7.5s var(--ease) infinite; }
@keyframes vzProg { 0% { transform: scaleX(0); } 12% { transform: scaleX(.12); } 24% { transform: scaleX(.38); }
  36% { transform: scaleX(.66); } 48%, 90% { transform: scaleX(1); } 100% { transform: scaleX(1); opacity: .4; } }
html.light .vz-prog-bg { fill: rgba(0,0,0,0.08); }

/* --- modes: the scanner reads, the bars train --- */
.vz-scan { fill: rgb(var(--accent-rgb) / 0.14); animation: vzScan 6s var(--ease) infinite; }
@keyframes vzScan { 0%, 12% { transform: translateY(0); } 25%, 37% { transform: translateY(20px); }
  50%, 62% { transform: translateY(40px); } 75%, 87% { transform: translateY(60px); } 100% { transform: translateY(0); } }
.vz-bar { fill: rgb(var(--accent-rgb) / 0.45); transform-box: fill-box; transform-origin: center bottom;
  animation: vzGrow 6s cubic-bezier(.34,1.56,.64,1) infinite; }
.vz-bar.b2 { animation-delay: .35s; fill: rgb(var(--accent-rgb) / 0.6); }
.vz-bar.b3 { animation-delay: .7s; fill: var(--green); }
@keyframes vzGrow { 0%, 8% { transform: scaleY(0); } 22%, 88% { transform: scaleY(1); } 98%, 100% { transform: scaleY(0); } }

/* --- shop: shelf-browsing — each card takes its turn in the light --- */
.vz-card { transform-box: fill-box; transform-origin: center; animation: vzBrowse 6s var(--ease) infinite; }
.vz-card .face { fill: rgba(255,255,255,0.05); stroke: var(--hairline); transition: none; }
.vz-card.k2 { animation-delay: 2s; }
@keyframes vzBrowse { 0%, 28%, 100% { transform: translateY(0); } 8%, 18% { transform: translateY(-7px); } }
.vz-card.k1 .face { animation: vzFace 6s var(--ease) infinite; }
.vz-card.k2 .face { animation: vzFace 6s var(--ease) infinite 2s; }
.vz-card.k3 .face { animation: vzFace 6s var(--ease) infinite 4s; }
@keyframes vzFace { 0%, 28%, 100% { stroke: rgba(255,255,255,0.08); } 8%, 18% { stroke: rgb(var(--accent-rgb) / 0.6); } }

/* --- privacy: the shield sends out a protective pulse --- */
.vz-shield { fill: rgb(var(--accent-rgb) / 0.07); stroke: var(--green); stroke-width: 2.5;
  stroke-dasharray: 430; stroke-dashoffset: 430; animation: vzShield 6s var(--ease) infinite; }
@keyframes vzShield { 0% { stroke-dashoffset: 430; } 35%, 92% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; opacity: .5; } }
.vz-lockchk { fill: none; stroke: var(--green-3); stroke-width: 4; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 70; stroke-dashoffset: 70; animation: vzLock 6s var(--ease) infinite; }
@keyframes vzLock { 0%, 38% { stroke-dashoffset: 70; } 55%, 100% { stroke-dashoffset: 0; } }
.vz-pulse { fill: none; stroke: rgb(var(--accent-rgb) / 0.5); stroke-width: 2;
  transform-box: fill-box; transform-origin: center; animation: vzPulse 6s var(--ease) infinite .8s; }
@keyframes vzPulse { 0%, 55% { transform: scale(.6); opacity: 0; } 65% { opacity: .8; } 100% { transform: scale(2.2); opacity: 0; } }

/* --- stillness for those who ask --- */
@media (prefers-reduced-motion: reduce) {
  .lp-chip { animation: none; opacity: 1; }
  .lp-sec.seen .lp-viz > svg,
  .vz-val, .vz-mem, .vz-delta, .vz-prog, .vz-scan, .vz-bar, .vz-card, .vz-card .face,
  .vz-shield, .vz-lockchk, .vz-pulse { animation: none; opacity: 1; transform: none; stroke-dashoffset: 0; }
}

/* ============================================================ SCENE CHOREOGRAPHY v43 — every panel is a mini-movie.
   A cursor performs the app; numbers land like heartbeats; text types itself. Sound-off legible. */

/* --- the cursor actor (shared) --- */
.sc-cursor .dot, .sc-cursor2 .dot, .sc-cursor3 .dot { fill: rgba(255,255,255,.92); }
.sc-cursor .halo, .sc-cursor2 .halo, .sc-cursor3 .halo { fill: none; stroke: rgb(var(--accent-rgb) / .6); stroke-width: 1.5; }
html.light .sc-cursor .dot, html.light .sc-cursor2 .dot, html.light .sc-cursor3 .dot { fill: rgba(20,30,24,.9); }
@keyframes scDoc { 0% { opacity: 0; transform: translateX(-28px) rotate(-4deg); }
  8%, 88% { opacity: 1; transform: none; } 96%, 100% { opacity: 0; } }
.sc-fly { fill: var(--green); filter: drop-shadow(0 0 5px rgb(var(--accent-rgb) / .8)); opacity: 0; }
.sc-fly.f1 { animation: scFly1 9s var(--ease) infinite; }
.sc-fly.f2 { animation: scFly2 9s var(--ease) infinite; }
.sc-fly.f3 { animation: scFly3 9s var(--ease) infinite; }
@keyframes scFly1 { 0%, 9% { opacity: 0; transform: translate(48px, 122px) scale(.4); }
  11% { opacity: 1; } 15% { opacity: 1; transform: translate(96px, 124px) scale(1); } 17%, 100% { opacity: 0; transform: translate(96px, 124px); } }
@keyframes scFly2 { 0%, 17% { opacity: 0; transform: translate(48px, 122px) scale(.4); }
  19% { opacity: 1; } 23% { opacity: 1; transform: translate(160px, 98px) scale(1); } 25%, 100% { opacity: 0; transform: translate(160px, 98px); } }
@keyframes scFly3 { 0%, 25% { opacity: 0; transform: translate(48px, 122px) scale(.4); }
  27% { opacity: 1; } 31% { opacity: 1; transform: translate(232px, 74px) scale(1); } 33%, 100% { opacity: 0; transform: translate(232px, 74px); } }
@keyframes scPt  { 0%, 14% { opacity: 0; transform: scale(0); } 18%, 92% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }
@keyframes scPt2 { 0%, 22% { opacity: 0; transform: scale(0); } 26%, 92% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }
@keyframes scPt3 { 0%, 30% { opacity: 0; transform: scale(0); } 34%, 92% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }
@keyframes scNum1 { 0%, 16% { opacity: 0; } 20%, 92% { opacity: 1; } 98%, 100% { opacity: 0; } }
@keyframes scNum2 { 0%, 24% { opacity: 0; } 28%, 92% { opacity: 1; } 98%, 100% { opacity: 0; } }
@keyframes scNum3 { 0%, 32% { opacity: 0; } 36%, 92% { opacity: 1; } 98%, 100% { opacity: 0; } }
@keyframes scLine { 0%, 13% { stroke-dashoffset: 220; } 36%, 92% { stroke-dashoffset: 0; } 98%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scBad  { 0%, 18% { opacity: 0; transform: translateY(5px); } 22%, 40% { opacity: 1; transform: none; } 46%, 100% { opacity: 0; } }
@keyframes scGood { 0%, 40% { opacity: 0; transform: scale(.6); } 46%, 92% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }
@keyframes scU { 0% { opacity: 0; transform: translateY(10px) scale(.96); } 5%, 94% { opacity: 1; transform: none; } 99%, 100% { opacity: 0; } }
.sc-t { animation: scT 11s var(--ease) infinite; }
@keyframes scT { 0%, 7% { opacity: 0; } 10%, 24% { opacity: 1; } 27%, 100% { opacity: 0; } }
.sc-r { animation: scR 11s var(--ease) infinite; }
@keyframes scR { 0%, 26% { opacity: 0; transform: translateY(8px); } 30%, 94% { opacity: 1; transform: none; } 99%, 100% { opacity: 0; } }
.sc-clip1 { animation: scType1 11s steps(24) infinite; }
@keyframes scType1 { 0%, 30% { width: 0; } 48%, 100% { width: 210px; } }
.sc-clip2 { animation: scType2 11s steps(22) infinite; }
@keyframes scType2 { 0%, 50% { width: 0; } 64%, 100% { width: 195px; } }
.sc-mini { fill: none; stroke: var(--green); stroke-width: 2.5; stroke-linecap: round;
  stroke-dasharray: 135; stroke-dashoffset: 135; animation: scMini 11s var(--ease) infinite; }
@keyframes scMini { 0%, 64% { stroke-dashoffset: 135; } 78%, 100% { stroke-dashoffset: 0; } }
.sc-mpt { fill: var(--green-3); transform-box: fill-box; transform-origin: center; opacity: 0; }
.sc-mpt.m1 { animation: scMpt1 11s var(--ease) infinite; }
.sc-mpt.m2 { animation: scMpt2 11s var(--ease) infinite; }
@keyframes scMpt1 { 0%, 64% { opacity: 0; } 67%, 94% { opacity: 1; } 99%, 100% { opacity: 0; } }
@keyframes scMpt2 { 0%, 77% { opacity: 0; } 80%, 94% { opacity: 1; } 99%, 100% { opacity: 0; } }
.sc-mlab { fill: var(--text-faint); font: 700 9.5px var(--font); opacity: 0; }
.sc-mlab.a { animation: scMpt1 11s var(--ease) infinite; }
.sc-mlab.b { fill: var(--green-3); animation: scMpt2 11s var(--ease) infinite; }
.sc-mem rect { fill: rgb(var(--accent-rgb) / .13); stroke: rgb(var(--accent-rgb) / .38); }
.sc-mem text { fill: var(--green-2); font: 600 10.5px var(--font); }
.sc-mem { transform-box: fill-box; transform-origin: center; opacity: 0; animation: scMem 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scMem { 0%, 82% { opacity: 0; transform: scale(.6); } 86%, 95% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }
@keyframes scFill { 0%, 22% { transform: scaleX(.08); } 38%, 94% { transform: scaleX(1); } 100% { transform: scaleX(.08); } }
@keyframes scKnob { 0%, 22% { transform: translateX(0); } 38%, 94% { transform: translateX(54px); } 100% { transform: translateX(0); } }
.sc-cursor { animation: scCursor 11s var(--ease) infinite; opacity: 0; }
@keyframes scCursor {
  0%, 6%  { opacity: 0; transform: translate(150px, 130px); }
  10%     { opacity: 1; transform: translate(150px, 130px); }
  20%     { opacity: 1; transform: translate(182px, 27px) scale(1); }
  22%     { transform: translate(182px, 27px) scale(.78); }
  38%     { transform: translate(236px, 27px) scale(.78); }
  40%     { transform: translate(236px, 27px) scale(1); }
  46%, 100% { opacity: 0; transform: translate(252px, 70px); } }
.sc-cur { fill: none; stroke: rgba(255,255,255,0.22); stroke-width: 2.5; stroke-dasharray: 205; stroke-dashoffset: 205;
  animation: scCurP 11s var(--ease) infinite; }
@keyframes scCurP { 0%, 5% { stroke-dashoffset: 205; } 18%, 96% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; } }
@keyframes scWhatP { 0%, 39% { stroke-dashoffset: 220; } 58%, 94% { stroke-dashoffset: 0; } 99%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scRing { 0%, 56% { opacity: 0; transform: scale(0); } 60%, 94% { opacity: 1; transform: scale(1); } 99%, 100% { opacity: 0; } }
@keyframes scWave { 0%, 58% { opacity: 0; transform: scale(1); } 62% { opacity: .8; } 74% { opacity: 0; transform: scale(2.6); }
  76% { opacity: .8; transform: scale(1); } 88%, 100% { opacity: 0; transform: scale(2.6); } }
@keyframes scDelta { 0%, 60% { opacity: 0; transform: scale(.6) translateY(6px); } 65%, 94% { opacity: 1; transform: none; } 99%, 100% { opacity: 0; } }

/* ============ SCENE 4 — PROTOCOLS: the cursor checks the week; the plan re-tunes itself (11s) ============ */
.sc-row .bg, .sc-old .bg, .sc-new .bg { fill: rgba(255,255,255,0.05); }
.sc-box { fill: none; stroke: rgba(255,255,255,0.28); stroke-width: 1.6; }
.sc-box.on { stroke: var(--green); fill: rgb(var(--accent-rgb) / .18); }
.sc-chk { fill: none; stroke: var(--green); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 26; stroke-dashoffset: 26; }
.sc-chk.k1 { animation: scChk1 11s var(--ease) infinite; }
.sc-chk.k2 { animation: scChk2 11s var(--ease) infinite; }
@keyframes scChk1 { 0%, 12% { stroke-dashoffset: 26; } 17%, 96% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 26; } }
@keyframes scChk2 { 0%, 24% { stroke-dashoffset: 26; } 29%, 96% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 26; } }
.sc-cursor2 { animation: scCursor2 11s var(--ease) infinite; opacity: 0; }
@keyframes scCursor2 {
  0%, 4%  { opacity: 0; transform: translate(120px, 150px); }
  8%      { opacity: 1; transform: translate(120px, 150px); }
  12%     { transform: translate(37px, 30px) scale(1); }
  14%     { transform: translate(37px, 30px) scale(.75); }
  16%     { transform: translate(37px, 30px) scale(1); }
  24%     { transform: translate(37px, 74px) scale(1); }
  26%     { transform: translate(37px, 74px) scale(.75); }
  28%     { transform: translate(37px, 74px) scale(1); }
  36%, 100% { opacity: 0; transform: translate(90px, 150px); } }
.sc-old { animation: scOld 11s var(--ease) infinite; }
@keyframes scOld { 0%, 40% { opacity: 1; transform: none; } 48%, 100% { opacity: 0; transform: translateX(26px); } }
.sc-new { animation: scNew 11s cubic-bezier(.34,1.56,.64,1) infinite; opacity: 0; }
@keyframes scNew { 0%, 46% { opacity: 0; transform: translateX(-26px); } 54%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
.sc-new .tag rect { fill: rgb(var(--accent-rgb) / .16); stroke: rgb(var(--accent-rgb) / .45); }
.sc-new .tag text { fill: var(--green-2); font: 700 9.5px var(--font); }
.sc-retune { transform-box: fill-box; transform-origin: center; fill: none; stroke: var(--green-2); stroke-width: 2.5; stroke-linecap: round;
  animation: scSpin 11s var(--ease) infinite; }
.sc-retune path:last-child { fill: var(--green-2); stroke: none; }
@keyframes scSpin { 0%, 40% { transform: rotate(0deg); opacity: .5; } 56%, 96% { transform: rotate(360deg); opacity: 1; } 100% { transform: rotate(360deg); opacity: .5; } }
.sc-prog { fill: var(--green); transform-box: fill-box; transform-origin: left center; animation: scProg 11s var(--ease) infinite; }
@keyframes scProg { 0%, 13% { transform: scaleX(0); } 17%, 24% { transform: scaleX(.33); } 29%, 52% { transform: scaleX(.66); }
  57%, 96% { transform: scaleX(1); } 100% { transform: scaleX(0); } }

/* ============ SCENE 5 — MODES: sources get cited (8s, bars/scan keep their rhythm) ============ */
.sc-cite rect { fill: rgb(var(--accent-rgb) / .12); stroke: rgb(var(--accent-rgb) / .35); }
.sc-cite text { fill: var(--green-2); font: 600 10px var(--font); }
.sc-cite { transform-box: fill-box; transform-origin: center; opacity: 0; }
@keyframes scCite1 { 0%, 30% { opacity: 0; transform: scale(.6); } 36%, 94% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }
@keyframes scCite2 { 0%, 55% { opacity: 0; transform: scale(.6); } 61%, 94% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }

/* ============ SCENE 6 — SHOP: browse, pick, link out (9s) ============ */
.vz-card { animation: none; }
.vz-card.k1, .vz-card.k3 { transform-box: fill-box; transform-origin: center; animation: scBob 9s ease-in-out infinite; }
.vz-card.k3 { animation-delay: 1.2s; }
@keyframes scBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.vz-card.hero { transform-box: fill-box; transform-origin: center bottom; animation: scHero 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scHero { 0%, 20% { transform: translateY(0) scale(1); } 26%, 66% { transform: translateY(-9px) scale(1.04); } 74%, 100% { transform: translateY(0) scale(1); } }
.vz-card.hero .face { animation: scHeroFace 9s var(--ease) infinite; }
@keyframes scHeroFace { 0%, 20% { stroke: rgba(255,255,255,0.08); } 26%, 66% { stroke: rgb(var(--accent-rgb) / .65); } 74%, 100% { stroke: rgba(255,255,255,0.08); } }
.sc-out rect { fill: rgb(var(--accent-rgb) / .15); stroke: rgb(var(--accent-rgb) / .42); }
.sc-out text { fill: var(--green-3); font: 700 11px var(--font); }
.sc-out { transform-box: fill-box; transform-origin: center bottom; opacity: 0; animation: scOut 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scOut { 0%, 26% { opacity: 0; transform: translateY(8px) scale(.7); } 32%, 66% { opacity: 1; transform: none; } 74%, 100% { opacity: 0; } }
.sc-cursor3 { animation: scCursor3 9s var(--ease) infinite; opacity: 0; }
@keyframes scCursor3 {
  0%, 6% { opacity: 0; transform: translate(210px, 150px); }
  10% { opacity: 1; transform: translate(210px, 150px); }
  20% { transform: translate(129px, 96px) scale(1); }
  23% { transform: translate(129px, 96px) scale(.75); }
  26% { transform: translate(129px, 96px) scale(1); }
  60% { opacity: 1; transform: translate(129px, 96px); }
  70%, 100% { opacity: 0; transform: translate(160px, 150px); } }

/* ============ SCENE 7 — PRIVACY: your numbers fly into the vault; it seals (10s) ============ */
.sc-dchip rect { fill: var(--elev-2); stroke: var(--hairline); }
.sc-dchip text { fill: var(--text-dim); font: 600 10px var(--font); }
.sc-dchip { transform-box: fill-box; transform-origin: center; }
@keyframes scD1 { 0%, 8% { opacity: 1; transform: none; } 22%, 96% { opacity: 0; transform: translate(78px, 62px) scale(.25); } 100% { opacity: 1; transform: none; } }
@keyframes scD2 { 0%, 14% { opacity: 1; transform: none; } 28%, 96% { opacity: 0; transform: translate(0px, 70px) scale(.25); } 100% { opacity: 1; transform: none; } }
@keyframes scD3 { 0%, 20% { opacity: 1; transform: none; } 34%, 96% { opacity: 0; transform: translate(-78px, 60px) scale(.25); } 100% { opacity: 1; transform: none; } }
@keyframes scShield { 0%, 26% { stroke-dashoffset: 430; } 54%, 94% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: 0; opacity: .5; } }
@keyframes scLock { 0%, 55% { stroke-dashoffset: 70; } 66%, 100% { stroke-dashoffset: 0; } }
@keyframes scPulse { 0%, 66% { transform: scale(.6); opacity: 0; } 72% { opacity: .8; } 84% { transform: scale(2.2); opacity: 0; }
  86% { transform: scale(.6); opacity: .6; } 96%, 100% { transform: scale(2); opacity: 0; } }

/* --- stillness: show the finished poses --- */
@media (prefers-reduced-motion: reduce) {
  .sc-doc, .sc-pt, .sc-num, .sc-flag.good, .sc-u, .sc-r, .sc-mem, .sc-ring, .sc-delta,
  .sc-new .tag, .sc-cite, .sc-out, .sc-dchip { animation: none; opacity: 1; transform: none; }
  .sc-fly, .sc-flag.bad, .sc-t, .sc-cursor, .sc-cursor2, .sc-cursor3, .sc-old { animation: none; opacity: 0; }
  .sc-new { animation: none; opacity: 1; transform: none; }
  .sc-line, .sc-mini, .sc-cur, .sc-what, .sc-chk, .vz-shield, .vz-lockchk { animation: none; stroke-dashoffset: 0; }
  .sc-clip1, .sc-clip2 { animation: none; width: 220px; }
  .sc-ctl .fill, .sc-prog { animation: none; transform: scaleX(1); }
  .sc-ringWave, .vz-pulse { animation: none; opacity: 0; }
}
.btn.primary::after { background: linear-gradient(100deg, transparent, rgba(255,255,255,.18), transparent); }

/* headlines — edge-burned: dark teal into full green into deep green. No pastels. */
.hero-tag { background: linear-gradient(100deg, var(--green-deep), var(--green-2) 18%, var(--green) 55%, var(--green-deep) 110%);
  -webkit-background-clip: text; background-clip: text; }
.lp-hero h1 { background: linear-gradient(92deg, var(--green-deep), var(--green-2) 20%, var(--green) 50%, var(--green-2) 80%, var(--green-deep));
  background-size: 220% 100%; -webkit-background-clip: text; background-clip: text; animation-duration: 14s; }

/* the vital underline — a hard two-tone bar, not a fading wisp */
.page-title::after { height: 4px; border-radius: 2px; width: 58px;
  background: linear-gradient(90deg, var(--green) 0 62%, var(--green-deep) 62% 100%);
  box-shadow: 0 0 8px rgb(var(--accent-rgb) / .5); }

/* avatars & badges — forged from the deep end */
.avatar { background: linear-gradient(145deg, var(--green-deep), var(--green-2) 58%, var(--green) 135%); }
.msg.bot .m-avatar { background: linear-gradient(145deg, var(--green-deep), var(--green-2) 58%, var(--green) 135%); }
.lp-step-n { background: linear-gradient(145deg, var(--green-deep), var(--green-2)); color: var(--on-accent, #eafff2); }

/* the EKG runs dark→bright, like a signal gaining strength */
/* (stops swapped in the shared defs) */
.hero-ekg path, .lp-ekg path { filter: drop-shadow(0 0 4px rgb(var(--accent-rgb) / 0.5)); }

/* tighter, electric glows — no haze */
.nav-item.active { box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / 0.32), 0 0 16px -8px rgb(var(--accent-rgb) / 0.6); }
@keyframes scBeam { 0%, 4% { opacity: 0; transform: translateY(0); } 6% { opacity: 1; }
  13% { opacity: 1; transform: translateY(58px); } 15%, 100% { opacity: 0; transform: translateY(58px); } }

/* scene 2: Hale's avatar joins the reply; follow-up pills invite the next question */
.sc-hav circle { fill: var(--green-deep); }
.sc-hav text { fill: #eafff2; font: 800 9px var(--font-display); }
.sc-hav { opacity: 0; animation: scR 11s var(--ease) infinite; }
.sc-fup rect { fill: rgba(255,255,255,.05); stroke: var(--hairline); }
.sc-fup text { fill: var(--text-dim); font: 600 9.5px var(--font); }
.sc-fup { transform-box: fill-box; transform-origin: center; opacity: 0; }
.sc-fup.u1 { animation: scFup1 11s cubic-bezier(.34,1.56,.64,1) infinite; }
.sc-fup.u2 { animation: scFup2 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scFup1 { 0%, 86% { opacity: 0; transform: translateY(6px); } 90%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes scFup2 { 0%, 89% { opacity: 0; transform: translateY(6px); } 93%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes scAx1 { 0%, 44% { opacity: 0; } 50%, 94% { opacity: .9; } 100% { opacity: 0; } }
@keyframes scAx2 { 0%, 52% { opacity: 0; } 58%, 94% { opacity: .9; } 100% { opacity: 0; } }
@keyframes scCone { 0%, 46% { opacity: 0; } 60%, 92% { opacity: 1; } 98%, 100% { opacity: 0; } }

/* scene 4: real habits in real words */
.sc-habit { fill: var(--text-dim); font: 600 10.5px var(--font); }
.sc-habit.lit { fill: var(--green-3); }

/* scene 5: a titled paper, labeled weeks */
.sc-paper { fill: var(--text-dim); font: 700 9.5px var(--font); }
.sc-wk { fill: var(--text-faint); font: 500 8px var(--font); }

/* scene 6: named products; the picked one carries its stars */
.sc-pname { fill: var(--text-dim); font: 600 8.5px var(--font); }
.sc-stars text { fill: #fbbf24; font: 700 9px var(--font); }
.sc-stars .off { fill: rgba(255,255,255,.18); }

/* scene 7: the seal gets its name */
.sc-enc rect { fill: rgb(var(--accent-rgb) / .13); stroke: rgb(var(--accent-rgb) / .4); }
.sc-enc text { fill: var(--green-2); font: 700 10px var(--font); letter-spacing: .3px; }
.sc-enc { transform-box: fill-box; transform-origin: center; opacity: 0; animation: scEnc 10s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scEnc { 0%, 66% { opacity: 0; transform: scale(.6); } 72%, 96% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .sc-date, .sc-axis-x, .sc-cone, .sc-hav, .sc-fup, .sc-enc { animation: none; opacity: 1; transform: none; }
}

/* ============================================================ PULL PASS v46 — the quiet things that draw people in. */

/* atmosphere gains blue depth below — teal/green light over deep water (like the reference) */
body::before { background:
    radial-gradient(560px 420px at 78% -4%, rgb(var(--accent-rgb) / 0.14), transparent 62%),
    radial-gradient(520px 420px at -6% 104%, rgb(21 128 61 / 0.13), transparent 60%); }
.main::before { background:
    radial-gradient(720px 460px at 82% -6%, rgb(var(--accent-rgb) / 0.10), transparent 60%),
    radial-gradient(560px 430px at 4% 108%, rgb(21 128 61 / 0.10), transparent 58%); }

/* sections arrive in order: panel first, headline breathes in, copy follows */
.lp-sec { transform: translateY(36px); transition: opacity .85s var(--ease), transform .85s var(--ease); }
.lp-copy h2, .lp-copy p { opacity: 0; transform: translateY(12px); transition: opacity .6s var(--ease), transform .6s var(--ease); }
.lp-sec.seen .lp-copy h2 { opacity: 1; transform: none; transition-delay: .25s; }
.lp-sec.seen .lp-copy p { opacity: 1; transform: none; transition-delay: .4s; }
.lp-sec.seen .lp-eyebrow { animation: eyebrowIn .5s var(--ease) .15s both; }
@keyframes eyebrowIn { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }
.lp-sec.seen .lp-viz > svg { animation-duration: .9s; }

/* the demo panels respond to presence */
.lp-viz { transition: transform .35s var(--ease), border-color .35s var(--ease), box-shadow .35s var(--ease); }
.lp-viz:hover { transform: translateY(-3px) scale(1.008); border-color: var(--outline-strong);
  box-shadow: 0 22px 48px -24px rgb(var(--accent-rgb) / 0.3); }

/* the main CTA breathes — once every six seconds, barely */
.lp-hero .btn.primary { animation: ctaBreath 6s ease-in-out infinite; }
@keyframes ctaBreath { 0%, 78%, 100% { transform: scale(1); box-shadow: 0 6px 18px -8px rgb(var(--accent-rgb) / .55), inset 0 1px 0 rgba(255,255,255,.16), inset 0 -2px 0 rgba(0,0,0,.28); }
  86% { transform: scale(1.025); box-shadow: 0 10px 26px -8px rgb(var(--accent-rgb) / .7), inset 0 1px 0 rgba(255,255,255,.2), inset 0 -2px 0 rgba(0,0,0,.28); } }

/* hero chips: each floats on its own tide, with a breath of tilt */
.lp-chip.c1 { animation-duration: 1s, 6.4s; }
.lp-chip.c2 { animation-duration: 1s, 7.6s; }
.lp-chip.c3 { animation-duration: 1s, 8.2s; }
.lp-chip.c4 { animation-duration: 1s, 7s; }
@keyframes chipFloat { 0%, 100% { translate: 0 0; rotate: 0deg; } 50% { translate: 0 -9px; rotate: .8deg; } }

/* the three steps introduce themselves one at a time */
.lp-step { animation: stepIn .8s var(--ease) both; }
.lp-step:nth-of-type(1) { animation-delay: .15s; }
.lp-step:nth-of-type(2) { animation-delay: .32s; }
.lp-step:nth-of-type(3) { animation-delay: .5s; }
@keyframes stepIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .lp-copy h2, .lp-copy p { opacity: 1; transform: none; transition: none; }
  .lp-hero .btn.primary, .lp-step, .lp-sec.seen .lp-eyebrow { animation: none; opacity: 1; }
  .lp-viz:hover { transform: none; }
}

/* ============================================================ LIVING EKG v47 — a monitor, not a squiggle. */
.hero-ekg path, .lp-ekg path { animation: none; stroke-dasharray: none; stroke-dashoffset: 0; }
@keyframes ekgComet { from { stroke-dashoffset: 620; } to { stroke-dashoffset: 0; } }
@keyframes ekgRip1 { 0%, 12% { opacity: 0; transform: scale(.4); } 15% { opacity: .9; } 26% { opacity: 0; transform: scale(3); } 100% { opacity: 0; } }
@keyframes ekgRip2 { 0%, 36% { opacity: 0; transform: scale(.4); } 39% { opacity: .9; } 50% { opacity: 0; transform: scale(3); } 100% { opacity: 0; } }
@keyframes ekgThump { 0%, 12%, 17%, 36%, 41%, 100% { transform: scale(1); } 14%, 38% { transform: scale(1.22); } }
@keyframes ekgAlive {
  0% { stroke-dashoffset: 620; opacity: 1; }
  18% { stroke-dashoffset: 0; opacity: 1; }
  55% { opacity: 1; filter: drop-shadow(0 0 6px rgb(var(--accent-rgb) / 0.45)); }
  70% { filter: drop-shadow(0 0 11px rgb(var(--accent-rgb) / 0.7)); }
  85%, 94% { stroke-dashoffset: 0; opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scWatch { 0%, 6% { opacity: 0; transform: translateY(6px); } 10%, 88% { opacity: 1; transform: none; }
  19%, 27% { opacity: 1; } 22% { transform: scale(1.06); } 96%, 100% { opacity: 0; } }
@keyframes scGoal { 0%, 8% { opacity: 0; } 12%, 92% { opacity: 1; } 98%, 100% { opacity: 0; } }
@keyframes scVerify { 0%, 70% { opacity: 0; transform: scale(.7); } 76%, 94% { opacity: 1; transform: scale(1); } 99%, 100% { opacity: 0; } }

/* scene 4: pillars + the cause of the re-tune */
.sc-pillar { fill: var(--text-faint); font: 700 7.5px var(--font); letter-spacing: .8px; opacity: .8; }
.sc-trig rect { fill: rgb(var(--accent-rgb) / .14); stroke: rgb(var(--accent-rgb) / .4); }
.sc-trig text { fill: var(--green-2); font: 700 10px var(--font); }
.sc-trig { transform-box: fill-box; transform-origin: center; opacity: 0; animation: scTrig 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scTrig { 0%, 32% { opacity: 0; transform: scale(.6); } 37%, 58% { opacity: 1; transform: scale(1); } 64%, 100% { opacity: 0; } }

/* scene 5: flip the modes on, THEN they perform — in turn */
.sc-mode rect { fill: rgba(255,255,255,.05); stroke: var(--hairline); }
.sc-mode circle { fill: rgba(255,255,255,.25); }
.sc-mode text { fill: var(--text-dim); font: 600 9.5px var(--font); }
.sc-mode.m1 { animation: scModeOn1 9s var(--ease) infinite; }
.sc-mode.m1 circle { animation: scModeDot1 9s var(--ease) infinite; }
.sc-mode.m2 { animation: scModeOn2 9s var(--ease) infinite; }
.sc-mode.m2 circle { animation: scModeDot2 9s var(--ease) infinite; }
@keyframes scModeOn1 { 0%, 4% { opacity: .55; } 8%, 100% { opacity: 1; } }
@keyframes scModeDot1 { 0%, 6% { fill: rgba(255,255,255,.25); } 9%, 100% { fill: var(--green); } }
@keyframes scModeOn2 { 0%, 48% { opacity: .55; } 52%, 100% { opacity: 1; } }
@keyframes scModeDot2 { 0%, 50% { fill: rgba(255,255,255,.25); } 53%, 100% { fill: var(--green); } }
.vz-scan { animation: scScanSeq 9s var(--ease) infinite; }
@keyframes scScanSeq { 0%, 10% { opacity: 0; transform: translateY(0); } 13% { opacity: 1; }
  20% { transform: translateY(20px); } 27% { transform: translateY(40px); } 34% { transform: translateY(60px); }
  40%, 100% { opacity: 0; transform: translateY(60px); } }
.sc-cite.c1 { animation: scCiteS1 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.sc-cite.c2 { animation: scCiteS2 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scCiteS1 { 0%, 22% { opacity: 0; transform: scale(.6); } 27%, 94% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }
@keyframes scCiteS2 { 0%, 32% { opacity: 0; transform: scale(.6); } 37%, 94% { opacity: 1; transform: scale(1); } 100% { opacity: 0; } }
.vz-bar { animation: scGrowSeq 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.vz-bar.b2 { animation-delay: .5s; } .vz-bar.b3 { animation-delay: 1s; }
@keyframes scGrowSeq { 0%, 52% { transform: scaleY(0); } 64%, 92% { transform: scaleY(1); } 99%, 100% { transform: scaleY(0); } }

/* scene 7: Deletable, demonstrated — 12s cycle */
.sc-dchip.d1 { animation: scD1b 12s var(--ease) infinite; }
.sc-dchip.d2 { animation: scD2b 12s var(--ease) infinite; }
.sc-dchip.d3 { animation: scD3b 12s var(--ease) infinite; }
@keyframes scD1b { 0%, 6% { opacity: 1; transform: none; } 18%, 100% { opacity: 0; transform: translate(78px, 62px) scale(.25); } }
@keyframes scD2b { 0%, 11% { opacity: 1; transform: none; } 23%, 100% { opacity: 0; transform: translate(0px, 70px) scale(.25); } }
@keyframes scD3b { 0%, 16% { opacity: 1; transform: none; } 28%, 100% { opacity: 0; transform: translate(-78px, 60px) scale(.25); } }
.vz-shield { animation: scShieldB 12s var(--ease) infinite; }
@keyframes scShieldB { 0%, 22% { stroke-dashoffset: 430; opacity: 1; } 46%, 82% { stroke-dashoffset: 0; opacity: 1; } 88%, 100% { stroke-dashoffset: 0; opacity: 0; } }
.vz-lockchk { animation: scLockB 12s var(--ease) infinite; }
@keyframes scLockB { 0%, 46% { stroke-dashoffset: 70; opacity: 1; } 56%, 82% { stroke-dashoffset: 0; opacity: 1; } 88%, 100% { opacity: 0; } }
.vz-pulse { animation: scPulseB 12s var(--ease) infinite; }
@keyframes scPulseB { 0%, 56% { transform: scale(.6); opacity: 0; } 61% { opacity: .8; } 72%, 100% { transform: scale(2.2); opacity: 0; } }
.sc-enc { animation: scEncB 12s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scEncB { 0%, 58% { opacity: 0; transform: scale(.6); } 64%, 82% { opacity: 1; transform: scale(1); } 88%, 100% { opacity: 0; } }
.sc-act rect { fill: rgba(255,255,255,.05); stroke: var(--hairline); }
.sc-act text { fill: var(--text-dim); font: 600 9.5px var(--font); }
.sc-act.e2 text { fill: #f87171; }
.sc-act { transform-box: fill-box; transform-origin: center; opacity: 0; }
.sc-act.e1 { animation: scActIn 12s var(--ease) infinite; }
.sc-act.e2 { animation: scActIn2 12s var(--ease) infinite; }
@keyframes scActIn  { 0%, 62% { opacity: 0; transform: translateY(6px); } 66%, 86% { opacity: 1; transform: none; } 90%, 100% { opacity: 0; } }
@keyframes scActIn2 { 0%, 64% { opacity: 0; transform: translateY(6px); } 68%, 86% { opacity: 1; transform: none; } 90%, 100% { opacity: 0; } }
.sc-cursor4 .dot { fill: rgba(255,255,255,.92); } .sc-cursor4 .halo { fill: none; stroke: rgb(var(--accent-rgb) / .6); stroke-width: 1.5; }
.sc-cursor4 { opacity: 0; animation: scCursor4 12s var(--ease) infinite; }
@keyframes scCursor4 {
  0%, 66% { opacity: 0; transform: translate(130px, 130px); }
  70% { opacity: 1; transform: translate(150px, 140px); }
  78% { transform: translate(212px, 155px) scale(1); }
  81% { transform: translate(212px, 155px) scale(.75); }
  84% { opacity: 1; transform: translate(212px, 155px) scale(1); }
  88%, 100% { opacity: 0; transform: translate(212px, 155px); } }

@media (prefers-reduced-motion: reduce) {
  .sc-watch, .sc-goal, .sc-goaltxt, .sc-verify, .sc-trig, .sc-mode, .sc-act, .sc-enc { animation: none; opacity: 1; transform: none; }
  .sc-cursor4 { animation: none; opacity: 0; }
  .sc-mode circle { animation: none; fill: var(--green); }
}

/* ============================================================ VITALS MORPH + HIGHLIGHTS v50 */
/* one line, three lives: it IS the heartbeat, then the sleep wave, then the training strain */
.vital-line { fill: none; stroke: url(#haleFlow); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round;
  filter: drop-shadow(0 0 6px rgb(var(--accent-rgb) / 0.45)); }
.vital-lab { fill: var(--text-faint); font: 600 9px var(--font); letter-spacing: .6px; text-transform: uppercase; opacity: 0; }
.vital-lab.l1 { animation: vlab1 12s linear infinite; }
.vital-lab.l2 { animation: vlab2 12s linear infinite; }
.vital-lab.l3 { animation: vlab3 12s linear infinite; }
@keyframes vlab1 { 0%, 3% { opacity: 0; } 6%, 23% { opacity: .9; } 26%, 96% { opacity: 0; } 100% { opacity: 0; } }
@keyframes vlab2 { 0%, 36% { opacity: 0; } 39%, 56% { opacity: .9; } 59%, 100% { opacity: 0; } }
@keyframes vlab3 { 0%, 69% { opacity: 0; } 72%, 89% { opacity: .9; } 92%, 100% { opacity: 0; } }

/* highlighted key phrases — green is the alpha */
.lp-copy .hl { color: var(--green-2); font-weight: 650; }

/* ============================================================ SYNC PASS v51 — every scene breathes as ONE.
   Common outro window (92–97%) so no frame ever shows a half-told story. */

/* scene 3 — twin: slider, paths, cone, chips all reset together */
@keyframes scFill { 0%, 22% { transform: scaleX(.08); } 38%, 91% { transform: scaleX(1); } 96%, 100% { transform: scaleX(.08); } }
@keyframes scKnob { 0%, 22% { transform: translateX(0); } 38%, 91% { transform: translateX(54px); } 96%, 100% { transform: translateX(0); } }
@keyframes scCurP { 0%, 5% { stroke-dashoffset: 205; opacity: 1; } 18%, 91% { stroke-dashoffset: 0; opacity: 1; } 96%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scWhatP { 0%, 39% { stroke-dashoffset: 220; opacity: 1; } 58%, 91% { stroke-dashoffset: 0; opacity: 1; } 96%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scCone { 0%, 46% { opacity: 0; } 60%, 91% { opacity: 1; } 96%, 100% { opacity: 0; } }
@keyframes scRing { 0%, 56% { opacity: 0; transform: scale(0); } 60%, 91% { opacity: 1; transform: scale(1); } 96%, 100% { opacity: 0; } }
@keyframes scWave { 0%, 58% { opacity: 0; transform: scale(1); } 62% { opacity: .8; } 74% { opacity: 0; transform: scale(2.6); }
  76% { opacity: .8; transform: scale(1); } 88%, 100% { opacity: 0; transform: scale(2.6); } }
@keyframes scDelta { 0%, 60% { opacity: 0; transform: scale(.6) translateY(6px); } 65%, 91% { opacity: 1; transform: none; } 96%, 100% { opacity: 0; } }
@keyframes scVerify { 0%, 70% { opacity: 0; transform: scale(.7); } 76%, 91% { opacity: 1; transform: scale(1); } 96%, 100% { opacity: 0; } }
@keyframes scAx1 { 0%, 44% { opacity: 0; } 50%, 91% { opacity: .9; } 96%, 100% { opacity: 0; } }
@keyframes scAx2 { 0%, 52% { opacity: 0; } 58%, 91% { opacity: .9; } 96%, 100% { opacity: 0; } }

/* scene 1 — data: everything already fades 92–100; align the two label groups to it */
@keyframes scGoal { 0%, 8% { opacity: 0; } 12%, 91% { opacity: 1; } 97%, 100% { opacity: 0; } }
@keyframes scWatch { 0%, 6% { opacity: 0; transform: translateY(6px); } 10%, 91% { opacity: 1; transform: none; }
  19%, 27% { opacity: 1; } 22% { transform: scale(1.06); } 97%, 100% { opacity: 0; } }

/* scene 4 — protocols: checks/rows/progress exit as one */
@keyframes scChk1 { 0%, 12% { stroke-dashoffset: 26; opacity: 1; } 17%, 91% { stroke-dashoffset: 0; opacity: 1; } 96%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scChk2 { 0%, 24% { stroke-dashoffset: 26; opacity: 1; } 29%, 91% { stroke-dashoffset: 0; opacity: 1; } 96%, 100% { stroke-dashoffset: 0; opacity: 0; } }
@keyframes scProg { 0%, 13% { transform: scaleX(0); opacity: 1; } 17%, 24% { transform: scaleX(.33); } 29%, 52% { transform: scaleX(.66); }
  57%, 91% { transform: scaleX(1); opacity: 1; } 96%, 100% { transform: scaleX(1); opacity: 0; } }
@keyframes scNew { 0%, 46% { opacity: 0; transform: translateX(-26px); } 54%, 91% { opacity: 1; transform: none; } 96%, 100% { opacity: 0; } }
@keyframes scSpin { 0%, 40% { transform: rotate(0deg); opacity: .5; } 56%, 91% { transform: rotate(360deg); opacity: 1; } 96%, 100% { transform: rotate(360deg); opacity: 0; } }

/* scene 6 — shop: the picked card's spotlight ends before the loop turns */
@keyframes scHero { 0%, 20% { transform: translateY(0) scale(1); } 26%, 66% { transform: translateY(-9px) scale(1.04); } 74%, 100% { transform: translateY(0) scale(1); } }

/* ============================================================ ALIVE PASS v54 — physics, anticipation, celebration, light.
   Nothing teleports; everything arrives. Nothing freezes; everything breathes. Payoffs land with sparks. */

/* --- light: a soft beam sweeps each panel once per loop --- */
.lp-viz { position: relative; overflow: hidden; }
.lp-viz::before { content: ""; position: absolute; top: -30%; bottom: -30%; left: -40%; width: 34%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.05), transparent);
  transform: skewX(-16deg); animation: panelSweep 11s ease-in-out infinite; pointer-events: none; }
@keyframes panelSweep { 0%, 62% { left: -40%; } 78%, 100% { left: 130%; } }
html.light .lp-viz::before { background: linear-gradient(100deg, transparent, rgba(255,255,255,.5), transparent); }

/* --- celebration: 4-point spark bursts at each scene's payoff moment --- */
.burst circle { fill: var(--green-3); opacity: 0; transform: translate(0,0); }
.b-data  circle { animation: burst9a 9s var(--ease) infinite; }
.b-chat  circle { animation: burst11a 11s var(--ease) infinite; }
.b-proto circle { animation: burst11c 11s var(--ease) infinite; }
.b-modes circle { animation: burst9b 9s var(--ease) infinite; }
.b-shop  circle { animation: burst9c 9s var(--ease) infinite; }
.b-priv  circle { animation: burst12a 12s var(--ease) infinite; }
@keyframes burst9a  { 0%, 45% { opacity: 0; transform: translate(0,0); } 47% { opacity: 1; } 53%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes burst9b  { 0%, 26% { opacity: 0; transform: translate(0,0); } 28% { opacity: 1; } 34%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes burst9c  { 0%, 31% { opacity: 0; transform: translate(0,0); } 33% { opacity: 1; } 39%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes burst11a { 0%, 85% { opacity: 0; transform: translate(0,0); } 87% { opacity: 1; } 93%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes burst11b { 0%, 64% { opacity: 0; transform: translate(0,0); } 66% { opacity: 1; } 72%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes burst11c { 0%, 55% { opacity: 0; transform: translate(0,0); } 57% { opacity: 1; } 63%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
@keyframes burst12a { 0%, 63% { opacity: 0; transform: translate(0,0); } 65% { opacity: 1; } 71%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }

/* --- scene 1: impacts ripple, values arrive with comet tails, the doc leans into its scan --- */
.land { fill: none; stroke: var(--green-2); stroke-width: 1.5; transform-box: fill-box; transform-origin: center; opacity: 0; }
.land.l1 { animation: land9a 9s var(--ease) infinite; }
.land.l2 { animation: land9b 9s var(--ease) infinite; }
.land.l3 { animation: land9c 9s var(--ease) infinite; }
@keyframes land9a { 0%, 15% { opacity: 0; transform: scale(.5); } 17% { opacity: .9; } 26%, 100% { opacity: 0; transform: scale(2.6); } }
@keyframes land9b { 0%, 23% { opacity: 0; transform: scale(.5); } 25% { opacity: .9; } 34%, 100% { opacity: 0; transform: scale(2.6); } }
@keyframes land9c { 0%, 31% { opacity: 0; transform: scale(.5); } 33% { opacity: .9; } 42%, 100% { opacity: 0; transform: scale(2.6); } }
@keyframes scDoc { 0% { opacity: 0; transform: translateX(-28px) rotate(-4deg); }
  8% { opacity: 1; transform: none; }
  10% { transform: rotate(1.6deg); }
  13%, 88% { opacity: 1; transform: none; }
  96%, 100% { opacity: 0; } }
@keyframes scPtB1 { 0%, 14% { opacity: 0; transform: scale(0); } 18% { opacity: 1; transform: scale(1.25); } 22% { transform: scale(1); }
  50% { transform: scale(1.07); } 64% { transform: scale(1); } 78% { transform: scale(1.07); } 91% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }
@keyframes scPtB2 { 0%, 22% { opacity: 0; transform: scale(0); } 26% { opacity: 1; transform: scale(1.25); } 30% { transform: scale(1); }
  56% { transform: scale(1.07); } 70% { transform: scale(1); } 84% { transform: scale(1.07); } 91% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }
@keyframes scPtB3 { 0%, 30% { opacity: 0; transform: scale(0); } 34% { opacity: 1; transform: scale(1.25); } 38% { transform: scale(1); }
  60% { transform: scale(1.07); } 74% { transform: scale(1); } 88% { transform: scale(1.07); } 91% { opacity: 1; transform: scale(1); } 98%, 100% { opacity: 0; } }

/* --- scene 2: the reply lands with body; chart dots keep a pulse --- */
.sc-r { transform-origin: 30% top; }
@keyframes scR { 0%, 26% { opacity: 0; transform: translateY(10px) scale(.97); }
  31% { opacity: 1; transform: translateY(-2px) scale(1.012); }
  34%, 91% { opacity: 1; transform: none; } 97%, 100% { opacity: 0; } }
@keyframes scMpt1 { 0%, 64% { opacity: 0; } 67% { opacity: 1; } 75% { opacity: .75; } 83%, 91% { opacity: 1; } 97%, 100% { opacity: 0; } }
@keyframes scMpt2 { 0%, 77% { opacity: 0; } 80% { opacity: 1; } 86% { opacity: .75; } 91% { opacity: 1; } 97%, 100% { opacity: 0; } }

/* --- scene 3: the knob wakes up before it's dragged; the cone shimmers --- */
@keyframes scKnob { 0%, 18% { transform: translateX(0); } 20% { transform: translateX(-2.5px); } 22% { transform: translateX(0); }
  38%, 91% { transform: translateX(54px); } 96%, 100% { transform: translateX(0); } }
@keyframes scCone { 0%, 46% { opacity: 0; } 60% { opacity: 1; } 72% { opacity: .75; } 82%, 91% { opacity: 1; } 96%, 100% { opacity: 0; } }

/* --- scene 5: the scan strip glows brighter while it reads --- */
@keyframes scScanSeq { 0%, 10% { opacity: 0; transform: translateY(0); } 13% { opacity: 1; fill: rgb(var(--accent-rgb) / .22); }
  20% { transform: translateY(20px); fill: rgb(var(--accent-rgb) / .14); } 27% { transform: translateY(40px); fill: rgb(var(--accent-rgb) / .22); }
  34% { transform: translateY(60px); fill: rgb(var(--accent-rgb) / .14); } 40%, 100% { opacity: 0; transform: translateY(60px); } }

/* --- scene 6: the picked card tilts into the light; its stars twinkle --- */
@keyframes scHero { 0%, 20% { transform: translateY(0) scale(1) rotate(0deg); } 26%, 66% { transform: translateY(-9px) scale(1.045) rotate(-1.2deg); } 74%, 100% { transform: translateY(0) scale(1) rotate(0deg); } }
.sc-stars text { animation: starTwinkle 3s ease-in-out infinite; }
.sc-stars text.off { animation: none; }
@keyframes starTwinkle { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }

/* --- scene 7: the chips arc into the vault with a tumble --- */
@keyframes scD1b { 0%, 6% { opacity: 1; transform: translate(0,0) rotate(0deg); } 12% { opacity: 1; transform: translate(34px, 14px) rotate(4deg) scale(.7); } 18%, 96% { opacity: 0; transform: translate(78px, 62px) rotate(8deg) scale(.25); } 100% { opacity: 1; transform: none; } }
@keyframes scD2b { 0%, 11% { opacity: 1; transform: translate(0,0) rotate(0deg); } 17% { opacity: 1; transform: translate(0px, 30px) rotate(-3deg) scale(.7); } 23%, 96% { opacity: 0; transform: translate(0px, 70px) rotate(-6deg) scale(.25); } 100% { opacity: 1; transform: none; } }
@keyframes scD3b { 0%, 16% { opacity: 1; transform: translate(0,0) rotate(0deg); } 22% { opacity: 1; transform: translate(-34px, 12px) rotate(-4deg) scale(.7); } 28%, 96% { opacity: 0; transform: translate(-78px, 60px) rotate(-8deg) scale(.25); } 100% { opacity: 1; transform: none; } }
@keyframes scWatchB { 0%, 6% { opacity: 0; transform: translateY(6px); } 10% { opacity: 1; transform: none; }
  22% { transform: scale(1.06); } 26% { transform: scale(1); }
  50% { transform: translateY(-1.5px); } 72% { transform: translateY(0); } 91% { opacity: 1; } 97%, 100% { opacity: 0; } }
@keyframes ambientFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1.5px); } }

@media (prefers-reduced-motion: reduce) {
  .lp-viz::before, .burst circle, .land, .sc-fly2, .sc-stars text, .sc-ctl { animation: none; opacity: 0; }
  .sc-stars text { opacity: 1; }
}

/* ============================================================ SHARPEN v55 — the last 5% that reads "professional". */

/* crisper edges, brighter reading text */
:root { --hairline: rgba(255,255,255,0.07); --text-dim: #a4b0a6; }

/* typographic finish: distinctive Inter alternates + tabular numbers everywhere numbers matter */
body { font-feature-settings: 'cv05' 1, 'cv11' 1, 'ss01' 1; text-rendering: optimizeLegibility; }
.mk-val, .stat-val, .m-val, .p-price, .pd-price, .lp-plan-price, .sc-num, .proj-vals { font-variant-numeric: tabular-nums; }
.lp-hero h1 { letter-spacing: -.8px; }
.hero-tag { letter-spacing: -.3px; }
.btn { letter-spacing: .15px; }
.page-eyebrow, .lp-eyebrow { letter-spacing: 1.1px; font-size: .74rem; }

/* headline gradients: tighter, punchier sweep + a breath of glow behind the words */
.lp-hero h1 { filter: drop-shadow(0 0 26px rgb(var(--accent-rgb) / 0.18)); }
.hero-tag { filter: drop-shadow(0 0 20px rgb(var(--accent-rgb) / 0.15)); }
.btn.ghost { box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / .08); }

/* cards: the frame light turns up one notch */
.card { background:
    linear-gradient(180deg, rgba(255,255,255,.032), rgba(255,255,255,0) 46%) padding-box,
    linear-gradient(var(--panel-solid), var(--panel-solid)) padding-box,
    linear-gradient(160deg, rgb(var(--accent-rgb) / 0.42), rgba(255,255,255,.07) 34%, rgba(255,255,255,.06) 62%, rgb(21 128 61 / 0.34)) border-box; }

/* demo panels get the same finished frame as cards */
.lp-viz { border: 1px solid transparent;
  background:
    linear-gradient(180deg, var(--elev), var(--bg-2)) padding-box,
    linear-gradient(160deg, rgb(var(--accent-rgb) / 0.38), rgba(255,255,255,.07) 36%, rgba(255,255,255,.06) 62%, rgb(21 128 61 / 0.3)) border-box; }

/* light theme keeps pace */
html.light { --hairline: rgba(0,0,0,0.09); --text-dim: #56645a; }
html.light .lp-viz { background:
    linear-gradient(180deg, #fff, #f6f8f6) padding-box,
    linear-gradient(160deg, rgb(var(--accent-rgb) / 0.4), rgba(0,0,0,.08) 36%, rgba(0,0,0,.07) 62%, rgb(var(--accent-rgb) / 0.32)) border-box; }

/* ============================================================ BIG SCREEN v56 — the demos take the stage. */

/* the page stretches; the demo panels dominate their sections */
.lp { max-width: 1140px; }
.lp-sec { grid-template-columns: 1.18fr 1fr; gap: 56px; margin: 110px 0; }
.lp-viz { padding: 30px; border-radius: 28px; }
@media (max-width: 760px) { .lp-sec { gap: 20px; margin: 56px 0; } }

/* chart grids — quiet engineering paper behind the data */
.sc-grid { stroke: rgba(255,255,255,.05); stroke-width: 1; fill: none; }
html.light .sc-grid { stroke: rgba(0,0,0,.06); }
@keyframes scStamp { 0%, 14% { stroke-dashoffset: 18; } 18%, 91% { stroke-dashoffset: 0; } 97%, 100% { stroke-dashoffset: 0; opacity: 0; } }

/* scene 2: timestamp fades in with the question */
.sc-ts { fill: var(--text-faint); font: 500 8px var(--font); opacity: 0; animation: scTs 11s var(--ease) infinite; }
@keyframes scTs { 0%, 4% { opacity: 0; } 8%, 91% { opacity: .9; } 97%, 100% { opacity: 0; } }

/* scene 4: the counter ticks with the checks — three states, one spot, never together */
.sc-count { fill: var(--text-faint); font: 700 9px var(--font); opacity: 0; }
.sc-count.c1 { animation: scCount1 11s var(--ease) infinite; }
.sc-count.c2 { animation: scCount2 11s var(--ease) infinite; }
.sc-count.c3 { animation: scCount3 11s var(--ease) infinite; }
@keyframes scCount1 { 0%, 16% { opacity: 0; } 18%, 27% { opacity: 1; } 29%, 100% { opacity: 0; } }
@keyframes scCount2 { 0%, 28% { opacity: 0; } 30%, 55% { opacity: 1; } 57%, 100% { opacity: 0; } }
@keyframes scCount3 { 0%, 56% { opacity: 0; } 58%, 91% { opacity: 1; } 97%, 100% { opacity: 0; } }
.sc-count.c3 { fill: var(--green-2); }

/* scene 5: superscripts pop as the scanner passes their lines */
.sc-sup { fill: var(--green-2); font: 700 9px var(--font); opacity: 0; transform-box: fill-box; transform-origin: center; }
.sc-sup.s1 { animation: scSup1 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.sc-sup.s2 { animation: scSup2 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes scSup1 { 0%, 14% { opacity: 0; transform: scale(.4); } 17%, 91% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
@keyframes scSup2 { 0%, 28% { opacity: 0; transform: scale(.4); } 31%, 91% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }

/* scene 6: the curation tag */
.sc-fortag { fill: var(--text-faint); font: 600 8.5px var(--font); letter-spacing: .4px; opacity: 0; animation: scTs 9s var(--ease) infinite; }

@media (prefers-reduced-motion: reduce) {
  .sc-ts, .sc-sup, .sc-fortag { animation: none; opacity: 1; }
  .sc-count { animation: none; opacity: 0; } .sc-count.c3 { opacity: 1; }
}
.lp-copy { max-width: 720px; margin: 0 auto 36px; text-align: center; }
.lp-copy .lp-eyebrow { margin-bottom: 14px; }
.lp-viz { max-width: 920px; margin: 0 auto; padding: 36px 40px; }
@media (max-width: 760px) { .lp-sec { margin: 64px 0; } .lp-copy { margin-bottom: 20px; } .lp-viz { padding: 18px; } }
@keyframes scWatchIdle { 0%, 18% { transform: scale(1); } 22% { transform: scale(1.06); } 26%, 100% { transform: scale(1); } }

/* --- scene 2: the question stays asked --- */
.sc-u { animation: none; opacity: 1; transform: none; }
.sc-ts { animation: none; opacity: .9; }

/* --- scene 3: the instrument is always on --- */
.sc-cur { animation: none; stroke-dashoffset: 0; opacity: 1; }

/* --- scene 4: the plan rows never vanish --- */
@keyframes scChk1 { 0%, 12% { stroke-dashoffset: 26; opacity: 1; } 17%, 93% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 26; } }
@keyframes scChk2 { 0%, 24% { stroke-dashoffset: 26; opacity: 1; } 29%, 93% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 26; } }

/* --- scene 5: the training bars idle low, then load — never an empty gym --- */
@keyframes scGrowSeq { 0%, 45% { transform: scaleY(.22); } 60%, 92% { transform: scaleY(1); } 100% { transform: scaleY(.22); } }

/* --- scene 7: a faint shield always guards the stage --- */
.sc-ghost { fill: rgb(var(--accent-rgb) / 0.04); stroke: rgb(var(--accent-rgb) / 0.18); stroke-width: 2; }
/* showcase order: words first, stage second */
.lp-sec { display: flex; flex-direction: column; }
.lp-copy { order: 0; }
.lp-viz { order: 1; width: 100%; }

/* ============================================================ PRODUCT WINDOWS v59 — restraint + realism.
   Each demo is a real window into Hale: chrome, title, model pill. Compact, dense, pixel-tight. */

/* restrained scale — premium is dense, not big */
.lp-sec { margin: 96px 0; }
.lp-copy { max-width: 640px; margin: 0 auto 26px; }
.lp-viz { max-width: 660px; padding: 0; overflow: hidden; }
.viz-body { padding: 20px 24px 22px; }
@media (max-width: 760px) { .lp-viz { max-width: 100%; } .viz-body { padding: 12px 14px; } }

/* the window chrome: traffic dots · page title · the model pill from the real topbar */
.viz-chrome { display: flex; align-items: center; gap: 7px; padding: 11px 16px;
  border-bottom: 1px solid var(--hairline); background: rgba(255,255,255,.015); }
.vd { width: 9px; height: 9px; border-radius: 50%; }
.vd.r { background: rgba(255,120,110,.55); } .vd.y { background: rgba(255,200,90,.5); } .vd.g { background: rgba(120,220,140,.55); }
.viz-title { margin-left: 10px; font: 600 11.5px var(--font); color: var(--text-dim); letter-spacing: .2px; }
.viz-pill { margin-left: auto; display: inline-flex; align-items: center; gap: 6px;
  font: 600 10.5px var(--font); color: var(--text-dim);
  border: 1px solid var(--hairline); border-radius: 999px; padding: 4px 10px; }
.viz-pill i { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 6px rgb(var(--accent-rgb) / .8); }
html.light .viz-chrome { background: rgba(0,0,0,.02); }

/* the light sweep respects the chrome */
.lp-viz::before { z-index: 1; }

/* ============================================================ TWIN DASHBOARD v60 — reference-grade: hero metric, delta badge,
   a curve that FALLS into its target zone (lower ApoB = down = good), confidence band, fine grids. 11s clock. */
.tw-lab { fill: var(--text-faint); font: 600 8.5px var(--font); letter-spacing: .4px; }
.tw-hero { fill: var(--text); font: 700 17px var(--font-display); }
.tw-hero.to { fill: var(--green-2); opacity: 0; animation: twTo 11s var(--ease) infinite; }
@keyframes twTo { 0%, 40% { opacity: 0; } 45%, 92% { opacity: 1; } 97%, 100% { opacity: 0; } }
.tw-delta rect { fill: rgb(var(--accent-rgb) / .14); stroke: rgb(var(--accent-rgb) / .4); }
.tw-delta text { fill: var(--green-2); font: 700 9px var(--font); }
.tw-delta { transform-box: fill-box; transform-origin: center; opacity: 0; animation: twDelta 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes twDelta { 0%, 42% { opacity: 0; transform: scale(.6); } 47%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
.tw-ctl .box { fill: var(--elev-2); stroke: var(--hairline); }
.tw-ctl .lab { fill: var(--text-dim); font: 600 8px var(--font); }
.tw-ctl .track { fill: rgba(255,255,255,.12); }
.tw-ctl .fill { fill: var(--green); transform-box: fill-box; transform-origin: left center; animation: twFill 11s var(--ease) infinite; }
@keyframes twFill { 0%, 14% { transform: scaleX(.06); } 26%, 92% { transform: scaleX(1); } 97%, 100% { transform: scaleX(.06); } }
.tw-knob { fill: var(--green-3); stroke: var(--bg-2); stroke-width: 1.5; animation: twKnob 11s var(--ease) infinite;
  filter: drop-shadow(0 0 4px rgb(var(--accent-rgb) / .7)); }
@keyframes twKnob { 0%, 12% { transform: translateX(0); } 14% { transform: translateX(-2px); }
  26%, 92% { transform: translateX(50px); } 97%, 100% { transform: translateX(0); } }
.tw-check { fill: var(--text-faint); font: 600 7.5px var(--font); opacity: 0; animation: twCheck 11s var(--ease) infinite; }
@keyframes twCheck { 0%, 52% { opacity: 0; } 57%, 92% { opacity: .9; } 97%, 100% { opacity: 0; } }
.tw-tick { fill: var(--text-faint); font: 500 8px var(--font); }
.tw-zone { fill: rgb(var(--accent-rgb) / .06); }
.tw-zonelab { fill: rgb(var(--accent-rgb) / .5); font: 600 7.5px var(--font); letter-spacing: .3px; }
.tw-band { fill: rgb(var(--accent-rgb) / .07); opacity: 0; animation: twBand 11s var(--ease) infinite; }
@keyframes twBand { 0%, 30% { opacity: 0; } 44%, 92% { opacity: 1; } 97%, 100% { opacity: 0; } }
.tw-same { fill: none; stroke: rgba(255,255,255,.2); stroke-width: 1.5; stroke-dasharray: 3 4; }
.tw-samelab { fill: var(--text-faint); font: 500 7.5px var(--font); }
.tw-proj { fill: none; stroke: var(--green); stroke-width: 2.5; stroke-linecap: round;
  stroke-dasharray: 215; stroke-dashoffset: 215; animation: twProj 11s var(--ease) infinite;
  filter: drop-shadow(0 0 5px rgb(var(--accent-rgb) / .45)); }
@keyframes twProj { 0%, 26% { stroke-dashoffset: 215; opacity: 1; } 44%, 92% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 0; opacity: 0; } }
.tw-today { fill: var(--green-3); }
.tw-end { fill: var(--green); transform-box: fill-box; transform-origin: center; opacity: 0; animation: twEnd 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes twEnd { 0%, 42% { opacity: 0; transform: scale(0); } 46%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
.tw-endwave { fill: none; stroke: var(--green); stroke-width: 1.5; transform-box: fill-box; transform-origin: center; opacity: 0;
  animation: twWave 11s var(--ease) infinite; }
@keyframes twWave { 0%, 44% { opacity: 0; transform: scale(1); } 48% { opacity: .8; } 58% { opacity: 0; transform: scale(2.4); }
  62% { opacity: .6; transform: scale(1); } 70%, 100% { opacity: 0; transform: scale(2.4); } }
.tw-x { fill: var(--text-faint); font: 500 8px var(--font); }
/* twin burst fires with the delta badge */
.b-twin circle { animation: burst11d 11s var(--ease) infinite; }
@keyframes burst11d { 0%, 45% { opacity: 0; transform: translate(0,0); } 47% { opacity: 1; } 53%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }
/* twin cursor rides the smaller control */
@keyframes scCursor {
  0%, 4% { opacity: 0; transform: translate(200px, 150px); }
  8% { opacity: 1; transform: translate(200px, 150px); }
  12% { transform: translate(180px, 38px) scale(1); }
  14% { transform: translate(180px, 38px) scale(.75); }
  26% { transform: translate(230px, 38px) scale(.75); }
  28% { transform: translate(230px, 38px) scale(1); }
  34%, 100% { opacity: 0; transform: translate(246px, 80px); } }

@media (prefers-reduced-motion: reduce) {
  .tw-hero.to, .tw-delta, .tw-band, .tw-check { animation: none; opacity: 1; transform: none; }
  .tw-proj { animation: none; stroke-dashoffset: 0; }
  .tw-end { animation: none; opacity: 1; transform: scale(1); }
  .tw-endwave, .b-twin circle { animation: none; opacity: 0; }
  .tw-knob { animation: none; transform: translateX(50px); }
  .tw-ctl .fill { animation: none; transform: scaleX(1); }
}
@keyframes dtTo { 0%, 38% { opacity: 0; } 43%, 92% { opacity: 1; } 97%, 100% { opacity: 0; } }
.dt-delta { animation-name: dtDelta; animation-duration: 9s; }
@keyframes dtDelta { 0%, 43% { opacity: 0; transform: scale(.6); } 48%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
.dt-flag rect { fill: rgba(251,191,36,.12); stroke: rgba(251,191,36,.45); }
.dt-flag text { fill: #fbbf24; font: 600 9.5px var(--font); }
.dt-flag.good rect { fill: rgb(var(--accent-rgb) / .14); stroke: rgb(var(--accent-rgb) / .45); }
.dt-flag.good text { fill: var(--green-2); }
.dt-flag { opacity: 0; }
.dt-flag.bad { animation: dtBad 9s var(--ease) infinite; }
.dt-flag.good { animation: dtGood 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes dtBad { 0%, 40% { opacity: 1; } 44%, 100% { opacity: 0; } }
@keyframes dtGood { 0%, 44% { opacity: 0; transform: scale(.7); } 49%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
.dt-src rect:first-child { fill: var(--elev-2); stroke: var(--hairline); }
.dt-src text { fill: var(--text-dim); font: 600 8px var(--font); }
.dt-beam { fill: rgb(var(--accent-rgb) / .3); opacity: 0; animation: dtBeam 9s var(--ease) infinite; }
@keyframes dtBeam { 0%, 4% { opacity: 0; transform: translateX(0); } 6% { opacity: 1; }
  12% { opacity: 1; transform: translateX(52px); } 14%, 100% { opacity: 0; } }
.dt-proj { fill: none; stroke: var(--green); stroke-width: 2.5; stroke-linecap: round;
  stroke-dasharray: 215; stroke-dashoffset: 215; animation: dtProj 9s var(--ease) infinite;
  filter: drop-shadow(0 0 5px rgb(var(--accent-rgb) / .45)); }
@keyframes dtProj { 0%, 14% { stroke-dashoffset: 215; opacity: 1; } 40%, 92% { stroke-dashoffset: 0; opacity: 1; } 97%, 100% { stroke-dashoffset: 0; opacity: 0; } }
.dt-pt { fill: var(--green-3); transform-box: fill-box; transform-origin: center; opacity: 0; }
.dt-pt.p1 { animation: dtPt1 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.dt-pt.p2 { animation: dtPt2 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.dt-pt.p3 { animation: dtPt3 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes dtPt1 { 0%, 14% { opacity: 0; transform: scale(0); } 18% { opacity: 1; transform: scale(1.25); } 22%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
@keyframes dtPt2 { 0%, 24% { opacity: 0; transform: scale(0); } 28% { opacity: 1; transform: scale(1.25); } 32%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
@keyframes dtPt3 { 0%, 34% { opacity: 0; transform: scale(0); } 38% { opacity: 1; transform: scale(1.25); } 42%, 92% { opacity: 1; transform: scale(1); } 97%, 100% { opacity: 0; } }
/* flights + ripples retimed to the new dots */
@keyframes scFly1 { 0%, 11% { opacity: 0; transform: translate(206px, 44px) scale(.4); } 13% { opacity: 1; }
  17% { opacity: 1; transform: translate(36px, 74px) scale(1); } 19%, 100% { opacity: 0; transform: translate(36px, 74px); } }
@keyframes scFly2 { 0%, 21% { opacity: 0; transform: translate(206px, 44px) scale(.4); } 23% { opacity: 1; }
  27% { opacity: 1; transform: translate(140px, 105px) scale(1); } 29%, 100% { opacity: 0; transform: translate(140px, 105px); } }
@keyframes scFly3 { 0%, 31% { opacity: 0; transform: translate(206px, 44px) scale(.4); } 33% { opacity: 1; }
  37% { opacity: 1; transform: translate(236px, 134px) scale(1); } 39%, 100% { opacity: 0; transform: translate(236px, 134px); } }
@keyframes land9a { 0%, 17% { opacity: 0; transform: scale(.5); } 19% { opacity: .9; } 28%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes land9b { 0%, 27% { opacity: 0; transform: scale(.5); } 29% { opacity: .9; } 38%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes land9c { 0%, 37% { opacity: 0; transform: scale(.5); } 39% { opacity: .9; } 48%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes burst9a { 0%, 47% { opacity: 0; transform: translate(0,0); } 49% { opacity: 1; } 55%, 100% { opacity: 0; transform: translate(var(--dx), var(--dy)); } }

@media (prefers-reduced-motion: reduce) {
  .dt-to, .dt-flag.good { animation: none; opacity: 1; transform: none; }
  .dt-flag.bad, .dt-beam { animation: none; opacity: 0; }
  .dt-proj { animation: none; stroke-dashoffset: 0; }
  .dt-pt { animation: none; opacity: 1; transform: scale(1); }
}

/* ============================================================ INTERIOR SHARPEN v62 — the app itself gets the keynote finish. */

/* every press is felt: uniform tactile feedback */
.btn:active, .nav-item:active, .tab-item:active, .daily-item:active, .followup-chip:active { transform: scale(.98); }
.btn, .nav-item, .tab-item { will-change: transform; }

/* sidebar: hover reveals direction */
.nav-item { transition: background .15s var(--ease), transform .15s var(--ease); }
.nav-item:hover:not(.active) { background: rgba(255,255,255,.03); transform: translateX(2px); }

/* daily checkboxes: the check lands with a pop and a ring */
.daily-item .dbox { position: relative; }
.daily-item.done .dbox { animation: dboxPop .35s cubic-bezier(.34,1.56,.64,1); background: linear-gradient(135deg, var(--green-2), var(--green) 60%); border-color: transparent; }
@keyframes dboxPop { 0% { transform: scale(.7); } 60% { transform: scale(1.15); } 100% { transform: scale(1); } }

/* card icon chips: forged, not flat */
.card-ic { background: linear-gradient(135deg, rgb(var(--accent-rgb) / .16), rgb(var(--accent-rgb) / .12));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.08); }

/* sparklines + the twin gauge run in the brand gradient */
.spark path.line { stroke: url(#haleFlow); stroke-width: 2.2; filter: drop-shadow(0 0 4px rgb(var(--accent-rgb) / .35)); }
.twin-gauge .prog { stroke: url(#haleFlow); filter: drop-shadow(0 0 6px rgb(var(--accent-rgb) / .4)); }

/* chat: user messages read as *yours* — softly tinted, right-weighted avatar */
.msg.user .m-avatar { background: linear-gradient(135deg, rgba(255,255,255,.09), rgba(255,255,255,.04)); border: 1px solid var(--hairline); }
.msg .m-body strong, .m-body b { color: var(--green-3); }

/* inputs: everything you can type in lights up the same way */
.modal-input:focus, input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="number"]:focus {
  border-color: var(--green); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.12); outline: none; }
/* the composer textarea lives inside a pill that already lights up — it must never draw its own box */
.composer textarea:focus { border: none; box-shadow: none; outline: none; -webkit-appearance: none; appearance: none; }
.modal-input { transition: border-color .15s var(--ease), box-shadow .15s var(--ease); }

/* stat rows: the number is the point — give it room and a baseline */
.stat-row { padding: 10px 0; }
.stat-val { font-size: 1.05rem; letter-spacing: .1px; }

/* page rhythm: consistent breathing between blocks */
.page > .card, .page section.card { margin-bottom: 16px; }

/* marker cards: hover lifts them like the landing panels */
.mk-card:hover, .tf-card:hover { transform: translateY(-2px); box-shadow: 0 14px 30px -18px rgb(var(--accent-rgb) / .28); }
.mk-card, .tf-card { transition: transform .18s var(--ease), box-shadow .18s var(--ease), border-color .18s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .btn:active, .nav-item:active, .tab-item:active, .daily-item:active { transform: none; }
  .daily-item.done .dbox { animation: none; }
  .nav-item:hover:not(.active) { transform: none; }
  .mk-card:hover, .tf-card:hover { transform: none; }
}

/* ============================================================ FILM PASS v63 — directed like a product film.
   Living light, breathing cameras, carets, readouts, ripples, flips, streaks, glints, blooms. */

/* --- the hero's glass floor --- */
.vital-mirror { opacity: .18; filter: blur(1.2px); transform: translateY(88px) scaleY(-.42); transform-origin: center; mask-image: linear-gradient(180deg, rgba(0,0,0,.9), transparent 85%); -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.9), transparent 85%); }
.vital-mirror .vital-line { filter: none; }

/* --- payoff blooms: a soft light flash behind every climax --- */
.bloom { fill: rgb(var(--accent-rgb) / .35); filter: blur(7px); transform-box: fill-box; transform-origin: center; opacity: 0; }
.bl-data  { animation: bloom9a 9s var(--ease) infinite; }
.bl-chat  { animation: bloom11a 11s var(--ease) infinite; }
.bl-twin  { animation: bloom11b 11s var(--ease) infinite; }
.bl-proto { animation: bloom11c 11s var(--ease) infinite; }
.bl-modes { animation: bloom9b 9s var(--ease) infinite; }
.bl-shop  { animation: bloom9c 9s var(--ease) infinite; }
.bl-priv  { animation: bloom12a 12s var(--ease) infinite; }
@keyframes bloom9a  { 0%, 44% { opacity: 0; transform: scale(.5); } 48% { opacity: 1; } 56%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes bloom9b  { 0%, 25% { opacity: 0; transform: scale(.5); } 29% { opacity: 1; } 37%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes bloom9c  { 0%, 29% { opacity: 0; transform: scale(.5); } 33% { opacity: 1; } 41%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes bloom11a { 0%, 83% { opacity: 0; transform: scale(.5); } 87% { opacity: 1; } 95%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes bloom11b { 0%, 43% { opacity: 0; transform: scale(.5); } 47% { opacity: 1; } 55%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes bloom11c { 0%, 53% { opacity: 0; transform: scale(.5); } 57% { opacity: 1; } 65%, 100% { opacity: 0; transform: scale(2.4); } }
@keyframes bloom12a { 0%, 62% { opacity: 0; transform: scale(.5); } 66% { opacity: 1; } 74%, 100% { opacity: 0; transform: scale(2.4); } }

/* --- data: a glimmer chases the drawn curve --- */
.dt-glim { fill: none; stroke: rgba(255,255,255,.65); stroke-width: 2.5; stroke-linecap: round;
  stroke-dasharray: 26 400; stroke-dashoffset: 26; opacity: 0; animation: dtGlim 9s linear infinite; }
@keyframes dtGlim { 0%, 40% { stroke-dashoffset: 26; opacity: 0; } 42% { opacity: .8; }
  54% { stroke-dashoffset: -215; opacity: .8; } 56%, 100% { stroke-dashoffset: -215; opacity: 0; } }

/* --- chat: the mind at work, then the words arrive one by one --- */
.halo-think { fill: none; stroke: rgb(var(--accent-rgb) / .5); stroke-width: 1.5; stroke-dasharray: 4 5;
  transform-box: fill-box; transform-origin: center; opacity: 0; animation: haloThink 11s linear infinite; }
@keyframes haloThink { 0%, 7% { opacity: 0; transform: rotate(0deg); } 10%, 25% { opacity: 1; } 27%, 100% { opacity: 0; transform: rotate(300deg); } }
.sc-caret { fill: var(--green-3); opacity: 0; animation: caretRide 11s steps(24) infinite, scCaretBlink .55s steps(1) infinite; }
@keyframes caretRide {
  0%, 29% { opacity: 0; transform: translate(0, 0); }
  30% { opacity: 1; transform: translate(0, 0); }
  48% { opacity: 1; transform: translate(208px, 0); }
  49% { opacity: 1; transform: translate(0, 20px); }
  63% { opacity: 1; transform: translate(193px, 20px); }
  67%, 100% { opacity: 0; transform: translate(193px, 20px); } }
@keyframes scCaretBlink { 0%, 60% { fill-opacity: 1; } 61%, 100% { fill-opacity: .25; } }

/* --- twin: the readout rides the falling line; the cone opens from today --- */
.tw-read { fill: var(--green-2); font: 700 8.5px var(--font); opacity: 0; }
.tw-read.r1 { animation: twRead1 11s var(--ease) infinite; }
.tw-read.r2 { animation: twRead2 11s var(--ease) infinite; }
.tw-read.r3 { animation: twRead3 11s var(--ease) infinite; }
@keyframes twRead1 { 0%, 29% { opacity: 0; } 31%, 34% { opacity: 1; } 37%, 100% { opacity: 0; } }
@keyframes twRead2 { 0%, 33% { opacity: 0; } 35%, 38% { opacity: 1; } 41%, 100% { opacity: 0; } }
@keyframes twRead3 { 0%, 37% { opacity: 0; } 39%, 42% { opacity: 1; } 45%, 100% { opacity: 0; } }
.tw-band { transform-box: fill-box; transform-origin: left center; }
@keyframes twBand { 0%, 28% { opacity: 0; transform: scaleX(.15); } 44%, 91% { opacity: 1; transform: scaleX(1); } 96%, 100% { opacity: 0; } }

/* --- protocols: clicks ripple, rows flash with satisfaction, the swap is a venetian flip --- */
.click-rip { fill: none; stroke: rgb(var(--accent-rgb) / .6); stroke-width: 1.5; transform-box: fill-box; transform-origin: center; opacity: 0; }
.click-rip.cr1 { animation: clickRip1 11s var(--ease) infinite; }
.click-rip.cr2 { animation: clickRip2 11s var(--ease) infinite; }
@keyframes clickRip1 { 0%, 13% { opacity: 0; transform: scale(.3); } 15% { opacity: .9; } 22%, 100% { opacity: 0; transform: scale(2.2); } }
@keyframes clickRip2 { 0%, 25% { opacity: 0; transform: scale(.3); } 27% { opacity: .9; } 34%, 100% { opacity: 0; transform: scale(2.2); } }
.row-flash { fill: rgb(var(--accent-rgb) / .12); transform-box: fill-box; transform-origin: left center; opacity: 0; }
.row-flash.rf1 { animation: rowFlash1 11s var(--ease) infinite; }
.row-flash.rf2 { animation: rowFlash2 11s var(--ease) infinite; }
@keyframes rowFlash1 { 0%, 13% { opacity: 0; transform: scaleX(0); } 16% { opacity: 1; transform: scaleX(1); } 24%, 100% { opacity: 0; transform: scaleX(1); } }
@keyframes rowFlash2 { 0%, 25% { opacity: 0; transform: scaleX(0); } 28% { opacity: 1; transform: scaleX(1); } 36%, 100% { opacity: 0; transform: scaleX(1); } }
.sc-old { transform-box: fill-box; transform-origin: center top; }
.sc-new { transform-box: fill-box; transform-origin: center bottom; }
@keyframes scOld { 0%, 40% { opacity: 1; transform: scaleY(1); } 47%, 100% { opacity: 0; transform: scaleY(.05); } }
@keyframes scNew { 0%, 46% { opacity: 0; transform: scaleY(.05); } 54%, 96% { opacity: 1; transform: scaleY(1); } 100% { opacity: 0; } }

/* --- modes: the beam has a bright leading edge; each line brightens as it's read; the weights announce themselves --- */
.scan-edge { fill: rgb(var(--accent-rgb) / .8); opacity: 0; animation: scanEdge 9s var(--ease) infinite;
  filter: drop-shadow(0 0 4px rgb(var(--accent-rgb) / .8)); }
@keyframes scanEdge { 0%, 10% { opacity: 0; transform: translateY(0); } 13% { opacity: 1; }
  20% { transform: translateY(20px); } 27% { transform: translateY(40px); } 34% { opacity: 1; transform: translateY(60px); }
  40%, 100% { opacity: 0; transform: translateY(60px); } }
.vz-doc { animation: docRead 9s var(--ease) infinite; }
.vz-doc:nth-of-type(2) { animation-name: docRead2; }
.vz-doc:nth-of-type(3) { animation-name: docRead3; }
.vz-doc:nth-of-type(4) { animation-name: docRead4; }
@keyframes docRead  { 0%, 12% { fill: rgba(255,255,255,.14); } 15%, 92% { fill: rgba(255,255,255,.28); } 100% { fill: rgba(255,255,255,.14); } }
@keyframes docRead2 { 0%, 19% { fill: rgba(255,255,255,.14); } 22%, 92% { fill: rgba(255,255,255,.28); } 100% { fill: rgba(255,255,255,.14); } }
@keyframes docRead3 { 0%, 26% { fill: rgba(255,255,255,.14); } 29%, 92% { fill: rgba(255,255,255,.28); } 100% { fill: rgba(255,255,255,.14); } }
@keyframes docRead4 { 0%, 33% { fill: rgba(255,255,255,.14); } 36%, 92% { fill: rgba(255,255,255,.28); } 100% { fill: rgba(255,255,255,.14); } }
.bar-wt { fill: var(--green-2); font: 700 7.5px var(--font); opacity: 0; }
.bar-wt.w1 { animation: barWt1 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.bar-wt.w2 { animation: barWt2 9s cubic-bezier(.34,1.56,.64,1) infinite; }
.bar-wt.w3 { animation: barWt3 9s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes barWt1 { 0%, 62% { opacity: 0; transform: translateY(4px); } 66%, 90% { opacity: 1; transform: none; } 96%, 100% { opacity: 0; } }
@keyframes barWt2 { 0%, 66% { opacity: 0; transform: translateY(4px); } 70%, 90% { opacity: 1; transform: none; } 96%, 100% { opacity: 0; } }
@keyframes barWt3 { 0%, 70% { opacity: 0; transform: translateY(4px); } 74%, 90% { opacity: 1; transform: none; } 96%, 100% { opacity: 0; } }

/* --- shop: the picked card casts weight --- */
.pick-shadow { fill: rgba(0,0,0,.5); transform-box: fill-box; transform-origin: center; opacity: 0; animation: pickShadow 9s var(--ease) infinite; }
@keyframes pickShadow { 0%, 20% { opacity: 0; transform: scale(.6); } 26%, 66% { opacity: .55; transform: scale(1); } 74%, 100% { opacity: 0; transform: scale(.6); } }

/* --- privacy: streaks of motion, then a glint seals the vault --- */
.chip-streak { stroke: rgb(var(--accent-rgb) / .5); stroke-width: 1.5; stroke-linecap: round; opacity: 0; }
.chip-streak.st1 { animation: streak1 12s var(--ease) infinite; }
.chip-streak.st2 { animation: streak2 12s var(--ease) infinite; }
.chip-streak.st3 { animation: streak3 12s var(--ease) infinite; }
@keyframes streak1 { 0%, 8% { opacity: 0; } 11% { opacity: .8; } 17%, 100% { opacity: 0; } }
@keyframes streak2 { 0%, 13% { opacity: 0; } 16% { opacity: .8; } 22%, 100% { opacity: 0; } }
@keyframes streak3 { 0%, 18% { opacity: 0; } 21% { opacity: .8; } 27%, 100% { opacity: 0; } }
.shield-glint { fill: none; stroke: rgba(255,255,255,.75); stroke-width: 2.5; stroke-linecap: round;
  stroke-dasharray: 34 396; stroke-dashoffset: 34; opacity: 0; animation: shieldGlint 12s linear infinite; }
@keyframes shieldGlint { 0%, 56% { stroke-dashoffset: 34; opacity: 0; } 58% { opacity: .9; }
  70% { stroke-dashoffset: -396; opacity: .9; } 72%, 100% { stroke-dashoffset: -396; opacity: 0; } }

/* --- the cameras: every scene breathes; entrances tilt in from depth --- */
.viz-body svg { animation: camBreath 16s ease-in-out infinite; transform-origin: center; }
@keyframes camBreath { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.016); } }
@keyframes vizTilt { from { opacity: 0; transform: perspective(1200px) rotateX(5deg) translateY(22px); }
  to { opacity: 1; transform: perspective(1200px) rotateX(0deg) translateY(0); } }
.lp-sec.seen .lp-viz > .viz-body > svg { animation-name: camBreath; }  /* entrance handled by the window now */

/* the windows respond to your presence (wired in JS, gently) */
.lp-viz { transform-style: preserve-3d; }
.lp-viz.tilting { transition: transform .12s linear; }

/* depth inside the frame */
.viz-body { position: relative; }
.viz-body::after { content: ""; position: absolute; inset: 0; pointer-events: none;
  box-shadow: inset 0 0 44px rgba(0,0,0,.32); border-radius: 0 0 26px 26px; }
html.light .viz-body::after { box-shadow: inset 0 0 34px rgba(0,0,0,.08); }

@media (prefers-reduced-motion: reduce) {
  .bloom, .dt-glim, .halo-think, .sc-caret, .tw-read, .click-rip, .row-flash, .scan-edge, .bar-wt,
  .pick-shadow, .chip-streak, .shield-glint, .vital-mirror { animation: none !important; opacity: 0; }
  .vital-mirror { opacity: .08; }
  .viz-body svg, .lp-sec.seen .lp-viz { animation: none !important; }
  .vz-doc { animation: none !important; }
}

/* ============================================================ OPENING TITLES v65 — the first screen IS the show.
   A one-time cinematic entrance, and the live metric chips visible at every window size. */

/* the curtain sequence — plays once on arrival */
.lp-hero .lp-eyebrow { animation: heroDrop .7s cubic-bezier(.22,.9,.36,1) .15s both; }
.lp-hero .lp-sub { animation: heroFade .8s var(--ease) .65s both; }
.lp-hero .lp-ekg { animation: heroFade .8s var(--ease) .85s both; }
.lp-hero .lp-cta .btn:first-child { animation: heroSpring .7s cubic-bezier(.34,1.56,.64,1) 1s both, ctaBreath 6s ease-in-out 2s infinite; }
.lp-hero .lp-cta .btn:last-child { animation: heroSpring .7s cubic-bezier(.34,1.56,.64,1) 1.12s both; }
.lp-hero .lp-fine { animation: heroFade .7s var(--ease) 1.3s both; }
@keyframes heroDrop { from { opacity: 0; transform: translateY(-14px); } to { opacity: 1; transform: none; } }
@keyframes heroRise { from { opacity: 0; transform: translateY(26px); filter: blur(7px); } to { opacity: 1; transform: none; filter: blur(0); } }
@keyframes heroFade { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes heroSpring { from { opacity: 0; transform: translateY(16px) scale(.94); } to { opacity: 1; transform: none; } }

/* the live chips: floating on wide screens, a proud proof-row on everything else */
@media (max-width: 1080px) {
  .lp-float { display: flex !important; position: static; justify-content: center; flex-wrap: wrap; gap: 10px;
    margin: 22px auto 0; max-width: 560px; order: 99; }
  .lp-chip { position: static; }
  .lp-chip.c1 { animation-delay: 1.35s, .5s; } .lp-chip.c2 { animation-delay: 1.45s, 1.6s; }
  .lp-chip.c3 { animation-delay: 1.55s, 3.1s; } .lp-chip.c4 { animation-delay: 1.65s, 4.4s; }
  .lp-hero { display: flex; flex-direction: column; align-items: center; }
}

@media (prefers-reduced-motion: reduce) {
  .lp-hero .lp-eyebrow, .lp-hero h1, .lp-hero .lp-sub, .lp-hero .lp-ekg,
  .lp-hero .lp-cta .btn, .lp-hero .lp-fine { animation: none; opacity: 1; transform: none; filter: none; }
}
/* build badge — the truth-teller */
.lp-build { color: rgb(var(--accent-rgb) / .55); font-weight: 600; letter-spacing: .4px; }

/* ============================================================ PROJECTION PASS v68 — the same film, projected at full brightness.
   Audit finding: every v63 effect ran, but tuned below the threshold of perception. Amplitudes doubled; choreography unchanged. */

/* the cameras: a breath you can actually see */
@keyframes camBreath { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.028); } }
.viz-body svg { animation-duration: 11s; }
#lps-chat .viz-body svg { animation-duration: 13s; }
#lps-twin .viz-body svg { animation-duration: 10s; }
#lps-protocols .viz-body svg { animation-duration: 12s; }
#lps-shop .viz-body svg { animation-duration: 14s; }

/* entrances: windows genuinely arrive from depth */
.lp-sec.seen .lp-viz { animation: vizTilt 1.05s cubic-bezier(.22,.9,.36,1) both; }
@keyframes vizTilt {
  from { opacity: 0; transform: perspective(1100px) rotateX(16deg) translateY(54px) scale(.94); filter: blur(5px); }
  55% { filter: blur(0); }
  to { opacity: 1; transform: perspective(1100px) rotateX(0deg) translateY(0) scale(1); filter: blur(0); } }

/* the light: a sweep you can't miss */
.lp-viz::before { width: 44%; background: linear-gradient(100deg, transparent, rgba(255,255,255,.13), transparent); }

/* payoff blooms: twice as loud */
.bloom { fill: rgb(var(--accent-rgb) / .5); filter: blur(9px); }

/* sparks and light trails: glowing, not faint */
.burst circle { filter: drop-shadow(0 0 4px var(--green-3)); }
.dt-glim { stroke-width: 3.5; stroke-dasharray: 40 400; filter: drop-shadow(0 0 5px rgba(255,255,255,.7)); }
.shield-glint { stroke-width: 3.5; stroke: rgba(255,255,255,.95); filter: drop-shadow(0 0 5px rgba(255,255,255,.6)); }
.halo-think { stroke-width: 2.2; stroke: rgb(var(--accent-rgb) / .8); filter: drop-shadow(0 0 4px rgb(var(--accent-rgb) / .55)); }
.sc-caret { filter: drop-shadow(0 0 3px var(--green-3)); }
.scan-edge { filter: drop-shadow(0 0 9px rgb(var(--accent-rgb) / .9)); }
.chip-streak { stroke-width: 2.4; }

/* readouts and labels: legible at a glance */
.tw-read { font-size: 10.5px; font-weight: 800; }
.bar-wt { font-size: 9px; font-weight: 800; }

/* interactions: ripples travel, rows flash brighter, lines brighten harder as read */
.click-rip { stroke-width: 2.2; stroke: rgb(var(--accent-rgb) / .85); }
@keyframes clickRip1 { 0%, 13% { opacity: 0; transform: scale(.3); } 15% { opacity: 1; } 22%, 100% { opacity: 0; transform: scale(3); } }
@keyframes clickRip2 { 0%, 25% { opacity: 0; transform: scale(.3); } 27% { opacity: 1; } 34%, 100% { opacity: 0; transform: scale(3); } }
.row-flash { fill: rgb(var(--accent-rgb) / .2); }
@keyframes docRead  { 0%, 12% { fill: rgba(255,255,255,.14); } 15%, 92% { fill: rgba(255,255,255,.4); } 100% { fill: rgba(255,255,255,.14); } }
@keyframes docRead2 { 0%, 19% { fill: rgba(255,255,255,.14); } 22%, 92% { fill: rgba(255,255,255,.4); } 100% { fill: rgba(255,255,255,.14); } }
@keyframes docRead3 { 0%, 26% { fill: rgba(255,255,255,.14); } 29%, 92% { fill: rgba(255,255,255,.4); } 100% { fill: rgba(255,255,255,.14); } }
@keyframes docRead4 { 0%, 33% { fill: rgba(255,255,255,.14); } 36%, 92% { fill: rgba(255,255,255,.4); } 100% { fill: rgba(255,255,255,.14); } }

/* the picked card lifts with real weight */
@keyframes scHero { 0%, 20% { transform: translateY(0) scale(1) rotate(0deg); } 26%, 66% { transform: translateY(-11px) scale(1.055) rotate(-1.4deg); } 74%, 100% { transform: translateY(0) scale(1) rotate(0deg); } }

/* hero: chips drift visibly; the reflection reads as glass */
@keyframes chipFloat { 0%, 100% { translate: 0 0; } 50% { translate: 0 -13px; } }
.vital-mirror { opacity: .24; }

/* ============================================================ ONE CONTINUOUS SHOT v69 — scroll is the timeline.
   The page stops being a stack of cards and becomes a camera: a pinned journey the visitor scrubs through,
   kinetic type that assembles under the thumb, and scenes that each enter from their own axis. */

/* --- hero: layers recede at different speeds as you leave (depth = expensive) --- */
.lp-hero { --hp: 0; }
.lp-hero .lp-eyebrow, .lp-hero h1, .lp-hero .lp-sub, .lp-hero .lp-ekg,
.lp-hero .lp-cta .btn, .lp-hero .lp-fine { animation-fill-mode: backwards; }
.lp-hero h1 { transform: translateY(calc(var(--hp) * -52px)); will-change: transform; }
.lp-float { transform: translateY(calc(var(--hp) * 40px)); opacity: calc(1 - var(--hp) * 1.35); }

/* --- kinetic type: statements assemble word by word, scrubbed by scroll --- */
.lp-kinetic { min-height: 42vh; display: flex; align-items: center; justify-content: center; text-align: center; padding: 34px 0; }
.lp-kinetic p { margin: 0; max-width: 840px; perspective: 800px; font-size: clamp(1.65rem, 4.4vw, 2.7rem);
  font-weight: 800; letter-spacing: -0.02em; line-height: 1.34; color: var(--text); }
.lp-kinetic .kw { display: inline-block; --wp: clamp(0, calc(var(--p, 1) * 3.4 - var(--i) * 0.12 - 0.6), 1);
  opacity: calc(0.07 + var(--wp) * 0.93);
  transform: translateY(calc((1 - var(--wp)) * 30px)) rotateX(calc((1 - var(--wp)) * 60deg));
  filter: blur(calc((1 - var(--wp)) * 7px)); will-change: transform, opacity, filter; }
.lp-kinetic .ka { background: linear-gradient(100deg, #3ecf8e 10%, #4ade80 90%); -webkit-background-clip: text; background-clip: text; color: transparent; }

/* --- the journey: one window, four beats, the camera swings between them --- */
.lp-journey { height: 430vh; position: relative; margin: 10px 0 40px; }
.jn-stick { position: sticky; top: 0; height: 100vh; height: 100dvh; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 16px; overflow: hidden; }
.jn-rail { display: flex; gap: 8px; z-index: 3; flex-wrap: wrap; justify-content: center; }
.jn-dot { font: 600 0.78rem var(--font); letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-dim);
  background: transparent; border: 1px solid var(--hairline); border-radius: 999px; padding: 7px 16px; cursor: pointer;
  transition: color .35s, border-color .35s, background .35s; }
.jn-dot:hover { color: var(--text); }
.jn-b0 .jn-dot:nth-child(1), .jn-b1 .jn-dot:nth-child(2), .jn-b2 .jn-dot:nth-child(3), .jn-b3 .jn-dot:nth-child(4) {
  color: var(--green-2); border-color: rgb(var(--accent-rgb) / .55); background: rgb(var(--accent-rgb) / .09); }
.jn-prog { width: min(400px, 70%); height: 3px; border-radius: 2px; background: var(--elev-2); overflow: hidden; }
.jn-prog i { display: block; height: 100%; transform-origin: left; transform: scaleX(var(--jp, 0));
  background: linear-gradient(90deg, var(--green-deep), var(--green-2), var(--green)); border-radius: 2px; }
.jn-stagewrap { display: grid; grid-template-columns: 1.12fr 1fr; gap: 46px; align-items: center;
  width: 100%; max-width: 1100px; padding: 0 6px; }
.jn-cam { transform-style: preserve-3d; transition: transform 1.15s cubic-bezier(.22,.9,.32,1); will-change: transform; }
.jn-b0 .jn-cam { transform: rotateY(-13deg) rotateX(3deg); }
.jn-b1 .jn-cam { transform: rotateY(11deg) rotateX(-2deg) scale(1.02); }
.jn-b2 .jn-cam { transform: rotateY(-9deg) rotateX(5deg) scale(1.045); }
.jn-b3 .jn-cam { transform: rotateY(12deg) rotateX(-3deg) scale(1.01); }
.jn-scene { grid-area: 1 / 1; opacity: 0; transform: translateX(42px) rotateY(10deg);
  transition: opacity .6s ease, transform .95s cubic-bezier(.22,.9,.32,1); }
.jn-scene svg { display: block; width: 100%; height: auto; }
.jn-b1 .s0, .jn-b2 .s0, .jn-b3 .s0, .jn-b2 .s1, .jn-b3 .s1, .jn-b3 .s2 { transform: translateX(-42px) rotateY(-10deg); }
.jn-b0 .s0, .jn-b1 .s1, .jn-b2 .s2, .jn-b3 .s3 { opacity: 1; transform: none; }
.jn-copy { display: grid; align-items: center; }
.jn-cp { grid-area: 1 / 1; opacity: 0; transform: translateY(30px);
  transition: opacity .55s ease .12s, transform .7s cubic-bezier(.22,.9,.36,1) .12s; }
.jn-cp h3 { font-size: clamp(1.45rem, 3vw, 1.9rem); font-weight: 700; color: var(--text); margin: 10px 0; line-height: 1.18; letter-spacing: -0.015em; }
.jn-cp p { color: var(--text-dim); line-height: 1.65; font-size: 0.98rem; margin: 0; }
@media (max-width: 880px) {
  .jn-stagewrap { grid-template-columns: 1fr; gap: 16px; max-width: 640px; }
  .jn-copy { text-align: center; min-height: 200px; align-items: start; }
}
@media (max-height: 720px) { .jn-cp p { font-size: 0.9rem; } }

/* --- the three closing scenes: each enters on its own axis, then drifts with the scroll --- */
.lp-sec.cine { opacity: 1; transform: none; }
.lp-vizwrap { width: 100%; will-change: transform; }
.lp-sec.rev .lp-vizwrap { order: 2; }
@media (max-width: 760px) { .lp-sec.rev .lp-vizwrap { order: 0; } }
.cine-left .lp-vizwrap { transform-origin: left center;
  transform: perspective(1300px) rotateY(calc(max(0, 0.62 - var(--p, 1)) * 55deg)) translateX(calc(max(0, 0.62 - var(--p, 1)) * -70px));
  opacity: min(1, calc(var(--p, 1) * 3.2)); }
.cine-rise .lp-vizwrap { transform-origin: center bottom;
  transform: perspective(1300px) rotateX(calc(max(0, 0.62 - var(--p, 1)) * 42deg)) translateY(calc(max(0, 0.62 - var(--p, 1)) * 90px));
  opacity: min(1, calc(var(--p, 1) * 3.2)); }
.cine-zoom .lp-vizwrap {
  transform: scale(calc(1 - max(0, 0.62 - var(--p, 1)) * 0.35)) translateY(calc(max(0, 0.62 - var(--p, 1)) * 30px));
  opacity: min(1, calc(var(--p, 1) * 3.2)); }
.lp-sec.cine .lp-copy { transform: translateY(calc(max(0, 0.6 - var(--p, 1)) * 46px)); }

/* --- pricing rises to meet you --- */
.lp-pricing[data-cine] { transform: translateY(calc(max(0, 0.55 - var(--p, 1)) * 60px));
  opacity: min(1, calc(0.05 + var(--p, 1) * 2.6)); }

/* --- stillness is honored --- */
@media (prefers-reduced-motion: reduce) {
  .lp-kinetic .kw { opacity: 1; transform: none; filter: none; }
  .lp-hero h1, .lp-hero .lp-sub, .lp-hero .lp-ekg, .lp-hero .lp-cta, .lp-float { transform: none; opacity: 1; }
  .lp-vizwrap, .lp-sec.cine .lp-copy, .lp-pricing[data-cine] { transform: none; opacity: 1; }
}

/* journey copy carries the same green keywords as section copy */
.jn-cp .hl { color: var(--green-2); font-weight: 650; }

/* ============================================================ PERFORMANCE PASS v70 — the actors, not the stage.
   Causality made visible: numbers roll like odometers as readings land, data physically flies from its
   source into the chart, a comet head rides every line as it draws, and payoffs land with spring physics. */

/* --- data: the hero number is an odometer; each landing reading rolls it down --- */
.dt-roll { animation: dtRoll 9s infinite; }
@keyframes dtRoll {
  0%, 16%  { transform: translateY(0);      animation-timing-function: cubic-bezier(.34,1.56,.64,1); opacity: 1; }
  19%, 24% { transform: translateY(-20px);  animation-timing-function: cubic-bezier(.34,1.56,.64,1); }
  27%, 32% { transform: translateY(-40px);  animation-timing-function: cubic-bezier(.34,1.56,.64,1); }
  35%, 94% { transform: translateY(-60px);  opacity: 1; }
  97%      { transform: translateY(-60px);  opacity: 0; }
  99.9%    { transform: translateY(0);      opacity: 0; }
  100%     { transform: translateY(0);      opacity: 1; }
}

/* --- data: readings fly from the source chip into the chart, landing on the ripples --- */
.dt-mote { fill: var(--green-3); filter: drop-shadow(0 0 4px var(--green-3)); opacity: 0; }
.dt-mote.m1 { animation: dtMote1 9s linear infinite; }
.dt-mote.m2 { animation: dtMote2 9s linear infinite; }
.dt-mote.m3 { animation: dtMote3 9s linear infinite; }
@keyframes dtMote1 { 0%, 10% { opacity: 0; } 11%, 15% { opacity: 1; } 16%, 100% { opacity: 0; } }
@keyframes dtMote2 { 0%, 18% { opacity: 0; } 19%, 23% { opacity: 1; } 24%, 100% { opacity: 0; } }
@keyframes dtMote3 { 0%, 26% { opacity: 0; } 27%, 31% { opacity: 1; } 32%, 100% { opacity: 0; } }

/* --- the brand's living light flows inside the drawn lines themselves --- */
.dt-proj, .tw-proj { stroke: url(#haleFlow); }

/* --- twin: a comet head rides the pen tip while the projection draws --- */
.tw-proj { animation-timing-function: linear; }
.tw-comet { fill: #eafff5; filter: drop-shadow(0 0 6px var(--green-3)); opacity: 0; animation: twComet 11s linear infinite; }
@keyframes twComet { 0%, 26% { opacity: 0; } 27%, 43% { opacity: 1; } 46%, 100% { opacity: 0; } }

/* --- chat: what Hale learns visibly leaves the reply and files itself into memory --- */
.sc-savemote { fill: var(--green-3); filter: drop-shadow(0 0 5px var(--green-3)); opacity: 0; animation: saveMote 11s linear infinite; }
@keyframes saveMote { 0%, 76% { opacity: 0; } 77%, 81% { opacity: 1; } 82%, 100% { opacity: 0; } }

/* --- chat: thinking dots get a real wave, not a blink --- */
.vz-tdot { transform-box: fill-box; }
@keyframes vzTdot { 0%, 100% { opacity: .25; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }

/* --- protocols: checkboxes pop under the click; the "new" tag stamps in with recoil --- */
.sc-box { transform-box: fill-box; transform-origin: center; }
.sc-row.r1 .sc-box { animation: boxPop1 11s cubic-bezier(.34,1.56,.64,1) infinite; }
.sc-row.r2 .sc-box { animation: boxPop2 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes boxPop1 { 0%, 13% { transform: scale(1); } 15% { transform: scale(1.28); } 18%, 100% { transform: scale(1); } }
@keyframes boxPop2 { 0%, 25% { transform: scale(1); } 27% { transform: scale(1.28); } 30%, 100% { transform: scale(1); } }
.sc-new .tag { transform-box: fill-box; transform-origin: center; animation: tagStamp 11s cubic-bezier(.34,1.56,.64,1) infinite; }
@keyframes tagStamp { 0%, 55% { transform: scale(0) rotate(-9deg); } 61%, 100% { transform: scale(1) rotate(0deg); } }

/* --- modes: research doesn't just scan — it highlights what it found --- */
.sc-hilite { fill: rgb(var(--accent-rgb) / .22); transform-box: fill-box; transform-origin: left center;
  transform: scaleX(0); animation: hilite 9s linear infinite; }
@keyframes hilite { 0%, 19% { transform: scaleX(0); opacity: 0; } 20% { opacity: 1; }
  27%, 91% { transform: scaleX(1); opacity: 1; } 96%, 100% { transform: scaleX(1); opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .dt-roll { animation: none; transform: translateY(-60px); }
  .dt-mote, .tw-comet, .sc-savemote { animation: none; opacity: 0; }
  .sc-hilite { animation: none; transform: scaleX(1); opacity: 1; }
  .sc-box, .sc-new .tag { animation: none; transform: none; }
}

/* ============================================================ SET DRESSING v71 — no empty frames, no oversized stage.
   The chart is an instrument even between readings: ghost trend, dashed landing rings, fine gridwork. */
@media (max-width: 880px) { .jn-stagewrap { max-width: 600px; } }
.vfaint { opacity: .38; }
.dt-ghost { fill: none; stroke: rgb(var(--accent-rgb) / .13); stroke-width: 2; stroke-dasharray: 3 6; stroke-linecap: round; }
.dt-ring { fill: none; stroke: rgb(var(--accent-rgb) / .3); stroke-width: 1.2; stroke-dasharray: 2.5 3;
  transform-box: fill-box; transform-origin: center; animation: ringSpin 14s linear infinite; }
@keyframes ringSpin { to { transform: rotate(360deg); } }
.sc-typing { fill: var(--text-faint); font: italic 500 8px var(--font); }
@media (prefers-reduced-motion: reduce) { .dt-ring { animation: none; } }

/* ============================================================ REACTION PASS v72 — the scenes acknowledge what happens to them.
   Rings retire when their reading strikes, the target zone flashes when the comet lands,
   and the trigger chip visibly causes the habit swap. */

/* --- measure: landing rings orbit while they wait, pop on impact, then retire --- */
.dt-ring.g1 { animation: ringG1 9s linear infinite; }
.dt-ring.g2 { animation: ringG2 9s linear infinite; }
@keyframes ringG1 {
  0% { transform: rotate(0deg) scale(1); opacity: 0; }
  4% { opacity: .85; }
  24% { transform: rotate(96deg) scale(1); opacity: .85; animation-timing-function: cubic-bezier(.34,1.56,.64,1); }
  27% { transform: rotate(104deg) scale(1.5); opacity: 1; }
  31%, 100% { transform: rotate(110deg) scale(1.75); opacity: 0; } }
@keyframes ringG2 {
  0% { transform: rotate(0deg) scale(1); opacity: 0; }
  4% { opacity: .85; }
  32% { transform: rotate(128deg) scale(1); opacity: .85; animation-timing-function: cubic-bezier(.34,1.56,.64,1); }
  35% { transform: rotate(136deg) scale(1.5); opacity: 1; }
  39%, 100% { transform: rotate(142deg) scale(1.75); opacity: 0; } }

/* --- model: the target zone lights up the moment the projection lands in it --- */
.jn-scene.s2 .tw-zone, #lps-twin .tw-zone { animation: zoneFlash 11s var(--ease) infinite; }
@keyframes zoneFlash { 0%, 43% { fill: rgb(var(--accent-rgb) / .06); } 45.5% { fill: rgb(var(--accent-rgb) / .2); }
  50%, 100% { fill: rgb(var(--accent-rgb) / .06); } }

/* --- act: the ApoB trigger visibly fires the swap --- */
.pr-mote { fill: var(--green-3); filter: drop-shadow(0 0 4px var(--green-3)); opacity: 0; animation: prMote 11s linear infinite; }
@keyframes prMote { 0%, 36% { opacity: 0; } 37%, 40% { opacity: 1; } 41%, 100% { opacity: 0; } }

/* --- ask: follow-up chips pop like chips, not fades --- */
@keyframes scFup1 { 0%, 86% { opacity: 0; transform: translateY(8px) scale(.8); } 90%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }
@keyframes scFup2 { 0%, 89% { opacity: 0; transform: translateY(8px) scale(.8); } 93%, 96% { opacity: 1; transform: none; } 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .dt-ring.g1, .dt-ring.g2, .pr-mote, .jn-scene.s2 .tw-zone, #lps-twin .tw-zone { animation: none; }
  .pr-mote { opacity: 0; }
}

/* ============================================================ TIGHTEN PASS v73 — joints, not features.
   The reply assembles its chart the moment the words mention it; the cone opens under the knob's drag;
   the habit swap hands off with no empty slot. */

/* --- ask: axis draws right as the second line finishes, then curve, points, labels — a cascade --- */
.mini-ax { stroke-dasharray: 1; stroke-dashoffset: 1; animation: miniAx 11s linear infinite; }
@keyframes miniAx { 0%, 61% { stroke-dashoffset: 1; } 65%, 100% { stroke-dashoffset: 0; } }

/* --- model: the cone is cast open by the drag itself --- */
@keyframes twBand { 0%, 23% { opacity: 0; transform: scaleX(.15); } 26% { opacity: 1; }
  40%, 91% { opacity: 1; transform: scaleX(1); } 96%, 100% { opacity: 0; } }

/* --- act: venetian handoff with no hole — new unfolds as old finishes collapsing --- */
@keyframes scNew { 0%, 45.5% { opacity: 0; transform: scaleY(.05); } 46.5% { opacity: 1; }
  52%, 96% { opacity: 1; transform: scaleY(1); } 100% { opacity: 0; } }
@keyframes tagStamp { 0%, 52% { transform: scale(0) rotate(-9deg); } 58%, 100% { transform: scale(1) rotate(0deg); } }

@media (prefers-reduced-motion: reduce) { .mini-ax { animation: none; stroke-dashoffset: 0; } }

/* ============================================================ MIRROR & LENS v74 — reflections that stay in their glass,
   and a camera that moves INSIDE each scene: wide → punch-in → follow → settle, cut to the story's beats. */

/* --- the reflection now lives INSIDE the svg canvas (viewBox grew 52→84) — it can never touch other content --- */
.hero-ekg, .lp-ekg { height: 84px; overflow: hidden; }
.vital-mirror { transform: translateY(71px) scaleY(-.42); }

/* --- the headline reflects too (landing only): a flipped, fading twin that reserves its own space --- */
.h1-mirror { font-family: var(--font); font-size: clamp(2.2rem, 6vw, 3.6rem); font-weight: 800; line-height: 1.05;
  letter-spacing: -.8px; margin: -6px 0 -14px; pointer-events: none; user-select: none;
  background: linear-gradient(92deg, var(--green-deep), var(--green-2) 20%, var(--green) 50%, var(--green-2) 80%, var(--green-deep));
  background-size: 220% 100%; -webkit-background-clip: text; background-clip: text; color: transparent;
  transform: translateY(calc(var(--hp, 0) * -52px)) scaleY(-1); opacity: .14; filter: blur(1.1px);
  mask-image: linear-gradient(180deg, transparent 46%, rgba(0,0,0,.6) 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 46%, rgba(0,0,0,.6) 100%);
  margin-bottom: -0.62em;   /* the masked-invisible tail does not hold layout space */
  animation: mirrorIn 1.2s ease .5s backwards; }
@keyframes mirrorIn { from { opacity: 0; } }

/* --- the lens: each journey scene has its own camera score, cut to its beats --- */
.jn-scene svg { transform-origin: center; }
.jn-scene.s0 svg { animation: camS0 9s cubic-bezier(.4,0,.2,1) infinite; }
.jn-scene.s1 svg { animation: camS1 11s cubic-bezier(.4,0,.2,1) infinite; }
.jn-scene.s2 svg { animation: camS2 11s cubic-bezier(.4,0,.2,1) infinite; }
.jn-scene.s3 svg { animation: camS3 11s cubic-bezier(.4,0,.2,1) infinite; }
/* Measure: wide → medium on the roll → close on the drawing curve → drift to the verdict flag → settle */
@keyframes camS0 {
  0%, 8%    { transform: none; }
  16%, 34%  { transform: scale(1.05) translate(0, -2%); }
  40%, 52%  { transform: scale(1.08) translate(-2%, -6%); }
  58%, 72%  { transform: scale(1.07) translate(-5%, 6%); }
  80%, 90%  { transform: scale(1.02); }
  97%, 100% { transform: none; }
}
/* Ask: open on the question → lean into the thinking → track the typed reply → close on the chart → cut to the memory stamp */
@keyframes camS1 {
  0%, 6%    { transform: none; }
  10%, 24%  { transform: scale(1.07) translate(6%, 2%); }
  28%, 46%  { transform: scale(1.05) translate(3%, 0); }
  52%, 66%  { transform: scale(1.08) translate(4%, -4%); }
  74%, 86%  { transform: scale(1.07) translate(-5%, -4%); }
  94%, 100% { transform: none; }
}
/* Model: close on the control being dragged → travel down the projection with the comet → pull up to the verdict → settle */
@keyframes camS2 {
  0%, 6%    { transform: none; }
  12%, 22%  { transform: scale(1.07) translate(-6%, 5%); }
  28%       { transform: scale(1.07) translate(5%, 1%); }
  42%, 48%  { transform: scale(1.07) translate(-5%, -5%); }
  54%, 64%  { transform: scale(1.05) translate(4%, 4%); }
  74%, 88%  { transform: scale(1.02); }
  96%, 100% { transform: none; }
}
/* Act: on the checklist as it ticks → whip to the trigger → close on the swap → pull back for the tally */
@keyframes camS3 {
  0%, 6%    { transform: none; }
  10%, 30%  { transform: scale(1.06) translate(3%, 2%); }
  36%, 42%  { transform: scale(1.07) translate(-6%, 1%); }
  46%, 58%  { transform: scale(1.08) translate(2%, -4%); }
  66%, 82%  { transform: scale(1.04) translate(-2%, -3%); }
  92%, 100% { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .h1-mirror { animation: none; opacity: .08; }
  .jn-scene svg { animation: none !important; }
}

/* --- v74: products the coach recommends, inline in chat — real links, no salesmanship --- */
.chat-product { display: flex; align-items: center; gap: 12px; margin: 8px 0; padding: 10px 14px;
  background: rgba(255,255,255,.04); border: 1px solid rgb(var(--accent-rgb) / .28); border-radius: 14px; }
.chat-product .cp-tile { flex: 0 0 auto; width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 10px; background: rgb(var(--accent-rgb) / .14); color: var(--green-2); font-size: 1rem; }
.chat-product .cp-meta { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.chat-product .cp-name { font-weight: 650; color: var(--text); font-size: .95rem; }
.chat-product .cp-links { display: flex; gap: 14px; flex-wrap: wrap; }
.chat-product .cp-links a { color: var(--green-2); font-size: .82rem; font-weight: 600; text-decoration: none; }
.chat-product .cp-links a:hover { text-decoration: underline; }
html.light .chat-product { background: rgba(255,255,255,.7); }

/* --- v75: real product photos on the cards (icon stays as instant fallback underneath) --- */
.p-tile { position: relative; overflow: hidden; }
.p-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; background: #fff; }
#modalLegal { z-index: 320; }   /* legal docs stack above the sign-in modal */

/* ============================================================ FIRST AID v77 — sweep findings, all verified */
/* the focus-ring leak family, closed everywhere it recurs */
.mkt-search input:focus, .twin-weeks input:focus { box-shadow: none; }
/* iPhone Safari: small-viewport height + home-indicator clearance */
.app { height: 100dvh; }
/* My Data sparklines join the brand gradient (rule previously targeted a class that never existed) */
.mk-spark polyline { stroke: url(#haleFlow); }
/* auth divider: undefined var(--dim) rendered it full-bright */
.oauth-div { color: var(--text-faint); }

/* ============================================================ CONNECTIONS v78 — the ring and the watch, wired in */
.conn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 760px) { .conn-grid { grid-template-columns: 1fr; } }
.conn-head { display: flex; align-items: center; gap: 9px; font-size: 1rem; margin-bottom: 6px; color: var(--text); }
.conn-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--text-faint); flex: 0 0 auto; }
.conn-dot.on { background: var(--green-2); box-shadow: 0 0 8px rgb(var(--accent-rgb) / .6); }
.conn-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* ============================================================================
   v83 — THE LIVING SYSTEM.  A real palette, semantic color, and motion with
   amplitude.  Prior passes built ~120 animations that each moved <90px in one
   hue; this layer trades a hundred whispers for a dozen statements.
   ============================================================================ */
:root {
  /* the body's own colors — oxygenation is the through-line */
  --arterial:    #F43F5E;   /* out of range / the beat            */
  --arterial-dp: #7F1D3A;
  --caution:     #FB7185;   /* borderline                          */
  --venous:      #15803D;   /* deoxygenated, the "before"          */
  --venous-dp:   #0A2E1E;
  --cyan:        #86EFAC;   /* measurement                         */
  --teal:        #3ECF8E;
  --emerald:     #4ADE80;   /* in range, resolved                  */
  --emerald-lt:  #86EFAC;
  --plasma:      #B7F7A8;   /* thinking / insight                  */
  --brass:       #3ECF8E;   /* the instrument (green now — name kept, 30+ callsites) */
  --brass-lit:   #EAFFF1;
  --violet:      #A9DDBD;   /* simulated, not measured             */
  --bone:        #F5F3EF;

  --abyss:       #05070A;
  --case-hi:     #121A18;
  --edge:        rgba(255,255,255,.09);
  --metal: linear-gradient(120deg, #14432A 0%, var(--brass) 22%, var(--brass-lit) 34%, var(--brass) 46%, #14432A 72%);
  --lume-glow: 0 0 24px rgb(var(--accent-rgb) / .45);
}

/* Numbers must never jitter.  Three words, every readout in the product. */
.mk-val, .stat-val, .lp-plan-price, .lp-chip, .tw-val, .mk-delta, .lp-num,
.sys-num, .jn-num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* --- SEMANTIC STATUS -------------------------------------------------------
   Until now "out of range" rendered dim gray while "you're fine" got the
   bright green — the alarming state was the quietest thing on screen.
   Status now owns hue: red is wrong, amber is borderline, green is right. */
.mk-flag.low, .mk-flag.high, .conf.low,
.mk-chip.out, [data-status="out"] {
  color: var(--arterial);
  border-color: rgb(244 63 94 / .45);
  background: rgb(244 63 94 / .10);
}
.mk-flag.border, .conf.mid, [data-status="borderline"] {
  color: var(--caution); border-color: rgb(251 113 133 / .40); background: rgb(251 113 133 / .09);
}
.mk-flag.ok, .conf.high, [data-status="in"] {
  color: var(--emerald); border-color: rgb(74 222 128 / .38); background: rgb(74 222 128 / .09);
}
/* the Twin projects — that is a different kind of fact from a measurement */
.tw-proj, .dt-proj, .twin-proj polyline, .tw-line.proj { stroke: var(--violet); color: var(--violet); }
.tw-proj { stroke-dasharray: 5 4; }
/* Hale thinking: insight has a color now */
.typing, .thinking { color: var(--green-2); }
.typing i, .thinking i { background: var(--plasma); box-shadow: 0 0 10px rgb(183 247 168 / .7); }

/* --- LANDING: undo the self-sabotage -------------------------------------- */
/* v68 built the boldest entrance in the file, then v70 disabled it on every
   section that actually exists.  It runs now — at roughly double amplitude. */
.lp-sec.cine .lp-viz { animation: vizTilt 1.15s cubic-bezier(.16,1,.3,1) both !important; }
@keyframes vizTilt {
  from { opacity: 0; transform: perspective(1500px) rotateY(30deg) rotateX(9deg) translateY(70px) translateZ(-190px); filter: blur(9px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
/* heroSheen slid a gradient whose two ends were the SAME hex — a 14s animation
   that mathematically could not be seen.  Now it travels the whole arc. */
.lp-hero h1 {
  background-image: linear-gradient(96deg,
    var(--emerald) 0%, var(--teal) 18%, var(--cyan) 34%,
    var(--plasma) 52%, var(--emerald-lt) 70%, var(--emerald) 100%) !important;
  background-size: 320% 100% !important;
  animation: heroRise 1.15s cubic-bezier(.16,1,.3,1) .25s both, heroSweep 9s ease-in-out 1.4s infinite !important;
}
@keyframes heroSweep { 0%,100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
/* the headline now arrives instead of politely fading up */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(64px) scale(.86); filter: blur(16px); letter-spacing: .12em; }
  to   { opacity: 1; transform: none; filter: blur(0); letter-spacing: inherit; }
}

/* --- PARALLAX WITH ACTUAL DEPTH ------------------------------------------
   was: every layer travelling ~50px in the same direction (a 5% parallax,
   i.e. no perceptible separation).  now: 5 planes, opposed directions,
   340px of differential travel, with scale and blur on the receding ones. */
.lp-hero h1        { transform: translate3d(0, calc(var(--hp,0) * -210px), 0) scale(calc(1 - var(--hp,0) * .14)); }
.lp-hero .h1-mirror{ transform: translate3d(0, calc(var(--hp,0) * -210px), 0) scale(calc(1 - var(--hp,0) * .14)) scaleY(-1); }
.lp-hero .lp-sub   { transform: translate3d(0, calc(var(--hp,0) * -128px), 0); opacity: calc(1 - var(--hp,0) * 1.5); filter: blur(calc(var(--hp,0) * 5px)); }
.lp-hero .lp-ekg   { transform: translate3d(0, calc(var(--hp,0) * -40px), 0) scale(calc(1 + var(--hp,0) * .5)); opacity: calc(1 - var(--hp,0) * .55); }
.lp-hero .lp-cta   { transform: translate3d(0, calc(var(--hp,0) * -62px), 0); opacity: calc(1 - var(--hp,0) * 1.7); }
.lp-hero .lp-float { opacity: calc(1 - var(--hp,0) * 1.5); }
/* the four chips scatter outward as the hero recedes — real separation, not a slab */
.lp-hero .lp-chip { transform: translate3d(calc(var(--hp,0) * var(--fx,0) * 190px), calc(var(--hp,0) * var(--fy,1) * 160px), 0) scale(calc(1 - var(--hp,0) * .34)); }
.lp-hero .lp-chip.c1 { --fx:-1; --fy:1 }
.lp-hero .lp-chip.c2 { --fx: 1; --fy:1 }
.lp-hero .lp-chip.c3 { --fx:-1.3; --fy:1.5 }
.lp-hero .lp-chip.c4 { --fx: 1.3; --fy:1.5 }

/* the EKG is the most brand-owning thing on the page and it was rendered
   smaller than the paragraph above it.  Give it the room it earns. */
.lp-hero .lp-ekg { width: min(880px, 96%) !important; height: 168px !important; filter: drop-shadow(0 0 22px rgb(62 207 142 / .45)); }
.lp-hero .lp-ekg polyline { stroke-width: 3.2 !important; }

/* --- SECTIONS THAT BREAK THE COLUMN AND CHANGE COLOR ---------------------- */
.lp-sec.bleed { position: relative; width: 100vw; margin-inline: calc(50% - 50vw); padding-inline: max(24px, calc(50vw - 620px)); }
.lp-sec.bleed::before { content: ""; position: absolute; inset: 0; z-index: -1; }
#lps-modes.bleed::before { background: radial-gradient(120% 90% at 18% 0%, var(--venous-dp), #0A1812 55%, var(--abyss)); }
#lps-shop.bleed::before  { background: radial-gradient(110% 80% at 82% 8%, #241A0D, #0D0B07 60%, var(--abyss)); }
#lps-privacy.bleed::before { background: radial-gradient(100% 80% at 50% 40%, #1A0A12, #0A0609 60%, var(--abyss)); }
/* a gold rule that draws itself across the whole viewport as the section lands */
.lp-sec.bleed .lp-eyebrow { position: relative; }
.lp-sec.bleed .lp-eyebrow::after {
  content: ""; position: absolute; left: 100%; top: 50%; margin-left: 14px;
  height: 1px; width: 60vw; background: var(--metal); background-size: 300% 100%;
  transform: scaleX(var(--e, 0)); transform-origin: left;
  transition: transform 900ms cubic-bezier(.16,1,.3,1);
}
.lp-sec.seen .lp-eyebrow::after, .lp-sec.cine .lp-eyebrow::after { --e: 1; }
.sys-comet {
  position: fixed; z-index: 61; pointer-events: none;
  font: 600 15px/1 var(--font); color: var(--bone); white-space: nowrap;
  padding: 9px 15px; border-radius: 999px;
  background: rgb(244 63 94 / .16); border: 1px solid rgb(244 63 94 / .5);
  box-shadow: 0 0 26px rgb(244 63 94 / .55), 0 0 90px rgb(244 63 94 / .25);
  transition: transform 820ms cubic-bezier(.5,0,.2,1), opacity 300ms linear;
  will-change: transform, opacity;
}
.sys-core {
  position: fixed; left: 50%; top: 52%; width: 128px; height: 128px; margin: -64px 0 0 -64px;
  z-index: 62; pointer-events: none; opacity: 0;
  background: radial-gradient(circle at 50% 45%, var(--arterial) 0%, var(--arterial-dp) 55%, transparent 72%);
  border-radius: 50%; filter: blur(2px);
  transition: opacity 420ms linear, transform 300ms cubic-bezier(.34,1.7,.5,1);
}
.sys-core.swell { opacity: 1; transform: scale(1.18); }
.sys-core.sealed {
  background: radial-gradient(circle at 50% 45%, var(--emerald) 0%, #0d5a30 55%, transparent 72%);
  transition: background 700ms ease, transform 700ms ease, opacity 500ms linear;
}
.sys-ring {
  position: fixed; left: 50%; top: 52%; width: 46px; height: 46px; margin: -23px 0 0 -23px;
  border-radius: 50%; border: 3px solid var(--bone); z-index: 63; pointer-events: none;
  animation: sysRing 820ms cubic-bezier(.12,.85,.25,1) forwards;
}
.sys-ring.b { border-width: 1px; border-color: var(--brass-lit); animation-delay: 90ms; }
@keyframes sysRing { from { transform: scale(1); opacity: .95; } to { transform: scale(56); opacity: 0; } }
.sys-wash {
  position: fixed; inset: 0; z-index: 59; pointer-events: none; opacity: 0;
  background: radial-gradient(62% 62% at 50% 52%, var(--arterial-dp), #3A0A1C 44%, transparent 78%);
  animation: sysWash 640ms ease-out forwards;
}
@keyframes sysWash { 0% { opacity: 0; } 14% { opacity: .8; } 100% { opacity: 0; } }
.sys-word {
  position: fixed; left: 0; right: 0; top: 50%; z-index: 64; pointer-events: none; text-align: center;
  font-family: var(--font-display); font-weight: 800; letter-spacing: -.03em; line-height: .92;
  font-size: clamp(3.6rem, 13vw, 10rem);
  background: linear-gradient(96deg, var(--emerald-lt), var(--plasma));
  -webkit-background-clip: text; background-clip: text; color: transparent;
  transform: translateY(-50%) scale(1.55); opacity: 0; filter: blur(22px);
  transition: transform 680ms cubic-bezier(.16,1,.3,1), opacity 500ms ease, filter 620ms ease;
}
.sys-word.in { transform: translateY(-50%) scale(1); opacity: 1; filter: blur(0); }
.sys-word small { display: block; margin-top: 16px; font-family: var(--font); font-weight: 500;
  font-size: clamp(.9rem, 2vw, 1.15rem); letter-spacing: .18em; text-transform: uppercase;
  color: var(--text-dim); -webkit-text-fill-color: var(--text-dim); opacity: 0; transition: opacity 500ms ease .26s; }
.sys-word.in small { opacity: 1; }
/* the page holds its breath just before the beat */
.lp.brownout { filter: saturate(.16) brightness(.72); transition: filter 420ms ease; }
.lp.brownout-off { filter: none; transition: filter 900ms ease; }

@media (prefers-reduced-motion: reduce) {
  .sys-comet, .sys-ring, .sys-wash { display: none !important; }
  .sys-word { opacity: 1; transform: translateY(-50%); filter: none; }
  .lp.brownout { filter: none; }
}

/* ============================================================================
   v84 — THE INTERIOR.  v83 shipped tokens; this is the pass that is actually
   visible.  Verified against the real class inventory (.card / .btn / .mk-* /
   .conf / .page-*), not invented selectors.  Saturated, lit, and moving.
   ============================================================================ */

/* --- THE ROOM ITSELF -------------------------------------------------------
   A flat #0a0c0b field is what made the interior feel like a dashboard.
   Four slow chromatic blooms drift underneath everything now — the surface
   is never the same color twice. */
body::after {
  content: ""; position: fixed; inset: -20%; z-index: -2; pointer-events: none;
  background:
    radial-gradient(38% 42% at 18% 22%, rgb(var(--accent-rgb) / .16), transparent 62%),
    radial-gradient(36% 40% at 82% 18%, rgb(169 221 189 / .14), transparent 62%),
    radial-gradient(42% 44% at 74% 82%, rgb(var(--accent-rgb) / .15), transparent 64%),
    radial-gradient(34% 38% at 24% 84%, rgb(183 247 168 / .11), transparent 62%);
  filter: blur(40px) saturate(1.5);
  animation: roomDrift 34s ease-in-out infinite alternate;
}
@keyframes roomDrift {
  0%   { transform: translate3d(-3%, -2%, 0) scale(1);    }
  50%  { transform: translate3d( 3%,  2%, 0) scale(1.14); }
  100% { transform: translate3d(-2%,  3%, 0) scale(1.06); }
}

/* --- SURFACES: glass with a lit edge --------------------------------------- */
.card, .set-group, .twin-list, .conn-card {
  position: relative;
  background:
    linear-gradient(158deg, rgb(255 255 255 / .055), rgb(255 255 255 / .012) 42%, transparent 70%),
    linear-gradient(180deg, #131A17, #0D1210);
  border: 1px solid rgb(255 255 255 / .085);
  box-shadow: 0 1px 0 0 rgb(255 255 255 / .07) inset, 0 18px 44px -18px rgb(0 0 0 / .8);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: transform .32s var(--ease), box-shadow .32s var(--ease), border-color .32s var(--ease);
}
/* a hairline of live color along the top edge of every surface */
.card::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px; border-radius: inherit;
  background: linear-gradient(90deg, transparent, var(--green-deep), var(--green), var(--green-3), transparent);
  opacity: .5; transition: opacity .32s var(--ease);
}
.card:hover {
  transform: translateY(-3px);
  border-color: rgb(var(--accent-rgb) / .3);
  box-shadow: 0 1px 0 0 rgb(255 255 255 / .1) inset, 0 26px 60px -20px rgb(0 0 0 / .9), 0 0 44px -12px rgb(var(--accent-rgb) / .3);
}
.card:hover::before { opacity: 1; }

/* --- TYPE THAT CARRIES COLOR ---------------------------------------------- */
.page-title {
  background: linear-gradient(96deg, var(--bone) 0%, var(--green-3) 46%, var(--green-deep) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  letter-spacing: -.025em;
}
.page-eyebrow { color: var(--green-2); letter-spacing: .22em; text-transform: uppercase; font-size: .68rem; font-weight: 700; }

/* --- THE BIG NUMBERS ------------------------------------------------------- */
.mk-val {
  font-family: var(--font-display); font-weight: 800; font-size: 2.05rem; letter-spacing: -.02em;
  background: linear-gradient(180deg, var(--bone), #9FB3A6);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.mk-val .tf-unit { -webkit-text-fill-color: var(--text-faint); font-size: .78rem; font-weight: 600; font-family: var(--font); }
.mk-card { overflow: hidden; }
/* status owns the card, not just a pill */
.mk-card:has(.conf.low)  { border-color: rgb(244 63 94 / .34); }
.mk-card:has(.conf.low)::before  { background: linear-gradient(90deg, transparent, var(--arterial), transparent); opacity: .95; }
.mk-card:has(.conf.low) .mk-val  { background: linear-gradient(180deg, #FFE4E9, var(--arterial)); -webkit-background-clip: text; background-clip: text; }
.mk-card:has(.conf.high)::before { background: linear-gradient(90deg, transparent, var(--emerald), transparent); opacity: .8; }
.mk-card:has(.conf.high) .mk-val { background: linear-gradient(180deg, var(--emerald-lt), var(--emerald)); -webkit-background-clip: text; background-clip: text; }

/* the range pill reads as a status light */
.conf { border-radius: 999px; padding: 3px 10px; font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.conf.low  { color: var(--arterial); background: rgb(244 63 94 / .14); border: 1px solid rgb(244 63 94 / .45); box-shadow: 0 0 16px -3px rgb(244 63 94 / .55); }
.conf.high { color: var(--emerald); background: rgb(74 222 128 / .13); border: 1px solid rgb(74 222 128 / .42); box-shadow: 0 0 16px -3px rgb(74 222 128 / .45); }

/* --- BUTTONS THAT LOOK POWERED -------------------------------------------- */
.btn.primary {
  /* Accent tokens, not the fixed v83 palette. This rule is the last .btn.primary background in
     the file, so it won — and it was pinned to --emerald-lt/--emerald/--teal, which meant the
     six-way accent picker in Settings had NO effect on the app's main call-to-action. Measured:
     the computed fill was byte-identical across all six accents. Dark label holds on every
     accent (worst is clay's mid band at 4.12:1; the rest are 7.5-17:1). */
  background: linear-gradient(135deg, var(--green-3) 0%, var(--green) 38%, var(--green-2) 100%);
  color: #04130a; font-weight: 700; border: none;
  box-shadow: 0 8px 26px -8px rgb(var(--accent-rgb) / .75), 0 0 0 1px rgb(var(--accent-rgb) / .35) inset;
  transition: transform .2s var(--ease), box-shadow .28s var(--ease), filter .28s var(--ease);
}
.btn.primary:hover { transform: translateY(-2px); filter: saturate(1.25) brightness(1.06); box-shadow: 0 14px 40px -8px rgb(var(--accent-rgb) / .9), 0 0 0 1px rgb(var(--accent-rgb) / .5) inset; }
.btn.primary:active { transform: translateY(0); }
.btn.ghost, .btn.outline { border: 1px solid rgb(255 255 255 / .13); background: rgb(255 255 255 / .035); transition: border-color .25s, background .25s, transform .2s; }
.btn.ghost:hover, .btn.outline:hover { border-color: rgb(var(--accent-rgb) / .5); background: rgb(var(--accent-rgb) / .09); transform: translateY(-1px); }

/* --- NAV: the active item is lit ------------------------------------------ */
.nav-item.active, .tab-item.active {
  background: linear-gradient(96deg, rgb(var(--accent-rgb) / .16), rgb(var(--accent-rgb) / .07));
  box-shadow: inset 2px 0 0 0 var(--green), 0 0 26px -10px rgb(var(--accent-rgb) / .8);
  /* --text, NOT --bone. --bone is a fixed near-white from the v83 palette and is never
     redefined for light mode, so the active nav/tab label rendered at 1.01:1 on the cream
     sidebar — whichever page you were on, its own label was invisible. --text flips with the
     theme and is visually identical to --bone in dark (#e8ece8 vs #F5F3EF). */
  color: var(--text);
}
.nav-item, .tab-item { transition: background .25s var(--ease), color .25s var(--ease), transform .2s var(--ease); }
.nav-item:hover { transform: translateX(2px); }

/* --- SPARKLINES SAY WHAT THEY MEAN ---------------------------------------- */
.mk-card:has(.conf.low) .mk-spark polyline, .mk-card:has(.conf.low) svg polyline {
  stroke: var(--arterial) !important; filter: drop-shadow(0 0 6px rgb(244 63 94 / .6)); }
.mk-card:has(.conf.high) svg polyline { filter: drop-shadow(0 0 6px rgb(74 222 128 / .5)); }

/* --- HALE THINKING: insight has a color ----------------------------------- */
.typing span, .typing i, .typing b {
  background: var(--green) !important; box-shadow: 0 0 12px rgb(var(--accent-rgb) / .8);
  animation: thinkPulse 1.1s ease-in-out infinite; }
.typing span:nth-child(2), .typing i:nth-child(2) { animation-delay: .16s; }
.typing span:nth-child(3), .typing i:nth-child(3) { animation-delay: .32s; }
@keyframes thinkPulse { 0%,100% { opacity: .35; transform: scale(.8); } 50% { opacity: 1; transform: scale(1.25); } }

/* --- DAILY TILES: energy ---------------------------------------------------- */
.dbox { transition: transform .28s var(--ease), box-shadow .28s var(--ease); }
.dbox:hover { transform: translateY(-2px) scale(1.015); }
.track > i, .track span { background: linear-gradient(90deg, var(--green-deep), var(--green)) !important; box-shadow: 0 0 14px -2px rgb(var(--accent-rgb) / .7); }

@media (prefers-reduced-motion: reduce) {
  body::after { animation: none; }
  .card:hover, .dbox:hover, .btn.primary:hover, .nav-item:hover { transform: none; }
}

/* ============================================================================
   v85 — THE DIAL.  The tour was four scenes playing on a 580px postage stamp
   inside an empty 1140px column, cross-fading every 600ms.  It is now a
   machined instrument: a brass-bezelled aperture filling the screen, rotating
   in detents, cutting with a shutter, and re-tinting the entire page on every
   quarter turn.
   ============================================================================ */
.lp-journey { height: 460vh; }
.jn-stick { overflow: hidden; }

/* the whole page takes the color of the beat — four times in twenty seconds */
.jn-stick::after {
  content: ""; position: absolute; inset: -30%; z-index: 0; pointer-events: none;
  background: radial-gradient(closest-side, rgb(var(--beat-rgb, 134 239 172) / .30), transparent 70%);
  filter: blur(90px); transition: background 900ms linear;
}
.jn-b0 { --beat-rgb: 134 239 172; }    /* Measure — cyan    */
.jn-b1 { --beat-rgb: 183 247 168; }    /* Ask     — plasma  */
.jn-b2 { --beat-rgb: 169 221 189; }   /* Model   — violet  */
.jn-b3 { --beat-rgb: 74 222 128; }    /* Act     — emerald */

/* the stage stops being a two-column grid and becomes one centred instrument */
.jn-stagewrap { display: block !important; position: relative; z-index: 1; max-width: none !important; }
.jn-stage { perspective: none; display: grid; place-items: center; }

/* THE BEZEL — ticks are two conic gradients, so 60 markers cost zero DOM */
.jn-cam {
  position: relative; width: min(84vh, 92vw); height: min(84vh, 92vw);
  border-radius: 50%; display: grid; place-items: center;
  transform: none !important; transition: none !important;
  background:
    repeating-conic-gradient(from 0deg, #3ECF8E 0 .40deg, transparent .40deg 6deg),
    repeating-conic-gradient(from 0deg, #B7F7A8 0 1.2deg, transparent 1.2deg 90deg),
    conic-gradient(from 210deg, #14432A, #3ECF8E 18%, #EAFFF1 27%, #3ECF8E 38%, #0E2E1C 62%, #3ECF8E 84%, #14432A);
  box-shadow: 0 0 0 1px rgb(201 162 39 / .5), 0 30px 90px -30px rgb(0 0 0 / .95), 0 0 120px -40px rgb(var(--beat-rgb) / .55);
  rotate: var(--dial, 0deg);
}
/* the moving highlight is a separate counter-rotating layer — pure compositing */
.jn-cam::before {
  content: ""; position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  background: conic-gradient(from 0deg, transparent, #EAFFF1 7%, transparent 21%);
  mix-blend-mode: screen; opacity: .5; rotate: calc(var(--dial, 0deg) * -1);
}
/* the machined recess the scenes sit in */
.jn-cam::after {
  content: ""; position: absolute; inset: 7%; border-radius: 50%; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .1), inset 0 26px 70px rgb(0 0 0 / .92), 0 0 0 6px #0A0E14;
}

/* the caption leaves the column and overlays the instrument */
.jn-cp {
  position: absolute; left: 4vw; bottom: 6vh; z-index: 3; max-width: 380px;
  text-shadow: 0 2px 20px rgb(0 0 0 / .9);
}
.jn-cp h3 { font-size: clamp(2.2rem, 5.2vw, 4rem) !important; letter-spacing: -.03em; line-height: 1;
  background: linear-gradient(96deg, var(--bone), rgb(var(--beat-rgb))); -webkit-background-clip: text; background-clip: text; color: transparent; }

/* the rail becomes four engraved markers on the rim */
.jn-rail { position: absolute; z-index: 4; bottom: 4vh; left: 50%; transform: translateX(-50%); }
.jn-dot { border-color: rgb(201 162 39 / .45) !important; letter-spacing: .16em; text-transform: uppercase; font-size: .64rem; }
.jn-stick.jn-b0 .jn-dot:nth-child(1), .jn-stick.jn-b1 .jn-dot:nth-child(2),
.jn-stick.jn-b2 .jn-dot:nth-child(3), .jn-stick.jn-b3 .jn-dot:nth-child(4) {
  color: rgb(var(--beat-rgb)); border-color: rgb(var(--beat-rgb) / .8) !important;
  box-shadow: 0 0 22px -6px rgb(var(--beat-rgb) / .9);
}
@media (max-width: 860px) {
  .jn-cam { width: min(88vw, 72vh); height: min(88vw, 72vh); }
  .jn-cp { left: 5vw; right: 5vw; bottom: 12vh; max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .jn-cam { rotate: 0deg !important; }
  .jn-cam::before { display: none; } .jn-stick::after { transition: none; }
}
/* THE WELL — a real dark recess the scene sits inside */
.jn-cam::after {
  inset: 8.5% !important; border-radius: 50%;
  background: radial-gradient(circle at 50% 34%, #10171B 0%, #070B0E 62%, #04070A 100%) !important;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .07), inset 0 30px 80px rgb(0 0 0 / .95),
              inset 0 -10px 40px rgb(var(--beat-rgb) / .16), 0 0 0 5px #05080B !important;
  z-index: 1;
}
.jn-cam::before { z-index: 2; opacity: .38; }

/* the copy leaves the dial entirely and sits bottom-left, out of the way */
.jn-copy {
  position: absolute; left: max(24px, 4vw); bottom: 8vh; z-index: 5;
  width: min(360px, 40vw); text-align: left; pointer-events: none;
}
.jn-copy h3 { font-size: clamp(1.5rem, 2.6vw, 2.3rem) !important; line-height: 1.05; margin: 0 0 8px; letter-spacing: -.02em;
  background: linear-gradient(96deg, var(--bone), rgb(var(--beat-rgb))); -webkit-background-clip: text; background-clip: text; color: transparent; }
.jn-copy p { font-size: .92rem !important; line-height: 1.5; color: var(--text-dim); text-shadow: 0 2px 18px #000; }
@media (max-width: 900px) {
  .jn-copy { position: static; width: auto; margin: 18px auto 0; text-align: center; max-width: 560px; }
  .jn-cam { width: min(86vw, 56vh); height: min(86vw, 56vh); }
}

/* v85.2 — keep the scene inside the well, and let the aperture clip it */
.jn-cam { overflow: hidden; }

/* ============================================================================
   v86 — THE CORRIDOR.  The camera stops watching sections arrive and starts
   MOVING.  Scroll drives a dolly down a tunnel built out of the hero's own
   EKG waveform; rings rush past the lens and an iris opens onto the dial.
   ============================================================================ */
.lp-corridor { height: 320vh; position: relative; margin: 0 0 -6vh; }
.cor-stick { position: sticky; top: 0; height: 100vh; height: 100dvh; overflow: hidden;
  perspective: 900px; perspective-origin: 50% 50%;
  background: radial-gradient(closest-side, #071318, #04070A 70%); }

.cor-world { position: absolute; inset: 0; transform-style: preserve-3d;
  transform: translateZ(calc(var(--cz, 0) * 1px)); }

/* each ring is the waveform, hung at its own depth.  --i is its index; the
   camera travels 480px per ring, so they arrive one after another. */
.cor-ring { position: absolute; left: 50%; top: 50%; width: 190vw; height: 46vh;
  margin: -23vh 0 0 -95vw; transform-style: preserve-3d;
  transform: translateZ(calc(var(--i) * -480px)) rotateX(58deg);
  opacity: calc(.14 + var(--i) * .045); }
.cor-ring svg { width: 100%; height: 100%; overflow: visible; }
.cor-ring polyline { fill: none; stroke-width: 2.4; vector-effect: non-scaling-stroke;
  stroke: rgb(134 239 172 / .8); filter: drop-shadow(0 0 10px rgb(134 239 172 / .55)); }
/* alternate rings mirror below the camera so you fly BETWEEN two walls */
.cor-ring:nth-child(even) { transform: translateZ(calc(var(--i) * -480px)) rotateX(-58deg); }
.cor-ring:nth-child(even) polyline { stroke: rgb(74 222 128 / .75); filter: drop-shadow(0 0 10px rgb(74 222 128 / .5)); }

/* real metrics streaming past the lens at different depths and offsets */
.cor-chip { position: absolute; left: 50%; top: 50%; transform-style: preserve-3d;
  font: 700 15px/1 var(--font); letter-spacing: .02em; white-space: nowrap;
  color: var(--bone); padding: 8px 14px; border-radius: 999px;
  background: rgb(255 255 255 / .06); border: 1px solid rgb(255 255 255 / .18);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  transform:
    translate3d(calc((var(--i) - 3.5) * 19vw), calc(sin(var(--i) * 1.7rad) * 26vh), calc(var(--i) * -760px - 400px));
  opacity: .9; }

/* the iris — closed while you travel, opening onto the dial at the end */
.cor-iris { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.cor-iris span { position: absolute; left: 0; right: 0; height: 52%; background: #04070A;
  box-shadow: 0 0 60px 20px #04070A; transition: transform 120ms linear; }
.cor-iris span:first-child { top: 0;    transform: translateY(calc(var(--iris, 1) * -100%)); }
.cor-iris span:last-child  { bottom: 0; transform: translateY(calc(var(--iris, 1) * 100%)); }

.cor-cap { position: absolute; left: 0; right: 0; bottom: 12vh; z-index: 4; text-align: center;
  opacity: var(--capo, 0); transition: opacity 240ms linear; }
.cor-cap b { display: block; font-family: var(--font-display); font-weight: 800;
  font-size: clamp(2rem, 6vw, 4.6rem); letter-spacing: -.03em; line-height: 1;
  background: linear-gradient(96deg, var(--cyan), var(--emerald-lt), var(--plasma));
  -webkit-background-clip: text; background-clip: text; color: transparent; }
.cor-cap i { display: block; margin-top: 10px; font-style: normal; letter-spacing: .28em;
  text-transform: uppercase; font-size: .74rem; color: var(--text-dim); }

@media (prefers-reduced-motion: reduce) {
  .lp-corridor { height: 100vh; }
  .cor-world { transform: none; }
  .cor-ring { display: none; } .cor-ring:nth-child(-n+3) { display: block; }
  .cor-iris span { transform: none !important; }
  .cor-cap { --capo: 1; }
}

/* ============================================================================
   v87 — THE INSTRUMENT IN HALE'S OWN COLORS, and the scene inside it finally
   performs instead of sitting there.
   ============================================================================ */

/* --- the bezel is anodized teal/cyan now, not brass ----------------------- */
.jn-cam {
  background:
    repeating-conic-gradient(from 0deg, rgb(134 239 172 / .5) 0 .35deg, transparent .35deg 6deg),
    repeating-conic-gradient(from 0deg, rgb(224 255 250 / .85) 0 1deg, transparent 1deg 90deg),
    conic-gradient(from 210deg,
      #06231A, #15803D 12%, #86EFAC 20%, #EAFFF1 24%, #3ECF8E 29%,
      #0B5536 44%, #051A12 58%, #15803D 78%, #4ADE80 86%, #06231A 100%) !important;
  box-shadow:
    0 0 0 1px rgb(62 207 142 / .55),
    0 30px 90px -30px rgb(0 0 0 / .95),
    0 0 140px -35px rgb(var(--beat-rgb) / .75) !important;
}
.jn-cam::before { background: conic-gradient(from 0deg, transparent, #EAFFF1 7%, transparent 21%) !important; opacity: .42; }
.jn-cam::after { box-shadow: inset 0 0 0 1px rgb(62 207 142 / .18), inset 0 30px 80px rgb(0 0 0 / .95),
                 inset 0 -12px 46px rgb(var(--beat-rgb) / .26), 0 0 0 5px #04090C !important; }
.jn-dot { border-color: rgb(62 207 142 / .4) !important; }

/* --- THE SCENE PERFORMS ---------------------------------------------------
   Previously the four scenes cross-faded and then sat still. Now each one
   lands with a push and BUILDS itself: lines draw, dots pop, rows stagger. */
.jn-stick .jn-scene { --sd: 0ms; }
.jn-b0 .s0, .jn-b1 .s1, .jn-b2 .s2, .jn-b3 .s3 { animation: sceneLand 720ms cubic-bezier(.16,1,.3,1) both; }
@keyframes sceneLand {
  from { opacity: 0; transform: perspective(1200px) rotateX(14deg) scale(1.12) translateY(26px); filter: blur(10px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
@keyframes drawStroke { to { stroke-dashoffset: 0; } }
/* data points pop in behind the line as it passes them */
.jn-b0 .s0 circle, .jn-b1 .s1 circle, .jn-b2 .s2 circle, .jn-b3 .s3 circle {
  animation: dotPop 520ms cubic-bezier(.34,1.7,.5,1) both;
  animation-delay: calc(380ms + var(--dn, 0) * 190ms); transform-box: fill-box; transform-origin: 50% 50%;
}
.jn-b0 .s0 circle:nth-of-type(2), .jn-b1 .s1 circle:nth-of-type(2), .jn-b2 .s2 circle:nth-of-type(2), .jn-b3 .s3 circle:nth-of-type(2) { --dn: 1; }
.jn-b0 .s0 circle:nth-of-type(3), .jn-b1 .s1 circle:nth-of-type(3), .jn-b2 .s2 circle:nth-of-type(3), .jn-b3 .s3 circle:nth-of-type(3) { --dn: 2; }
.jn-b0 .s0 circle:nth-of-type(4), .jn-b1 .s1 circle:nth-of-type(4), .jn-b2 .s2 circle:nth-of-type(4), .jn-b3 .s3 circle:nth-of-type(4) { --dn: 3; }
@keyframes dotPop { from { opacity: 0; transform: scale(0); } to { opacity: 1; transform: scale(1); } }
/* chips, pills and rows stagger in rather than appearing all at once */
.jn-b0 .s0 g, .jn-b1 .s1 g, .jn-b2 .s2 g, .jn-b3 .s3 g {
  animation: rowIn 520ms cubic-bezier(.16,1,.3,1) both; animation-delay: calc(300ms + var(--gn, 0) * 90ms); }
.jn-scene g:nth-of-type(2) { --gn: 1; } .jn-scene g:nth-of-type(3) { --gn: 2; }
.jn-scene g:nth-of-type(4) { --gn: 3; } .jn-scene g:nth-of-type(5) { --gn: 4; }
@keyframes rowIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .jn-b0 .s0, .jn-b1 .s1, .jn-b2 .s2, .jn-b3 .s3,
  .jn-scene polyline, .jn-scene path, .jn-scene circle, .jn-scene g { animation: none !important;
    stroke-dashoffset: 0 !important; opacity: 1 !important; transform: none !important; }
}

/* v87.1 — the mock window must sit fully inside the well, not overflow it */
#jnTitle { font-size: .92rem !important; font-weight: 650 !important; letter-spacing: -.01em !important;
  -webkit-text-fill-color: var(--text) !important; background: none !important; white-space: nowrap; }

/* ============================================================================
   v88 — THE FACE.  The tour no longer shows screenshots of a fake app window
   inside the instrument.  The data is drawn ON the instrument: polar, frameless,
   engraved.  Also trims the corridor's cost.
   ============================================================================ */
.jn-face { position: relative; z-index: 3; width: 74%; aspect-ratio: 1; display: grid; place-items: center;
  rotate: calc(var(--dial, 0deg) * -1); }
.jn-face .jn-scene { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0; pointer-events: none; }
.jn-b0 .jn-face .s0, .jn-b1 .jn-face .s1, .jn-b2 .jn-face .s2, .jn-b3 .jn-face .s3 { opacity: 1; }
.fx { width: 100%; height: 100%; overflow: visible; }
.fx text { text-anchor: middle; font-family: var(--font); fill: var(--text-dim); }
@keyframes fxArc { to { stroke-dashoffset: 500; } }
@keyframes fxSwing { from { rotate: -128deg; } to { rotate: 62deg; } }
@keyframes fxOrbit { to { rotate: 360deg; } }
@keyframes fxPulse { 0% { opacity: 0; scale: .55; } 25% { opacity: .9; } 100% { opacity: 0; scale: 1.25; } }
@keyframes fxDraw { to { stroke-dashoffset: 0; } }
@keyframes fxDashIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fxSeg { to { stroke-dasharray: 224 942; } }

/* the beat name, engraved on the face instead of a fake window title */
.jn-legend { position: absolute; bottom: 4%; left: 0; right: 0; text-align: center; z-index: 4;
  font-size: .7rem; font-weight: 800; letter-spacing: .34em; text-transform: uppercase;
  color: rgb(var(--beat-rgb)); text-shadow: 0 0 18px rgb(var(--beat-rgb) / .6); }          /* 16 rings -> 10 */
.cor-ring polyline { filter: none !important; }        /* drop-shadow per ring was the killer */
.cor-world, .cor-ring { will-change: auto; }
.cor-stick { contain: layout paint; }

/* v88.1 — the caption row collided with the engraved beat name, and the rail
   below the dial already names the beat, so the legend is redundant. */
.jn-legend { display: none; }
@keyframes fxRim { from { opacity: 0; rotate: -14deg; } to { opacity: 1; rotate: 0deg; } }
.fx path { fill: none; stroke-linecap: round; }
@keyframes fxLine { to { stroke-dashoffset: 0; } }
@keyframes fxPop { from { opacity: 0; scale: 0; } to { opacity: .55; scale: 1; } }
@keyframes fxRide { from { rotate: 20deg; } to { rotate: var(--to); } }
@keyframes fxWave { 0% { opacity: 0; scale: .42; } 18% { opacity: .85; } 100% { opacity: 0; scale: 1.08; } }
@keyframes fxFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes fxText { from { opacity: 0; transform: translateY(9px); } to { opacity: 1; transform: none; } }
@keyframes fxBig  { from { opacity: 0; transform: scale(.62); filter: blur(9px); } to { opacity: 1; transform: none; filter: blur(0); } }

@media (prefers-reduced-motion: reduce) {
  .fx *, .jn-face .jn-scene text { animation: none !important; stroke-dashoffset: 0 !important;
    opacity: 1 !important; transform: none !important; rotate: 0deg !important; }
}

/* ============================================================================
   v90 — SAY WHAT IT MEANS.  The faces were beautiful and mute: an ad has to
   explain.  The small print comes off the artwork (it was unreadable sitting
   on the rings), and a colour key next to the dial names every element.
   ============================================================================ */

/* two real columns again: the instrument on the right, words on the left */
.jn-stagewrap { display: grid !important; grid-template-columns: minmax(300px, 30vw) 1fr;
  gap: clamp(20px, 4vw, 64px); align-items: center; padding-inline: clamp(16px, 4vw, 60px); }
.jn-stage { order: 2; }
.jn-cam { width: min(64vh, 46vw); height: min(64vh, 46vw); }
.jn-face { width: 78%; }

/* the copy is static, off the artwork, on a legible surface */
.jn-copy { position: static !important; order: 1; width: auto !important; text-align: left; pointer-events: auto; }
.jn-cp { position: absolute; opacity: 0; transition: opacity 280ms ease, transform 380ms var(--ease);
  transform: translateY(10px); pointer-events: none; max-width: 30vw; }
.jn-b0 .c0, .jn-b1 .c1, .jn-b2 .c2, .jn-b3 .c3 { position: relative; opacity: 1; transform: none; pointer-events: auto; }
.jn-cp h3 { font-size: clamp(1.6rem, 2.5vw, 2.5rem) !important; margin: 6px 0 10px; }
.jn-cp p { font-size: clamp(.86rem, 1.05vw, .98rem) !important; line-height: 1.55; color: var(--text-dim); }

/* THE KEY — the part that makes it an advertisement instead of a mood piece */
.jn-key { list-style: none; margin: 18px 0 0; padding: 14px 16px; display: grid; gap: 11px;
  border-radius: 14px; border: 1px solid rgb(255 255 255 / .1);
  background: linear-gradient(160deg, rgb(255 255 255 / .05), rgb(255 255 255 / .015));
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.jn-key li { display: grid; grid-template-columns: 12px 1fr; gap: 10px; align-items: start;
  font-size: .82rem; line-height: 1.35;
  animation: keyIn 480ms cubic-bezier(.16,1,.3,1) both; }
.jn-key li:nth-child(2) { animation-delay: .1s; } .jn-key li:nth-child(3) { animation-delay: .2s; }
@keyframes keyIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }
.jn-key i { width: 12px; height: 12px; border-radius: 4px; margin-top: 3px;
  box-shadow: 0 0 12px -2px currentColor; }
.jn-key b { display: block; color: var(--text); font-weight: 600; }
.jn-key span { display: block; color: rgb(var(--beat-rgb)); font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: .01em; }

@media (max-width: 1000px) {
  .jn-stagewrap { grid-template-columns: 1fr !important; }
  .jn-stage { order: 1; } .jn-copy { order: 2; }
  .jn-cam { width: min(52vh, 84vw); height: min(52vh, 84vw); }
  .jn-cp { max-width: none; text-align: center; }
  .jn-key { text-align: left; max-width: 460px; margin-inline: auto; }
}

/* v90.1 — the key rows wrapped ("81 / mg/dL" on two lines) and the stacked
   layout overflowed the sticky viewport.  Compact rows, tighter stack. */
.jn-key { padding: 12px 14px; gap: 9px; }
/* The value track was `auto` while its content was `white-space: nowrap`, so its min-content
   equalled its max-content and it could never give ground; the label track (minmax(0,1fr))
   absorbed the whole shortfall. Most values are short numbers, but MODEL's third row is a
   sentence — at ~935px it crushed the label "How sure it is" to 0px wide, wrapping it over four
   lines beside a single-line value. A floor on the label and a shrinkable value track fix it. */
.jn-key li { grid-template-columns: 11px minmax(7.5ch,1fr) minmax(0,auto); align-items: center; gap: 9px; font-size: .8rem; }
.jn-key i { width: 11px; height: 11px; margin-top: 0; }
.jn-key b { font-size: .8rem; font-weight: 550; line-height: 1.25; }
/* nowrap only where it earns its keep — a measurement like "81 mg/dL" must not split across
   lines, but a sentence-length value has to be allowed to wrap or it starves the label. */
.jn-key span { text-align: right; white-space: normal; text-wrap: balance; font-size: .78rem; }

/* two columns wherever there's room at all */
@media (min-width: 900px) {
  .jn-stagewrap { grid-template-columns: minmax(280px, 34%) 1fr !important; }
  .jn-stage { order: 2; } .jn-copy { order: 1; }
  .jn-cam { width: min(62vh, 42vw); height: min(62vh, 42vw); }
}
/* stacked: everything must still fit one screen */
@media (max-width: 899px) {
  .jn-cam { width: min(40vh, 76vw); height: min(40vh, 76vw); }
  .jn-cp h3 { font-size: clamp(1.25rem, 4.6vw, 1.7rem) !important; margin: 4px 0 6px; }
  .jn-cp p { font-size: .84rem !important; line-height: 1.45;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
  .jn-key { margin-top: 12px; }
}
/* v90.2 — the step eyebrow was sitting on the bottom of the dial */
.jn-cp .lp-eyebrow { display: inline-block; margin-bottom: 4px; }
@media (max-width: 899px) { .jn-stagewrap { gap: 26px; } .jn-copy { padding-top: 6px; } }

/* ============================================================================
   v91 — no gold anywhere, ACT's open pillar stops reading as a glitch, and the
   corridor gets dense enough to feel like somewhere you are.
   ============================================================================ */

/* --- 1. THE YELLOW IS GONE ------------------------------------------------ */
/* ASK's beat moves from gold to teal, so the four beats run cyan -> teal ->
   violet -> emerald: one cool family, no warm outlier. */
.jn-b1 { --beat-rgb: 62 207 142; }
/* the corridor headline was cyan -> mint -> GOLD.
   background-image, NOT the `background` shorthand: the shorthand resets every background
   longhand to its initial value, which includes background-clip -> border-box. This headline is
   painted with background-clip:text + color:transparent, so the shorthand (made !important, so
   it beat the base rule) turned "Every number you own" into a solid 863x56 gradient BAR with
   completely invisible letters from v91 until v97. */
.cor-cap b { background-image: linear-gradient(96deg, var(--cyan), var(--emerald-lt), var(--teal)) !important; }
/* card hairline, ambient blooms, thinking dots and rim ticks all drop plasma */
.card::before { background: linear-gradient(90deg, transparent, var(--green-deep), var(--green), var(--green-2), transparent); }
body::after {
  background:
    radial-gradient(38% 42% at 18% 22%, rgb(var(--accent-rgb) / .17), transparent 62%),
    radial-gradient(36% 40% at 82% 18%, rgb(169 221 189 / .13), transparent 62%),
    radial-gradient(42% 44% at 74% 82%, rgb(var(--accent-rgb) / .16), transparent 64%),
    radial-gradient(34% 38% at 24% 84%, rgb(var(--accent-rgb) / .13), transparent 62%) !important;
}
.typing span, .typing i, .typing b { background: var(--green) !important; box-shadow: 0 0 12px rgb(var(--accent-rgb) / .8); }
@keyframes fxOpen { to { opacity: 1; } }

/* --- 3. THE CORRIDOR, DENSER --------------------------------------------- */
.cor-ring:nth-child(n+11) { display: block; }     /* all 16 back in play */
.cor-ring { width: 240vw; margin-left: -120vw; height: 54vh; margin-top: -27vh;
  opacity: calc(.20 + var(--i) * .05); }
.cor-ring:nth-child(odd) polyline  { stroke: rgb(134 239 172 / .95); }
.cor-ring:nth-child(even) polyline { stroke: rgb(74 222 128 / .9); }
.cor-ring:nth-child(3n) polyline   { stroke: rgb(62 207 142 / .95); }
.cor-ring:nth-child(5n) polyline   { stroke: rgb(169 221 189 / .8); }
/* volumetric haze so the tunnel has air in it, and a vignette so it has walls */
.cor-stick::before {
  content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: radial-gradient(closest-side, rgb(134 239 172 / .13), rgb(6 20 26 / .5) 58%, #02050A 100%); }
.cor-stick::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(38% 38% at 50% 50%, rgb(62 207 142 / .3), transparent 70%);
  filter: blur(60px); }
.cor-chip { font-size: 17px; background: rgb(6 24 30 / .72); border-color: rgb(62 207 142 / .45);
  box-shadow: 0 0 26px -6px rgb(62 207 142 / .6); backdrop-filter: none; -webkit-backdrop-filter: none; }
.cor-cap { z-index: 5; }
/* v91.1 — the vignette was smothering the tunnel instead of giving it air */
.cor-stick::before { background: radial-gradient(closest-side, transparent 40%, rgb(2 6 10 / .55) 92%) !important; }
.cor-stick::after { background: radial-gradient(30% 30% at 50% 50%, rgb(62 207 142 / .38), transparent 72%) !important; filter: blur(70px) !important; }
.cor-ring { opacity: calc(.4 + var(--i) * .04) !important; }
.cor-ring polyline { stroke-width: 3.4 !important; }
.cor-chip { background: rgb(8 30 36 / .85) !important; border-color: rgb(62 207 142 / .7) !important;
  box-shadow: 0 0 34px -6px rgb(62 207 142 / .85) !important; }

/* ============================================================================
   v94 — THE SHUTTER CUT, restored.
   This rule used to live on .jn-window and died with it when the mock-window was
   replaced by the polar faces, leaving app.js adding a `cutting` class that styled
   nothing — and a 180ms pause in the beat change with no animation to fill it.
   Re-scoped to .jn-face, the live element.

   .jn-face already drives the dial counter-rotation through the standalone `rotate`
   property, so the squash uses the standalone `scale` property too — neither one
   clobbers the other the way a combined `transform` would.

   Killing the scene transition WHILE the shutter is shut is what makes this a cut
   instead of a dissolve behind a blind: the swap happens instantly, unseen, and the
   aperture reopens on a finished frame.
   ============================================================================ */
.jn-face {
  transform-origin: 50% 50%;
  transition: opacity 180ms cubic-bezier(.7,0,.84,0), scale 320ms cubic-bezier(.16,1,.3,1);
}
.jn-stick.cutting .jn-face { opacity: 0; scale: 1 .02; }
.jn-stick.cutting .jn-face .jn-scene { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .jn-face { transition: opacity 120ms linear; }
  .jn-stick.cutting .jn-face { scale: 1 1; }     /* fade only — no shutter travel */
}

/* ============================================================================
   v99 — six defects found by auditing the surfaces that had never been rendered:
   the light theme's buttons and modals, the landing under light, and the shipped
   iOS shell. The first entry is a regression this session introduced.
   ============================================================================ */

/* 1. REGRESSION FROM v98. Pointing .btn.primary at the accent tokens was right for dark, but
   html.light deliberately collapses --green/-2/-3 to ONE dark mix (they were darkened so green
   TEXT would read on cream). So in light mode the gradient flattens to a solid dark pill and the
   hardcoded near-black label measured 1.76-3.59:1 across the six accents — all below AA, clay
   effectively invisible. Measured on the same fills, a white label is 5.30-10.81:1. */
html.light .btn.primary { color: #fff; }

/* 2. .modal-err is a dark-mode salmon (#f87171) with no light counterpart — every inline error
   in sign-in, sign-up, password reset, the upgrade modal and the AGE GATE sat at ~2.3:1 on the
   modal's white surface. This is the text that tells you what went wrong. */
html.light .modal-err { color: #b91c1c; }

/* 3. .btn.ghost / .btn.outline draw a pure-white border and a white wash — invisible on cream,
   so every secondary button lost its outline in light mode. */
html.light .btn.ghost, html.light .btn.outline {
  border-color: rgb(0 0 0 / .16); background: rgb(0 0 0 / .022); }

/* 4. The landing is art-directed against a near-black backdrop: its hero headline, its full-bleed
   section backdrops and their headings are all tuned for dark. Under html.light the copy flipped
   to near-black while those backdrops stayed dark, dropping headings to 1.07-1.27:1. Rather than
   re-art-direct the marketing page for a theme it was never designed for, pin it to its intended
   palette — the in-app UI still honours the user's choice. */
html.light body[data-view="landing"] {
  --bg: #0d100e; --bg-2: #141814; --panel-solid: #141814; --elev: #141814; --elev-2: #232823;
  --text: #e8ece8; --text-dim: #a4b0a6; --text-faint: #828e84;
  --hairline: rgba(255,255,255,0.07);
  --green: #4ade80; --green-2: #3ecf8e; --green-3: #86efac;
}

/* 5. THE SHIPPED iOS APP. HaleApp.swift hands the page the full screen (.ignoresSafeArea,
   contentInsetAdjustmentBehavior = .never, "the site owns safe areas") and index.html asks for
   viewport-fit=cover — but env(safe-area-inset-top) appeared nowhere, while the BOTTOM inset was
   claimed twice. The ~65px top bar therefore sat inside the 47-59pt status-bar/Dynamic-Island
   region on every screen: the menu button, model pill and avatar drawn under the clock. */
.topbar { padding-top: calc(12px + env(safe-area-inset-top, 0px)); }
@media (min-width: 769px) { .topbar { padding-top: calc(14px + env(safe-area-inset-top, 0px)); } }

/* 6. The Copy / Read aloud / Edit row on every chat message is opacity:0, revealed only by
   :hover — which no phone has. On the iOS build that leaves a permanently blank 32px strip under
   each message and no way to see the controls. Show them wherever hover doesn't exist. */
@media (hover: none) {
  .msg-actions { opacity: 1; transform: none; }
}

/* ============================================================================
   v105 — ONE consent sheet instead of three documents to hunt through.
   ============================================================================ */
.consent-modal { max-width: 560px; }
.consent-doc {
  text-align: left; max-height: 46vh; overflow-y: auto; margin: 6px 0 14px;
  padding: 16px 18px; border-radius: 14px;
  background: var(--elev); border: 1px solid var(--hairline);
  color: var(--text-dim); font-size: .8125rem; line-height: 1.62;
  overscroll-behavior: contain;                 /* scrolling the terms doesn't drag the page */
}
.consent-doc:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.consent-sec + .consent-sec { margin-top: 20px; padding-top: 18px; border-top: 1px solid var(--hairline); }
.consent-sec h3 {
  margin: 0 0 8px; font-family: var(--font); font-size: .9375rem; font-weight: 700;
  color: var(--text); letter-spacing: -.01em;
}
.consent-sec p { margin: 0 0 9px; }
.consent-sec p:last-child { margin-bottom: 0; }
.consent-sec b { color: var(--text); font-weight: 650; }
.consent-agree { margin: 2px 0 12px; }
/* a soft fade at the foot of the box so it reads as "there is more below" */
.consent-doc { -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 26px), transparent);
               mask-image: linear-gradient(180deg, #000 calc(100% - 26px), transparent); }

/* The #haleFlow gradient paints the EKG line, every sparkline, the twin gauge and the marker
   sparks — all in-app surfaces. Its stops were hardcoded teal-to-green in the markup, so those
   stayed emerald whatever accent you picked. stop-color is a real CSS property, so the stops can
   simply read the accent tokens like everything else. */
#haleFlow .hf-a { stop-color: var(--green-deep); }
#haleFlow .hf-b { stop-color: var(--green-2); }
#haleFlow .hf-c { stop-color: var(--green); }

/* ============================================================================
   v108 — FOUR INSTRUMENTS, ONE BEZEL.

   The tour was four coats of paint on one dial: same 60-tick rim, same r=150
   ring, four colours. The rim alone was 240 of the ~284 SVG nodes in the face,
   and three faces were partly broken on top of that (`.fx path{fill:none}` hid
   MODEL's cone, an !important dashoffset lock hid ACT's fourth pillar, and
   `.fx text{fill:var(--text-dim)}` greyed out every readout).

   Now the rim is ONE persistent piece of chrome that re-shapes per beat, and
   each scene is a different instrument. Two invariants hold everywhere below:

   1. THREE DOTS. The v87 layer animates every `path`, `circle` and `g` inside
      the live scene at specificity (0,2,1). Every static rule here is
      `.jn-face .fx2 …` and every animated one is `.jn-bN .sN …` — both (0,3,0)
      or better — so nothing inherits a 620-unit draw-in by accident.
   2. NEVER `opacity` FOR STATE. The reduced-motion blanket sets
      `opacity: 1 !important` on `.fx *`, so anything that means "not yet" or
      "still open" carries it in the colour's alpha channel instead. Otherwise a
      late reduced-motion toggle makes ACT claim a week that never happened.
   ============================================================================ */

/* The beat colour was the whole disease: four hues doing the work that form
   should do. It is now simply the accent, so all six accents repaint the tour
   — chassis, rail, legend and artwork together. */
.jn-b0, .jn-b1, .jn-b2, .jn-b3 { --beat-rgb: var(--accent-rgb); }

/* -- 0. retire the v87 blanket choreography ---------------------------------
   Same selectors, later in the file, so source order breaks the tie at equal
   specificity. The new scenes choreograph themselves per element. */
.jn-b0 .s0 polyline, .jn-b1 .s1 polyline, .jn-b2 .s2 polyline, .jn-b3 .s3 polyline,
.jn-b0 .s0 path, .jn-b1 .s1 path, .jn-b2 .s2 path, .jn-b3 .s3 path {
  stroke-dasharray: none; stroke-dashoffset: 0; animation: none;
}
.jn-b0 .s0 circle, .jn-b1 .s1 circle, .jn-b2 .s2 circle, .jn-b3 .s3 circle { animation: none; }
.jn-b0 .s0 g, .jn-b1 .s1 g, .jn-b2 .s2 g, .jn-b3 .s3 g { animation: none; }
.jn-face .jn-scene text { animation: none; }
/* camS0..camS3 were authored in v74 for the old fake-app-window scenes and were
   never scoped to the live beat, so four ken-burns loops ran forever on content
   they no longer describe. One drift on the face replaces all four, and chassis
   and instrument now move as one object. */
.jn-scene.s0 svg, .jn-scene.s1 svg, .jn-scene.s2 svg, .jn-scene.s3 svg { animation: none; }
.jn-stagewrap .jn-face { animation: faceDrift 19s cubic-bezier(.45,0,.55,1) infinite; }
@keyframes faceDrift {
  0%, 100% { translate: 0 0; scale: 1; }
  50%      { translate: 0 -1.1%; scale: 1.012; }
}
/* hidden scenes leave the render tree instead of sitting composited at 0 */
.jn-face .jn-scene { content-visibility: hidden; }
.jn-b0 .jn-face .s0, .jn-b1 .jn-face .s1, .jn-b2 .jn-face .s2, .jn-b3 .jn-face .s3 { content-visibility: visible; }

/* -- 1. the chrome: one tick ring that re-shapes per beat -------------------
   This is what makes four instruments read as one range. It sits outside the
   scenes, so it survives the shutter cut — the eye holds onto it while the
   guts are swapped underneath. Two paths, baked coordinates, 4 nodes total,
   replacing 240. */
.jn-face .jn-chrome {
  position: absolute; inset: 0; width: 100%; height: 100%;
  overflow: visible; pointer-events: none; z-index: 1;
}
.jn-face .jn-chrome .jc-min { stroke: rgb(var(--accent-rgb) / .34); stroke-width: 1.3; fill: none; }
.jn-face .jn-chrome .jc-maj { stroke: rgb(var(--accent-rgb) / .8); stroke-width: 2.4; fill: none; }
/* The per-beat morph of the circular tick ring went with the ring itself — the
   chrome is a linear rule on the faceplate now, and the beat is marked by the
   channel bar sliding along it. */

/* -- 2. shared type ladder. Fresh names: the old .fx-big/.fx-unit carry
      blur() keyframes and five !important declarations between them. -------- */
.jn-face .fx2 text { text-anchor: middle; font-family: var(--font); }
/* The bezel's specular sweep crosses the top of the face, so the engraved name
   has to carry more weight than an engraving normally would to stay readable. */
.jn-face .fx2 .i-name {
  font-size: 11px; font-weight: 700; letter-spacing: .34em;
  fill: rgb(var(--accent-rgb) / .8);
}
.jn-face .fx2 .i-unit { font-size: 12.5px; letter-spacing: .04em; fill: var(--text-faint); }
.jn-face .fx2 .i-ans {
  font-family: var(--font-display); font-weight: 800; font-size: 54px;
  letter-spacing: -.03em; fill: rgb(245 243 239);
}
.jn-face .fx2 .i-ans.sm { font-size: 38px; }
.jn-face .fx2 .i-num {
  font-family: var(--font-display); font-weight: 800; font-size: 40px;
  letter-spacing: -.02em; fill: rgb(var(--accent-rgb));
}
.jn-face .fx2 .i-num.sm { font-size: 30px; fill: rgb(245 243 239); }
.jn-face .fx2 .i-lab { font-size: 10.5px; font-weight: 700; letter-spacing: .26em; fill: var(--text-faint); }

/* every scene lands with a push; no blur, so nothing re-rasterises per frame */
.jn-b0 .s0 .fx2, .jn-b1 .s1 .fx2, .jn-b2 .s2 .fx2, .jn-b3 .s3 .fx2 {
  animation: fx2Land 640ms cubic-bezier(.16,1,.3,1) both;
}
@keyframes fx2Land { from { opacity: 0; translate: 0 18px; scale: 1.05; } to { opacity: 1; translate: 0 0; scale: 1; } }

/* ---------------------------------------------------------------- MEASURE --
   A RULE. Horizontal beam, tick comb, three ghost readings and a vernier
   cursor that rides to 81 under your finger. Nothing curved, nothing radial. */
.jn-b0 .s0 { --t: clamp(0, calc(var(--jb, 1) / .55), 1); }
.jn-face .fx2 .i-rule  { stroke: rgb(245 243 239 / .55); stroke-width: 1.6; fill: none; }
/* the comb is two elements, not 51: stroke-width IS the tick height and
   stroke-dasharray IS the pitch (2 mg/dL = 5.6px, 10 mg/dL = 28px) */
.jn-face .fx2 .i-tickm { stroke: rgb(245 243 239 / .3); stroke-width: 8;  fill: none; stroke-dasharray: 1 4.6;  stroke-linecap: butt; }
.jn-face .fx2 .i-tickM { stroke: rgb(245 243 239 / .5); stroke-width: 16; fill: none; stroke-dasharray: 1.6 26.4; stroke-linecap: butt; }
.jn-face .fx2 .i-fig   { font-size: 11px; letter-spacing: .06em; fill: var(--text-faint); }
.jn-face .fx2 .i-band  { fill: rgb(74 222 128 / .16); stroke: rgb(74 222 128 / .4); stroke-width: 1; }
/* start-anchored and hard left, so the cursor body — which always lands inside
   the optimal band — can never sit on top of the band's own label */
.jn-face .fx2 .i-bandlab { font-size: 9.5px; font-weight: 700; letter-spacing: .18em; fill: rgb(74 222 128 / .85); text-anchor: start; }
.jn-face .fx2 .i-ghost path   { stroke: rgb(245 243 239 / .16); stroke-width: 1; fill: none; stroke-dasharray: 2 4; }
.jn-face .fx2 .i-ghost circle { fill: rgb(245 243 239 / .45); }
.jn-face .fx2 .i-slot  { fill: rgb(0 0 0 / .5); stroke: rgb(245 243 239 / .12); stroke-width: 1; }
.jn-face .fx2 .i-cursor .i-slide { stroke: rgb(245 243 239 / .5); stroke-width: 1.4; fill: none; }
.jn-face .fx2 .i-cursor .i-hair  { stroke: rgb(var(--accent-rgb)); stroke-width: 2; fill: none; }
.jn-face .fx2 .i-cursor .i-vern  { stroke: rgb(var(--accent-rgb) / .75); stroke-width: 1.2; fill: none; }
/* The cursor parks clear of the scale and slides onto 81 as you scroll:
   x = 60 + (v-60)*2.8, so 81 lands at 118.8. It parks at 366 rather than at the
   340 end-stop, because sitting exactly on the last graduation reads as a
   reading of 160 rather than as a reading not yet taken. */
.jn-b0 .s0 .i-cursor { translate: calc(366px - 247.2px * var(--t)) 0; }
.jn-b0 .s0 .i-ghost  { translate: 0 calc((1 - var(--t)) * 8px); }
/* the odometer latches like a mechanical counter rather than sliding: three
   clamps, each completing over ~4% of the beat */
.jn-b0 .s0 .i-roll {
  --s1: clamp(0, calc((var(--t) - .26) * 22), 1);
  --s2: clamp(0, calc((var(--t) - .52) * 22), 1);
  --s3: clamp(0, calc((var(--t) - .78) * 22), 1);
  translate: 0 calc(-44px * (var(--s1) + var(--s2) + var(--s3)));
}
/* the one loop, and it means something: a live reading never sits perfectly still */
.jn-b0 .s0 .i-cursor .i-hair { animation: iHair 4.2s cubic-bezier(.45,0,.55,1) infinite; }
@keyframes iHair { 0%, 100% { stroke: rgb(var(--accent-rgb) / .75); } 50% { stroke: rgb(var(--accent-rgb)); } }

/* ------------------------------------------------------------------- ASK --
   THE ANSWER, WRITING ITSELF. The reply is drawn out of the question under
   your finger — the same four readings, falling and settling. A bare diagonal
   with type above and below: no rule, no comb, no frame. */
.jn-b1 .s1 { --t: clamp(0, calc(var(--jb, 1) / .62), 1); }
.jn-face .fx2 .i-q { font-size: 15px; font-style: italic; fill: rgb(245 243 239 / .9); }
.jn-face .fx2 .i-caret { fill: rgb(var(--accent-rgb)); font-style: normal; }
.jn-face .fx2 .i-trace {
  stroke: rgb(var(--accent-rgb)); stroke-width: 3; fill: none;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: var(--len); stroke-dashoffset: calc(var(--len) * (1 - var(--t)));
}
/* each reading fades up in ALPHA as the line reaches it — never in opacity */
.jn-face .fx2 .i-read {
  --seen: clamp(0, calc((var(--t) - var(--ri) * .3) * 9), 1);
  fill: rgb(var(--accent-rgb) / var(--seen));
}
.jn-face .fx2 .i-readlab {
  --seen: clamp(0, calc((var(--t) - var(--ri) * .3) * 9), 1);
  font-size: 13px; font-weight: 650; fill: rgb(245 243 239 / calc(var(--seen) * .55));
}
.jn-face .fx2 .i-readlab.now { font-size: 17px; fill: rgb(var(--accent-rgb) / var(--seen)); }
.jn-b1 .s1 .i-ans { --seen: clamp(0, calc((var(--t) - .72) * 6), 1); fill: rgb(245 243 239 / var(--seen)); }
.jn-b1 .s1 .i-caret { animation: iCaret 1.05s steps(1, end) infinite; }
@keyframes iCaret { 0%, 49% { fill: rgb(var(--accent-rgb)); } 50%, 100% { fill: rgb(var(--accent-rgb) / 0); } }

/* ----------------------------------------------------------------- MODEL --
   A DIVERGING BENCH. History arrives, pivots at NOW, and two futures leave at
   visibly different angles inside a cone that opens as you scroll. The only
   pointed, forking shape in the set. */
.jn-b2 .s2 { --t: clamp(0, calc(var(--jb, 1) / .60), 1); }
.jn-face .fx2 .i-rail  { stroke: rgb(245 243 239 / .22); stroke-width: 2; fill: none; }
.jn-face .fx2 .i-mount { stroke: rgb(245 243 239 / .3); stroke-width: 1.4; fill: none; }
.jn-face .fx2 .i-hist  { stroke: rgb(245 243 239 / .55); stroke-width: 2.6; fill: none; stroke-linecap: round; }
.jn-face .fx2 .i-same  { stroke: rgb(245 243 239 / .25); stroke-width: 2; fill: none; stroke-dasharray: 5 5; }
.jn-face .fx2 .i-proj  {
  stroke: var(--violet); stroke-width: 3; fill: none; stroke-linecap: round;
  stroke-dasharray: var(--len); stroke-dashoffset: calc(var(--len) * (1 - var(--t)));
}
/* .fx path{fill:none} is (0,1,1) and used to erase the cone entirely — the copy
   promised an uncertainty band that never rendered. Three dots fixes it. */
.jn-face .fx2 .i-fan   { fill: rgb(169 221 189 / .13); stroke: none; transform-origin: 200px 200px; }
.jn-face .fx2 .i-prism { fill: rgb(var(--accent-rgb) / .9); stroke: none; }
.jn-face .fx2 .i-samelab { font-size: 11px; fill: rgb(245 243 239 / .4); text-anchor: end; }
.jn-face .fx2 .i-projlab { font-size: 11px; fill: rgb(169 221 189 / .9); text-anchor: end; }
.jn-face .fx2 .i-endp  { fill: var(--violet); }
.jn-b2 .s2 .i-fan  { scale: var(--t) 1; }
.jn-b2 .s2 .i-endp { --seen: clamp(0, calc((var(--t) - .86) * 8), 1); fill: rgb(169 221 189 / var(--seen)); }
.jn-b2 .s2 .i-projlab { --seen: clamp(0, calc((var(--t) - .86) * 8), 1); fill: rgb(169 221 189 / var(--seen)); }
/* the loop that earns its place: a confidence band that is never certain */
.jn-b2 .s2 .i-fan { animation: iFan 6.4s cubic-bezier(.45,0,.55,1) infinite; }
@keyframes iFan { 0%, 100% { scale: var(--t) .84; } 50% { scale: var(--t) 1.12; } }

/* ------------------------------------------------------------------- ACT --
   A WEEK REGISTER. Three habit rows across seven days, punched shut one day at
   a time as the head advances in seven quantised steps. Days still open are
   marching-ant outlines, not dimmer versions of "done" — v91 and v92 both
   fought an unlit segment that read as a rendering bug. */
.jn-b3 .s3 {
  --t: clamp(0, calc(var(--jb, 1) / .88), 1);
  --k0: clamp(0, calc((var(--t) - .00) * 26), 1); --k1: clamp(0, calc((var(--t) - .14) * 26), 1);
  --k2: clamp(0, calc((var(--t) - .28) * 26), 1); --k3: clamp(0, calc((var(--t) - .42) * 26), 1);
  --k4: clamp(0, calc((var(--t) - .56) * 26), 1); --k5: clamp(0, calc((var(--t) - .70) * 26), 1);
  --k6: clamp(0, calc((var(--t) - .84) * 26), 1);
  --day: calc(var(--k0) + var(--k1) + var(--k2) + var(--k3) + var(--k4) + var(--k5) + var(--k6));
}
.jn-face .fx2 .i-day  { font-size: 10px; font-weight: 700; letter-spacing: .1em; fill: var(--text-faint); }
.jn-face .fx2 .i-row  { font-size: 10px; font-weight: 700; letter-spacing: .16em; fill: var(--text-faint); text-anchor: end; }
.jn-face .fx2 .i-base { stroke: rgb(245 243 239 / .18); stroke-width: 1.4; fill: none; }
.jn-face .fx2 .i-head path { stroke: rgb(var(--accent-rgb) / .9); stroke-width: 2; fill: none; }
.jn-face .fx2 .i-head .i-headfoot { stroke-width: 3; }
.jn-face .fx2 .i-cell rect { stroke-width: 1.4; }
.jn-face .fx2 .i-cell.done rect { fill: rgb(var(--accent-rgb) / .16); stroke: rgb(var(--accent-rgb) / .5); }
.jn-face .fx2 .i-cell.open rect { fill: none; stroke: rgb(245 243 239 / .3); stroke-dasharray: 3 4; }
/* every latch is alpha + translate, so a reduced-motion toggle cannot make the
   unfinished half of the week claim it was done */
.jn-b3 .s3 .i-cell {
  --on: clamp(0, calc((var(--t) - var(--d) * .14) * 26), 1);
  translate: 0 calc((1 - var(--on)) * 9px);
}
.jn-b3 .s3 .i-cell.done rect {
  fill: rgb(var(--accent-rgb) / calc(var(--on) * .22));
  stroke: rgb(var(--accent-rgb) / calc(.08 + var(--on) * .6));
}
.jn-b3 .s3 .i-cell.open rect { stroke: rgb(245 243 239 / calc(.08 + var(--on) * .22)); }
.jn-b3 .s3 .i-head { translate: calc(104px + var(--day) * 34px) 0; }
.jn-b3 .s3 .i-tot {
  --u1: clamp(0, calc((var(--t) - .22) * 24), 1);
  --u2: clamp(0, calc((var(--t) - .50) * 24), 1);
  --u3: clamp(0, calc((var(--t) - .78) * 24), 1);
  translate: 0 calc(-38px * (var(--u1) + var(--u2) + var(--u3)));
}
/* marching ants: "open" as an explicit state rather than a dim one */
.jn-b3 .s3 .i-cell.open rect { animation: iAnts 1.1s linear infinite; }
@keyframes iAnts { to { stroke-dashoffset: -14; } }

/* reduced motion: land every instrument on its finished frame */
@media (prefers-reduced-motion: reduce) {
  .jn-face .fx2 *, .jn-face .jn-chrome * { animation: none !important; }
  .jn-b0 .s0, .jn-b1 .s1, .jn-b2 .s2, .jn-b3 .s3 { --t: 1; }
  .jn-b3 .s3 { --k0: 1; --k1: 1; --k2: 1; --k3: 1; --k4: 1; --k5: 1; --k6: 1; }
}

/* The bezel was a hand-tuned anodized teal: nine hardcoded stops that stayed
   teal whatever accent you picked, so a clay instrument sat in a cyan ring.
   The metal's LIGHT structure is what makes it read as machined — dark, rising
   to a specular, falling back — so that is kept exactly and only the hue is
   handed to the accent. Built from rgb(... / a) over a fixed dark base rather
   than color-mix(), which is iOS 16.2 and the deployment target is 16.0. */
.jn-cam {
  background-color: #070B09 !important;
  background:
    repeating-conic-gradient(from 0deg, rgb(var(--accent-rgb) / .42) 0 .35deg, transparent .35deg 6deg),
    repeating-conic-gradient(from 0deg, rgb(255 255 255 / .7) 0 1deg, transparent 1deg 90deg),
    conic-gradient(from 210deg,
      #070B09,
      rgb(var(--accent-rgb) / .5) 12%,
      rgb(var(--accent-rgb) / .92) 20%,
      rgb(255 255 255 / .9) 24%,
      rgb(var(--accent-rgb) / .78) 29%,
      rgb(var(--accent-rgb) / .3) 44%,
      #070B09 58%,
      rgb(var(--accent-rgb) / .46) 78%,
      rgb(var(--accent-rgb) / .88) 86%,
      #070B09 100%) !important;
  box-shadow:
    0 0 0 1px rgb(var(--accent-rgb) / .5),
    0 30px 90px -30px rgb(0 0 0 / .95),
    0 0 140px -35px rgb(var(--accent-rgb) / .7) !important;
}
/* a specular highlight is white on any metal */
.jn-cam::before { background: conic-gradient(from 0deg, transparent, rgb(255 255 255 / .92) 7%, transparent 21%) !important; }
.jn-cam::after {
  box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / .2), inset 0 30px 80px rgb(0 0 0 / .95),
              inset 0 -12px 46px rgb(var(--accent-rgb) / .26), 0 0 0 5px #04090C !important;
}
.jn-dot { border-color: rgb(var(--accent-rgb) / .42) !important; }

/* v109 — the stacked (phone) tour actually fits.
   `.jn-cam` carries a per-beat rotateY/rotateX/scale, so its PAINTED box is bigger than the grid
   row it was allotted — measured 299px against a 285px row. The copy column below it therefore had
   the dial's lower rim sitting on top of the step eyebrow and the headline (54px of overlap on a
   375px viewport, confirmed with elementsFromPoint). Shrinking the dial and widening the row gap
   fixes it at the source rather than pushing the text around. */
@media (max-width: 899px) {
  .jn-cam { width: min(34vh, 66vw); height: min(34vh, 66vw); }
  .jn-stagewrap { gap: 34px; }
  /* the extra room buys back a line of copy — three lines was cutting sentences mid-clause */
  .jn-cp p { -webkit-line-clamp: 4; }
  /* THE ACTUAL CAUSE of the overlap. `.jn-cp { bottom: 12vh }` dates from a layout where the copy
     was position:absolute and deliberately sat over the dial. The ACTIVE panel is now
     position:relative — where `bottom` shifts an element UP out of its own grid row. 12vh is 97px
     on an 812px phone, which is precisely the measured overlap. Only the active panel is relative,
     so only it needs correcting. */
  .jn-b0 .c0, .jn-b1 .c1, .jn-b2 .c2, .jn-b3 .c3 { bottom: auto; }
  /* The beat rail is absolutely positioned at bottom:4vh, and on a phone it wraps to two rows —
     102px of it — which nothing above it accounts for. ACT has the longest key list and ran 56px
     underneath it. `.jn-stick` is already a flex column, so putting the rail back in flow and
     ordering it last makes the layout self-sizing instead of guessing at a padding value. */
  .jn-rail { position: static; order: 3; transform: none; left: auto; bottom: auto;
             margin-top: 4px; flex: 0 0 auto; }
}
/* Very short viewports (landscape phones, small Androids) lose the copy entirely otherwise. */
@media (max-width: 899px) and (max-height: 700px) {
  .jn-cam { width: min(28vh, 54vw); height: min(28vh, 54vw); }
  .jn-cp p { -webkit-line-clamp: 3; }
}

/* ============================================================================
   v112 — THE LAG, AND THE GREEN.

   Hale was laggy in a way that got worse the more data you had, and the cause
   was not the amount of markup. Four SMIL animations were running forever:

     - two on the SHARED #haleFlow gradient (x1/x2, 4.2s, indefinite)
     - two morphing the `points` of the EKG polylines (12s, indefinite)

   Animating a paint server invalidates every element painted with it, and
   #haleFlow strokes the EKG, EVERY sparkline, the twin gauge, the projection
   lines and EVERY marker spark. So a My Data screen with twenty markers was
   repainting twenty-odd elements at 60fps forever — several through a
   drop-shadow blur. SMIL runs on the main thread and cannot be composited.
   Measured on the live site: 6 CSS animations, all compositor-safe, and 4 SMIL
   animations doing all the damage.

   All four are gone. The gradient is static, the EKG cross-fades three static
   waveforms on opacity, and the green got richer rather than poorer for it.
   ============================================================================ */

/* --- the gradient: dark shoulders, a bright specular band off-centre -------- */
#haleFlow .hf-a { stop-color: var(--green-deep); }
#haleFlow .hf-b { stop-color: var(--green-2); }
#haleFlow .hf-c { stop-color: rgb(var(--accent-rgb) / 1); }
#haleFlow .hf-d { stop-color: var(--green); }
#haleFlow .hf-e { stop-color: var(--green-deep); }
/* the specular band, lifted toward white so a stroke reads as lit metal rather
   than flat paint. @supports-gated: color-mix is iOS 16.2 and the target is 16.0. */
@supports (color: color-mix(in srgb, red 50%, white)) {
  #haleFlow .hf-c { stop-color: color-mix(in srgb, rgb(var(--accent-rgb)) 62%, white); }
}

/* --- EKG: three static waveforms, cross-faded. No geometry animation. ------- */
.vital-line { opacity: 0; }
.vital-line.w1 { animation: ekgW1 12s linear infinite; }
.vital-line.w2 { animation: ekgW2 12s linear infinite; }
.vital-line.w3 { animation: ekgW3 12s linear infinite; }
/* Each waveform holds, then hands over. Matches the vlab1/2/3 label cycle that
   was already keyed to the same 12s, so signal and caption still agree. */
@keyframes ekgW1 { 0%, 26% { opacity: 1; } 33%, 92% { opacity: 0; } 100% { opacity: 1; } }
@keyframes ekgW2 { 0%, 26% { opacity: 0; } 33%, 59% { opacity: 1; } 66%, 100% { opacity: 0; } }
@keyframes ekgW3 { 0%, 59% { opacity: 0; } 66%, 92% { opacity: 1; } 100%, 100% { opacity: 0; } }
/* One glow for the whole stack instead of one per line: the drop-shadow now sits
   on the parent, so it is rasterised once rather than three times. */
.hero-ekg, .lp-ekg { filter: drop-shadow(0 0 6px rgb(var(--accent-rgb) / 0.4)); }
.vital-line { filter: none !important; }
/* The mirrored reflection lives inside `filter: blur(1.2px)` and a mask-image. Changing a child's
   opacity inside a filtered, masked subtree invalidates that subtree, so cross-fading the mirror
   too would have re-run the blur every frame — reintroducing exactly the per-frame cost this
   layer exists to remove. It holds one waveform, statically, and at .18 opacity behind a fade
   mask nobody can tell it is not keeping step. */
.vital-mirror .vital-line { animation: none !important; opacity: 0; }
.vital-mirror .vital-line.w1 { opacity: 1; }

/* --- the surface: a real green gradient, not a hint of one ----------------
   The app sat on #0a0c0b with two 6%-alpha blooms — effectively flat black. The
   base is now a deep green that travels across the viewport, so the product has
   a colour of its own rather than a dark grey with green accents on it. */
html:not(.light) body {
  background:
    radial-gradient(1500px 900px at 82% -14%, rgb(var(--accent-rgb) / .22), transparent 60%),
    radial-gradient(1200px 800px at -14% 112%, rgb(var(--accent-rgb) / .17), transparent 58%),
    linear-gradient(158deg, #0e1f16 0%, #0a1610 38%, #070f0b 72%, #050a07 100%);
  background-attachment: fixed;
}

/* --- the texture ---------------------------------------------------------
   Two layers, both STATIC and both painted once into a single fixed layer:
   a fine diagonal weave that catches the light like anodised metal, and a film
   grain over the top to break up the gradient banding. This is the whole point
   of the change above — texture is free, ANIMATED texture is what cost us. */
.app::after {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-image:
    /* the weave — 3px pitch, barely-there light and dark ribs on the diagonal */
    repeating-linear-gradient(122deg,
      rgb(255 255 255 / .028) 0 1px,
      transparent 1px 3px),
    repeating-linear-gradient(32deg,
      rgb(0 0 0 / .05) 0 1px,
      transparent 1px 4px),
    /* the grain */
    url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'>\
<filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' seed='11'/>\
<feColorMatrix type='saturate' values='0'/></filter>\
<rect width='180' height='180' filter='url(%23g)' opacity='0.55'/></svg>");
  background-size: auto, auto, 180px 180px;
  background-repeat: repeat, repeat, repeat;
  /* Deliberately NOT mix-blend-mode. A blended full-viewport layer has to be re-composited
     against its backdrop whenever anything underneath repaints — every scroll, every message,
     every hover. Plain alpha over a dark UI gets the same grain for a one-off paint. */
  opacity: .5;
}
/* The wash sits BEHIND the content, via a negative z-index inside a positioned .app — not by
   forcing the content above it.

   The first version of this rule did the latter: `.sidebar, .main, .topbar, .view, .scrim
   { position: relative; z-index: 1 }`. On mobile the sidebar is off-canvas via
   `position: fixed` + translateX(-100%), and forcing it to `relative` put it back in the layout
   flow — so the hidden sidebar still consumed 315px and the whole app rendered in the 60px
   sliver left over. Broken on every phone from v112 until this build. Never override `position`
   on a layout element to solve a stacking problem. */
.app { position: relative; }

/* Cards pick the green up too, so the theme reads as one material rather than a
   backdrop with grey panels floating on it. */
html:not(.light) .card,
html:not(.light) .sidebar,
html:not(.light) .main {
  background-image: linear-gradient(158deg, rgb(var(--accent-rgb) / .055), transparent 62%);
}

@media (prefers-reduced-motion: reduce) {
  .vital-line.w1, .vital-line.w2, .vital-line.w3 { animation: none; }
  .vital-line.w1 { opacity: 1; }
}

/* ============================================================================
   v114 — THE ACTUAL LAG. Three full-viewport animated blur layers.

   The SMIL fix above was real but it was not the big one. Measured on the live
   site at 1280x720 @DPR2 (viewport = 3,686,400 device px):

     body::before   1536x864 CSS = 5,308,416 device px   blur(46px)  aura 26s
     body::after    2043x1149 CSS = 9,390,057 device px   blur(40px) saturate(1.5)  roomDrift 34s
     .main::before  1213x868 CSS = 4,210,564 device px   blur(38px)  aura 32s

   18,909,037 device px of blurred surface — 5.1x the viewport — re-rasterised
   every frame, forever, on every route. About 1.13 billion blurred device-px per
   second here, ~2.4 billion on a 16-inch MacBook. And because all three animate
   a scale(), a cached raster cannot serve them: changing the scale of a blurred
   layer forces a genuine re-raster rather than a compositor transform.

   The worst of it is invisible. body::after is declared twice — once at :1316 as
   the film grain with opacity .045, once at :3065 as "THE ROOM". CSS merges
   same-selector pseudo-element rules, so the room inherited the grain's opacity:
   the single most expensive layer in the app renders at 4.5% opacity. It was
   burning 9.4M blurred, saturated pixels a frame to paint something nobody can
   see.

   Then backdrop-filter multiplies all of it. A backdrop-filter must read back
   and re-blur everything behind it whenever the backdrop changes — and the
   backdrop was moving every frame. .card carries blur(8px), and My Data renders
   one card per marker, so twenty markers meant twenty-plus independent per-frame
   backdrop re-blurs. That is the real reason the app got worse the more data you
   had.
   ============================================================================ */

/* 1. The room is gone. Its colour now lives in the static body gradient above,
      where it is actually visible and costs one paint. */
body::after { display: none !important; }

/* 2. The two aura layers keep their look and lose their motion. A STATIC blurred
      layer is rasterised once and then just composited; it was only ever the
      animated scale that made these expensive. */
body::before, .main::before { animation: none !important; }

/* 3. No backdrop-filter on repeated surfaces. One per screen is affordable; one
      per marker is not. The cards get a slightly more opaque base instead, so
      they still read as panels rather than holes. */
.card, .set-group, .twin-list, .conn-card, .mk-card {
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
@media (prefers-reduced-motion: no-preference) {
  /* the singleton chrome can keep its glass — there is exactly one of each */
  .composer, .tabbar { backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }
}

/* ============================================================================
   v117 — THE CHART. The landing page is a document, not a demo.

   It was dark-glassy app marketing: a gradient-clipped headline in a blur halo,
   four drifting chips, 20px corners, a 2,880px fly-through corridor and a
   4,140px pinned carousel — 61% of the page height spent on metaphors FOR Hale
   before a visitor met a single argument.

   It is now one continuous precision document — a readout of a person. Numbered
   sections, a ruled substrate, hairlines instead of glow, 2px corners instead of
   20px, and two type voices: Inter for prose, monospace for every figure, unit,
   label and section id.

   The corridor is deleted outright. So is spin(), the rAF loop that wrote --dial
   on .jn-cam every frame from page load until you navigated away.

   ONE DELIBERATE DEPARTURE from the design as drawn: it wanted fixed `ch` grid
   columns for the panel. System monospace has different metrics on every OS, so
   ch-widths that align on a Mac wrap on Windows — on a page whose whole premise
   is that the alignment is exact. Columns are fr/minmax with tabular-nums.
   ============================================================================ */

.lp-chart {
  --paper:   #070A08;
  --paper-2: #0B100C;
  --rule:    rgb(74 222 128 / .13);
  --rule-2:  rgb(74 222 128 / .28);
  --edge:    rgb(233 246 236 / .11);
  --ink:     #E9F6EC;
  --ink-2:   #9DB3A3;
  --ink-3:   #5E7566;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  background:
    repeating-linear-gradient(90deg, var(--rule) 0 1px, transparent 1px 26px),
    repeating-linear-gradient(0deg,  var(--rule) 0 1px, transparent 1px 26px),
    var(--paper);
  color: var(--ink);
}
/* the rounded display face is the single most anti-clinical thing on the page */
.lp-chart h1, .lp-chart h2, .lp-chart h3, .lp-chart .lp-plan-price, .lp-chart .lp-brand {
  font-family: var(--font) !important;
}
.lp-chart .lp-sec, .lp-chart .lp-hero, .lp-chart .lp-how,
.lp-chart .lp-pricing, .lp-chart .lp-journey { background: transparent; }

/* --- section furniture ---------------------------------------------------- */
.ch-head { display: flex; align-items: center; gap: 12px; margin: 0 auto 26px;
  max-width: 1120px; padding: 0 24px; font-family: var(--mono);
  font-size: 11px; letter-spacing: .14em; color: var(--ink-3); }
.ch-head .ch-hr { display: none; }
.ch-head .ch-id { color: var(--green); }
.ch-head .ch-fill { flex: 1 1 auto; height: 1px; background: repeating-linear-gradient(90deg, var(--rule-2) 0 3px, transparent 3px 7px); }
.ch-head .ch-name-h { color: var(--ink-2); }
.ch-head .ch-meta { color: var(--ink-3); }

/* --- §01 the hero as a specimen sheet ------------------------------------- */
.ch-hero { display: grid; grid-template-columns: 7fr 5fr; gap: 46px; align-items: start;
  max-width: 1120px; margin: 0 auto; padding: 74px 24px 64px; text-align: left; min-height: 0; }
.ch-masthead { display: flex; align-items: center; gap: 12px; margin-bottom: 30px; }
.ch-mast-name { font-family: var(--mono); font-size: 12px; letter-spacing: .34em; color: var(--green); }
.ch-masthead .ch-rule { flex: 0 0 46px; height: 1px; background: var(--rule-2); }
.ch-mast-meta { font-family: var(--mono); font-size: 10.5px; letter-spacing: .16em; color: var(--ink-3); }
/* The old headline was a gradient clipped to the text. That rule ships its background-image with
   !important (a v-layer override of an earlier v-layer), so unwinding it needs !important back —
   the one place on this page where the arms race is unavoidable rather than lazy. Flat ink with a
   single green word is the whole point: no gradient, no clip, no halo. */
.lp-chart .ch-hero h1 { font-size: clamp(2.4rem, 5.4vw, 4.2rem); font-weight: 700;
  line-height: .96; letter-spacing: -.035em; margin: 0 0 20px;
  color: var(--ink) !important;
  background: none !important; background-image: none !important;
  -webkit-background-clip: border-box !important; background-clip: border-box !important;
  -webkit-text-fill-color: currentColor !important; }
.lp-chart .ch-hero h1 em { font-style: normal; color: var(--green); }
.lp-chart .ch-hero .lp-sub { max-width: 46ch; margin: 0 0 26px; color: var(--ink-2); font-size: 1.02rem; }
.lp-chart .ch-hero .lp-cta { justify-content: flex-start; margin: 0 0 14px; }
.lp-chart .lp-fine { font-family: var(--mono); font-size: 10.5px; letter-spacing: .12em; color: var(--ink-3); }

/* the panel — eight real markers, each against its typical range */
.ch-panel { border: 1px solid var(--edge); background: var(--paper-2); border-radius: 2px; }
.ch-panel-head, .ch-panel-foot { display: flex; justify-content: space-between;
  font-family: var(--mono); font-size: 10px; letter-spacing: .14em; color: var(--ink-3);
  padding: 10px 12px; }
.ch-panel-head { border-bottom: 1px solid var(--edge); }
.ch-panel-foot { border-top: 1px solid var(--edge); }
.ch-row { display: grid; grid-template-columns: minmax(64px, 1.1fr) auto auto minmax(70px, 1.4fr) auto;
  align-items: center; gap: 10px; padding: 0 12px; height: 26px;
  font-family: var(--mono); font-size: 11px; font-variant-numeric: tabular-nums; }
.ch-row:nth-child(odd) { background: rgb(233 246 236 / .022); }
.ch-name { color: var(--ink-2); font-family: var(--font); font-size: 12px; }
.ch-fig  { color: var(--ink); text-align: right; }
.ch-unit { color: var(--ink-3); }
.ch-scale { position: relative; height: 6px; background: rgb(233 246 236 / .06); border-radius: 1px; }
.ch-band  { position: absolute; top: 0; bottom: 0; background: rgb(74 222 128 / .20); }
.ch-needle { position: absolute; top: -3px; bottom: -3px; left: var(--at); width: 1px;
  background: var(--green); }
.ch-flag { font-size: 9px; letter-spacing: .1em; color: var(--green); }
.ch-flag.watch, .ch-flag.high { color: #f87171; }   /* the alarm stays red — see v116 */

/* --- §06 the spec sheet --------------------------------------------------- */
.ch-spec { max-width: 1120px; margin: 0 auto; padding: 0 24px 64px; display: block; }
.ch-table { width: 100%; border-collapse: collapse; font-size: .95rem; }
.ch-table th, .ch-table td { text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--rule); }
.ch-table thead th { font-family: var(--mono); font-size: 10.5px; letter-spacing: .16em;
  color: var(--ink-3); border-bottom: 1px solid var(--rule-2); font-weight: 400; }
.ch-table tbody tr:nth-child(odd) { background: rgb(233 246 236 / .018); }
.ch-table td:first-child { color: var(--ink-2); }
.ch-table .pro { border-left: 2px solid var(--green); background: rgb(74 222 128 / .03); }
.ch-yes { display: inline-block; width: 11px; height: 6px; border-left: 1.5px solid var(--green);
  border-bottom: 1.5px solid var(--green); transform: rotate(-45deg); }
.ch-no { display: inline-block; width: 4px; height: 4px; border-radius: 50%; background: var(--ink-3); }
.ch-fig.sm { font-family: var(--mono); font-size: 11px; color: var(--ink); }
.ch-price { display: block; font-family: var(--mono); font-size: 2.1rem; color: var(--ink);
  font-variant-numeric: tabular-nums; margin-bottom: 10px; }
.ch-price i { font-style: normal; font-size: 11px; color: var(--ink-3); }
.ch-table tfoot td { border-bottom: 0; vertical-align: top; }

/* --- square everything ---------------------------------------------------- */
.lp-chart .card, .lp-chart .lp-plan, .lp-chart .lp-viz, .lp-chart .btn,
.lp-chart .lp-chip, .lp-chart .jn-dot { border-radius: 2px !important; }
.lp-chart .lp-sec .lp-viz { box-shadow: none !important; border: 1px solid var(--edge) !important; }

@media (max-width: 899px) {
  .ch-hero { grid-template-columns: 1fr; gap: 30px; padding: 40px 18px 44px; }
  .ch-head { padding: 0 18px; }
  .ch-table th:first-child, .ch-table td:first-child { max-width: 46vw; }
}

/* --- §04–06: the plates stop looping ---------------------------------------
   The three closing sections carried ~45 infinite animations between them (the
   sc-*, vz-*, tw-* families) and ran them forever, including while entirely
   off-screen. The design calls for them frozen at their resting frame so they
   read as finished engravings rather than loops.

   `animation-iteration-count: 1` rather than `animation: none` on purpose: many
   of these start at opacity 0 or scale 0, so cancelling them outright would
   leave the plates blank. Running each exactly once lands every element on its
   final keyframe — which IS the resting frame — and then stops. */
.lp-chart .lp-sec .lp-viz *,
.lp-chart .lp-sec .lp-viz { animation-iteration-count: 1 !important; }

/* Decorative loops that carry no information and repaint every frame. */
.lp-chart .lp-hero h1 { animation: none !important; }        /* heroSweep — animated background-position
                                                                on a gradient this layer already removed */
.lp-chart .btn.primary.lp-btn { animation: none !important; } /* ctaBreath — box-shadow per frame */
.lp-chart .lp-step-n { animation: none !important; }          /* heartbeat x3 — box-shadow per frame */
.lp-chart .stars, .lp-chart .chip-streak, .lp-chart .pick-shadow { animation: none !important; }
.jn-b0 .s0 .i-cursor .i-hair { animation: none; }             /* iHair — animated `stroke` is a repaint */

/* panelSweep animated `left` on a ::before across all three .lp-viz plates — a layout-triggering
   property, 11s, forever, on the one page that is meant to cost nothing at rest. The glass sheen
   it produced is exactly the "glossy app marketing" this layer set out to remove, so it goes
   rather than being converted to a transform. */
.lp-chart .lp-viz::before { animation: none !important; display: none; }

/* ============================================================================
   v121 — §02 becomes a rack-mount faceplate.

   The tour was the last thing on the landing still speaking the old language: a
   round machined bezel with three stacked conic gradients, a rotating specular
   sweep, and a circular 60-tick ring — a beautiful object, and completely at
   odds with a page that is otherwise ruled chart paper.

   It is now a flat instrument faceplate: a squared aperture with chamfered
   corners, L-bracket registration marks, a linear tick rule along the bottom,
   and a channel selector down the side. The four instruments inside are
   untouched — every designer on the panel said they were the best thing here.
   ============================================================================ */

/* --- the plate ------------------------------------------------------------ */
.lp-chart .jn-cam {
  border-radius: 2px !important;
  width: min(58vh, 44vw); height: min(58vh, 44vw);
  background-color: var(--paper-2) !important;
  /* the plate carries the page grid, so it registers to the same lattice as
     every rule elsewhere in the document */
  background:
    repeating-linear-gradient(90deg, var(--rule) 0 1px, transparent 1px 26px),
    repeating-linear-gradient(0deg,  var(--rule) 0 1px, transparent 1px 26px),
    linear-gradient(168deg, rgb(233 246 236 / .045), transparent 60%),
    var(--paper-2) !important;
  box-shadow: 0 0 0 1px var(--edge), 0 24px 60px -30px rgb(0 0 0 / .9) !important;
  overflow: visible !important;
  rotate: none !important;
}
/* the rotating specular sweep and the round inner well both go */
.lp-chart .jn-cam::before { display: none !important; }
.lp-chart .jn-cam::after {
  content: ""; position: absolute; inset: 7%; border-radius: 2px;
  box-shadow: inset 0 0 0 1px var(--edge), inset 0 18px 50px rgb(0 0 0 / .8) !important;
  background: #050907; z-index: 0;
}

/* --- the aperture --------------------------------------------------------- */
.lp-chart .jn-face {
  width: 88%; border-radius: 2px; rotate: none !important;
  animation: none !important;                 /* faceDrift belonged to a round dial */
}

/* --- the faceplate furniture ---------------------------------------------- */
.lp-chart .jn-chrome { overflow: visible; }
.lp-chart .jn-chrome .jc-reg { stroke: rgb(var(--accent-rgb) / .75); stroke-width: 2; fill: none; }
.lp-chart .jn-chrome .jc-min { stroke: rgb(233 246 236 / .28); stroke-width: 1.1; fill: none; }
.lp-chart .jn-chrome .jc-maj { stroke: rgb(233 246 236 / .5);  stroke-width: 1.6; fill: none; }
/* the channel indicator — the one moving thing on the plate, and it moves on
   transform only, so it composites rather than repaints */
.lp-chart .jn-chrome .jc-chan {
  fill: rgb(var(--accent-rgb)); transform-origin: 22px 0;
  transition: transform 260ms cubic-bezier(.16,1,.3,1);
}
.jn-b0 .jn-chrome .jc-chan { transform: translateX(0); }
.jn-b1 .jn-chrome .jc-chan { transform: translateX(89px); }
.jn-b2 .jn-chrome .jc-chan { transform: translateX(178px); }
.jn-b3 .jn-chrome .jc-chan { transform: translateX(267px); }

/* the scene ident, printed flush left like a channel label */
.jn-face .fx2 .i-ident { text-anchor: start; font-size: 10.5px; letter-spacing: .16em;
  fill: rgb(var(--accent-rgb) / .9); }
.jn-face .fx2 .i-name  { text-anchor: start; font-size: 10.5px; letter-spacing: .16em;
  fill: var(--ink-3, #5E7566); }

/* --- the rail becomes a channel selector ---------------------------------- */
.lp-chart .jn-rail {
  position: absolute; left: 50%; bottom: 3vh; transform: translateX(-50%);
  display: flex; gap: 0; border: 0; background: none;
  border-top: 1px solid var(--rule-2); padding-top: 0;
}
.lp-chart .jn-dot {
  border: 0 !important; border-radius: 0 !important; background: none !important;
  font-family: var(--mono); font-size: 10px; letter-spacing: .18em;
  padding: 10px 18px 8px; color: var(--ink-3); position: relative;
}
.lp-chart .jn-dot::after {
  content: ""; position: absolute; left: 0; right: 0; top: -1px; height: 2px;
  background: rgb(var(--accent-rgb)); transform: scaleX(0); transform-origin: left;
  transition: transform 240ms cubic-bezier(.16,1,.3,1);
}
.lp-chart .jn-stick.jn-b0 .jn-dot:nth-child(1)::after,
.lp-chart .jn-stick.jn-b1 .jn-dot:nth-child(2)::after,
.lp-chart .jn-stick.jn-b2 .jn-dot:nth-child(3)::after,
.lp-chart .jn-stick.jn-b3 .jn-dot:nth-child(4)::after { transform: scaleX(1); }
.lp-chart .jn-stick.jn-b0 .jn-dot:nth-child(1),
.lp-chart .jn-stick.jn-b1 .jn-dot:nth-child(2),
.lp-chart .jn-stick.jn-b2 .jn-dot:nth-child(3),
.lp-chart .jn-stick.jn-b3 .jn-dot:nth-child(4) { color: var(--ink) !important; }

/* --- the legend becomes a key table --------------------------------------- */
.lp-chart .jn-key { border-top: 1px solid var(--rule); padding: 0; margin-top: 18px;
  background: none !important; border-radius: 0 !important; }
.lp-chart .jn-key li {
  display: grid; grid-template-columns: 22px 1fr auto; align-items: center; gap: 12px;
  padding: 9px 0; border-bottom: 1px solid var(--rule); background: none;
}
.lp-chart .jn-key b { font-family: var(--font); font-weight: 400; color: var(--ink-2); font-size: .86rem; }
.lp-chart .jn-key span { font-family: var(--mono); font-size: 11px; color: var(--ink);
  font-variant-numeric: tabular-nums; text-align: right; }
/* the swatch is a specimen of the actual stroke, not a dot that shares its colour */
.lp-chart .jn-key .jk-seg { width: 14px; height: 2px; border-radius: 0;
  background: var(--seg); display: block; }
.lp-chart .jn-key .jk-seg.dash {
  background: repeating-linear-gradient(90deg, var(--seg) 0 3px, transparent 3px 6px);
}

/* --- the progress bar squares up ------------------------------------------ */
.lp-chart .jn-prog { border-radius: 0; height: 1px; background: var(--rule-2); }
.lp-chart .jn-prog i { border-radius: 0; background: rgb(var(--accent-rgb)); }

@media (max-width: 899px) {
  .lp-chart .jn-cam { width: min(38vh, 74vw); height: min(38vh, 74vw); }
}

/* ===== v127 — screenshot pass ========================================================= */
/* The Twin scenario box is rows="1", but its placeholder wraps to two lines at phone
   widths, so the second line was clipped by the fixed one-row height. Give it room for
   two lines; `resize: vertical` still lets it grow from there. */
#twinScenario { min-height: calc(3em + 20px); }

/* ===== v128 — native-app launch gate ==================================================
   Opening the app signed out shows the sign-up / sign-in screen and NOTHING else. Same
   idiom as the landing rule up at the "Public landing page" block: one body attribute,
   written by syncAppGate() in app.js, hides every piece of persistent chrome.

   Read the hide-list carefully — it is the landing list PLUS two. `.tabbar`, because the
   tab bar was added long after that rule was written and got its own line 150 lines away;
   miss it and the bottom bar is still sitting there under the card. And `.view`, which the
   landing rule deliberately leaves alone because the marketing page renders INSIDE it —
   here `.view` is exactly the problem, since the home hero and the vitals EKG are what the
   owner saw ghosting through the sheet.

   `display` only. Never reach for position or z-index on these elements to solve a
   stacking problem — see the v112 note above for what that cost last time. */
body[data-gate="auth"] .sidebar,
body[data-gate="auth"] .scrim,
body[data-gate="auth"] .topbar,
body[data-gate="auth"] #verifyBar,
body[data-gate="auth"] #installBar,
body[data-gate="auth"] .view,
body[data-gate="auth"] .tabbar { display: none !important; }

/* Nothing is left to dim. The backdrop behind the card should be the app's own surface —
   the green gradient on <body>, the weave and grain from .app::after, and the corner glow
   and contour tiles from .main's pseudo-elements, all of which keep painting with .main's
   children hidden. A 55% black scrim and a 10px blur over that just makes it look broken,
   so the gate drops both. */
#authgate { background: none; backdrop-filter: none; -webkit-backdrop-filter: none; }

/* .topbar claims the top safe area and .tabbar the bottom one; with both hidden nobody is
   reserving the Dynamic Island or the home indicator, so the gate claims them itself. */
#authgate {
  padding-top: calc(20px + env(safe-area-inset-top, 0px));
  padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
}
/* The sign-up form is tall (name, email, password, date of birth, two fine-print lines).
   88vh ignores the padding above, which on a notched phone pushes the card under the
   clock; bound it by the space the grid actually has instead. */
#authgate .modal { max-height: 100%; }

/* Light mode: pin the gate to the dark palette, exactly as the landing page is pinned in
   the accessibility block above. The auth card is art-directed dark, and the green surface
   itself is a `html:not(.light)` rule — without this a user who picked Light in settings,
   then signed out, would get a pale panel behind a dark card. */
html.light body[data-gate="auth"] {
  --bg: #0d100e; --bg-2: #141814; --panel-solid: #141814; --elev: #141814; --elev-2: #232823;
  --text: #e8ece8; --text-dim: #a4b0a6; --text-faint: #828e84;
  --hairline: rgba(255,255,255,0.07);
  --green: #4ade80; --green-2: #3ecf8e; --green-3: #86efac;
  background:
    radial-gradient(1500px 900px at 82% -14%, rgb(var(--accent-rgb) / .22), transparent 60%),
    radial-gradient(1200px 800px at -14% 112%, rgb(var(--accent-rgb) / .17), transparent 58%),
    linear-gradient(158deg, #0e1f16 0%, #0a1610 38%, #070f0b 72%, #050a07 100%);
  background-attachment: fixed;
}
/* In dark mode .main is already transparent over that surface; in light it keeps its own
   opaque panel gradient, which would cover it. */
html.light body[data-gate="auth"] .main { background: none; }

/* ===== v129 — the launch gate, hardened ================================================
   Four defects the v128 layer left behind, all of them on the one screen a native user
   cannot walk away from. */

/* 1. THE FIRST FRAME. The chrome hidden by body[data-gate="auth"] is STATIC MARKUP, and the
   attribute that hides it used to be written only after init() awaited /api/me — so the
   shell was painted first and the gate landed on top of it a round trip later (measured
   767ms of app shell at +800ms latency). boot.js now raises the attribute in the parser,
   which leaves a real gap of its own: the gate's own card does not exist yet either, so
   the screen would be a bare gradient. #gateBoot fills it with the brand mark and nothing
   else. It is display:none unless the gate is up, so the web never renders it, and it is
   removed outright by syncAppGate() as soon as the card (or the app) takes over. */
#gateBoot { display: none; }
body[data-gate="auth"] #gateBoot {
  display: grid; place-items: center;
  position: fixed; inset: 0; z-index: 90; pointer-events: none;
  animation: gateBootIn 240ms var(--ease, ease-out) both;
}
body[data-gate="auth"] #gateBoot svg { width: 64px; height: 64px; opacity: .9; }
@keyframes gateBootIn { from { opacity: 0; } to { opacity: 1; } }
/* :has() is the belt to syncAppGate()'s brace — if the card is up, the mark is gone either way. */
body[data-gate="auth"]:has(#authgate) #gateBoot { display: none; }
@media (prefers-reduced-motion: reduce) { body[data-gate="auth"] #gateBoot { animation: none; } }

/* 2. #offlineBar joins the two banners that were already in the hide lists. It is a new
   element, so it is missing from BOTH earlier rules — the gate's (v128) and the landing
   page's — and a banner is exactly the kind of thing that would reappear over the
   marketing page or beside the auth card. */
body[data-gate="auth"] #offlineBar,
body[data-view="landing"] #offlineBar { display: none !important; }

/* 3. LIGHT MODE ON THE SCREEN YOU CANNOT LEAVE. v128 pins the gate's TOKENS to the dark
   palette but leaves the html.light COMPONENT overrides in place, and those exist because
   light mode normally collapses --green/-2/-3 to one dark mix. With the bright gradient
   restored underneath them:
     .btn.primary  white label on rgb(74,222,128)  = 1.74:1 (1.40:1 on the light stop)
     .modal-err    #b91c1c on the pinned dark card = 2.96:1
   — the primary call to action and the text that says why sign-in failed, both far under
   AA, measured in the gate itself. Put each back to its dark-mode value; the dark control
   measures 9.54:1 and 6.92:1. The date input is the same family: color-scheme:light paints
   a white native picker on the dark card. */
html.light body[data-gate="auth"] .btn.primary { color: #04130a; }
html.light body[data-gate="auth"] .modal-err { color: #f87171; }
html.light body[data-gate="auth"] .modal-input[type="date"] { color-scheme: dark; }

/* 4. Nothing is left to dim behind the card, so a sheet that was already open when the gate
   went up (the consent sheet, the age gate, a legal doc) rendered AROUND it instead of
   being covered — measured with the consent card at [20,94,390,743] and its own 55% black
   backdrop framing the 390x628 auth card. Fixed where it belongs, in syncAppGate(), which
   clears those overlays as it raises the gate. Deliberately NOT done here as a
   `.modal-overlay:not(#authgate) { display: none }` rule: support and the Terms/Privacy
   sheets are meant to stack ON TOP of the gate afterwards — a user locked out of their
   account must still be able to reach a human — and that rule would erase them too. */

/* ===== v130 — the app does not scroll like a web page ==================================
   The owner's words: "i shouldnt be able to move the entire screen around like the website".
   Three separate things produce that feeling, and each needs its own switch:

     1. the outer WebView rubber-banding             -> killed natively (bounces = false on iOS,
                                                        OVER_SCROLL_NEVER on Android)
     2. pull-to-refresh dragging the whole screen    -> removed natively (UIRefreshControl gone,
                                                        SwipeRefreshLayout disabled)
     3. an inner scroller chaining its overscroll     -> this layer

   Only the third is the web page's to fix, and it is real: .view and .messages are the actual
   scrollers, so flicking past the end of a conversation chains the scroll outward and drags the
   document with it. overscroll-behavior stops the chain at the element.

   Scoped to html[data-app="1"], set by boot.js from the shell's user agent, so NONE of this
   reaches allhale.app — a website is supposed to bounce, and Chrome on Android puts its own
   pull-to-refresh on the document, which real web visitors expect. */
html[data-app="1"],
html[data-app="1"] body { overscroll-behavior: none; }

html[data-app="1"] .view,
html[data-app="1"] .messages,
html[data-app="1"] .recents,
html[data-app="1"] .modal-body { overscroll-behavior: contain; }

/* The launch gate has nothing to scroll at all — pin it so a stray drag cannot move it. */
html[data-app="1"] body[data-gate="auth"] { overflow: hidden; touch-action: pan-y; }

/* ===== v131 — the launch gate's one deliberate door ====================================
   The gate keeps every seal it had: no ✕, no click-outside, Escape scoped away from
   #authgate, and the shells refusing edge-swipe/hardware Back. What it gains is a single
   labelled exit a person has to choose — "Look around first" — and that distinction is the
   whole feature, so it has to LOOK like a choice rather than a second call to action.

   Hence a text link and not a button: separated from the card's actions by a hairline,
   set in the same fine print as the support line above it, its label in --text-dim rather
   than the --green-2 every other .modal-fine link uses. Green there would have read as a
   third CTA stacked under "Create account". The padding is the tap target — the type is
   0.72rem, which is nowhere near 44px on its own. */
#authgate .gate-guest,
#consent .gate-guest {
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--hairline);
}
#authgate .gate-guest a,
#consent .gate-guest a {
  display: inline-block;
  padding: 5px 2px;
  color: var(--text-dim);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}
#authgate .gate-guest a:hover,
#authgate .gate-guest a:active,
#consent .gate-guest a:hover,
#consent .gate-guest a:active { color: var(--text); }
/* Light mode is pinned to the dark palette across the gate (v128/v129); --text-dim and
   --hairline are both in that pinned set, so these need no light-mode twin. The consent
   sheet is not pinned, and there --text-dim is already the light theme's own dim text. */

/* The guest's standing route to an account (index.html #topSignup). It is a real element in
   the static chrome, shown by renderUser() only in the native app and only signed out, so
   the hidden attribute has to beat .btn's display:inline-flex. */
#topSignup[hidden] { display: none; }
#topSignup { white-space: nowrap; }
/* renderUser() hides the "?" avatar while that button is showing — same reason. .avatar is
   display:grid, so the hidden attribute needs beating here too. */
.topbar-right .avatar[hidden] { display: none; }
@media (max-width: 480px) {
  /* .badge-privacy is already dropped at this width; tighten the rest so the pill, this and
     the avatar all fit a 430px top bar without the model pill being squeezed. Behind the app
     marker, because #topSignup is the only reason the row got tighter and it never renders on
     the website — a mobile web visitor's top bar must keep the spacing it has today. */
  html[data-app="1"] .topbar-right { gap: 10px; }
  html[data-app="1"] #topSignup { padding: 7px 12px; }
}

/* ===== v132 — the door has to be reachable, and hittable ==============================
   Two measured defects in v131's door, both on the one screen a signed-out app user cannot
   leave.

   1. TAP TARGET. The link rendered 99 x 24 CSS px against Apple's 44pt HIG minimum — the
      primary CTA above it is 42px tall. Padding it out to 44 is the whole fix; the type stays
      small so it stays visually secondary. The <p> keeps the hairline, the <a> gets the height.

   2. BELOW THE FOLD ON SHORT PHONES. At 375x667 (iPhone SE 2/3, iPhone 8 — all still on iOS 18)
      the door sat at y=686 against a 667pt viewport. The card scrolls, so it was reachable by
      dragging, but the card clipped after "Contact support" with no partial line and no cue, so
      there was nothing to suggest dragging. Tighten the card's vertical rhythm once the viewport
      is too short to hold it, rather than shrinking it for everyone. */
#authgate .gate-guest a,
#consent .gate-guest a {
  min-height: 44px;
  padding: 10px 6px;
  display: inline-flex;
  align-items: center;
}

@media (max-height: 700px) {
  html[data-app="1"] #authgate .modal-mark { display: none; }
  html[data-app="1"] #authgate .modal-title { font-size: 1.15rem; margin-bottom: 2px; }
  html[data-app="1"] #authgate .modal-sub { font-size: 0.8rem; margin-bottom: 8px; }
  html[data-app="1"] #authgate .modal-input { padding-top: 8px; padding-bottom: 8px; }
  html[data-app="1"] #authgate .oauth-btn { padding-top: 8px; padding-bottom: 8px; }
  html[data-app="1"] #authgate .gate-guest { margin-top: 8px; padding-top: 8px; }
}

/* ===== v133 — the hero says what it is for =============================================
   GO_TO_MARKET_PLAN.md §4 gives the Optimizer hook as "Stop guessing" and says to lead with
   "encrypted, never sold, delete anytime" in EVERY channel — it is called the trust unlock for
   health data. The page had neither: the H1 was the brand pun alone, and privacy appeared once,
   as small type in the masthead. The pun stays (it is the brand); the kicker above it carries the
   proposition, and the trust line now sits where someone deciding whether to hand over a lab
   panel will actually read it. Both claims are true as of 271c86c — encryption is live
   (healthz reports memEnc) and deletion now erases support tickets and the on-device copy. */
.lp-kicker {
  margin: 0 0 6px; font-size: clamp(.82rem, 1.15vw, .95rem); font-weight: 600;
  letter-spacing: .01em; color: rgb(var(--accent-rgb));
}
.lp-trust {
  margin: 14px 0 0; font-size: clamp(.76rem, 1vw, .84rem); color: var(--text-dim);
  display: inline-flex; align-items: center; gap: 8px;
}
.lp-trust::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: rgb(var(--accent-rgb)); flex: none;
}

/* ===== v134 — nothing gets pulled, on the website either =============================
   v130 stopped the rubber-band and the scroll-chaining in the native app and deliberately left
   the website alone, on the reasoning that a web page is supposed to bounce. The owner asked for
   it gone on both, so this widens those rules to every surface. Their call, and the right one:
   Hale reads as an application on the web too, and the bounce is the thing that gives away that
   it is a page in a frame.

   Both axes. Horizontal already had overscroll-behavior-x at line 280 to clip the off-canvas
   drawer, but only on the document — an inner scroller could still chain sideways out of it.

   The one real consequence, stated so nobody has to rediscover it: `none` on the document also
   disables Chrome-on-Android's pull-to-refresh for allhale.app. That is the same trade already
   accepted in the app, and it is what "don't pull the page around" means on that browser. Reload
   still works from the browser UI. */
html,
body { overscroll-behavior: none; }

.view,
.messages,
.recents,
.modal-body,
.lp-scroll { overscroll-behavior: contain; }

/* ===== v135 — the drawer stops repeating the tab bar =================================
   Below 901px the bottom tab bar carries Home, Chat, Twin, My Data and Shop — and the drawer
   carried exactly the same five again. Two places to tap for the same destination, one of them
   two taps deep, on the surface with the least room.

   So on those widths the drawer keeps only what nothing else offers: New chat, your recent
   conversations, Help, and your account and settings. Above 901px there is no tab bar and this
   list is the only navigation, so it stays untouched.

   Hidden rather than removed from the markup on purpose: updateNav() and the click wiring walk
   .nav-item, #helpBtn is one of those and lives OUTSIDE .nav, and display:none also takes the
   buttons out of the tab order. Nothing in the script has to know about the breakpoint.

   Checked before writing this: no route is orphaned. Protocols is reached from Home and from
   Settings, Privacy from the account sheet — neither was ever in this list. */
@media (max-width: 900px) {
  .sidebar .nav { display: none; }
  .sidebar .recents { margin-top: 4px; }
}

/* ===== v136 — a shorter focus row, and search over your own chats ====================
   The row carried eight chips (Research, Training and six focuses) and wrapped to three lines on
   a phone, pushing the composer down the screen. It is four now, and collapsed by default: the
   "Focus" label became the toggle and shows the active mode beside it, so nothing is hidden that
   you cannot see is on. The choice is remembered — someone who wants the chips wants them every
   time, and someone who does not should not have to shut them on every new chat. */
.composer-tools.collapsed .tool-chip { display: none; }
.composer-tools .mode-label {
  font: inherit; font-size: 0.75rem; color: var(--text-dim); background: transparent;
  border: 1px solid transparent; border-radius: 999px; padding: 4px 8px; cursor: pointer;
  min-height: 32px; display: inline-flex; align-items: center; gap: 4px;
}
.composer-tools .mode-label:hover { color: var(--text); border-color: var(--hairline); }
.composer-tools .mode-label b { color: rgb(var(--accent-rgb)); font-weight: 600; }

/* Search sits with the recents it filters, in the drawer — which, since v135, is what the drawer
   is FOR on a phone. Hidden until there are enough chats to be worth searching. */
.chat-search { padding: 2px 2px 8px; }
.chat-search input {
  width: 100%; font: inherit; font-size: 0.8125rem; color: var(--text);
  background: var(--elev); border: 1px solid var(--hairline); border-radius: 10px;
  padding: 8px 10px; min-height: 38px;
}
.chat-search input:focus { outline: none; border-color: var(--green); }
.chat-search input::placeholder { color: var(--text-faint); }

/* ===== v137 — the shut Focus row is a button, not a bar =============================
   (Numbered 137 because 135 was already the drawer layer — this shipped mislabelled.)

   Collapsing only hid the chips; the container kept its padding, border, background and full
   width, so "closed" was a wide empty slab with the word FOCUS floating in it — taking more
   vertical room than the composer it sits above and reading as a disabled input rather than a
   control. Shut, it should be the size of what it contains. */
.composer-tools.collapsed {
  display: inline-flex; width: auto; align-self: flex-start;
  padding: 0; border: none; background: none; border-radius: 0;
  margin-bottom: 8px;
}
.composer-tools.collapsed .mode-label {
  margin: 0; padding: 5px 12px;
  border: 1px solid var(--hairline); border-radius: 999px;
  background: var(--bg-2); line-height: 1.5;
}
html[data-app="1"] .composer-tools.collapsed .mode-label,
.composer-tools.collapsed .mode-label { min-height: 30px; display: inline-flex; align-items: center; }

/* Open, the chips now carry a mark as well as a word, so tighten the gap the icon adds. */
.composer-tools .tool-chip .ic { width: 14px; height: 14px; flex: 0 0 auto; }
.composer-tools .tool-chip { gap: 6px; }

/* ===== v138 — and it has to READ as a button ========================================
   Making it button-SHAPED was only half of it. .mode-label still carried the typography it had
   when it was a caption sitting beside the chips — uppercase, letter-spaced, --text-faint, 600
   weight. Wrapped in a pill that renders as a stray heading in a box, not something you press:
   the one word on screen set in small caps is the one word that looks like a label.

   So: sentence case, normal tracking, readable colour. The markup already says "Focus" — only
   the CSS was shouting it. */
.composer-tools .mode-label {
  text-transform: none; letter-spacing: normal; font-weight: 500; color: var(--text-dim);
}
.composer-tools.collapsed .mode-label { color: var(--text); }

/* A chevron, because every other thing in this app that opens says so — the model pill at the top
   of this same screen has one. aria-expanded is already kept in sync by the click handler, so the
   mark can hang off the state that is actually true rather than a second class to remember. */
.composer-tools .mode-label::after {
  content: ""; width: 6px; height: 6px; margin-left: 1px; flex: 0 0 auto;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform .18s ease;
}
.composer-tools .mode-label[aria-expanded="true"]::after {
  transform: translateY(1px) rotate(-135deg);
}

/* 30px tall is right for the look and wrong for a thumb — Apple asks 44. Grow the hit area, not
   the button: an overlay that reaches past the pill on the axis where there is room to spare.
   Inline-flex needs the positioning context declared here, and the chevron is ::after, so this
   is free. */
.composer-tools .mode-label { position: relative; }
.composer-tools .mode-label::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: 44px; transform: translateY(-50%);
}

/* ===== v139 — open, it stops at the last chip ======================================
   The shut row was made to fit its label, but the open one still stretched to the full width of
   the composer below it. On a 1280px screen that is 772px of panel holding 570px of chips —
   202px of empty box hanging off the right end, which reads as a row that failed to fill rather
   than a panel sized to what is in it.

   fit-content, not align-self: .composer-wrap is display:block, so the row is a block-level flex
   CONTAINER, not a flex item — align-self has nothing to talk to and width:auto fills the line.
   (The shut row only shrinks because it also switches to inline-flex.) fit-content resolves to
   max-content capped at what is available, which is exactly the behaviour wanted at both ends:
   it stops at the last chip on a desktop, and on a phone, where the chips need more than one
   line, it still takes the whole width and wraps. */
.composer-tools:not(.collapsed) { width: fit-content; max-width: 100%; }

/* ===== v140 — the drawer is opaque again ===========================================
   Narrow enough and the sidebar stops being docked and becomes `position: fixed` over the chat.
   It was see-through: computed background-color rgba(0,0,0,0), and the only paint on it a 5.5%
   green wash. The scrim dims the page underneath but sits BELOW the drawer (z-index 25 against
   30), so the conversation read straight through the menu — "New chat" and a headache protocol
   sharing the same pixels.

   How it lost its background: the base rule paints with the `background` SHORTHAND, which sets
   background-color to transparent and puts the colour in background-image. The later accent-wash
   rule then set `background-image` on .card/.sidebar/.main — replacing that gradient rather than
   layering over it, and taking the sidebar's only opaque paint with it.

   So: an opaque colour underneath, and both gradients stacked in the order they were meant to be
   — the wash on top of the elevation ramp, which is what the wash rule assumed was still there.

   Specificity has to match `html:not(.light) .sidebar` (0,2,1) or the wash wins on source order
   alone; a bare `.sidebar` inside this query would lose. Dark only, because the light theme never
   hit this — its wash rule is html:not(.light), so the base shorthand still applies there.

   Scoped to the drawer on purpose. Docked, the sidebar overlays nothing and the transparency
   reads as the intended one-material look. */
@media (max-width: 768px) {
  html:not(.light) .sidebar {
    background-color: var(--bg-2);
    background-image:
      linear-gradient(158deg, rgb(var(--accent-rgb) / .055), transparent 62%),
      linear-gradient(180deg, var(--elev), var(--bg-2));
  }
}

/* ===== v141 — and the content pane gets its floor back ==============================
   .main lost its background to the same rule, for the same reason: `background: linear-gradient(
   180deg, var(--bg-2), var(--bg))` put the colour in background-image, and the wash replaced it.
   Nothing showed, because what sits behind .main is the app background — close enough in the dark
   theme that a missing floor looked like a floor. It stops being invisible the moment anything
   scrolls under the pane, which is exactly how the drawer bug surfaced.

   Restored at every width, not just the drawer: .main is the content pane everywhere, and unlike
   the docked sidebar there is no width at which being transparent is the intended look.

   The aura and the grain are unaffected. Both are .main::before/::after at z-index -1 inside the
   stacking context .main itself creates (position:relative, z-index:0), and negative-index
   descendants paint AFTER the element's own background — so they still sit on top of this, which
   is where they were always meant to be. */
html:not(.light) .main {
  background-color: var(--bg);
  background-image:
    linear-gradient(158deg, rgb(var(--accent-rgb) / .055), transparent 62%),
    linear-gradient(180deg, var(--bg-2), var(--bg));
}

/* ===== v142 — cards get their lit frame back =======================================
   The last of the three the wash rule stripped, and the one that lost the most. A card is not a
   box with a border: `border` is 1px solid TRANSPARENT, and the edge light is the third layer of
   the background, clipped to border-box while the body is clipped to padding-box. Replacing
   background-image took all three layers — the sheen, the solid --panel-solid body, and the frame
   — leaving a transparent panel with a literally transparent border. What still read as an edge on
   the Home cards was the wash meeting the pane behind it.

   Written as the `background` shorthand, not background-image: padding-box/border-box are clip and
   origin values, and setting the image alone would restore the layers while dropping the very
   thing that puts one of them in the border. That is a subtler version of the same mistake this
   whole layer exists to undo.

   No background-color, deliberately. The solid is the middle layer as a gradient, because a colour
   is painted to the clip of the LAST layer — border-box here — and would fill the border area,
   burying the frame it is meant to sit inside.

   Dark only: the wash rule is html:not(.light), so light cards kept html.light .card and were
   never touched. */
html:not(.light) .card {
  background:
    linear-gradient(158deg, rgb(var(--accent-rgb) / .055), transparent 62%) padding-box,
    linear-gradient(180deg, rgba(255,255,255,.032), rgba(255,255,255,0) 46%) padding-box,
    linear-gradient(var(--panel-solid), var(--panel-solid)) padding-box,
    linear-gradient(160deg, rgb(var(--accent-rgb) / 0.42), rgba(255,255,255,.07) 34%, rgba(255,255,255,.06) 62%, rgb(21 128 61 / 0.34)) border-box;
}

/* ===== v143 — the Shop looks like a shop ============================================
   It was a spec sheet. Each product got a 46px .p-tile — and the catalog ships photography at
   1000–1200px, so a real product shot was being scaled to favicon size and then object-fit:cover
   cropped it to its centre, which on a white tub is a blank patch. That is why the tiles read as
   empty grey squares. Below 860px `.grid.cols-3` collapsed to ONE column, so a phone got one
   product per full-width row with a blank thumbnail on it.

   The photo is now the card: a square shot at full card width, `contain` so the whole product is
   in frame rather than cropped, on white because that is what the packaging was shot against.

   auto-fill/minmax rather than a fixed column count, so the grid answers to the space it actually
   has — four across in the content pane at 1280, more with the drawer shut or on a wider display,
   without another breakpoint. Phones get an explicit two-up: the pane is ~343px there, under the
   180px minimum twice over, so auto-fill would drop straight back to the single column this is
   meant to fix. */
.mkt-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
@media (max-width: 560px) { .mkt-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } }

.mkt-grid .product { padding: 10px; gap: 3px; }
.p-shot {
  position: relative; width: 100%; aspect-ratio: 1 / 1; margin-bottom: 9px;
  border-radius: 12px; overflow: hidden;
  display: grid; place-items: center;
  background: rgb(var(--accent-rgb) / 0.10); color: var(--green);
}
/* The tint above is the no-photo state, with the type icon on it. A photo covers it completely and
   brings its own white, so the two never mix. */
.p-shot .p-photo { object-fit: contain; padding: 10px; background: #fff; }
.p-shot .ic { width: 26px; height: 26px; }

/* Two lines of name, then the price row pinned to the bottom of the card — grid stretches every
   card in a row to the tallest, and without this the prices sat at ragged heights wherever each
   name happened to stop. */
.mkt-grid .p-name {
  font-size: 0.875rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.mkt-grid .p-brand { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mkt-grid .p-foot { margin-top: auto; padding-top: 8px; }

/* ===== v144 — every control is 44px to a thumb =====================================
   Ten controls on the chat screen alone were under Apple's 44pt minimum, and they were the ones
   people press most: send, attach, mic, the menu, the account button. Measured at 375px —
   .icon-btn 38 (40 in the composer), .avatar 34, .model-pill 35 tall, .msg-action 28, .modal-x 30,
   .r-act 26.

   The hit area grows, not the button. An overlay centred on the control reaches out to 44 without
   moving a pixel of the design, which is the same trick v137 used on the Focus button.

   What this is NOT is a blanket rule, because two of these sit in tight rows and a naive 44px box
   would overlap its neighbour — and an overlapping hit area does not help anyone, it just hands
   one button's edge taps to the other. Both exceptions are handled below, and the result is
   measured: no two hit areas on any screen intersect. */
.icon-btn, .avatar, .model-pill, .msg-action, .r-act { position: relative; }
/* .modal-x is already position:absolute and is its own containing block — do not restyle it. */
.icon-btn::before, .avatar::before, .model-pill::before, .modal-x::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: max(100%, 44px); height: max(100%, 44px);
  transform: translate(-50%, -50%);
}

/* Exception 1 — Copy and Read-aloud sit 4px apart under every reply. 44px wide each would need 8px
   of expansion a side and overlap by 16px. The gap goes to 10 so each can take 5 a side without
   touching, and the full 44 is taken vertically, where there is nothing to collide with. */
.msg-actions { gap: 10px; }
.msg-action::before {
  content: ""; position: absolute; left: -5px; right: -5px; top: 50%;
  height: 44px; transform: translateY(-50%);
}

/* Exception 2 — the marker delete. Its left neighbour is the marker NAME, so expanding evenly
   would put an invisible delete target over the text you were only trying to read. It grows into
   the card's own right padding instead: 4px left (half the 8px gap, so it never reaches the name),
   14px right into padding that holds nothing. 44 wide, 44 tall, no ambiguity about what you hit. */
.r-act::before {
  content: ""; position: absolute; left: -4px; right: -14px; top: 50%;
  height: 44px; transform: translateY(-50%);
}

/* ===== v145 — the recents row, which v144 made worse ================================
   A chat row carries three .r-act buttons — pin, rename, DELETE — 2px apart, inside a container
   held at opacity:0 until :hover. No phone has hover, so on a touch screen they were already
   invisible and still tappable. v144 then gave every .r-act a 44px hit area and turned that into
   three invisible, overlapping delete targets: measured 16px of overlap between each pair, 2px
   reaching over the chat title, and — because a row is 36px on a 38px pitch — 3px bleeding into
   the rows above and below, so the top edge of one row's Delete sat inside its neighbour's.
   Deleting a chat has no undo. That is the worst possible thing to make easier to hit by accident.

   Three separate mistakes, so three fixes.

   1. Visible wherever hover does not exist. The @media (hover: none) block above already does
      exactly this for .msg-actions, for exactly this reason, and the note there says so — it was
      simply never applied to the row that also needs it.

   2. Room to be 44. The row was 36 tall, so no honest 44px target fit inside it; the primary
      control, the chat title itself, was equally undersized. The row grows on touch instead of the
      hit areas silently overflowing it.

   3. Space between them. 2px gaps cannot hold three 44px targets, so the gap opens to 6 and each
      button takes 3 a side — meeting exactly, never overlapping — with the container's own left
      padding keeping the first one off the title. */
@media (hover: none) {
  .recent-actions { opacity: 1; }
  .recent { min-height: 44px; }
  .recent-main { min-height: 44px; }
}
.recent-actions { gap: 6px; padding-left: 4px; }
/* Default (a real pointer): the row is 36 tall, so the hit area stays inside the button rather
   than spilling into the rows on either side. A mouse does not need the slack. */
.recent-actions .r-act::before {
  left: -3px; right: -3px; top: 50%; height: 100%;
  transform: translateY(-50%); width: auto;
}
/* 44 only where the row grew to hold it: 46 tall on a 48px pitch, so this clears its neighbours by
   2px. The bleed that put one row's Delete inside the row above is exactly what constraining the
   height above prevents.
   AFTER the rule it overrides, not inside the block above — a media query adds no specificity, so
   written first it loses to the plain `height: 100%` on source order alone. It did, and the
   measurement said 26px until it was moved here. */
@media (hover: none) {
  .recent-actions .r-act::before { height: 44px; }
}

/* ===== v146 — the lab panel's own date ==============================================
   Uploads were filed as today, so an older panel landed after readings that came before it and
   inverted the trend the chat, the insights and the Twin all read from. The date is now shown and
   editable at the point of saving, because an extracted date is a guess and this is the one moment
   the person holding the paper can check it. */
.lab-date {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 12px; font-size: 0.8125rem; color: var(--text-dim);
}
.lab-date input {
  font: inherit; font-size: 0.875rem; color: var(--text);
  background: var(--elev); border: 1px solid var(--hairline); border-radius: 10px;
  padding: 8px 10px; min-height: 44px;      /* a date field is a tap target like any other */
}
.lab-date input:focus { outline: none; border-color: var(--green); }
.lab-date-why { margin: 6px 0 0; font-size: 0.75rem; }

/* ===== v147 — the extraction review is editable =====================================
   Extraction is OCR plus a model: it transposes a digit, drops a unit, reads "VO₂ max" where every
   other path in this app writes "VO2max". The card rendered all of it as plain text, so one wrong
   character left exactly one button that helped — Discard — and the panel had to be re-uploaded or
   typed in by hand. The values are inputs now, and each row can be dropped without dropping the
   rest.

   Two lines per row on purpose. Name, value and unit side by side inside a card on a 375px screen
   leaves each field too narrow to read what is in it, and the name is the long one. So the name
   takes a full line with its checkbox, and value/unit share the line below — where value gets the
   room because it is the number someone is actually checking. */
.lab-rows { display: flex; flex-direction: column; gap: 8px; margin: 4px 0 2px; }
.lab-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
  padding: 8px; border: 1px solid var(--hairline); border-radius: 12px; background: var(--bg-2);
}
.lab-keep-l { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; }
.lab-keep { width: 18px; height: 18px; accent-color: var(--green); flex: 0 0 auto; cursor: pointer; }
.lab-in {
  font: inherit; font-size: 0.875rem; color: var(--text); min-width: 0;
  background: var(--elev); border: 1px solid var(--hairline); border-radius: 9px;
  padding: 9px 10px; min-height: 44px;      /* an input is a tap target too */
}
.lab-in:focus { outline: none; border-color: var(--green); }
.lab-name { flex: 1 1 auto; font-weight: 600; }
.lab-unit { color: var(--text-dim); }
.lab-in::placeholder { color: var(--text-faint); }
/* An unticked row stays legible — it is still information, it is just not being saved. */
.lab-row:has(.lab-keep:not(:checked)) { opacity: .5; }
.lab-row:has(.lab-keep:not(:checked)) .lab-in { text-decoration: line-through; }

/* ===== v148 — text buttons clear 44 too ============================================
   v144 gave the icon controls 44px hit areas and left the text buttons alone: .btn measured 43 —
   one pixel short — and .btn-sm 36. Home's empty cards now put .btn-sm in front of a brand-new
   account as the only thing to press on the screen, so shipping it at 36 would have been adding a
   control that fails the rule set two builds ago.

   Grown rather than overlaid, which is the opposite of v144 and deliberate. An overlay was right
   for icon buttons because their size IS the design; a text button has padding to spare and
   growing it changes nothing anyone would notice. It also has to be grown here, because these come
   in adjacent pairs — Cancel next to Save & resend, the three Apple Health buttons — and reaching
   past the box would put each one's hit area inside its neighbour's, the exact mistake v145 had to
   undo. A taller box respects the gap between them; an overlay does not. */
.btn { min-height: 44px; }
.btn-sm { min-height: 44px; }

/* ===== v149 — say what resending costs =============================================
   Editing a message drops everything after it, which is correct — a reply to a question you have
   just rewritten no longer follows from it. It was silent, though, and the pencil is on every user
   message, so rewriting the second question in a long thread discarded every answer after it with
   no warning. This is the line that says how many, sitting between the box and the button so it is
   read on the way to pressing it. */
.edit-warn {
  margin: 8px 0 0; font-size: 0.78125rem; color: var(--text-dim);
  padding-left: 10px; border-left: 2px solid rgb(var(--accent-rgb) / .45);
}

/* ===== v150 — animations stop asking for layout ====================================
   The choppiness is not the JavaScript. A route change measured 3–11ms and the page runs 11
   animations at rest, all of them transform/opacity, which the compositor handles for free. What
   was expensive is a handful of transitions that animate LAYOUT properties — every frame of those
   makes the browser re-measure the page before it can paint, and no amount of GPU helps.

   1. The primary button's sheen. It swept by animating `left` from -70% to 130% over half a
      second, on ::after of .btn.primary — the most-pressed thing in the app. Worse on a phone than
      a desktop, because :hover STICKS after a tap, so every tap of Send, Save, Upgrade or "Add to
      my data" ran a 500ms layout animation. Same sweep, done with translateX: the percentages
      change because translateX is a share of the ELEMENT (44% wide) where left was a share of the
      PARENT — -70/44 ≈ -159%, 130/44 ≈ 295% — and the skew rides along in the same transform.

   2. The protocol progress bar animated `width`. scaleX from the left edge is the same picture and
      costs nothing; the inline width the markup sets becomes the scale factor via a custom
      property, so app.js keeps setting one number. */
.btn.primary::after {
  left: 0; transform: translateX(-159%) skewX(-18deg);
  transition: transform .5s var(--ease);
}
.btn.primary:hover::after { left: 0; transform: translateX(295%) skewX(-18deg); }

.proto-bar i {
  width: 100% !important;            /* the markup's inline width becomes the scale below */
  transform-origin: left center;
  transform: scaleX(var(--fill, 0));
  transition: transform .4s var(--ease);
}

/* A tap is not a hover. Touch browsers leave :hover applied after a tap until you touch elsewhere,
   so every sheen and lift stayed on screen and re-ran on the next press. Hover effects belong to
   devices that actually have a pointer. */
@media (hover: none) {
  .btn.primary::after { transition: none; }
  .btn.primary:hover::after { transform: translateX(-159%) skewX(-18deg); }
}

/* ===== v151 — the Health card can say when it last synced ==========================
   Its status dot was hardcoded `on` — green from the moment the page rendered, on every platform,
   whether or not Health had ever been connected — sitting beside an Oura card whose dot stays grey
   until it genuinely is. The dot now follows healthSynced, and this is the timestamp beside it. */
.conn-when { margin-left: auto; font-size: 0.72rem; color: var(--text-faint); white-space: nowrap; }

/* ===== v152 — the Pro mark on a connection card ====================================
   Wearables are Pro now, and the card says so before you press anything. It sits where the
   "synced 3h ago" line sits for someone who has it, because they are the same fact answered two
   ways: whether this connection is yours to use. */
.conn-pro {
  margin-left: auto; font-size: 0.66rem; font-weight: 700; letter-spacing: .4px;
  text-transform: uppercase; color: var(--green-2); white-space: nowrap;
  background: rgb(var(--accent-rgb) / 0.12); border: 1px solid var(--outline);
  border-radius: 999px; padding: 3px 9px;
}

/* ===== v153 — Protocols is reachable ===============================================
   It was a full route with a page behind it and no entry in either nav surface. Its only doors
   were a Home card that renders once a plan EXISTS, and a settings pane — so a Pro user who had
   never made a protocol needed a plan in order to find the page that creates plans. The owner
   could not find it either, which is how this came up.

   It joins the sidebar list. On a phone that list is hidden wholesale by v135, and v135's reason
   was that the drawer repeated the five items already in the tab bar — "the drawer keeps only what
   nothing else offers". Protocols is exactly that: it is in no tab bar. So the rule becomes what it
   always meant — hide the DUPLICATES, not the list — and Protocols is the one item that stays.

   Hidden per item rather than by rebuilding the list, because updateNav() and the click wiring walk
   .nav-item, and display:none also takes them out of the tab order. Nothing in the script has to
   know about the breakpoint, which was v135's other reason. */
@media (max-width: 900px) {
  .sidebar .nav { display: flex; }
  .sidebar .nav .nav-item[data-route="home"],
  .sidebar .nav .nav-item[data-route="chat"],
  .sidebar .nav .nav-item[data-route="twin"],
  .sidebar .nav .nav-item[data-route="data"],
  .sidebar .nav .nav-item[data-route="marketplace"] { display: none; }
}

/* ===== v154 — Protocols moved to the tab bar, so the drawer stops carrying it =======
   v153 put Protocols in the drawer because it was in no tab bar. It is in the tab bar now, which
   makes the drawer copy exactly the duplicate v135 removed — two places to tap for the same
   destination, one of them two taps deep. So the drawer goes back to carrying none of them, and
   v135's rule stands unchanged: it keeps only what nothing else offers. */
@media (max-width: 900px) {
  .sidebar .nav { display: none; }
}

/* ===== v155 — six tabs, and the label still says what the page is ==================
   Protocols joins the tab bar, so each slot goes from 75px to 52 on a 375px screen — 48 of it
   usable. "Protocols" measures 49 at the old size and clipped by a pixel. Shortening it to "Plan"
   would have fitted and quietly renamed the feature in the one place people look for it, so the
   type gives way instead: a hair smaller, a hair tighter, across all six so they stay a set. */
@media (max-width: 420px) {
  .tabbar .tab-item span { font-size: 0.6rem; letter-spacing: -0.1px; }
}

/* ===== v156 — the composer stops eating the conversation ===========================
   Measured on a 375x812 phone: topbar 65 + tab bar 63 + composer block 178 = 306px, 38% of the
   screen, before a single word of the conversation. With the keyboard up that leaves roughly 206px
   — about three lines of a reply visible while you type a follow-up about it.

   The disclaimer stays; it is compliance, not decoration. The padding is the discretionary part:
   26px above a 59px input and 18px below, on a surface where vertical space is the scarcest thing
   there is. 14/12 keeps the composer clearly separated from the last message without spending a
   fifth of the block on air. Phones only — a desktop has room to breathe and the wider padding
   reads better there. */
@media (max-width: 900px) {
  .composer-wrap { padding: 14px 0 12px; }
}

/* ===== v157 — the drawer holds every destination, and the chats =====================
   The owner's call, and it reverses v135 and v154 knowingly: "that's the main reason I made it, to
   hold those and the chats the user has."

   v135 hid this list on phones because it repeated the tab bar exactly — five items, twice, one of
   them two taps deep. That was true then. The drawer now carries something the tab bar cannot: the
   full destination list AND the recent conversations, in one place you reach from anywhere. The
   duplication is the point rather than an oversight, which is the difference between this and what
   v135 removed — a menu you can open to see everything, over a bar showing where you can go next.

   Home is here and not in the tab bar. It was removed from the bar as unnecessary, but the page
   still exists and still holds Today's needs, your stats and recommendations, so it keeps a door. */
@media (max-width: 900px) {
  .sidebar .nav { display: flex; }
}

/* v153 hid the five tab-bar duplicates item by item, and those rules outrank the one above by
   specificity — so restoring `.nav` alone left the drawer showing Protocols and nothing else.
   Same selectors, later in the file, putting them back. This is what append-only costs: a rule
   is undone by naming it again, not by deleting it. */
@media (max-width: 900px) {
  .sidebar .nav .nav-item[data-route="home"],
  .sidebar .nav .nav-item[data-route="chat"],
  .sidebar .nav .nav-item[data-route="twin"],
  .sidebar .nav .nav-item[data-route="data"],
  .sidebar .nav .nav-item[data-route="marketplace"] { display: flex; }
}

/* ===== v158 — the bottom bar is gone; the drawer is the navigation =================
   Asked for, and it follows from the drawer holding every destination: two navigations for the
   same six places, one of them always on screen, was the thing that made the bar worth removing.

   What the bar quietly did besides navigate was reserve the home indicator —
   env(safe-area-inset-bottom) lived on .tabbar. Delete it and the composer sits under the swipe
   bar on every iPhone since the X. The composer claims that space now, so the input is still
   reachable and the send button is not fighting a system gesture.

   The screen it gives back is the point: 63px of a 812px phone, on the surface where vertical
   space was already the scarcest thing. */
@media (max-width: 900px) {
  .composer-wrap { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
  /* The pages without a composer need it too, or their last row sits under the indicator. */
  .page { padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px)); }
}

/* ===== v159 — marker cards, much smaller ===========================================
   171px each, four to a phone screen, for one number. The breakdown was 26px of name, 53px of
   value, 15px of "Trend builds as your values change", 27px of goal button and 28px of padding —
   so more than half the card was scaffolding around a two-digit reading, and a panel of twelve
   markers was three screens of scrolling.

   The value moves up beside nothing and the goal moves out beside IT, so the two rows carry four
   things instead of one each. The trend placeholder goes entirely: it appears only when there is
   no trend, which is exactly when it has nothing to say, and it said the same sentence on every
   card. A real sparkline still spans the full width when there is one to draw. */
.mk-card {
  display: grid; grid-template-columns: 1fr auto; column-gap: 10px; row-gap: 2px;
  padding: 10px 12px;
}
.mk-top { grid-column: 1 / -1; }
.mk-val { grid-column: 1; margin: 0; font-size: 1.15rem; align-self: center; }
.mk-goal, .mk-goal-set { grid-column: 2; justify-self: end; align-self: center; margin: 0; }
.mk-trend-empty { display: none; }
/* Anything with a shape to it — the sparkline, a real trend row — still gets the full width. */
.mk-spark, .mk-trend, .mk-hist { grid-column: 1 / -1; }

/* The Trending chip reads as a different KIND of filter, because it is: the others narrow by
   category, this one ranks the whole catalog. A warm tint separates it from the four that are
   simply "which shelf". */
.mkt-chip.hot { border-color: rgb(251 146 60 / .38); color: #fbbf6c; }
.mkt-chip.hot.active { background: rgb(251 146 60 / .12); border-color: rgb(251 146 60 / .6); color: #fdba74; }

/* ===== v161 — the Twin stops being three forms ======================================
   The page opened with every input and no output: six suggestion chips, a what-if form, a data
   drawer and a second full form for "Reach a goal" — all of the asking above the fold and none of
   the answering. The result rendered below the data drawer, so on a phone a finished projection
   appeared off-screen, under a collapsed section nobody had opened, and reading as nothing having
   happened at all. */

/* One line, swipeable. Three chips still wrapped onto three rows at 375px — the labels are whole
   sentences — so the row scrolls sideways instead of stacking, with the last chip clipped just
   enough to read as "there is more this way". */
.scenario-chips {
  flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
  -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity;
  margin-left: -2px; padding: 2px;                 /* room for the focus ring at the edges */
}
.scenario-chips::-webkit-scrollbar { display: none; }
.scenario-chip { flex: 0 0 auto; white-space: nowrap; scroll-snap-align: start; }

/* The basis line: how many markers a projection is standing on, next to the button that runs it. */
.twin-basis {
  margin: 16px 0 0; font-size: 0.78rem; line-height: 1.45; color: var(--text-dim);
}
.twin-basis b, .twin-basis #twinFilledInline { color: var(--text); font-weight: 700; }
/* 44px of thumb on an 18px line of text, bought with a pseudo-element so the paragraph does not
   grow and nothing below it moves. Deliberately NOT centred: the band is pushed downward, into the
   36px of dead space above the data drawer, because centring it would put the top edge inside the
   Run button. That is exactly the mistake v144 made and v145 had to undo. */
.twin-basis-edit::after {
  content: ""; position: absolute; left: -10px; right: -10px; top: -8px; bottom: -18px;
}
.twin-basis-edit {
  font: inherit; background: none; border: none; padding: 0; cursor: pointer; position: relative;
  color: var(--green-2); text-decoration: underline; text-underline-offset: 2px;
}
.twin-basis-edit:hover { color: var(--text); }

/* Reach a goal, collapsed — the same summary treatment the data drawer already uses, so the page
   reads as one thing with two drawers under it rather than three competing panels. */
.twin-reach { margin-top: 22px; }
.twin-reach > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  padding: 12px 14px; min-height: 44px;
  border: 1px solid var(--hairline); border-radius: 14px; background: var(--bg-2);
  font-size: 0.9375rem;
}
.twin-reach > summary::-webkit-details-marker { display: none; }
.twin-reach > summary::after {
  content: ""; margin-left: auto; width: 7px; height: 7px; flex: 0 0 auto;
  border-right: 1.5px solid var(--text-dim); border-bottom: 1.5px solid var(--text-dim);
  transform: translateY(-2px) rotate(45deg); transition: transform .18s var(--ease);
}
.twin-reach[open] > summary::after { transform: translateY(1px) rotate(-135deg); }
.twin-reach > summary:hover { border-color: var(--outline); }
.twin-reach[open] > summary { margin-bottom: 12px; }

/* ===== v162 — a third pricing column ===============================================
   The table was built for two configurations and now carries three, so the feature column has to
   give up width and the numbers have to stop being headlines. Free is no longer the default path
   for new sign-ups, so it reads as the quiet column and Pro keeps the emphasis. */
.ch-table-3 th, .ch-table-3 td { padding-left: 8px; padding-right: 8px; }
.ch-table-3 th:first-child, .ch-table-3 td:first-child { width: 42%; }
.ch-table-3 th:not(:first-child), .ch-table-3 td:not(:first-child) {
  width: 19.33%; text-align: center; white-space: nowrap;
}
.ch-table-3 .ch-price { font-size: 1.05rem; }
.ch-table-3 .ch-price i { font-size: 0.68em; font-style: normal; opacity: .72; }
.ch-table-3 .lp-btn { width: 100%; margin-top: 8px; font-size: 0.8125rem; padding: 8px 10px; }
/* Said instead of a button that would not work. Anyone who already has a free account keeps it —
   this line is about new sign-ups, and a disabled button would not say that. */
.ch-closed {
  display: block; margin-top: 8px; font-size: 0.72rem; line-height: 1.3;
  color: var(--text-dim); opacity: .8;
}
@media (max-width: 720px) {
  /* Four columns do not fit a phone. Let the table scroll sideways inside its own box rather than
     shrinking the type to the point where nobody reads the prices at all. */
  .lp-pricing { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .ch-table-3 { min-width: 560px; }
}

/* ===== v163 — the date field stops looking broken ==================================
   <input type="date"> with no value renders as a COMPLETELY EMPTY BOX on iOS. Desktop draws
   "mm/dd/yyyy"; WebKit on a phone draws nothing — so the first thing a new user meets, on the
   screen that decides whether they can use Hale at all, is a field that reads as a bug. It cannot
   take a placeholder, and ::before on a date input is not dependable in WebKit, so the hint is a
   real element behind the input, switched off by JS the moment a value exists. */
.dob-wrap { position: relative; }
.dob-wrap > .modal-input { position: relative; z-index: 1; background: transparent; }
.dob-hint {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  font-size: 0.9375rem; color: var(--text-dim); opacity: .55;
  pointer-events: none; letter-spacing: .04em; z-index: 0;
}
.dob-wrap.has-value > .dob-hint { display: none; }
/* WebKit hides its own date text behind our hint otherwise — once a value exists the input paints
   normally, and before that the field is visually ours. */
.dob-wrap > .modal-input::-webkit-date-and-time-value { text-align: left; }
.dob-wrap > .modal-input::-webkit-calendar-picker-indicator {
  opacity: .55; filter: invert(1) brightness(1.4);   /* the native glyph, made visible on dark */
  cursor: pointer;
}
html.light .dob-wrap > .modal-input::-webkit-calendar-picker-indicator { filter: none; }

/* ===== v164 — the paywall, and a table with one column fewer =======================
   There is no free plan any more, so the landing table carries two configurations instead of
   three, and a signed-in account with no subscription meets a sheet that sells both rather than a
   screen full of failed requests. */
.ch-table-3 th:first-child, .ch-table-3 td:first-child { width: 46%; }
.ch-table-3 th:not(:first-child), .ch-table-3 td:not(:first-child) { width: 27%; }
@media (max-width: 720px) { .ch-table-3 { min-width: 460px; } }

/* The sheet. No ✕ and no click-outside: once the tier is closed there is nothing behind it, so a
   dismiss control would only return someone to a screen that cannot answer them. Sign out is the
   deliberate way back, and it is spelled out rather than implied. */
.modal.pw { max-width: 460px; }
.pw-plans { display: grid; gap: 10px; margin: 4px 0 10px; }
.pw-plan {
  position: relative; display: grid; gap: 3px; width: 100%; text-align: left;
  padding: 16px 18px 15px; border-radius: 16px; cursor: pointer;
  border: 1px solid var(--outline); background: var(--panel-solid);
  font: inherit; color: var(--text); transition: border-color .15s var(--ease), transform .12s var(--ease);
}
.pw-plan:hover { border-color: var(--green); }
.pw-plan:active { transform: scale(.995); }
.pw-plan:disabled { opacity: .55; cursor: default; }
.pw-plan.best { border-color: var(--green-2); }
.pw-badge {
  position: absolute; top: -8px; right: 14px; padding: 2px 8px; border-radius: 999px;
  background: var(--green); color: #08120b; font-size: 0.62rem; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase;
}
.pw-name { font-size: 0.8125rem; color: var(--text-dim); letter-spacing: .04em; text-transform: uppercase; }
.pw-price { font-size: 1.55rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.1; }
.pw-price i { font-size: 0.5em; font-style: normal; font-weight: 600; opacity: .7; margin-left: 2px; }
.pw-line { font-size: 0.8125rem; line-height: 1.45; color: var(--text-dim); margin-top: 2px; }

/* ===== v165 — the app stops looking assembled ======================================
   Ten findings from a measured audit at 375x812. Everything here is additive and none of it
   touches the palette: the colours were never the problem, the missing state machinery was. */

/* --- 1. Disabled meant nothing. There was no :disabled rule in this file, so the consent gate's
   only button rendered as a full-strength green gradient with a 26px glow while being dead to the
   touch. Users tap the brightest thing; nothing happened, with no shake, hint or reason, on an
   inescapable first-run screen. Proven by cloning the button and flipping the attribute: the
   computed styles came back byte-identical. */
.btn:disabled, .btn[disabled] {
  background: var(--elev-2); background-image: none;
  color: var(--text-faint); box-shadow: none; cursor: not-allowed;
  transform: none; filter: none;
}
.btn.primary:disabled::after, .btn.primary[disabled]::after { display: none; }   /* kill the sheen sweep */
.btn:disabled:hover, .btn[disabled]:hover { transform: none; filter: none; box-shadow: none; }

/* --- 2. Nothing green answered a tap. The one universal press state, scale(.98), is cancelled on
   .btn.primary by a later and more specific rule, so every CTA in the product — Create account,
   Agree & continue, Run simulation, Generate protocols — was visually inert until the network
   replied. On a phone that reads as a missed tap, and people press again. */
.btn.primary:active:not(:disabled) { transform: scale(.98); }
.card:active, .mk-card:active, .product:active, .conn-card:active,
.icon-btn:active, .mkt-chip:active, .scenario-chip:active, .pw-plan:active {
  transform: scale(.985);
}
/* The inverse: a tap on a touch screen leaves :hover latched, so cards stayed lit after the finger
   left and the screen slowly filled with things that looked selected. */
@media (hover: none) {
  .card:hover, .mk-card:hover, .product:hover, .conn-card:hover,
  .recent:hover, .nav-item:hover, .pw-plan:hover {
    transform: none; border-color: var(--hairline); box-shadow: none;
  }
}

/* --- 3. Chat read as one voice. Rendered at 375px, a question and an answer computed to the same
   transparent background, the same 4px 0 0 padding, the same 0 radius, the same 15px and the same
   colour, in the same left-aligned column — separable only by a 32px avatar tint. Scrolling a
   thread you could not see where your turn ended and Hale's began without stopping to read.
   Markup is untouched; this is alignment and a bubble. */
.msg.user { justify-content: flex-end; }
.msg.user .m-avatar { display: none; }
.msg.user .m-col { align-items: flex-end; max-width: 86%; margin-left: auto; }
.msg.user .m-body {
  background: var(--elev); border: 1px solid var(--hairline);
  border-radius: 16px 16px 4px 16px; padding: 10px 14px; text-align: left;
}
.msg.user .msg-actions { justify-content: flex-end; }

/* --- 4. The consent checkbox was the raw browser control: an unstyled native box on a near-black
   card, painting in the UA's light default as a solid white square. It was the highest-contrast
   rectangle on the screen and the only element in the product belonging to no design system. */
.agree-row input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; flex: 0 0 auto;
  width: 22px; height: 22px; border-radius: 7px; margin: 0;
  border: 1.5px solid var(--outline); background: var(--bg-2);
  display: grid; place-items: center; cursor: pointer;
  transition: background .14s var(--ease), border-color .14s var(--ease);
}
.agree-row input[type="checkbox"]:checked { background: var(--green); border-color: var(--green); }
.agree-row input[type="checkbox"]:checked::after {
  content: ""; width: 5px; height: 9px; margin-top: -2px;
  border: solid #08120b; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.agree-row input[type="checkbox"]:focus-visible { outline: 2px solid var(--green-2); outline-offset: 2px; }

/* --- 5. Progress was painted in the error colour. "Working…", "Sending…" and "Updating…" all
   rendered salmon-red above the button, so for the whole network call the screen said, in the
   app's failure colour, that something had gone wrong. One element carried three meanings. */
.modal-status {
  font-size: 0.78125rem; text-align: left; margin: 2px 2px 6px;
  min-height: 1.15em; color: var(--text-dim); line-height: 1.4;
}
.modal-status.err { color: #f87171; }
.modal-status.ok { color: var(--green-2); }

/* --- 6. Loading, error, empty and success were the same grey line with no motion, so a failed run
   was indistinguishable from a running one. Give waiting a pulse and failure a colour. */
.state-msg { color: var(--text-dim); padding: 4px 0; line-height: 1.5; }
.state-msg.err { color: #f87171; }
.state-msg.ok { color: var(--green-2); }
.state-msg.busy::after {
  content: ""; display: inline-block; width: 6px; height: 6px; margin-left: 8px;
  border-radius: 50%; background: currentColor; vertical-align: middle;
  animation: statePulse 1.1s ease-in-out infinite;
}
@keyframes statePulse { 0%, 100% { opacity: .25; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .state-msg.busy::after { animation: none; opacity: .6; } }

/* --- 7. Eight control classes shipped under 44px. v144 and v148 declared "every control is 44px
   to a thumb" against an enumerated list, and everything added since was missed — including the
   Twin's week steppers at 26x26, which are the ONLY way to set the projection horizon. */
.mkt-chip, .scenario-chip, .reach-select, .reach-target { min-height: 44px; }
.wk-step {
  position: relative;   /* the button stays 26px; the target grows around it */
}
.wk-step::after { content: ""; position: absolute; inset: -9px -6px; }
.lp-foot-links a, .modal-fine a { position: relative; }
.lp-foot-links a::after, .modal-fine a::after { content: ""; position: absolute; inset: -13px -6px; }

/* --- 8. Two flex rows collapsed at phone width. "Clear all" snapped to two lines inside a
   near-circular pill and stole width from its own heading, leaving one card carrying two different
   text measures. */
.mem-head > button { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 560px) {
  .mem-head { flex-wrap: wrap; row-gap: 10px; }
  .mem-head > div { flex: 1 1 100%; order: 2; }
}

/* --- 9. MY REGRESSION, four hours old. v163 fixed the empty date field on iOS by putting a real
   "MM / DD / YYYY" element behind the input, because WebKit paints nothing there. Chrome DOES
   paint its own "mm/dd/yyyy" — so on the Android shell and on allhale.app in mobile Chrome the two
   strings rendered on top of each other at different sizes and letter-spacings, and the field read
   as garbled. Fixing one engine broke the other. Hide the native edit region while the field is
   empty so exactly one hint ever paints, and restore it the moment there is a value. */
.dob-wrap > .modal-input:not([disabled])::-webkit-datetime-edit { opacity: 0; }
.dob-wrap.has-value > .modal-input::-webkit-datetime-edit { opacity: 1; }

/* --- 10. The launch gate's primary CTA was bisected by the card edge: 873px of content in a 689px
   card, the button's top at 748 against a card bottom of 771, so 23px of a 43px button was visible
   with no scroll cue and the scrollbar hidden by the shell. This is the app's only entry screen.
   The tightening block existed but fired below 700px, which no modern phone is. */
@media (max-height: 880px) {
  html[data-app="1"] #authgate .modal-mark { display: none; }
  html[data-app="1"] #authgate .modal-title { font-size: 1.15rem; margin-bottom: 2px; }
  html[data-app="1"] #authgate .modal-sub { font-size: 0.8rem; margin-bottom: 8px; }
  html[data-app="1"] #authgate .modal-input { padding-top: 8px; padding-bottom: 8px; }
  html[data-app="1"] #authgate .oauth-btn { padding-top: 8px; padding-bottom: 8px; }
  html[data-app="1"] #authgate .gate-guest { margin-top: 8px; padding-top: 8px; }
}
/* And say there is more, the way .consent-doc already does — a scroll cue is not optional when the
   platform hides the scrollbar. */
#authgate .modal {
  -webkit-mask-image: linear-gradient(180deg, #000 calc(100% - 22px), transparent);
  mask-image: linear-gradient(180deg, #000 calc(100% - 22px), transparent);
}
#authgate .modal.scrolled-end { -webkit-mask-image: none; mask-image: none; }

/* ===== v166 — the drawer stops sliding under the status bar =========================
   Reported from a phone: with the drawer open, the Hale wordmark sits behind the clock and the
   "◀ TestFlight" return banner. On a phone the drawer is `position: fixed; top: 0` (styles.css:624)
   with a flat 14px of padding, so its first row begins 14px from the physical top of the display —
   above the status bar, not below it. .topbar has carried env(safe-area-inset-top) since v132; the
   drawer was simply never given the same treatment, and it is the one panel that spans the full
   height of the screen. The bottom inset matters for the same reason: the drawer runs to bottom: 0,
   so Settings and Sign out sat in the home-indicator's swipe strip. */
@media (max-width: 899px) {
  .sidebar {
    padding-top: calc(14px + env(safe-area-inset-top, 0px));
    padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  }
}

/* ===== v167 — Settings stops looking like a form someone gave up on ================
   Measured on a phone: six tabs wrapping onto THREE ragged rows, which is the single thing that
   makes this screen read as unfinished. Everything else in the app that has too many chips for one
   line scrolls sideways — the Twin's scenarios do, the Shop's filters do — so this should too,
   rather than being the one place that stacks. */
@media (max-width: 700px) {
  .set-nav {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity;
    gap: 4px; padding: 0 0 10px; margin: 0 -2px;
  }
  .set-nav::-webkit-scrollbar { display: none; }
  /* inline-flex + centred, not just a min-height: as a plain block the button's height is its
     line box and min-height was measuring 39.2px in practice. This makes 44 structural, which is
     the number v144 set for anything a thumb has to hit. */
  .set-nav button {
    flex: 0 0 auto; white-space: nowrap; scroll-snap-align: start;
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 44px; padding: 0 15px; border-radius: 999px;
  }
  /* The active tab has to be findable in a row you can only see part of. */
  .set-nav button.active { background: rgb(var(--accent-rgb) / 0.16); }
}
/* The section labels sat on the group's top border like a mis-drawn fieldset legend. Give them
   room and a consistent rhythm so the page reads as sections rather than boxes. */
.set-group > .set-label { margin-bottom: 8px; letter-spacing: .06em; }
.set-group + .set-group { margin-top: 4px; }

/* ===== v168 — two plans as tabs, with names of their own =============================
   Core used to be a ghost link beneath Pro, which is how you present a consolation prize rather
   than a plan you want bought. Two cards side by side now, Pro LEFT and Core RIGHT, each carrying
   a monogram so they read as named products rather than two prices in a list. */
.plan-tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 6px 0 10px;
  align-items: stretch;
}
.plan-tabs:has(> :only-child) { grid-template-columns: 1fr; }   /* Core unavailable → Pro alone */

.plan-tab {
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  padding: 16px 14px 15px; border-radius: 16px; cursor: pointer; text-align: left;
  border: 1px solid var(--outline); background: var(--panel-solid);
  font: inherit; color: var(--text); min-height: 44px;
  transition: border-color .15s var(--ease), transform .12s var(--ease), background .15s var(--ease);
}
.plan-tab.static { cursor: default; }
.plan-tab:hover:not(.static):not(:disabled) { border-color: var(--green); }
.plan-tab:active:not(.static):not(:disabled) { transform: scale(.985); }
.plan-tab:disabled { opacity: .5; cursor: not-allowed; }
.plan-tab:focus-visible { outline: 2px solid var(--green-2); outline-offset: 2px; }

/* Pro is the recommendation, so it carries the accent; Core is a peer, not a downgrade, so it gets
   the same card and a quieter monogram rather than a smaller box. */
.plan-tab.is-pro { border-color: rgb(var(--accent-rgb) / .45); background: rgb(var(--accent-rgb) / .06); }

.plan-mono {
  display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px;
  font-family: var(--font); font-weight: 800; font-size: 0.8125rem; letter-spacing: .02em;
  margin-bottom: 2px;
}
.is-pro .plan-mono { background: var(--green); color: #08120b; }
.is-core .plan-mono { background: var(--elev-2); color: var(--green-2); border: 1px solid var(--outline); }

.plan-name { font-size: 0.9375rem; font-weight: 700; letter-spacing: -.01em; }
.plan-cost { font-size: 1.25rem; font-weight: 800; letter-spacing: -.02em; line-height: 1.15; }
.plan-cost i { font-size: 0.52em; font-style: normal; font-weight: 600; opacity: .7; margin-left: 1px; }
.plan-note { font-size: 0.72rem; line-height: 1.4; color: var(--text-dim); }

/* Below ~340px two columns stop being readable — one above the other, Pro still first. */
@media (max-width: 360px) {
  .plan-tabs { grid-template-columns: 1fr; }
}

/* ===== v169 — Settings reads as sections, not crammed boxes ========================
   .set-group is swept up by the shared card rule (`.card, .set-group, .twin-list, .conn-card`),
   so every section is a bordered, shadowed panel — but it was never given the padding a panel
   needs, and .set-label is its FIRST CHILD. The result is a heading pressed against the top border
   like a mis-drawn fieldset legend, with the inputs touching the sides. That, plus six tabs on
   three rows (fixed in v167), is what made this screen read as unfinished.

   The label now sits ABOVE the panel rather than inside it — the arrangement iOS Settings and
   every grouped-list app uses — by lifting it out with a negative offset. No markup change. */
.set-group {
  padding: 14px;
  border-radius: 14px;
  margin-bottom: 26px;
  overflow: visible;                     /* the lifted label must not be clipped */
}
.set-group > .set-label {
  position: relative;
  top: -26px; margin-bottom: -18px;      /* out of the box, into the gap above it */
  padding-left: 2px;
  font-size: 0.6875rem; letter-spacing: .07em;
  color: var(--text-dim);
}
/* The first group would otherwise lift its label into the heading above it. The container is
   #setPane / .set-pane — .set-body was a guess and matched nothing. */
.set-pane > .set-group:first-of-type,
.set-pane > h2 + .set-group,
.set-pane > .acct-head + .set-group { margin-top: 30px; }

/* Rows inside a padded panel no longer need their own side padding, and the last row's rule was
   drawing a line 14px short of the panel edge. */
.set-group > .set-row:last-child { border-bottom: none; padding-bottom: 2px; }
.set-group > .set-row:first-of-type { padding-top: 2px; }
.set-hint { margin: -2px 0 10px; }

/* The plan row carries a price and an action, which is the one row people come here to act on.
   Give it room and stop the button being squeezed by a long plan description. */
.set-row > .btn.btn-sm { flex: 0 0 auto; white-space: nowrap; }
.set-row small { line-height: 1.45; }

/* ===== v170 — the sweep: things that were clipped, invisible, or lying ==============
   A visual audit at 375px (the size almost everyone actually uses) plus a pass over the
   light theme, which had drifted because every recent layer was written and checked in
   dark. Grouped by what a person sees go wrong, not by file order. */

/* --- 1. The install banner rendered a 150px Hale "H" ------------------------------
   .vb-ic only ever sized `.ic`, which is the class I.info carries. The install banner is
   the one banner that passes I.leaf, whose class is `hlogo` — and `hlogo` is styled
   nowhere in this file. An <svg viewBox> with no width/height and no CSS size is a
   replaced element with no intrinsic size, so it fell back to the ~150px default and
   swallowed the bar, squeezing the sentence next to it to a sliver. Size the child svg
   itself so it covers `.ic` and `hlogo` alike, now and later. */
.vb-ic > svg { width: 16px; height: 16px; flex: 0 0 auto; }

/* --- 2. In light mode, the line that says what went wrong was the least readable ---
   v165 renamed the inline modal error from .modal-err to .modal-status.err. The light
   theme darkening (v127) and its gate pin (v129) still name .modal-err, which app.js
   emits nowhere — so "Passwords don't match", "Couldn't start checkout" and every other
   failure painted #f87171 salmon on a white modal, about 2.8:1. The auth gate and the
   landing both pin their tokens to the DARK palette even in light mode, so they keep the
   salmon; darkening those would be the same 2.96:1 mistake v129 was written to fix. */
html.light body:not([data-gate="auth"]):not([data-view="landing"]) .modal-status.err { color: #b91c1c; }

/* --- 3. In light mode, a disabled primary button had no label at all ---------------
   v165 added the first :disabled rule (0,2,0). `html.light .btn.primary` (0,3,1) sets
   color:#fff and wins on specificity no matter the source order, so the disabled colour
   never applied: #fff on --elev-2 #ecebe4 is 1.2:1. "Nothing selected" on the lab-review
   save button, and every in-flight "Modeling…" / "Working…" / "Building…", rendered as an
   empty pill. */
html.light .btn:disabled, html.light .btn[disabled] { color: var(--text-faint); }

/* --- 4. Tapping any field on iOS zoomed the page and left it zoomed ---------------
   Mobile Safari zooms when a focused control computes under 16px, and does not zoom back
   out when the keyboard closes — so the modal ends up wider than the screen and you have
   to pinch out to find the button you were reaching for. Every typeable control was
   under: .modal-input and .composer textarea 15px, .edit-area 15px, .mkt-search 14.5px.
   Phones and tablets only; desktop keeps the tighter type. */
@media (max-width: 900px) {
  /* The bare element selectors carry this; the class list is only for controls whose own rule
     sets a smaller size and would otherwise win on specificity. (.twin-input and .reach-input
     were in this list on the first pass and exist nowhere — the Twin's fields are .twin-field
     input and .reach-target, both already covered by the bare `input`.) */
  .modal-input, .composer textarea, .edit-area, .mkt-search input, .reach-select,
  input[type="text"], input[type="email"], input[type="password"], input[type="search"],
  input[type="number"], input[type="date"], textarea, select { font-size: 16px; }
  /* the hint sits behind the date input and has to track its type size exactly */
  .dob-hint { font-size: 16px; }
}

/* --- 5. Autofill painted two bright rectangles inside the near-black sign-in card --
   The page never declared a colour scheme, so the moment a saved credential filled in,
   the UA applied its light-mode autofill wash (#E8F0FE in Chrome) with near-black text —
   two glowing boxes stacked in a dark modal. There is no property to restyle autofill
   directly; the long-standing route is an inset shadow large enough to repaint the box. */
:root { color-scheme: dark; }
html.light { color-scheme: light; }
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--bg-2) inset;
  box-shadow: 0 0 0 1000px var(--bg-2) inset;
  caret-color: var(--text);
  transition: background-color 9999s ease-out 0s;   /* outlast the UA's own repaint */
}

/* --- 6. On a phone the landing hid the Hale Pro column entirely -------------------
   The table is pinned to min-width 460px inside a 325px scroll box, so PRO began at
   x=360 — off-screen. You saw FEATURE and CORE, and the $14.99 column, every Pro check
   and its Get started button existed only for someone who happened to swipe a table
   sideways. Three columns fit 375px comfortably once the section stops spending 48px of
   a 375px screen on its own side padding. */
@media (max-width: 560px) {
  .lp-pricing.ch-spec { padding-left: 0; padding-right: 0; }
  .ch-table-3 { min-width: 0; }
  .ch-table-3 th, .ch-table-3 td { padding-left: 6px; padding-right: 6px; }
  .ch-table-3 thead th { font-size: 9.5px; letter-spacing: .1em; }
  .ch-table-3 .ch-price { font-size: 0.95rem; }
  .ch-table-3 .lp-btn { font-size: 0.75rem; padding: 8px 4px; }
}

/* --- 7. The Twin's two drawer captions were cut off mid-word ----------------------
   Both summaries are single-line flex rows with nowrap + ellipsis, so at 375px they read
   "— auto-filled fro…" and "— pick a marker and a t…". Those captions are the only thing
   telling you what the drawer does. Give the caption its own line under the title (order
   puts it after the chevron and the view/edit link, which stay on the first row). */
@media (max-width: 560px) {
  .twin-data summary, .twin-reach > summary { flex-wrap: wrap; row-gap: 3px; }
  .twin-data-sub {
    flex: 1 1 100%; order: 9; min-width: 0;
    white-space: normal; overflow: visible; text-overflow: clip;
    font-size: 0.8125rem; line-height: 1.4;
  }
  .twin-data summary .twin-edit { order: 5; margin-left: auto; }
  .twin-reach > summary::after { order: 5; }
}

/* --- 8. Every product card's selling point was truncated to nonsense --------------
   .p-badge is capped at 58% of a 140px card footer — 79px — for text that needs 158px,
   so the Shop showed "6,000+ a…", "Pro-athle…", "Compact col…" on card after card. The
   price and the badge do not have to share a line; the badge gets the full card width
   underneath it and is allowed to wrap. */
@media (max-width: 560px) {
  .mkt-grid .p-foot { flex-wrap: wrap; row-gap: 6px; align-items: center; }
  .mkt-grid .p-price { order: 1; }
  .mkt-grid .p-badge {
    order: 2; flex: 1 1 100%; max-width: 100%;
    white-space: normal; overflow: visible; text-overflow: clip;
    line-height: 1.35; border-radius: 9px; padding: 4px 8px; text-align: center;
  }
}

/* --- 9. Settings: the tab strip scrolled underneath the close button --------------
   .modal-x is absolute at top:14 right:14, 30px square, and the scrolling tab row starts
   at the same y. So the pills slid under the ✕, and reaching for the last tab closed
   Settings instead. Drop the strip below the ✕'s band. */
@media (max-width: 700px) {
  .modal:has(.settings-paged) .settings-paged { margin-top: 24px; }
}

/* --- 10. Settings rows squashed their own controls -------------------------------
   "Text size" is 187px of buttons in a 175px box with overflow:hidden, so "Large" was
   sliced in half; "Accent color" and "Text size" wrapped their labels to two lines to
   make room. Let the row wrap: a control that fits stays inline, one that does not gets
   its own line, right-aligned like the rest. */
@media (max-width: 560px) {
  /* Only the rows whose control cannot shrink. A toggle row must NOT wrap: its label carries a
     description line, which is wide enough to push a 42px switch onto a line of its own — the
     switch belongs beside the thing it switches. */
  .set-row:has(> .seg), .set-row:has(> .swatches) { flex-wrap: wrap; row-gap: 10px; }
  .set-row > .seg, .set-row > .swatches { flex: 0 0 auto; margin-left: auto; }
  .set-row:has(> .seg) > span:first-child,
  .set-row:has(> .swatches) > span:first-child { flex: 1 1 auto; min-width: 0; }
}

/* --- 11. v169 put 30px of empty space under three Settings headings ---------------
   v169 lifts each section label up out of its panel, and pays for the lift with 30px of
   margin on the first group. Appearance, General and Notifications have no label to lift,
   so they got the gap and nothing in it. Only groups that actually carry a label need it.
   :first-of-type also reaches <label class="set-row"> siblings, so name .set-group. */
.set-pane > .set-group:not(:has(> .set-label)) { margin-top: 0; }

/* --- 12. Six 22px accent swatches, side by side, on a touch screen ----------------
   The visual size is right; the target was not. Expand the hit area to the 44px v144 set
   for anything a thumb has to hit, without moving a pixel of what is drawn. Half the
   horizontal gap on each side so neighbouring targets meet but never overlap. */
.swatches { gap: 10px; }
.swatch { flex: 0 0 22px; position: relative; }
.swatch::after { content: ""; position: absolute; inset: -11px -5px; }

/* --- 13. The Twin's scenario chips looked cut off rather than scrollable ----------
   The row scrolls sideways with its scrollbar hidden, so the second chip was simply
   sheared off at the card edge and read as a rendering bug. Fade the trailing edge: the
   standard cue that the row continues. */
.scenario-chips {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
}

/* --- 14. MY REGRESSION AGAIN: the date field hid the digits as you typed them ------
   v163 hides the native date editor while the field is empty, so only our "MM / DD / YYYY"
   hint paints (Chrome draws its own and the two overlapped). The catch: an <input type=date>
   reports value "" until the WHOLE date is valid, so .has-value stays off through the month
   and the day — and on Chrome and desktop Safari you typed 0, 8, 1, 4 into a field that
   showed nothing back. Gate on focus instead of on value: unfocused and empty is the only
   state that needs our hint, and the moment you tap in, the real editor takes over. */
.dob-wrap > .modal-input:not([disabled]):focus::-webkit-datetime-edit { opacity: 1; }
.dob-wrap > .modal-input:focus ~ .dob-hint { display: none; }

/* --- 15. The launch gate's bottom edge dissolved, permanently ---------------------
   v165 masked the bottom 22px of the gate card and wrote .scrolled-end to lift the mask at
   the end of the scroll. Nothing ever adds that class — the token appears once in this file
   and nowhere in app.js, boot.js or either shell — so the fade is forever. It is not cheap
   either: the card's radius is 26px, so a 22px fade eats the whole bottom corner, its border
   and its shadow, and the "You're offline" variant is far too short to scroll yet still
   claims there is more below. A cue that is permanent is not a cue. The bisected-CTA problem
   it shipped alongside is handled by the max-height: 880px block, which stands. */
#authgate .modal { -webkit-mask-image: none; mask-image: none; }

/* --- 16. Waiting, failing and succeeding all looked the same ----------------------
   v165 wrote busy/err/ok variants for a class called .state-msg that nothing ever emitted.
   The class the app actually ships is .twin-loading, and it carries four different meanings
   under one grey rule — so "✓ Added 5 values" and "Couldn't read that file" print identically,
   and a twenty-second model call is a motionless grey line. Give the shipped class the
   variants, reusing colours already in the file. Bare .twin-loading is untouched, so the
   neutral prompts ("Describe a change to simulate first.") stay exactly as they are. */
.twin-loading.err { color: #f87171; }
.twin-loading.ok  { color: var(--green-2); }
html.light .twin-loading.err { color: #b91c1c; }
.twin-loading.busy::after {
  content: ""; display: inline-block; width: 6px; height: 6px; margin-left: 8px;
  border-radius: 50%; background: currentColor; vertical-align: middle;
  animation: statePulse 1.1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .twin-loading.busy::after { animation: none; opacity: .6; } }

/* --- 17. A long filename pushed its own remove button off the composer -----------
   .attach-chip is an inline-flex with no width ceiling and a filename that cannot shrink, so
   "2026-07-14-quest-diagnostics-comprehensive-panel.pdf" ran past the edge and took the ✕
   with it. Cap the chip, let the name ellipsize, pin the ✕. */
.attach-chip { max-width: 100%; }
.attach-chip > span:not(.x) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-chip .x { flex: 0 0 auto; }

/* --- 18. v169's :first-of-type reached rows it was not aimed at -------------------
   .set-row is emitted as both <div> and <label>, and :first-of-type counts per tag name, so
   "the first row" matched twice in any group that mixes them — the first div AND the first
   label both got the top padding. Name the child position instead of the tag. */
.set-group > .set-row:first-child { padding-top: 2px; }
.set-group > .set-row:not(:first-child) { padding-top: 12px; }

/* --- 19. The hero headline rose straight through the line above it ----------------
   .lp-hero h1 travels -210px on scroll; .lp-kicker, which sits directly above it, does not
   move at all — so about 20px into the scroll "All Hale good health." crosses "Stop guessing
   about your own body." and the two words overlap. Give the kicker the same journey, slightly
   faster, so it stays ahead of the headline and clears out first. */
.lp-hero .lp-kicker {
  transform: translate3d(0, calc(var(--hp,0) * -250px), 0);
  opacity: calc(1 - var(--hp,0) * 1.9);
}

/* --- 20. The product page's hero photo was a 64px thumbnail, centre-cropped -------
   v143 gave the grid cards real photography but only inside .mkt-grid, so the detail page
   kept the old 64px .p-tile and cropped a 1200px catalog shot to a square postage stamp. */
.p-tile.lg { width: 100%; max-width: 260px; height: 200px; border-radius: 18px; background: #fff; }
.p-tile.lg .p-photo { width: 100%; height: 100%; object-fit: contain; padding: 12px; }
@media (max-width: 560px) { .p-tile.lg { max-width: 100%; height: 190px; } }

/* --- 21. The middle confidence pill was the only one with no outline -------------
   The Twin emits "low" | "moderate" | "high"; the borderline rule names .conf.mid, which is
   never any of them. So low and high got a tinted, outlined pill and moderate got bare text. */
.conf.moderate {
  color: var(--amber, #fbbf6c); background: rgb(251 191 108 / .13);
  border: 1px solid rgb(251 191 108 / .42); box-shadow: 0 0 16px -3px rgb(251 191 108 / .4);
}

/* --- 22. The Hale "H" filled its tile in the Help sheet, like the install banner --
   Same root cause as §1: `.help-ic .ic` sizes the icons that carry class="ic", and the Hale
   leaf carries `hlogo`, which is styled nowhere. In a definite 30px tile the svg's auto size
   became 100% and the mark sat edge-to-edge next to 17px siblings. */
.help-ic > svg { width: 17px; height: 17px; }

/* --- 23. "edit" and "+ Set a goal" were 15px and 25px tall ------------------------
   Both are real buttons on the marker cards and both were well under the 44px v144 set for
   anything a thumb has to hit. Expand the target, leave the drawing alone. */
.mk-goal-edit, .mk-goal-set { position: relative; }
.mk-goal-edit::after { content: ""; position: absolute; inset: -14px -10px; }
.mk-goal-set::after { content: ""; position: absolute; inset: -9px -4px; }

/* ===== v171 — what Hale writes, rendered ============================================
   formatReply only ever handled bold, italic, links, bullets and paragraphs. Hale is a
   health assistant: it reaches for a table the moment it puts a marker next to a reference
   range, and for a numbered list the moment it answers "how do I…" with the structured plan
   its own system prompt asks for. Those arrived as raw pipe characters, visible "###", and
   an unindented run of <br>-joined lines. The renderer now emits tables, ordered lists,
   headings and code; this is what they look like. */

.m-body h3, .m-body h4 {
  margin: 16px 0 6px; line-height: 1.3; color: var(--text); font-weight: 650;
}
.m-body h3 { font-size: 1rem; }
.m-body h4 { font-size: 0.9375rem; color: var(--text-dim); }
.m-body > h3:first-child, .m-body > h4:first-child { margin-top: 2px; }

.m-body ol { margin: 0 0 10px; padding-left: 22px; }
.m-body ol li { margin: 3px 0; }
.m-body ol li::marker { color: var(--green-2); font-variant-numeric: tabular-nums; }

/* A lab table on a 375px screen is wider than the screen. It scrolls inside its own box —
   never the page — and the wrapper is what scrolls, so the border stays put. */
.m-tablewrap {
  margin: 10px 0; overflow-x: auto; -webkit-overflow-scrolling: touch;
  border: 1px solid var(--outline); border-radius: 12px;
}
.m-table { border-collapse: collapse; width: 100%; font-size: 0.875rem; }
.m-table th, .m-table td {
  padding: 8px 12px; text-align: left; white-space: nowrap;
  border-bottom: 1px solid var(--hairline);
}
.m-table thead th {
  font-size: 0.6875rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-dim); font-weight: 600; background: var(--elev-2);
}
.m-table tbody tr:last-child td { border-bottom: none; }
.m-table td:first-child { color: var(--text-dim); }
/* The number is the thing you came to read. */
.m-table td:not(:first-child) { color: var(--text); font-variant-numeric: tabular-nums; }

.m-pre {
  margin: 10px 0; padding: 11px 13px; overflow-x: auto;
  background: var(--elev-2); border: 1px solid var(--outline); border-radius: 12px;
}
.m-pre code {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.8125rem; line-height: 1.55; color: var(--text); white-space: pre;
}

/* ===== v172 — the light theme's vitals palette ======================================
   An accessibility sweep found the marker values on My Data rendering at about 1.1:1 in
   light theme — the number you opened the app to read, painted near-white on a white card.
   The cause is one gap: html.light already redefines the ACCENT greens (see the note in
   that block — "tuned for a near-black background… deriving from each accent's own
   --green-deep keeps every theme on-brand"). The VITALS palette at :root — --emerald,
   --emerald-lt, --arterial, plus two raw literals — was never included in that pass, and
   it is the palette every number, pill and range flag is painted with.

   Same approach as that earlier pass: same hues, darkened for a light ground, so light mode
   reads as the same product rather than a different one. The dark theme is untouched. */
html.light {
  --emerald:     #0F7A3D;   /* was #4ADE80 — 1.74:1 as text on white */
  --emerald-lt:  #15803D;   /* was #86EFAC — 1.40:1, the top stop of the big number */
  --arterial:    #BE123C;   /* was #F43F5E — 3.63:1, under the floor for pill text */
}

/* The big number is background-clip: text, so it needs its gradient re-stopped, not a colour.
   The neutral pair was --bone to #9FB3A6 (an off-white to a pale sage) and the low-confidence
   pair opened on #FFE4E9, a near-white pink. All three were invisible on a white card. */
html.light .mk-val { background-image: linear-gradient(180deg, #1c241e, #3d4a40); }
html.light .mk-card:has(.conf.low)  .mk-val { background-image: linear-gradient(180deg, #E11D48, #9F1239); }
html.light .mk-card:has(.conf.high) .mk-val { background-image: linear-gradient(180deg, #15803D, #0B5D2B); }
/* The accent glow behind the figure is a dark-theme device; on white it only greys the edges.
   (.m-val rides along in the older rule at :1339 but is emitted nowhere, so naming it here
   would only have added a second dead selector — the guard caught it.) */
html.light .mk-val { text-shadow: none; }

/* #fbbf6c is a raw literal in two places rather than a token, so it could not flip with the
   theme: the Twin's confidence percentage (--cc, and low confidence is the DEFAULT state, so
   this is the first number a new user sees) and the moderate pill that v172's predecessor
   added yesterday. Both at ~1.6:1 on white. */
html.light #twinModelCard.conf-low { --cc: #A16207; }
html.light .conf.moderate {
  color: #A16207; background: rgb(161 98 7 / .10); border-color: rgb(161 98 7 / .38);
  box-shadow: none;
}

/* The sparkline's specular stop is lifted toward WHITE so a stroke reads as lit metal on a
   near-black page. On a white card that is the one thing you must not do — it erases the line,
   and since v159 the sparkline is the only trend rendering on the card. Lift toward the deep
   green instead, which is the same gesture inverted. */
@supports (color: color-mix(in srgb, red 50%, white)) {
  html.light #haleFlow .hf-c { stop-color: color-mix(in srgb, rgb(var(--accent-rgb)) 55%, black); }
}

/* Terms, Privacy and the 18+ line sit on the dark end of the modal's own gradient rather than
   on --bg, where --text-faint was measured. */
html.light .modal-fine { color: var(--text-dim); }

/* ===== v173 — things you can reach with a keyboard but could not see ================
   An accessibility sweep found three controls that take focus and then show nothing for it.
   None of this changes what a pointer user sees; it only makes the focused state visible. */

/* Message actions and the recents row are opacity:0 until :hover. The touch escape hatch is
   @media (hover: none), which does NOT match a laptop with a trackpad — so a keyboard-only
   desktop user tabs through two or three fully transparent buttons per message, and through
   pin / rename / delete on every chat in the sidebar, with no idea what they are on. */
.msg:focus-within .msg-actions { opacity: 1; transform: none; }
.recent:focus-within .recent-actions { opacity: 1; }

/* The switch's real <input> is opacity:0 and stretched over the track, so it owns the focus
   ring and paints it nowhere. Put the ring on the track it controls. */
.toggle input:focus-visible + .track {
  outline: 2px solid var(--green); outline-offset: 2px;
}
/* Same shape for the two custom controls whose focus lands on a bare styled button. .seg is
   overflow:hidden (it clips its children to the pill's rounded ends), so an outline drawn
   OUTSIDE a segment is clipped away to nothing — the ring has to be inset. Measured: without
   the negative offset this rule paints and is then invisible, which is the same bug it fixes. */
.seg button:focus-visible { outline: 2px solid var(--green); outline-offset: -3px; }
.swatch:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
/* The upload panel is a real button now (it was a bare div with a click handler), so it
   inherits button defaults it never had — undo the ones that would change how it looks. */
.upload-zone {
  width: 100%; font: inherit; text-align: inherit; color: inherit; cursor: pointer;
  display: block;
}

/* ===== v174 — motion: smooth and sticky ============================================
   The owner's report was "choppy — clicking, hovering, tab switching, everything". Measured
   rather than guessed, three things caused it and none of them was frame rate on idle:

   1. THE ENTRY ANIMATION FIGHTS THE LAYOUT IT ANIMATES. `.chat-wrap, .page` run
      `viewIn .32s` (styles.css:665) at the exact moment render() replaces the whole view.
      Timed on a fast Mac: a first visit to Marketplace costs 15.8ms of layout, My Data 13.5ms,
      the Twin 14.4ms — so on a phone at roughly 4x slower, 60-70ms of main-thread work lands
      inside the first frames of a 320ms animation. The animation does not start late; it starts
      and then stutters. Promoting the element to its own compositor layer BEFORE it animates
      hands the whole thing to the compositor thread, where main-thread layout cannot touch it.

   2. THIRTY-FIVE TRANSITIONS ANIMATE `all`. `transition: .15s var(--ease)` names no property,
      which means every property — so any class toggle animates through layout and paint, not
      just the colour that was meant. `.main` is one of them, and `.main` is the element whose
      entire innerHTML is replaced on every route change.

   3. THE CURVE AND THE CLOCK. --ease was cubic-bezier(.22,.61,.36,1) and .15s dominated the file
      (35 of 93 transitions). 150ms with a mild ease-out arrives rather than settles — it reads as
      a snap. And the most-used raw curve, cubic-bezier(.34,1.56,.64,1) at 36 uses, OVERSHOOTS;
      at 150ms an overshoot is not a bounce, it is a twitch.

   Sticky comes from a long, hard deceleration: most of the distance covered early, then a slow
   settle. That is what the redefined --ease is. Redefining the token rather than adding a new one
   is deliberate — it re-times all 93 existing transitions and every animation that references it
   in one move, which is the only way this reads as one system rather than a patch. */

:root {
  /* easeOutExpo-ish. Travels ~80% in the first third, then settles. This is the "sticky". */
  --ease: cubic-bezier(.16, 1, .3, 1);
  /* For things that leave, or move both ways under a finger. */
  --ease-inout: cubic-bezier(.65, 0, .35, 1);
  /* A press must beat the finger, so it front-loads instead. */
  --ease-press: cubic-bezier(.2, 0, 0, 1);
  /* Tamed from cubic-bezier(.34,1.56,.64,1): enough overshoot to feel physical at 300ms+,
     not enough to twitch. */
  --ease-spring: cubic-bezier(.34, 1.28, .64, 1);

  --dur-press: 90ms;    /* acknowledgement — must feel instant */
  --dur-1: 200ms;       /* a tint, a border, a chip */
  --dur-2: 320ms;       /* a panel, a card, a drawer */
  --dur-3: 460ms;       /* a whole view */
}

/* --- 1. The view change, handed to the compositor ---------------------------------
   will-change before the animation starts is the point: it creates the layer during the frame
   that inserts the content, so the animation itself never waits on a layer promotion. The
   contain hint stops the new subtree's layout from invalidating anything outside it. */
.chat-wrap, .page {
  animation-duration: var(--dur-3);
  animation-timing-function: var(--ease);
  will-change: transform, opacity;
  contain: layout paint;
}
/* Travel further than 8px, or the eye reads a fade rather than a movement — and a fade with a
   stutter in it just looks like a slow paint. */
@keyframes viewIn { from { opacity: 0; transform: translate3d(0, 18px, 0); } to { opacity: 1; transform: none; } }

/* --- 2. Name the properties, everywhere it is hot --------------------------------
   Each of these carried `transition: <time> var(--ease)` with no property, i.e. `all`. Listing
   what actually changes keeps the animation on the compositor and off the layout engine. */
.main { transition: none; }
.nav-item { transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease), transform var(--dur-press) var(--ease-press); }
.icon-btn, .model-pill, .user, .tool-chip, .msg-action, .r-act {
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease), opacity var(--dur-1) var(--ease),
              transform var(--dur-press) var(--ease-press);
}
.card, .composer, .upload-zone, .recent {
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-2) var(--ease),
              background var(--dur-1) var(--ease), transform var(--dur-2) var(--ease);
}
.msg-actions, .recent-actions { transition: opacity var(--dur-1) var(--ease), transform var(--dur-1) var(--ease); }

/* --- 3. The press: the one thing that has to beat the finger ----------------------
   Fast in on --ease-press so the acknowledgement lands under the fingertip, slow out on the
   spring so releasing feels like letting go of something with weight. */
.btn:active:not(:disabled), .nav-item:active, .plan-tab:active:not(.static):not(:disabled),
.card:active, .mk-card:active, .product:active, .conn-card:active, .icon-btn:active,
.mkt-chip:active, .scenario-chip:active, .set-action:active, .seg button:active,
.tool-chip:active, .msg-action:active, .r-act:active, .recent:active {
  transition-duration: var(--dur-press);
  transition-timing-function: var(--ease-press);
}

/* --- 4. Stop the sheen animating `left` ------------------------------------------
   styles.css:1334 sweeps `left` from -70% to 130% on every primary button hover. `left` is a
   layout property: each frame relays out the button's containing block. transform does the same
   sweep on the compositor. */
.btn.primary::after {
  left: 0; transform: translate3d(-180%, 0, 0) skewX(-18deg);
  transition: transform .62s var(--ease-inout);
}
.btn.primary:hover::after { left: 0; transform: translate3d(320%, 0, 0) skewX(-18deg); }

/* --- 5. Lists that grow should not pop ------------------------------------------- */
.msg { animation-duration: var(--dur-2); animation-timing-function: var(--ease); }

/* Everything above is motion, so all of it yields to the OS preference. */
@media (prefers-reduced-motion: reduce) {
  .chat-wrap, .page { animation: none; will-change: auto; }
  .btn.primary::after { display: none; }
}

/* ===== v175 — the press you can feel, and the hover that lets go ===================
   v174 fixed the clock and the curve. This is the amplitude, which was the other half of
   "choppy": several of these animations were running correctly and were simply too small to
   perceive, which reads as nothing happening at all.

   Do the arithmetic. .icon-btn is 38x38 and pressed to scale(.985) — that moves each edge
   0.29px, over 180ms. .btn is min-height 44 at scale(.98) — 0.44px. Those are sub-pixel on a
   1x display and imperceptible on any. A press has to be seen to register as an answer. */

/* Small controls need a bigger ratio than large ones to travel the same visible distance, so
   the scale is graded by size rather than applied uniformly. */
.icon-btn:active, .msg-action:active, .r-act:active, .swatch:active, .wk-step:active,
.mkt-chip:active, .scenario-chip:active, .tool-chip:active, .seg button:active {
  transform: scale(.90);
}
.btn:active:not(:disabled), .btn.primary:active:not(:disabled), .nav-item:active,
.set-action:active, .plan-tab:active:not(.static):not(:disabled) {
  transform: scale(.965);
}
/* Cards are big, so they press INTO the page rather than shrinking — a shrink that size reads
   as the card moving away from you, which is the opposite of tactile. */
.card:active, .mk-card:active, .product:active, .conn-card:active, .recent:active {
  transform: scale(.992) translateY(1px);
}

/* --- The inverse problem: hover latches after a tap ------------------------------
   WebKit leaves :hover on the last-tapped element until you touch somewhere else, so every green
   CTA stayed lifted, brightened and glowing after the finger left. The existing unlatch list
   (v165) names seven selectors and misses every button in the product — including .btn.primary,
   whose :hover carries translateY(-2px), a saturate/brightness filter and a 40px glow. */
@media (hover: none) {
  .btn:hover, .btn.primary:hover, .set-action:hover, .seg button:hover, .swatch:hover,
  .icon-btn:hover, .msg-action:hover, .r-act:hover, .tool-chip:hover, .plan-tab:hover,
  .mkt-chip:hover, .scenario-chip:hover, .upload-zone:hover, .composer:hover {
    transform: none; filter: none; box-shadow: none;
  }
  /* The sheen sweep is a hover affordance with no meaning on a touch screen, and it is the one
     thing here that animates on every frame it runs. */
  .btn.primary::after { display: none; }
}

/* --- The OS grey box is not a design decision -------------------------------------
   -webkit-tap-highlight-color appears nowhere in 6,000 lines, so every tap on iOS draws Apple's
   default grey rectangle over whatever press state we just designed — including over rounded
   corners, as a rectangle. Ours is the only feedback now. */
html { -webkit-tap-highlight-color: transparent; }

/* --- Long threads stop relaying out as a whole ------------------------------------
   .messages is a flex column with no containment, so appending to the last bubble invalidated
   layout for every message above it. At 30 messages that is the real cost of a streaming reply.
   contain-intrinsic-size keeps the scrollbar honest for the ones currently skipped. */
.messages > .msg { content-visibility: auto; contain-intrinsic-size: auto 120px; }

/* --- The drawer and its scrim as one gesture -------------------------------------
   The scrim ran .2s with the browser default ease; the panel .26s on --ease. Two clocks and two
   curves for one movement, so the dim and the panel visibly came apart at both ends. */
.scrim { transition: opacity var(--dur-2) var(--ease); }
.sidebar { transition: transform var(--dur-2) var(--ease); }

/* --- The docked sidebar stops relaying out the whole app every frame ---------------
   styles.css:310 animates width AND padding, which relayouts the sidebar, the main pane and
   every card inside it on each of ~16 frames. Transform does the same visual move on the
   compositor; the grid column it occupies is what actually needs the width, so only that is
   still animated and it is animated alone. */
@media (min-width: 769px) {
  .sidebar { transition: transform var(--dur-2) var(--ease), opacity var(--dur-1) var(--ease); }
  .app.closed .sidebar { transform: translate3d(-100%, 0, 0); opacity: 0; }
}

/* --- Modals: they arrive over 240ms and used to vanish between two frames ----------
   closeModal() is `$("#modal")?.remove()`. Nothing can animate an element that is already gone,
   so the exit is a hard cut every time — the single most jarring moment in the app, and it
   happens on every ✕. The class is added by closeModal before the removal is deferred. */
.modal-overlay.closing { animation: overlayOut var(--dur-1) var(--ease) both; }
.modal-overlay.closing .modal { animation: modalOut var(--dur-1) var(--ease-inout) both; }
@keyframes overlayOut { to { opacity: 0; } }
@keyframes modalOut { to { opacity: 0; transform: translate3d(0, 10px, 0) scale(.97); } }

@media (prefers-reduced-motion: reduce) {
  .modal-overlay.closing, .modal-overlay.closing .modal { animation: none; }
  .messages > .msg { content-visibility: visible; }
}

/* ===== v176 — searching your own markers ===========================================
   The marker field reuses .mkt-search wholesale rather than inventing a lookalike: same
   component, same focus ring, same language as the Shop's filter, and nothing new to keep in
   step. .mk-search only adjusts what differs — it sits inside a card rather than at the top of
   a page, so it needs less air beneath it. */
.mk-search { margin-bottom: 16px; }
.mk-search .ic { width: 17px; height: 17px; color: var(--text-faint); flex: 0 0 17px; }

/* ===== v177 — the Twin says what to do, not just what will happen ==================
   The simulation projected markers and stopped: ApoB down 9%, HRV up 4, and nothing you could
   act on come Monday. The regimen is the answer to "so what", so it is the widest thing on the
   page rather than a footnote under the projection cards. */
.rg-card { margin-top: 16px; }
.rg-headline { font-size: 0.9375rem; color: var(--text); line-height: 1.55; margin: 2px 0 14px; }

/* One row per day. The grid is named so a rest day can sit visibly lighter than a session
   without becoming a different component. */
.rg-week { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline);
           border-radius: 12px; overflow: hidden; margin-bottom: 14px; }
.rg-day { display: grid; grid-template-columns: 52px 118px 1fr; gap: 12px; align-items: baseline;
          padding: 11px 13px; background: var(--elev); }
.rg-dayname { font-family: var(--mono, ui-monospace, monospace); font-size: 0.6875rem;
              letter-spacing: .08em; text-transform: uppercase; color: var(--text-dim); }
.rg-focus { font-size: 0.8125rem; font-weight: 650; color: var(--green-2); }
.rg-detail { font-size: 0.8125rem; color: var(--text-dim); line-height: 1.5; }
.rg-day.is-rest { background: var(--bg-2); }
.rg-day.is-rest .rg-focus { color: var(--text-faint); font-weight: 500; }

.rg-side { margin-top: 12px; }
.rg-side .twin-list { margin-top: 4px; }
.rg-checkin { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hairline);
              display: flex; align-items: flex-start; gap: 8px; line-height: 1.5; }
.rg-checkin .ic { width: 15px; height: 15px; flex: 0 0 15px; color: var(--green-2); margin-top: 2px; }
.rg-checkin b { color: var(--text); }

/* A three-column day row does not survive 375px — the detail is the part worth reading, so it
   takes the full width and the day and focus become its label. */
@media (max-width: 560px) {
  .rg-day { grid-template-columns: 52px 1fr; row-gap: 3px; }
  .rg-detail { grid-column: 1 / -1; }
}

/* The profile drawer borrows the Twin's existing summary treatment rather than inventing one. */
.twin-profile > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none;
  padding: 12px 14px; min-height: 44px;
  border: 1px solid var(--hairline); border-radius: 14px; background: var(--bg-2);
  font-size: 0.9375rem; margin-bottom: 10px;
}
.twin-profile > summary::-webkit-details-marker { display: none; }
.twin-profile > summary::after {
  content: ""; margin-left: auto; width: 7px; height: 7px; flex: 0 0 auto;
  border-right: 1.5px solid var(--text-dim); border-bottom: 1.5px solid var(--text-dim);
  transform: translateY(-2px) rotate(45deg); transition: transform .18s var(--ease);
}
.twin-profile[open] > summary::after { transform: translateY(1px) rotate(-135deg); }

/* ===== v178 — the plan you are about to move to, shown before you move ==============
   Switching plans was a bare window.confirm() naming a plan the user could not see. This is the
   sheet that replaced it: one card, centred, at the width the two-up tabs use for a single
   column rather than stretched across the modal. */
.plan-tabs.one { grid-template-columns: minmax(0, 260px); justify-content: center; }
/* Left-aligned deliberately: .modal centres its text, which on a bulleted list leaves each
   bullet stranded to the left of a ragged, centred line. A list of what changes is read, not
   admired — so the text lines up under itself and beside its own marker. */
.pc-changes { margin: 16px 0 4px; text-align: left; }
.pc-changes li { line-height: 1.55; }
.pc-changes b { color: var(--text); }

/* ===== v179 — a plan that is ending says so =========================================
   The Subscription row and the sidebar pill had one voice for four different states. A plan that
   renews, a plan that ends in nine days, a card that was declined and a plan that is already over
   all rendered identically, the pill in the same confident green. Colour is the only thing that
   makes the two states you can still act on look different from the two you cannot.
   --amber is not a declared token (see .pill.amber, line 515); this is the same literal that pill
   has used since v10. */
.user-plan.warn { color: #fbbf6c; }
.sub-note.warn { color: #fbbf6c; }

/* ===== v180 — the hero names the tier ===============================================
   .hl was styled under .lp-copy and .jn-cp only, so the same markup in the hero rendered as
   plain bold. The hero now attributes three Pro rows — wearables, the Digital Twin, and the
   plan that re-tunes — and the plan name is the whole point of the sentence, so it gets the
   same green the rest of the landing page gives an emphasised phrase. */
.lp-sub .hl { color: var(--green-2); font-weight: 650; }

/* ===== v181 — the export sheet sits above whatever raised it ========================
   The app's export is a two-step sheet, and every surface that can start it is itself an overlay:
   Settings (#modal) and the paywall (#paywall), both .modal-overlay. Source order alone would win,
   but only by accident — one future overlay appended after this one and the download link is
   behind a scrim. 150 is above those and deliberately BELOW #agegate's 200, which nothing may
   cover. App-only by construction: exportMyData routes here on IS_APP and nowhere else. */
html[data-app="1"] #exportSheet { z-index: 150; }

/* ===== v182 — four pillars, and a tracked marker that shows its number ==============
   The pillar cards were .grid.cols-3. Build 216 added a fourth pillar, so Peptides sat alone on a
   second row at one third width. Its own class rather than a .cols-4: .cols-3 is shared with the
   Twin and marker grids, and auto-fit means a fifth pillar or a narrow window never needs another
   hardcoded count — the same lesson .mkt-grid already records. */
/* .proto-grid alone, not .grid.proto-grid: .grid sets only display and gap, never columns, so
   there is no specificity fight to win — and the single class is what the CSS scope guard can
   actually match against its allow-list. auto-fit collapses to one column on a phone by itself,
   so it needs no companion media query the way .cols-3 did. */
.proto-grid { grid-template-columns: repeat(auto-fit, minmax(232px, 1fr)); }

/* "Track" used to be a sentence and nothing else, while the markers page held that very marker
   with months of history. When a pillar links one, its current value and movement sit under the
   sentence. Block, not inline: the sentence runs to a line already, and the number has to be
   findable at a glance rather than buried at the end of prose. */
.proto-track .pt-now { display: block; margin-top: 3px; color: var(--text); font-size: 0.86rem; }
.proto-track .pt-move { display: block; font-style: normal; font-size: 0.78rem; color: var(--text-dim); }
.proto-track .pt-move.down { color: var(--green-2); }
.proto-track .pt-move.up { color: #fbbf6c; }

/* ===== v183 — the facts Hale keeps permanently are marked ===========================
   Memories were read newest-first and sliced by every caller, so whether Hale knew you were on an
   anticoagulant depended on how recently you had said it. Safety-relevant facts are now exempt
   from that window — and the ones getting that treatment say so, because a person is entitled to
   see which of their own data is being held permanently. Deliberately quiet: a label, not a badge.
   It sits beside the fact rather than above it, so the list still reads as a list. */
.mem-pin { display: inline-block; margin-left: 8px; font-style: normal; font-size: 0.68rem;
           letter-spacing: 0.04em; text-transform: uppercase; color: var(--green-2);
           border: 1px solid var(--outline); border-radius: 999px; padding: 1px 7px; vertical-align: 1px; }

/* ===== v184 — a pillar with nothing to report still looks like a pillar =============
   The status pill had exactly two tones, green and amber, because the model only ever wrote "on
   track", "adjust" or "review". Now that the pill can report what the marker actually DID, there
   is a third answer — measured, and no change yet — and it is neither good news nor bad. Without
   a tone of its own a bare .pill inherits no colour, background or border, and renders as naked
   text beside two properly-chromed siblings. Muted on purpose: "nothing has moved" should not
   compete for attention with "moving the wrong way". */
.pill.neutral { color: var(--text-dim); background: rgba(127,140,133,0.10); border: 1px solid var(--outline); }

/* ===== v185 — the build allowance is visible ========================================
   protocolFreeLeft and protocolResetAt were computed server-side, sent on every /api/me and
   rendered nowhere, so a Core subscriber could not see they had four builds a month and an
   account with one lifetime build had no warning before spending it. Sits under the Generate
   button rather than above it: it qualifies the action you are about to take, and reads as a
   footnote to that button rather than as a banner competing with the page title. */
.proto-meter { margin: 10px 0 0; font-size: 0.82rem; color: var(--text-dim); }

/* ── v186 · protocols on a phone: the bar you can always see, the rationale you can put away ──
   Measured on a 390px viewport: a four-pillar plan is ~2,900px of scroll, and the progress bar
   sat at the very top of it. From the third pillar down you could not see how far through you
   were without scrolling back, which is the one number the page exists to tell you. Sticking it
   under the topbar costs 34px and makes it permanent.

   And the rationale — three or four lines under EVERY pillar — was the single biggest reason
   the page was that long. It is genuinely worth reading once and almost never worth re-reading,
   which is exactly what <details> is for. Closed by default; the actions come first.

   GLOBAL, not html[data-app="1"], and deliberately: Protocols is not an app-only route —
   canAccess("protocols") does not test IS_APP — so a phone on allhale.app renders the same
   2,900px page from the same markup. Scoping this would fix it in the shell and leave mobile web
   exactly as it was, which is the mistake ALLOWED_GLOBAL already records against .mkt-grid. The
   three selectors are emitted only by protocolsHTML(), so there is nothing on the landing page
   for them to reach. */
/* Sticky lives on the WRAPPER, not on the bar. Sticky is clipped to its own parent's box, and
   the bar's parent is only as tall as the bar — it would have unstuck about 34px into the scroll.
   #protoProgress is a direct child of #protoArea, which spans the whole plan, so that is the
   element that can actually follow you. The topbar is a flex sibling of the scrollport rather
   than inside it, so top is 0 and not a topbar height. Not sticky once the completion banner is
   in there: at 100% the number has stopped changing and the banner is the tall thing you would
   be pinning to the top of the screen. */
#protoProgress:not(.done) {
  position: sticky; top: 0; z-index: 5;
  padding: 10px 0 8px; margin-bottom: -4px;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
}
.proto-why { margin-top: 10px; }
.proto-why > summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.8rem; font-weight: 600; color: var(--text-dim);
  padding: 4px 0; user-select: none;
}
.proto-why > summary::-webkit-details-marker { display: none; }
.proto-why > summary::after {
  content: "›"; font-size: 1rem; line-height: 1; transform: rotate(90deg);
  transition: transform .18s ease;
}
.proto-why[open] > summary::after { transform: rotate(-90deg); }
.proto-why > summary:hover { color: var(--text); }
.proto-why > summary:focus-visible { outline: 2px solid var(--green); outline-offset: 3px; border-radius: 4px; }

/* The load dial. Deliberately quiet — it is a question the page asks once you have scrolled past
   the actions, not a call to action competing with them. */
.proto-load {
  margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
.proto-load .pl-q { font-size: 0.8rem; color: var(--text-dim); }
.proto-load .pl-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.proto-load .pl-chip {
  font: inherit; font-size: 0.78rem; line-height: 1;
  padding: 7px 11px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--outline); background: transparent; color: var(--text-dim);
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.proto-load .pl-chip:hover { color: var(--text); border-color: var(--text-dim); }
.proto-load .pl-chip.on {
  background: color-mix(in srgb, var(--green) 14%, transparent);
  border-color: var(--green); color: var(--green); font-weight: 600;
}
.proto-load .pl-chip:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.proto-load .pl-said { flex-basis: 100%; font-size: 0.76rem; color: var(--green); }
@media (prefers-reduced-motion: reduce) {
  .proto-why > summary::after,
  .proto-load .pl-chip { transition: none; }
}

/* Two .pill declarations older than this layer had the same defect and were fixed in place rather
   than overridden: they said `border: 1px solid var(--line)`, and --line has never been declared
   anywhere in the file. An invalid var() invalidates the WHOLE shorthand at computed-value time,
   so border-style fell back to none and those pills have been rendering with no border at all —
   which is why an override of border-color alone would have changed nothing. Fixed at the source
   because a declaration that has never once applied is not a layer anyone can be depending on.
   Measured, not read: getComputedStyle on a live .pl-chip returned borderWidth "0px" against a
   rule that plainly says 1px. */
