// rotation-v6.jsx - v6 Rotation Maker page. All top-level names are ROT_*
// to satisfy the single global lexical scope rule in CLAUDE.md.
// v2 (2026-07-08): position-lane drag editor matching thehoopsgeek.com/rotation-maker
// (5 position lanes PG/SG/SF/PF/C, quarters in a 2x2 grid, drag bench chips in + stretch).
// grid {playerId:[[s,e]]} stays canonical for analysis/print; `assign` holds the layout.
// v8.53 (v9 pass, design_handoff_v9/ 1a–1d): /rotation leaves the warm-paper world —
// tool chrome (S1), dark SAMPLE bar (S2), player hue as rationed DATA color
// (ROT_TINT/ROT_RING stints, white bench rows w/ hue dot), eligibility flip (S6),
// quiet-valid/washed-invalid banner naming the open lane (S7), save-failed retry (S5),
// buttons on the V6_Btn kit, grayscale-safe print card (1d). Pointer model + every
// control kept verbatim per V9-PARITY-DELTA.md §1.
// v8.62 (Tim 07-25): POSITIONS REMOVED from the UI — lanes are five anonymous floor
// spots (labels blank, eligibility gone, everyone placeable anywhere; ROT_POSITIONS
// keys kept as internal lane ids for saved-plan compat). NEW: game attach — a plan
// stores gameId (cla_teams.calendar event id) + gamePhase 'pregame'|'postgame';
// picker row under the toolbar, phase in the print meta, surfaced on the game sheet.

const ROT_BG = '#101014';
const ROT_INK = '#f7f4ec';
const ROT_RED = '#ff5c5c';
const ROT_GREEN = '#2f8a3e';
const ROT_LOCAL_KEY = 'cla.rotation.draft.v1';

const ROT_POSITIONS = ['PG', 'SG', 'SF', 'PF', 'C'];
// chip palette (cycles per roster index) — the player-identity DATA palette. Since
// v8.53 it is NEVER a full-saturation fill: stints use ROT_TINT(c) + ROT_RING(c) +
// a 3px solid hue edge; bench rows a 10px hue dot (V9-TOKEN-NOTES.md color rules).
const ROT_CHIP_COLORS = ['#e8663d', '#e79a2b', '#e7c33d', '#5aa668', '#2fa3a3', '#5aa6e8', '#4f6fc0', '#7b5ec0', '#c05e9e', '#9099a8'];
const ROT_TINT = (c) => `color-mix(in srgb, ${c} 13%, #fff)`;
const ROT_RING = (c) => `color-mix(in srgb, ${c} 38%, transparent)`;

const ROT_PRESETS = {
  fiba:   { key:'fiba',   label:'FIBA 4x10', periods:[{ label:'1st Quarter', minutes:10 }, { label:'2nd Quarter', minutes:10 }, { label:'3rd Quarter', minutes:10 }, { label:'4th Quarter', minutes:10 }] },
  nba:    { key:'nba',    label:'4x12',      periods:[{ label:'1st Quarter', minutes:12 }, { label:'2nd Quarter', minutes:12 }, { label:'3rd Quarter', minutes:12 }, { label:'4th Quarter', minutes:12 }] },
  halves: { key:'halves', label:'2x20',      periods:[{ label:'1st Half', minutes:20 }, { label:'2nd Half', minutes:20 }] },
};

function ROT_uid(prefix){ return `${prefix || 'rot'}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,8)}`; }
function ROT_clone(obj){ return JSON.parse(JSON.stringify(obj)); }
function ROT_periodsForPreset(key){ const p = ROT_PRESETS[key] || ROT_PRESETS.fiba; return ROT_clone(p.periods); }
function ROT_totalMinutes(periods){ return (periods || []).reduce((sum, p) => sum + (Number(p.minutes) || 0), 0); }

// quarter/period geometry: cumulative start minute + index, for the 2x2 grid + rulers
function ROT_periodMeta(periods){
  const out = []; let cursor = 0;
  (periods || []).forEach((p, i) => { out.push({ index:i, label:p.label, start:cursor, minutes:p.minutes, end:cursor + p.minutes }); cursor += p.minutes; });
  return out;
}
function ROT_periodForMinute(meta, minute){ return meta.find((q) => minute >= q.start && minute < q.end) || meta[meta.length - 1]; }

// v8.61 (Tim 07-25): positions REMOVED from the rotation UI. The five lanes are
// now anonymous floor spots; every player is eligible everywhere. ROT_POSITIONS
// keys survive as internal lane ids so existing saved plans (assign[].pos) load
// unchanged — they are never shown to the coach anymore.
function ROT_defaultEligible(){
  return ['PG', 'SG', 'SF', 'PF', 'C']; // everyone eligible everywhere
}

// ---- assign (positional blocks) <-> grid (canonical per-player intervals) ----
// assign: [{ id, playerId, pos, start, end }]
function ROT_assignToGrid(assign){
  const byPlayer = {};
  (assign || []).forEach((b) => {
    if (!b || b.end <= b.start) return;
    (byPlayer[b.playerId] = byPlayer[b.playerId] || []).push([b.start, b.end]);
  });
  const grid = {};
  Object.keys(byPlayer).forEach((pid) => {
    const arr = byPlayer[pid].slice().sort((a, z) => a[0] - z[0]);
    const merged = [];
    arr.forEach((iv) => {
      const last = merged[merged.length - 1];
      if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]);
      else merged.push([iv[0], iv[1]]);
    });
    grid[pid] = merged;
  });
  return grid;
}

// blocks currently in one position lane, sorted by start
function ROT_laneBlocks(assign, pos){
  return (assign || []).filter((b) => b.pos === pos).sort((a, z) => a.start - z.start);
}

// per-minute count of filled positions (0..5); 5 == exactly five on the floor
function ROT_coverage(assign, total){
  const out = new Array(total).fill(0);
  for (let m = 0; m < total; m++){
    let filled = 0;
    for (const pos of ROT_POSITIONS){
      if ((assign || []).some((b) => b.pos === pos && m >= b.start && m < b.end)) filled++;
    }
    out[m] = filled;
  }
  return out;
}

// Can [start,end] hold player in lane `pos`? Clamps to the lane's free space (mode-aware:
// 'resize-l'/'resize-r' clamp the moving edge to the neighbor; place/move nudge the start
// out of an occupied slot into the next gap) and rejects same-player-in-two-lanes overlaps.
// blockId excludes the block being moved. Returns {start,end} (clamped) or null.
function ROT_place(assign, blockId, playerId, pos, start, end, bounds, mode){
  const bLo = bounds ? bounds[0] : 0, bHi = bounds ? bounds[1] : Infinity;
  const occ = ROT_laneBlocks(assign, pos).filter((b) => b.id !== blockId);
  let s = start, e = end;
  if (mode === 'resize-l'){
    let lo = bLo; occ.forEach((b) => { if (b.start < e && b.end <= e) lo = Math.max(lo, b.end); });
    s = Math.max(s, lo); e = Math.min(e, bHi);
  } else if (mode === 'resize-r'){
    let hi = bHi; occ.forEach((b) => { if (b.end > s && b.start >= s) hi = Math.min(hi, b.start); });
    e = Math.min(e, hi); s = Math.max(s, bLo);
  } else { // place / move: nudge start out of any slot it lands in, then clamp to that gap
    occ.forEach((b) => { if (s >= b.start && s < b.end) s = b.end; });
    let lo = bLo, hi = bHi;
    occ.forEach((b) => { if (b.end <= s) lo = Math.max(lo, b.end); else if (b.start >= s) hi = Math.min(hi, b.start); });
    s = Math.max(s, lo); e = Math.min(e, hi);
  }
  if (e <= s) return null;
  const clash = (assign || []).some((b) => b.id !== blockId && b.playerId === playerId && b.pos !== pos && s < b.end && e > b.start);
  if (clash) return null;
  return { start: s, end: e };
}

// legacy grid (no positions) → position-lane assign, packing into eligible lanes;
// stints that can't fit go to `overflow` for the coach to place. Never loses a stint.
function ROT_gridToAssign(grid, roster){
  const assign = [];
  const overflow = [];
  const elig = {}; (roster || []).forEach((p) => { elig[p.id] = ROT_defaultEligible(); });
  // stints sorted by start so earlier ones claim lanes first
  const stints = [];
  Object.keys(grid || {}).forEach((pid) => (grid[pid] || []).forEach((iv) => stints.push({ playerId: pid, start: iv[0], end: iv[1] })));
  stints.sort((a, z) => a.start - z.start || a.end - z.end);
  stints.forEach((st) => {
    const lanes = (elig[st.playerId] || ROT_POSITIONS);
    let placed = null;
    for (const pos of lanes){
      const r = ROT_place(assign, null, st.playerId, pos, st.start, st.end);
      if (r && r.start === st.start && r.end === st.end){ placed = { id: ROT_uid('b'), playerId: st.playerId, pos, start: r.start, end: r.end }; break; }
    }
    if (placed) assign.push(placed);
    else overflow.push({ id: ROT_uid('b'), playerId: st.playerId, pos: null, start: st.start, end: st.end });
  });
  return { assign, overflow };
}

function ROT_rosterCandidates(team){
  // Real roster lives in default_roster.rosterGroups[*].players (POINT GUARDS, Shooters,
  // WINGS, CENTERS...), NOT .teams (scrimmage squads Lights/Darks/Scout, usually empty).
  const seen = new Set(); const out = [];
  const dr = team && team.default_roster ? team.default_roster : {};
  const groups = [ ...(Array.isArray(dr.rosterGroups) ? dr.rosterGroups : []), ...(Array.isArray(dr.teams) ? dr.teams : []) ];
  const POS = { 'POINT GUARDS':'PG', 'GUARDS':'G', 'SHOOTERS':'G', 'WINGS':'W', 'FORWARDS':'F', 'BIGS':'C', 'CENTERS':'C' };
  groups.forEach((g) => {
    const groupName = String((g && (g.name || g.label)) || '').trim();
    if (groupName.toLowerCase() === 'roster') return;
    const pos = POS[groupName.toUpperCase()] || '';
    (Array.isArray(g.players) ? g.players : []).forEach((p) => {
      const raw = typeof p === 'string' ? { name:p } : (p || {});
      const name = String(raw.name || raw.label || '').trim();
      if (!name) return;
      const key = name.toLowerCase(); if (seen.has(key)) return; seen.add(key);
      const finalPos = raw.pos || raw.position || pos;
      out.push({ id: ROT_uid('team'), name, num: raw.num || raw.number || '', pos: finalPos, eligible: ROT_defaultEligible(), target: 20 });
    });
  });
  return out;
}

function ROT_makePlan(name, roster, preset){
  const periods = ROT_periodsForPreset(preset || 'fiba');
  const snap = (roster || []).map((p, i) => ({
    id: p.id || ROT_uid('player'),
    name: p.name || `Player ${i + 1}`,
    num: p.num || '',
    pos: p.pos || '',
    eligible: ROT_defaultEligible(),
    target: Number.isFinite(Number(p.target)) ? Number(p.target) : 20,
  }));
  return { preset: preset || 'fiba', periods, resolution: 1, roster: snap, grid: {}, assign: [], notes: '', name: name || 'Untitled rotation' };
}

function ROT_demoPlan(){
  const roster = [
    { id:'p1', name:'Player 1', num:'1', pos:'PG' }, { id:'p2', name:'Player 2', num:'2', pos:'SG' },
    { id:'p3', name:'Player 3', num:'3', pos:'SF' }, { id:'p4', name:'Player 4', num:'4', pos:'PF' },
    { id:'p5', name:'Player 5', num:'5', pos:'C'  }, { id:'p6', name:'Player 6', num:'6', pos:'PG' },
    { id:'p7', name:'Player 7', num:'7', pos:'SF' }, { id:'p8', name:'Player 8', num:'8', pos:'PF' },
    { id:'p9', name:'Player 9', num:'9', pos:'SG' }, { id:'p10', name:'Player 10', num:'10', pos:'C' },
  ];
  const plan = ROT_makePlan('Demo rotation', roster, 'fiba');
  // starters 0-5,10-15,... ; bench fives 5-10,15-20,... — one per position lane
  const starters = ['p1','p2','p3','p4','p5'], bench = ['p6','p9','p7','p8','p10'];
  const assign = [];
  [[0,5],[10,15],[20,25],[30,35]].forEach((iv) => ROT_POSITIONS.forEach((pos, i) => assign.push({ id:ROT_uid('b'), playerId:starters[i], pos, start:iv[0], end:iv[1] })));
  [[5,10],[15,20],[25,30],[35,40]].forEach((iv) => ROT_POSITIONS.forEach((pos, i) => assign.push({ id:ROT_uid('b'), playerId:bench[i], pos, start:iv[0], end:iv[1] })));
  plan.assign = assign;
  plan.grid = ROT_assignToGrid(assign);
  plan.notes = 'Sample plan: drag bench chips into the lanes, then print the bench card.';
  return plan;
}

function ROT_gridToMinutes(intervals, total){
  const arr = Array.from({ length: total }, () => false);
  (intervals || []).forEach((pair) => { const s = Math.max(0, Math.min(total, pair[0]|0)); const e = Math.max(s, Math.min(total, pair[1]|0)); for (let m = s; m < e; m++) arr[m] = true; });
  return arr;
}

function ROT_minuteMeta(periods){
  const out = []; let cursor = 0;
  (periods || []).forEach((p, pi) => { for (let i = 0; i < p.minutes; i++) out.push({ minute: cursor + i, periodIndex: pi, period: p.label, inPeriod: i, label: `${p.minutes - i}:00` }); cursor += p.minutes; });
  return out;
}
function ROT_timeLabel(periods, minute){
  let cursor = 0;
  for (let i = 0; i < periods.length; i++){ const p = periods[i]; if (minute <= cursor + p.minutes){ const inside = Math.max(0, Math.min(p.minutes, minute - cursor)); return `${ROT_shortP(p.label)} ${p.minutes - inside}:00`; } cursor += p.minutes; }
  const last = periods[periods.length - 1] || { label:'Q4' }; return `${ROT_shortP(last.label)} 0:00`;
}
function ROT_subTimeLabel(periods, minute){
  let cursor = 0;
  for (let i = 0; i < periods.length; i++){ const p = periods[i]; if (minute < cursor + p.minutes) return `${ROT_shortP(p.label)} ${p.minutes - (minute - cursor)}:00`; cursor += p.minutes; }
  const last = periods[periods.length - 1] || { label:'Q4' }; return `${ROT_shortP(last.label)} 0:00`;
}
function ROT_shortP(label){ const s = String(label || ''); const m = s.match(/(\d)(st|nd|rd|th)\s+Quarter/i); if (m) return 'Q' + m[1]; const h = s.match(/(\d)(st|nd)\s+Half/i); if (h) return 'H' + h[1]; return s; }
function ROT_mmss(mins){ const m = Math.max(0, Math.round(mins)); return `${m}:00`; }

function ROT_analysis(plan){
  const roster = plan.roster || []; const total = ROT_totalMinutes(plan.periods);
  const nameById = {}; roster.forEach((p) => { nameById[p.id] = p.name; });
  const playerMinutes = {}; const playerStints = {};
  roster.forEach((p) => {
    const arr = ROT_gridToMinutes((plan.grid || {})[p.id], total);
    playerMinutes[p.id] = arr.filter(Boolean).length;
    let stints = 0, longest = 0, longestRest = 0, curOn = 0, curOff = 0;
    arr.forEach((on) => { if (on){ if (curOn === 0) stints++; curOn++; longest = Math.max(longest, curOn); curOff = 0; } else { curOff++; longestRest = Math.max(longestRest, curOff); curOn = 0; } });
    playerStints[p.id] = { stints, longest, longestRest };
  });
  const counts = []; const units = [];
  for (let m = 0; m < total; m++){ const ids = roster.filter((p) => ROT_gridToMinutes((plan.grid || {})[p.id], total)[m]).map((p) => p.id); counts.push(ids.length); units.push(ids); }
  const lineupMap = new Map(); let activeKey = null; let activeStart = 0;
  function closeUnit(end){ if (!activeKey || activeKey === 'invalid') return; const ex = lineupMap.get(activeKey) || { key:activeKey, ids: activeKey.split('|'), ranges: [], minutes: 0, first: activeStart }; ex.ranges.push([activeStart, end]); ex.minutes += end - activeStart; lineupMap.set(activeKey, ex); }
  for (let m = 0; m <= total; m++){ const ids = m < total ? units[m].slice().sort() : []; const key = ids.length === 5 ? ids.join('|') : 'invalid'; if (m === 0){ activeKey = key; activeStart = 0; } else if (key !== activeKey){ closeUnit(m); activeKey = key; activeStart = m; } }
  const lineups = Array.from(lineupMap.values()).sort((a,b) => a.first - b.first).map((u) => ({ ...u, names: u.ids.map((id) => nameById[id] || id) }));
  const subs = [];
  for (let m = 1; m < total; m++){ const prev = new Set(units[m - 1]); const next = new Set(units[m]); const inn = units[m].filter((id) => !prev.has(id)).map((id) => nameById[id] || id); const out = units[m - 1].filter((id) => !next.has(id)).map((id) => nameById[id] || id); if (inn.length || out.length) subs.push({ minute:m, in:inn, out }); }
  return { playerMinutes, playerStints, counts, lineups, subs };
}
function ROT_rangesText(periods, ranges){ return (ranges || []).map((r) => `${ROT_timeLabel(periods, r[0])}-${ROT_timeLabel(periods, r[1])}`).join(', '); }

// v8.45: claGetUser (auth.jsx) is the LOCAL session read — an offline reopen
// keeps the session (the old network auth.getUser() reported signed-out with
// no connection). Fallback kept only for pages that somehow miss auth.jsx.
async function ROT_getUser(){
  if (window.claGetUser) return await window.claGetUser();
  if (!window.claSupabase) return null; const res = await window.claSupabase.auth.getUser(); return res && res.data ? res.data.user : null;
}
// List + save delegate to the offline-aware helpers in auth.jsx (read-cache
// 't:<teamId>:rotations' + write queue kind 'rotation', CAS-or-copy on replay).
// v8.50: team-shared — no .eq('user_id') on reads/updates, RLS gates visibility
// (owner policy + cla_rotations_member_all), same idiom as practices.
async function ROT_listRotations(teamId){
  if (window.claListRotations) return await window.claListRotations(teamId);
  if (!window.claSupabase) return []; const user = await ROT_getUser(); if (!user) return [];
  let q = window.claSupabase.from('cla_rotations').select('id, user_id, name, data, team_id, created_at, updated_at').order('updated_at', { ascending:false });
  q = teamId ? q.eq('team_id', teamId) : q.is('team_id', null);
  const res = await q; if (res.error){ console.warn('[rotation] list', res.error); return []; } return res.data || [];
}
// baseUpdatedAt (v8.3, MC 28adecc3): the row stamp THIS editor last saw — arms
// the CAS-or-copy guard on the online update in claSaveRotation (auth.jsx).
async function ROT_saveRotation(id, name, data, teamId, baseUpdatedAt){
  if (window.claSaveRotation) return await window.claSaveRotation(id, name, data, teamId, baseUpdatedAt);
  if (!window.claSupabase) return null; const user = await ROT_getUser(); if (!user) return null;
  if (id){ const res = await window.claSupabase.from('cla_rotations').update({ name, data, updated_at: new Date().toISOString() }).eq('id', id).select().single(); if (res.error){ console.warn('[rotation] save', res.error); return null; } return res.data; }
  const res = await window.claSupabase.from('cla_rotations').insert({ user_id:user.id, team_id:teamId || null, name, data }).select().single();
  if (res.error){ console.warn('[rotation] insert', res.error); return null; } return res.data;
}
async function ROT_deleteRotation(id){
  if (!window.claSupabase) return false; const user = await ROT_getUser(); if (!user) return false;
  // deletes stay ONLINE-ONLY by design — be honest about it instead of failing silently
  if (!navigator.onLine){ if (window.claOfflineDeleteToast) window.claOfflineDeleteToast(); return false; }
  // No user_id filter (v8.50): the member policy allows team-wide delete, so any
  // caller MUST confirm with "Delete this rotation for the whole team?" first.
  const res = await window.claSupabase.from('cla_rotations').delete().eq('id', id).select('id');
  if (res.error){ console.warn('[rotation] delete', res.error); return false; } return Array.isArray(res.data) && res.data.length > 0;
}

function ROT_LoginGate(){
  const [login, setLogin] = React.useState(''); const [password, setPassword] = React.useState('');
  const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState('');
  const submit = async (e) => { e.preventDefault(); setBusy(true); setError(''); try { const email = window.claResolveLogin ? window.claResolveLogin(login) : login; await window.signInWithPassword(email, password); } catch (err){ setError(err && err.message ? err.message : 'Sign-in failed.'); } finally { setBusy(false); } };
  // v8.53 (S1 / canvas 2a): the gate adopts the TOOL lockup (CLA block + sans) —
  // same fields, same claResolveLogin flow, submit = kit xl primary (KIT-SPEC §1.1).
  return (
    <div style={{ minHeight:'100vh', background:'#f6f6f8', color:'#1d1d1f', display:'grid', placeItems:'center', fontFamily:V6_FONT }}>
      <form onSubmit={submit} style={{ width:'min(420px, calc(100vw - 32px))', background:'#fff', border:'.5px solid rgba(0,0,0,.1)', borderRadius:14, padding:24, boxShadow:'0 10px 40px rgba(0,0,0,.08)' }}>
        <div style={{ display:'flex', alignItems:'center', gap:10, marginBottom:14 }}>
          <span style={{ width:30, height:26, borderRadius:7, background:'#16110e', color:'#fff', display:'inline-flex', alignItems:'center', justifyContent:'center', fontSize:8.5, fontWeight:800, letterSpacing:'.02em' }}>CLAP</span>
          <span style={{ fontSize:13.5, fontWeight:700 }}>Practice Planner</span>
        </div>
        <div style={{ fontSize:23, fontWeight:800, letterSpacing:'-.02em', marginBottom:8 }}>Rotation Maker</div>
        <div style={{ color:'rgba(0,0,0,.55)', fontSize:13, lineHeight:1.45, marginBottom:18 }}>Sign in to save team rotations, or add <code>?demo=1</code> for the read-only sample.</div>
        <input value={login} onChange={(e)=>setLogin(e.target.value)} placeholder="Email or username" autoFocus style={ROT_field()} />
        <input value={password} onChange={(e)=>setPassword(e.target.value)} placeholder="Password" type="password" style={{ ...ROT_field(), marginTop:10 }} />
        {error && <div style={{ color:'#a01717', fontSize:12, marginTop:10 }}>{error}</div>}
        <V6_Btn type="submit" kind="primary" size="xl" disabled={busy} style={{ marginTop:14, width:'100%' }}>{busy ? 'Signing in…' : 'Sign in'}</V6_Btn>
      </form>
    </div>
  );
}
function ROT_field(){ return { width:'100%', height:40, borderRadius:9, border:0, boxShadow:'inset 0 0 0 .5px rgba(0,0,0,.16)', background:'#fff', color:'#1d1d1f', padding:'0 11px', outline:'none', fontFamily:V6_FONT, fontSize:13, boxSizing:'border-box' }; }

function ROT_RotationApp(){
  const params = new URLSearchParams(window.location.search || '');
  const demo = params.get('demo') === '1' || params.get('smoke') === '1';
  // Rules of Hooks: session hook called UNCONDITIONALLY (see home-v6.jsx /
  // outputs-v6.jsx — the `!demo &&` gate is the React #311 pattern verify.mjs
  // now rejects). Demo variant preserved by zeroing `user`, not skipping hooks.
  const auth = (typeof useClaSession !== 'undefined') ? useClaSession() : { user:null, loading:false };
  const user = demo ? null : auth.user;
  if (!demo && auth.loading) return <div style={{ minHeight:'100vh', background:'#f6f6f8', color:'#1d1d1f', display:'grid', placeItems:'center' }}>Loading…</div>;
  if (!demo && !user) return <ROT_LoginGate />;
  return <ROT_MainPage user={user} demo={demo} />;
}

function ROT_MainPage({ user, demo, embedded = false, accent = '#16110e', teamsApi: teamsApiOverride = null, initialPlanId = null }){
  const teamsApiRaw = (typeof useClaTeams !== 'undefined') ? useClaTeams() : null;
  const teamsApi = user ? (teamsApiOverride || teamsApiRaw) : null; // demo/signed-out: no team UI, but the hook still runs
  const currentTeam = teamsApi ? teamsApi.currentTeam : null;
  const teamId = teamsApi ? teamsApi.currentId : null;
  const candidates = React.useMemo(() => ROT_rosterCandidates(currentTeam), [currentTeam]);
  // v8.61: games from the team calendar (jsonb on cla_teams) — a rotation can attach
  // to one as the PREGAME plan or the POST-GAME (actual) rotation. Stored on the plan
  // doc itself: plan.gameId (calendar event id) + plan.gamePhase ('pregame'|'postgame').
  const gameEvents = React.useMemo(() => ((currentTeam && Array.isArray(currentTeam.calendar)) ? currentTeam.calendar : [])
    .filter((e) => e && (e.type === 'game' || e.type === 'scrimmage'))
    .sort((a, z) => String(a.date || '').localeCompare(String(z.date || ''))), [currentTeam]);
  const [plans, setPlans] = React.useState([]);
  const [activeId, setActiveId] = React.useState(null);
  const [plan, setPlan] = React.useState(() => {
    if (demo){ try { const s = JSON.parse(localStorage.getItem(`${ROT_LOCAL_KEY}.demo`) || 'null'); if (s && s.roster && s.assign) return s; } catch (_){} return ROT_demoPlan(); }
    return ROT_makePlan('New rotation', [], 'fiba');
  });
  const [overflow, setOverflow] = React.useState([]);   // legacy stints that couldn't auto-pack
  const [status, setStatus] = React.useState(demo ? 'Demo mode' : 'Loading');
  const [adding, setAdding] = React.useState({ name:'', num:'' });
  const [hint, setHint] = React.useState('');
  const loadedRef = React.useRef(false); const dirtyRef = React.useRef(false); const saveTimer = React.useRef(null);
  // v8.3 (MC 28adecc3): the updated_at THIS editor last loaded/saved — the CAS
  // base for online saves. A ref (read at save time, not from the effect
  // closure) so a save landing mid-debounce can't leave a stale stamp behind
  // and turn the very next autosave into a false conflict copy.
  const rowStampRef = React.useRef(null);

  const total = ROT_totalMinutes(plan.periods);
  const pmeta = React.useMemo(() => ROT_periodMeta(plan.periods), [plan.periods]);
  const analysis = React.useMemo(() => ROT_analysis(plan), [plan]);
  const coverage = React.useMemo(() => ROT_coverage(plan.assign, total), [plan.assign, total]);
  const invalidCount = coverage.filter((c) => c !== 5).length;
  const filledMin = coverage.reduce((a, c) => a + c, 0);
  const nameById = {}; (plan.roster || []).forEach((p) => { nameById[p.id] = p.name; });
  const colorById = {}; (plan.roster || []).forEach((p, i) => { colorById[p.id] = ROT_CHIP_COLORS[i % ROT_CHIP_COLORS.length]; });

  React.useEffect(() => { if (demo) window.__rot_smoke_ok = true; }, [demo]);
  const flashHint = (msg) => { setHint(msg); clearTimeout(flashHint._t); flashHint._t = setTimeout(() => setHint(''), 2200); };

  const loadPlans = React.useCallback(async () => {
    if (demo || !user) return;
    setStatus('Loading');
    const rows = await ROT_listRotations(teamId);
    setPlans(rows);
    const wanted = initialPlanId || new URLSearchParams(window.location.search || '').get('plan');
    const pick = rows.find((r) => r.id === wanted) || rows[0];
    if (pick){
      const base = ROT_makePlan(pick.name, candidates, 'fiba');
      const merged = { ...base, ...(pick.data || {}), name: pick.name };
      if (!merged.assign || !merged.assign.length){ const { assign, overflow: ov } = ROT_gridToAssign(merged.grid || {}, merged.roster || []); merged.assign = assign; setOverflow(ov); }
      else setOverflow([]);
      setActiveId(pick.id); setPlan(merged); setStatus('Loaded');
      rowStampRef.current = pick.updated_at || null;
    } else { setActiveId(null); setPlan(ROT_makePlan('New rotation', candidates, 'fiba')); setOverflow([]); setStatus('Draft'); rowStampRef.current = null; }
    dirtyRef.current = false; loadedRef.current = true;
  }, [demo, user, teamId, candidates.length, initialPlanId]);
  React.useEffect(() => { loadPlans(); }, [loadPlans]);
  React.useEffect(() => { const on = () => loadPlans(); window.addEventListener('cla:team-changed', on); return () => window.removeEventListener('cla:team-changed', on); }, [loadPlans]);
  // v8.45: after the offline queue replays (rotation ops may have landed as-is
  // or as "— offline copy" rows), re-pull the list so the picker shows truth.
  React.useEffect(() => { const on = () => { if (!dirtyRef.current) loadPlans(); }; window.addEventListener('cla:offline-replayed', on); return () => window.removeEventListener('cla:offline-replayed', on); }, [loadPlans]);

  // keep grid in sync with assign, then autosave (after a real edit only)
  React.useEffect(() => {
    if (demo){ try { localStorage.setItem(`${ROT_LOCAL_KEY}.demo`, JSON.stringify(plan)); } catch (_){} return; }
    if (!user || !plan || !loadedRef.current || !dirtyRef.current) return;
    setStatus('Unsaved'); clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(async () => {
      const saved = await ROT_saveRotation(activeId, plan.name || 'Untitled rotation', plan, teamId, rowStampRef.current);
      // A CAS miss returns the "— conflict copy" row: setActiveId adopts it, so
      // this editor keeps saving into ITS copy while the teammate keeps theirs.
      if (saved){
        rowStampRef.current = saved.updated_at || null;
        // Adopted a differently-named row (the conflict copy): mirror its name
        // locally so the header matches the list — honest, visible divergence.
        if (saved.id !== activeId && saved.name && saved.name !== (plan.name || 'Untitled rotation')) setPlan((p) => ({ ...p, name: saved.name }));
        setActiveId(saved.id); setStatus('Saved'); setPlans((old) => [saved, ...old.filter((r) => r.id !== saved.id)]);
      } else setStatus('Save failed');
    }, 700);
    return () => clearTimeout(saveTimer.current);
  }, [plan, activeId, user && user.id, teamId, demo]);

  // commit an assign mutation: recompute grid, mark dirty
  const setAssign = React.useCallback((nextAssign) => {
    dirtyRef.current = true;
    setPlan((p) => ({ ...p, assign: nextAssign, grid: ROT_assignToGrid(nextAssign) }));
  }, []);
  const updatePlan = React.useCallback((fn) => { dirtyRef.current = true; setPlan((p) => fn(p)); }, []);

  // ---- drag controller (pointer events; bench-drag + block move + edge resize) ----
  const dragRef = React.useRef(null);
  const [ghost, setGhost] = React.useState(null);   // {x,y,label,color} floating chip while dragging from bench
  const laneRefs = React.useRef({});                // key `${qIndex}:${pos}` -> track element
  const setLaneRef = (q, pos) => (el) => { if (el) laneRefs.current[`${q}:${pos}`] = el; };

  function minuteFromEvent(clientX, clientY){
    const el = document.elementFromPoint(clientX, clientY);
    const lane = el && el.closest ? el.closest('[data-lane]') : null;
    if (!lane) return null;
    const [qs, pos] = lane.getAttribute('data-lane').split(':');
    const q = pmeta[Number(qs)]; if (!q) return null;
    const rect = lane.getBoundingClientRect();
    const frac = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
    return { pos, q, minute: q.start + Math.max(0, Math.min(q.minutes - 1, Math.floor(frac * q.minutes))), edgeMinute: q.start + Math.round(frac * q.minutes) };
  }

  const onPointerMove = (e) => {
    const d = dragRef.current; if (!d) return;
    e.preventDefault && e.preventDefault();
    if (d.kind === 'bench'){ setGhost({ x:e.clientX, y:e.clientY, label:d.label, color:d.color }); return; }
    const hit = minuteFromEvent(e.clientX, e.clientY); if (!hit) return;
    const bounds = [hit.q.start, hit.q.end];
    if (d.kind === 'move'){
      const len = d.orig.end - d.orig.start;
      let start = hit.minute - d.grab; let end = start + len;
      const targetPos = hit.pos;
      const r = ROT_place(plan.assign, d.id, d.playerId, targetPos, start, end, bounds, 'move');
      if (r) setAssign(plan.assign.map((b) => b.id === d.id ? { ...b, pos:targetPos, start:r.start, end:r.end } : b));
    } else if (d.kind === 'resize-l'){
      const r = ROT_place(plan.assign, d.id, d.playerId, d.pos, Math.min(hit.edgeMinute, d.orig.end - 1), d.orig.end, [pmeta[d.qi].start, pmeta[d.qi].end], 'resize-l');
      if (r) setAssign(plan.assign.map((b) => b.id === d.id ? { ...b, start:r.start } : b));
    } else if (d.kind === 'resize-r'){
      const r = ROT_place(plan.assign, d.id, d.playerId, d.pos, d.orig.start, Math.max(hit.edgeMinute, d.orig.start + 1), [pmeta[d.qi].start, pmeta[d.qi].end], 'resize-r');
      if (r) setAssign(plan.assign.map((b) => b.id === d.id ? { ...b, end:r.end } : b));
    }
  };
  const onPointerUp = (e) => {
    const d = dragRef.current;
    if (d && d.kind === 'bench'){
      const hit = minuteFromEvent(e.clientX, e.clientY);
      if (hit){
        const bounds = [hit.q.start, hit.q.end];
        const r = ROT_place(plan.assign, null, d.playerId, hit.pos, hit.minute, Math.min(hit.minute + 5, hit.q.end), bounds);
        if (r) setAssign(plan.assign.concat([{ id:ROT_uid('b'), playerId:d.playerId, pos:hit.pos, start:r.start, end:r.end }]));
        else flashHint('Can’t place there — that spot is taken or the player is already on the floor.');
      }
    }
    dragRef.current = null; setGhost(null);
    window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp);
  };
  const startDrag = (d) => { dragRef.current = d; window.addEventListener('pointermove', onPointerMove); window.addEventListener('pointerup', onPointerUp); };

  const beginBench = (player) => (e) => { e.preventDefault(); startDrag({ kind:'bench', playerId:player.id, label:player.name, color:colorById[player.id] }); setGhost({ x:e.clientX, y:e.clientY, label:player.name, color:colorById[player.id] }); };
  const beginBlock = (block, mode, e) => { e.preventDefault(); e.stopPropagation(); const qi = ROT_periodForMinute(pmeta, block.start).index; const hit = minuteFromEvent(e.clientX, e.clientY); const grab = hit ? (hit.minute - block.start) : 0; startDrag({ kind:mode, id:block.id, playerId:block.playerId, pos:block.pos, qi, orig:{ start:block.start, end:block.end }, grab: Math.max(0, grab) }); };
  const deleteBlock = (id) => setAssign(plan.assign.filter((b) => b.id !== id));
  // positions removed (v8.61): overflow placement tries every lane and takes the first fit
  const placeOverflow = (o) => {
    const qi = ROT_periodForMinute(pmeta, o.start).index;
    for (const pos of ROT_POSITIONS){
      const r = ROT_place(plan.assign, null, o.playerId, pos, o.start, o.end, [pmeta[qi].start, pmeta[qi].end]);
      if (r){ setAssign(plan.assign.concat([{ id:o.id, playerId:o.playerId, pos, start:r.start, end:r.end }])); setOverflow((ov) => ov.filter((x) => x.id !== o.id)); return; }
    }
    flashHint('No open spot at that time — clear a lane first.');
  };

  // roster ops
  const importRoster = () => { if (!candidates.length) return; updatePlan((p) => ({ ...p, roster: candidates.map((c) => ({ ...c, id:c.id || ROT_uid('team') })), assign:[], grid:{} })); setOverflow([]); };
  const addPlayer = () => { const name = adding.name.trim(); if (!name) return; updatePlan((p) => ({ ...p, roster:[...(p.roster||[]), { id:ROT_uid('player'), name, num:adding.num.trim(), pos:'', eligible:ROT_defaultEligible(), target:20 }] })); setAdding({ name:'', num:'' }); };
  const removePlayer = (id) => updatePlan((p) => ({ ...p, roster:(p.roster||[]).filter((x)=>x.id!==id), assign:(p.assign||[]).filter((b)=>b.playerId!==id), grid: ROT_assignToGrid((p.assign||[]).filter((b)=>b.playerId!==id)) }));
  const setPreset = (key) => { updatePlan((p) => { const periods = ROT_periodsForPreset(key); const t = ROT_totalMinutes(periods); const assign = (p.assign||[]).map((b)=>({ ...b, start:Math.min(b.start,t), end:Math.min(b.end,t) })).filter((b)=>b.end>b.start); return { ...p, preset:key, periods, assign, grid: ROT_assignToGrid(assign) }; }); };
  const addOt = () => updatePlan((p) => ({ ...p, periods:[...(p.periods||[]), { label:`OT${(p.periods||[]).filter((x)=>String(x.label).startsWith('OT')).length+1}`, minutes:5 }] }));
  const removeOt = () => updatePlan((p) => { const per=(p.periods||[]).slice(); const oti=per.map((x)=>String(x.label).startsWith('OT')).lastIndexOf(true); if (oti<0) return p; per.splice(oti,1); const t=ROT_totalMinutes(per); const assign=(p.assign||[]).map((b)=>({...b,start:Math.min(b.start,t),end:Math.min(b.end,t)})).filter((b)=>b.end>b.start); return { ...p, periods:per, assign, grid:ROT_assignToGrid(assign) }; });

  // v8.53 (S5): "Save failed → retry" re-fires the EXISTING debounced autosave —
  // a new plan identity with dirtyRef set walks the same effect → same save route.
  const retrySave = React.useCallback(() => { if (demo) return; dirtyRef.current = true; setStatus('Unsaved'); setPlan((p) => ({ ...p })); }, [demo]);

  // v8.53 (S7): invalid copy names the first open window + which lane(s) are open,
  // derived from the SAME coverage/assign data (no new computation path).
  const invalidInfo = React.useMemo(() => {
    if (!invalidCount) return null;
    const a = coverage.findIndex((c) => c !== 5);
    if (a < 0) return null;
    let b = a; while (b < total && coverage[b] !== 5) b++;
    const minFilled = Math.min(...coverage.slice(a, b));
    const openLanes = ROT_POSITIONS.filter((pos) => {
      for (let m = a; m < b; m++){ if (!(plan.assign || []).some((k) => k.pos === pos && m >= k.start && m < k.end)) return true; }
      return false;
    });
    const wStart = ROT_subTimeLabel(plan.periods, a), wEnd = ROT_timeLabel(plan.periods, b);
    const ps = wStart.split(' ')[0], pe = wEnd.split(' ')[0];
    const win = ps === pe ? `${wStart} – ${wEnd.slice(ps.length + 1)}` : `${wStart} – ${wEnd}`;
    return { minFilled, openLanes, win };
  }, [invalidCount, coverage, total, plan.assign, plan.periods]);

  // v8.53 (1b): autosave status chip states — text chip right of the name.
  const statusUi = status === 'Saved' ? { dot:'#2fa25c', text:'Saved · just now' }
    : status === 'Loaded' ? { dot:'#2fa25c', text:'Loaded' }
    : status === 'Unsaved' ? { dot:'rgba(0,0,0,.25)', text:'Unsaved changes — autosaving…' }
    : status === 'Save failed' ? { dot:'#d64545', text:'Save failed — ', fail:true }
    : { dot:'rgba(0,0,0,.25)', text:status };

  const cols = pmeta.length >= 4 ? 2 : 1;
  return (
    <div className={embedded ? 'rot-page rot-embedded' : 'rot-page'}
      style={{ minHeight: embedded ? 0 : '100vh', height: embedded ? '100%' : undefined, overflow: embedded ? 'auto' : undefined,
        background:'#f6f6f8', color:'#1d1d1f', fontFamily:V6_FONT, '--rot-accent': accent }}
      data-screen-label="Rotations" data-ux={embedded ? 'rotations-planner-workspace' : 'rotations-standalone'}>
      <ROT_Styles />
      {!embedded && typeof V6_TopNav !== 'undefined' && <V6_TopNav active="rotations" skin="tool" sub="Rotation Maker" />}
      {demo && !embedded && (
        <div className="rot-demo">
          <span className="rot-demo-k">SAMPLE DATA</span>
          <span className="rot-demo-t">You're looking at a sample rotation — edits stay in this browser.</span>
          <a href="rotation.html">Sign in</a>
          <span className="rot-demo-t">to build your own.</span>
        </div>
      )}
      <div className="rot-shell">
        <header className="rot-hero">
          <div className="rot-hero-name">
            <input value={plan.name || ''} onChange={(e)=>updatePlan((p)=>({ ...p, name:e.target.value }))} className="rot-title" aria-label="Rotation name" />
            <span className={statusUi.fail ? 'rot-save-chip fail' : 'rot-save-chip'}>
              <i className="dot" style={{ background: statusUi.dot }} />{statusUi.text}
              {statusUi.fail && <span className="retry" data-ux="rot-save-retry" onClick={retrySave}>retry</span>}
            </span>
          </div>
          <div className="rot-actions">
            {teamsApi && teamsApi.teams.length > 1 && <select value={teamId||''} onChange={(e)=>teamsApi.setCurrent(e.target.value)} className="rot-select">{teamsApi.teams.map((tm)=><option key={tm.id} value={tm.id}>{tm.name}</option>)}</select>}
            <select value={activeId||''} onChange={(e)=>{ const row=plans.find((r)=>r.id===e.target.value); if (!row) return; const base=ROT_makePlan(row.name,candidates,'fiba'); const merged={...base,...(row.data||{}),name:row.name}; if(!merged.assign||!merged.assign.length){const{assign,overflow:ov}=ROT_gridToAssign(merged.grid||{},merged.roster||[]);merged.assign=assign;setOverflow(ov);}else setOverflow([]); setActiveId(row.id); dirtyRef.current=false; setPlan(merged); }} className="rot-select">
              <option value="">Current draft</option>{plans.map((r)=><option key={r.id} value={r.id}>{r.name}{r.user_id && user && r.user_id !== user.id ? ' · teammate' : ''}</option>)}
            </select>
            <V6_Btn onClick={()=>{ setActiveId(null); setOverflow([]); setPlan(ROT_makePlan('New rotation', candidates, 'fiba')); }}>New</V6_Btn>
            <V6_Btn onClick={()=>{ dirtyRef.current=true; setActiveId(null); setPlan({ ...ROT_clone(plan), name:`${plan.name||'Rotation'} copy` }); }}>Duplicate</V6_Btn>
            <V6_Btn kind="primary" onClick={()=>window.print()}>Print</V6_Btn>
          </div>
        </header>

        <section className="rot-toolbar">
          <div className="rot-pills">
            {Object.values(ROT_PRESETS).map((pr)=><button key={pr.key} className={pr.key===plan.preset?'rot-pill on':'rot-pill'} onClick={()=>setPreset(pr.key)}>{pr.label}</button>)}
            <span className="rot-vr" />
            <V6_Btn size="sm" onClick={addOt}>+ OT</V6_Btn><V6_Btn size="sm" onClick={removeOt}>− OT</V6_Btn>
          </div>
          <div className="rot-status"><span className="min">{total} min</span><span className={invalidCount?'bad':'good'}><i className="dot" />{invalidCount?`${invalidCount} min not full`:'Full 5-on-floor'}</span></div>
        </section>

        {(gameEvents.length > 0 || plan.gameId) && (
          <section className="rot-gamerow" data-ux="rot-game-attach">
            <span className="rot-game-k">Game</span>
            <select className="rot-select" value={plan.gameId || ''}
              onChange={(e)=>{ const v = e.target.value; updatePlan((p)=>({ ...p, gameId: v || null, gamePhase: v ? (p.gamePhase || 'pregame') : null })); }}>
              <option value="">Not attached to a game</option>
              {gameEvents.map((g)=><option key={g.id} value={g.id}>{g.date} · {g.title || (g.type === 'scrimmage' ? 'Scrimmage' : 'Game')}</option>)}
              {plan.gameId && !gameEvents.some((g)=>g.id===plan.gameId) && <option value={plan.gameId}>Attached game (removed from calendar)</option>}
            </select>
            {plan.gameId && (
              <div className="rot-phase">
                <button className={plan.gamePhase !== 'postgame' ? 'on' : ''} onClick={()=>updatePlan((p)=>({ ...p, gamePhase:'pregame' }))}>Pregame plan</button>
                <button className={plan.gamePhase === 'postgame' ? 'on' : ''} onClick={()=>updatePlan((p)=>({ ...p, gamePhase:'postgame' }))}>Post-game (actual)</button>
              </div>
            )}
          </section>
        )}

        <div className="rot-main">
          <div className="rot-court">
            {(plan.roster||[]).length < 1 && <div className="rot-empty-note">Add players (or “Use team roster”) on the right, then drag them into the lanes.</div>}
            <div className="rot-quarters" style={{ gridTemplateColumns:`repeat(${cols}, 1fr)` }}>
              {pmeta.map((q) => (
                <div key={q.index} className="rot-q">
                  <div className="rot-q-title">{q.label}</div>
                  <div className="rot-ruler" style={{ gridTemplateColumns:`34px repeat(${q.minutes}, 1fr)` }}>
                    <div />{Array.from({length:q.minutes}).map((_,i)=><div key={i} className="rot-tick">{q.minutes-i}:00</div>)}
                  </div>
                  {ROT_POSITIONS.map((pos) => {
                    const blocks = ROT_laneBlocks(plan.assign, pos).filter((b)=>b.start<q.end && b.end>q.start);
                    return (
                      <div key={pos} className="rot-lane-row">
                        <div className="rot-lane-label" />
                        <div className="rot-lane-wrap">
                          <div className="rot-lane" data-lane={`${q.index}:${pos}`} ref={setLaneRef(q.index, pos)}>
                            {blocks.length===0 && <div className="rot-lane-hint">Drag players here</div>}
                            {blocks.map((b) => {
                              const s=Math.max(b.start,q.start), e=Math.min(b.end,q.end);
                              const left=((s-q.start)/q.minutes)*100, width=((e-s)/q.minutes)*100;
                              const c=colorById[b.playerId]||'#888';
                              // FU89: player color spent ONCE per stint — the 3px identity
                              // edge; the fill/ring go neutral (bench keeps its small dot).
                              return (
                                <div key={b.id} className="rot-block" style={{ left:`${left}%`, width:`${width}%`, background:'#fff', boxShadow:'inset 0 0 0 1px rgba(0,0,0,.14)', borderLeft:`3px solid ${c}` }}
                                  onPointerDown={(ev)=>beginBlock(b,'move',ev)}>
                                  <span className="rot-grip l" onPointerDown={(ev)=>beginBlock(b,'resize-l',ev)} />
                                  <span className="rot-block-name">{nameById[b.playerId]||'?'}</span>
                                  <span className="rot-block-x" onPointerDown={(ev)=>{ev.stopPropagation();}} onClick={(ev)=>{ev.stopPropagation();deleteBlock(b.id);}}>×</span>
                                  <span className="rot-grip r" onPointerDown={(ev)=>beginBlock(b,'resize-r',ev)} />
                                </div>
                              );
                            })}
                          </div>
                          <div className="rot-cover">{Array.from({length:q.minutes}).map((_,i)=>{ const m=q.start+i; return <span key={i} className={coverage[m]===5?'ok':'bad'} />; })}</div>
                        </div>
                      </div>
                    );
                  })}
                </div>
              ))}
            </div>

            {overflow.length>0 && (
              <div className="rot-overflow">
                <div className="rot-of-kicker">Couldn’t auto-place {overflow.length} stint{overflow.length===1?'':'s'} — pick a lane</div>
                {overflow.map((o)=>{ const c=colorById[o.playerId]||'#888'; return (
                  <div key={o.id} className="rot-of-row">
                    <span className="rot-of-name" style={{ background:'#fff', boxShadow:'inset 0 0 0 1px rgba(0,0,0,.14)' }}><i className="rot-dot sm" style={{ background:c }} />{nameById[o.playerId]||'?'} · {ROT_subTimeLabel(plan.periods,o.start)}→{ROT_timeLabel(plan.periods,o.end)}</span>
                    <button className="rot-of-pos" onClick={()=>placeOverflow(o)}>Place</button>
                  </div>
                );})}
              </div>
            )}

            <div className={invalidCount?'rot-banner bad':'rot-banner ok'}>
              {invalidCount && invalidInfo
                ? <><i className="dot" /><b>{invalidInfo.minFilled} on the floor</b><span className="sub">{invalidInfo.win} — {invalidInfo.openLanes.length} spot{invalidInfo.openLanes.length===1?' is':'s are'} open</span><span className="faint">· Playing time {ROT_mmss(filledMin)} / {ROT_mmss(total*5)}</span></>
                : <><i className="dot" /><b>Valid rotation</b><span className="sub">{ROT_mmss(total*5)} assigned · five on the floor every minute</span></>}
            </div>
          </div>

          <aside className="rot-bench">
            <div className="rot-bench-head"><span>Bench</span><V6_Btn kind="quiet" size="sm" onClick={importRoster} disabled={!candidates.length}>Use team roster</V6_Btn></div>
            <div className="rot-add">
              <input placeholder="Name" value={adding.name} onChange={(e)=>setAdding({...adding,name:e.target.value})} onKeyDown={(e)=>{if(e.key==='Enter')addPlayer();}} />
              <input placeholder="#" value={adding.num} onChange={(e)=>setAdding({...adding,num:e.target.value})} onKeyDown={(e)=>{if(e.key==='Enter')addPlayer();}} />
              <V6_Btn kind="primary" size="sm" style={{ height:30, borderRadius:8 }} onClick={addPlayer}>Add</V6_Btn>
            </div>
            <div className="rot-chips">
              {(plan.roster||[]).map((p)=>(
                <div key={p.id} className="rot-row">
                  <div className="rot-row-top" onPointerDown={beginBench(p)}>
                    <span className="rot-dot" style={{ background: colorById[p.id] }} />
                    <span className="rot-row-name">{p.num?`#${p.num} `:''}{p.name}</span>
                    <span className="rot-row-min">{ROT_mmss(analysis.playerMinutes[p.id]||0)}</span>
                    <button className="rot-row-del" title="Remove player" onPointerDown={(ev)=>ev.stopPropagation()} onClick={()=>removePlayer(p.id)}>🗑</button>
                  </div>
                </div>
              ))}
              {(plan.roster||[]).length===0 && (
                <div className="rot-bench-empty">
                  <div className="t">No players yet</div>
                  <div className="s">Pull in your roster with “Use team roster”, or add players by hand above.</div>
                </div>
              )}
            </div>
          </aside>
        </div>

        <ROT_AnalysisCard plan={plan} analysis={analysis} />
        <section className="rot-notes-card">
          <div className="rot-notes-head">Bench reminders</div>
          <textarea value={plan.notes||''} onChange={(e)=>updatePlan((p)=>({...p,notes:e.target.value}))} className="rot-notes" placeholder="Late-game rules, foul trouble, ATO lineup…" />
        </section>
      </div>
      <ROT_PrintCard plan={plan} analysis={analysis} gameLabel={(()=>{ const g = gameEvents.find((x)=>x.id===plan.gameId); return g ? `${g.date} · ${g.title || 'Game'} · ${plan.gamePhase === 'postgame' ? 'POST-GAME (ACTUAL)' : 'PREGAME PLAN'}` : null; })()} />
      {ghost && <div className="rot-ghost" style={{ left:ghost.x+12, top:ghost.y+12, background:'#fff', boxShadow:'inset 0 0 0 1px rgba(0,0,0,.16), 0 4px 14px rgba(0,0,0,.18)' }}><i className="rot-dot sm" style={{ background:ghost.color }} />{ghost.label}</div>}
      {hint && <div className="rot-toast">{hint}</div>}
      {demo && <div id="rot-smoke-ok" style={{ display:'none' }}>ok</div>}
    </div>
  );
}

function ROT_AnalysisCard({ plan, analysis }){
  return (
    <section className="rot-analysis">
      <div className="rot-a-col">
        <h3>Lineups</h3>
        {analysis.lineups.length===0 ? <p>No legal 5-player units yet.</p> : analysis.lineups.map((u)=>(
          <div key={u.key} className="rot-a-row"><b className="mono">{ROT_mmss(u.minutes)}</b><span>{u.names.join(' / ')}</span><em>{ROT_rangesText(plan.periods,u.ranges)}</em></div>
        ))}
      </div>
      <div className="rot-a-col">
        <h3>Substitutions</h3>
        {analysis.subs.length===0 ? <p>No substitutions yet.</p> : analysis.subs.map((s,i)=>(
          <div key={i} className="rot-a-row"><b className="mono">{ROT_subTimeLabel(plan.periods,s.minute)}</b><span>{s.in.length?`IN ${s.in.join(', ')}`:''}{s.in.length&&s.out.length?' · ':''}{s.out.length?`OUT ${s.out.join(', ')}`:''}</span></div>
        ))}
      </div>
      <div className="rot-a-col">
        <h3>Minutes</h3>
        {(plan.roster||[]).map((p)=>{ const m=analysis.playerMinutes[p.id]||0; const d=m-(Number(p.target)||0); return <div key={p.id} className="rot-a-row"><b>{p.name}</b><span><span className="mono">{ROT_mmss(m)}</span> <em className={d===0?'':d>0?'pos':'neg'}>({d>0?'+':''}{d})</em></span></div>; })}
      </div>
    </section>
  );
}

// v8.53 (1d): grayscale-safe bench card — identity = lane row + jersey number +
// OUTLINED stint boxes with time labels (print floor ≥9pt). No hue dependency:
// the card survives any B/W printer; plan.grid stays the one data source.
function ROT_PrintCard({ plan, analysis, gameLabel }){
  const total = ROT_totalMinutes(plan.periods); const pm = ROT_periodMeta(plan.periods);
  const invalidMin = analysis.counts.filter((c)=>c!==5).length;
  const presetLabel = (ROT_PRESETS[plan.preset] || {}).label || '';
  return (
    <div className="rot-print"><div className="rot-print-page">
      <header>
        <div><div className="rot-print-kicker">CLAP Rotation Maker</div><h1>{plan.name||'Rotation'}</h1></div>
        <div className="rot-print-meta">{gameLabel?`${gameLabel} · `:''}{presetLabel?`${presetLabel} · `:''}{total} MIN · {invalidMin?'NEEDS REVIEW':'5 ON FLOOR'}</div>
      </header>
      <div className="rot-print-grid" style={{ gridTemplateColumns:`150px repeat(${pm.length}, 1fr)` }}>
        <span></span>{pm.map((q)=><span key={q.index} className="rot-print-qh">{ROT_shortP(q.label)}</span>)}
        {(plan.roster||[]).map((p)=>(
          <React.Fragment key={p.id}>
            <span className="rot-print-name">{p.num?`#${p.num} `:''}{p.name}</span>
            {pm.map((q)=>{
              const segs=((plan.grid||{})[p.id]||[]).map((iv)=>[Math.max(iv[0],q.start),Math.min(iv[1],q.end)]).filter((iv)=>iv[1]>iv[0]);
              return (
                <span key={q.index} className="rot-print-cell">
                  {segs.map((iv,i)=><i key={i} className="rot-print-stint" style={{ left:`${((iv[0]-q.start)/q.minutes)*100}%`, width:`${((iv[1]-iv[0])/q.minutes)*100}%` }}>{`${q.minutes-(iv[0]-q.start)}–${q.minutes-(iv[1]-q.start)}`}</i>)}
                </span>
              );
            })}
          </React.Fragment>
        ))}
      </div>
      <div className="rot-print-bottom">
        <section><h2>Substitution list</h2>{analysis.subs.slice(0,26).map((s,i)=><p key={i}><b>{ROT_subTimeLabel(plan.periods,s.minute)}</b> {s.in.length?`IN ${s.in.join(', ')}`:''}{s.in.length&&s.out.length?' / ':''}{s.out.length?`OUT ${s.out.join(', ')}`:''}</p>)}</section>
        <section><h2>Minutes</h2>{(plan.roster||[]).map((p)=><p key={p.id}><b>{p.name}</b><span>{ROT_mmss(analysis.playerMinutes[p.id]||0)} / {p.target||0}</span></p>)}</section>
        <section><h2>Notes</h2><p>{plan.notes||' '}</p></section>
      </div>
      <div className="rot-print-foot">
        <span>{invalidMin?`${invalidMin} min not full — check coverage before tip-off`:'Valid — five on the floor every minute'}</span>
        <span>CLAP — CLA Planner</span>
      </div>
    </div></div>
  );
}

function ROT_Styles(){
  // v8.53: tool-world skin — kit radii (6–7 chips/stints · 8–9 inputs/buttons · 12 cards),
  // .5px hairlines, V6_MONO for numbers, hue via ROT_TINT/ROT_RING only. ≤920px: lanes
  // 44px, validity line goes sticky (canvas 1c). v8.62: position pills gone.
  return (<style>{`
    .rot-page.rot-embedded { min-width:0; flex:1; }
    .rot-shell { padding: 18px clamp(14px,2.4vw,30px) 44px; max-width: 1560px; margin: 0 auto; }
    .rot-embedded .rot-shell { width:100%; max-width:none; box-sizing:border-box; }
    .rot-hero { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
    .rot-hero-name { display:flex; align-items:baseline; gap:12px; flex:1; min-width:260px; }
    .rot-title { border:0; background:transparent; color:#1d1d1f; font-family:${V6_FONT}; font-size:23px; font-weight:800; letter-spacing:-.02em; outline:0; padding:2px 0; min-width:200px; flex:1; }
    .rot-save-chip { display:inline-flex; align-items:center; gap:6px; font-size:11.5px; color:rgba(0,0,0,.45); white-space:nowrap; }
    .rot-save-chip .dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
    .rot-save-chip.fail { color:#a01717; font-weight:700; }
    .rot-save-chip .retry { text-decoration:underline; text-underline-offset:2px; cursor:pointer; margin-left:2px; }
    .rot-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
    .rot-select { height:32px; border-radius:9px; border:0; box-shadow:inset 0 0 0 .5px rgba(0,0,0,.16); background:#fff; color:#1d1d1f; padding:0 10px; font-weight:600; font-size:12.5px; font-family:inherit; }
    .rot-demo { padding:7px clamp(14px,2.4vw,30px); background:#16110e; color:#fff; font-size:12.5px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
    .rot-demo-k { font-family:${V6_MONO}; font-size:10.5px; font-weight:700; letter-spacing:.12em; }
    .rot-demo-t { color:rgba(255,255,255,.75); }
    .rot-demo a { color:#fff; font-weight:700; text-decoration:underline; text-underline-offset:2px; }
    .rot-toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:14px; padding:10px 14px; background:#fff; border:.5px solid rgba(0,0,0,.08); border-radius:12px; }
    .rot-pills { display:flex; gap:7px; flex-wrap:wrap; align-items:center; }
    .rot-pill { height:30px; border-radius:999px; border:0; box-shadow:inset 0 0 0 .5px rgba(0,0,0,.16); background:#fff; color:rgba(0,0,0,.6); padding:0 13px; font-weight:600; cursor:pointer; font-size:12px; font-family:inherit; }
    .rot-pill.on { background:var(--rot-accent, #16110e); color:#fff; box-shadow:none; font-weight:700; }
    .rot-vr { width:.5px; height:20px; background:rgba(0,0,0,.12); margin:0 4px; }
    .rot-status { display:flex; align-items:center; gap:14px; }
    .rot-status .min { font-family:${V6_MONO}; font-size:12px; color:rgba(0,0,0,.5); }
    .rot-status .good, .rot-status .bad { display:inline-flex; align-items:center; gap:6px; font-size:12.5px; font-weight:700; }
    .rot-status .good { color:#0a6b33; } .rot-status .bad { color:#a01717; }
    .rot-status .dot { width:7px; height:7px; border-radius:50%; }
    .rot-status .good .dot { background:#2fa25c; } .rot-status .bad .dot { background:#d64545; }
    .rot-main { display:grid; grid-template-columns:minmax(0,1fr) 300px; gap:16px; align-items:start; margin-top:14px; }
    .rot-court { min-width:0; }
    .rot-empty-note { font-size:12px; color:rgba(0,0,0,.5); padding:8px 2px; }
    .rot-quarters { display:grid; gap:14px; }
    .rot-q { background:#fff; border:.5px solid rgba(0,0,0,.08); border-radius:12px; padding:12px 14px 13px; }
    .rot-q-title { font-size:10.5px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:rgba(0,0,0,.42); margin-bottom:8px; }
    .rot-ruler { display:grid; margin-bottom:3px; }
    .rot-tick { font-family:${V6_MONO}; font-size:10px; color:rgba(0,0,0,.4); text-align:left; border-left:1px solid rgba(0,0,0,.08); padding-left:3px; }
    .rot-lane-row { display:grid; grid-template-columns:34px 1fr; align-items:stretch; gap:0; }
    .rot-lane-label { display:flex; align-items:center; font-size:11px; font-weight:800; color:rgba(0,0,0,.5); }
    .rot-lane-wrap { margin:3px 0; }
    .rot-lane { position:relative; height:34px; background:rgba(0,0,0,.03); box-shadow:inset 0 0 0 .5px rgba(0,0,0,.08); border-radius:7px; overflow:hidden; touch-action:none; }
    .rot-lane-hint { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:10.5px; color:rgba(0,0,0,.3); pointer-events:none; }
    .rot-block { position:absolute; top:0; bottom:0; border-radius:6px; color:#1d1d1f; display:flex; align-items:center; padding:0 4px; cursor:grab; min-width:14px; overflow:hidden; touch-action:none; }
    .rot-block-name { flex:1; font-size:12px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none; }
    .rot-block-x { font-size:12px; font-weight:700; color:rgba(0,0,0,.35); cursor:pointer; padding:0 2px; }
    .rot-block-x:hover { color:rgba(0,0,0,.7); }
    .rot-grip { width:8px; align-self:stretch; cursor:ew-resize; flex-shrink:0; }
    .rot-grip.l { margin-right:1px; } .rot-grip.r { margin-left:1px; }
    .rot-cover { height:3px; border-radius:2px; margin-top:2px; display:grid; grid-auto-flow:column; grid-auto-columns:1fr; overflow:hidden; }
    .rot-cover span { display:block; } .rot-cover .ok { background:rgba(47,162,92,.4); } .rot-cover .bad { background:rgba(214,69,69,.55); }
    .rot-overflow { margin-top:12px; padding:10px 12px; border-radius:12px; background:#fff; border:.5px solid rgba(0,0,0,.1); font-size:12px; }
    .rot-of-kicker { font-size:10.5px; font-weight:800; letter-spacing:.1em; text-transform:uppercase; color:rgba(0,0,0,.42); }
    .rot-of-row { display:flex; align-items:center; gap:8px; margin-top:8px; flex-wrap:wrap; }
    .rot-of-name { display:inline-flex; align-items:center; gap:6px; height:26px; padding:0 9px; border-radius:6px; font-size:11.5px; font-weight:600; color:#1d1d1f; }
    .rot-of-pos { height:24px; border:0; box-shadow:inset 0 0 0 .5px rgba(0,0,0,.16); background:#fff; border-radius:6px; padding:0 8px; font-weight:700; font-size:10.5px; cursor:pointer; font-family:inherit; }
    .rot-dot { width:10px; height:10px; border-radius:50%; flex-shrink:0; display:inline-block; }
    .rot-dot.sm { width:8px; height:8px; }
    .rot-banner { margin-top:12px; display:flex; align-items:center; gap:8px; font-size:12.5px; flex-wrap:wrap; }
    .rot-banner .dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
    .rot-banner .sub { color:rgba(0,0,0,.5); } .rot-banner .faint { color:rgba(0,0,0,.4); }
    .rot-banner.ok { padding:2px 4px; }
    .rot-banner.ok .dot { background:#2fa25c; } .rot-banner.ok b { color:#0a6b33; }
    .rot-banner.bad { background:color-mix(in srgb, #d64545 8%, #fff); border-radius:9px; padding:9px 12px; }
    .rot-banner.bad .dot { background:#d64545; } .rot-banner.bad b { color:#a01717; }
    .rot-banner.bad .sub { color:rgba(0,0,0,.55); }
    .rot-bench { position:sticky; top:12px; background:#fff; border:.5px solid rgba(0,0,0,.08); border-radius:12px; padding:14px 16px 8px; max-height:calc(100vh - 30px); overflow:auto; }
    .rot-bench-head { display:flex; align-items:center; justify-content:space-between; font-weight:700; font-size:14.5px; margin-bottom:10px; }
    .rot-add { display:grid; grid-template-columns:1fr 44px auto; gap:6px; margin-bottom:6px; }
    .rot-add input, .rot-add select { height:30px; border:0; box-shadow:inset 0 0 0 .5px rgba(0,0,0,.16); border-radius:8px; padding:0 9px; font-size:11.5px; background:#fff; box-sizing:border-box; min-width:0; font-family:inherit; }
    .rot-chips { display:flex; flex-direction:column; }
    .rot-row { padding:9px 0 8px; border-top:.5px solid rgba(0,0,0,.07); }
    .rot-row-top { display:flex; align-items:center; gap:8px; cursor:grab; touch-action:none; }
    .rot-row-name { font-size:13px; font-weight:600; flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .rot-row-min { font-family:${V6_MONO}; font-size:12px; color:rgba(0,0,0,.5); font-variant-numeric:tabular-nums; }
    .rot-row-del { margin-left:2px; border:0; background:transparent; color:rgba(0,0,0,.3); font-size:12px; cursor:pointer; padding:2px 4px; flex-shrink:0; }
    .rot-row-del:hover { color:#a01717; }
    .rot-bench-empty { text-align:center; padding:18px 12px 22px; }
    .rot-bench-empty .t { font-size:13px; font-weight:700; }
    .rot-bench-empty .s { font-size:12px; color:rgba(0,0,0,.5); margin-top:4px; line-height:1.45; }
    .rot-analysis { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:16px; margin-top:16px; background:#fff; border:.5px solid rgba(0,0,0,.08); border-radius:12px; padding:14px 16px; }
    .rot-a-col h3 { margin:0 0 8px; font-size:10.5px; font-weight:800; text-transform:uppercase; letter-spacing:.12em; color:rgba(0,0,0,.42); }
    .rot-a-col p { font-size:12px; color:rgba(0,0,0,.45); }
    .rot-a-row { display:grid; grid-template-columns:78px 1fr; gap:8px; padding:6px 0; border-bottom:.5px solid rgba(0,0,0,.06); font-size:12px; align-items:baseline; }
    .rot-a-row b { color:#1d1d1f; } .rot-a-row .mono { font-family:${V6_MONO}; font-size:11.5px; font-variant-numeric:tabular-nums; }
    .rot-a-row em { font-style:normal; color:rgba(0,0,0,.45); } .rot-a-row em.pos { color:#0a6b33; } .rot-a-row em.neg { color:#a01717; }
    .rot-notes-card { margin-top:16px; background:#fff; border:.5px solid rgba(0,0,0,.08); border-radius:12px; padding:14px 16px; }
    .rot-notes-head { font-size:10.5px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:rgba(0,0,0,.42); margin-bottom:8px; }
    .rot-notes { width:100%; min-height:70px; border:0; box-shadow:inset 0 0 0 .5px rgba(0,0,0,.08); background:rgba(0,0,0,.03); border-radius:8px; padding:9px 11px; font-family:inherit; font-size:12.5px; resize:vertical; box-sizing:border-box; }
    .rot-ghost { position:fixed; z-index:9999; pointer-events:none; color:#1d1d1f; font-size:11px; font-weight:700; padding:4px 9px; border-radius:6px; display:flex; align-items:center; gap:6px; }
    .rot-toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%); background:#1d1d1f; color:#fff; font-size:12px; font-weight:700; padding:9px 15px; border-radius:999px; z-index:9999; box-shadow:0 6px 20px rgba(0,0,0,.25); }
    .rot-gamerow { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:10px; padding:9px 14px; background:#fff; border:.5px solid rgba(0,0,0,.08); border-radius:12px; }
    .rot-game-k { font-size:10.5px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:rgba(0,0,0,.42); }
    .rot-phase { display:flex; gap:6px; }
    .rot-phase button { height:28px; border:0; border-radius:999px; box-shadow:inset 0 0 0 .5px rgba(0,0,0,.16); background:#fff; color:rgba(0,0,0,.55); padding:0 12px; font-weight:600; font-size:11.5px; cursor:pointer; font-family:inherit; }
    .rot-phase button.on { background:var(--rot-accent, #16110e); color:#fff; box-shadow:none; font-weight:700; }
    @media (max-width: 920px){
      .rot-main { grid-template-columns:1fr; } .rot-quarters { grid-template-columns:1fr !important; }
      .rot-bench { position:static; max-height:none; }
      .rot-lane { height:44px; }
      .rot-pill { height:32px; }
      .rot-banner { position:sticky; bottom:8px; z-index:5; }
      .rot-banner.ok { background:#fff; border-radius:9px; padding:9px 12px; box-shadow:0 2px 12px rgba(0,0,0,.12); }
      .rot-banner.bad { box-shadow:0 2px 12px rgba(0,0,0,.12); }
    }
    .rot-print { display:none; }
    @media print {
      @page { size: landscape; margin:.35in; }
      body { background:#fff !important; }
      [data-screen-label="Rotations"] > *:not(.rot-print) { display:none !important; }
      .rot-print { display:block !important; color:#111; font-family:Arial, sans-serif; -webkit-print-color-adjust:exact; print-color-adjust:exact; }
      /* app.html's practice-print base hides body descendants with visibility.
         The embedded Rotation Maker explicitly re-opens only its bench card. */
      .rot-embedded .rot-print, .rot-embedded .rot-print * { visibility:visible !important; }
      .rot-embedded .rot-print { position:absolute !important; left:0 !important; top:0 !important; width:100% !important; }
      .rot-print-page header { display:flex; justify-content:space-between; align-items:flex-end; border-bottom:1.5pt solid #111; padding-bottom:8px; margin-bottom:10px; }
      .rot-print-page h1 { margin:0; font-size:15pt; font-weight:800; }
      .rot-print-kicker { font-size:9pt; text-transform:uppercase; letter-spacing:.12em; color:#666; font-weight:800; }
      .rot-print-meta { font-family:${V6_MONO}; font-size:9pt; color:#444; text-transform:uppercase; }
      .rot-print-grid { display:grid; }
      .rot-print-qh { font-family:${V6_MONO}; font-size:9pt; font-weight:700; color:#444; text-align:center; padding:2pt 0; border-left:1pt solid #ccc; }
      .rot-print-name { font-size:9.5pt; font-weight:700; border-top:.5pt solid #ddd; padding:3pt 5pt 3pt 0; display:flex; align-items:center; white-space:nowrap; overflow:hidden; }
      .rot-print-name i { font-style:normal; font-weight:500; color:#666; margin-left:4pt; font-size:9pt; }
      .rot-print-cell { position:relative; border-top:.5pt solid #ddd; border-left:1pt solid #eee; height:17pt; display:block; }
      .rot-print-stint { position:absolute; top:2.5pt; bottom:2.5pt; border:1pt solid #333; border-radius:2pt; font-family:${V6_MONO}; font-size:9pt; font-style:normal; display:flex; align-items:center; justify-content:center; overflow:hidden; box-sizing:border-box; }
      .rot-print-bottom { display:grid; grid-template-columns:1.3fr .7fr 1fr; gap:16px; margin-top:12px; }
      .rot-print-bottom h2 { font-size:10pt; text-transform:uppercase; letter-spacing:.08em; border-bottom:1pt solid #111; padding-bottom:3px; }
      .rot-print-bottom p { display:flex; justify-content:space-between; gap:8px; margin:3px 0; font-size:9pt; line-height:1.3; }
      .rot-print-foot { display:flex; justify-content:space-between; gap:12px; border-top:1.5pt solid #111; margin-top:12px; padding-top:6px; font-size:9pt; color:#555; }
      .rot-print-foot span:last-child { color:#888; }
    }
  `}</style>);
}

Object.assign(window, { ROT_RotationApp, ROT_MainPage, ROT_PrintCard });
