// fc-shared.jsx, OnWeight · v3 (themed)
// Three visual directions, each a full CSS-variable theme applied at the .phone
// root so components inherit. Philosophy unchanged: interpret first, plain
// language, numbers one glance away.
//
//   hig     , Clean iOS: system-gray grouped UI, SF-like sans, brand-red accent
//   fighter , Retro Fighter: warm poster paper, Oswald, crop-marks, red
//   elite   , Elite Lab: graphite dark, mono labels, teal signal, instrument feel
// ─────────────────────────────────────────────────────────────────────────

const FC = {
  athlete: 'Alex Mercer', firstName: 'Alex', sport: 'MMA', division: 'Lightweight',
  fightDate: '01 JAN 2026', fightDateShort: 'Jan 1',
  goalWeight: 74.4, maxWithGi: 76.0, giBuffer: 0.5, planStart: 78.4,
  daysOut: 23, campDays: 50, todayDay: 27,
  weighIn: 'Day before',
  // Daily fasted weigh-in preference. consistency is what matters; morning is most
  // accurate. `targetHour` is 24h; entries log their real time so drift is visible.
  weighInTime: '6:50 AM', weighInTargetHour: 7,
  // Cycle model (see Logic & Flows §7). dayToday positions "now" in late luteal so
  // the athlete is holding ~+0.5kg water, and weigh-in (campDays out) also lands in
  // luteal, demonstrating the cycle buffer. retention = peak water amplitude (kg).
  // Default 0.5 kg reflects the evidence (~0.45 kg avg, fluid not fat; many see none),
  // not the earlier +1.0 guess. Personalised upward/downward from logged data over time.
  cycle: { phase: 'LUTEAL', day: 26, dayToday: 26, length: 28, retention: 0.5 },
};

// ── Cycle water-weight model ───────────────────────────────────────────────
// One smooth curve, shared by the projection math and every cycle chart so they
// can never disagree: water offset dips to −retention in follicular (~day 10) and
// peaks at +retention in late luteal (~day 24). cycleOffsetKg(campDay) returns the
// kg the SCALE reads above (or below) true weight on that camp day.
function cycleDayAt(campDay) {
  const { dayToday, length } = FC.cycle;
  return ((dayToday + (campDay - FC.todayDay)) % length + length) % length;
}
function cycleOffsetKg(campDay) {
  const { length, retention } = FC.cycle;
  return +(retention * -Math.cos(2 * Math.PI * (cycleDayAt(campDay) - 10) / length)).toFixed(3);
}
function cyclePhaseAt(campDay) {
  const cd = cycleDayAt(campDay);
  return cd < 5 ? 'menstrual' : cd < 13 ? 'follicular' : cd < 16 ? 'ovulation' : 'luteal';
}
const CYCLE_PHASE = {
  menstrual:  { label: 'Menstruation', short: 'Menses', color: '#b0432f' },
  follicular: { label: 'Follicular',   short: 'Follic.', color: '#6b8f5a' },
  ovulation:  { label: 'Ovulation',    short: 'Ovul.',  color: '#c79a3a' },
  luteal:     { label: 'Luteal',       short: 'Luteal', color: '#7a6aa0' },
};

// ── Weigh-in clock (deterministic per camp day) ────────────────────────────
// Real logged time of each fasted weigh-in, as a 24h decimal hour. Mostly sits in
// the ~6:45–7:10 window; a couple drift off (a 9am, an evening) and one recent day
// has TWO readings, the earlier fasted one is canonical, the later is context.
const _WI_CLOCK = [6.8,7.0,6.9,7.1,6.7,7.05,6.95,9.2,7.0,6.8,7.1,6.9,7.0,6.7,6.95,7.0,6.9,7.1,6.8,7.0,6.9,7.05,6.8,7.1,6.9,20.6,6.7,7.0];
const FC_DOUBLE_DAY_OFFSET = 5;   // a double weigh-in this many days before today
function weighInClock(day) { return _WI_CLOCK[((day % _WI_CLOCK.length) + _WI_CLOCK.length) % _WI_CLOCK.length]; }
function fmtClock(h) {
  let hr = Math.floor(h), m = Math.round((h - hr) * 60);
  if (m === 60) { hr += 1; m = 0; }
  const ap = hr >= 12 ? 'PM' : 'AM'; let h12 = hr % 12; if (h12 === 0) h12 = 12;
  return `${h12}:${String(m).padStart(2, '0')} ${ap}`;
}
function weighInOffWindow(day) { return Math.abs(weighInClock(day) - FC.weighInTargetHour) > 1; }

const PACE_STATES = {
  onTrack: {
    key: 'onTrack', tone: 'green', weeklyVel: -0.62, current: 76.0, badge: 'ON TRACK',
    verdict: 'On track to make weight.',
    line: 'Hold this pace. You hit 74.4 by fight day with room to spare.',
    chartCaption: 'Trend’s sitting right on the plan line.',
    weekLine: 'On pace this week.',
    resultHead: 'Right where you need to be.',
    resultSub: 'Hold the line.',
    coach: 'Trend’s on target. Daily swings are normal, so trust the line. Keep this up and you walk into fight week sharp.',
  },
  tooFast: {
    key: 'tooFast', tone: 'amber', weeklyVel: -1.18, current: 75.3, badge: 'CUTTING FAST',
    verdict: 'Cutting too fast.',
    line: 'You’re dropping quicker than the plan. Hard cuts cost power and recovery.',
    chartCaption: 'Trend’s dipping below the plan line.',
    weekLine: 'Ahead of schedule this week.',
    resultHead: 'Ahead of schedule.',
    resultSub: 'Ease off the cut.',
    coach: 'You’re losing it faster than mapped. Add some food back this week. Walk in with a full tank and your legs under you.',
  },
  trailing: {
    key: 'trailing', tone: 'amber', weeklyVel: -0.34, current: 76.9, badge: 'SLIGHTLY BEHIND',
    verdict: 'Slightly behind pace.',
    line: 'Small gap to close. Tighten up this week and you’re back on track.',
    chartCaption: 'Trend’s drifting just above the plan line.',
    weekLine: 'A little behind this week.',
    resultHead: 'Time for a small adjustment.',
    resultSub: 'You’re close. Tighten up.',
    coach: 'Progress is good, just a touch behind. Trim the food slightly this week. Small moves now mean an easy fight week.',
  },
  danger: {
    key: 'danger', tone: 'red', weeklyVel: -0.14, current: 77.6, badge: 'OFF TRACK',
    verdict: 'Your plan needs attention.',
    line: 'At this pace you miss 74.4. Change the plan before fight week.',
    chartCaption: 'Trend’s well above the plan line.',
    weekLine: 'Off pace this week.',
    resultHead: 'Your plan needs attention.',
    resultSub: 'Time to change the plan.',
    coach: 'Straight talk: this rate isn’t safe. Move up a class or rework the cut. No crash diets this close in.',
  },
};

// Tone colors come from theme CSS vars so they read correctly on light & dark.
const TONE = {
  green: { ink: 'var(--t-green)', soft: 'var(--t-green-soft)' },
  amber: { ink: 'var(--t-amber)', soft: 'var(--t-amber-soft)' },
  red:   { ink: 'var(--t-red)',   soft: 'var(--t-red-soft)' },
};
const SHADOW = 'var(--card-shadow)';

// ── THEMES ───────────────────────────────────────────────────────────────────
const THEMES = {
  // Editorial, the original: warm white cards, Oswald eyebrows, red accent.
  paper: {
    flags: { layout: 'classic', heroInverted: false, motif: 'none' },
    vars: {
      '--paper': '#ffffff', '--paper-2': '#f6f4ef',
      '--ink': '#15140f', '--ink-2': '#3a3833', '--ink-3': '#524d44',
      '--rule': '#ddd5c6', '--rule-2': '#8a7f68',
      '--accent': '#a32a18', '--accent-soft': 'rgba(163,42,24,0.10)',
      '--red': '#a32a18', '--mustard-deep': '#a87b22',
      '--t-green': '#2f5a24', '--t-green-soft': 'rgba(58,107,46,0.13)',
      // Unprefixed tone aliases — many verdict chips, tint discs and the safe-pace
      // gradient reference these names. Pointed at the --t-* values above so the
      // palette can't drift into two greens/ambers.
      '--green': '#2f5a24', '--green-soft': 'rgba(58,107,46,0.13)',
      '--amber': '#6f5210', '--amber-soft': 'rgba(168,123,34,0.15)',
      '--t-amber': '#6f5210', '--t-amber-soft': 'rgba(168,123,34,0.15)',
      '--t-red': '#a32a18', '--t-red-soft': 'rgba(163,42,24,0.11)',
      '--display': "'Oswald', sans-serif",
      '--num': "'IBM Plex Sans', system-ui, sans-serif",
      '--title-font': "'IBM Plex Sans', system-ui, sans-serif",
      '--title-transform': 'none', '--title-spacing': '-0.02em', '--eyebrow-spacing': '0.16em',
      '--radius': '14px', '--radius-ctl': '13px',
      '--card-shadow': '0 1px 2px rgba(20,18,12,0.05), 0 10px 26px rgba(20,18,12,0.05)',
      '--card-border': '1px solid var(--rule)', '--chrome-bg': 'rgba(246,244,239,0.92)',
    },
  },
  // Full Editorial, flat Weight-Camp look: warm paper, hairline rules (no floating cards),
  // huge light numerals, Oswald caps, sharp corners, no shadow.
  editorial: {
    flags: { layout: 'full', heroInverted: false, motif: 'none' },
    vars: {
      '--paper': '#faf7f1', '--paper-2': '#faf7f1',
      '--ink': '#15140f', '--ink-2': '#3a3833', '--ink-3': '#524d44',
      '--rule': '#cfc6b4', '--rule-2': '#8a7f68',
      '--accent': '#a32a18', '--accent-soft': 'rgba(163,42,24,0.10)',
      '--red': '#a32a18', '--mustard-deep': '#a87b22',
      '--t-green': '#2f5a24', '--t-green-soft': 'rgba(58,107,46,0.13)',
      // Unprefixed tone aliases — many verdict chips, tint discs and the safe-pace
      // gradient reference these names. Pointed at the --t-* values above so the
      // palette can't drift into two greens/ambers.
      '--green': '#2f5a24', '--green-soft': 'rgba(58,107,46,0.13)',
      '--amber': '#6f5210', '--amber-soft': 'rgba(168,123,34,0.15)',
      '--t-amber': '#6f5210', '--t-amber-soft': 'rgba(168,123,34,0.15)',
      '--t-red': '#a32a18', '--t-red-soft': 'rgba(163,42,24,0.11)',
      '--display': "'Oswald', sans-serif",
      '--num': "'IBM Plex Sans', system-ui, sans-serif",
      '--title-font': "'Oswald', sans-serif",
      '--title-transform': 'uppercase', '--title-spacing': '0.04em', '--eyebrow-spacing': '0.22em',
      '--radius': '2px', '--radius-ctl': '2px',
      '--card-shadow': 'none',
      '--card-border': '1px solid var(--rule)', '--chrome-bg': 'rgba(250,247,241,0.94)',
    },
  },
  // Balanced, editorial hero + native iOS structure: rounded grouped cells, segmented
  // control, large title, soft depth; keeps Oswald eyebrows + red for brand personality.
  balanced: {
    flags: { layout: 'balanced', heroInverted: false, motif: 'none' },
    vars: {
      '--paper': '#ffffff', '--paper-2': '#f1ede6',
      '--ink': '#15140f', '--ink-2': '#3a3833', '--ink-3': '#524d44',
      '--rule': '#e6ded2', '--rule-2': '#8a7f68',
      '--accent': '#a32a18', '--accent-soft': 'rgba(163,42,24,0.10)',
      '--red': '#a32a18', '--mustard-deep': '#a87b22',
      '--t-green': '#2f5a24', '--t-green-soft': 'rgba(58,107,46,0.13)',
      // Unprefixed tone aliases — many verdict chips, tint discs and the safe-pace
      // gradient reference these names. Pointed at the --t-* values above so the
      // palette can't drift into two greens/ambers.
      '--green': '#2f5a24', '--green-soft': 'rgba(58,107,46,0.13)',
      '--amber': '#6f5210', '--amber-soft': 'rgba(168,123,34,0.15)',
      '--t-amber': '#6f5210', '--t-amber-soft': 'rgba(168,123,34,0.15)',
      '--t-red': '#a32a18', '--t-red-soft': 'rgba(163,42,24,0.11)',
      '--display': "'Oswald', sans-serif",
      '--num': "'IBM Plex Sans', system-ui, sans-serif",
      '--title-font': "'IBM Plex Sans', system-ui, sans-serif",
      '--title-transform': 'none', '--title-spacing': '-0.03em', '--eyebrow-spacing': '0.16em',
      '--radius': '20px', '--radius-ctl': '14px',
      '--card-shadow': '0 1px 3px rgba(20,18,12,0.04), 0 8px 22px rgba(20,18,12,0.05)',
      '--card-border': 'none', '--chrome-bg': 'rgba(241,237,230,0.9)',
    },
  },
};

const themeVars = (k) => (THEMES[k] || THEMES.paper).vars;
const themeFlags = (k) => (THEMES[k] || THEMES.paper).flags;
const getStyle = (k) => themeFlags(k);

// Background tweak, every direction can sit on pure white or the warm off-white.
const FC_BG = { white: '#ffffff', offwhite: '#f9f7f2' };
const FCBgContext = React.createContext('offwhite');

// Phone, screen frame; applies the active theme's CSS vars at the root, then
// overrides the page surface (--paper-2) from the global Background tweak. On the
// flat Full-Editorial layout the content sits on that surface, so --paper follows too.
function Phone({ styleKey = 'paper', label, children }) {
  const bg = React.useContext(FCBgContext);
  const surface = FC_BG[bg] || FC_BG.offwhite;
  const flat = themeFlags(styleKey).layout === 'full';
  const vars = { ...themeVars(styleKey), '--paper-2': surface };
  if (flat) vars['--paper'] = surface;
  return <div className="phone" data-screen-label={label} style={{ ...vars, background: 'var(--paper-2)' }}>{children}</div>;
}

function buildHistory(state) {
  const daily = state.weeklyVel / 7;
  const noise = [0.18,-0.12,0.05,-0.22,0.14,-0.06,0.20,-0.16,0.02,0.12,-0.20,0.08,-0.04,0.16,-0.14,0.06,-0.18,0.10,-0.02,0.14,-0.10,0.04,-0.12,0];
  const n = FC.todayDay + 1;   // every day from camp start (day 0) through today
  return Array.from({ length: n }, (_, i) => [i, +(state.current - daily * (n - 1 - i) + (i === n - 1 ? 0 : noise[i % noise.length])).toFixed(2)]);
}
function smooth(hist) {
  return hist.map((p, i, a) => {
    const win = a.slice(Math.max(0, i - 2), i + 1);
    let w = 0, tw = 0;
    win.forEach((x, j) => { const k = Math.pow(0.75, win.length - 1 - j); w += x[1] * k; tw += k; });
    return [p[0], w / tw];
  });
}
function computePace(stateKey) {
  const state = PACE_STATES[stateKey] || PACE_STATES.onTrack;
  const hist = buildHistory(state), tr = smooth(hist);
  const trendingToday = tr[tr.length - 1][1];
  const dailyVel = state.weeklyVel / 7;
  const projected = trendingToday + dailyVel * (FC.campDays - FC.todayDay);
  // Cycle-aware projection (Logic & Flows §4.4/§7): weigh-in lands in a cycle phase
  // that adds/removes water. The honest SCALE projection is the true-weight
  // projection plus the weigh-in-day water offset; the buffer is the extra true
  // weight you must pre-cut so the scale still reads the limit.
  const weighInOffset = cycleOffsetKg(FC.campDays);
  const projectedScale = projected + weighInOffset;
  const cycleBuffer = Math.max(0, weighInOffset);
  const trueTarget = FC.goalWeight - cycleBuffer;
  return {
    state, hist, trend: tr, trendingToday, dailyVel, projected, gap: projected - FC.goalWeight,
    toGo: trendingToday - FC.goalWeight, weeklyVel: state.weeklyVel,
    tone: state.tone, badge: state.badge, daysOut: FC.daysOut,
    weighInOffset, projectedScale, cycleBuffer, trueTarget,
    scaleGap: projectedScale - FC.goalWeight,
  };
}

// ── type primitives ──────────────────────────────────────────────────────────
function Eyebrow({ children, color = 'var(--ink-3)', style = {} }) {
  return <div style={{ fontFamily: 'var(--display)', fontSize: 11, fontWeight: 600, letterSpacing: 'var(--eyebrow-spacing)', textTransform: 'uppercase', color, ...style }}>{children}</div>;
}
function Num({ children, size = 40, color = 'var(--ink)', weight = 300, style = {} }) {
  return <span style={{ fontFamily: 'var(--num)', fontFeatureSettings: '"tnum"', fontWeight: weight, letterSpacing: '-0.02em', fontSize: size, lineHeight: 0.92, color, ...style }}>{children}</span>;
}
function Hairline({ style = {} }) { return <div role="presentation" style={{ height: 1, background: 'var(--rule)', ...style }} />; }

function ToneBadge({ tone, children, dark = false }) {
  const t = TONE[tone];
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, background: dark ? 'rgba(255,255,255,0.14)' : 'transparent', border: dark ? 'none' : `1px solid ${t.ink}`, borderRadius: 100, padding: dark ? '5px 11px 5px 9px' : '4px 10px 4px 8px', fontFamily: 'var(--display)', fontSize: 11, letterSpacing: '0.12em', color: dark ? '#fff' : t.ink, fontWeight: 600, textTransform: 'uppercase', whiteSpace: 'nowrap' }}>
      <span style={{ width: 6, height: 6, borderRadius: '50%', background: dark ? '#fff' : t.ink }} />{children}
    </span>
  );
}

// motif overlays
function CornerTicks({ color = 'var(--accent)', inset = 12, size = 10, stroke = 1.5 }) {
  const base = { position: 'absolute', width: size, height: size, pointerEvents: 'none' };
  const path = (d) => <path d={d} stroke={color} strokeWidth={stroke} fill="none" strokeLinecap="square" />;
  return (<>
    <svg style={{ ...base, top: inset, left: inset }} viewBox="0 0 10 10">{path('M0 4 L0 0 L4 0')}</svg>
    <svg style={{ ...base, top: inset, right: inset }} viewBox="0 0 10 10">{path('M6 0 L10 0 L10 4')}</svg>
    <svg style={{ ...base, bottom: inset, left: inset }} viewBox="0 0 10 10">{path('M0 6 L0 10 L4 10')}</svg>
    <svg style={{ ...base, bottom: inset, right: inset }} viewBox="0 0 10 10">{path('M6 10 L10 10 L10 6')}</svg>
  </>);
}
function Hatch({ opacity = 0.05, color = '#ffffff' }) {
  const svg = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M-1 1 L1 -1 M0 8 L8 0 M7 9 L9 7' stroke='${encodeURIComponent(color)}' stroke-width='1'/%3E%3C/svg%3E")`;
  return <div aria-hidden="true" style={{ position: 'absolute', inset: 0, backgroundImage: svg, opacity, pointerEvents: 'none', borderRadius: 'inherit' }} />;
}
function GridOverlay({ color = 'rgba(255,255,255,0.05)' }) {
  const svg = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath d='M24 0H0V24' fill='none' stroke='${encodeURIComponent(color)}' stroke-width='1'/%3E%3C/svg%3E")`;
  return <div aria-hidden="true" style={{ position: 'absolute', inset: 0, backgroundImage: svg, pointerEvents: 'none', borderRadius: 'inherit' }} />;
}
function HeroMotif({ motif }) {
  if (motif === 'poster') return <><CornerTicks /><Hatch opacity={0.05} /></>;
  if (motif === 'grid') return <GridOverlay />;
  if (motif === 'grid-light') return <GridOverlay color="rgba(16,22,32,0.045)" />;
  return null;
}

// Card, themed surface. `inverted` = ink-on-paper hero (Fighter only).
function Card({ pad = 20, inverted = false, tint = null, style = {}, children }) {
  return (
    <div style={{
      background: inverted ? 'var(--ink)' : tint || 'var(--paper)',
      color: inverted ? 'var(--paper)' : 'var(--ink)',
      borderRadius: 'var(--radius)', padding: pad, position: 'relative',
      boxShadow: inverted ? '0 10px 30px rgba(0,0,0,0.22)' : 'var(--card-shadow)',
      border: inverted ? 'none' : 'var(--card-border)',
      display: 'flex', flexDirection: 'column', ...style,
    }}>{children}</div>
  );
}

function SectionHeader({ children, trailing = null }) {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: '0 4px 9px' }}>
      <Eyebrow>{children}</Eyebrow>{trailing}
    </div>
  );
}

// Stat, canonical stat column. Left-rule divider when i>0. Optional sub caption.
function Stat({ label, value, unit, color = 'var(--ink)', sub = null, subColor = 'var(--ink-3)', i = 0, valueSize = 26 }) {
  return (
    <div style={{ flex: 1, minWidth: 0, paddingLeft: i ? 16 : 0, borderLeft: i ? '1px solid var(--rule)' : 'none' }}>
      <div style={{ fontFamily: 'var(--display)', fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: 'var(--ink-3)', fontWeight: 600, whiteSpace: 'nowrap' }}>{label}</div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 3, marginTop: 9, whiteSpace: 'nowrap' }}>
        <span style={{ fontFamily: 'var(--num)', fontWeight: 300, fontSize: valueSize, letterSpacing: '-0.02em', color }}>{value}</span>
        {unit && <span style={{ fontFamily: 'var(--mono)', fontSize: 9.5, color: 'var(--ink-3)', fontWeight: 600 }}>{unit}</span>}
      </div>
      {sub && <div style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.06em', textTransform: 'uppercase', color: subColor, fontWeight: 600, marginTop: 7 }}>{sub}</div>}
    </div>
  );
}

// ── device chrome ────────────────────────────────────────────────────────────
function StatusBar() {
  const c = 'var(--ink)';
  return (
    <div style={{ height: 50, display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: '0 26px 8px', fontFamily: 'system-ui', fontSize: 15, fontWeight: 600, color: c, flexShrink: 0 }}>
      <span>9:41</span>
      <div style={{ display: 'flex', gap: 7, alignItems: 'center' }}>
        <svg width="18" height="11" viewBox="0 0 18 11" fill="none"><rect x="1" y="3" width="2" height="6" rx="0.5" fill={c}/><rect x="5" y="1" width="2" height="8" rx="0.5" fill={c}/><rect x="9" y="3" width="2" height="6" rx="0.5" fill={c}/><rect x="13" y="5" width="2" height="4" rx="0.5" fill={c}/></svg>
        <svg width="23" height="11" viewBox="0 0 23 11" fill="none"><rect x="0.5" y="0.8" width="19" height="9.4" rx="2.2" stroke={c} fill="none"/><rect x="2" y="2.3" width="14" height="6.4" rx="1.2" fill={c}/><rect x="20" y="3.4" width="2" height="4.2" rx="0.5" fill={c}/></svg>
      </div>
    </div>
  );
}
function HomeIndicator() {
  return <div style={{ height: 26, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', paddingBottom: 8, flexShrink: 0 }}><div style={{ width: 140, height: 5, background: 'var(--ink)', borderRadius: 3, opacity: 0.82 }} /></div>;
}
function LargeTitle({ eyebrow, title, trailing = null, eyebrowColor = 'var(--accent)' }) {
  return (
    <div style={{ padding: '8px 20px 14px', flexShrink: 0 }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
        <div>
          {eyebrow && <Eyebrow color={eyebrowColor} style={{ marginBottom: 7 }}>{eyebrow}</Eyebrow>}
          <div style={{ fontFamily: 'var(--title-font)', fontSize: 27, fontWeight: 700, letterSpacing: 'var(--title-spacing)', textTransform: 'var(--title-transform)', color: 'var(--ink)', lineHeight: 1.05 }}>{title}</div>
        </div>
        {trailing}
      </div>
    </div>
  );
}
function CompactNav({ title, onBack = true, trailing = null }) {
  return (
    <div style={{ height: 46, padding: '0 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
      <div style={{ minWidth: 72, display: 'flex' }}>
        {onBack && <button aria-label="Back" style={{ width: 44, height: 44, display: 'flex', alignItems: 'center' }}><svg width="11" height="18" viewBox="0 0 11 18" fill="none"><path d="M9 1 L2 9 L9 17" stroke="var(--ink)" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg></button>}
      </div>
      <span style={{ fontFamily: 'var(--display)', fontSize: 14, letterSpacing: '0.08em', textTransform: 'uppercase', fontWeight: 600, whiteSpace: 'nowrap' }}>{title}</span>
      <div style={{ minWidth: 72, display: 'flex', justifyContent: 'flex-end' }}>{trailing}</div>
    </div>
  );
}
function NavAction({ children, onClick }) { return <button onClick={onClick} style={{ fontFamily: 'var(--sans)', fontSize: 15, color: 'var(--accent)', fontWeight: 500, minHeight: 44, minWidth: 44, padding: '0 8px', margin: '0 -8px', display: 'inline-flex', alignItems: 'center', justifyContent: 'flex-end' }}>{children}</button>; }
// AvatarButton — the single canonical door to Account/Settings (S7). Uses the
// app's bespoke per-sport athlete mark (CoachMark) in avatar form — circular,
// ink ground, paper line work, red accent — so it matches the brand, not a
// generic pictogram. Reads the active sport.
function GearButton() {
  const raw = (window.__sport || (window.FC && window.FC.sport) || 'MMA');
  const M = window.CoachMark;
  return <button aria-label="Account" style={{ width: 44, height: 44, borderRadius: '50%', background: 'var(--paper)', boxShadow: 'var(--card-shadow)', border: '1px solid var(--rule)', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
    {M ? <M sport={raw} size={38} bg="transparent" ink="var(--ink-2)" accent="var(--accent)" /> : <span style={{ fontFamily: 'var(--display)', fontSize: 14, fontWeight: 600, color: 'var(--ink-2)' }}>A</span>}
  </button>;
}
function TabBar({ active = 'home' }) {
  const tabs = [{ id: 'home', label: 'Today', icon: 'today' }, { id: 'plan', label: 'Plan', icon: 'plan' }, { id: 'add', label: 'Log', icon: 'log' }, { id: 'progress', label: 'Trend', icon: 'trend' }, { id: 'more', label: 'More', icon: 'more' }];
  const Icon = window.UIIcon;
  return (
    <div style={{ borderTop: '1px solid var(--rule)', background: 'var(--chrome-bg)', backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)', padding: '9px 0 4px', display: 'flex', flexShrink: 0, minHeight: 54 }}>
      {tabs.map(t => {
        const on = t.id === active, col = on ? 'var(--accent)' : 'var(--ink-3)';
        return (
          <button key={t.id} aria-current={on ? 'page' : undefined} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, color: col, minHeight: 44 }}>
            <span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '3px 16px', borderRadius: 11, background: on ? 'var(--accent-soft)' : 'transparent' }}>
              {Icon ? <Icon name={t.icon} size={25} color={col} accent={on ? 'var(--accent)' : 'var(--ink-3)'} /> : <TabIcon id={t.id} active={on} />}
            </span>
            <span style={{ fontFamily: 'var(--sans)', fontSize: 10, letterSpacing: '0.02em', fontWeight: on ? 700 : 500 }}>{t.label}</span>
          </button>
        );
      })}
    </div>
  );
}
function TabIcon({ id, active }) {
  const s = active ? 'var(--ink)' : 'var(--ink-3)', sw = active ? 1.7 : 1.4;
  if (id === 'home') return (<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M4 10.5 L12 4 L20 10.5 L20 19.5 L4 19.5 Z" stroke={s} strokeWidth={sw} strokeLinejoin="round"/></svg>);
  if (id === 'plan') return (<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect x="4.5" y="4" width="15" height="16" rx="2" stroke={s} strokeWidth={sw}/><line x1="8" y1="8.5" x2="16" y2="8.5" stroke={s} strokeWidth={sw} strokeLinecap="round"/><line x1="8" y1="12" x2="16" y2="12" stroke={s} strokeWidth={sw} strokeLinecap="round"/><line x1="8" y1="15.5" x2="13" y2="15.5" stroke={s} strokeWidth={sw} strokeLinecap="round"/></svg>);
  if (id === 'add') return (<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8.5" stroke={s} strokeWidth={sw}/><line x1="12" y1="8" x2="12" y2="16" stroke={s} strokeWidth={sw} strokeLinecap="round"/><line x1="8" y1="12" x2="16" y2="12" stroke={s} strokeWidth={sw} strokeLinecap="round"/></svg>);
  if (id === 'progress') return (<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M3.5 18.5 C 8 18.5, 10 11, 13 10 S 18 6, 20.5 4" stroke={s} strokeWidth={sw} strokeLinecap="round" fill="none"/><circle cx="20.5" cy="4" r={active ? 1.8 : 1.4} fill={s}/></svg>);
  if (id === 'more') return (<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><circle cx="6" cy="12" r="1.5" fill={s}/><circle cx="12" cy="12" r="1.5" fill={s}/><circle cx="18" cy="12" r="1.5" fill={s}/></svg>);
  return null;
}

// PrimaryButton, adaptive high-contrast filled (ink bg / paper text), accent plus.
function PrimaryButton({ label, plus = false, big = false, style = {} }) {
  return (
    <button style={{ width: '100%', background: 'var(--ink)', color: 'var(--paper)', border: 'none', borderRadius: 'var(--radius-ctl)', padding: '0 18px', minHeight: big ? 58 : 52, display: 'flex', alignItems: 'center', justifyContent: plus ? 'space-between' : 'center', gap: 12, ...style }}>
      <span style={{ fontFamily: 'var(--sans)', fontSize: big ? 17 : 16, fontWeight: 600, letterSpacing: '0.01em' }}>{label}</span>
      {plus && <span aria-hidden="true" style={{ width: big ? 28 : 24, height: big ? 28 : 24, background: 'var(--accent)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}><svg width="11" height="11" viewBox="0 0 10 10" fill="none"><line x1="5" y1="1.5" x2="5" y2="8.5" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/><line x1="1.5" y1="5" x2="8.5" y2="5" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/></svg></span>}
    </button>
  );
}
// HeroButton, THE daily-weigh-in CTA — the one red action the whole app is built
// around. Distinct from PrimaryButton (ink): red accent fill, white plus-disc,
// Oswald label. Use ONLY for logging today's weight (Today, empty, trend).
function HeroButton({ label = 'ADD TODAY’S WEIGHT', onClick, style = {} }) {
  return (
    <button onClick={onClick} style={{ width: '100%', minHeight: 58, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12, border: 'none', borderRadius: 16, background: 'var(--accent)', color: '#fff', ...style }}>
      <span aria-hidden="true" style={{ width: 24, height: 24, background: 'rgba(255,255,255,0.22)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
        <svg width="11" height="11" viewBox="0 0 10 10" fill="none"><line x1="5" y1="1.5" x2="5" y2="8.5" stroke="#fff" strokeWidth="1.6" strokeLinecap="round"/><line x1="1.5" y1="5" x2="8.5" y2="5" stroke="#fff" strokeWidth="1.6" strokeLinecap="round"/></svg>
      </span>
      <span style={{ fontFamily: 'var(--display)', fontSize: 15, letterSpacing: '0.13em', fontWeight: 600, textTransform: 'uppercase', whiteSpace: 'nowrap' }}>{label}</span>
    </button>
  );
}
function TextButton({ children }) { return <button style={{ fontFamily: 'var(--sans)', fontSize: 15, color: 'var(--accent)', fontWeight: 600, minHeight: 44, display: 'inline-flex', alignItems: 'center', margin: '-13px 0' }}>{children}</button>; }

function GroupedList({ children }) {
  return <div style={{ background: 'var(--paper)', borderRadius: 'var(--radius)', boxShadow: 'var(--card-shadow)', border: 'var(--card-border)', overflow: 'hidden' }}>{children}</div>;
}

// AvatarList / AvatarRow — selectable or navigational list of separated rounded
// cards, each led by an icon in a tinted tile. The app's standard treatment for
// any list whose options carry a distinct visual identity (sports, menu sections,
// strategies). mode 'select' shows a radio-check and an accent ring when active;
// mode 'nav' shows a chevron. Pass `icon` as a rendered node (a SportGlyph, a
// UIIcon, initials), sized ~26. Keep it for icon-bearing lists; a plain list with
// no per-row identity should still use GroupedList.
function AvatarList({ children, gap = 8 }) {
  return <div style={{ display: 'flex', flexDirection: 'column', gap }}>{children}</div>;
}
function AvatarRow({ label, sub, icon, on = false, mode = 'select', onClick, tone = 'var(--accent)' }) {
  const active = mode === 'select' && on;
  return (
    <button onClick={onClick} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: sub ? '11px 14px' : '12px 14px', background: 'var(--paper)', border: `1.5px solid ${active ? tone : 'var(--rule)'}`, borderRadius: 14, minHeight: 60, width: '100%', textAlign: 'left', cursor: 'pointer' }}>
      {icon && <span style={{ flexShrink: 0, width: 40, height: 40, borderRadius: 11, background: active ? tone : 'var(--paper-2)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: active ? 'var(--paper)' : 'var(--ink-2)' }}>{icon}</span>}
      <span style={{ flex: 1, minWidth: 0 }}>
        <span style={{ display: 'block', fontFamily: 'var(--sans)', fontSize: 16, fontWeight: on ? 600 : 500, color: 'var(--ink)' }}>{label}</span>
        {sub && <span style={{ display: 'block', fontFamily: 'var(--sans)', fontSize: 12.5, color: 'var(--ink-3)', marginTop: 2, textWrap: 'pretty' }}>{sub}</span>}
      </span>
      {mode === 'select'
        ? <span style={{ width: 21, height: 21, borderRadius: '50%', flexShrink: 0, border: on ? 'none' : '1.6px solid var(--rule-2)', background: on ? tone : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>{on && <svg width="10" height="8" viewBox="0 0 11 9" fill="none"><path d="M1 4.5 L4 7.5 L10 1" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>}</span>
        : <svg width="7" height="12" viewBox="0 0 7 12" fill="none" style={{ flexShrink: 0 }}><path d="M1 1 L6 6 L1 11" stroke="var(--rule-2)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}
    </button>
  );
}

// StepList, shared protocol timeline (descent, rebuild). One vocabulary everywhere.
// states: 'now'/'today' = live (green halo), 'goal' = endpoint (gold), else upcoming.
function StepList({ steps }) {
  return (
    <div>
      {steps.map((d, i) => {
        const hot = d.state === 'now' || d.state === 'today', goal = d.state === 'goal';
        const dot = goal ? 'var(--accent)' : hot ? 'var(--t-green)' : 'var(--rule-2)';
        return (
          <div key={i} style={{ display: 'flex', gap: 14, alignItems: 'flex-start', padding: '13px 0', borderTop: i ? '1px solid var(--rule)' : 'none' }}>
            <span style={{ width: 54, fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '0.06em', color: hot ? 'var(--ink)' : 'var(--ink-3)', fontWeight: 700, paddingTop: 3 }}>{d.label}</span>
            <span style={{ width: 9, height: 9, borderRadius: '50%', background: dot, marginTop: 5, flexShrink: 0, boxShadow: hot ? '0 0 0 4px var(--t-green-soft)' : 'none' }} />
            <div style={{ flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <span style={{ fontFamily: 'var(--sans)', fontSize: 15.5, fontWeight: hot || goal ? 600 : 500, color: 'var(--ink)' }}>{d.title}</span>
                {d.tag && <span style={{ fontFamily: 'var(--display)', fontSize: 9, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--t-green)', fontWeight: 700, border: '1px solid var(--t-green)', borderRadius: 100, padding: '1px 7px' }}>{d.tag}</span>}
              </div>
              <div style={{ fontFamily: 'var(--sans)', fontSize: 13, color: 'var(--ink-3)', marginTop: 3 }}>{d.detail}</div>
            </div>
          </div>
        );
      })}
    </div>
  );
}
function Row({ label, value, valueColor = 'var(--ink)', chevron = true, note = null, control = null, last = false, onClick = null }) {
  const body = (
    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, padding: '0 18px', minHeight: 50 }}>
      <span style={{ fontFamily: 'var(--sans)', fontSize: 15.5, color: 'var(--ink)', fontWeight: 400, whiteSpace: 'nowrap' }}>{label}</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
        {control || (value != null && <span style={{ fontFamily: 'var(--sans)', fontSize: 15.5, fontWeight: 500, color: valueColor, fontFeatureSettings: '"tnum"', whiteSpace: 'nowrap' }}>{value}</span>)}
        {chevron && !control && <svg width="7" height="12" viewBox="0 0 7 12" fill="none"><path d="M1 1 L6 6 L1 11" stroke="var(--rule-2)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}
      </div>
    </div>
  );
  return (
    <div>
      {onClick
        ? <button type="button" onClick={onClick} style={{ display: 'block', width: '100%', textAlign: 'left', background: 'none', border: 'none', padding: 0, font: 'inherit', color: 'inherit', cursor: 'pointer' }}>{body}</button>
        : body}
      {note && <div style={{ fontFamily: 'var(--sans)', fontSize: 12.5, color: 'var(--ink-3)', lineHeight: 1.5, padding: '0 18px 13px', marginTop: -3, textWrap: 'pretty' }}>{note}</div>}
      {!last && <div style={{ height: 1, background: 'var(--rule)', marginLeft: 18 }} />}
    </div>
  );
}
function Toggle({ on: onProp = true, onChange }) {
  // S3 — a real switch: flips on tap (local state), mirrors an external `on` when it changes.
  const [on, setOn] = React.useState(onProp);
  React.useEffect(() => setOn(onProp), [onProp]);
  const flip = (e) => { e.stopPropagation(); setOn(v => { const nv = !v; onChange && onChange(nv); return nv; }); };
  return <button type="button" role="switch" aria-checked={on} onClick={flip} style={{ width: 50, height: 30, borderRadius: 15, background: on ? 'var(--t-green)' : 'var(--rule-2)', position: 'relative', flexShrink: 0, border: 'none', padding: 0, cursor: 'pointer', transition: 'background 200ms ease' }}><span style={{ position: 'absolute', top: 2.5, left: on ? 22.5 : 2.5, width: 25, height: 25, borderRadius: '50%', background: '#fff', boxShadow: '0 1px 3px rgba(0,0,0,0.25)', transition: 'left 200ms ease' }} /></button>;
}
function ProgressBar({ pct = 0.5, tone = 'green' }) {
  return <div style={{ height: 6, background: 'var(--rule)', borderRadius: 4, overflow: 'hidden' }}><div style={{ width: `${Math.max(3, Math.min(100, pct * 100))}%`, height: '100%', background: TONE[tone].ink, borderRadius: 4 }} /></div>;
}

// CoachTip, the personalisation/intelligence surface. A named coach (Donnelly)
// “reads” the athlete’s own inputs and explains the reasoning. The data chip is the
// proof signal: it shows the tip was derived from THIS athlete (their sport, their
// timeline, their numbers), not a generic blurb. Used heavily through onboarding to
// earn trust before the paywall, and in results/dashboards after.
//   chip    , the input this tip reacts to, e.g. 'Boxing', '8 weeks out', '0.5% / wk'
//   headline, the insight, in plain corner-man language
//   body    , the why
//   tone    , 'accent' (default) | 'green' | 'amber' | 'red' colours the eyebrow + chip
//   coach   , false hides the avatar/byline for a lighter inline tip
// The coach voice gets ONE distinct, consistent surface across the whole app: a
// tone-tinted flat panel (not the white, shadowed surface normal cards use) with a
// hairline keyed to the tone and a small coach mark. Same recipe everywhere so the
// athlete always recognises "the coach is talking" at a glance.
// `bullets` (array) renders a skimmable list instead of a paragraph `body`.
// `note` is a small muted line below (disclaimers, "not medical advice").
// Canonical "read card" list primitives, shared by the biological-sex read AND
// every coach card so the cards are literally one component family: a green check
// for a recommended/affirmative point, a muted ✕ for a not-recommended one, sans
// line alongside. SubLabel is the bold sentence-case section heading.
function StratRow({ ok = true, label }) {
  return (
    <li style={{ display: 'flex', gap: 9, alignItems: 'flex-start', fontFamily: 'var(--sans)', fontSize: 13, lineHeight: 1.4, color: 'var(--ink-2)' }}>
      <span style={{ flexShrink: 0, marginTop: 1, color: ok ? '#2f5a24' : 'var(--ink-3)', display: 'inline-flex' }}>
        {ok
          ? <svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><path d="M2 7 L5 10 L11 3" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"/></svg>
          : <svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true"><path d="M3.2 3.2 L9.8 9.8 M9.8 3.2 L3.2 9.8" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round"/></svg>}
      </span>
      <span style={{ flex: 1, textWrap: 'pretty' }}>{label}</span>
    </li>
  );
}
function SubLabel({ children, top }) {
  return <div style={{ fontFamily: 'var(--sans)', fontSize: 14, fontWeight: 700, letterSpacing: '-0.01em', color: 'var(--ink)', marginTop: top || 0, marginBottom: 6 }}>{children}</div>;
}
// SourceTag — the citation line, pinned to the BOTTOM of a proof/stat card with a
// small document indicator so every card cites its source the same way. `rule` sets
// the divider colour to match the host card's surface.
function SourceTag({ src, rule = 'var(--rule)', color = 'var(--ink-3)', top = 14 }) {
  if (!src) return null;
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: top, paddingTop: 11, borderTop: `1px solid ${rule}` }}>
      <svg width="11" height="12" viewBox="0 0 12 13" fill="none" aria-hidden="true" style={{ flexShrink: 0 }}>
        <path d="M2.5 1.5 H7 L9.5 4 V11.5 H2.5 Z" stroke={color} strokeWidth="1" strokeLinejoin="round" />
        <path d="M7 1.5 V4 H9.5" stroke={color} strokeWidth="1" strokeLinejoin="round" />
        <path d="M4.2 6.4 H7.8 M4.2 8.4 H7.8" stroke={color} strokeWidth="1" strokeLinecap="round" />
      </svg>
      <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.03em', color, fontWeight: 500 }}>Source · {src}</span>
    </div>
  );
}

// The coach card and the biological-sex read card are now ONE design: a warm-sand
// flat panel (the default accent surface, #e7d9c6), a single muted display tag at
// the top — no coach avatar, no mono chip — a sans headline, a hairline, then the
// SAME StratRow green-tick list the read card uses. Warning tones (amber/red) keep
// a tinted surface + quiet dots so a caution never reads as an endorsement.
function CoachTip({ chip, headline, body, bullets, note, tone = 'accent', surface = null, coach = true, compact = false, sport = null, eyebrow = true, label }) {
  const warn = tone === 'amber' || tone === 'red';
  // `surface` overrides only the card fill, so a caution can sit on beige while
  // `tone` still drives the quiet-dot markers and accent colour.
  const bgTone = surface || tone;
  const bg = bgTone === 'accent' ? '#e7d9c6' : (TONE[bgTone] ? TONE[bgTone].soft : '#e7d9c6');
  const tc = tone === 'accent' ? 'var(--accent)' : (TONE[tone] ? TONE[tone].ink : 'var(--accent)');
  const hair = 'rgba(21,20,15,0.12)';
  const list = bullets ? bullets.filter(Boolean) : null;
  // No decorative tag by default — the headline carries the message. A tag renders
  // only when an explicit `label` is passed (and is worth the line).
  const tag = (label && eyebrow !== false) ? label : null;
  return (
    <div style={{ background: bg, borderRadius: 'var(--radius)', boxShadow: 'none', padding: compact ? '18px 18px 20px' : '20px 20px 22px', position: 'relative' }}>
      {tag && (
        <div style={{ fontFamily: 'var(--display)', fontSize: 10.5, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'var(--ink-3)', fontWeight: 600, marginBottom: 12 }}>{tag}</div>
      )}
      <div style={{ fontFamily: 'var(--sans)', fontSize: compact ? 16 : 18, fontWeight: 600, lineHeight: 1.3, color: 'var(--ink)', letterSpacing: '-0.01em', textWrap: 'balance' }}>{headline}</div>
      {body && !list && <div style={{ fontFamily: 'var(--sans)', fontSize: 13.5, lineHeight: 1.55, color: 'var(--ink-2)', marginTop: 9, textWrap: 'pretty' }}>{body}</div>}
      {list && (
        <React.Fragment>
          <div style={{ height: 1, background: hair, margin: '16px 0 12px' }} />
          <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
            {list.map((b, i) => warn
              ? <li key={i} style={{ display: 'flex', gap: 9, alignItems: 'flex-start' }}><span style={{ flexShrink: 0, width: 5, height: 5, borderRadius: '50%', background: tc, marginTop: 7 }} /><span style={{ fontFamily: 'var(--sans)', fontSize: 13, lineHeight: 1.4, color: 'var(--ink-2)', textWrap: 'pretty' }}>{b}</span></li>
              : <StratRow key={i} ok label={b} />)}
          </ul>
        </React.Fragment>
      )}
      {note && <div style={{ fontFamily: 'var(--sans)', fontSize: 11.5, lineHeight: 1.5, color: 'var(--ink-3)', marginTop: 13, textWrap: 'pretty' }}>{note}</div>}
    </div>
  );
}

// DashBanner, OnWeight's full-bleed phase strip. One shape across every Today
// view; colour encodes the camp phase so the state is legible at a glance. A soft
// tint (not a solid fill) keeps it quiet, accent/red still reads as fight week.
//   tone , 'ink' (active camp) | 'accent' (fight wk/day) | 'green' (made/rebuild)
//           | 'amber' (missed) | 'muted' (no camp / paused / recovery)
//   label, phase, e.g. 'IN CAMP'         meta, countdown, e.g. '32 DAYS OUT'
//   icon , optional leading glyph (uses currentColor)
function DashBanner({ tone = 'ink', label, meta = null, icon = null }) {
  const C = {
    ink:    { bg: 'rgba(21,20,15,0.05)',  fg: 'var(--ink-2)' },
    accent: { bg: 'var(--accent-soft)',   fg: 'var(--accent)' },
    green:  { bg: 'var(--t-green-soft)',  fg: 'var(--t-green)' },
    amber:  { bg: 'var(--t-amber-soft)',  fg: 'var(--t-amber)' },
    muted:  { bg: 'rgba(21,20,15,0.04)',  fg: 'var(--ink-3)' },
  }[tone] || { bg: 'rgba(21,20,15,0.05)', fg: 'var(--ink-2)' };
  return (
    <div style={{ background: C.bg, color: C.fg, padding: '7px 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexShrink: 0 }}>
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
        {icon}
        <span style={{ fontFamily: 'var(--display)', fontSize: 11, letterSpacing: '0.16em', fontWeight: 600, whiteSpace: 'nowrap' }}>{label}</span>
      </span>
      {meta && <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.08em', fontWeight: 600, opacity: 0.8, whiteSpace: 'nowrap' }}>{meta}</span>}
    </div>
  );
}

Object.assign(window, {
  FC, PACE_STATES, TONE, SHADOW, THEMES, themeVars, themeFlags, getStyle, Phone, FCBgContext, FC_BG,
  buildHistory, smooth, computePace,
  SourceTag,
  cycleDayAt, cycleOffsetKg, cyclePhaseAt, CYCLE_PHASE, weighInClock, fmtClock, weighInOffWindow, FC_DOUBLE_DAY_OFFSET,
  Eyebrow, Num, Hairline, ToneBadge, CornerTicks, Hatch, GridOverlay, HeroMotif, Card, SectionHeader, Stat,
  StatusBar, HomeIndicator, LargeTitle, CompactNav, NavAction, GearButton, DashBanner,
  TabBar, TabIcon, PrimaryButton, HeroButton, TextButton, GroupedList, AvatarList, AvatarRow, Row, Toggle, ProgressBar, StepList, CoachTip, StratRow, SubLabel,
});
