// ─── Builder Module (migrated from builder.html) ────────

// ═══ TEMPLATES ═══
var TIER_15='Q1-Q4: 1 PT | Q5 BONUS: 3 PT | Q6-Q9: 1 PT | Q10 BONUS: 5 PT | Q11-Q14: 1 PT | Q15 BONUS: 10 PT';
var TIER_10='Q1-Q3: 1 PT | Q4 BONUS: 3 PT | Q5-Q6: 1 PT | Q7 BONUS: 5 PT | Q8-Q9: 1 PT | Q10 BONUS: 10 PT';
var TIER_8='Q1-Q2: 1 PT | Q3 BONUS: 3 PT | Q4-Q5: 1 PT | Q6 BONUS: 5 PT | Q7: 1 PT | Q8 BONUS: 10 PT';
var BONUS_15={5:3,10:5,15:10};
var BONUS_10={4:3,7:5,10:10};
var BONUS_8={3:3,6:5,8:10};

// FORMAT_RULES loaded from brand data (canon source of truth).
// Falls back to inline defaults if brand data hasn't loaded.
var FORMAT_RULES = (window.BrandData && window.BrandData.shows && window.BrandData.shows.format_rules) || {
  house_rules:{
    tier_copy:{source_q:"WE SHOW THE LINE. YOU WRITE WHERE IT'S FROM.",acronym_q:'WE SHOW THE ACRONYM. YOU DECODE THE LINE.',hotseat_prompt:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.'},
    score_closing:'SWAP SCORECARDS. MARK THEM.',score_closing_final:'FINAL SCORES. SWAP AND TALLY.',
    fluency_sub:'WE SHOW THE LINE. YOU SHOUT THE SOURCE. NO WRITING. NO SCORING.',
    round_sub:{source_q:'GUESS THE SOURCE',acronym_q:'GUESS THE LINECONIC: {MEDIA}',hotseat_prompt:'THE HOT SEAT'},
    fishbowl:{primary:'WRITE YOUR RED FLAG.',secondary:'DROP IT IN THE BOWL. WE WILL USE IT AGAINST YOU.'},
    intermission:{primary:'INTERMISSION.',secondary:'10 MINUTES. GET A DRINK.'},
    hotseat_rules:{primary:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.',secondary:'BACK TO SCREEN. YOUR HALF SCREAMS CLUES.|OTHER HALF STAYS QUIET.|30 SECONDS PER ROUND.'},
    sentence:{primary:'THE SENTENCE.',secondary:"WINNING TABLE. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:"YOU'VE BEEN LINECONIC'D."},
    last_line:{primary:'NOBODY PUTS BABY IN A CORNER.',secondary:''}
  },
  standard:{
    tier_copy:{source_q:'ACTIVE SIDE ANSWERS COLLECTIVELY. SILENT SIDE SAYS NOTHING.',acronym_q:'ACTIVE SIDE ANSWERS COLLECTIVELY. SILENT SIDE SAYS NOTHING.',hotseat_prompt:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.'},
    score_closing:'',score_closing_final:'FINAL SCORES.',
    fluency_sub:'ROOM REUNITES. WE SHOW THE LINE. YOU SHOUT THE SOURCE. NO SCORING.',
    round_sub:{source_q:'GUESS THE SOURCE',acronym_q:'GUESS THE LINECONIC: {MEDIA}',hotseat_prompt:'THE HOT SEAT'},
    fishbowl:{primary:'WRITE YOUR RED FLAG.',secondary:'DROP IT IN THE BOWL. WE WILL USE IT AGAINST YOU.'},
    intermission:{primary:'INTERMISSION.',secondary:'10 MINUTES. GET A DRINK.'},
    hotseat_rules:{primary:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.',secondary:'BACK TO SCREEN. YOUR HALF SCREAMS CLUES.|OTHER HALF STAYS QUIET.|30 SECONDS PER ROUND.'},
    sentence:{primary:'THE SENTENCE.',secondary:"WINNING SIDE. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:"YOU'VE BEEN LINECONIC'D."},
    last_line:{primary:'NOBODY PUTS BABY IN A CORNER.',secondary:''}
  },
  scorecard:{
    tier_copy:{source_q:"WE SHOW THE LINE. YOU WRITE WHERE IT'S FROM.",acronym_q:'WE SHOW THE ACRONYM. YOU DECODE THE LINE.',hotseat_prompt:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.'},
    score_closing:'SWAP SCORECARDS. MARK THEM.',score_closing_final:'FINAL SCORES. SWAP AND TALLY.',
    fluency_sub:'WE SHOW THE LINE. YOU SHOUT THE SOURCE. NO WRITING. NO SCORING.',
    round_sub:{source_q:'GUESS THE SOURCE',acronym_q:'GUESS THE LINECONIC: {MEDIA}',hotseat_prompt:'THE HOT SEAT'},
    fishbowl:{primary:'WRITE YOUR RED FLAG.',secondary:'DROP IT IN THE BOWL. WE WILL USE IT AGAINST YOU.'},
    intermission:{primary:'INTERMISSION.',secondary:'10 MINUTES. GET A DRINK.'},
    hotseat_rules:{primary:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.',secondary:'BACK TO SCREEN. YOUR HALF SCREAMS CLUES.|OTHER HALF STAYS QUIET.|30 SECONDS PER ROUND.'},
    sentence:{primary:'THE SENTENCE.',secondary:"WINNING TABLE. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:'STOP BEING UNCULTURED.'}
  },
  tribunal:{
    tier_copy:{acronym_q:"WE SHOW THE ACRONYM. BOTH CLAIM TO KNOW. WHO'S TELLING THE TRUTH?",hotseat_prompt:'ROOM SPLITS. WINNING TABLE SENDS A CHAMPION.'},
    score_closing:'',score_closing_final:'FINAL TALLY.',
    round_sub:{acronym_q:'THE TRIAL',hotseat_prompt:'THE HOT SEAT'},
    fishbowl:{primary:'WRITE YOUR ALIBI.',secondary:"IF YOU'RE CAUGHT LYING, YOU READ THE SENTENCE."},
    sentence:{primary:'THE SENTENCE.',secondary:"THE ACQUITTED. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:'COURT ADJOURNED.'}
  },
  faceoff:{
    tier_copy:{hotseat_prompt:'8 PLAYERS. 3 ROUNDS. 1 SURVIVOR.'},
    score_closing:'',
    round_sub:{hotseat_prompt:'THE FACE-OFF'},
    sentence:{primary:'THE SENTENCE.',secondary:"CHAMPION. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:'K.O.'}
  },
  draft:{
    tier_copy:{hotseat_prompt:'SURVIVE THE HOT SEAT. EARN YOUR PLACE.',acronym_q:'DECODE THE ACRONYM. 10 SECONDS. NO MERCY.'},
    score_closing:'',
    round_sub:{hotseat_prompt:'THE TRYOUTS',acronym_q:'THE GAUNTLET'},
    fishbowl:{primary:'WRITE YOUR APPLICATION.',secondary:'WHY SHOULD YOU BE DRAFTED?'},
    sentence:{primary:'THE SENTENCE.',secondary:"FINAL CUT. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:'DRAFT CLOSED.'}
  },
  split:{
    tier_copy:{acronym_q:'YOUR CAPTAIN CALLS YOU UP. BACK TO SCREEN. YOUR SIDE SCREAMS CLUES.',hotseat_prompt:'BACK TO SCREEN. YOUR SIDE SCREAMS CLUES. OPPOSING SIDE SABOTAGES.'},
    score_closing:'',
    round_sub:{acronym_q:'THE GRINDER',hotseat_prompt:'THE TAROT'},
    fishbowl:{primary:'WRITE YOUR RED FLAG.',secondary:'DROP IT IN THE BOWL. YOUR CAPTAIN WILL USE IT AGAINST THE OTHER SIDE.'},
    sentence:{primary:'THE SENTENCE.',secondary:"WINNING SIDE. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:"YOU'VE BEEN SPLIT."}
  },
  purge:{
    score_closing:'',
    round_sub:{fluency_pair:'THE PURGE'},
    sentence:{primary:'THE SENTENCE.',secondary:"WINNING SIDE. IN UNISON.|'YOU CAN'T SIT WITH US': MEAN GIRLS."},
    endcard:{primary:'L',secondary:'STOP BEING UNCULTURED.'}
  }
};
var TIER_MAP={15:TIER_15,10:TIER_10,8:TIER_8};
var BONUS_MAP={15:BONUS_15,10:BONUS_10,8:BONUS_8};
var MEDIA_QUALIFIER={'movie,tv':'CINEMA & TV ONLY.','music':'MUSIC ONLY.'};
var MEDIA_LABEL={'movie,tv':'SCREEN','music':'LYRICS'};

function buildTierString(count){
  if(TIER_MAP[count])return TIER_MAP[count];
  var b1=Math.ceil(count/3),b2=Math.ceil(count*2/3),b3=count;
  var parts=[],tiers=[[1,b1,3],[b1+1,b2,5],[b2+1,b3,10]];
  tiers.forEach(function(t){var start=t[0],bonus=t[1],val=t[2];if(bonus>start){var re=bonus-1;parts.push('Q'+start+(re>start?'-Q'+re:'')+': 1 PT')}parts.push('Q'+bonus+' BONUS: '+val+' PT')});
  return parts.join(' | ');
}

function resolveCopy(slide,ctx){
  if(!ctx.format||!FORMAT_RULES[ctx.format])return slide.content?{...slide.content}:{primary:'',secondary:''};
  var rules=FORMAT_RULES[ctx.format];
  if(slide.type==='tier_title'){
    var base=rules.tier_copy&&rules.tier_copy[ctx.mode]||'';var parts=[base];
    if(ctx.mediaFilter&&ctx.mediaFilter.length>0){var key=ctx.mediaFilter.slice().sort().join(',');if(MEDIA_QUALIFIER[key])parts.push(MEDIA_QUALIFIER[key])}
    if(slide.hardest)parts.push('HARDEST REFERENCES.');
    return{primary:parts.join(' '),secondary:buildTierString(ctx.slotCount)};
  }
  if(slide.type==='score'){
    var label=ctx.closingLabel||(ctx.secName?ctx.secName.split(':')[0].trim()+' COMPLETE':'COMPLETE');
    var sec=ctx.isLastScoredRound?(rules.score_closing_final||'FINAL SCORES.'):(rules.score_closing||'');
    return{primary:label,secondary:sec};
  }
  if(slide.type==='round_title'){
    if(ctx.mode==='fluency_pair'){
      return{primary:(slide.content&&slide.content.primary)||'FLUENCY CHECK',secondary:rules.fluency_sub||''};
    }
    // Scored rounds: preserve creative primary, resolve secondary from round_sub
    var pri=(slide.content&&slide.content.primary)||'';
    if(rules.round_sub&&rules.round_sub[ctx.mode]){
      var sub=rules.round_sub[ctx.mode];
      if(sub.indexOf('{MEDIA}')>=0){
        var mKey=ctx.mediaFilter?ctx.mediaFilter.slice().sort().join(','):'';
        sub=sub.replace('{MEDIA}',MEDIA_LABEL[mKey]||'EVERYTHING');
      }
      return{primary:pri,secondary:sub};
    }
    return{primary:pri,secondary:(slide.content&&slide.content.secondary)||''};
  }
  if(slide.type==='fishbowl'&&rules.fishbowl){
    return{primary:rules.fishbowl.primary,secondary:rules.fishbowl.secondary};
  }
  if(slide.type==='intermission'&&rules.intermission){
    return{primary:rules.intermission.primary,secondary:rules.intermission.secondary};
  }
  if(slide.type==='hotseat_rules'&&rules.hotseat_rules){
    return{primary:rules.hotseat_rules.primary,secondary:rules.hotseat_rules.secondary};
  }
  if(slide.type==='sentence'&&rules.sentence){
    return{primary:rules.sentence.primary,secondary:rules.sentence.secondary};
  }
  if(slide.type==='endcard'&&rules.endcard){
    return{primary:rules.endcard.primary,secondary:rules.endcard.secondary};
  }
  if(slide.type==='last_line'&&rules.last_line){
    return{primary:rules.last_line.primary,secondary:rules.last_line.secondary};
  }
  return slide.content?{...slide.content}:{primary:'',secondary:''};
}

// Legacy alias map — old Firebase shows saved with 'the-stripped-*' templateIds still load correctly.
var TEMPLATE_ALIASES={'the-stripped-90':'the-scorecard-90','the-stripped-60':'the-scorecard-60','the-stripped-30':'the-scorecard-30','the-standard-90':'the-alternate-90'};

var TEMPLATES={
  'house-rules-90':{id:'house-rules-90',name:'HOUSE RULES',duration:'90 min',format:'house_rules',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:15,timer:30,bonusAt:BONUS_15},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-2',name:'ROUND 2: SCREEN',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15,mediaFilter:['movie','tv']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'fluency',name:'FLUENCY CHECK',fixed:[{type:'round_title',content:{primary:'FLUENCY CHECK'},copy:'auto'}],slots:{mode:'fluency_pair',count:10,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'intermission',name:'INTERMISSION',fixed:[{type:'intermission',copy:'auto'},{type:'warning',content:{primary:'60 SECONDS.',secondary:''}},{type:'blackout',content:{primary:'',secondary:''}}]},
    {id:'doa',name:'DEAD OR ALIVE',fixed:[{type:'round_title',content:{primary:'DEAD OR ALIVE',secondary:'CULTURALLY RELEVANT OR CULTURALLY EXPIRED. YOU DECIDE.'}}],slots:{mode:'doa_triplet',count:10,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'round-3',name:'ROUND 3: LYRICS',fixed:[{type:'round_title',content:{primary:'ROUND 3'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15,mediaFilter:['music']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-4',name:'ROUND 4: EVERYTHING',fixed:[{type:'round_title',content:{primary:'ROUND 4'},copy:'auto'},{type:'tier_title',copy:'auto',hardest:true}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'last_line',copy:'auto'},{type:'endcard',copy:'auto'}]}
  ]},
  'house-rules-60':{id:'house-rules-60',name:'HOUSE RULES',duration:'60 min',format:'house_rules',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'doa',name:'DEAD OR ALIVE',fixed:[{type:'round_title',content:{primary:'DEAD OR ALIVE',secondary:'CULTURALLY RELEVANT OR CULTURALLY EXPIRED. YOU DECIDE.'}}],slots:{mode:'doa_triplet',count:8,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'round-2',name:'ROUND 2: SCREEN',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10,mediaFilter:['movie','tv']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-3',name:'ROUND 3: EVERYTHING',fixed:[{type:'round_title',content:{primary:'ROUND 3'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'last_line',copy:'auto'},{type:'endcard',copy:'auto'}]}
  ]},
  'the-alternate-90':{id:'the-alternate-90',name:'THE ALTERNATE',duration:'90 min',format:'standard',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:15,timer:30,bonusAt:BONUS_15},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-2',name:'ROUND 2: SCREEN',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15,mediaFilter:['movie','tv']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'fluency',name:'FLUENCY CHECK',fixed:[{type:'round_title',content:{primary:'FLUENCY CHECK'},copy:'auto'}],slots:{mode:'fluency_pair',count:10,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'intermission',name:'INTERMISSION',fixed:[{type:'intermission',copy:'auto'},{type:'warning',content:{primary:'60 SECONDS.',secondary:''}},{type:'blackout',content:{primary:'',secondary:''}}]},
    {id:'doa',name:'DEAD OR ALIVE',fixed:[{type:'round_title',content:{primary:'DEAD OR ALIVE',secondary:'CULTURALLY RELEVANT OR CULTURALLY EXPIRED. YOU DECIDE.'}}],slots:{mode:'doa_triplet',count:10,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'round-3',name:'ROUND 3: LYRICS',fixed:[{type:'round_title',content:{primary:'ROUND 3'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15,mediaFilter:['music']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-4',name:'ROUND 4: EVERYTHING',fixed:[{type:'round_title',content:{primary:'ROUND 4'},copy:'auto'},{type:'tier_title',copy:'auto',hardest:true}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'last_line',copy:'auto'},{type:'endcard',copy:'auto'}]}
  ]},
  'the-scorecard-90':{id:'the-scorecard-90',name:'THE SCORECARD',duration:'90 min',format:'scorecard',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:15,timer:30,bonusAt:BONUS_15},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-2',name:'ROUND 2: SCREEN',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15,mediaFilter:['movie','tv']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'fluency',name:'FLUENCY CHECK',fixed:[{type:'round_title',content:{primary:'FLUENCY CHECK'},copy:'auto'}],slots:{mode:'fluency_pair',count:10,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'intermission',name:'INTERMISSION',fixed:[{type:'intermission',copy:'auto'},{type:'warning',content:{primary:'60 SECONDS.',secondary:''}},{type:'blackout',content:{primary:'',secondary:''}}]},
    {id:'doa',name:'DEAD OR ALIVE',fixed:[{type:'round_title',content:{primary:'DEAD OR ALIVE',secondary:'CULTURALLY RELEVANT OR CULTURALLY EXPIRED. YOU DECIDE.'}}],slots:{mode:'doa_triplet',count:10,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'round-3',name:'ROUND 3: LYRICS',fixed:[{type:'round_title',content:{primary:'ROUND 3'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15,mediaFilter:['music']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-4',name:'ROUND 4: EVERYTHING',fixed:[{type:'round_title',content:{primary:'ROUND 4'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30,bonusAt:BONUS_15},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'last_line',copy:'auto'},{type:'endcard',copy:'auto'}]},
    {id:'hot-seat',name:'BONUS: HOT SEAT',fixed:[{type:'round_title',content:{primary:'BONUS ROUND'},copy:'auto'},{type:'hotseat_rules',copy:'auto'}],slots:{mode:'hotseat_prompt',count:6,timer:30},closing:[{type:'score',copy:'auto'},{type:'endcard',copy:'auto'}]}
  ]},
  'the-scorecard-60':{id:'the-scorecard-60',name:'THE SCORECARD',duration:'60 min',format:'scorecard',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'doa',name:'DEAD OR ALIVE',fixed:[{type:'round_title',content:{primary:'DEAD OR ALIVE',secondary:'CULTURALLY RELEVANT OR CULTURALLY EXPIRED. YOU DECIDE.'}}],slots:{mode:'doa_triplet',count:8,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'round-2',name:'ROUND 2: SCREEN',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10,mediaFilter:['movie','tv']},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-3',name:'ROUND 3: EVERYTHING',fixed:[{type:'round_title',content:{primary:'ROUND 3'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'last_line',copy:'auto'},{type:'endcard',copy:'auto'}]}
  ]},
  'the-scorecard-30':{id:'the-scorecard-30',name:'THE SCORECARD',duration:'30 min',format:'scorecard',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:8,timer:30,bonusAt:BONUS_8},closing:[{type:'score',copy:'auto'}]},
    {id:'round-2',name:'ROUND 2: EVERYTHING',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:8,timer:30,bonusAt:BONUS_8},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'endcard',copy:'auto'}]}
  ]},
  'the-purge-20':{id:'the-purge-20',name:'THE PURGE',duration:'20 min',format:'purge',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'purge',name:'THE PURGE',fixed:[{type:'round_title',content:{primary:'THE PURGE',secondary:'50 LINES. 10 MINUTES. THE ROOM SHOUTS THE SOURCE.'}}],slots:{mode:'fluency_pair',count:25,timer:null},closing:[{type:'score',content:{primary:'THE PURGE COMPLETE',secondary:'HOW MANY DID YOU CLEAR?'}},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'endcard',copy:'auto'}]}
  ]},
  'the-tribunal-45':{id:'the-tribunal-45',name:'THE TRIBUNAL',duration:'45 min',format:'tribunal',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: THE TRIAL',fixed:[{type:'round_title',content:{primary:'THE TRIAL',secondary:'TWO STAND. ONE LINE. ONE LIAR. THE ROOM VOTES.'}},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'}]},
    {id:'round-2',name:'ROUND 2: THE APPEAL',fixed:[{type:'round_title',content:{primary:'THE APPEAL',secondary:'HARDER LINES. HIGHER STAKES.'}},{type:'tier_title',content:{primary:'SAME RULES. SAME CONSEQUENCES.',secondary:TIER_10}}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'endcard',copy:'auto'}]}
  ]},
  'the-faceoff-30':{id:'the-faceoff-30',name:'THE FACE-OFF',duration:'30 min',format:'faceoff',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'bracket',name:'THE BRACKET',fixed:[{type:'round_title',content:{primary:'THE FACE-OFF',secondary:'1v1. BACK TO SCREEN. FIRST TO MISS IS OUT.'}},{type:'tier_title',copy:'auto'}],slots:{mode:'hotseat_prompt',count:16,timer:10},closing:[{type:'score',content:{primary:'THE FACE-OFF COMPLETE',secondary:''}}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'endcard',copy:'auto'}]}
  ]},
  'the-audit-60':{id:'the-audit-60',name:'THE AUDIT',duration:'60 min',format:'scorecard',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'round-1',name:'ROUND 1: GUESS THE SOURCE',fixed:[{type:'round_title',content:{primary:'ROUND 1'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'source_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'doa',name:'DEAD OR ALIVE',fixed:[{type:'round_title',content:{primary:'DEAD OR ALIVE',secondary:'CULTURALLY RELEVANT OR CULTURALLY EXPIRED. YOU DECIDE.'}}],slots:{mode:'doa_triplet',count:8,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'round-2',name:'ROUND 2: DECODE',fixed:[{type:'round_title',content:{primary:'ROUND 2'},copy:'auto'},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'},{type:'crate_drop',content:{primary:'',secondary:''}}]},
    {id:'round-3',name:'ROUND 3: THE DEEP CUT',fixed:[{type:'round_title',content:{primary:'ROUND 3',secondary:'THE DEEP CUT'}},{type:'tier_title',copy:'auto',hardest:true}],slots:{mode:'acronym_q',count:10,timer:30,bonusAt:BONUS_10},closing:[{type:'score',copy:'auto'}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'endcard',content:{primary:'L',secondary:'AUDIT COMPLETE.'}}]}
  ]},
  'the-draft-60':{id:'the-draft-60',name:'THE DRAFT',duration:'60 min',format:'draft',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'WARM-UP',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'tryouts',name:'THE TRYOUTS',fixed:[{type:'round_title',content:{primary:'THE TRYOUTS',secondary:'EACH CANDIDATE. 3 ROUNDS IN THE HOT SEAT. THE ROOM VOTES: DRAFT OR CUT.'}},{type:'tier_title',copy:'auto'}],slots:{mode:'hotseat_prompt',count:12,timer:30},closing:[{type:'score',content:{primary:'TRYOUTS COMPLETE',secondary:'THE CANDIDATES HAVE BEEN TESTED.'}}]},
    {id:'doa',name:'DRAFT OR CUT',fixed:[{type:'round_title',content:{primary:'DRAFT OR CUT',secondary:'THE ROOM DECIDES. VOTE FROM YOUR PHONES.'}}],slots:{mode:'doa_triplet',count:8,timer:null},closing:[{type:'transition_beat',content:{primary:'',secondary:''}}]},
    {id:'gauntlet',name:'THE GAUNTLET',fixed:[{type:'round_title',content:{primary:'THE GAUNTLET',secondary:'RAPID-FIRE. BOTH CAPTAINS ATTACKING. LAST ONE STANDING.'}},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:10,timer:10,bonusAt:BONUS_10},closing:[{type:'score',content:{primary:'THE GAUNTLET COMPLETE',secondary:''}}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'endcard',copy:'auto'}]}
  ]},
  'the-split-90':{id:'the-split-90',name:'THE SPLIT',duration:'90 min',format:'split',sections:[
    {id:'pre-show',name:'PRE-SHOW',fixed:[{type:'attract',content:{primary:'L',secondary:''}},{type:'checkin',content:{primary:'lineconic.live/play',secondary:'SCAN TO PLAY ALONG',eventId:''}}]},
    {id:'warm-up',name:'THE FISHBOWL',fixed:[{type:'fishbowl',copy:'auto'}]},
    {id:'act-1',name:'ACT I: THE GRINDER',fixed:[{type:'round_title',content:{primary:'ACT I',secondary:'THE GRINDER'}},{type:'tier_title',copy:'auto'}],slots:{mode:'acronym_q',count:15,timer:30},closing:[{type:'score',content:{primary:'ACT I COMPLETE',secondary:''}}]},
    {id:'act-2',name:'ACT II: THE TAROT',fixed:[{type:'round_title',content:{primary:'ACT II',secondary:'THE TAROT'}},{type:'tier_title',copy:'auto'}],slots:{mode:'hotseat_prompt',count:10,timer:30},closing:[{type:'score',content:{primary:'ACT II COMPLETE',secondary:''}}]},
    {id:'act-3',name:'ACT III: THE GAUNTLET',fixed:[{type:'round_title',content:{primary:'ACT III',secondary:'THE GAUNTLET'}},{type:'tier_title',content:{primary:'FULL LINES. BOTH CAPTAINS HECKLING. OPPOSING SIDE SCREAMS DISINFORMATION.',secondary:'15 SECONDS. MAXIMUM CHAOS.'}}],slots:{mode:'hotseat_prompt',count:10,timer:15},closing:[{type:'score',content:{primary:'THE GAUNTLET COMPLETE',secondary:''}},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}}]},
    {id:'verdict',name:'THE VERDICT',fixed:[{type:'verdict',content:{primary:'THE VERDICT.',secondary:''}},{type:'sentence',copy:'auto'},{type:'receipt_rain',content:{primary:'MAKE IT RAIN.',secondary:''}},{type:'endcard',copy:'auto'}]}
  ]}
};

// ═══ CARD ADAPTER ═══
var slideCounter=0;
function makeSlideId(){return 'g'+(++slideCounter)}
function cardToSlide(card,mode,opts){
  var id=makeSlideId();
  var base={id:id,cardId:card.id,reveal_state:'hidden',team:'neutral',host_notes:null,points:opts&&opts.points||null,timer_seconds:opts&&opts.timer||null};
  switch(mode){
    case'source_q':return{...base,type:'source_q',content:{primary:card.content.answer.toUpperCase(),secondary:'',answer:card.source}};
    case'acronym_q':return{...base,type:'acronym_q',content:{primary:card.code,secondary:'',answer:card.content.answer.toUpperCase(),source:card.source}};
    case'hotseat_prompt':return{...base,type:'hotseat_prompt',content:{primary:card.content.answer.toUpperCase(),secondary:'',source:card.source}};
    default:return{...base,type:mode,content:{primary:card.content.answer.toUpperCase(),secondary:'',answer:card.source}};
  }
}
function cardToFluencyPair(card){
  return[
    {id:makeSlideId(),cardId:card.id,type:'fluency_line',content:{primary:card.content.answer.toUpperCase(),secondary:''},reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null},
    {id:makeSlideId(),cardId:card.id,type:'fluency_source',content:{primary:card.source.toUpperCase(),secondary:''},reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null}
  ];
}
function buildDoaTriplet(topic,verdict){
  return[
    {id:makeSlideId(),type:'doa_ref',content:{primary:topic,secondary:''},reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null},
    {id:makeSlideId(),type:'doa_vote',content:{primary:topic,secondary:''},reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null},
    {id:makeSlideId(),type:verdict==='dead'?'doa_verdict_dead':'doa_verdict_alive',content:{primary:verdict==='dead'?'DEAD':'ALIVE',secondary:''},reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null}
  ];
}

// ═══ SELECTION ALGORITHM ═══
function weightedRandom(items){
  var total=items.reduce(function(s,x){return s+Math.max(x.weight,1)},0);
  var r=Math.random()*total;var acc=0;
  for(var i=0;i<items.length;i++){acc+=Math.max(items[i].weight,1);if(r<=acc)return items[i]}
  return items[items.length-1];
}
function getDifficultyCurve(pos,total,curve){
  var ratio=pos/total;
  if(curve==='ramp_up'){if(ratio<.33)return 1;if(ratio<.66)return 2;return 3}
  if(curve==='ramp_down'){if(ratio<.33)return 3;if(ratio<.66)return 2;return 1}
  return[1,2,3][pos%3];
}

function buildDeck(library,templateId,config,customTemplates){
  slideCounter=0;
  var resolvedId=TEMPLATE_ALIASES[templateId]||templateId;
  var tmpl=(customTemplates&&customTemplates[resolvedId])||TEMPLATES[resolvedId];if(!tmpl)return null;
  var pool=library.filter(function(c){
    if(c.staged===true)return false;
    if(c.scores.lineconic_score<config.minScore)return false;
    if(config.excludedTags.length){for(var i=0;i<c.themes.length;i++){if(config.excludedTags.indexOf(c.themes[i])>=0)return false}}
    if(config.excludedIds&&config.excludedIds.length&&config.excludedIds.indexOf(c.id)>=0)return false;
    if(config.mediaFilter&&config.mediaFilter.length>0&&config.mediaFilter.indexOf(c.media_type)<0)return false;
    return true;
  });
  var totalSlots=0;
  tmpl.sections.forEach(function(s){if(s.disabled)return;if(s.slots&&s.slots.mode!=='doa_triplet')totalSlots+=s.slots.count});
  var diffQ={1:Math.floor(totalSlots*config.difficulty[0]/100),2:Math.floor(totalSlots*config.difficulty[1]/100),3:0};
  diffQ[3]=totalSlots-diffQ[1]-diffQ[2];if(diffQ[3]<0)diffQ[3]=0;
  var budgetQ={};
  var budgetMap={'Evergreen Canon':config.budget[0],'Deep Cuts':config.budget[1],'Seasonal':config.budget[2]};
  Object.keys(budgetMap).forEach(function(k){budgetQ[k]=Math.floor(totalSlots*budgetMap[k]/100)});
  var usedIds={};var tagCounts={};var sections=[];
  var format=tmpl.format||null;
  var lastScoredIdx=-1;
  if(format){tmpl.sections.forEach(function(sec,i){if(sec.disabled)return;if(sec.closing)sec.closing.forEach(function(f){if(f.type==='score')lastScoredIdx=i})})}
  else{tmpl.sections.forEach(function(sec,i){if(sec.disabled)return;if(sec.closing)sec.closing.forEach(function(f){if(f.type==='score')lastScoredIdx=i})})}

  tmpl.sections.forEach(function(sec,idx){
    if(sec.disabled)return;
    var slides=[];
    var sectionCtx={format:format,mode:sec.slots&&sec.slots.mode||null,mediaFilter:sec.slots&&sec.slots.mediaFilter||null,slotCount:sec.slots&&sec.slots.count||0,isLastScoredRound:idx===lastScoredIdx,secName:sec.name,closingLabel:sec.closingLabel||null};
    if(sec.fixed)sec.fixed.forEach(function(f){
      var content;
      if(f.copy==='auto'&&format){content=resolveCopy(f,sectionCtx)}else{content=f.content?{...f.content}:{primary:'',secondary:''}}
      if(f.type==='checkin'&&config.eventId)content.eventId=config.eventId;
      slides.push({id:makeSlideId(),type:f.type,content:content,reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null});
    });
    if(sec.slots){
      var mode=sec.slots.mode;var count=sec.slots.count;var timer=sec.slots.timer||null;var bonusAt=sec.slots.bonusAt||{};
      if(mode==='doa_triplet'){
        var doaList=config.doaTopics||[];
        var doaUseCount=Math.min(count,doaList.length);
        for(var dp=0;dp<doaUseCount;dp++){var triplet=buildDoaTriplet(doaList[dp].topic,doaList[dp].verdict);slides.push(triplet[0]);slides.push(triplet[1]);slides.push(triplet[2])}
      }else{
        // Section's mediaFilter always wins. The deck-level config.mediaFilter
        // already pre-narrowed the global pool at line 299 — section filter
        // stacks on top, never replaced by it. Without this, a deck-level
        // filter caused music/screen rounds to bleed wrong content types.
        var sectionMedia=sec.slots&&sec.slots.mediaFilter||null;
        for(var pos=0;pos<count;pos++){
          var eligible=pool.filter(function(c){if(usedIds[c.id])return false;if(sectionMedia&&sectionMedia.indexOf(c.media_type)<0)return false;return true});
          if(eligible.length===0)break;
          var targetDiff=getDifficultyCurve(pos,count,config.curve);
          var scored=eligible.map(function(c){
            var cs=crowdScores[c.id];var staticScore=c.scores.lineconic_score*10;var w;
            if(cs&&cs.showCount>0){var blend=Math.min(cs.showCount/5,1);w=staticScore*(1-blend)+cs.crowd_score*10*blend;if(cs.lastPlayed){var daysSince=(Date.now()-new Date(cs.lastPlayed).getTime())/86400000;if(daysSince>14)w+=5;else if(daysSince<3)w-=10}}else{w=staticScore}
            if(c.difficulty===targetDiff)w+=30;if(diffQ[c.difficulty]!==undefined&&diffQ[c.difficulty]<=0)w-=40;if(budgetQ[c.category]!==undefined&&budgetQ[c.category]<=0)w-=40;
            if(config.diversity){var maxTag=0;c.themes.forEach(function(t){var u=tagCounts[t]||0;if(u>maxTag)maxTag=u});w-=maxTag*5}
            return{card:c,weight:Math.max(w,1)};
          });
          var pick=weightedRandom(scored);var card=pick.card;
          usedIds[card.id]=true;if(diffQ[card.difficulty]!==undefined)diffQ[card.difficulty]--;if(budgetQ[card.category]!==undefined)budgetQ[card.category]--;
          card.themes.forEach(function(t){tagCounts[t]=(tagCounts[t]||0)+1});
          var qNum=pos+1;
          if(bonusAt[qNum])slides.push({id:makeSlideId(),type:'bonus_marker',content:{primary:'BONUS: '+bonusAt[qNum]+' POINTS',secondary:''},reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null});
          if(mode==='fluency_pair'){var pair=cardToFluencyPair(card);slides.push(pair[0]);slides.push(pair[1])}else{slides.push(cardToSlide(card,mode,{timer:timer,points:bonusAt[qNum]||null}))}
        }
      }
    }
    if(sec.closing)sec.closing.forEach(function(f){var content;if(f.copy==='auto'&&format){content=resolveCopy(f,sectionCtx)}else{content=f.content?{...f.content}:{primary:'',secondary:''}};slides.push({id:makeSlideId(),type:f.type,content:content,reveal_state:'hidden',team:'neutral',timer_seconds:null,host_notes:null,points:null})});
    sections.push({id:sec.id,name:sec.name,slides:slides});
  });
  var dateStr=new Date().toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'}).toUpperCase();
  var deck={id:'deck-'+Date.now(),name:(config.showName||'THE LAUNCH').toUpperCase()+' \u2014 '+dateStr,templateId:templateId,duration:tmpl.duration,mediaFilter:config.mediaFilter||[],eventId:config.eventId||'',sections:sections,created:Date.now(),version:4};
  // Opt-out flag for the cyan/pink/open alternating-side mechanic. Only written
  // when explicitly turned off in the builder — absence means the mechanic is on.
  if(config.activeSideAlternation===false)deck.disableActiveSide=true;
  if(format!=='scorecard')deck.showAnswerSheets=false;
  return deck;
}

function applyCopyOverrides(deck,overrides){
  if(!deck||!overrides)return deck;
  deck.sections.forEach(function(sec){
    sec.slides.forEach(function(sl){
      var ov=overrides[sl.type];
      if(ov){
        if(ov.primary!==undefined)sl.content.primary=ov.primary;
        if(ov.secondary!==undefined)sl.content.secondary=ov.secondary;
      }
    });
  });
  return deck;
}

var ALL_TAGS=['Hype Moment','Comedy Staples','Dramatic Delivery','90s Nostalgia','00s Nostalgia','Main Character Energy','10s Nostalgia','Reality TV','Millennial Cringe','Rom-Com','Action Sequences','Sci-Fi/Fantasy','Internet Native','Music Drops','Friendship Chaos','Black Culture','Horror Adjacent','Crying Scene','Villain Arc','UK Culture','Gen Z Brain Rot','Vine Era','Corporate Satire','TikTok Native','Toxic Romance'];

var DOA_POOL=[{topic:'SKINNY JEANS',verdict:'dead'},{topic:'ROM COMS',verdict:'alive'},{topic:'LAUGH TRACKS',verdict:'dead'},{topic:'NEPO BABIES',verdict:'alive'},{topic:'MARVEL PHASE 5',verdict:'dead'},{topic:'REALITY TV',verdict:'alive'},{topic:'METHOD ACTING',verdict:'dead'},{topic:'THE OFFICE REBOOT',verdict:'dead'},{topic:'A24',verdict:'alive'},{topic:'MOVIE THEATRES',verdict:'alive'},{topic:'CRYPTO',verdict:'dead'},{topic:'VINYL RECORDS',verdict:'alive'},{topic:'FAST FASHION',verdict:'dead'},{topic:'PODCASTS',verdict:'alive'},{topic:'NFTs',verdict:'dead'},{topic:'SOURDOUGH',verdict:'dead'},{topic:'K-POP',verdict:'alive'},{topic:'CABLE TV',verdict:'dead'},{topic:'THRIFT SHOPPING',verdict:'alive'},{topic:'GENDER REVEALS',verdict:'dead'},{topic:'TRUE CRIME DOCS',verdict:'alive'},{topic:'DVD COLLECTIONS',verdict:'dead'},{topic:'BRUNCH CULTURE',verdict:'alive'},{topic:'FANNY PACKS',verdict:'alive'},{topic:'SITUATIONSHIPS',verdict:'alive'},{topic:'CROCS',verdict:'alive'},{topic:'QUIET QUITTING',verdict:'alive'},{topic:'PHONE CASES',verdict:'alive'},{topic:'CANCEL CULTURE',verdict:'dead'},{topic:'ASTROLOGY',verdict:'alive'},{topic:'MEAL PREP',verdict:'alive'},{topic:'HANDSHAKES',verdict:'dead'},{topic:'HOUSE PLANTS',verdict:'alive'},{topic:'VOICEMAIL',verdict:'dead'},{topic:'KARAOKE',verdict:'alive'},{topic:'PRINT MEDIA',verdict:'dead'},{topic:'TINY HOMES',verdict:'dead'},{topic:'STREET FOOD',verdict:'alive'},{topic:'BEIGE INTERIORS',verdict:'dead'},{topic:'HOT YOGA',verdict:'alive'}];

function parseDoaTopics(text){
  if(!text||!text.trim())return[];
  return text.trim().split('\n').map(function(line,i){line=line.trim();if(!line)return null;var idx=line.lastIndexOf(':');var topic,verdict;if(idx>0){topic=line.substring(0,idx).trim().toUpperCase();verdict=line.substring(idx+1).trim().toLowerCase()}else{topic=line.toUpperCase();verdict=''}if(verdict!=='dead'&&verdict!=='alive')verdict=i%2===0?'alive':'dead';return{topic:topic,verdict:verdict}}).filter(Boolean);
}
function shuffleDoaPool(count){
  var shuffled=DOA_POOL.slice().sort(function(){return Math.random()-0.5});var picked=shuffled.slice(0,count);
  var deadTarget=Math.floor(count/2);var aliveTarget=count-deadTarget;var deads=[];var alives=[];
  picked.forEach(function(t){if(t.verdict==='dead')deads.push(t);else alives.push(t)});
  while(deads.length>deadTarget&&alives.length<aliveTarget){alives.push({topic:deads.pop().topic,verdict:'alive'})}
  while(alives.length>aliveTarget&&deads.length<deadTarget){deads.push({topic:alives.pop().topic,verdict:'dead'})}
  var result=[];var di=0,ai=0;
  for(var i=0;i<count;i++){if(i%2===0&&ai<alives.length)result.push(alives[ai++]);else if(di<deads.length)result.push(deads[di++]);else if(ai<alives.length)result.push(alives[ai++]);else if(di<deads.length)result.push(deads[di++])}
  return result.map(function(t){return t.topic+':'+t.verdict}).join('\n');
}

// ═══ BUILDER MODULE COMPONENT ═══
function SetlistManager(props) {
  var db = props.db, showIndex = props.showIndex, activeShow = props.activeShow, showToast = props.showToast;
  var [confirmDelete, setConfirmDelete] = useState(null);
  var [expandedShow, setExpandedShow] = useState(null);
  var [showData, setShowData] = useState(null);
  var [editingShow, setEditingShow] = useState(null);
  var [dirty, setDirty] = useState(false);
  var [loadingShow, setLoadingShow] = useState(null);
  var [renaming, setRenaming] = useState(null);
  var [renameValue, setRenameValue] = useState('');
  var [editingSlide, setEditingSlide] = useState(null);

  var shows = useMemo(function() {
    if (!showIndex) return [];
    return Object.keys(showIndex).map(function(id) {
      var entry = showIndex[id];
      return { id: id, name: entry.name || id, created: entry.created || null, template: entry.template || entry.templateId || '', duration: entry.duration || '' };
    }).sort(function(a, b) { return (b.created || 0) - (a.created || 0); });
  }, [showIndex]);

  function handleActivate(showId) {
    db.ref('active_show').set(showId).then(function() {
      showToast('ACTIVATED: ' + showId);
    }).catch(function(e) { showToast('FAILED: ' + e.message); });
  }

  function handleDelete(showId) {
    if (confirmDelete !== showId) { setConfirmDelete(showId); return; }
    var updates = {};
    updates['shows/' + showId] = null;
    updates['show_index/' + showId] = null;
    if (activeShow === showId) updates['active_show'] = null;
    db.ref().update(updates).then(function() {
      showToast('DELETED: ' + showId);
      setConfirmDelete(null);
      if (expandedShow === showId) { setExpandedShow(null); setShowData(null); setEditingShow(null); setDirty(false); }
    }).catch(function(e) { showToast('DELETE FAILED: ' + e.message); });
  }

  function loadShow(id) {
    if (expandedShow === id) { setExpandedShow(null); setShowData(null); setEditingShow(null); setDirty(false); setEditingSlide(null); return; }
    setLoadingShow(id);
    setEditingSlide(null);
    db.ref('shows/' + id).once('value').then(function(snap) {
      var data = snap.val();
      setShowData(data);
      setEditingShow(JSON.parse(JSON.stringify(data)));
      setExpandedShow(id);
      setDirty(false);
      setLoadingShow(null);
    }).catch(function(e) { showToast('LOAD FAILED: ' + e.message); setLoadingShow(null); });
  }

  function updateShowSlide(secIdx, slideIdx, field, value) {
    setEditingShow(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      if (!next.sections[secIdx] || !next.sections[secIdx].slides[slideIdx]) return next;
      if (field === 'host_notes') { next.sections[secIdx].slides[slideIdx].host_notes = value; }
      else { if (!next.sections[secIdx].slides[slideIdx].content) next.sections[secIdx].slides[slideIdx].content = {}; next.sections[secIdx].slides[slideIdx].content[field] = value; }
      return next;
    });
    setDirty(true);
  }

  function removeShowSlide(secIdx, slideIdx) {
    setEditingShow(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.sections[secIdx].slides.splice(slideIdx, 1);
      return next;
    });
    setDirty(true);
  }

  function moveShowSlide(secIdx, slideIdx, dir) {
    setEditingShow(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var slides = next.sections[secIdx].slides;
      var newIdx = slideIdx + dir;
      if (newIdx < 0 || newIdx >= slides.length) return next;
      var temp = slides[slideIdx];
      slides[slideIdx] = slides[newIdx];
      slides[newIdx] = temp;
      return next;
    });
    setDirty(true);
  }

  function saveShow() {
    if (!editingShow || !expandedShow) return;
    db.ref('shows/' + expandedShow).set(editingShow).then(function() {
      setShowData(JSON.parse(JSON.stringify(editingShow)));
      setDirty(false);
      showToast('SHOW SAVED');
    }).catch(function(e) { showToast('SAVE FAILED: ' + e.message); });
  }

  function discardEdits() {
    if (!showData) return;
    setEditingShow(JSON.parse(JSON.stringify(showData)));
    setDirty(false);
    setEditingSlide(null);
  }

  function duplicateShow(id) {
    var source = expandedShow === id && editingShow ? editingShow : null;
    function doDuplicate(data) {
      var newId = 'deck-' + Date.now();
      var copy = JSON.parse(JSON.stringify(data));
      copy.id = newId;
      copy.name = copy.name + ' (COPY)';
      copy.created = Date.now();
      var updates = {};
      updates['shows/' + newId] = copy;
      updates['show_index/' + newId] = { name: copy.name, created: copy.created, templateId: copy.templateId || null, duration: copy.duration || null, mediaFilter: copy.mediaFilter || [] };
      db.ref().update(updates).then(function() {
        showToast('DUPLICATED: ' + copy.name);
      }).catch(function(e) { showToast('DUPLICATE FAILED: ' + e.message); });
    }
    if (source) { doDuplicate(source); }
    else {
      db.ref('shows/' + id).once('value').then(function(snap) {
        var data = snap.val();
        if (!data) { showToast('SHOW NOT FOUND'); return; }
        doDuplicate(data);
      }).catch(function(e) { showToast('DUPLICATE FAILED: ' + e.message); });
    }
  }

  function startRename(id, name) {
    setRenaming(id);
    setRenameValue(name);
  }

  function confirmRename(id) {
    if (!renameValue.trim()) { setRenaming(null); return; }
    var newName = renameValue.trim().toUpperCase();
    var updates = {};
    updates['shows/' + id + '/name'] = newName;
    updates['show_index/' + id + '/name'] = newName;
    db.ref().update(updates).then(function() {
      showToast('RENAMED');
      setRenaming(null);
      if (expandedShow === id && editingShow) {
        setEditingShow(function(prev) { var next = JSON.parse(JSON.stringify(prev)); next.name = newName; return next; });
        setShowData(function(prev) { if (!prev) return prev; var next = JSON.parse(JSON.stringify(prev)); next.name = newName; return next; });
      }
    }).catch(function(e) { showToast('RENAME FAILED: ' + e.message); });
  }

  if (!showIndex) return <div style={{padding:16,color:'var(--text-dim)',fontSize:12,letterSpacing:1}}>LOADING SHOWS...</div>;

  return (
    <div style={{marginTop:24,borderTop:'1px solid var(--border)',paddingTop:16}}>
      <div style={{fontSize:11,letterSpacing:2,color:'var(--text-sec)',marginBottom:12}}>SHOWS ({shows.length})</div>
      {shows.length === 0 && <div style={{padding:16,color:'var(--text-dim)',fontSize:11}}>NO SHOWS PUSHED YET</div>}
      {shows.map(function(s) {
        var isActive = activeShow === s.id;
        var isExpanded = expandedShow === s.id;
        var isLoading = loadingShow === s.id;
        var isRenaming = renaming === s.id;
        return (
          <div key={s.id} style={{marginBottom:4}}>
            <div style={{
              display:'flex', alignItems:'center', gap:8, padding:'8px 12px',
              background: isActive ? 'rgba(0,255,255,.06)' : 'var(--surface-2)',
              border: isActive ? '1px solid var(--cyan)' : '1px solid var(--border)',
              fontSize:11, fontFamily:'var(--mono)', cursor:'pointer'
            }} onClick={function(){loadShow(s.id)}}>
              <span style={{fontSize:9,color:'var(--text-dim)',minWidth:16}}>{isExpanded?'▼':'▶'}</span>
              {isRenaming ? (
                <input className="A-input" style={{flex:1,fontSize:11,padding:'2px 6px'}} value={renameValue} onChange={function(e){setRenameValue(e.target.value)}} onKeyDown={function(e){if(e.key==='Enter')confirmRename(s.id);if(e.key==='Escape')setRenaming(null)}} onBlur={function(){confirmRename(s.id)}} onClick={function(e){e.stopPropagation()}} autoFocus />
              ) : (
                <span style={{flex:1,color:isActive?'var(--cyan)':'#fff',letterSpacing:'.04em'}}>{s.name}</span>
              )}
              {isLoading && <span style={{fontSize:9,color:'var(--text-dim)'}}>LOADING...</span>}
              {s.template && <span style={{color:'var(--text-dim)',fontSize:9}}>{s.template}</span>}
              {s.duration && <span style={{color:'var(--text-dim)',fontSize:9}}>{s.duration}</span>}
              {s.created && <span style={{color:'var(--text-dim)',fontSize:9}}>{new Date(s.created).toLocaleDateString()}</span>}
              {isActive && <span style={{fontSize:8,letterSpacing:1,color:'var(--cyan)',padding:'2px 6px',border:'1px solid var(--cyan)'}}>ACTIVE</span>}
              {!isActive && <button className="A-btn" style={{fontSize:9,padding:'3px 8px'}} onClick={function(e){e.stopPropagation();handleActivate(s.id)}}>ACTIVATE</button>}
              <button className="A-btn" style={{fontSize:9,padding:'3px 8px'}} onClick={function(e){e.stopPropagation();startRename(s.id,s.name)}}>RENAME</button>
              <button className="A-btn" style={{fontSize:9,padding:'3px 8px'}} onClick={function(e){e.stopPropagation();duplicateShow(s.id)}}>DUPLICATE</button>
              <button className="A-btn" style={{fontSize:9,padding:'3px 8px',color:confirmDelete===s.id?'var(--signal)':'var(--text-dim)',borderColor:confirmDelete===s.id?'var(--signal)':'var(--border)'}} onClick={function(e){e.stopPropagation();handleDelete(s.id)}}>
                {confirmDelete === s.id ? 'CONFIRM?' : 'DELETE'}
              </button>
            </div>
            {isExpanded && editingShow && editingShow.sections && (
              <div style={{border:'1px solid var(--border)',borderTop:'none',background:'#0a0a0a'}}>
                {dirty && <div style={{display:'flex',gap:8,padding:'8px 12px',background:'rgba(0,255,255,.04)',borderBottom:'1px solid var(--border)'}}>
                  <span style={{flex:1,fontSize:10,color:'var(--signal)',letterSpacing:1}}>UNSAVED CHANGES</span>
                  <button className="A-btn cyan" style={{fontSize:9,padding:'3px 10px'}} onClick={saveShow}>SAVE</button>
                  <button className="A-btn" style={{fontSize:9,padding:'3px 10px'}} onClick={discardEdits}>DISCARD</button>
                </div>}
                {editingShow.sections.map(function(sec, si) {
                  return (
                    <div className="B-p-sec" key={sec.id || si}>
                      <div className="B-p-sec-head" style={{fontSize:10,padding:'6px 12px'}}>
                        <span>{sec.name}</span>
                        <span className="ct">{sec.slides ? sec.slides.length : 0} SLIDES</span>
                      </div>
                      {sec.slides && sec.slides.map(function(sl, sli) {
                        var slideKey = si + '-' + sli;
                        var isSlideEditing = editingSlide === slideKey;
                        return (
                          <div key={sl.id || sli}>
                            <div className={'B-p-slide' + (['source_q','acronym_q','hotseat_prompt','fluency_line','doa_vote','doa_ref'].indexOf(sl.type) >= 0 ? '' : ' struct')} style={{display:'flex',alignItems:'center',gap:6}} onClick={function(){setEditingSlide(isSlideEditing ? null : slideKey)}}>
                              <span className="num">{sli + 1}</span>
                              <span className="tp">{sl.type.replace(/_/g, ' ')}</span>
                              <span className="ln" style={{flex:1}}>{sl.content && sl.content.primary || ''}</span>
                              {sl.content && sl.content.answer && <span style={{color:'var(--text-dim)',fontSize:9}}>{sl.content.answer}</span>}
                              <button className="A-btn" style={{fontSize:8,padding:'1px 4px'}} onClick={function(e){e.stopPropagation();moveShowSlide(si,sli,-1)}} disabled={sli===0}>▲</button>
                              <button className="A-btn" style={{fontSize:8,padding:'1px 4px'}} onClick={function(e){e.stopPropagation();moveShowSlide(si,sli,1)}} disabled={sli===sec.slides.length-1}>▼</button>
                              <button className="A-btn danger" style={{fontSize:8,padding:'1px 4px'}} onClick={function(e){e.stopPropagation();removeShowSlide(si,sli)}}>✕</button>
                            </div>
                            {isSlideEditing && (
                              <div style={{padding:'8px 12px 8px 32px',background:'rgba(0,255,255,.03)',borderBottom:'1px solid var(--border)'}}>
                                <input className="A-input" style={{marginBottom:4,fontSize:10}} value={sl.content && sl.content.primary || ''} onChange={function(e){updateShowSlide(si,sli,'primary',e.target.value)}} placeholder="primary"/>
                                <input className="A-input" style={{marginBottom:4,fontSize:10}} value={sl.content && sl.content.secondary || ''} onChange={function(e){updateShowSlide(si,sli,'secondary',e.target.value)}} placeholder="secondary"/>
                                <input className="A-input" style={{fontSize:10}} value={sl.host_notes || ''} onChange={function(e){updateShowSlide(si,sli,'host_notes',e.target.value)}} placeholder="host notes"/>
                              </div>
                            )}
                          </div>
                        );
                      })}
                    </div>
                  );
                })}
                {dirty && <div style={{display:'flex',gap:8,padding:'8px 12px',borderTop:'1px solid var(--border)',background:'rgba(0,255,255,.04)'}}>
                  <button className="A-btn cyan" style={{fontSize:9,padding:'3px 10px'}} onClick={saveShow}>SAVE</button>
                  <button className="A-btn" style={{fontSize:9,padding:'3px 10px'}} onClick={discardEdits}>DISCARD</button>
                </div>}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

function BuilderModule(props) {
  var library = props.library, db = props.db, showToast = props.showToast, showIndex = props.showIndex;
  var [templateId,setTemplateId]=useState('house-rules-90');
  var [difficulty,setDifficulty]=useState([30,50,20]);
  var [budget,setBudget]=useState([55,35,10]);
  var [minScore,setMinScore]=useState(5.0);
  var [excludedTags,setExcludedTags]=useState([]);
  var [curve,setCurve]=useState('ramp_up');
  var [diversity,setDiversity]=useState(true);
  // Active-side alternation: cyan/pink/open per-question mechanic. Default ON.
  // Stored on the built deck as disableActiveSide:true when OFF.
  var [activeSideAlternation,setActiveSideAlternation]=useState(true);
  var [excludedIds,setExcludedIds]=useState('');
  var [showAdvanced,setShowAdvanced]=useState(false);
  var [showName,setShowName]=useState('THE LAUNCH');
  var [eventId,setEventId]=useState('');
  var [mediaFilter,setMediaFilter]=useState([]);
  var [doaTopics,setDoaTopics]=useState(function(){return shuffleDoaPool(10)});
  var [ros,setRos]=useState(null);
  var [pushing,setPushing]=useState(false);
  var [aiSuggest,setAiSuggest]=useState(null);
  var [aiLoading,setAiLoading]=useState(false);
  var [aiVenue,setAiVenue]=useState('');
  var [aiAudience,setAiAudience]=useState('');
  var [aiAutoGenerate,setAiAutoGenerate]=useState(false);
  var [aiCopyOverrides,setAiCopyOverrides]=useState(null);
  var [builderView, setBuilderView] = useState('generate');
  var [gigBrief,setGigBrief]=useState('');
  var [gigLoading,setGigLoading]=useState(false);
  var [manualMode,setManualMode]=useState(false);
  var [gigReasoning,setGigReasoning]=useState(null);

  var mergedTemplates = useMemo(function() {
    return Object.assign({}, TEMPLATES, props.templates || {});
  }, [props.templates]);

  var eligible=useMemo(function(){
    if(!library)return 0;
    return library.filter(function(c){if(c.scores.lineconic_score<minScore)return false;for(var i=0;i<c.themes.length;i++){if(excludedTags.indexOf(c.themes[i])>=0)return false}if(mediaFilter.length>0&&mediaFilter.indexOf(c.media_type)<0)return false;return true}).length;
  },[library,minScore,excludedTags,mediaFilter]);

  var templateNeeds=useMemo(function(){var tmpl=mergedTemplates[templateId];if(!tmpl)return 0;var total=0;tmpl.sections.forEach(function(s){if(s.disabled)return;if(s.slots&&s.slots.mode!=='doa_triplet')total+=s.slots.count});return total},[templateId,mergedTemplates]);

  var doaCount=useMemo(function(){var tmpl=mergedTemplates[templateId];if(!tmpl)return 10;var doaSec=tmpl.sections.find(function(s){return s.id==='doa'&&!s.disabled});return doaSec&&doaSec.slots?doaSec.slots.count:0},[templateId,mergedTemplates]);

  var tagCounts=useMemo(function(){if(!library)return{};var counts={};library.forEach(function(c){c.themes.forEach(function(t){counts[t]=(counts[t]||0)+1})});return counts},[library]);

  function toggleTag(tag){setExcludedTags(function(prev){return prev.indexOf(tag)>=0?prev.filter(function(t){return t!==tag}):prev.concat([tag])})}

  function generate(){
    if(!library)return;
    var config={difficulty:difficulty,budget:budget,minScore:minScore,excludedTags:excludedTags,curve:curve,diversity:diversity,excludedIds:excludedIds.trim()?excludedIds.trim().split(/[\n,]+/).map(function(s){return s.trim()}).filter(Boolean):[],doaTopics:parseDoaTopics(doaTopics),showName:showName.trim()||'THE LAUNCH',mediaFilter:mediaFilter,eventId:eventId.trim(),activeSideAlternation:activeSideAlternation};
    var deck=buildDeck(library,templateId,config,props.templates);
    if(aiCopyOverrides)deck=applyCopyOverrides(deck,aiCopyOverrides);
    setRos(deck);
  }

  function fetchAiSuggest(){
    if(!library)return;
    setAiLoading(true);setAiSuggest(null);
    var tmpl=TEMPLATES[templateId];
    var recentCards=[];
    if(showIndex){Object.values(showIndex).sort(function(a,b){return(b.created||0)-(a.created||0)}).slice(0,5).forEach(function(s){if(s.cardIds)recentCards=recentCards.concat(s.cardIds)})}
    firebase.auth().currentUser.getIdToken().then(function(token){
      return aiFetch('/api/ai/optimize-setlist',{
        method:'POST',
        headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},
        body:JSON.stringify({template:{id:templateId,name:tmpl.name,duration:tmpl.duration,sections:tmpl.sections.map(function(s){return{id:s.id,name:s.name,slots:s.slots||null}})},venue:aiVenue||undefined,audience:aiAudience||undefined,previousCards:recentCards.length?recentCards:undefined,currentConfig:{difficulty:difficulty,budget:budget,minScore:minScore,curve:curve,diversity:diversity,mediaFilter:mediaFilter}})
      });
    }).then(function(res){
      return res.json();
    }).then(function(data){
      setAiSuggest(data.suggestions);setAiLoading(false);
      if(data.suggestions&&data.suggestions.copy_overrides)setAiCopyOverrides(data.suggestions.copy_overrides);
      if(data.suggestions&&data.suggestions.recommended_config){applyAiConfig(data.suggestions.recommended_config);setAiAutoGenerate(true)}
    })
    .catch(function(e){showToast('AI SUGGEST FAILED: '+formatAiError(e));setAiLoading(false)});
  }

  function applyAiConfig(cfg){
    if(!cfg)return;
    if(Array.isArray(cfg.difficulty)&&cfg.difficulty.length===3){
      var d=cfg.difficulty.map(function(v){return Math.max(0,Math.round(Number(v)||0))});
      var dSum=d[0]+d[1]+d[2];
      if(dSum>0){setDifficulty(dSum===100?d:[Math.round(d[0]*100/dSum),Math.round(d[1]*100/dSum),100-Math.round(d[0]*100/dSum)-Math.round(d[1]*100/dSum)])}
    }
    if(Array.isArray(cfg.budget)&&cfg.budget.length===3){
      var b=cfg.budget.map(function(v){return Math.max(0,Math.round(Number(v)||0))});
      var bSum=b[0]+b[1]+b[2];
      if(bSum>0){setBudget(bSum===100?b:[Math.round(b[0]*100/bSum),Math.round(b[1]*100/bSum),100-Math.round(b[0]*100/bSum)-Math.round(b[1]*100/bSum)])}
    }
    if(typeof cfg.minScore==='number'){setMinScore(Math.min(8.5,Math.max(3.8,cfg.minScore)))}
    if(typeof cfg.curve==='string'&&['ramp_up','flat','ramp_down'].indexOf(cfg.curve)>=0){setCurve(cfg.curve)}
    if(typeof cfg.diversity==='boolean'){setDiversity(cfg.diversity)}
    if(typeof cfg.activeSideAlternation==='boolean'){setActiveSideAlternation(cfg.activeSideAlternation)}
    if(Array.isArray(cfg.mediaFilter)){
      var valid=['movie','tv','music','internet'];
      setMediaFilter(cfg.mediaFilter.filter(function(m){return valid.indexOf(m)>=0}));
    }
  }

  function fetchGigBuild(){
    if(!library||!gigBrief.trim())return;
    setGigLoading(true);setGigReasoning(null);setAiSuggest(null);setRos(null);
    var templateSummaries=Object.keys(mergedTemplates).map(function(k){
      var t=mergedTemplates[k];var secs=t.sections||[];
      var secSummary=secs.filter(function(s){return s.slots}).map(function(s){return s.name+' ('+s.slots.mode+'×'+s.slots.count+')'}).join(', ');
      return{id:k,name:t.name,duration:t.duration,format:t.format||'custom',sectionSummary:secSummary};
    });
    var stats={total:library.length,byMedia:{},byDifficulty:{}};
    library.forEach(function(c){stats.byMedia[c.media_type]=(stats.byMedia[c.media_type]||0)+1;stats.byDifficulty[c.difficulty]=(stats.byDifficulty[c.difficulty]||0)+1});
    var recentShows=[];
    if(showIndex){recentShows=Object.values(showIndex).sort(function(a,b){return(b.created||0)-(a.created||0)}).slice(0,5).map(function(s){return{name:s.name||'',template:s.templateId||'',duration:s.duration||''}})}
    firebase.auth().currentUser.getIdToken().then(function(token){
      return aiFetch('/api/ai/generate-show',{
        method:'POST',
        headers:{'Content-Type':'application/json','Authorization':'Bearer '+token},
        body:JSON.stringify({brief:gigBrief,availableTemplates:templateSummaries,libraryStats:stats,previousShows:recentShows,doaPool:DOA_POOL})
      });
    }).then(function(res){
      return res.json();
    }).then(function(data){
      if(!data.ok||!data.result){throw new Error('Invalid AI response')}
      var r=data.result;
      // Apply template
      var resolvedTmplId=TEMPLATE_ALIASES[r.templateId]||r.templateId;
      if(resolvedTmplId&&mergedTemplates[resolvedTmplId])setTemplateId(resolvedTmplId);
      // Apply show name
      if(r.showName)setShowName(r.showName);
      // Apply config
      if(r.config){
        applyAiConfig(r.config);
        if(Array.isArray(r.config.excludedTags))setExcludedTags(r.config.excludedTags.filter(function(t){return ALL_TAGS.indexOf(t)>=0}));
        if(typeof r.config.doaTopics==='string'&&r.config.doaTopics.trim())setDoaTopics(r.config.doaTopics);
      }
      // Apply copy overrides
      if(r.copy_overrides)setAiCopyOverrides(r.copy_overrides);
      // Store reasoning
      if(r.reasoning)setGigReasoning(r.reasoning);
      setGigLoading(false);
      setAiAutoGenerate(true);
    }).catch(function(e){showToast('BUILD FAILED: '+formatAiError(e));setGigLoading(false)});
  }

  useEffect(function(){
    if(aiAutoGenerate){generate();setAiAutoGenerate(false)}
  },[aiAutoGenerate]);

  function doExport(){if(!ros)return;var blob=new Blob([JSON.stringify(ros,null,2)],{type:'application/json'});var url=URL.createObjectURL(blob);var a=document.createElement('a');a.href=url;a.download=ros.id+'.json';a.click();URL.revokeObjectURL(url);showToast('JSON DOWNLOADED')}
  function doPush(){
    if(!ros||!db)return;setPushing(true);
    try{localStorage.setItem('lineconic_show',JSON.stringify(ros))}catch(e){}
    var updates={};updates['shows/'+ros.id]=ros;updates['show_index/'+ros.id]={name:ros.name,created:ros.created,templateId:ros.templateId||null,duration:ros.duration||null,mediaFilter:ros.mediaFilter||[]};updates['active_show']=ros.id;
    db.ref().update(updates).then(function(){setPushing(false);showToast('PUSHED TO FIREBASE')}).catch(function(e){setPushing(false);showToast('PUSH FAILED: '+e.message)});
  }
  function doCopy(){if(!ros)return;navigator.clipboard.writeText(JSON.stringify(ros,null,2)).then(function(){showToast('COPIED')}).catch(function(){showToast('COPY FAILED')})}

  if(!library)return <div className="A-empty"><div className="icon">L</div><div className="msg">LOADING CARD LIBRARY...</div></div>;

  return(
    <div style={{display:'flex',flexDirection:'column',height:'100%',overflow:'hidden'}}>
      <div style={{display:'flex',gap:0,flexShrink:0,borderBottom:'1px solid var(--border)',background:'#000'}}>
        {['generate','setlist','templates'].map(function(v) {
          return <div key={v} className={'A-tab' + (builderView===v?' on':'')} onClick={function(){setBuilderView(v)}} style={{padding:'8px 20px'}}>{v.toUpperCase()}</div>;
        })}
      </div>
      <div style={{flex:1,overflow:'hidden'}}>
        {builderView === 'templates' && <TemplateEditor db={db} showToast={showToast} templates={props.templates} />}
        {builderView === 'setlist' && <div style={{padding:24,overflow:'auto',height:'100%'}}><SetlistManager db={db} showIndex={showIndex} activeShow={props.activeShow} showToast={showToast} /></div>}
        {builderView === 'generate' && (
    <div className="B">
      <div className="B-form">
        <div className="B-sec"><div className="B-sec-head" style={{color:'var(--cyan)',fontSize:12,letterSpacing:2}}>DESCRIBE YOUR GIG</div>
          <textarea style={{width:'100%',background:'var(--surface-2)',border:'1px solid var(--cyan)',color:'#fff',fontFamily:'var(--mono)',fontSize:'.85rem',padding:'14px',outline:'none',resize:'vertical',lineHeight:1.6,minHeight:80}} rows={4} value={gigBrief} onChange={function(e){setGigBrief(e.target.value)}} placeholder="Saturday night, 200 people, rooftop bar in Shoreditch, mostly 25-35, heavy on music and memes"/>
          <div style={{fontSize:9,color:'var(--text-dim)',marginTop:4,letterSpacing:'.06em'}}>VENUE, CROWD SIZE, AUDIENCE, DURATION, VIBE — THE AI HANDLES THE REST</div>
          <button className="B-gen" style={{marginTop:12,background:'rgba(0,255,255,.08)',color:'var(--cyan)',border:'1px solid var(--cyan)',fontSize:13,letterSpacing:2}} onClick={fetchGigBuild} disabled={gigLoading||!gigBrief.trim()}>{gigLoading?'BUILDING SHOW...':'BUILD SHOW'}</button>
        </div>
        <div className="B-sec"><div className="B-sec-head">CIRCUIT EVENT ID</div>
          <input style={{width:'100%',background:'var(--surface-2)',border:'1px solid '+(eventId.trim()?'var(--border)':'var(--signal)'),color:'#fff',fontFamily:'var(--mono)',fontSize:'.8rem',padding:'10px 14px',outline:'none',letterSpacing:'.04em'}} value={eventId} onChange={function(e){setEventId(e.target.value)}} placeholder="paste from circuit"/>
          <div style={{fontSize:9,color:eventId.trim()?'var(--text-dim)':'var(--signal)',marginTop:4,letterSpacing:'.06em'}}>{eventId.trim()?'CHECK-IN QR LINKED':'NO EVENT ID'}</div>
        </div>
        <div className="B-sec"><div className="B-adv-toggle" onClick={function(){setManualMode(!manualMode)}} style={{color:manualMode?'var(--signal)':'var(--text-dim)'}}>{manualMode?'\u25BC':'\u25B6'} MANUAL OVERRIDE</div>
        {manualMode&&<div>
        <div className="B-sec"><div className="B-sec-head">SHOW NAME</div>
          <input style={{width:'100%',background:'var(--surface-2)',border:'1px solid var(--border)',color:'#fff',fontFamily:"var(--display)",fontSize:'1.1rem',padding:'10px 14px',outline:'none',textTransform:'uppercase',letterSpacing:'.04em'}} value={showName} onChange={function(e){setShowName(e.target.value)}} placeholder="THE LAUNCH"/>
          <div style={{fontSize:9,color:'var(--text-dim)',marginTop:4,letterSpacing:'.06em'}}>{(showName.trim()||'THE LAUNCH').toUpperCase()} — {new Date().toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'}).toUpperCase()}</div>
        </div>
        <div className="B-sec"><div className="B-sec-head">SHOW FORMAT</div>
          <div className="B-row" style={{flexWrap:'wrap',gap:'6px'}}>{Object.keys(mergedTemplates).map(function(k){var t=mergedTemplates[k];return <button key={k} className={'B-preset'+(templateId===k?' on':'')} onClick={function(){setTemplateId(k)}}>{t.name} ({t.duration})</button>})}</div>
        </div>
        <div className="B-sec"><div className="B-sec-head">CONTENT FILTER</div>
          <div className="B-row" style={{flexWrap:'wrap'}}>{[{label:'ALL',filter:[]},{label:'MOVIES',filter:['movie']},{label:'TV',filter:['tv']},{label:'MUSIC',filter:['music']},{label:'INTERNET',filter:['internet']},{label:'SCREEN',filter:['movie','tv']}].map(function(opt){return <button key={opt.label} className={'B-preset'+(JSON.stringify(mediaFilter)===JSON.stringify(opt.filter)?' on':'')} onClick={function(){setMediaFilter(opt.filter)}}>{opt.label}</button>})}</div>
          <div style={{fontSize:9,color:eligible<templateNeeds?'var(--signal)':'var(--text-dim)',marginTop:4,letterSpacing:'.06em'}}>{eligible} AVAILABLE / {templateNeeds} NEEDED{eligible<templateNeeds?' — POOL TOO SMALL':''}</div>
        </div>
        <div className="B-sec"><div className="B-sec-head">DIFFICULTY MIX</div>
          <div className="B-row"><button className={'B-preset'+(difficulty[0]===60?' on':'')} onClick={function(){setDifficulty([60,30,10])}}>EASY</button><button className={'B-preset'+(difficulty[0]===30?' on':'')} onClick={function(){setDifficulty([30,50,20])}}>BALANCED</button><button className={'B-preset'+(difficulty[0]===15?' on':'')} onClick={function(){setDifficulty([15,35,50])}}>HARD</button></div>
          <div className="B-row" style={{alignItems:'center'}}><div className="B-label">EASY</div><input className="B-input" type="number" min="0" max="100" value={difficulty[0]} onChange={function(e){var v=parseInt(e.target.value)||0;setDifficulty([v,difficulty[1],100-v-difficulty[1]])}}/><div className="B-label">MED</div><input className="B-input" type="number" min="0" max="100" value={difficulty[1]} onChange={function(e){var v=parseInt(e.target.value)||0;setDifficulty([difficulty[0],v,100-difficulty[0]-v])}}/><div className="B-label">HARD</div><input className="B-input" type="number" min="0" max="100" value={difficulty[2]} readOnly style={{opacity:.5}}/></div>
        </div>
        <div className="B-sec"><div className="B-sec-head">BUDGET MIX</div>
          <div className="B-row"><button className={'B-preset'+(budget[0]===70?' on':'')} onClick={function(){setBudget([70,25,5])}}>SAFE</button><button className={'B-preset'+(budget[0]===55?' on':'')} onClick={function(){setBudget([55,35,10])}}>BALANCED</button><button className={'B-preset'+(budget[0]===30?' on':'')} onClick={function(){setBudget([30,55,15])}}>DEEP</button></div>
          <div className="B-row" style={{alignItems:'center'}}><div className="B-label">EVERGREEN</div><input className="B-input" type="number" min="0" max="100" value={budget[0]} onChange={function(e){var v=parseInt(e.target.value)||0;setBudget([v,budget[1],100-v-budget[1]])}}/><div className="B-label">DEEP</div><input className="B-input" type="number" min="0" max="100" value={budget[1]} onChange={function(e){var v=parseInt(e.target.value)||0;setBudget([budget[0],v,100-budget[0]-v])}}/><div className="B-label">SEASONAL</div><input className="B-input" type="number" min="0" max="100" value={budget[2]} readOnly style={{opacity:.5}}/></div>
        </div>
        <div className="B-sec"><div className="B-sec-head">QUALITY FLOOR</div>
          <div className="B-label">MIN SCORE: <b style={{color:'var(--gold)'}}>{minScore.toFixed(1)}</b></div>
          <input className="B-range" type="range" min="3.8" max="8.5" step="0.1" value={minScore} onChange={function(e){setMinScore(parseFloat(e.target.value))}}/>
          <div className="B-label" style={{color:'var(--cyan)'}}>{eligible} / {library.length} ELIGIBLE</div>
        </div>
        <div className="B-sec"><div className="B-sec-head">THEME CONTROLS</div>
          <div className="B-row"><button className="B-preset" onClick={function(){setExcludedTags([])}}>ALL ON</button><button className="B-preset" onClick={function(){setExcludedTags(ALL_TAGS.slice())}}>ALL OFF</button></div>
          <div>{ALL_TAGS.map(function(tag){return <span key={tag} className={'B-tag'+(excludedTags.indexOf(tag)<0?' on':'')} onClick={function(){toggleTag(tag)}}>{tag} <span className="cnt">{tagCounts[tag]||0}</span></span>})}</div>
        </div>
        {doaCount>0&&<div className="B-sec"><div className="B-sec-head">DEAD OR ALIVE ({doaCount})</div>
          <div className="B-row" style={{justifyContent:'space-between'}}><div className="B-label">{parseDoaTopics(doaTopics).length} TOPICS</div><button className="B-preset" onClick={function(){setDoaTopics(shuffleDoaPool(doaCount))}}>RANDOMIZE</button></div>
          <textarea className="B-input wide" rows={Math.min(doaCount+2,12)} value={doaTopics} onChange={function(e){setDoaTopics(e.target.value)}} placeholder="TOPIC:verdict" style={{resize:'vertical',textAlign:'left',fontSize:10,lineHeight:'1.6'}}/>
        </div>}
        <div className="B-sec"><div className="B-adv-toggle" onClick={function(){setShowAdvanced(!showAdvanced)}}>{showAdvanced?'\u25BC':'\u25B6'} ADVANCED</div>
          {showAdvanced&&<div><div className="B-row"><button className={'B-preset'+(curve==='ramp_up'?' on':'')} onClick={function(){setCurve('ramp_up')}}>RAMP UP</button><button className={'B-preset'+(curve==='flat'?' on':'')} onClick={function(){setCurve('flat')}}>FLAT</button><button className={'B-preset'+(curve==='ramp_down'?' on':'')} onClick={function(){setCurve('ramp_down')}}>RAMP DOWN</button></div>
            <div className="B-row" style={{alignItems:'center',marginTop:8}}><div className="B-label">DIVERSITY</div><button className={'B-preset'+(diversity?' on':'')} onClick={function(){setDiversity(!diversity)}}>{diversity?'ON':'OFF'}</button></div>
            <div className="B-row" style={{alignItems:'center',marginTop:8}}><div className="B-label" title="Cyan / pink / open indicator that alternates each non-bonus question. Turn off for formats that don't use team-side alternation.">ACTIVE SIDE</div><button className={'B-preset'+(activeSideAlternation?' on':'')} onClick={function(){setActiveSideAlternation(!activeSideAlternation)}}>{activeSideAlternation?'ON':'OFF'}</button></div>
            <div className="B-label" style={{marginTop:8}}>EXCLUDE IDS</div><textarea className="B-input wide" rows="3" value={excludedIds} onChange={function(e){setExcludedIds(e.target.value)}} style={{resize:'vertical',textAlign:'left',fontSize:10}}/>
          </div>}
        </div>
        <button className="B-gen" onClick={generate} disabled={!library||eligible<templateNeeds}>GENERATE DECK</button>
        {ros&&<button className="B-gen regen" onClick={generate}>REGENERATE</button>}
        <div className="B-sec" style={{marginTop:16,borderTop:'1px solid var(--border)',paddingTop:16}}>
          <div className="B-sec-head">AI SUGGEST (ADVANCED)</div>
          <div style={{display:'flex',gap:8,marginBottom:8}}>
            <input style={{flex:1,background:'var(--surface-2)',border:'1px solid var(--border)',color:'#fff',fontFamily:'var(--mono)',fontSize:'.75rem',padding:'8px 10px',outline:'none'}} value={aiVenue} onChange={function(e){setAiVenue(e.target.value)}} placeholder="Venue context (optional)"/>
          </div>
          <div style={{display:'flex',gap:8,marginBottom:8}}>
            <input style={{flex:1,background:'var(--surface-2)',border:'1px solid var(--border)',color:'#fff',fontFamily:'var(--mono)',fontSize:'.75rem',padding:'8px 10px',outline:'none'}} value={aiAudience} onChange={function(e){setAiAudience(e.target.value)}} placeholder="Audience profile (optional)"/>
          </div>
          <button className="B-gen" style={{background:'rgba(0,255,255,.08)',color:'var(--cyan)',border:'1px solid var(--cyan)'}} onClick={fetchAiSuggest} disabled={aiLoading}>{aiLoading?'THINKING...':'AI SUGGEST'}</button>
        </div>
        </div>}
        </div>
      </div>
      <div className="B-preview">
        {gigReasoning&&!aiSuggest&&<div style={{padding:12,background:'rgba(0,255,255,.04)',border:'1px solid var(--cyan)',marginBottom:16}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
            <span style={{fontSize:10,letterSpacing:2,color:'var(--cyan)'}}>AI REASONING</span>
            <button className="A-btn" style={{fontSize:9,padding:'2px 8px'}} onClick={function(){setGigReasoning(null)}}>✕</button>
          </div>
          <div style={{fontSize:11,color:'var(--text-sec)',lineHeight:1.5}}>{gigReasoning}</div>
        </div>}
        {aiSuggest&&<div style={{padding:16,background:'rgba(0,255,255,.04)',border:'1px solid var(--cyan)',marginBottom:16}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
            <span style={{fontSize:11,letterSpacing:2,color:'var(--cyan)'}}>AI STRATEGY</span>
            <button className="A-btn" style={{fontSize:9,padding:'2px 8px'}} onClick={function(){setAiSuggest(null)}}>✕</button>
          </div>
          {aiSuggest.strategy&&<div style={{fontSize:12,color:'#fff',marginBottom:12,lineHeight:1.5}}>{aiSuggest.strategy}</div>}
          {aiSuggest.difficulty_advice&&<div style={{fontSize:11,color:'var(--gold)',marginBottom:8}}>DIFFICULTY: {aiSuggest.difficulty_advice}</div>}
          {aiSuggest.energy_map&&<div style={{fontSize:11,color:'var(--text-sec)',marginBottom:12}}>ENERGY: {aiSuggest.energy_map}</div>}
          {aiSuggest.round_suggestions&&aiSuggest.round_suggestions.map(function(r,i){
            return <div key={i} style={{padding:'8px 0',borderTop:'1px solid rgba(0,255,255,.15)'}}>
              <div style={{fontSize:10,color:'var(--cyan)',letterSpacing:1,textTransform:'uppercase'}}>{r.round}</div>
              <div style={{fontSize:11,color:'var(--text-sec)',marginTop:2}}>{r.approach}</div>
              {r.reasoning&&<div style={{fontSize:10,color:'var(--text-dim)',marginTop:2}}>{r.reasoning}</div>}
            </div>;
          })}
          {aiSuggest.avoid_reasoning&&<div style={{marginTop:8,fontSize:10,color:'var(--signal)'}}>AVOID: {aiSuggest.avoid_reasoning}</div>}
          {aiSuggest.recommended_config&&<button className="A-btn cyan" style={{fontSize:9,padding:'3px 10px',marginTop:10}} onClick={function(){applyAiConfig(aiSuggest.recommended_config);setAiAutoGenerate(true)}}>RE-APPLY & GENERATE</button>}
        </div>}
        {!ros&&!aiSuggest?<div className="A-empty"><div className="icon">L</div><div className="msg">CONFIGURE AND GENERATE</div></div>
        :ros?<BuilderPreview ros={ros} onExport={doExport} onPush={doPush} onCopy={doCopy} pushing={pushing} aiCopyOverrides={aiCopyOverrides} onUpdateSlide={function(si,sli,field,val){setRos(function(prev){var next=JSON.parse(JSON.stringify(prev));next.sections[si].slides[sli].content[field]=val;return next})}}/>:null}
      </div>
    </div>
        )}
      </div>
    </div>
  );
}

function BuilderPreview(props){
  var ros=props.ros,onExport=props.onExport,onPush=props.onPush,onCopy=props.onCopy,pushing=props.pushing,aiOverrides=props.aiCopyOverrides,onUpdateSlide=props.onUpdateSlide;
  var[expanded,setExpanded]=useState({});
  var[editingSlide,setEditingSlide]=useState(null);
  function toggle(id){setExpanded(function(p){var n={...p};n[id]=!n[id];return n})}
  var autoTypes=['tier_title','score','round_title','fishbowl','intermission','hotseat_rules','sentence','endcard','last_line'];
  var totalSlides=0;ros.sections.forEach(function(sec){totalSlides+=sec.slides.length});
  return(
    <div>
      <div className="B-stat"><b>{totalSlides}</b> SLIDES &nbsp; <b>{ros.sections.length}</b> SECTIONS &nbsp; <span className="pk">{ros.name}</span>{ros.showAnswerSheets===false&&<span style={{color:'var(--signal)',fontSize:9,marginLeft:8}}>NO ANSWER SHEETS</span>}</div>
      {ros.sections.map(function(sec,si){
        var isOpen=expanded[sec.id]!==false;
        return(<div className="B-p-sec" key={sec.id}>
          <div className="B-p-sec-head" onClick={function(){toggle(sec.id)}}><span>{sec.name}</span><span className="ct">{sec.slides.length} {isOpen?'\u25BC':'\u25B6'}</span></div>
          {isOpen&&sec.slides.map(function(sl,i){
            var isQ=['source_q','acronym_q','hotseat_prompt','fluency_line','doa_vote','doa_ref'].indexOf(sl.type)>=0;
            var cs=sl.cardId?crowdScores[sl.cardId]:null;
            var isAuto=!isQ&&autoTypes.indexOf(sl.type)>=0;
            var hasAiOverride=aiOverrides&&aiOverrides[sl.type];
            var editKey=si+'-'+i;
            var isEditing=editingSlide===editKey;
            return(<div key={sl.id||i}>
              <div className={'B-p-slide'+(isQ?'':' struct')} onClick={function(){if(!isQ)setEditingSlide(isEditing?null:editKey)}}>
                <span className="num">{i+1}</span><span className="tp">{sl.type.replace(/_/g,' ')}</span>
                {isAuto&&<span style={{fontSize:8,letterSpacing:1,color:hasAiOverride?'var(--gold)':'var(--cyan)',marginRight:4}}>{hasAiOverride?'AI':'AUTO'}</span>}
                <span className="ln">{sl.content.primary||''}</span>
                {sl.content.answer&&<span style={{color:'var(--text-dim)',fontSize:9,minWidth:80,textAlign:'right'}}>{sl.content.answer}</span>}
                {sl.cardId&&(cs?<span className={'crowd '+(cs.crowd_score>=5?'hot':'cold')}>{Math.round(cs.crowd_score*10)}%{cs.showCount>1?' \u00D7'+cs.showCount:''}</span>:<span className="crowd new">NEW</span>)}
              </div>
              {isEditing&&onUpdateSlide&&<div style={{padding:'8px 12px 8px 32px',background:'rgba(0,255,255,.03)',borderBottom:'1px solid var(--border)'}}>
                <input className="A-input" style={{marginBottom:4,fontSize:10}} value={sl.content.primary||''} onChange={function(e){onUpdateSlide(si,i,'primary',e.target.value)}} placeholder="primary"/>
                <input className="A-input" style={{fontSize:10}} value={sl.content.secondary||''} onChange={function(e){onUpdateSlide(si,i,'secondary',e.target.value)}} placeholder="secondary"/>
              </div>}
            </div>);
          })}
        </div>);
      })}
      <div className="B-export"><button onClick={onExport}>DOWNLOAD JSON</button><button className="push" onClick={onPush} disabled={pushing}>{pushing?'PUSHING...':'PUSH TO FIREBASE'}</button><button onClick={onCopy}>COPY</button></div>
    </div>
  );
}

function TemplateEditor(props) {
  var db = props.db, showToast = props.showToast, templates = props.templates;
  var [selectedId, setSelectedId] = useState(null);
  var [editTemplate, setEditTemplate] = useState(null);
  var [editSection, setEditSection] = useState(null);
  var [seeding, setSeeding] = useState(false);

  var [isMobile, setIsMobile] = useState(window.innerWidth < 768);
  useEffect(function(){
    function onResize(){setIsMobile(window.innerWidth < 768)}
    window.addEventListener('resize', onResize);
    return function(){window.removeEventListener('resize', onResize)};
  },[]);

  function deselect() { setSelectedId(null); setEditTemplate(null); setEditSection(null); }

  var allTemplates = useMemo(function() {
    return Object.assign({}, TEMPLATES, templates || {});
  }, [templates]);

  function selectTemplate(id) {
    setSelectedId(id);
    setEditTemplate(JSON.parse(JSON.stringify(allTemplates[id])));
    setEditSection(null);
  }

  function seedTemplates() {
    setSeeding(true);
    var updates = {};
    Object.keys(TEMPLATES).forEach(function(k) {
      updates['templates/' + k] = JSON.parse(JSON.stringify(TEMPLATES[k]));
    });
    db.ref().update(updates).then(function() {
      setSeeding(false);
      showToast('TEMPLATES SEEDED');
    }).catch(function(e) { setSeeding(false); showToast('SEED FAILED: ' + e.message); });
  }

  function duplicateTemplate() {
    if (!editTemplate) return;
    var newId = editTemplate.id + '-copy-' + Date.now();
    var copy = JSON.parse(JSON.stringify(editTemplate));
    copy.id = newId;
    copy.name = copy.name + ' COPY';
    db.ref('templates/' + newId).set(copy).then(function() {
      showToast('DUPLICATED');
      setSelectedId(newId);
      setEditTemplate(copy);
    }).catch(function(e) { showToast('DUPLICATE FAILED: ' + e.message); });
  }

  function saveTemplate() {
    if (!editTemplate || !editTemplate.id) return;
    db.ref('templates/' + editTemplate.id).set(editTemplate).then(function() {
      showToast('TEMPLATE SAVED');
    }).catch(function(e) { showToast('SAVE FAILED: ' + e.message); });
  }

  function updateTemplateName(val) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.name = val;
      return next;
    });
  }

  function updateTemplateDuration(val) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.duration = val;
      return next;
    });
  }

  function updateTemplateFormat(val) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      if (val) { next.format = val; } else { delete next.format; }
      return next;
    });
  }

  function moveSection(idx, dir) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var sections = next.sections;
      var newIdx = idx + dir;
      if (newIdx < 0 || newIdx >= sections.length) return next;
      var temp = sections[idx];
      sections[idx] = sections[newIdx];
      sections[newIdx] = temp;
      return next;
    });
  }

  function removeSection(idx) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.sections.splice(idx, 1);
      if (editSection === idx) setEditSection(null);
      return next;
    });
  }

  function toggleSectionDisabled(idx) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.sections[idx].disabled = !next.sections[idx].disabled;
      return next;
    });
  }

  function addSection() {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var useAuto = !!next.format;
      var newSec = {
        id: 'section-' + Date.now(),
        name: 'NEW SECTION',
        fixed: [
          {type:'round_title',content:{primary:'NEW SECTION',secondary:''}},
          useAuto ? {type:'tier_title',copy:'auto'} : {type:'tier_title',content:{primary:'',secondary:''}}
        ],
        slots: {mode:'source_q',count:10,timer:30,bonusAt:{}},
        closing: [useAuto ? {type:'score',copy:'auto'} : {type:'score',content:{primary:'',secondary:''}}]
      };
      next.sections.push(newSec);
      return next;
    });
  }

  function updateSectionField(secIdx, field, value) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var sec = next.sections[secIdx];
      if (field === 'name') { sec.name = value; sec.id = value.toLowerCase().replace(/[^a-z0-9]+/g, '-'); }
      else if (field === 'mode') { if (!sec.slots) sec.slots = {}; sec.slots.mode = value; }
      else if (field === 'count') { if (!sec.slots) sec.slots = {}; sec.slots.count = parseInt(value) || 0; }
      else if (field === 'timer') { if (!sec.slots) sec.slots = {}; sec.slots.timer = value ? parseInt(value) : null; }
      return next;
    });
  }

  function updateSlideContent(secIdx, arrayName, slideIdx, field, value) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var arr = next.sections[secIdx][arrayName];
      if (!arr || !arr[slideIdx]) return next;
      if (!arr[slideIdx].content) arr[slideIdx].content = {primary:'',secondary:''};
      arr[slideIdx].content[field] = value;
      return next;
    });
  }

  function toggleSlideAuto(secIdx, arrayName, slideIdx) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var arr = next.sections[secIdx][arrayName];
      if (!arr || !arr[slideIdx]) return next;
      var sl = arr[slideIdx];
      if (sl.copy === 'auto') {
        delete sl.copy;
        var fmt = next.format;
        if (fmt && FORMAT_RULES[fmt]) {
          var resolved = resolveCopy(sl, {format:fmt,mode:next.sections[secIdx].slots&&next.sections[secIdx].slots.mode||null,mediaFilter:null,slotCount:0,isLastScoredRound:false,secName:next.sections[secIdx].name});
          sl.content = resolved;
        } else if (!sl.content) {
          sl.content = {primary:'',secondary:''};
        }
      } else {
        sl.copy = 'auto';
      }
      return next;
    });
  }

  function addSlide(secIdx, arrayName) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      if (!next.sections[secIdx][arrayName]) next.sections[secIdx][arrayName] = [];
      var useAuto = !!next.format;
      next.sections[secIdx][arrayName].push(useAuto ? {type:'transition_beat',copy:'auto'} : {type:'transition_beat',content:{primary:'',secondary:''}});
      return next;
    });
  }

  function removeSlide(secIdx, arrayName, slideIdx) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.sections[secIdx][arrayName].splice(slideIdx, 1);
      return next;
    });
  }

  function moveSlide(secIdx, arrayName, slideIdx, dir) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      var arr = next.sections[secIdx][arrayName];
      var newIdx = slideIdx + dir;
      if (newIdx < 0 || newIdx >= arr.length) return next;
      var temp = arr[slideIdx];
      arr[slideIdx] = arr[newIdx];
      arr[newIdx] = temp;
      return next;
    });
  }

  function updateSlideType(secIdx, arrayName, slideIdx, newType) {
    setEditTemplate(function(prev) {
      var next = JSON.parse(JSON.stringify(prev));
      next.sections[secIdx][arrayName][slideIdx].type = newType;
      return next;
    });
  }

  var SLIDE_TYPES = ['round_title','tier_title','score','fishbowl','intermission','hotseat_rules','sentence','endcard','last_line','verdict','receipt_rain','crate_drop','transition_beat','warning','blackout','attract','checkin'];

  function renderSlideList(secIdx, arrayName, slides) {
    if (!slides || slides.length === 0) return null;
    var fmt = editTemplate.format;
    var sec = editTemplate.sections[secIdx];
    return slides.map(function(sl, si) {
      var isAuto = sl.copy === 'auto';
      var resolved = null;
      if (isAuto && fmt && FORMAT_RULES[fmt]) {
        resolved = resolveCopy(sl, {format:fmt,mode:sec.slots&&sec.slots.mode||null,mediaFilter:sec.slots&&sec.slots.mediaFilter||null,slotCount:sec.slots&&sec.slots.count||0,isLastScoredRound:false,secName:sec.name});
      }
      return (
        <div key={arrayName+'-'+si} style={{padding:'6px 8px',borderBottom:'1px solid #1a1a1a',display:'flex',flexDirection:'column',gap:4}}>
          <div style={{display:'flex',alignItems:'center',gap:6}}>
            <select className="A-select" style={{width:120,fontSize:9,padding:'2px 4px'}} value={sl.type} onChange={function(e){updateSlideType(secIdx,arrayName,si,e.target.value)}}>
              {SLIDE_TYPES.map(function(t){return <option key={t} value={t}>{t.replace(/_/g,' ').toUpperCase()}</option>})}
            </select>
            {isAuto && <span style={{fontSize:8,letterSpacing:1,color:'var(--cyan)',padding:'1px 4px',border:'1px solid var(--cyan)'}}>AUTO</span>}
            {isAuto && resolved && <span style={{fontSize:9,color:'var(--text-dim)',flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{resolved.primary} — {resolved.secondary}</span>}
            <button className="A-btn" style={{fontSize:8,padding:'1px 4px'}} onClick={function(){toggleSlideAuto(secIdx,arrayName,si)}}>{isAuto?'OVERRIDE':'SET AUTO'}</button>
            <button className="A-btn" style={{fontSize:8,padding:'1px 4px'}} onClick={function(){moveSlide(secIdx,arrayName,si,-1)}} disabled={si===0}>▲</button>
            <button className="A-btn" style={{fontSize:8,padding:'1px 4px'}} onClick={function(){moveSlide(secIdx,arrayName,si,1)}} disabled={si===slides.length-1}>▼</button>
            <button className="A-btn danger" style={{fontSize:8,padding:'1px 4px'}} onClick={function(){removeSlide(secIdx,arrayName,si)}}>✕</button>
          </div>
          {!isAuto && <div style={{display:'flex',gap:4}}>
            <input className="A-input" style={{flex:1,fontSize:9}} value={sl.content&&sl.content.primary||''} onChange={function(e){updateSlideContent(secIdx,arrayName,si,'primary',e.target.value)}} placeholder="primary"/>
            <input className="A-input" style={{flex:1,fontSize:9}} value={sl.content&&sl.content.secondary||''} onChange={function(e){updateSlideContent(secIdx,arrayName,si,'secondary',e.target.value)}} placeholder="secondary"/>
          </div>}
        </div>
      );
    });
  }

  var templateKeys = Object.keys(allTemplates);

  return (
    <div style={{display:'flex',flexDirection:isMobile?'column':'row',height:'100%',overflow:'hidden'}}>
      {/* Left: Template list */}
      <div style={{width:isMobile?'100%':280,flexShrink:0,borderRight:isMobile?'none':'1px solid var(--border)',display:isMobile&&selectedId?'none':'flex',flexDirection:'column',overflow:'hidden'}}>
        <div style={{padding:'12px 16px',borderBottom:'1px solid var(--border)',background:'var(--surface)'}}>
          <div style={{fontSize:10,letterSpacing:2,color:'var(--signal)',marginBottom:8}}>TEMPLATES ({templateKeys.length})</div>
          <button className="A-btn signal" style={{fontSize:9,padding:'4px 10px',width:'100%',marginBottom:6}} onClick={seedTemplates} disabled={seeding}>{seeding ? 'SEEDING...' : 'SEED TEMPLATES'}</button>
        </div>
        <div style={{flex:1,overflow:'auto'}}>
          {templateKeys.map(function(k) {
            var t = allTemplates[k];
            var sel = selectedId === k;
            var isCustom = templates && templates[k];
            return (
              <div key={k} onClick={function(){selectTemplate(k)}} style={{
                padding:'10px 16px',borderBottom:'1px solid #1a1a1a',cursor:'pointer',
                background:sel?'rgba(255,0,127,.05)':'transparent',
                borderLeft:sel?'2px solid var(--signal)':'2px solid transparent'
              }}>
                <div style={{fontSize:11,color:sel?'var(--signal)':'#e0e0e0',letterSpacing:1}}>{t.name}</div>
                <div style={{fontSize:9,color:'var(--text-dim)',marginTop:2}}>{t.duration} · {t.sections?t.sections.length:0} sections {isCustom?'· CUSTOM':''}</div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Right: Template editor */}
      <div style={{flex:1,overflow:'auto',padding:isMobile?16:24,display:isMobile&&!selectedId?'none':'block'}}>
        {!editTemplate && <div className="A-empty"><div className="icon">L</div><div className="msg">SELECT A TEMPLATE</div></div>}
        {editTemplate && (
          <div>
            {isMobile && <button className="A-btn" style={{marginBottom:12}} onClick={deselect}>← BACK</button>}
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:16}}>
              <span style={{fontSize:14,letterSpacing:2,color:'var(--signal)',textTransform:'uppercase'}}>{editTemplate.name}</span>
              <div style={{display:'flex',gap:8}}>
                <button className="A-btn" style={{fontSize:9,padding:'4px 10px'}} onClick={duplicateTemplate}>DUPLICATE</button>
                <button className="A-btn cyan" style={{fontSize:9,padding:'4px 10px'}} onClick={saveTemplate}>SAVE</button>
              </div>
            </div>

            <div style={{display:'flex',gap:12,marginBottom:20}}>
              <div style={{flex:1}}><label className="A-label">Name</label><input className="A-input" value={editTemplate.name||''} onChange={function(e){updateTemplateName(e.target.value)}} /></div>
              <div style={{width:120}}><label className="A-label">Duration</label><input className="A-input" value={editTemplate.duration||''} onChange={function(e){updateTemplateDuration(e.target.value)}} /></div>
              <div style={{width:140}}><label className="A-label">Format</label><select className="A-select" value={editTemplate.format||''} onChange={function(e){updateTemplateFormat(e.target.value)}}><option value="">None</option><option value="standard">Standard</option><option value="scorecard">Scorecard</option><option value="tribunal">Tribunal</option><option value="faceoff">Face-Off</option><option value="draft">Draft</option><option value="split">Split</option><option value="purge">Purge</option></select></div>
            </div>

            <div style={{marginBottom:16}}>
              <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
                <span style={{fontSize:10,letterSpacing:2,color:'var(--text-sec)'}}>SECTIONS ({editTemplate.sections.filter(function(s){return !s.disabled}).length}/{editTemplate.sections.length})</span>
                <button className="A-btn" style={{fontSize:9,padding:'4px 10px'}} onClick={addSection}>+ ADD SECTION</button>
              </div>
              {editTemplate.sections.map(function(sec, idx) {
                var isEditing = editSection === idx;
                var isDisabled = !!sec.disabled;
                return (
                  <div key={sec.id || idx}>
                    <div style={{
                      display:'flex',alignItems:'center',gap:8,padding:'8px 12px',
                      borderBottom:'1px solid #1a1a1a',cursor:'pointer',
                      background:isEditing?'rgba(0,255,255,.05)':'transparent',
                      opacity:isDisabled?0.4:1
                    }} onClick={function(){setEditSection(isEditing?null:idx)}}>
                      <span style={{fontSize:10,color:'var(--text-dim)',minWidth:20}}>{idx+1}</span>
                      <span style={{flex:1,fontSize:11,color:isEditing?'var(--cyan)':'#e0e0e0',letterSpacing:1}}>{sec.name}</span>
                      {isDisabled && <span style={{fontSize:8,letterSpacing:1,color:'var(--signal)',padding:'1px 4px',border:'1px solid var(--signal)'}}>DISABLED</span>}
                      {sec.slots && <span style={{fontSize:9,color:'var(--text-dim)'}}>{sec.slots.mode} · {sec.slots.count} · {sec.slots.timer||'—'}s</span>}
                      <button className="A-btn" style={{fontSize:9,padding:'2px 6px'}} onClick={function(e){e.stopPropagation();toggleSectionDisabled(idx)}}>{isDisabled?'ENABLE':'SKIP'}</button>
                      <button className="A-btn" style={{fontSize:9,padding:'2px 6px'}} onClick={function(e){e.stopPropagation();moveSection(idx,-1)}} disabled={idx===0}>▲</button>
                      <button className="A-btn" style={{fontSize:9,padding:'2px 6px'}} onClick={function(e){e.stopPropagation();moveSection(idx,1)}} disabled={idx===editTemplate.sections.length-1}>▼</button>
                      <button className="A-btn danger" style={{fontSize:9,padding:'2px 6px'}} onClick={function(e){e.stopPropagation();removeSection(idx)}}>✕</button>
                    </div>
                    {isEditing && (
                      <div style={{padding:'12px 16px',background:'var(--surface-2)',borderBottom:'1px solid var(--border)'}}>
                        {isDisabled && <div style={{padding:'6px 10px',marginBottom:12,background:'rgba(255,0,127,.06)',border:'1px solid var(--signal)',fontSize:10,color:'var(--signal)',letterSpacing:1}}>THIS SECTION IS DISABLED — IT WILL BE SKIPPED DURING DECK GENERATION</div>}
                        <div style={{display:'grid',gridTemplateColumns:isMobile?'1fr':'1fr 1fr',gap:12}}>
                          <div><label className="A-label">Section Name</label><input className="A-input" value={sec.name||''} onChange={function(e){updateSectionField(idx,'name',e.target.value)}} /></div>
                          <div><label className="A-label">Mode</label>
                            <select className="A-select" value={sec.slots&&sec.slots.mode||'source_q'} onChange={function(e){updateSectionField(idx,'mode',e.target.value)}}>
                              <option value="source_q">source_q</option>
                              <option value="acronym_q">acronym_q</option>
                              <option value="hotseat_prompt">hotseat_prompt</option>
                              <option value="fluency_pair">fluency_pair</option>
                              <option value="doa_triplet">doa_triplet</option>
                            </select>
                          </div>
                          <div><label className="A-label">Count</label><input className="A-input" type="number" min="1" max="50" value={sec.slots&&sec.slots.count||10} onChange={function(e){updateSectionField(idx,'count',e.target.value)}} /></div>
                          <div><label className="A-label">Timer (seconds)</label><input className="A-input" type="number" min="0" max="120" value={sec.slots&&sec.slots.timer||''} onChange={function(e){updateSectionField(idx,'timer',e.target.value)}} placeholder="none" /></div>
                        </div>
                        {sec.fixed && sec.fixed.length > 0 && <div style={{marginTop:12}}>
                          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4}}>
                            <span style={{fontSize:9,letterSpacing:1,color:'var(--text-sec)'}}>FIXED SLIDES ({sec.fixed.length})</span>
                            <button className="A-btn" style={{fontSize:8,padding:'2px 6px'}} onClick={function(){addSlide(idx,'fixed')}}>+ ADD</button>
                          </div>
                          {renderSlideList(idx, 'fixed', sec.fixed)}
                        </div>}
                        {sec.closing && sec.closing.length > 0 && <div style={{marginTop:12}}>
                          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:4}}>
                            <span style={{fontSize:9,letterSpacing:1,color:'var(--text-sec)'}}>CLOSING SLIDES ({sec.closing.length})</span>
                            <button className="A-btn" style={{fontSize:8,padding:'2px 6px'}} onClick={function(){addSlide(idx,'closing')}}>+ ADD</button>
                          </div>
                          {renderSlideList(idx, 'closing', sec.closing)}
                        </div>}
                        {!sec.fixed && !sec.closing && <div style={{marginTop:8}}>
                          <button className="A-btn" style={{fontSize:8,padding:'2px 6px'}} onClick={function(){addSlide(idx,'fixed')}}>+ ADD FIXED SLIDE</button>
                          <button className="A-btn" style={{fontSize:8,padding:'2px 6px',marginLeft:4}} onClick={function(){addSlide(idx,'closing')}}>+ ADD CLOSING SLIDE</button>
                        </div>}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
