// Draft helpers: constants, pure functions, scoring & grading logic.
// All exported via window so draft.jsx can pick them up across <script> scopes.

const DRAFT_SUPPLEMENTAL_RANKINGS = [
  ["jgibbs", "Jahmyr Gibbs", "RB", "DET", 26, 21.4, 2, 2, "low", 60, 13],
  ["achane", "De'Von Achane", "RB", "MIA", 28, 19.2, 7, 8, "med", 58, 22],
  ["btj", "Brian Thomas Jr.", "WR", "JAX", 7, 17.8, 9, 10, "low", 55, 17],
  ["nabers", "Malik Nabers", "WR", "NYG", 1, 17.6, 10, 11, "med", 57, 20],
  ["breece", "Breece Hall", "RB", "NYJ", 20, 18.1, 13, 14, "med", 51, 21],
  ["jtaylor", "Jonathan Taylor", "RB", "IND", 28, 17.7, 15, 16, "med", 49, 22],
  ["ajbrown", "A.J. Brown", "WR", "NE", 11, 17.1, 16, 17, "low", 50, 16],
  ["ladd", "Ladd McConkey", "WR", "LAC", 15, 15.9, 20, 21, "low", 46, 17],
  ["jsn", "Jaxon Smith-Njigba", "WR", "SEA", 11, 15.8, 22, 23, "low", 48, 16],
  ["kyren", "Kyren Williams", "RB", "LAR", 23, 16.2, 24, 25, "med", 43, 24],
  ["mahomes", "Patrick Mahomes", "QB", "KC", 15, 21.8, 27, 28, "low", 47, 13],
  ["love", "Jordan Love", "QB", "GB", 10, 19.4, 54, 55, "med", 39, 22],
  ["stroud", "C.J. Stroud", "QB", "HOU", 7, 19.0, 59, 60, "med", 37, 23],
  ["bowers", "Brock Bowers", "TE", "LV", 89, 14.3, 18, 19, "low", 50, 14],
  ["mcbride", "Trey McBride", "TE", "ARI", 85, 12.9, 38, 39, "low", 41, 18],
  ["laporta", "Sam LaPorta", "TE", "DET", 87, 11.6, 48, 49, "med", 38, 23],
  ["walker", "Kenneth Walker III", "RB", "KC", 9, 14.8, 42, 43, "med", 43, 27],
  ["cook", "James Cook", "RB", "BUF", 4, 15.0, 36, 37, "med", 42, 24],
  ["monty", "David Montgomery", "RB", "HOU", 5, 12.8, 70, 71, "med", 33, 26],
  ["swift", "D'Andre Swift", "RB", "CHI", 4, 12.2, 82, 83, "high", 32, 31],
  ["metcalf", "DK Metcalf", "WR", "PIT", 14, 14.6, 45, 46, "med", 43, 24],
  ["olave", "Chris Olave", "WR", "NO", 12, 14.2, 50, 51, "med", 40, 26],
  ["wilson", "Garrett Wilson", "WR", "NYJ", 17, 15.4, 31, 32, "low", 45, 19],
  ["evans", "Mike Evans", "WR", "SF", 13, 13.6, 62, 63, "med", 39, 27],
  ["worthy", "Xavier Worthy", "WR", "KC", 1, 12.8, 76, 77, "high", 44, 34],
  ["flowers", "Zay Flowers", "WR", "BAL", 4, 13.0, 72, 73, "med", 38, 25],
  ["kincaid", "Dalton Kincaid", "TE", "BUF", 86, 10.6, 78, 79, "med", 34, 26],
  ["pitts", "Kyle Pitts", "TE", "ATL", 8, 9.8, 92, 93, "high", 36, 34],
  ["daniels", "Jayden Daniels", "QB", "WAS", 5, 22.2, 26, 27, "med", 55, 21],
  ["richardson", "Anthony Richardson", "QB", "IND", 5, 20.2, 68, 69, "high", 55, 39],
  ["prescott", "Dak Prescott", "QB", "DAL", 4, 18.6, 86, 87, "med", 34, 25],
  ["mayfield", "Baker Mayfield", "QB", "TB", 6, 18.0, 102, 103, "med", 30, 27],
  ["njoku", "David Njoku", "TE", "CLE", 85, 8.9, 106, 107, "med", 27, 28],
  ["engram", "Evan Engram", "TE", "JAX", 17, 8.6, 114, 115, "med", 25, 29],
  ["pollard", "Tony Pollard", "RB", "TEN", 20, 11.6, 96, 97, "med", 29, 29],
  ["rwhite", "Rachaad White", "RB", "TB", 1, 11.2, 104, 105, "med", 28, 30],
  ["ridley", "Calvin Ridley", "WR", "TEN", 0, 11.8, 110, 111, "high", 31, 33],
  ["reed", "Jayden Reed", "WR", "GB", 11, 12.0, 88, 89, "med", 35, 27],
  ["addison", "Jordan Addison", "WR", "MIN", 3, 11.4, 116, 117, "high", 29, 34],
  ["downs", "Josh Downs", "WR", "IND", 1, 10.8, 128, 129, "med", 24, 29],
  ["mclaurin", "Terry McLaurin", "WR", "WAS", 17, 12.6, 80, 81, "med", 34, 25],
  ["deebo", "Deebo Samuel Sr.", "WR", "WAS", 1, 11.7, 94, 95, "high", 35, 33],
  ["moore", "DJ Moore", "WR", "BUF", 2, 13.8, 56, 57, "med", 38, 24],
  ["odunze", "Rome Odunze", "WR", "CHI", 15, 12.4, 84, 85, "med", 39, 28],
  ["pickens", "George Pickens", "WR", "DAL", 3, 12.5, 90, 91, "high", 41, 34],
  ["sutton", "Courtland Sutton", "WR", "DEN", 14, 12.1, 98, 99, "med", 32, 27],
  ["aiyuk", "Brandon Aiyuk", "WR", "SF", 11, 11.6, 108, 109, "high", 34, 35],
  ["kupp", "Cooper Kupp", "WR", "SEA", 10, 11.5, 112, 113, "high", 32, 36],
  ["godwin", "Chris Godwin", "WR", "TB", 14, 10.9, 118, 119, "high", 29, 34],
  ["shakir", "Khalil Shakir", "WR", "BUF", 10, 10.6, 132, 133, "med", 24, 28],
  ["coleman", "Keon Coleman", "WR", "BUF", 0, 10.4, 140, 141, "high", 30, 36],
  ["waddle", "Jaylen Waddle", "WR", "DEN", 17, 13.4, 64, 65, "med", 39, 27],
  ["rice", "Rashee Rice", "WR", "KC", 4, 13.5, 66, 67, "high", 42, 35],
  ["johnston", "Quentin Johnston", "WR", "LAC", 1, 9.8, 150, 151, "high", 24, 38],
  ["meyers", "Jakobi Meyers", "WR", "LV", 16, 10.5, 136, 137, "med", 22, 27],
  ["pearce", "Alec Pierce", "WR", "IND", 14, 9.2, 170, 171, "high", 23, 39],
  ["mitchell", "Adonai Mitchell", "WR", "IND", 10, 9.1, 178, 179, "high", 27, 42],
  ["doubs", "Romeo Doubs", "WR", "GB", 87, 9.9, 156, 157, "med", 23, 31],
  ["watson", "Christian Watson", "WR", "GB", 9, 10.1, 146, 147, "high", 31, 41],
  ["shaheed", "Rashid Shaheed", "WR", "NO", 22, 9.7, 160, 161, "high", 30, 40],
  ["leggette", "Xavier Legette", "WR", "CAR", 17, 9.6, 164, 165, "high", 26, 37],
  ["thielen", "Adam Thielen", "WR", "CAR", 19, 9.3, 188, 189, "high", 18, 35],
  ["dhop", "DeAndre Hopkins", "WR", "BAL", 10, 8.8, 194, 195, "high", 21, 39],
  ["douglas", "Demario Douglas", "WR", "NE", 3, 8.9, 196, 197, "med", 18, 32],
  ["jeudy", "Jerry Jeudy", "WR", "CLE", 3, 11.1, 122, 123, "med", 28, 29],
  ["tillman", "Cedric Tillman", "WR", "CLE", 19, 9.4, 176, 177, "high", 24, 38],
  ["judkins", "Quinshon Judkins", "RB", "CLE", 10, 13.9, 58, 59, "med", 40, 27],
  ["henderson", "TreVeyon Henderson", "RB", "NE", 32, 13.2, 74, 75, "med", 43, 31],
  ["omarion", "Omarion Hampton", "RB", "LAC", 8, 14.6, 44, 45, "med", 45, 27],
  ["conner", "James Conner", "RB", "ARI", 6, 13.0, 78, 80, "med", 31, 28],
  ["chuba", "Chuba Hubbard", "RB", "CAR", 30, 12.7, 86, 88, "med", 28, 27],
  ["najee", "Najee Harris", "RB", "LAC", 22, 11.9, 100, 101, "med", 27, 30],
  ["brob", "Brian Robinson Jr.", "RB", "WAS", 8, 11.4, 120, 121, "med", 26, 31],
  ["javonte", "Javonte Williams", "RB", "DAL", 33, 10.2, 142, 143, "high", 24, 36],
  ["bucky", "Bucky Irving", "RB", "TB", 7, 13.7, 60, 61, "low", 38, 22],
  ["spears", "Tyjae Spears", "RB", "TEN", 2, 9.8, 152, 153, "high", 25, 39],
  ["benson", "Trey Benson", "RB", "ARI", 33, 9.7, 158, 159, "high", 29, 40],
  ["allgeier", "Tyler Allgeier", "RB", "ATL", 25, 8.6, 190, 191, "med", 18, 34],
  ["charbonnet", "Zach Charbonnet", "RB", "SEA", 26, 9.5, 166, 167, "med", 24, 34],
  ["mason", "Jordan Mason", "RB", "MIN", 27, 8.9, 184, 185, "high", 22, 37],
  ["ekeler", "Austin Ekeler", "RB", "WAS", 30, 8.4, 204, 205, "high", 17, 38],
  ["roschon", "Roschon Johnson", "RB", "CHI", 23, 8.2, 214, 215, "high", 18, 40],
  ["tua", "Tua Tagovailoa", "QB", "MIA", 1, 17.6, 130, 131, "high", 27, 34],
  ["herbert", "Justin Herbert", "QB", "LAC", 10, 18.4, 96, 98, "med", 34, 25],
  ["purdy", "Brock Purdy", "QB", "SF", 13, 17.8, 124, 125, "med", 26, 27],
  ["goff", "Jared Goff", "QB", "DET", 16, 17.7, 126, 127, "med", 24, 28],
  ["caleb", "Caleb Williams", "QB", "CHI", 18, 18.8, 106, 108, "med", 39, 28],
  ["murray", "Kyler Murray", "QB", "MIN", 1, 19.2, 90, 92, "med", 43, 27],
  ["lawrence", "Trevor Lawrence", "QB", "JAX", 16, 16.6, 154, 155, "med", 24, 32],
  ["darnold", "Sam Darnold", "QB", "SEA", 14, 15.8, 198, 199, "high", 21, 39],
  ["hock", "T.J. Hockenson", "TE", "MIN", 87, 9.2, 134, 135, "high", 27, 34],
  ["goedert", "Dallas Goedert", "TE", "PHI", 88, 8.4, 172, 173, "high", 21, 36],
  ["ferguson", "Jake Ferguson", "TE", "DAL", 87, 8.8, 148, 149, "med", 24, 30],
  ["likely", "Isaiah Likely", "TE", "BAL", 80, 7.8, 206, 207, "high", 23, 42],
  ["muth", "Pat Freiermuth", "TE", "PIT", 88, 7.9, 200, 201, "med", 18, 34],
  ["kraft", "Tucker Kraft", "TE", "GB", 85, 8.1, 182, 183, "med", 22, 35],
  ["otton", "Cade Otton", "TE", "TB", 88, 7.2, 224, 225, "med", 16, 35],
  ["ertz", "Zach Ertz", "TE", "WAS", 86, 6.9, 232, 233, "high", 14, 38],
  ["kmitchell", "Keaton Mitchell", "RB", "BAL", 34, 10.0, 125, 125, "high", 31, 40],
  ["rhamondre", "Rhamondre Stevenson", "RB", "NE", 38, 9.7, 127, 127, "med", 25, 31],
  ["mgolden", "Matthew Golden", "WR", "GB", 22, 9.8, 128, 128, "med", 29, 34],
  ["dobbins", "J.K. Dobbins", "RB", "DEN", 27, 9.8, 129, 129, "high", 27, 42],
  ["braelon", "Braelon Allen", "RB", "NYJ", 0, 9.4, 132, 132, "med", 28, 34],
  ["hhenry", "Hunter Henry", "TE", "NE", 85, 7.8, 133, 133, "med", 18, 31],
  ["strange", "Brenton Strange", "TE", "JAX", 85, 7.7, 134, 134, "med", 20, 33],
  ["vidal", "Kimani Vidal", "RB", "LAC", 30, 9.2, 136, 136, "high", 26, 39],
  ["jbrooks", "Jonathon Brooks", "RB", "CAR", 24, 9.0, 138, 138, "high", 28, 43],
  ["barner", "A.J. Barner", "TE", "SEA", 88, 7.6, 139, 139, "med", 19, 34],
  ["jcoker", "Jalen Coker", "WR", "CAR", 18, 9.3, 140, 140, "med", 24, 33],
  ["tuten", "Bhayshul Tuten", "RB", "JAX", 33, 9.1, 142, 142, "high", 28, 41],
  ["jlove", "Jeremiyah Love", "RB", "ARI", 4, 14.8, 33, 33, "med", 44, 28],
  ["jprice", "Jadarian Price", "RB", "SEA", 8, 12.4, 66, 66, "high", 36, 34],
  ["tdell", "Tank Dell", "WR", "HOU", 3, 9.2, 143, 143, "high", 27, 42],
  ["kmiller", "Kendre Miller", "RB", "NO", 25, 8.9, 144, 144, "high", 25, 40],
  ["stucker", "Sean Tucker", "RB", "TB", 44, 8.8, 147, 147, "med", 22, 36],
  ["ewilson", "Emanuel Wilson", "RB", "GB", 31, 8.7, 150, 150, "med", 20, 35],
  ["rdavis", "Ray Davis", "RB", "BUF", 22, 8.6, 151, 151, "med", 22, 36],
  ["pwashington", "Parker Washington", "WR", "JAX", 11, 8.8, 153, 153, "med", 21, 34],
  ["bigsby", "Tank Bigsby", "RB", "PHI", 4, 8.6, 154, 154, "med", 20, 35],
  ["jhiggins", "Jayden Higgins", "WR", "HOU", 81, 8.7, 155, 155, "high", 26, 39],
  ["thorton", "Tory Horton", "WR", "SEA", 14, 8.6, 156, 156, "high", 24, 38],
  ["treharris", "Tre' Harris", "WR", "LAC", 9, 8.7, 158, 158, "high", 25, 40],
  ["pbryant", "Pat Bryant", "WR", "DEN", 13, 8.5, 159, 159, "high", 24, 39],
  ["ttracy", "Tyrone Tracy Jr.", "RB", "NYG", 29, 8.7, 160, 160, "med", 22, 35],
  ["cdike", "Chimere Dike", "WR", "TEN", 15, 8.4, 161, 161, "high", 23, 39],
  ["tfranklin", "Troy Franklin", "WR", "DEN", 16, 8.4, 162, 162, "high", 23, 38],
  ["jmcmillan", "Jalen McMillan", "WR", "TB", 15, 8.5, 166, 166, "high", 24, 38],
  ["jwright", "Jaylen Wright", "RB", "MIA", 25, 8.3, 167, 167, "high", 24, 39],
  ["kallen", "Keenan Allen", "WR", "FA", 13, 8.6, 168, 168, "high", 18, 34],
  ["ogordon", "Ollie Gordon II", "RB", "MIA", 22, 8.3, 169, 169, "high", 25, 40],
  ["teslaa", "Isaac TeSlaa", "WR", "DET", 18, 8.2, 170, 170, "high", 24, 39],
  ["dvele", "Devaughn Vele", "WR", "NO", 81, 8.1, 172, 172, "med", 18, 33],
  ["idavis", "Isaiah Davis", "RB", "NYJ", 32, 8.0, 173, 173, "med", 19, 35],
  ["tez", "Tez Johnson", "WR", "TB", 83, 8.0, 175, 175, "high", 23, 39],
  ["dschultz", "Dalton Schultz", "TE", "HOU", 86, 7.6, 176, 176, "med", 17, 33],
  ["ttucker", "Tre Tucker", "WR", "LV", 11, 7.9, 178, 178, "med", 19, 34],
  ["elic", "Elic Ayomanor", "WR", "TEN", 15, 7.9, 179, 179, "high", 23, 39],
  ["wshipley", "Will Shipley", "RB", "PHI", 28, 7.8, 180, 180, "med", 20, 35],
  ["mooney", "Darnell Mooney", "WR", "ATL", 1, 7.9, 181, 181, "high", 17, 35],
  ["kwilliams", "Kyle Williams", "WR", "NE", 10, 7.8, 182, 182, "high", 22, 38],
  ["dneal", "Devin Neal", "RB", "NO", 23, 7.7, 183, 183, "high", 22, 39],
  ["pacheco", "Isiah Pacheco", "RB", "FA", 10, 8.0, 185, 185, "high", 18, 38],
  ["bateman", "Rashod Bateman", "WR", "BAL", 7, 7.8, 186, 186, "med", 17, 34],
  ["perine", "Samaje Perine", "RB", "CIN", 34, 7.5, 187, 187, "med", 15, 33],
  ["flournoy", "Ryan Flournoy", "WR", "DAL", 80, 7.5, 188, 188, "high", 21, 38],
  ["bsmith", "Brashard Smith", "RB", "KC", 26, 7.6, 189, 189, "high", 22, 39],
  ["ravensdst", "Ravens DST", "DST", "BAL", 0, 7.6, 210, 211, "low", 18, 18],
  ["steelersdst", "Steelers DST", "DST", "PIT", 0, 7.4, 218, 219, "low", 17, 18],
  ["cowboysdst", "Cowboys DST", "DST", "DAL", 0, 7.2, 226, 227, "med", 16, 20],
  ["ninersdst", "49ers DST", "DST", "SF", 0, 7.1, 230, 231, "med", 16, 20],
  ["aubrey", "Brandon Aubrey", "K", "DAL", 17, 8.5, 212, 213, "low", 15, 15],
  ["tucker", "Justin Tucker", "K", "BAL", 9, 7.8, 240, 241, "med", 12, 17],
  ["butker", "Harrison Butker", "K", "KC", 7, 7.9, 236, 237, "med", 13, 17],
  ["moody", "Jake Moody", "K", "SF", 4, 7.5, 248, 249, "med", 11, 18],
];

const DEFAULT_ROSTER_CONFIG = { QB: 1, RB: 2, WR: 2, TE: 1, FLEX: 1, SUPERFLEX: 0, DST: 1, K: 1, BENCH: 3 };
const SUPERFLEX_ROSTER_CONFIG = { QB: 1, RB: 2, WR: 2, TE: 1, FLEX: 1, SUPERFLEX: 1, DST: 1, K: 1, BENCH: 3 };
const TWO_QB_ROSTER_CONFIG = { QB: 2, RB: 2, WR: 2, TE: 1, FLEX: 1, SUPERFLEX: 0, DST: 1, K: 1, BENCH: 3 };

const DRAFT_ROOKIE_IDS = new Set([
  "judkins", "henderson", "omarion", "mgolden", "tuten", "jhiggins", "thorton",
  "treharris", "pbryant", "cdike", "tfranklin", "jmcmillan", "ogordon", "teslaa",
  "dneal", "elic", "bsmith", "jlove", "jeremiyahlove", "csv-jeremiyah-love",
  "jprice", "jadarianprice", "csv-jadarian-price",
]);

function isRookieProfile(player) {
  return player.rookie
    || DRAFT_ROOKIE_IDS.has(player.id)
    || (player.age && player.age <= 22 && !player.nine49Model?.history?.length && !player.traj?.length);
}

function overallPickFromAdp(adp, teamCount = 12) {
  if (!Number.isFinite(adp) || adp <= 0) return 0;
  const round = Math.floor(adp);
  const pick = Math.round((adp % 1) * 100);
  if (adp < 20 && round >= 1 && pick >= 1 && pick <= teamCount) {
    return (round - 1) * teamCount + pick;
  }
  return Math.max(1, Math.round(adp));
}

function isAdpBoardRookie(player) {
  if (player.pos === "DST" || player.pos === "K") return false;
  if (isRookieProfile(player)) return true;
  const noHistory = !(player.nine49Model?.history?.length) && !(player.traj?.length);
  return !!(player.adpSource && noHistory);
}

const KICKER_OVERALL_FLOOR = 100;

function placeAfterOverallFloor(board, incoming, afterPick = KICKER_OVERALL_FLOOR) {
  const seen = new Set(board.map((player) => player.id));
  const extra = incoming.filter((player) => player.id && !seen.has(player.id));
  if (!extra.length) return board;
  const next = board.slice();
  next.splice(Math.min(next.length, Math.max(0, afterPick)), 0, ...extra);
  return next;
}

function placeRookiesByAdp(veterans, rookies, teamCount = 12) {
  const seen = new Set(veterans.map((player) => player.id));
  const incoming = rookies
    .filter((player) => player.id && !seen.has(player.id) && isAdpBoardRookie(player))
    .sort((a, b) => overallPickFromAdp(a.adp || 0, teamCount) - overallPickFromAdp(b.adp || 0, teamCount)
      || String(a.id).localeCompare(String(b.id)));
  const board = veterans.slice();
  incoming.forEach((rookie) => {
    const pick = overallPickFromAdp(rookie.adp || 0, teamCount);
    if (!pick) {
      board.push(rookie);
      return;
    }
    board.splice(Math.min(board.length, Math.max(0, pick - 1)), 0, rookie);
    seen.add(rookie.id);
  });
  return board;
}

const DRAFT_TEAM_NAMES = [
  "The Vector Reapers", "Dynasty Warriors", "Saquon Family Robinson", "Bijan Mustard",
  "Allen Wrenches", "Multiple Scorgasms", "Lamb Chops", "Mahomies & Co.",
  "Justifiably Jefferson", "Hurts So Good", "Punt God", "Last Place Lizards"
];

const VALUE_BANDS = [
  { label: "High Steal", color: "#123E7C", tone: "var(--mint-soft)", min: 24 },
  { label: "Mid Steal",  color: "#2F9BEA", tone: "var(--black)",     min: 16 },
  { label: "Low Steal",  color: "#58C9BE", tone: "var(--black)",     min: 8  },
  { label: "Expected",   color: "#5DD36D", tone: "var(--black)",     min: -6 },
  { label: "Low Reach",  color: "#F2C94C", tone: "var(--black)",     min: -18 },
  { label: "Mid Reach",  color: "#FF9E3D", tone: "var(--black)",     min: -32 },
  { label: "High Reach", color: "#F0442A", tone: "white",            min: -999 },
];

const TEAM_SOS_OPPONENT_RECORDS = {
  BAL: { wins: 166, losses: 123, ties: 0 },
  CIN: { wins: 164, losses: 125, ties: 0 },
  PIT: { wins: 160, losses: 129, ties: 0 },
  CLE: { wins: 158, losses: 131, ties: 0 },
  NYG: { wins: 156, losses: 133, ties: 0 },
  CHI: { wins: 154, losses: 135, ties: 0 },
  MIN: { wins: 152, losses: 137, ties: 0 },
  SEA: { wins: 150, losses: 139, ties: 0 },
  DEN: { wins: 148, losses: 141, ties: 0 },
  LV:  { wins: 146, losses: 143, ties: 0 },
  NE:  { wins: 144, losses: 145, ties: 0 },
  TEN: { wins: 142, losses: 147, ties: 0 },
  JAX: { wins: 140, losses: 149, ties: 0 },
  DAL: { wins: 138, losses: 151, ties: 0 },
  PHI: { wins: 136, losses: 153, ties: 0 },
  WAS: { wins: 134, losses: 155, ties: 0 },
  BUF: { wins: 132, losses: 157, ties: 0 },
  NYJ: { wins: 130, losses: 159, ties: 0 },
  MIA: { wins: 128, losses: 161, ties: 0 },
  KC:  { wins: 126, losses: 163, ties: 0 },
  LAC: { wins: 124, losses: 165, ties: 0 },
  HOU: { wins: 122, losses: 167, ties: 0 },
  IND: { wins: 120, losses: 169, ties: 0 },
  TB:  { wins: 118, losses: 171, ties: 0 },
  NO:  { wins: 116, losses: 173, ties: 0 },
  ATL: { wins: 114, losses: 175, ties: 0 },
  CAR: { wins: 112, losses: 177, ties: 0 },
  GB:  { wins: 110, losses: 179, ties: 0 },
  DET: { wins: 108, losses: 181, ties: 0 },
  ARI: { wins: 106, losses: 183, ties: 0 },
  LAR: { wins: 104, losses: 185, ties: 0 },
  SF:  { wins: 102, losses: 187, ties: 0 },
};

const TEAM_SOS_RANKS = Object.fromEntries(
  Object.entries({})
    .map(([team, record]) => {
      const games = record.wins + record.losses + record.ties;
      const winPct = games ? (record.wins + record.ties * 0.5) / games : 0.5;
      return { team, winPct };
    })
    .sort((a, b) => b.winPct - a.winPct)
    .map((entry, index) => [entry.team, { rank: index + 1, winPct: entry.winPct }])
);

const POSITION_VOLATILITY_SD_BY_FORMAT = {
  ppr: { QB: 0.185, RB: 0.323, WR: 0.236, TE: 0.487, K: 0.25, DST: 0.25 },
  half: { QB: 0.185, RB: 0.380, WR: 0.242, TE: 0.572, K: 0.25, DST: 0.25 },
  std: { QB: 0.185, RB: 0.436, WR: 0.248, TE: 0.657, K: 0.25, DST: 0.25 },
};

function positionVolatilitySd(pos, scoring = "ppr") {
  const key = scoring === "std" || scoring === "standard" ? "std" : scoring === "half" ? "half" : "ppr";
  const table = POSITION_VOLATILITY_SD_BY_FORMAT[key] || POSITION_VOLATILITY_SD_BY_FORMAT.ppr;
  return table[pos] ?? 0.25;
}

const POSITION_RELEVANT_COUNTS = { QB: 16, RB: 30, WR: 36, TE: 14, DST: 12, K: 12 };
const VALUE_A_PLUS_MIN = 8;
const VALUE_A_PLUS_MAX = 10;
const POSITION_VALUE_THRESHOLDS = {
  QB: { "A+": 1.08, A: 1.02, B: 0.90, C: 0.70, D: 0.50, F: 0.50 },
  RB: { "A+": 1.20, A: 1.08, B: 0.90, C: 0.70, D: 0.50, F: 0.50 },
  WR: { "A+": 1.18, A: 1.08, B: 0.90, C: 0.70, D: 0.50, F: 0.50 },
  TE: { "A+": 1.10, A: 1.02, B: 0.90, C: 0.70, D: 0.50, F: 0.50 },
  DST: { "A+": 1.06, A: 1.02, B: 0.90, C: 0.70, D: 0.50, F: 0.50 },
  K: { "A+": 1.06, A: 1.02, B: 0.90, C: 0.70, D: 0.50, F: 0.50 },
};
const VALUE_GRADE_ORDER = ["A+", "A", "B", "C", "D", "F"];

const NINE49_2026_MODEL = {
  "Jahmyr Gibbs": { r: 1, s: 364.7, p: 352.0, c: 372.2, f: 331.8, k: 86, v: 2.049, pv: 2.396, history: [216.1, 336.9, 289.9], historyGames: [15, 17, 17] },
  "Bijan Robinson": { r: 2, s: 328.7, p: 322.9, c: 343.6, f: 302.3, k: 87, v: 1.958, pv: 2.198, history: [217.3, 311.2, 291.8], historyGames: [17, 17, 17] },
  "Saquon Barkley": { r: 3, s: 320.7, p: 307.4, c: 326.0, f: 288.9, k: 100, v: 1.969, pv: 2.093, history: [202.7, 338.8, 195.3], historyGames: [14, 16, 17] },
  "Derrick Henry": { r: 4, s: 316.4, p: 304.0, c: 318.6, f: 289.3, k: 100, v: 2.104, pv: 2.069 },
  "Ja'Marr Chase": { r: 5, s: 306.5, p: 318.5, c: 336.1, f: 300.9, k: 100, v: 1.682, pv: 2.095 },
  "Kyren Williams": { r: 6, s: 280.1, p: 279.9, c: 327.8, f: 232.0, k: 94, v: 1.285, pv: 1.905 },
  "Lamar Jackson": { r: 7, s: 260.8, p: 493.3, c: 515.0, f: 471.6, k: 92, v: 1.782, pv: 2.351 },
  "De'von Achane": { r: 8, s: 252.4, p: 271.4, c: 274.2, f: 268.5, k: 75, v: 1.623, pv: 1.847 },
  "Amon-Ra St. Brown": { r: 9, s: 251.0, p: 270.0, c: 279.4, f: 260.6, k: 100, v: 1.673, pv: 1.776 },
  "James Cook": { r: 10, s: 240.5, p: 258.5, c: 284.6, f: 232.4, k: 80, v: 1.375, pv: 1.760 },
  "Josh Jacobs": { r: 11, s: 231.1, p: 236.8, c: 251.0, f: 222.5, k: 100, v: 1.826, pv: 1.611 },
  "Justin Jefferson": { r: 12, s: 227.8, p: 249.8, c: 255.8, f: 243.8, k: 100, v: 1.655, pv: 1.643 },
  "Josh Allen": { r: 13, s: 224.9, p: 435.9, c: 445.5, f: 426.3, k: 100, v: 2.073, pv: 2.078 },
  "Ceedee Lamb": { r: 14, s: 223.7, p: 246.4, c: 262.7, f: 230.0, k: 100, v: 1.785, pv: 1.621 },
  "Breece Hall": { r: 15, s: 216.3, p: 238.7, c: 243.9, f: 233.5, k: 81, v: 1.409, pv: 1.625 },
  "Christian McCaffrey": { r: 16, s: 208.6, p: 219.3, c: 252.5, f: 186.1, k: 100, v: 1.764, pv: 1.492 },
  "Joe Mixon": { r: 17, s: 206.1, p: 229.9, c: 234.5, f: 225.4, k: 82, v: 1.662, pv: 1.565 },
  "Joe Burrow": { r: 18, s: 205.4, p: 410.5, c: 437.8, f: 383.3, k: 100, v: 1.621, pv: 1.957 },
  "Tyreek Hill": { r: 19, s: 196.5, p: 222.9, c: 258.3, f: 187.6, k: 100, v: 1.604, pv: 1.466 },
  "Alvin Kamara": { r: 20, s: 196.2, p: 226.5, c: 238.0, f: 214.9, k: 76, v: 1.507, pv: 1.541 },
  "Chase Brown": { r: 21, s: 191.3, p: 250.7, c: 289.4, f: 212.0, k: 57, v: 1.017, pv: 1.706 },
  "James Conner": { r: 22, s: 188.6, p: 224.1, c: 226.5, f: 221.7, k: 70, v: 1.471, pv: 1.525 },
  "Brian Thomas": { r: 23, s: 183.4, p: 240.5, c: 255.4, f: 225.6, k: 63, v: 1.616, pv: 1.582 },
  "Mike Evans": { r: 24, s: 182.5, p: 217.8, c: 223.8, f: 211.8, k: 90, v: 1.419, pv: 1.433 },
  "Jonathan Taylor": { r: 25, s: 182.2, p: 221.6, c: 234.3, f: 208.9, k: 67, v: 1.272, pv: 1.508 },
  "Chuba Hubbard": { r: 26, s: 178.5, p: 220.5, c: 245.1, f: 196.0, k: 65, v: 1.146, pv: 1.501 },
  "Terry McLaurin": { r: 27, s: 175.3, p: 212.0, c: 221.8, f: 202.1, k: 90, v: 1.301, pv: 1.394 },
  "Jalen Hurts": { r: 28, s: 174.9, p: 370.1, c: 384.1, f: 356.1, k: 100, v: 1.804, pv: 1.764 },
  "David Montgomery": { r: 29, s: 174.6, p: 209.5, c: 223.0, f: 196.0, k: 76, v: 1.397, pv: 1.426 },
  "Jared Goff": { r: 30, s: 171.8, p: 372.0, c: 381.5, f: 362.5, k: 95, v: 1.670, pv: 1.773 },
  "Jayden Daniels": { r: 31, s: 171.8, p: 405.8, c: 431.0, f: 380.6, k: 67, v: 1.867, pv: 1.934 },
  "DJ Moore": { r: 32, s: 169.5, p: 207.2, c: 218.5, f: 195.9, k: 90, v: 1.333, pv: 1.363 },
  "Drake London": { r: 33, s: 166.4, p: 214.1, c: 229.4, f: 198.8, k: 77, v: 1.150, pv: 1.408 },
  "Nico Collins": { r: 34, s: 164.8, p: 211.1, c: 231.7, f: 190.4, k: 80, v: 1.061, pv: 1.388 },
  "Davante Adams": { r: 35, s: 161.6, p: 192.2, c: 203.4, f: 181.0, k: 100, v: 1.564, pv: 1.264 },
  "AJ Brown": { r: 36, s: 159.8, p: 190.5, c: 193.7, f: 187.3, k: 100, v: 1.513, pv: 1.253 },
  "Malik Nabers": { r: 37, s: 158.8, p: 219.1, c: 234.5, f: 203.7, k: 63, v: 1.472, pv: 1.441 },
  "Rachaad White": { r: 38, s: 155.6, p: 201.1, c: 220.0, f: 182.1, k: 67, v: 1.294, pv: 1.368 },
  "D'Andre Swift": { r: 39, s: 149.8, p: 192.7, c: 194.6, f: 190.7, k: 72, v: 1.335, pv: 1.312 },
  "Garrett Wilson": { r: 40, s: 146.6, p: 193.9, c: 199.7, f: 188.1, k: 81, v: 1.212, pv: 1.275 },
  "Baker Mayfield": { r: 41, s: 146.4, p: 343.9, c: 382.3, f: 305.5, k: 91, v: 1.410, pv: 1.639 },
  "Courtland Sutton": { r: 42, s: 145.6, p: 198.7, c: 207.7, f: 189.8, k: 74, v: 1.097, pv: 1.307 },
  "Jordan Love": { r: 43, s: 143.2, p: 349.8, c: 403.4, f: 296.2, k: 83, v: 1.044, pv: 1.667 },
  "Aaron Jones": { r: 44, s: 141.8, p: 185.9, c: 191.7, f: 180.1, k: 72, v: 1.371, pv: 1.265 },
  "Puka Nacua": { r: 45, s: 141.3, p: 184.9, c: 187.5, f: 182.2, k: 87, v: 1.388, pv: 1.216 },
  "Bucky Irving": { r: 46, s: 139.5, p: 220.9, c: 234.6, f: 207.2, k: 41, v: 1.637, pv: 1.504 },
  "Patrick Mahomes": { r: 47, s: 139.4, p: 324.5, c: 350.3, f: 298.8, k: 99, v: 1.791, pv: 1.547 },
  "Najee Harris": { r: 48, s: 137.4, p: 182.7, c: 186.3, f: 179.2, k: 72, v: 1.409, pv: 1.244 },
  "Tony Pollard": { r: 49, s: 135.3, p: 178.4, c: 187.5, f: 169.4, k: 76, v: 1.493, pv: 1.215 },
  "Bo Nix": { r: 50, s: 135.1, p: 374.7, c: 398.0, f: 351.4, k: 72, v: 1.724, pv: 1.786 },
  "Jaxon Smith-Njigba": { r: 51, s: 134.8, p: 213.6, c: 232.2, f: 195.0, k: 63, v: 1.080, pv: 1.405 },
  "Keenan Allen": { r: 52, s: 128.7, p: 175.8, c: 195.7, f: 156.0, k: 84, v: 1.136, pv: 1.157 },
  "Brock Purdy": { r: 53, s: 127.9, p: 345.3, c: 366.3, f: 324.3, k: 69, v: 1.222, pv: 1.646 },
  "Jakobi Meyers": { r: 54, s: 127.6, p: 181.4, c: 184.8, f: 178.0, k: 76, v: 1.134, pv: 1.193 },
  "Dak Prescott": { r: 55, s: 120.8, p: 283.2, c: 302.0, f: 264.5, k: 91, v: 1.224, pv: 1.350 },
  "Jerry Jeudy": { r: 56, s: 116.7, p: 174.0, c: 187.1, f: 161.0, k: 73, v: 1.078, pv: 1.145 },
  "Justin Herbert": { r: 57, s: 115.7, p: 313.7, c: 318.5, f: 308.8, k: 80, v: 1.435, pv: 1.495 },
  "DeVonta Smith": { r: 58, s: 115.3, p: 166.7, c: 169.4, f: 164.0, k: 81, v: 1.253, pv: 1.097 },
  "Tee Higgins": { r: 59, s: 112.2, p: 164.4, c: 180.0, f: 148.9, k: 81, v: 1.089, pv: 1.082 },
  "DK Metcalf": { r: 60, s: 109.9, p: 165.7, c: 170.0, f: 161.4, k: 76, v: 1.193, pv: 1.090 },
  "Sam Darnold": { r: 61, s: 108.1, p: 315.6, c: 368.7, f: 262.6, k: 72, v: 0.776, pv: 1.504 },
  "Stefon Diggs": { r: 62, s: 106.8, p: 114.8, c: 126.5, f: 103.2, k: 100, v: 1.302, pv: 0.755 },
  "Brian Robinson": { r: 63, s: 106.7, p: 164.6, c: 172.5, f: 156.7, k: 63, v: 1.082, pv: 1.120 },
  "Jordan Addison": { r: 64, s: 106.4, p: 180.3, c: 184.5, f: 176.2, k: 74, v: 1.234, pv: 1.186 },
  "Kyler Murray": { r: 65, s: 106.2, p: 308.7, c: 311.3, f: 306.2, k: 70, v: 1.126, pv: 1.472 },
  "Geno Smith": { r: 66, s: 104.6, p: 288.0, c: 298.5, f: 277.4, k: 72, v: 1.440, pv: 1.373 },
  "Tua Tagovailoa": { r: 67, s: 102.9, p: 276.8, c: 283.0, f: 270.6, k: 72, v: 1.272, pv: 1.319 },
  "Trevor Lawrence": { r: 68, s: 101.9, p: 240.4, c: 250.0, f: 230.7, k: 76, v: 1.254, pv: 1.146 },
  "Jameson Williams": { r: 69, s: 101.8, p: 169.5, c: 195.8, f: 143.2, k: 63, v: 0.596, pv: 1.115 },
  "Matthew Stafford": { r: 70, s: 101.7, p: 282.3, c: 296.2, f: 268.5, k: 70, v: 1.034, pv: 1.346 },
  "Ladd McConkey": { r: 71, s: 99.0, p: 199.9, c: 213.1, f: 186.7, k: 41, v: 1.343, pv: 1.315 },
  "Zay Flowers": { r: 72, s: 98.1, p: 173.1, c: 174.4, f: 171.8, k: 74, v: 1.144, pv: 1.139 },
  "Brandon Aiyuk": { r: 73, s: 97.8, p: 135.4, c: 151.8, f: 119.1, k: 87, v: 1.009, pv: 0.891 },
  "CJ Stroud": { r: 74, s: 97.2, p: 252.8, c: 275.6, f: 229.9, k: 69, v: 1.339, pv: 1.205 },
  "Zach Charbonnet": { r: 75, s: 96.8, p: 175.4, c: 191.0, f: 159.9, k: 57, v: 0.947, pv: 1.194 },
  "Aaron Rodgers": { r: 76, s: 96.7, p: 279.6, c: 333.8, f: 225.4, k: 65, v: 0.926, pv: 1.333 },
  "Deebo Samuel": { r: 77, s: 96.5, p: 153.0, c: 168.2, f: 137.9, k: 78, v: 1.080, pv: 1.007 },
  "Russell Wilson": { r: 78, s: 93.3, p: 230.2, c: 239.4, f: 221.1, k: 68, v: 1.183, pv: 1.098 },
  "Javonte Williams": { r: 79, s: 93.2, p: 152.0, c: 157.3, f: 146.7, k: 66, v: 0.794, pv: 1.035 },
  "Travis Etienne": { r: 80, s: 92.8, p: 144.9, c: 164.8, f: 125.1, k: 76, v: 1.363, pv: 0.987 },
  "Kirk Cousins": { r: 81, s: 92.2, p: 209.2, c: 230.8, f: 187.5, k: 71, v: 1.147, pv: 0.997 },
  "Jaylen Waddle": { r: 82, s: 91.8, p: 120.5, c: 134.1, f: 106.9, k: 82, v: 1.131, pv: 0.793 },
  "Raheem Mostert": { r: 83, s: 91.7, p: 109.6, c: 142.3, f: 76.9, k: 84, v: 1.159, pv: 0.746 },
  "Jayden Reed": { r: 84, s: 91.1, p: 167.5, c: 173.5, f: 161.6, k: 73, v: 1.192, pv: 1.102 },
  "Justin Fields": { r: 85, s: 90.9, p: 214.6, c: 239.9, f: 189.2, k: 68, v: 1.106, pv: 1.023 },
  "Bryce Young": { r: 86, s: 90.7, p: 230.8, c: 249.2, f: 212.5, k: 66, v: 0.928, pv: 1.100 },
  "Chris Godwin": { r: 87, s: 90.7, p: 122.3, c: 139.5, f: 105.1, k: 81, v: 1.011, pv: 0.804 },
  "Cooper Kupp": { r: 88, s: 90.2, p: 136.5, c: 157.0, f: 116.1, k: 78, v: 0.986, pv: 0.898 },
  "Michael Pittman": { r: 89, s: 88.5, p: 146.6, c: 157.3, f: 136.0, k: 74, v: 1.106, pv: 0.964 },
  "Austin Ekeler": { r: 90, s: 86.8, p: 102.2, c: 127.9, f: 76.5, k: 80, v: 1.467, pv: 0.696 },
  "Calvin Ridley": { r: 91, s: 86.7, p: 164.1, c: 169.5, f: 158.7, k: 72, v: 1.207, pv: 1.080 },
  "Joe Flacco": { r: 92, s: 86.6, p: 132.6, c: 170.0, f: 95.2, k: 76, v: 0.473, pv: 0.632 },
  "George Pickens": { r: 93, s: 84.9, p: 147.3, c: 153.1, f: 141.5, k: 70, v: 1.014, pv: 0.969 },
  "Caleb Williams": { r: 94, s: 83.7, p: 294.5, c: 312.8, f: 276.2, k: 68, v: 1.355, pv: 1.404 },
  "Rhamondre Stevenson": { r: 95, s: 82.4, p: 142.1, c: 148.3, f: 135.9, k: 68, v: 1.237, pv: 0.967 },
  "Daniel Jones": { r: 96, s: 82.1, p: 137.6, c: 163.4, f: 111.8, k: 70, v: 0.815, pv: 0.656 },
  "Amari Cooper": { r: 97, s: 81.2, p: 114.9, c: 131.0, f: 98.8, k: 72, v: 1.117, pv: 0.756 },
  "Gardner Minshew": { r: 98, s: 79.9, p: 158.3, c: 170.9, f: 145.7, k: 65, v: 0.588, pv: 0.754 },
  "Jerome Ford": { r: 99, s: 79.9, p: 142.8, c: 169.2, f: 116.5, k: 65, v: 0.673, pv: 0.972 },
  "Kareem Hunt": { r: 100, s: 78.9, p: 138.2, c: 151.1, f: 125.3, k: 65, v: 0.900, pv: 0.941 },
};

function normalizePlayerKey(name) {
  return String(name || "").toLowerCase().replace(/[^a-z0-9]/g, "");
}

function stripPlayerSuffixKey(key) {
  return key.replace(/(jr|sr|ii|iii|iv|v)$/i, "");
}

const NINE49_2026_MODEL_BY_KEY = Object.fromEntries(
  Object.entries(NINE49_2026_MODEL).map(([name, data]) => [normalizePlayerKey(name), data])
);

function getNine49Model(player) {
  // Prefer API-derived model when projections hydrate successfully.
  if (player?.nine49Model?.p) return player.nine49Model;
  if (window.PROJECTIONS_OK) return null;
  const key = normalizePlayerKey(player.name);
  return NINE49_2026_MODEL[player.name] || NINE49_2026_MODEL_BY_KEY[key] || NINE49_2026_MODEL_BY_KEY[stripPlayerSuffixKey(key)] || null;
}

const BAND_CONSTANTS = {
  QB: { p10: 0.72, med: 0.95, p90: 1.20, sd: 0.185, k: 8,  pMiss: 0.38, hurtMult: 0.52 },
  RB: { p10: 0.50, med: 0.92, p90: 1.38, sd: 0.323, k: 10, pMiss: 0.28, hurtMult: 0.34 },
  WR: { p10: 0.66, med: 0.92, p90: 1.28, sd: 0.236, k: 6,  pMiss: 0.28, hurtMult: 0.62 },
  TE: { p10: 0.66, med: 0.92, p90: 1.28, sd: 0.236, k: 6,  pMiss: 0.28, hurtMult: 0.62 },
};

const ROOKIE_BAND_TOTALS = {
  QB: { floor: 0, proj: 23, ceiling: 173 },
  RB: { floor: 24, proj: 56, ceiling: 178 },
  WR: { floor: 67, proj: 112, ceiling: 211 },
  TE: { floor: 177, proj: 205, ceiling: 241 },
};

function median(values) {
  const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b);
  if (!sorted.length) return 0;
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

function variance(values) {
  const finite = values.filter(Number.isFinite);
  if (finite.length < 2) return 0;
  const avg = finite.reduce((sum, value) => sum + value, 0) / finite.length;
  return finite.reduce((sum, value) => sum + Math.pow(value - avg, 2), 0) / finite.length;
}

function bandConstantsFor(pos) {
  return BAND_CONSTANTS[pos] || BAND_CONSTANTS.WR;
}

function productionAnchor(history, historyGames = [], targetYear = 2026, rosterStatus = "active") {
  if (!history || !history.length) return null;
  const rows = history
    .map((pts, index) => ({
      pts,
      g: historyGames[index] || 17,
      y: targetYear - history.length + index,
    }))
    .filter(row => Number.isFinite(row.pts) && row.pts > 0 && row.g >= 6);
  if (!rows.length) return null;
  const weights = rows.map(row => Math.pow(0.5, (targetYear - 1 - row.y) / 1.0));
  const weightSum = weights.reduce((sum, value) => sum + value, 0) || 1;
  const ppg = rows.reduce((sum, row, index) => sum + (row.pts / row.g) * weights[index], 0) / weightSum;
  const rawGames = rows.reduce((sum, row, index) => sum + row.g * weights[index], 0) / weightSum;
  const expectedGames = Math.min(17, Math.max(1, (rawGames * rows.length + 15.5 * 3) / (rows.length + 3)));
  const censorDiscount = rosterStatus === "active" && rows.length < history.length ? 0.90 : 1;
  return { ppg, expectedGames, anchor: ppg * expectedGames * censorDiscount, seasons: rows.length };
}

function transitionRatiosFromHistory(history, historyGames = []) {
  const h = (history || [])
    .map((value, index) => ({ value, games: historyGames[index] || 17 }))
    .filter(row => Number.isFinite(row.value) && row.value > 0 && row.games >= 6)
    .map(row => row.value / row.games);
  const ratios = [];
  for (let i = 1; i < h.length; i += 1) {
    ratios.push(Math.max(0.35, Math.min(1.9, h[i] / h[i - 1])));
  }
  return ratios;
}

function shrunkRatioStats(pos, ownRatios = []) {
  const constants = bandConstantsFor(pos);
  const ratios = ownRatios.filter(value => Number.isFinite(value) && value > 0);
  const n = ratios.length;
  const weight = n / (n + constants.k);
  const ownMed = n ? median(ratios) : constants.med;
  const ownVar = n > 1 ? variance(ratios) : Math.pow(constants.sd, 2);
  return {
    med: weight * ownMed + (1 - weight) * constants.med,
    sd: Math.sqrt(weight * ownVar + (1 - weight) * Math.pow(constants.sd, 2)),
    weight,
  };
}

const ROOKIE_TIERS = {
  RB: { bellcow: { floor: 150, proj: 205, ceiling: 255 }, committee: { floor: 70, proj: 120, ceiling: 190 }, depth: { floor: 15, proj: 45, ceiling: 110 } },
  WR: { featured: { floor: 150, proj: 215, ceiling: 290 }, rotational: { floor: 80, proj: 140, ceiling: 220 }, depth: { floor: 30, proj: 75, ceiling: 150 } },
  TE: { starter: { floor: 70, proj: 130, ceiling: 210 }, depth: { floor: 20, proj: 55, ceiling: 120 } },
  QB: { starter: { floor: 140, proj: 230, ceiling: 320 }, backup: { floor: 10, proj: 60, ceiling: 160 } },
};

const ROOKIE_MANUAL_META = {
  judkins: { nflDraftRound: 1, depthChartRole: "starter" },
  henderson: { nflDraftRound: 2, depthChartRole: "committee" },
  omarion: { nflDraftRound: 1, depthChartRole: "starter" },
  mgolden: { nflDraftRound: 1, depthChartRole: "starter" },
  tuten: { nflDraftRound: 3, depthChartRole: "committee" },
  jlove: { nflDraftRound: 1, depthChartRole: "starter" },
  "csv-jeremiyah-love": { nflDraftRound: 1, depthChartRole: "starter" },
  jprice: { nflDraftRound: 2, depthChartRole: "committee" },
  "csv-jadarian-price": { nflDraftRound: 2, depthChartRole: "committee" },
  jhiggins: { nflDraftRound: 2, depthChartRole: "committee" },
  thorton: { nflDraftRound: 3, depthChartRole: "committee" },
  treharris: { nflDraftRound: 2, depthChartRole: "committee" },
  pbryant: { nflDraftRound: 3, depthChartRole: "committee" },
  cdike: { nflDraftRound: 4, depthChartRole: "backup" },
  tfranklin: { nflDraftRound: 4, depthChartRole: "backup" },
  jmcmillan: { nflDraftRound: 3, depthChartRole: "committee" },
  ogordon: { nflDraftRound: 4, depthChartRole: "backup" },
  teslaa: { nflDraftRound: 3, depthChartRole: "committee" },
  dneal: { nflDraftRound: 4, depthChartRole: "backup" },
  elic: { nflDraftRound: 4, depthChartRole: "backup" },
  bsmith: { nflDraftRound: 7, depthChartRole: "backup" },
};

function rookieTier(player) {
  const draftRound = player.nflDraftRound || ROOKIE_MANUAL_META[player.id]?.nflDraftRound || 7;
  const role = player.depthChartRole || ROOKIE_MANUAL_META[player.id]?.depthChartRole || "unknown";
  if (player.pos === "RB") return draftRound === 1 && role === "starter" ? "bellcow" : draftRound <= 3 ? "committee" : "depth";
  if (player.pos === "WR") return draftRound === 1 ? "featured" : draftRound <= 3 ? "rotational" : "depth";
  if (player.pos === "TE") return draftRound <= 2 ? "starter" : "depth";
  return role === "starter" ? "starter" : "backup";
}

function rookieBand(player, scoring = "ppr") {
  const tier = rookieTier(player);
  const band = ROOKIE_TIERS[player.pos]?.[tier] || ROOKIE_TIERS.WR.depth;
  const scale = player.pos === "QB" ? 1 : scoring === "std" ? 0.62 : scoring === "half" ? 0.80 : 1;
  return {
    floor: Math.round(band.floor * scale),
    proj: Math.round(band.proj * scale),
    ceiling: Math.round(band.ceiling * scale),
    conditional: true,
    confidence: "rookie",
    isRookie: true,
    seasons: 0,
    rookieTier: tier,
    source: "rookie-opportunity-tier",
  };
}

function projectionFallbackBand(player) {
  const constants = bandConstantsFor(player.pos);
  const shippedProjection = player.nine49Model?.p || player.ros || (player.proj || 0) * 17;
  if (!shippedProjection) return rookieBand(player);
  const regression = ({ QB: 0.84, RB: 0.82, WR: 0.86, TE: 0.86 })[player.pos] || 0.84;
  const projected = shippedProjection * regression;
  const anchor = projected / constants.med;
  return {
    floor: Math.max(0, Math.round(anchor * constants.p10)),
    proj: Math.max(0, Math.round(projected)),
    ceiling: Math.max(0, Math.round(anchor * constants.p90)),
    conditional: true,
    confidence: "low",
    shrinkWeight: 0,
    source: "projection-regressed-reference-class",
  };
}

function healthyBand(player, scoring = "ppr") {
  const anchorData = productionAnchor(player.nine49Model?.history || player.history, player.nine49Model?.historyGames || player.historyGames, 2026, player.rosterStatus || "active");
  if (anchorData == null) return isRookieProfile(player) ? rookieBand(player, scoring) : projectionFallbackBand(player);
  const { med, sd, weight } = shrunkRatioStats(player.pos, player.transitionRatios || []);
  return {
    floor: Math.max(0, Math.round(anchorData.anchor * Math.max(0, med - 1.28 * sd))),
    proj: Math.max(0, Math.round(anchorData.anchor * med)),
    ceiling: Math.max(0, Math.round(anchorData.anchor * (med + 1.28 * sd))),
    conditional: true,
    confidence: player.pos === "TE" ? "low" : "normal",
    shrinkWeight: weight,
    seasons: anchorData.seasons,
    source: "healthy-conditional",
  };
}

function injuryProfile(player) {
  const constants = bandConstantsFor(player.pos);
  const games = player.nine49Model?.historyGames || player.historyGames || [];
  const prior = 8;
  const misses = games.filter(gameCount => gameCount < 14).length;
  const pMiss = (misses + constants.pMiss * prior) / (games.length + prior);
  const anchor = productionAnchor(player.nine49Model?.history || player.history, player.nine49Model?.historyGames || player.historyGames, 2026, player.rosterStatus || "active")?.anchor;
  return {
    pMiss,
    pointsIfHurt: anchor ? Math.round(anchor * constants.hurtMult) : null,
    formula: `${misses}/${games.length} seasons under 14 games, shrunk to ${(constants.pMiss * 100).toFixed(0)}% position rate`,
  };
}

const WORKBOOK_VALUE_INDEX = {
  "A.J. Brown": 1.51325417,
  "Alvin Kamara": 1.506631849,
  "Amon-Ra St. Brown": 1.672508716,
  "Anthony Richardson": 0.5927361788,
  "Baker Mayfield": 1.410202819,
  "Bijan Robinson": 1.958003932,
  "Breece Hall": 1.409071262,
  "Brock Purdy": 1.222288269,
  "Bucky Irving": 1.636794961,
  "C.J. Stroud": 1.33894869,
  "Caleb Williams": 1.35528575,
  "CeeDee Lamb": 1.784502068,
  "Christian McCaffrey": 1.76350023,
  "D'Andre Swift": 1.334727625,
  "DJ Moore": 1.33316886,
  "DK Metcalf": 1.19250521,
  "Dak Prescott": 1.223975665,
  "David Montgomery": 1.39696881,
  "De'Von Achane": 1.623087081,
  "DeVonta Smith": 1.253429594,
  "Derrick Henry": 2.104097824,
  "Drake London": 1.14972375,
  "Garrett Wilson": 1.212440027,
  "George Pickens": 1.013987807,
  "Ja'Marr Chase": 1.682364131,
  "Jahmyr Gibbs": 2.048772326,
  "Jalen Hurts": 1.803826275,
  "James Conner": 1.470818469,
  "James Cook": 1.374739815,
  "Jared Goff": 1.670368595,
  "Jaxon Smith-Njigba": 1.079503918,
  "Jayden Daniels": 1.867487122,
  "Jayden Reed": 1.191721257,
  "Jaylen Waddle": 1.130908867,
  "Joe Burrow": 1.620973913,
  "Jonathan Taylor": 1.271992463,
  "Jordan Addison": 1.234054744,
  "Jordan Love": 1.044191296,
  "Josh Allen": 2.072582431,
  "Josh Jacobs": 1.826482381,
  "Justin Herbert": 1.43490016,
  "Justin Jefferson": 1.655037753,
  "Kyren Williams": 1.284588893,
  "Ladd McConkey": 1.343248262,
  "Lamar Jackson": 1.782043527,
  "Malik Nabers": 1.472264603,
  "Mike Evans": 1.418507794,
  "Najee Harris": 1.409318251,
  "Patrick Mahomes": 1.791094105,
  "Puka Nacua": 1.387933609,
  "Rachaad White": 1.294468446,
  "Rhamondre Stevenson": 1.23667306,
  "Saquon Barkley": 1.968500957,
  "Terry McLaurin": 1.301362748,
  "Tony Pollard": 1.493294453,
  "Trevor Lawrence": 1.253735194,
  "Tua Tagovailoa": 1.271529551,
  "Tyreek Hill": 1.604416758,
  "Zay Flowers": 1.143676109
};

function draftPlayerPreference(player) {
  return (Number.isFinite(player?.num) && player.num > 0 ? 100 : 0)
    + (player?.team && player.team !== "FA" ? 20 : 0)
    + (player?.profile ? 10 : 0)
    + (Number.isFinite(player?.ros) && player.ros > 0 ? 1 : 0);
}

function dedupeDraftPlayerSource(players) {
  const canonical = new Map();
  (players || []).forEach(player => {
    const key = `${player.id || normalizePlayerKey(player.name)}:${player.pos}`;
    const existing = canonical.get(key);
    if (!existing || draftPlayerPreference(player) > draftPlayerPreference(existing)) {
      canonical.set(key, player);
    }
  });
  return Array.from(canonical.values());
}

function observedNameKey(name) {
  const raw = String(name || "")
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, " ")
    .replace(/\b(jr|sr|ii|iii|iv|v)\b/g, "")
    .replace(/\s+/g, " ")
    .trim();
  const aliases = window.OBSERVED_DRAFT_ADP?.aliases || {};
  return aliases[raw] || raw;
}

function blendEspnObservedAdp(espnAdp, observedAdp, drafts, espnPrior = 6) {
  const espn = Number(espnAdp);
  const observed = Number(observedAdp);
  if (!(observed > 0)) return espn > 0 ? +espn.toFixed(2) : null;
  if (!(espn > 0)) return +observed.toFixed(2);
  const sample = Math.max(1, Number(drafts) || 1);
  const observedWeight = sample / (sample + espnPrior);
  return +(espn * (1 - observedWeight) + observed * observedWeight).toFixed(2);
}

function mergeObservedDraftPlayers(players) {
  const observed = window.OBSERVED_DRAFT_ADP?.players || [];
  if (!observed.length) return players || [];
  const list = (players || []).slice();
  const byKey = new Map(list.map((player) => [`${observedNameKey(player.name)}:${player.pos}`, player]));
  observed.forEach((row) => {
    const key = `${observedNameKey(row.name)}:${row.pos}`;
    const existing = byKey.get(key) || list.find((player) => player.id === row.id);
    if (existing) {
      existing.name = row.name || existing.name;
      existing.team = row.team || existing.team;
      existing.empiricalAdp = row.adp;
      existing.observedDrafts = row.drafts;
      const source = String(existing.adpSource || "");
      const alreadyBlended = source.includes("949-observed");
      const espn = Number(existing.espnAdp) > 0
        ? Number(existing.espnAdp)
        : (alreadyBlended ? 0 : (Number(existing.liveDraftAdp) || Number(existing.adp) || 0));
      if (espn > 0) existing.espnAdp = espn;
      if (!alreadyBlended) {
        const blended = blendEspnObservedAdp(espn, row.adp, row.drafts);
        if (blended > 0) {
          existing.adp = blended;
          existing.liveDraftAdp = blended;
          existing.adpSource = espn > 0 ? `${source || "espn-live-draft-results"}+949-observed` : "949-observed-drafts";
        }
      }
      existing.rosterStatus = existing.team === "FA" ? "free_agent" : "active";
      if (row.name === "Jeremiyah Love" || row.name === "Jadarian Price") existing.rookie = true;
      return;
    }
    const weekly = Math.max(5, +(16 - row.adp / 14).toFixed(1));
    const added = {
      id: row.id,
      name: row.name,
      pos: row.pos,
      team: row.team,
      proj: weekly,
      ros: weekly * 17,
      adp: row.adp,
      empiricalAdp: row.adp,
      liveDraftAdp: row.adp,
      observedDrafts: row.drafts,
      rank: Math.round(row.adp),
      risk: "med",
      boom: 28,
      bust: 32,
      rookie: true,
      rosterStatus: row.team === "FA" ? "free_agent" : "active",
      adpSource: "949-observed-drafts",
      notes: "Added from 949 observed 2026 drafts.",
    };
    list.push(added);
    byKey.set(key, added);
  });
  return list;
}

function isExcludedFrom2026Draft(player) {
  if (!player) return false;
  if (player.draftEligible === false || player.profile?.draftEligible === false) return true;
  const id = String(player.id || "").toLowerCase();
  if (id === "mixon" || id === "csv-joe-mixon") return true;
  const name = String(player.name || "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, " ")
    .replace(/\b(jr|sr|ii|iii|iv|v)\b/g, "")
    .replace(/\s+/g, " ")
    .trim();
  return name === "joe mixon";
}

function buildDraftPool(players, scoring = "ppr") {
  const lateDefaultAdp = pos => ({ QB: 140, RB: 130, WR: 140, TE: 130, K: 170, DST: 150 }[pos] || 999);
  const resolveDraftAdp = (player) => {
    if (Number.isFinite(player?.liveDraftAdp)) return player.liveDraftAdp;
    if (Number.isFinite(player?.empiricalAdp)) return player.empiricalAdp;
    if (Number.isFinite(player?.adp)) return player.adp;
    return lateDefaultAdp(player?.pos);
  };
  // Prefer hydrated RapidAPI/projection rows when available.
  if (window.PROJECTIONS_OK && (window.DRAFT_PLAYERS || []).length) {
    const source = typeof window.getDraftPlayersForScoring === "function"
      ? window.getDraftPlayersForScoring("ppr")
      : window.DRAFT_PLAYERS;
    return mergeObservedDraftPlayers(dedupeDraftPlayerSource(source))
      .filter((p) => !isExcludedFrom2026Draft(p))
      .map((p, i) => {
      const rosterStatus = p.rosterStatus || (p.team === "FA" ? "free_agent" : "active");
      const nine49 = getNine49Model(p);
      const transitionRatios = transitionRatiosFromHistory(nine49?.history || p.history, nine49?.historyGames || p.historyGames);
      const band = p.nine49Range || healthyBand({ ...p, rosterStatus, nine49Model: nine49, transitionRatios }, scoring);
      const adp = resolveDraftAdp(p);
      const rank = p.rank || p.profile?.projection?.positionRank || i + 1;
      return {
        ...p,
        rosterStatus,
        depthChartRole: p.depthChartRole || ROOKIE_MANUAL_META[p.id]?.depthChartRole || "unknown",
        nflDraftRound: p.nflDraftRound || ROOKIE_MANUAL_META[p.id]?.nflDraftRound || null,
        nflDraftPick: p.nflDraftPick || null,
        rookieEvaluation: band.isRookie ? `First-year evaluation. Rookie ${p.pos}s in a projected ${band.rookieTier} role have produced ${band.floor} / ${band.proj} / ${band.ceiling} over recent classes. This is an opportunity estimate, not a track record.` : null,
        nine49Model: nine49 || p.nine49Model,
        nine49Range: band,
        transitionRatios,
        adp,
        rank,
        boardRank: rank,
      };
    });
  }

  const source = mergeObservedDraftPlayers(dedupeDraftPlayerSource(window.REQUIRE_RAPIDAPI_DATA ? [] : players));
  const seen = new Set(source.map(p => p.id));
  const allowSynthetic = !window.REQUIRE_RAPIDAPI_DATA || window.ALLOW_SEEDED_PROTOTYPE_DATA;
  const supplemental = allowSynthetic ? DRAFT_SUPPLEMENTAL_RANKINGS
    .filter(([id]) => !seen.has(id))
    .map(([id, name, pos, team, num, proj, rank, adp, risk, boom, bust]) => ({
      id, name, pos, team, num, proj, actual: 0, rank, prev: rank, ros: proj * 17,
      own: 90, trend: "flat", risk, adp, round: Math.ceil(adp / 12), matchup: "TBD",
      matchupRating: 3, traj: [proj * .82, proj * .92, proj, proj * 1.05, proj * .96, proj * 1.1],
      notes: "Draft ranking placeholder for GM simulator testing.", boom, bust,
      snap: 0, tgt: pos === "WR" || pos === "TE" ? 7 : 2, rush: pos === "RB" ? 14 : 0,
      news: "Draft board testing profile.", ceiling: proj * 1.55, floor: proj * .52, synthetic: true,
      rosterStatus: team === "FA" ? "free_agent" : "active",
      depthChartRole: ROOKIE_MANUAL_META[id]?.depthChartRole || "unknown",
      nflDraftRound: ROOKIE_MANUAL_META[id]?.nflDraftRound || null,
      nflDraftPick: null,
    })) : [];
  return [...source, ...supplemental].filter((p) => !isExcludedFrom2026Draft(p)).map((p, i) => {
    const nine49 = getNine49Model(p);
    const rosterStatus = p.rosterStatus || (p.team === "FA" ? "free_agent" : "active");
    const transitionRatios = transitionRatiosFromHistory(nine49?.history || p.history, nine49?.historyGames || p.historyGames);
    const band = healthyBand({ ...p, rosterStatus, nine49Model: nine49, transitionRatios }, scoring);
    const projectedWeekly = band.proj / 17;
    if (!projectedWeekly && window.REQUIRE_RAPIDAPI_DATA) return null;
    const adp = resolveDraftAdp(p);
    const rank = p.rank || i + 1;
    return {
      ...p,
      rosterStatus,
      depthChartRole: p.depthChartRole || ROOKIE_MANUAL_META[p.id]?.depthChartRole || "unknown",
      nflDraftRound: p.nflDraftRound || ROOKIE_MANUAL_META[p.id]?.nflDraftRound || null,
      nflDraftPick: p.nflDraftPick || null,
      rookieEvaluation: band.isRookie ? `First-year evaluation. Rookie ${p.pos}s in a projected ${band.rookieTier} role have produced ${band.floor} / ${band.proj} / ${band.ceiling} over recent classes. This is an opportunity estimate, not a track record.` : null,
      nine49Model: nine49,
      nine49Range: band,
      transitionRatios,
      nine49Rank: nine49?.r || null,
      nine49Score: nine49?.s || null,
      nine49Consistency: nine49?.k || null,
      valueIndex: nine49?.v || WORKBOOK_VALUE_INDEX[p.name] || p.valueIndex || null,
      projectedValueIndex: nine49?.pv || p.projectedValueIndex || null,
      proj: projectedWeekly,
      adjusted: projectedWeekly,
      ros: band.proj,
      ceiling: band.ceiling / 17,
      floor: band.floor / 17,
      adp,
      rank,
      boardRank: rank,
    };
  }).filter(Boolean);
}

function buildDraftOrder(teamCount, rounds) {
  const order = [];
  for (let r = 0; r < rounds; r++) {
    const row = Array.from({ length: teamCount }, (_, i) => i + 1);
    if (r % 2 === 1) row.reverse();
    order.push(...row);
  }
  return order;
}

function buildRosterSlots(config) {
  if (Array.isArray(config)) return config.filter(Boolean);
  const src = config && typeof config === "object" ? config : (DEFAULT_ROSTER_CONFIG || {});
  return ["QB", "RB", "WR", "TE", "FLEX", "SUPERFLEX", "DST", "K", "BENCH"].flatMap(slot => (
    Array.from({ length: src[slot] || 0 }, () => slot)
  ));
}

function assignRosterSlots(roster, rosterSlots) {
  const slots = Array.isArray(rosterSlots)
    ? rosterSlots
    : buildRosterSlots(rosterSlots || DEFAULT_ROSTER_CONFIG);
  const players = Array.isArray(roster) ? roster : [];
  const used = new Set();
  return slots.map(slot => {
    const eligible = players.find(p => {
      if (used.has(p.id)) return false;
      if (slot === "FLEX") return ["RB", "WR", "TE"].includes(p.pos);
      if (slot === "SUPERFLEX") return ["QB", "RB", "WR", "TE"].includes(p.pos);
      if (slot === "BENCH") return true;
      return p.pos === slot;
    });
    if (eligible) used.add(eligible.id);
    return eligible || null;
  });
}

const SCORING_RECEPTION_POINTS = { ppr: 1, half: 0.5, std: 0 };

const RANKING_LEVELS = [
  { id: "std", label: "Standard", shortLabel: "STD", scoringLabel: "Standard", description: "Position ranks with no reception premium." },
  { id: "half", label: "Half PPR", shortLabel: "HALF", scoringLabel: "Half PPR", description: "Position ranks with 0.5 points per reception." },
  { id: "ppr", label: "PPR", shortLabel: "PPR", scoringLabel: "PPR", description: "Position ranks with 1.0 point per reception." },
];

const OVERALL_BASELINE_RANK = { QB: 12, RB: 30, WR: 30, TE: 12, DST: 4, K: 4 };
const OVERALL_SCARCITY_WEIGHT = { QB: 0.62, RB: 1.12, WR: 1.0, TE: 0.76, DST: 0.18, K: 0.12 };
const OVERALL_MARKET_WEIGHT = { QB: 0.68, RB: 0.98, WR: 0.96, TE: 0.64, DST: 0.18, K: 0.12 };
const OVERALL_BLUE_CHIP_WEIGHT = { QB: 0.82, RB: 1.16, WR: 1.05, TE: 0.72, DST: 0.16, K: 0.10 };
const OVERALL_FLOOR_WEIGHT = { QB: 0.34, RB: 0.66, WR: 0.59, TE: 0.39, DST: 0.07, K: 0.04 };

function formatOverallWeights(scoring) {
  return {
    scarcity: OVERALL_SCARCITY_WEIGHT,
    blueChip: OVERALL_BLUE_CHIP_WEIGHT,
    floor: OVERALL_FLOOR_WEIGHT,
    qbCompress: 1,
    qbGuardrailAfter: 3,
    qbGuardrail: 18,
  };
}

const POSITION_USAGE_DEFAULTS = {
  QB: { catchRate: 0, completedAirYardsPerReception: 0, yacPerReception: 0, yardsBeforeContactPerRush: 2.0, yardsAfterContactPerRush: 3.4 },
  RB: { catchRate: 0.78, completedAirYardsPerReception: 0.6, yacPerReception: 6.6, yardsBeforeContactPerRush: 1.5, yardsAfterContactPerRush: 2.7 },
  WR: { catchRate: 0.64, completedAirYardsPerReception: 7.7, yacPerReception: 4.3, yardsBeforeContactPerRush: 2.0, yardsAfterContactPerRush: 3.6 },
  TE: { catchRate: 0.68, completedAirYardsPerReception: 5.6, yacPerReception: 4.7, yardsBeforeContactPerRush: 1.8, yardsAfterContactPerRush: 3.0 },
  DST: { catchRate: 0, completedAirYardsPerReception: 0, yacPerReception: 0, yardsBeforeContactPerRush: 0, yardsAfterContactPerRush: 0 },
  K: { catchRate: 0, completedAirYardsPerReception: 0, yacPerReception: 0, yardsBeforeContactPerRush: 0, yardsAfterContactPerRush: 0 },
};

const POSITION_RECEPTION_POINT_CAP = {
  RB: 0.24,
  WR: 0.36,
  TE: 0.40,
};

function projectionUsageProfile(player) {
  const defaults = POSITION_USAGE_DEFAULTS[player.pos] || POSITION_USAGE_DEFAULTS.WR;
  const targets = Math.max(0, player.projectedTargetsPerGame ?? player.tgt ?? 0);
  const carries = Math.max(0, player.projectedRushesPerGame ?? player.rush ?? 0);
  const catchRate = Math.max(0, Math.min(1, player.catchRate ?? defaults.catchRate));
  const receptions = targets * catchRate;
  const completedAirYardsPerReception = Math.max(0, player.completedAirYardsPerReception ?? defaults.completedAirYardsPerReception);
  const yacPerReception = Math.max(0, player.yacPerReception ?? defaults.yacPerReception);
  const receivingYards = receptions * (completedAirYardsPerReception + yacPerReception);
  const yardsBeforeContactPerRush = Math.max(0, player.yardsBeforeContactPerRush ?? defaults.yardsBeforeContactPerRush);
  const yardsAfterContactPerRush = Math.max(0, player.yardsAfterContactPerRush ?? defaults.yardsAfterContactPerRush);
  const rushingYards = carries * (yardsBeforeContactPerRush + yardsAfterContactPerRush);
  const pprAnchor = Math.max(0, player.proj || 0);
  const receptionCap = POSITION_RECEPTION_POINT_CAP[player.pos] ?? 1;
  const scoringReceptions = Math.min(receptions, pprAnchor * receptionCap);
  const nonReceptionBase = Math.max(0, pprAnchor - scoringReceptions);

  return {
    targets,
    carries,
    catchRate,
    receptions,
    scoringReceptions,
    completedAirYardsPerReception,
    yacPerReception,
    receivingYards,
    yardsBeforeContactPerRush,
    yardsAfterContactPerRush,
    rushingYards,
    nonReceptionBase,
    pprAnchor,
    season: {
      targets: targets * 17,
      carries: carries * 17,
      receptions: receptions * 17,
      receivingYards: receivingYards * 17,
      rushingYards: rushingYards * 17,
    },
  };
}

function isCatchScoringPosition(player) {
  return ["RB", "WR", "TE"].includes(player?.pos);
}

function receptionPointValue(scoring) {
  if (scoring === "std" || scoring === "standard") return 0;
  if (scoring === "half" || scoring === "halfPpr") return 0.5;
  return 1;
}

function projectedReceptionsPerGame(player) {
  const fromPlayer = Number(player?.rec);
  if (Number.isFinite(fromPlayer) && fromPlayer > 0) return fromPlayer;
  const fromWorkload = Number(
    player?.profile?.workload?.receptionsPerGame
    ?? player?.profile?.receiving?.receptionsPerGame
  );
  if (Number.isFinite(fromWorkload) && fromWorkload > 0) return fromWorkload;
  const seasons = player?.historicalSeasons || player?.profile?.historical?.seasons || [];
  const recent = seasons
    .filter((row) => Number(row.receptions) > 0 && Number(row.gamesPlayed) > 0)
    .sort((a, b) => Number(b.year || b.season) - Number(a.year || a.season))[0];
  if (recent) return Number(recent.receptions) / Number(recent.gamesPlayed);
  return 0;
}

function pprSeasonTotal(player) {
  const fromBlock = Number(player?.profile?.scoring?.ppr?.season ?? player?.scoring?.ppr?.season);
  if (Number.isFinite(fromBlock) && fromBlock > 0) return fromBlock;
  const weekly = Number(player?.profile?.scoring?.ppr?.weekly ?? player?.scoring?.ppr?.weekly);
  if (Number.isFinite(weekly) && weekly > 0) return weekly * 17;
  const ros = Number(player?.ros);
  if (Number.isFinite(ros) && ros >= 20) return ros;
  return (Number(player?.proj) || 0) * 17;
}

function formatSeasonPoints(player, scoring) {
  const ppr = pprSeasonTotal(player);
  if (!isCatchScoringPosition(player)) return ppr;
  const recPts = receptionPointValue(scoring);
  if (recPts >= 1) return ppr;
  const recs = projectedReceptionsPerGame(player) * 17;
  return Math.max(0, ppr - recs * (1 - recPts));
}

function compareByFormatPoints(a, b, scoring) {
  return formatSeasonPoints(b, scoring) - formatSeasonPoints(a, scoring)
    || ((b.adjusted || b.proj || 0) - (a.adjusted || a.proj || 0))
    || (a.adp || 999) - (b.adp || 999)
    || String(a.name || "").localeCompare(String(b.name || ""));
}

function scoringProjection(player, scoring) {
  return formatSeasonPoints(player, scoring) / 17;
}

function positionProductionBaseline(points, pos) {
  const valid = (points || []).filter((value) => value > 0).sort((a, b) => b - a);
  if (!valid.length) return 0;
  const cap = POSITION_RELEVANT_COUNTS[pos] || Math.max(8, Math.ceil(valid.length / 2));
  const relevant = valid.slice(0, Math.min(valid.length, Math.max(8, cap)));
  return relevant.reduce((sum, value) => sum + value, 0) / relevant.length;
}

function letterFromValueIndex(value, pos) {
  const thresholds = POSITION_VALUE_THRESHOLDS[pos] || POSITION_VALUE_THRESHOLDS.RB;
  if (value <= thresholds.F) return "F";
  if (value >= thresholds["A+"]) return "A+";
  if (value >= thresholds.A) return "A";
  if (value >= thresholds.B) return "B";
  if (value >= thresholds.C) return "C";
  return "D";
}

function clampAPlusLetters(scored) {
  const byPos = {};
  scored.forEach((player) => {
    if (!byPos[player.pos]) byPos[player.pos] = [];
    byPos[player.pos].push(player);
  });
  const letterById = {};
  Object.values(byPos).forEach((group) => {
    const sorted = group.slice().sort((a, b) =>
      (b.valueIndex || 0) - (a.valueIndex || 0)
      || formatSeasonPoints(b, "ppr") - formatSeasonPoints(a, "ppr")
    );
    const buckets = Object.fromEntries(VALUE_GRADE_ORDER.map((grade) => [grade, []]));
    sorted.forEach((player) => {
      buckets[letterFromValueIndex(player.valueIndex || 0, player.pos)].push(player);
    });
    const min = Math.min(VALUE_A_PLUS_MIN, sorted.length);
    const max = Math.min(VALUE_A_PLUS_MAX, sorted.length);
    while (buckets["A+"].length < min) {
      const donor = ["A", "B", "C", "D"].find((grade) => buckets[grade].length > 0);
      if (!donor) break;
      buckets["A+"].push(buckets[donor].shift());
    }
    while (buckets["A+"].length > max) {
      buckets.A.unshift(buckets["A+"].pop());
    }
    VALUE_GRADE_ORDER.forEach((grade) => {
      buckets[grade].forEach((player) => {
        letterById[player.id] = grade;
      });
    });
  });
  return scored.map((player) => ({
    ...player,
    valueLetter: letterById[player.id] || letterFromValueIndex(player.valueIndex || 0, player.pos),
  }));
}

function applyFormatValueGrades(players, scoring) {
  const byPos = {};
  (players || []).forEach((player) => {
    if (!byPos[player.pos]) byPos[player.pos] = [];
    byPos[player.pos].push(player);
  });
  const baselineByPos = {};
  Object.entries(byPos).forEach(([pos, group]) => {
    baselineByPos[pos] = positionProductionBaseline(group.map((player) => formatSeasonPoints(player, scoring)), pos);
  });
  const scored = (players || []).map((player) => {
    const baseline = baselineByPos[player.pos] || 0;
    const points = formatSeasonPoints(player, scoring);
    const valueIndex = baseline ? +(points / baseline).toFixed(3) : (player.valueIndex || 1);
    return {
      ...player,
      valueIndex,
      adjusted: scoringProjection(player, scoring),
    };
  });
  return clampAPlusLetters(scored);
}

function scoringValueIndex(player, scoring = "ppr") {
  if (player.valueIndex && player.valueIndex > 0) return player.valueIndex;
  const points = formatSeasonPoints(player, scoring);
  const weekly = scoringProjection(player, scoring);
  if (!points && !weekly) return null;
  return player.valueIndex || null;
}

function scoringProjectionLine(player) {
  const usage = projectionUsageProfile(player);
  return {
    ...usage,
    standard: scoringProjection(player, "std"),
    halfPpr: scoringProjection(player, "half"),
    ppr: scoringProjection(player, "ppr"),
  };
}

function nine49ScoringRankScore(player, scoring) {
  const baseScore = player.nine49Score ?? Math.max(0, scoringProjection(player, scoring) * 17);
  const weeklyDelta = scoringProjection(player, scoring) - scoringProjection(player, "ppr");
  const receptionSensitivity = ["RB", "WR", "TE"].includes(player.pos) ? 7.5 : 0;
  return baseScore + weeklyDelta * receptionSensitivity;
}

function projectionRankScore(player, scoring = "ppr", mode = "overall") {
  const projection = player.profile?.projection;
  if (window.PROJECTIONS_OK && projection && mode === "overall") {
    const apiRank = mode === "position"
      ? (projection.positionRank || projection.impactRank)
      : (projection.impactRank || projection.positionRank);
    if (typeof apiRank === "number" && apiRank > 0) return 10000 - apiRank;
    if (typeof projection.rankingScore === "number") return projection.rankingScore;
    if (typeof projection.overallScore === "number") return projection.overallScore;
  }
  const projected = scoringProjection(player, scoring) * 17;
  const ceiling = (player.ceiling || player.proj || 0) * 17;
  const floor = (player.floor || player.proj || 0) * 17;
  const adjusted = scoringProjection(player, scoring);
  const effectiveValueIndex = scoringValueIndex(player, scoring);
  const value = seasonValueScore({ ...player, adjusted, valueIndex: effectiveValueIndex });
  const future = futureProjectionScore(player);
  const consistency = consistencyScore(player) ?? (window.PROJECTIONS_OK ? 0 : 70);
  const riskPenalty = player.risk === "high" ? 10 : player.risk === "med" ? 4 : 0;
  const scarcity = ({ QB: 0.72, RB: 1.12, WR: 1.0, TE: 0.84, DST: 0.20, K: 0.16 })[player.pos] || 0.7;

  if (mode === "position") {
    return value * 2.45 + projected * 0.30 + ceiling * 0.018 + floor * 0.016 + consistency * 0.20 + future * 0.48 - riskPenalty;
  }

  const overallValueWeight = ({ QB: 0.72, RB: 1.05, WR: 0.96, TE: 0.58, DST: 0.12, K: 0.08 })[player.pos] || 0.6;
  const overallFutureWeight = ({ QB: 0.22, RB: 0.32, WR: 0.30, TE: 0.20, DST: 0.06, K: 0.04 })[player.pos] || 0.18;
  const weighted =
    (projected * scarcity) * 0.72 +
    value * overallValueWeight +
    ceiling * 0.012 +
    floor * 0.012 +
    consistency * 0.10 +
    future * overallFutureWeight -
    riskPenalty;

  return weighted;
}

function ageCurveScore(player) {
  if (!player.age || ["DST", "K"].includes(player.pos)) return 70;
  const prime = {
    QB: { start: 25, end: 33 },
    RB: { start: 22, end: 27 },
    WR: { start: 23, end: 29 },
    TE: { start: 24, end: 31 },
  }[player.pos] || { start: 23, end: 29 };
  if (player.age >= prime.start && player.age <= prime.end) return 84;
  if (player.age < prime.start) return Math.max(66, 82 - (prime.start - player.age) * 4);
  return Math.max(38, 82 - (player.age - prime.end) * (player.pos === "RB" ? 8 : 5));
}

function sampleConfidenceScore(player) {
  const games = player.nine49Model?.historyGames || player.historyGames || [];
  if (!games.length) return player.rookie ? 42 : 58;
  const recentGames = games.slice(-3);
  const total = recentGames.reduce((sum, count) => sum + count, 0);
  return Math.max(35, Math.min(92, 38 + total * 1.1));
}

function futureProjectionScore(player) {
  const modelScore = player.nine49Score != null
    ? Math.max(35, Math.min(100, player.nine49Score / 3.8))
    : seasonValueScore(player);
  const ageScore = ageCurveScore(player);
  const sampleScore = sampleConfidenceScore(player);
  const consistency = player.nine49Consistency || consistencyScore(player) || 60;
  const availability = availabilityBreakdown(player).score || 60;
  const volatilityPenalty = player.risk === "high" ? 9 : player.risk === "med" ? 4 : 0;

  return Math.max(30, Math.min(100,
    modelScore * 0.30 +
    ageScore * 0.18 +
    sampleScore * 0.16 +
    consistency * 0.20 +
    availability * 0.16 -
    volatilityPenalty
  ));
}

function standardDeviation(values) {
  if (!values.length) return 0;
  const avg = values.reduce((sum, value) => sum + value, 0) / values.length;
  const variance = values.reduce((sum, value) => sum + Math.pow(value - avg, 2), 0) / values.length;
  return Math.sqrt(variance);
}

function consistencyBreakdown(player) {
  const canonical = player.profile?.projection?.consistency;
  if (window.PROJECTIONS_OK && typeof canonical === "number" && canonical > 0) {
    return {
      score: canonical,
      raw: 0, within: 0, above: 0, below: 0, games: 0, std: 0,
      formula: "API consistency",
    };
  }
  const log = typeof window.getGameLog === "function" ? window.getGameLog(player.id) : [];
  const played = log.filter(g => g.played && !g.bye && g.actual != null && g.proj != null);
  if (!played.length) {
    const fallback = player.nine49Consistency ?? player.consistency ?? 0;
    return { score: fallback, raw: 0, within: 0, above: 0, below: 0, games: 0, std: 0, formula: "No weekly sample" };
  }

  const deltas = played.map(g => g.actual - g.proj);
  const std = standardDeviation(deltas) || 1;
  let raw = 0;
  let within = 0;
  let above = 0;
  let below = 0;
  played.forEach(g => {
    const delta = g.actual - g.proj;
    if (delta < -std) { raw -= 2; below += 1; }
    else if (delta > std) { raw += 2; above += 1; }
    else { raw += 1; within += 1; }
  });
  const avgRaw = raw / played.length;
  const score = Math.max(0, Math.min(100, 50 + avgRaw * 22));
  return {
    score, raw, within, above, below, games: played.length, std,
    formula: `${within}×1 + ${above}×2 - ${below}×2 = ${raw.toFixed(0)} over ${played.length} games`,
  };
}

function availabilityBreakdown(player) {
  const injury = injuryProfile(player);
  return {
    score: Math.max(0, Math.min(100, 100 - injury.pMiss * 100)),
    full: null,
    seasons: player.nine49Model?.historyGames?.length || player.historyGames?.length || 0,
    avgGames: null,
    pMiss: injury.pMiss,
    pointsIfHurt: injury.pointsIfHurt,
    formula: injury.formula,
  };
}

function consistencyScore(player) {
  return consistencyBreakdown(player).score;
}

function consistencyGrade(player, peers = []) {
  const score = consistencyScore(player);
  const positionPeers = peers
    .filter(p => p.pos === player.pos && consistencyScore(p) > 0)
    .sort((a, b) => consistencyScore(b) - consistencyScore(a));

  if (positionPeers.length >= 4 && score > 0) {
    const index = positionPeers.findIndex(p => p.id === player.id || p.name === player.name);
    const percentile = index < 0 ? 0 : 1 - (index / Math.max(1, positionPeers.length - 1));
    if (percentile >= 0.75) return { label: "Green", color: "var(--mint)", percentile, score };
    if (percentile >= 0.50) return { label: "Yellow", color: "var(--gold)", percentile, score };
    return { label: "Red", color: "var(--red)", percentile, score };
  }

  if (score >= 85) return { label: "Green", color: "var(--mint)", percentile: null, score };
  if (score >= 70) return { label: "Yellow", color: "var(--gold)", percentile: null, score };
  return { label: "Red", color: "var(--red)", percentile: null, score };
}

function valueBand(score) {
  return VALUE_BANDS.find(b => score >= b.min) || VALUE_BANDS[VALUE_BANDS.length - 1];
}

function marketDeltaForPick(pickNumber, adp) {
  if (!Number.isFinite(adp) || adp <= 0) return 0;
  return pickNumber - adp;
}

function marketBandForPick(pickNumber, adp, teamCount = 12) {
  const delta = marketDeltaForPick(pickNumber, adp);
  const round = Math.max(8, teamCount || 12);
  const half = Math.max(6, Math.round(round / 2));
  const noise = 4;
  if (delta >= round) return VALUE_BANDS[0];
  if (delta >= half) return VALUE_BANDS[1];
  if (delta >= noise) return VALUE_BANDS[2];
  if (delta <= -round) return VALUE_BANDS[6];
  if (delta <= -half) return VALUE_BANDS[5];
  if (delta <= -noise) return VALUE_BANDS[4];
  return VALUE_BANDS[3];
}

function enrichDraftPlayer(player, scoring, drafted, pickNumber, nextUserPick, teamCount = 12) {
  const adjusted = scoringProjection(player, scoring);
  const adp = Number.isFinite(player.adp) && player.adp > 0 ? player.adp : null;
  const marketDelta = marketDeltaForPick(pickNumber, adp);
  const nextDelta = nextUserPick && adp ? nextUserPick - adp : marketDelta;
  const riskPenalty = player.risk === "high" ? 4 : player.risk === "med" ? 1.8 : 0;
  const scarcity = ["RB", "WR"].includes(player.pos) ? 1.8 : player.pos === "TE" ? 1.1 : 0.4;
  const vona = Math.max(-4, Math.min(9, nextDelta / 5)) * scarcity;
  const draftScore = (150 - (adp || 150)) + adjusted * 1.5 + marketDelta * 2 + vona - riskPenalty;
  const band = marketBandForPick(pickNumber, adp, teamCount);
  return { ...player, adjusted, drafted, marketDelta, draftScore, valueLabel: band.label, valueColor: band.color, valueTone: band.tone };
}

function buildOverallBaselines(players) {
  return Object.fromEntries(Object.entries(OVERALL_BASELINE_RANK).map(([position, rank]) => {
    const sorted = players
      .filter(p => p.pos === position)
      .sort((a, b) => ((b.adjusted || b.proj || 0) - (a.adjusted || a.proj || 0)));
    const baselinePlayer = sorted[Math.min(rank - 1, sorted.length - 1)];
    return [position, baselinePlayer ? (baselinePlayer.adjusted || baselinePlayer.proj || 0) * 17 : 0];
  }));
}

function deterministicNormalQuantile(index, total) {
  const p = (index + 0.5) / total;
  return Math.sqrt(2) * inverseErf(2 * p - 1);
}

function inverseErf(value) {
  const a = 0.147;
  const sign = value < 0 ? -1 : 1;
  const ln = Math.log(1 - value * value);
  const first = 2 / (Math.PI * a) + ln / 2;
  return sign * Math.sqrt(Math.sqrt(first * first - ln / a) - first);
}

function expectedSurplus(band, baseline, draws = 81) {
  if (!band) return 0;
  const mean = band.proj ?? 0;
  const sd = Math.max(1, ((band.ceiling ?? mean) - (band.floor ?? mean)) / 2.56);
  let sum = 0;
  for (let i = 0; i < draws; i += 1) {
    const sample = mean + sd * deterministicNormalQuantile(i, draws);
    sum += Math.max(0, sample - baseline);
  }
  return sum / draws;
}

function overallImpactScore(player, baselines, scoring = "ppr") {
  const weights = formatOverallWeights(scoring);
  const baseline = baselines[player.pos] || 0;
  const range = player.nine49Range || {
    floor: (player.floor || player.proj || 0) * 17,
    proj: (player.adjusted || player.proj || 0) * 17,
    ceiling: (player.ceiling || player.proj || 0) * 17,
  };
  const replacementEdge = expectedSurplus(range, baseline);
  const scarcity = weights.scarcity[player.pos] || 0.5;
  const valueLift = player.valueIndex ? Math.max(-10, Math.min(22, (player.valueIndex - 1) * 24)) : 0;
  const riskPenalty = player.risk === "high" ? 12 : player.risk === "med" ? 5 : 0;
  return replacementEdge * scarcity + valueLift - riskPenalty;
}

function applyOverallTheory(players, baselines, scoring) {
  const positionBuckets = {};
  players.forEach(player => {
    if (!positionBuckets[player.pos]) positionBuckets[player.pos] = [];
    positionBuckets[player.pos].push(player);
  });

  const positionRanks = {};
  Object.entries(positionBuckets).forEach(([position, list]) => {
    list.slice()
      .sort((a, b) => compareByFormatPoints(a, b, scoring))
      .forEach((player, index, sorted) => {
        const baselineTotal = baselines[position] || 0;
        const baselinePpg = baselineTotal ? baselineTotal / 17 : (sorted[Math.min(sorted.length - 1, OVERALL_BASELINE_RANK[position] || 1)]?.adjusted || 0);
        const projectedPpg = formatSeasonPoints(player, scoring) / 17;
        const nextPpg = sorted[index + 1] ? formatSeasonPoints(sorted[index + 1], scoring) / 17 : baselinePpg;
        const aboveBaselinePpg = Math.max(0, projectedPpg - baselinePpg);
        const localTierGap = Math.max(0, projectedPpg - nextPpg);
        const baselineRank = OVERALL_BASELINE_RANK[position] || 12;
        const rankScarcity = Math.max(0, (baselineRank - index - 1) / baselineRank);
        positionRanks[player.id] = {
          positionRank: index + 1,
          aboveBaselinePpg,
          localTierGap,
          rankScarcity,
        };
      });
  });

  return players.map(player => {
    const rankData = positionRanks[player.id] || {};
    const marketScore = 0;
    const weights = formatOverallWeights(scoring);
    const blueChipScore = (
      rankData.aboveBaselinePpg * 9 +
      rankData.localTierGap * 7 +
      rankData.rankScarcity * 28
    ) * (weights.blueChip[player.pos] || 0.5);
    const floorEdge = Math.max(0, (player.floor || player.proj || 0) - ((baselines[player.pos] || 0) / 17));
    const floorScore = floorEdge * (weights.floor[player.pos] || 0.28);
    const marketGuardrailPenalty =
      player.pos === "QB" && (rankData.positionRank || 99) > weights.qbGuardrailAfter ? weights.qbGuardrail :
      player.pos === "TE" && (rankData.positionRank || 99) > 3 ? 8 : 0;
    const overallTheoryScore =
      marketScore +
      (player.overallImpact || 0) * 1.08 +
      blueChipScore +
      floorScore -
      marketGuardrailPenalty;

    return {
      ...player,
      positionRank: rankData.positionRank || null,
      overallTheoryScore,
      overallTheory: {
        marketScore,
        blueChipScore,
        floorScore,
        positionRank: rankData.positionRank || null,
      },
    };
  });
}

function enforcePositionalMonotonicity(players) {
  const byPosition = {};
  players.forEach(player => {
    if (!byPosition[player.pos]) byPosition[player.pos] = [];
    byPosition[player.pos].push(player);
  });

  const scoredById = {};
  Object.values(byPosition).forEach(list => {
    let cap = Infinity;
    list.slice()
      .sort((a, b) => (a.positionRank || 999) - (b.positionRank || 999)
        || (b.overallTheoryScore || b.overallImpact || 0) - (a.overallTheoryScore || a.overallImpact || 0)
        || (a.adp || 999) - (b.adp || 999))
      .forEach(player => {
        const rawScore = player.overallTheoryScore || player.overallImpact || 0;
        cap = Math.min(cap, rawScore);
        scoredById[player.id] = cap - (player.positionRank || 999) * 0.000001;
      });
  });

  return players.map(player => ({
    ...player,
    boardScore: scoredById[player.id] ?? (player.overallTheoryScore || player.overallImpact || 0),
  }));
}

function sortOverallDraftBoard(players, scoring) {
  return enforcePositionalMonotonicity(players).sort((a, b) => {
    return (b.boardScore || -999) - (a.boardScore || -999)
      || (b.overallTheoryScore || b.overallImpact || 0) - (a.overallTheoryScore || a.overallImpact || 0)
      || b.overallImpact - a.overallImpact
      || projectionRankScore(b, scoring, "overall") - projectionRankScore(a, scoring, "overall")
      || (b.valueIndex || 0) - (a.valueIndex || 0)
      || a.adp - b.adp;
  });
}

function buildOverallDraftBoard(players, scoring, draftedByPlayerId = {}, pickNumber = 1, nextUserPick = null) {
  if (typeof window !== "undefined" && typeof window.buildOverallDraftBoardFromSettings === "function") {
    return window.buildOverallDraftBoardFromSettings(players, scoring, draftedByPlayerId, pickNumber, nextUserPick, {});
  }
  const scored = players.map(player => {
    const enriched = enrichDraftPlayer(player, scoring, draftedByPlayerId[player.id], pickNumber, nextUserPick);
    const valueIndex = scoringValueIndex(enriched, scoring);
    return { ...enriched, valueIndex: valueIndex || enriched.valueIndex };
  });
  const baselines = buildOverallBaselines(scored);
  const withImpact = scored.map(player => ({ ...player, overallImpact: overallImpactScore(player, baselines, scoring) }));
  const withTheory = applyOverallTheory(withImpact, baselines, scoring);
  return sortOverallDraftBoard(withTheory, scoring).map((player, index) => ({
    ...player,
    displayRank: index + 1,
    overallRank: index + 1,
  }));
}

function applyLiveBoardBands(players, pickNumber, teamCount = 12) {
  let availableRank = 0;
  return players.map(player => {
    if (player.drafted) return player;
    availableRank += 1;
    const gm = gmBandForPlayer(player, pickNumber, availableRank);
    const market = marketBandForPick(pickNumber, player.adp, teamCount);
    return {
      ...player,
      liveAvailableRank: availableRank,
      liveExpectedPick: player.adp,
      marketDelta: marketDeltaForPick(pickNumber, player.adp),
      gmBandScore: gm.score,
      gmBandLabel: gm.label,
      valueLabel: market.label,
      valueColor: market.color,
      valueTone: market.tone,
    };
  });
}

function applyDraftLiveState(scoredBoard, draftedByPlayerId = {}, pickNumber = 1, teamCount = 12) {
  let availableRank = 0;
  return (scoredBoard || []).map((player) => {
    const drafted = draftedByPlayerId[player.id] || null;
    if (drafted) {
      if (player.drafted === drafted) return player;
      return { ...player, drafted };
    }
    availableRank += 1;
    if (availableRank > 180) return player.drafted ? { ...player, drafted: null } : player;
    const gm = gmBandForPlayer(player, pickNumber, availableRank);
    const market = marketBandForPick(pickNumber, player.adp, teamCount);
    return {
      ...player,
      drafted: null,
      liveAvailableRank: availableRank,
      liveExpectedPick: player.adp,
      marketDelta: marketDeltaForPick(pickNumber, player.adp),
      gmBandScore: gm.score,
      gmBandLabel: gm.label,
      valueLabel: market.label,
      valueColor: market.color,
      valueTone: market.tone,
    };
  });
}

function gmBandForPlayer(player, pickNumber, availableRank = null) {
  const pickWindowDelta = pickNumber - player.adp;
  const boardDelta = availableRank == null ? 0 : player.adp - availableRank;
  const value = seasonValueGrade(player);
  const valueBoost = ({ "A+": 7, "A": 5, "B": 2.5, "C": 0, "D": -3, "F": -7 })[value.letter] || 0;
  const riskPenalty = player.risk === "high" ? 6 : player.risk === "med" ? 2.5 : 0;
  const lifecyclePenalty = (player.age && player.age >= 34 ? 4 : 0) + (player.id === "richardson" ? 5 : 0);
  const futureWindow = pickNumber <= 30 ? 12 : pickNumber <= 80 ? 24 : 46;
  const nearPickWindow = player.adp <= pickNumber + futureWindow;
  const boardOpportunity = nearPickWindow && !["K", "DST"].includes(player.pos) && !["D", "F"].includes(value.letter)
    ? Math.min(12, Math.max(0, boardDelta * 0.16)) : 0;
  const marketPressure = availableRank == null
    ? Math.max(-18, Math.min(24, pickWindowDelta))
    : Math.max(-8, Math.min(10, pickWindowDelta * 0.2));
  const availabilityScore = availableRank == null ? 0 : Math.max(-10, Math.min(16, (6 - availableRank) * 3));
  let score = marketPressure + availabilityScore + boardOpportunity + valueBoost - riskPenalty - lifecyclePenalty;
  if (player.valueIndex >= 1.35 && pickNumber >= 50 && !["K", "DST"].includes(player.pos)) {
    score = Math.max(score, 10);
  }
  if (player.valueIndex >= 1.8 && pickNumber >= 35 && !["K", "DST"].includes(player.pos)) {
    score = Math.max(score, 16);
  }
  if (pickNumber >= 60 && availableRank != null && !["K", "DST"].includes(player.pos) && !["F"].includes(value.letter)) {
    score = Math.max(score, 8 - availableRank * 2.8);
  } else if (pickNumber >= 40 && availableRank != null && !["K", "DST"].includes(player.pos) && !["F"].includes(value.letter)) {
    score = Math.max(score, 2 - availableRank * 2.4);
  }
  if (pickNumber <= 12) {
    score = pickWindowDelta >= 9 && value.letter === "A+" ? 8 : Math.min(score, 5);
  }
  const band = valueBand(score);
  return { ...band, score };
}

function buildRosters(board, teams, draftedByPlayerId) {
  const rosters = Object.fromEntries(teams.map(t => [t.id, []]));
  board.forEach(p => {
    const draft = draftedByPlayerId[p.id];
    if (draft && rosters[draft.teamId]) rosters[draft.teamId].push({ ...p, draft });
  });
  Object.values(rosters).forEach(list => list.sort((a, b) => a.draft.pick - b.draft.pick));
  return rosters;
}

function chooseComputerPick(available, roster, pickNumber, options = {}) {
  const counts = countPositions(roster);
  const scored = available
    .map(p => ({ ...p, computerScore: computerPickScore(p, counts, pickNumber, options) }))
    .filter(p => p.computerScore > -9000)
    .sort((a, b) => b.computerScore - a.computerScore);
  return scored[0] || available
    .filter(p => !["K", "DST"].includes(p.pos))
    .sort((a, b) => (a.adp || 999) - (b.adp || 999))[0] || available[0];
}

function computerPickScore(player, counts, pickNumber, options = {}) {
  const rosterSlots = options.rosterSlots || buildRosterSlots(DEFAULT_ROSTER_CONFIG);
  const need = rosterNeedBonus(player, counts, pickNumber, rosterSlots, options);
  const timing = cpuPositionTimingAdjustment(player, counts, pickNumber, rosterSlots, options);
  if (timing <= -9000) return timing;
  const boardRank = player.liveAvailableRank || player.overallRank || player.displayRank || player.adp || 240;
  const boardScore = Math.max(0, 260 - boardRank * 2.4);
  const theoryScore = Math.max(0, Math.min(160, (player.overallTheoryScore || 0) * 0.16));
  const marketPressure = Math.max(-10, Math.min(10, (pickNumber - player.adp) * 0.25));
  return boardScore + theoryScore + need * 4 + marketPressure + timing;
}

function slotCount(rosterSlots, slot) {
  return rosterSlots.filter(item => item === slot).length;
}

function draftRoundForPick(pickNumber, teamCount = 12) {
  return Math.max(1, Math.ceil(pickNumber / Math.max(1, teamCount)));
}

function rosterHasPositionSlot(pos, rosterSlots) {
  if (["K", "DST"].includes(pos)) return rosterSlots.includes(pos);
  if (pos === "QB") return rosterSlots.includes("QB") || rosterSlots.includes("SUPERFLEX") || rosterSlots.includes("BENCH");
  if (["RB", "WR", "TE"].includes(pos)) {
    return rosterSlots.includes(pos) || rosterSlots.includes("FLEX") || rosterSlots.includes("SUPERFLEX") || rosterSlots.includes("BENCH");
  }
  return rosterSlots.includes("BENCH");
}

function cpuPositionTimingAdjustment(player, counts, pickNumber, rosterSlots, options = {}) {
  const teamCount = options.teamCount || 12;
  const round = draftRoundForPick(pickNumber, teamCount);
  const totalRounds = Math.max(1, rosterSlots.length || 1);
  const finalTwoRounds = round >= Math.max(1, totalRounds - 1);

  if (!rosterHasPositionSlot(player.pos, rosterSlots)) return -9999;

  if (player.pos === "K" || player.pos === "DST") {
    const maxAtPosition = slotCount(rosterSlots, player.pos);
    if (!maxAtPosition || (counts[player.pos] || 0) >= maxAtPosition) return -9999;
    return finalTwoRounds ? -20 : -9999;
  }

  if (player.pos === "QB") {
    const qbSlots = Math.max(1, slotCount(rosterSlots, "QB") + slotCount(rosterSlots, "SUPERFLEX"));
    const qbCount = counts.QB || 0;
    if (qbCount >= qbSlots) return -9999;
    if (qbSlots > 1 && qbCount < qbSlots) return round <= 2 ? -15 : round <= 5 ? 8 : 14;
    if (round <= 2) return -90;
    if (round <= 4) return -55;
    if (round <= 6) return -22;
    return 0;
  }

  if (player.pos === "TE") {
    const teSlots = Math.max(1, slotCount(rosterSlots, "TE"));
    if ((counts.TE || 0) >= teSlots && round < totalRounds - 1) return -28;
  }

  return 0;
}

function rosterNeedBonus(player, counts, pickNumber, rosterSlots = buildRosterSlots(DEFAULT_ROSTER_CONFIG), options = {}) {
  const starters = {
    QB: Math.max(1, slotCount(rosterSlots, "QB") + slotCount(rosterSlots, "SUPERFLEX")),
    RB: Math.max(1, slotCount(rosterSlots, "RB")),
    WR: Math.max(1, slotCount(rosterSlots, "WR")),
    TE: Math.max(1, slotCount(rosterSlots, "TE")),
  };
  const round = draftRoundForPick(pickNumber, options.teamCount || 12);
  const count = counts[player.pos] || 0;
  if (player.pos === "QB" && count >= starters.QB) return -80;
  if (player.pos === "QB" && starters.QB === 1 && round <= 4) return -8;
  if (player.pos === "QB" && starters.QB > 1 && count < starters.QB) return round <= 2 ? 4 : 10;
  if (player.pos === "TE" && count >= Math.max(1, slotCount(rosterSlots, "TE")) && pickNumber < 80) return -10;
  if (starters[player.pos] && count < starters[player.pos]) return 8 - count * 2;
  if (["RB", "WR"].includes(player.pos) && count < 4) return 5;
  if (["RB", "WR"].includes(player.pos) && count < 6 && rosterSlots.includes("BENCH")) return 2;
  return 0;
}

function countPositions(roster) {
  return roster.reduce((acc, p) => { acc[p.pos] = (acc[p.pos] || 0) + 1; return acc; }, {});
}

function buildGmReadout(available, userRoster, pickNumber) {
  const counts = countPositions(userRoster);
  const needOrder = ["RB", "WR", "QB", "TE"].sort((a, b) => rosterNeedScore(b, counts, pickNumber) - rosterNeedScore(a, counts, pickNumber));
  const biggestNeed = needOrder[0];
  const gmRanked = available.slice().sort((a, b) => (b.gmBandScore || -999) - (a.gmBandScore || -999) || a.adp - b.adp);
  const best = gmRanked[0];
  const value = gmRanked.filter(p => p.marketDelta >= 4)[0] || best;
  const safe = gmRanked.filter(p => p.risk === "low")[0] || best;
  const upside = available.slice().sort((a, b) => (b.boom || 0) - (a.boom || 0))[0];
  const reach = available.find(p => p.marketDelta < -9);
  const needPick = gmRanked.find(p => p.pos === biggestNeed) || best;
  return { best, value, safe, upside, reach, needPick, biggestNeed, counts, ranked: gmRanked };
}

function buildBandGroups(available, pickNumber) {
  const labels = VALUE_BANDS.map(b => b.label);
  let availableRank = 0;
  const ranked = available.map(player => {
    availableRank += 1;
    return { ...player, gmBand: gmBandForPlayer(player, pickNumber, availableRank) };
  });
  return labels.map(label => ({
    label,
    players: ranked
      .filter(p => p.gmBand.label === label)
      .sort((a, b) => b.gmBand.score - a.gmBand.score || a.adp - b.adp)
      .slice(0, 3),
  }));
}

function rosterNeedScore(pos, counts, pickNumber, rosterSlots) {
  const targets = { QB: 1, RB: 3, WR: 3, TE: 1 };
  const have = counts[pos] || 0;
  const superflex = (rosterSlots || []).includes("SUPERFLEX")
    || ((rosterSlots || []).filter((slot) => slot === "QB").length >= 2);
  if (pos === "QB" && !superflex) {
    if (have >= 1) return -2;
    const pick = Number(pickNumber) || 0;
    if (pick && pick < 84) return 0;
    return 1;
  }
  return (targets[pos] || 1) - have;
}

function spendEfficiencyScore(roster) {
  const buckets = countDraftBuckets(roster);
  const total = Math.max(1, roster.length);
  const raw = 72 + (buckets.steals / total) * 22 + (buckets.expected / total) * 4 - (buckets.reaches / total) * 28;
  return { score: Math.max(30, Math.min(100, raw)), buckets };
}

function rosterConstructionScore(roster, rosterSlots = buildRosterSlots(DEFAULT_ROSTER_CONFIG)) {
  const lineup = buildScoredLineup(roster, rosterSlots);
  const starterSlots = rosterSlots.filter(slot => slot !== "BENCH");
  const filledStarters = lineup.filter(item => item.slot !== "BENCH").length;
  const expectedStarters = Math.min(starterSlots.length, roster.length);
  const fill = expectedStarters ? (filledStarters / expectedStarters) * 100 : 70;
  const counts = countPositions(roster);
  const qbSlots = starterSlots.filter(slot => slot === "QB" || slot === "SUPERFLEX").length || 1;
  let penalty = 0;
  if ((counts.QB || 0) >= qbSlots + 2) penalty += 12;
  if (roster.length >= 8 && qbSlots === 1 && !(counts.QB || 0)) penalty += 6;
  if (roster.length >= 6 && !(counts.RB || 0)) penalty += 8;
  if (roster.length >= 6 && !(counts.WR || 0)) penalty += 8;
  if (roster.length >= starterSlots.length && filledStarters < Math.ceil(starterSlots.length * 0.7)) penalty += 8;
  return Math.max(30, Math.min(100, fill - penalty));
}

function gradeUserDraft(roster, rosterSlots = buildRosterSlots(DEFAULT_ROSTER_CONFIG)) {
  if (!roster.length) return { letter: "—", color: "var(--slate)", note: "No picks yet", core: 0 };
  const lineup = buildScoredLineup(roster, rosterSlots);
  const starters = lineup.filter(item => item.slot !== "BENCH").map(item => item.player);
  const valueScore = roster.reduce((sum, player) => sum + seasonValueScore(player), 0) / roster.length;
  const spend = spendEfficiencyScore(roster);
  const construction = rosterConstructionScore(roster, rosterSlots);
  const score = Math.max(30, Math.min(100, valueScore * 0.5 + spend.score * 0.25 + construction * 0.25));
  const core = roster.reduce((s, p) => s + (p.adjusted || p.proj || 0), 0);
  const buckets = spend.buckets;
  const letter = draftScoreLetter(score);
  const note = `${roster.length} picks · value ${valueScore.toFixed(0)} · spend ${spend.score.toFixed(0)} · build ${construction.toFixed(0)} · ${buckets.steals} steals · ${buckets.reaches} reaches`;
  return { letter, color: gradeColor(letter), note, core, score, rawScore: valueScore, buckets, starters: starters.length, valueScore, spendScore: spend.score, constructionScore: construction };
}

function countDraftBuckets(roster) {
  return roster.reduce((acc, player) => {
    const label = player.draft?.boardBand || player.valueLabel || player.draft?.selectedBand || "";
    if (label.includes("Steal")) acc.steals += 1;
    else if (label === "Expected") acc.expected += 1;
    else acc.reaches += 1;
    return acc;
  }, { steals: 0, expected: 0, reaches: 0 });
}

function buildScoredLineup(roster, rosterSlots = buildRosterSlots(DEFAULT_ROSTER_CONFIG)) {
  const used = new Set();
  const slots = Array.isArray(rosterSlots) ? rosterSlots : buildRosterSlots(rosterSlots || DEFAULT_ROSTER_CONFIG);
  const lineup = slots.map(slot => {
    const player = roster.find(p => {
      if (used.has(p.id)) return false;
      if (slot === "FLEX") return ["RB", "WR", "TE"].includes(p.pos);
      if (slot === "SUPERFLEX") return ["QB", "RB", "WR", "TE"].includes(p.pos);
      if (slot === "BENCH") return true;
      return p.pos === slot;
    });
    if (player) used.add(player.id);
    return { slot, player };
  }).filter(item => item.player);
  roster.forEach(player => {
    if (!used.has(player.id)) lineup.push({ slot: "BENCH", player });
  });
  return lineup;
}

function bucketAdjustedDraftScore(players) {
  if (!players.length) return { score: 0, rawScore: 0, buckets: { steals: 0, expected: 0, reaches: 0 } };
  const rawScore = players.reduce((sum, p) => sum + (p.draft?.draftScore || draftStockGrade(p).score || 60), 0) / players.length;
  const buckets = countDraftBuckets(players);
  const score = Math.max(30, Math.min(100,
    rawScore + buckets.steals * 2.2 + buckets.expected * 0.3 - buckets.reaches * 6.5
  ));
  return { score, rawScore, buckets };
}

function cumulativeDraftStockScore(players) {
  if (!players.length) return { score: 0, buckets: { steals: 0, expected: 0, reaches: 0 } };
  const score = players.reduce((sum, p) => sum + (p.draft?.draftScore || draftStockGrade(p).score || 60), 0) / players.length;
  return { score, buckets: countDraftBuckets(players) };
}

function draftScoreLetter(score) {
  if (score >= 92) return "A+";
  if (score >= 86) return "A";
  if (score >= 76) return "B";
  if (score >= 64) return "C";
  if (score >= 52) return "D";
  return "F";
}

function gradeLineupDraft(starters) {
  if (!starters.length) return { letter: "—", color: "var(--slate)", note: "No starters drafted", score: 0 };
  const valueScore = starters.reduce((sum, player) => sum + seasonValueScore(player), 0) / starters.length;
  const spend = spendEfficiencyScore(starters);
  const score = Math.max(30, Math.min(100, valueScore * 0.65 + spend.score * 0.35));
  const letter = draftScoreLetter(score);
  const note = `${starters.length} starters · value ${valueScore.toFixed(0)} · spend ${spend.score.toFixed(0)}`;
  return { letter, color: gradeColor(letter), note, score, rawScore: valueScore, buckets: spend.buckets, valueScore, spendScore: spend.score };
}

function playerTier(player) {
  const letter = seasonValueGrade(player).letter;
  const number = ({ "A+": 1, "A": 2, "B": 3, "C": 4, "D": 5, "F": 6 })[letter] || 6;
  return { number, label: `Tier ${number}`, letter, color: seasonValueGrade(player).color };
}

function bestPositionScore(roster, pos) {
  return roster
    .filter(p => p.pos === pos)
    .reduce((best, p) => Math.max(best, p.draft?.draftScore || draftStockGrade(p).score || 0), 0);
}

function buildPickLog(board, teams, draftedByPlayerId) {
  return board
    .filter(p => draftedByPlayerId[p.id])
    .map(p => ({ player: p, draft: draftedByPlayerId[p.id], team: teams.find(t => t.id === draftedByPlayerId[p.id].teamId) }))
    .sort((a, b) => a.draft.pick - b.draft.pick);
}

function findNextUserPick(order, pickIndex, userSlot) {
  const idx = order.findIndex((team, i) => i > pickIndex && team === userSlot);
  return idx >= 0 ? idx + 1 : null;
}

function formatRoundPick(pickIndex, teamCount) {
  const round = Math.floor(pickIndex / teamCount) + 1;
  const pick = (pickIndex % teamCount) + 1;
  return `${round}.${String(pick).padStart(2, "0")}`;
}

function formatAdpPick(adp, teamCount = 12) {
  const safeAdp = Math.max(1, Math.round(adp || 1));
  const round = Math.floor((safeAdp - 1) / teamCount) + 1;
  const pick = ((safeAdp - 1) % teamCount) + 1;
  return `${round}.${String(pick).padStart(2, "0")}`;
}

function formatAdpTrend(change) {
  if (!Number.isFinite(change)) return null;
  const rounded = Math.round(change * 10) / 10;
  const label = `${rounded > 0 ? "+" : ""}${rounded.toFixed(1)}`;
  return {
    label,
    color: rounded > 0 ? "var(--mint)" : rounded < 0 ? "var(--red)" : "var(--slate-dim)",
  };
}

function formatClock(seconds) {
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${mins}:${String(secs).padStart(2, "0")}`;
}

function buildGmAnswer(question, gm, roster, extra = {}) {
  if (typeof window.answerGmQuestion === "function") {
    return window.answerGmQuestion(question, gm, roster, extra);
  }
  return gm.best
    ? `GM leans ${gm.best.name}.`
    : "GM is waiting for available players.";
}

function gmRequestedPosition(q) {
  if (/\brb\b|\brb1\b|\brb2\b|running back/.test(q)) return "RB";
  if (/\bwr\b|\bwr1\b|\bwr2\b|wide receiver/.test(q)) return "WR";
  if (/\bqb\b|\bqb1\b|quarterback/.test(q)) return "QB";
  if (/\bte\b|\bte1\b|tight end/.test(q)) return "TE";
  if (/\bdst\b|defense/.test(q)) return "DST";
  if (/\bk\b|kicker/.test(q)) return "K";
  return null;
}

function gmMatchedPlayers(q, players) {
  return players.filter(p => {
    const full = p.name.toLowerCase();
    const last = full.split(/\s+/).slice(-1)[0];
    return q.includes(full) || (last.length > 4 && q.includes(last));
  });
}

function gmRecommendPlayer(player, label) {
  const value = seasonValueGrade(player).letter;
  const role = roleProjectionLabel(player);
  const stock = Math.round(seasonValueScore(player));
  const schedule = scheduleEaseRank(player);
  const scheduleText = Number.isFinite(schedule) ? ` · SOS ${schedule}/32` : "";
  const decisionText = player.decision
    ? ` · ${Math.round((player.decision.survives || 0) * 100)}% survives to your next pick · wait cost ${Math.round(player.decision.dropoffIfGone || 0)}`
    : "";
  return `${label}: ${player.name}. ${player.pos} · ${role} · ${player.valueLabel || "Expected"} · 949 Value ${value} (${stock})${scheduleText}${decisionText}. This is a positional-edge answer, not just raw projected points.`;
}

function gmComparePlayers(players, requestedPos) {
  const scoped = requestedPos ? players.filter(p => p.pos === requestedPos) : players;
  const candidates = scoped.length >= 2 ? scoped : players;
  const ranked = candidates.slice().sort((a, b) => {
    const aScore = (a.gmBandScore || 0) + seasonValueScore(a) * 0.35 + (a.adjusted || 0);
    const bScore = (b.gmBandScore || 0) + seasonValueScore(b) * 0.35 + (b.adjusted || 0);
    return bScore - aScore;
  });
  const winner = ranked[0];
  const other = ranked[1];
  if (!winner || !other) return "Neutral read: I found one named player, but not enough comparable candidates on the live board.";
  return `${winner.name} over ${other.name} by current GM context. ${winner.name} has the better mix of ${winner.valueLabel || "market price"}, ${roleProjectionLabel(winner)}, and 949 Value ${seasonValueGrade(winner).letter}.`;
}

function positionColor(pos) {
  return ({ QB: "#FFE45C", RB: "#57FF8A", WR: "#42D9FF", TE: "#D58BFF", DST: "#FF5E5E", K: "#FFAE45" })[pos] || "var(--mint)";
}

function positionBg(pos) {
  return `${positionColor(pos)}2B`;
}

function valueToneForChip(label) {
  if (label.includes("Steal")) return "var(--mint)";
  if (label === "Expected") return "var(--mint-soft)";
  if (label.includes("Low Reach")) return "var(--gold)";
  return "var(--red)";
}

function remainingPositionState(player, availablePlayers = []) {
  const peers = (availablePlayers || [])
    .filter(item => !item.drafted && item.pos === player.pos)
    .sort((a, b) => (a.overallRank || a.displayRank || 999) - (b.overallRank || b.displayRank || 999)
      || (b.adjusted || b.proj || 0) - (a.adjusted || a.proj || 0));
  const rank = Math.max(1, peers.findIndex(item => item.id === player.id) + 1 || peers.length || 1);
  const total = Math.max(1, peers.length || 1);
  return { rank, total, quality: (total - rank + 1) / total, peers };
}

function remainingBoardLetter(rank, total) {
  const quality = (total - rank + 1) / Math.max(1, total);
  if (rank === 1 && total <= 3) return "A+";
  if (quality >= 0.85 || rank === 1) return "A";
  if (quality >= 0.48 || (total <= 4 && rank <= 3)) return "B";
  if (quality >= 0.32) return "C";
  if (quality >= 0.12) return "D";
  return "F";
}

function bumpDraftLetter(letter, steps) {
  const order = ["F", "D", "C", "B", "A", "A+"];
  const index = Math.max(0, Math.min(order.length - 1, order.indexOf(letter === "B+" || letter === "B-" ? "B" : letter) + steps));
  return order[index];
}

function liveDraftGrade(player, options = {}) {
  const { pickNumber = 1, teamCount = 12 } = options;
  const grade = draftStockGradeForPick(player, pickNumber, teamCount);
  const market = marketBandForPick(pickNumber, player.adp, teamCount);
  const value = seasonValueGrade(player);
  return {
    ...grade,
    marketLabel: market.label,
    valueLetter: value.letter,
    note: `Pick grade at this slot vs ADP (${market.label}). 949 Value ${value.letter} is player quality, not this pick.`,
  };
}

function draftStockGrade(player) {
  const pickNumber = Number(player.draft?.pick || player.draft?.pickNumber || 0);
  const teamCount = Number(player.draft?.teamCount) || 12;
  if (pickNumber > 0) return draftStockGradeForPick(player, pickNumber, teamCount);
  return draftStockGradeForPick(player, player.adp || 1, teamCount);
}

function draftStockGradeForPick(player, pickNumber, teamCount = 12) {
  const expectedPick = player.adp || player.liveExpectedPick || pickNumber;
  const adpDelta = pickNumber - expectedPick;
  const roundDelta = adpDelta / Math.max(1, teamCount);
  const value = seasonValueScore(player);
  const model = cfModelScore(player);

  let score = roundDelta >= 1.75 ? 96
    : roundDelta >= 0.75 ? 88
    : roundDelta >= 0.25 ? 82
    : roundDelta >= -0.25 ? 78
    : roundDelta >= -0.75 ? 72
    : roundDelta >= -1.5 ? 66
    : 56;

  if (pickNumber <= teamCount && roundDelta >= -0.25 && roundDelta < 0.25 && player.nine49Rank) {
    score = player.nine49Rank <= 3 ? 90
      : player.nine49Rank <= 8 ? 86
      : player.nine49Rank <= 14 ? 82
      : 78;
  }

  const modelNudge = model >= 88 ? 2
    : model >= 78 ? 2
    : model <= 55 ? -3
    : 0;
  const valueNudge = value >= 88 ? 2
    : value >= 78 ? 1
    : value <= 55 ? -2
    : 0;
  const riskNudge = player.risk === "high" ? -2 : 0;
  score = Math.max(48, Math.min(99, score + modelNudge + valueNudge + riskNudge));
  if (pickNumber <= teamCount && roundDelta >= -0.25 && roundDelta < 0.25 && player.nine49Rank > 14) {
    score = Math.min(score, 80);
  }

  const letter = scoreToLetter(score, value, adpDelta);
  return { letter, color: gradeColor(letter), score, roundDelta };
}

function scoreToLetter(score, valueScore, adpDelta) {
  if (score >= 94) return "A+";
  if (score >= 86) return "A";
  if (score >= 81) return "B+";
  if (score >= 76) return "B";
  if (score >= 70) return "B-";
  if (score >= 64) return "C";
  if (score >= 52) return "D";
  if (valueScore < 55 && adpDelta < -75) return "F";
  return "D";
}

function gradeColor(letter) {
  if (letter === "A+" || letter === "A") return "var(--mint)";
  if (letter === "B+" || letter === "B" || letter === "B-") return "var(--mint-soft)";
  if (letter === "C" || letter === "D") return "var(--gold)";
  return "var(--red)";
}

function seasonValueScore(player) {
  const consistencyLift = Math.max(-4, Math.min(5, ((consistencyScore(player) || 60) - 60) * 0.08));
  if (player.valueIndex && player.valueIndex > 0) {
    const thresholds = POSITION_VALUE_THRESHOLDS[player.pos] || POSITION_VALUE_THRESHOLDS.RB;
    const value = player.valueIndex;
    let score = 0;
    if (value >= thresholds["A+"]) score = Math.max(94, Math.min(100, 94 + (value - thresholds["A+"]) * 12));
    else if (value >= thresholds.A) score = 86 + ((value - thresholds.A) / (thresholds["A+"] - thresholds.A)) * 8;
    else if (value >= thresholds.B) score = 76 + ((value - thresholds.B) / (thresholds.A - thresholds.B)) * 10;
    else if (value >= thresholds.C) score = 64 + ((value - thresholds.C) / (thresholds.B - thresholds.C)) * 12;
    else if (value >= thresholds.D) score = 52 + ((value - thresholds.D) / (thresholds.C - thresholds.D)) * 12;
    else score = Math.max(35, 42 + (value / thresholds.D) * 10);
    return Math.max(35, Math.min(100, score + consistencyLift));
  }
  const grade = seasonValueGrade(player).letter;
  const gradeBase = ({ "A+": 96, "A": 88, "B": 78, "C": 67, "D": 56, "F": 42 })[grade] || 60;
  const projectionBonus = Math.max(-5, Math.min(8, ((player.adjusted || player.proj || 0) - 10) * 0.6));
  return Math.max(35, Math.min(100, gradeBase + projectionBonus + consistencyLift));
}

function cfModelScore(player) {
  const adjusted = player.adjusted || player.proj || 0;
  const boom = player.boom || 25;
  const bust = player.bust || 30;
  const positionBaseline = ({ QB: 17, RB: 10.5, WR: 10.5, TE: 7.5, DST: 6.5, K: 7 })[player.pos] || 9;
  const score = 62 + (adjusted - positionBaseline) * 4 + (boom - bust) * 0.25;
  return Math.max(35, Math.min(100, score));
}

function roleProjectionLabel(player) {
  const adjusted = player.adjusted || player.proj || 0;
  if (player.pos === "RB") {
    if (adjusted >= 16.5) return "RB1";
    if (adjusted >= 12.2) return "RB2";
    if (adjusted >= 8.8) return "FLEX";
    return "BENCH";
  }
  if (player.pos === "WR") {
    if (adjusted >= 16) return "WR1";
    if (adjusted >= 12.5) return "WR2";
    if (adjusted >= 9) return "FLEX";
    return "BENCH";
  }
  if (player.pos === "TE") {
    if (adjusted >= 11) return "TE1";
    if (adjusted >= 8) return "START";
    return "BENCH";
  }
  if (player.pos === "QB") {
    if (adjusted >= 18) return "QB1";
    if (adjusted >= 15.5) return "STREAM";
    return "BENCH";
  }
  if (player.pos === "DST" || player.pos === "K") {
    return adjusted >= 7 ? "START" : "STREAM";
  }
  return "BENCH";
}

function sosScore(player) {
  const teamRank = TEAM_SOS_RANKS[player.team];
  if (!teamRank) return null;
  return Math.max(35, Math.min(100, 35 + ((teamRank.rank - 1) / 31) * 65));
}

function scheduleEaseRank(player) {
  const team = player?.team;
  const live = (window.NFL_SOS || []).find((row) => row.team === team);
  if (live && Number(live.sosRank) > 0) return Number(live.sosRank);
  const teamRank = TEAM_SOS_RANKS[team];
  if (teamRank) return teamRank.rank;
  const canonical = player.profile?.projection?.sosRank;
  if (typeof canonical === "number" && canonical > 0) return canonical;
  return null;
}

function scheduleStrengthNote(player) {
  const canonical = player.profile?.projection?.sosNote;
  if (window.PROJECTIONS_OK && canonical) return canonical;
  const teamRank = TEAM_SOS_RANKS[player.team];
  if (!teamRank) return "SOS pending provider opponent data";
  return `Opp win% ${teamRank.winPct.toFixed(3)}`;
}

function seasonValueGrade(player) {
  if (player.valueLetter) {
    return { letter: player.valueLetter, color: gradeColor(player.valueLetter) };
  }
  if (player.valueIndex && player.valueIndex > 0) {
    const letter = letterFromValueIndex(player.valueIndex, player.pos);
    return { letter, color: gradeColor(letter) };
  }
  if (player.pos === "DST" || player.pos === "K") {
    const adjusted = player.adjusted || player.proj || 0;
    if (adjusted >= 8) return { letter: "B", color: "var(--mint-soft)" };
    if (adjusted >= 6.5) return { letter: "C", color: "var(--gold)" };
    return { letter: "D", color: "var(--gold)" };
  }
  const adjusted = player.adjusted || player.proj || 0;
  const thresholds = {
    QB: { plus: 22, a: 19, b: 17, c: 15 },
    RB: { plus: 18, a: 14, b: 10.5, c: 8 },
    WR: { plus: 17, a: 13.5, b: 10, c: 8 },
    TE: { plus: 12, a: 9.5, b: 7.5, c: 6 },
  }[player.pos] || { plus: 16, a: 12, b: 9, c: 7 };
  if (adjusted >= thresholds.plus) return { letter: "A+", color: "var(--mint)" };
  if (adjusted >= thresholds.a) return { letter: "A", color: "var(--mint)" };
  if (adjusted >= thresholds.b) return { letter: "B", color: "var(--mint-soft)" };
  if (adjusted >= thresholds.c) return { letter: "C", color: "var(--gold)" };
  return { letter: player.risk === "high" ? "F" : "D", color: player.risk === "high" ? "var(--red)" : "var(--gold)" };
}

Object.assign(window, {
  DRAFT_SUPPLEMENTAL_RANKINGS, DEFAULT_ROSTER_CONFIG, SUPERFLEX_ROSTER_CONFIG, TWO_QB_ROSTER_CONFIG, DRAFT_TEAM_NAMES, VALUE_BANDS, POSITION_VALUE_THRESHOLDS, VALUE_A_PLUS_MIN, VALUE_A_PLUS_MAX,
  RANKING_LEVELS, BAND_CONSTANTS,
  NINE49_2026_MODEL, getNine49Model,
  productionAnchor, transitionRatiosFromHistory, shrunkRatioStats, rookieTier, rookieBand, healthyBand, injuryProfile, expectedSurplus,
  isRookieProfile, isAdpBoardRookie, overallPickFromAdp, placeRookiesByAdp,
  isExcludedFrom2026Draft,
  blendEspnObservedAdp,
  KICKER_OVERALL_FLOOR, placeAfterOverallFloor,
  buildDraftPool, buildDraftOrder, buildRosterSlots, assignRosterSlots,
  SCORING_RECEPTION_POINTS, POSITION_USAGE_DEFAULTS,
  projectionUsageProfile, scoringProjection, scoringProjectionLine, scoringValueIndex, formatSeasonPoints, compareByFormatPoints,
  applyFormatValueGrades, pprSeasonTotal, projectedReceptionsPerGame, receptionPointValue, formatOverallWeights,
  positionVolatilitySd, POSITION_VOLATILITY_SD_BY_FORMAT,
  nine49ScoringRankScore, projectionRankScore, futureProjectionScore,
  buildOverallBaselines, overallImpactScore, applyOverallTheory, enforcePositionalMonotonicity, sortOverallDraftBoard, buildOverallDraftBoard,
  consistencyBreakdown, availabilityBreakdown, consistencyScore, consistencyGrade,
  valueBand, enrichDraftPlayer, applyLiveBoardBands, applyDraftLiveState, gmBandForPlayer,
  buildRosters, chooseComputerPick, rosterNeedBonus, countPositions,
  buildGmReadout, buildBandGroups, rosterNeedScore, gradeUserDraft,
  countDraftBuckets, buildScoredLineup, gradeLineupDraft, playerTier, bestPositionScore, buildPickLog,
  findNextUserPick, formatRoundPick, formatAdpPick, formatAdpTrend, marketDeltaForPick, marketBandForPick, formatClock, buildGmAnswer,
  positionColor, positionBg, valueToneForChip,
  liveDraftGrade, remainingPositionState, draftStockGrade, draftStockGradeForPick, scoreToLetter, gradeColor,
  seasonValueScore, cfModelScore, roleProjectionLabel, sosScore, scheduleEaseRank,
  scheduleStrengthNote, seasonValueGrade
});
