/* ============================================================
   Magic — Magic View
   Extracted from components.jsx via split.py.
   Each component reaches React hooks via window.React.
   ============================================================ */
const { useState, useEffect, useRef, useMemo, useCallback, Fragment } = React;


/* ============================================================
   Phase 5a — Magic (Tradition + Spells)
   ============================================================ */

/* ------------------------------------------------------------
   MagicView — main magic tab. Shows:
   1. Mundane disclaimer if character isn't awakened
   2. Tradition tiles (pick one)
   3. Magic summary ribbon (Magic, Drain pool, Spellcasting pool)
   4. Drain calculator for any-force previews
   5. Spell list with categories, filters, picker
   ------------------------------------------------------------ */
window.MagicView = function MagicView({ character, onChange }) {
    const C = SR5_CALC;
    const M = SR5_MAGIC;
    const [previewForce, setPreviewForce] = useState(5);
    const [pickerOpen, setPickerOpen] = useState(false);
    const [customOpen, setCustomOpen] = useState(false);
    const [categoryFilter, setCategoryFilter] = useState('all');

    const magicRow = C.priorityRow(character, 'magic');
    const magicType = magicRow?.type;
    const isAwakened = magicType && magicType !== 'mundane';
    const castsSpells = magicType === 'magician' || magicType === 'mystic_adept';

    if (!isAwakened) {
        return (
            <>
                <ContentHeader title="06 / Magic" heading="Magical capabilities" />
                <div className="card"><div className="card-body muted">
                    This character is <strong>mundane</strong> — no magic or resonance. Set the <strong className="accent">Magic</strong> priority to anything other than E to awaken the character.
                </div></div>
            </>
        );
    }

    if (magicType === 'technomancer') {
        return (
            <>
                <ContentHeader title="06 / Magic" heading="Technomancer — Complex Forms" />
                <ComplexFormsSection character={character} onChange={onChange} />
            </>
        );
    }

    if (magicType === 'adept') {
        return (
            <>
                <ContentHeader title="06 / Magic" heading="Adept" />
                <AdeptPowersSection character={character} onChange={onChange} />
                <MagicalEquipmentSection character={character} onChange={onChange} />
            </>
        );
    }

    /* At this point: magician or mystic adept */
    const tradKey = C.characterTradition(character);
    const tradition = tradKey ? M.findTradition(tradKey) : null;

    /* Tradition picker */
    if (!tradition) {
        return (
            <>
                <ContentHeader title="06 / Magic" heading="Choose a tradition" />
                <p className="muted" style={{ marginBottom: 20, fontFamily: 'var(--font-sans)' }}>
                    Your tradition determines which attribute (alongside Willpower) you use to resist Drain,
                    and which spirit types you can summon. Choose carefully — it can't be changed later.
                </p>
                <div className="tradition-grid">
                    {M.TRADITIONS.map(t => (
                        <div key={t.key} className="tradition-tile"
                             onClick={() => onChange(C.setTradition(character, t.key))}>
                            <div className="tt-name">{t.name}</div>
                            <div className="tt-drain">Drain: Willpower + {t.drainAttr.toUpperCase()}</div>
                            <div className="tt-desc">{t.description}</div>
                            <div className="tt-spirits">
                                <span className="tt-spirit">C: {t.combatSpirit}</span>
                                <span className="tt-spirit">D: {t.detectSpirit}</span>
                                <span className="tt-spirit">H: {t.healthSpirit}</span>
                                <span className="tt-spirit">I: {t.illusionSpirit}</span>
                                <span className="tt-spirit">M: {t.manipSpirit}</span>
                            </div>
                        </div>
                    ))}
                </div>
            </>
        );
    }

    /* Main magic view */
    const magic = C.specialAttributeValue(character, 'mag');
    const drainPool = C.drainDicePool(character);
    const spellcastPool = C.spellcastingDicePool(character);
    const slotsTotal = C.spellSlotsTotal(character);
    const slotsUsed = C.spellSlotsUsed(character);
    const slotsRemaining = C.spellSlotsRemaining(character);
    const spells = C.characterSpells(character);

    /* Group by category for filtering and display */
    const spellsByCategory = {};
    spells.forEach(s => {
        (spellsByCategory[s.category] ??= []).push(s);
    });

    const displayedSpells = categoryFilter === 'all' ? spells : (spellsByCategory[categoryFilter] || []);

    const inCareer = C.isCareerMode(character);
    const karmaAvailable = inCareer ? C.karmaCurrent(character) : 0;
    const pendingDelta = inCareer ? C.pendingKarmaDelta(character) : 0;
    const karmaRemaining = inCareer ? karmaAvailable - pendingDelta : 0;
    const pendingCh = inCareer ? C.pendingChanges(character) : {};
    const pendingNewSpells = pendingCh.newSpells || [];

    function handleChangeTradition() {
        if (confirm("Change tradition? Your existing spells stay, but drain resistance will recalculate with the new attribute.")) {
            onChange(C.setTradition(character, null));
        }
    }

    function handlePick(template) {
        if (inCareer) {
            if (pendingDelta + C.KARMA_COST_NEW_SPELL > karmaAvailable) {
                alert(`Not enough karma: a new spell costs ${C.KARMA_COST_NEW_SPELL} karma, you have ${karmaRemaining} remaining.`);
                return;
            }
            onChange(C.stagePendingNewSpell(character, template));
            setPickerOpen(false);
        } else {
            /* Phase 17 — creation: allow spells past the free-priority count,
               each additional costs 5 karma from the leftover pool. Warn at
               the boundary and block if karma is insufficient. */
            const usedNow = C.spellSlotsUsed(character);
            const free = C.spellSlotsTotal(character);
            const willExceed = usedNow >= free;
            if (willExceed) {
                const kt = C.karmaTotals(character);
                if (kt.remaining < 5) {
                    alert(`Free spell slots exhausted. An additional spell costs 5 karma, but you only have ${kt.remaining} remaining.`);
                    return;
                }
                if (usedNow === free) {
                    /* First over-cap spell — warn once. Subsequent adds go silent. */
                    if (!confirm(`Your ${free} priority-free spell slots are full. Additional spells cost 5 karma each from your leftover karma pool. Add this spell for 5 karma?`)) {
                        return;
                    }
                }
            }
            onChange(C.addSpellFromTemplate(character, template));
            setPickerOpen(false);
        }
    }

    function handleRemove(id) {
        if (inCareer) {
            alert("Spells can't be unlearned in career mode.");
            return;
        }
        onChange(C.removeSpell(character, id));
    }

    function handleUnstagePendingSpell(tempId) {
        onChange(C.unstagePendingNewSpell(character, tempId));
    }

    function handleUpdateNotes(id, notes) {
        if (inCareer) return;
        onChange(C.updateSpell(character, id, { notes }));
    }

    function renderPendingSpellRow(ps) {
        return (
            <div className={`spell-row ${ps.category} pending-new`} key={ps.tempId}>
                <div className="sp-name">
                    {ps.name}
                    <span style={{ marginLeft: 6, fontSize: 10, letterSpacing: '0.1em', color: 'var(--accent)' }}>
                        — pending ({ps.karmaCost} karma)
                    </span>
                </div>
                <div className="sp-stat muted" style={{ gridColumn: 'span 6' }}>new — commits with pending changes</div>
                <button className="knowledge-remove-btn"
                        onClick={() => handleUnstagePendingSpell(ps.tempId)}
                        title="Discard this pending spell">×</button>
            </div>
        );
    }

    function renderSpellRow(s) {
        const tpl = s.key ? M.findSpell(s.key) : null;
        const stats = tpl || s;
        const dv = C.computeDrainValue(stats.drain, previewForce);
        return (
            <div className={`spell-row ${s.category}`} key={s.id}>
                <div className="sp-name">
                    {s.name}
                    {stats.description && <span className="sp-desc">{stats.description}</span>}
                </div>
                <div className={`sp-stat type-${stats.type}`}>{M.TYPE_LABEL[stats.type] || stats.type}</div>
                <div className="sp-stat">{stats.range}</div>
                <div className="sp-stat">{M.DAMAGE_LABEL[stats.damage] || stats.damage}</div>
                <div className="sp-stat">{M.DURATION_LABEL[stats.duration] || stats.duration}</div>
                <div className="sp-stat drain">{stats.drain}</div>
                <div className="sp-stat">DV {dv}</div>
                <button className="knowledge-remove-btn"
                        onClick={() => { if (confirm(`Remove ${s.name}?`)) handleRemove(s.id); }}
                        title="Remove">×</button>
            </div>
        );
    }

    const slotsState = slotsRemaining < 0 ? 'over' : (slotsRemaining === 0 ? 'done' : 'free');

    return (
        <>
            <ContentHeader title="06 / Magic" heading={`${tradition.name} ${magicType === 'mystic_adept' ? 'Mystic Adept' : 'Magician'}`} />

            {/* Tradition summary bar with small "change" link */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 18,
                          padding: '10px 16px', background: 'var(--bg-surface)',
                          border: '1px solid var(--accent-dim)', borderLeft: '2px solid var(--accent)',
                          borderRadius: 2 }}>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, letterSpacing: '0.1em',
                               color: 'var(--text-muted)', textTransform: 'uppercase' }}>
                    Tradition
                </span>
                <span style={{ fontWeight: 600, color: 'var(--accent-bright)' }}>{tradition.name}</span>
                <span className="muted mono" style={{ fontSize: 11 }}>
                    · WIL + {tradition.drainAttr.toUpperCase()} for drain
                    · spirits: {tradition.combatSpirit}, {tradition.detectSpirit}, {tradition.healthSpirit}, {tradition.illusionSpirit}, {tradition.manipSpirit}
                </span>
                <button className="add-button" style={{ marginLeft: 'auto', fontSize: 10, padding: '3px 10px' }}
                        onClick={handleChangeTradition}
                        disabled={inCareer}
                        title={inCareer ? "Tradition locked in career mode" : ''}>
                    change
                </button>
            </div>

            {C.isAspectedMagician(character) && (
                <AspectedGroupPicker character={character} onChange={onChange} inCareer={inCareer} />
            )}

            {/* Stats ribbon */}
            <div className="magic-summary">
                <div className="magic-stat">
                    <span className="m-label">Magic Attribute</span>
                    <span className="m-value accent">{magic}</span>
                    <span className="m-hint">capped by essence ({C.currentEssence(character).toFixed(2)})</span>
                </div>
                <div className="magic-stat">
                    <span className="m-label">Drain Resistance</span>
                    <span className="m-value">{drainPool}</span>
                    <span className="m-hint">WIL {C.attributeValue(character, 'wil')} + {tradition.drainAttr.toUpperCase()} {C.attributeValue(character, tradition.drainAttr)}</span>
                </div>
                <div className="magic-stat">
                    <span className="m-label">Spellcasting Pool</span>
                    <span className="m-value">{spellcastPool}</span>
                    <span className="m-hint">skill + Magic (+2 spec)</span>
                </div>
            </div>

            {/* Drain calculator */}
            <div className="drain-preview">
                <div>
                    <div className="dp-label">Preview at Force</div>
                </div>
                <div className="dp-force-input">
                    <button className="stepper-btn" style={{ width: 22, height: 22 }}
                            onClick={() => setPreviewForce(Math.max(1, previewForce - 1))}
                            disabled={previewForce <= 1}>−</button>
                    <input type="number" min="1" max="12" value={previewForce}
                           onChange={e => setPreviewForce(Math.max(1, Math.min(12, parseInt(e.target.value, 10) || 1)))} />
                    <button className="stepper-btn" style={{ width: 22, height: 22 }}
                            onClick={() => setPreviewForce(Math.min(12, previewForce + 1))}>+</button>
                </div>
                <div className="dp-hint">
                    DV column reflects this Force. Most casters pick Force = Magic attribute ({magic}); higher Force = more dice but more drain.
                </div>
            </div>

            {/* Mystic Adept split slider — only for mystic adepts.
                In career mode, the slider is read-only; use the Buy Power Point
                button to increase the split via staged karma buys. */}
            {magicType === 'mystic_adept' && (() => {
                const split = C.mysticAdeptSplit(character);
                const pendingPP = pendingCh.newPowerPoints || 0;
                const ppShown = inCareer ? (split + pendingPP) : split;
                const slotsShown = (magic - (inCareer ? (split + pendingPP) : split)) * 2;
                const canBuyMore = ppShown < magic;
                return (
                    <div className="ma-split">
                        <div className="ma-split-header">
                            <span className="ma-label">Mystic Adept Split</span>
                            <span className="ma-values">
                                <strong className={pendingPP > 0 ? 'accent' : ''}>{ppShown}</strong> Power Points
                                {pendingPP > 0 && <span style={{ fontSize: 11, color: 'var(--accent)', marginLeft: 4 }}>(+{pendingPP} pending)</span>}
                                 · <strong>{slotsShown}</strong> Spell Slots
                            </span>
                        </div>
                        {inCareer ? (
                            <div className="ma-slider-row" style={{ alignItems: 'center' }}>
                                <span className="muted mono" style={{ fontSize: 11 }}>
                                    Split locked in career mode. Buy more Power Points below.
                                </span>
                                <button className="add-button"
                                        style={{ marginLeft: 'auto', fontSize: 11 }}
                                        disabled={!canBuyMore || pendingDelta + C.KARMA_COST_POWER_POINT > karmaAvailable}
                                        onClick={() => onChange(C.stagePendingBuyPowerPoint(character))}
                                        title={!canBuyMore ? `Already at Magic cap (${magic})` :
                                               pendingDelta + C.KARMA_COST_POWER_POINT > karmaAvailable ? 'Not enough karma' :
                                               `Buy 1 Power Point (5 karma)`}>
                                    <span className="plus-icon">+</span>Buy PP (5 karma)
                                </button>
                                {pendingPP > 0 && (
                                    <button className="add-button"
                                            style={{ marginLeft: 6, fontSize: 11 }}
                                            onClick={() => onChange(C.unstagePendingBuyPowerPoint(character))}
                                            title="Undo last pending PP buy">
                                        ↶ Undo
                                    </button>
                                )}
                            </div>
                        ) : (() => {
                            /* Creation-mode: mystic adept PP is 5 karma each. The slider
                               shows the split, and the hint text calls out the karma cost. */
                            const ppKarmaCost = split * 5;
                            const { remaining: karmaAfterMA } = C.karmaTotals(character);
                            const overBudget = karmaAfterMA < 0;
                            return (
                                <>
                                    <div className="ma-slider-row">
                                        <span className="muted mono" style={{ fontSize: 11, minWidth: 80 }}>all spells</span>
                                        <input
                                            type="range" min="0" max={magic} step="1" value={split}
                                            className="ma-slider"
                                            onChange={e => onChange(C.setMysticAdeptSplit(character, parseInt(e.target.value, 10)))}
                                        />
                                        <span className="muted mono" style={{ fontSize: 11, minWidth: 80, textAlign: 'right' }}>all powers</span>
                                        <input
                                            type="number" min="0" max={magic} value={split}
                                            className="ma-slider-num"
                                            onChange={e => onChange(C.setMysticAdeptSplit(character, parseInt(e.target.value, 10) || 0))}
                                        />
                                    </div>
                                    <div style={{ marginTop: 10, padding: '8px 12px',
                                                  background: overBudget ? 'rgba(227, 93, 106, 0.10)' : 'var(--accent-glow)',
                                                  border: `1px solid ${overBudget ? 'var(--bad)' : 'var(--accent-dim)'}`,
                                                  borderRadius: 3,
                                                  display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                                                  fontFamily: 'var(--font-mono)', fontSize: 12 }}>
                                        <span>
                                            <strong style={{ color: overBudget ? 'var(--bad)' : 'var(--accent-bright)', fontSize: 14 }}>
                                                {ppKarmaCost} karma
                                            </strong>
                                            <span className="muted" style={{ marginLeft: 8 }}>
                                                for {split} Power Point{split === 1 ? '' : 's'} (5 karma each)
                                            </span>
                                        </span>
                                        <span style={{ color: overBudget ? 'var(--bad)' : 'var(--text-secondary)' }}>
                                            {overBudget ? `⚠ over by ${Math.abs(karmaAfterMA)}` : `${karmaAfterMA} karma left`}
                                        </span>
                                    </div>
                                </>
                            );
                        })()}
                        <div style={{ marginTop: 8, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-faint)' }}>
                            {inCareer
                                ? `Mystic adepts spend 5 karma per Power Point, capped at Magic (${magic}). Each PP takes 2 spell slots with it.`
                                : `At creation, mystic adept Power Points cost 5 karma each (RAW, CRB p. 69). Each PP allocated removes 2 spell slots from your Magic (${magic}) pool. Cap at Magic.`}
                        </div>
                    </div>
                );
            })()}

            {/* Spell slots summary */}
            <div className="card" style={{ marginBottom: 14 }}>
                <div className="card-header">
                    <h3 className="card-title">Spells Known</h3>
                    <div className="card-header-right">
                        <div className={`nuyen-stat remaining ${slotsState}`} style={{ padding: 0, background: 'transparent', border: 'none' }}>
                            <div style={{ display: 'flex', gap: 12, alignItems: 'baseline' }}>
                                <span className="muted mono" style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase' }}>
                                    slots
                                </span>
                                <span className="n-value" style={{ fontSize: 18 }}>
                                    {slotsUsed}<span className="muted">/{slotsTotal}</span>
                                </span>
                            </div>
                        </div>
                    </div>
                </div>

                {/* Category filter chips */}
                <div className="skill-filters" style={{ padding: '12px 20px', borderTop: '1px solid var(--border-subtle)' }}>
                    <div className={`filter-chip ${categoryFilter === 'all' ? 'active' : ''}`}
                         onClick={() => setCategoryFilter('all')}>
                        All ({spells.length})
                    </div>
                    {M.SPELL_CATEGORIES.map(cat => (
                        <div key={cat.key}
                             className={`filter-chip ${categoryFilter === cat.key ? 'active' : ''}`}
                             onClick={() => setCategoryFilter(cat.key)}>
                            {cat.label} ({spellsByCategory[cat.key]?.length || 0})
                        </div>
                    ))}
                </div>
            </div>

            {/* Spell table */}
            {(displayedSpells.length > 0 || pendingNewSpells.length > 0) ? (
                <div className="spell-table">
                    <div className="spell-table-head">
                        <div>Spell</div>
                        <div style={{ textAlign: 'center' }}>Type</div>
                        <div style={{ textAlign: 'center' }}>Range</div>
                        <div style={{ textAlign: 'center' }}>Damage</div>
                        <div style={{ textAlign: 'center' }}>Duration</div>
                        <div style={{ textAlign: 'center' }}>Drain</div>
                        <div style={{ textAlign: 'center' }}>DV@F{previewForce}</div>
                        <div></div>
                    </div>
                    {displayedSpells.map(renderSpellRow)}
                    {pendingNewSpells
                        .filter(ps => categoryFilter === 'all' || ps.category === categoryFilter)
                        .map(renderPendingSpellRow)}
                </div>
            ) : (
                <div className="spell-empty">
                    {categoryFilter === 'all'
                        ? 'no spells yet — browse the catalog or add a custom spell'
                        : `no ${categoryFilter} spells — try another category or add one`}
                </div>
            )}

            <div className="gear-add-bar">
                <button className="add-button" onClick={() => setPickerOpen(true)}>
                    <span className="plus-icon">+</span>Browse Spells {inCareer ? '(5 karma each)' : ''}
                </button>
                {!inCareer && (
                    <button className="add-button" onClick={() => setCustomOpen(true)}>
                        <span className="plus-icon">+</span>Custom Spell
                    </button>
                )}
            </div>

            {pickerOpen && (
                <SpellPickerModal
                    character={character}
                    previewForce={previewForce}
                    onPick={handlePick}
                    onClose={() => setPickerOpen(false)}
                />
            )}

            {customOpen && (
                <CustomSpellDialog
                    onSubmit={payload => {
                        onChange(C.addCustomSpell(character, payload));
                        setCustomOpen(false);
                    }}
                    onClose={() => setCustomOpen(false)}
                />
            )}

            {/* Mystic adepts also get adept powers — show them below spells */}
            {magicType === 'mystic_adept' && (
                <div style={{ marginTop: 28 }}>
                    <h3 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, letterSpacing: '0.2em',
                                 color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: 12 }}>
                        Adept Powers
                    </h3>
                    <AdeptPowersSection character={character} onChange={onChange} embedded />
                </div>
            )}

            {/* Magical equipment: foci, reagents, lodges — for all awakened (Phase 5d) */}
            <div style={{ marginTop: 28 }}>
                <h3 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, letterSpacing: '0.2em',
                             color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: 12 }}>
                    Magical Equipment
                </h3>
                <MagicalEquipmentSection character={character} onChange={onChange} />
            </div>

            {/* Phase 17 — Bound spirits at creation (magician + mystic adept only) */}
            <div style={{ marginTop: 28 }}>
                <h3 style={{ fontFamily: 'var(--font-mono)', fontSize: 14, letterSpacing: '0.2em',
                             color: 'var(--text-secondary)', textTransform: 'uppercase', marginBottom: 12 }}>
                    Bound Spirits
                </h3>
                <BoundSpiritsSection character={character} onChange={onChange} />
            </div>

            <div className="phase-note" style={{ marginTop: 20 }}>
                <strong>Phase 5d.</strong> Magical equipment: 7 focus categories with per-Force scaling,
                reagents (raw/refined/radical), and magical lodges. Bonded focus count is capped at Magic;
                sum of Forces at Magic × 5. Costs feed the unified nuyen budget; bonded Karma is tracked separately.
            </div>
        </>
    );
};

/* ============================================================
   AspectedGroupPicker (Phase 13)

   Aspected magicians must pick one of the three magical skill
   groups at creation and can only take skills/groups from it.
   Per CRB p. 69: the choice is permanent once made.
   ============================================================ */
window.AspectedGroupPicker = function AspectedGroupPicker({ character, onChange, inCareer }) {
    const C = SR5_CALC;
    const chosen = C.aspectedMagicalGroup(character);
    /* Locked only in career mode. Within creation the player can change
       their mind — the choice only becomes permanent at finalize. */
    const locked = inCareer;

    const groups = [
        { key: 'sorcery',    label: 'Sorcery',    desc: 'Spellcasting, Counterspelling, Ritual Spellcasting — direct mana manipulation.' },
        { key: 'conjuring',  label: 'Conjuring',  desc: 'Summoning, Binding, Banishing — deals with spirits.' },
        { key: 'enchanting', label: 'Enchanting', desc: 'Alchemy, Artificing, Disenchanting — creating and dispelling foci and preparations.' },
    ];

    function pick(key) {
        if (locked) return;
        if (chosen === key) return;  /* already chosen; no-op */
        /* Warn only if switching AND the player has already invested in
           skills from the current group — otherwise silent. */
        if (chosen && hasInvestmentInGroup(character, chosen)) {
            if (!confirm(`Switch from ${groups.find(g => g.key === chosen).label} to ${groups.find(g => g.key === key).label}? This will hide any skills/groups you've already bought in the old group.`)) return;
        }
        onChange(C.setAspectedMagicalGroup(character, key));
    }

    function hasInvestmentInGroup(char, groupKey) {
        const D = SR5_DATA;
        const group = D.SKILL_GROUPS.find(g => g.key === groupKey);
        if (!group) return false;
        /* Individual skill ranks */
        if ((char?.skills || {})[groupKey] > 0) return false;  /* groupKey != skillKey */
        for (const skillKey of group.skills) {
            if ((char?.skills || {})[skillKey] > 0) return true;
            if ((char?.specializations || {})[skillKey]) return true;
        }
        /* Group rating */
        if ((char?.skillGroups || {})[groupKey] > 0) return true;
        return false;
    }

    return (
        <div style={{ marginBottom: 18, padding: 16,
                      background: 'var(--bg-surface)',
                      border: '1px solid var(--accent-dim)',
                      borderLeft: '2px solid var(--accent)',
                      borderRadius: 3 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 10 }}>
                <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11,
                               letterSpacing: '0.2em', color: 'var(--accent-bright)',
                               textTransform: 'uppercase' }}>
                    Aspected Focus {locked && '🔒'}
                </span>
                <span className="muted mono" style={{ fontSize: 11 }}>
                    {locked
                        ? 'Locked in career mode — your aspected focus is permanent.'
                        : chosen
                            ? 'You can change your choice up until you finalize. Click a different tile to switch.'
                            : 'Pick one magical skill group. You can change this until you finalize the character.'}
                </span>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: 10 }}>
                {groups.map(g => {
                    const isSelected = chosen === g.key;
                    return (
                        <div key={g.key}
                             className={`tradition-tile ${locked && !isSelected ? 'locked-dim' : ''}`}
                             style={{ cursor: locked ? 'default' : 'pointer',
                                      opacity: locked && !isSelected ? 0.35 : 1 }}
                             onClick={() => pick(g.key)}>
                            <div className="tt-name" style={{
                                color: isSelected ? 'var(--accent-bright)' : 'var(--text-primary)',
                            }}>
                                {g.label}
                            </div>
                            <div className="tt-desc">{g.desc}</div>
                            {isSelected && (
                                <div style={{ marginTop: 4, fontFamily: 'var(--font-mono)',
                                              fontSize: 10, letterSpacing: '0.1em',
                                              color: 'var(--accent)', textTransform: 'uppercase' }}>
                                    ✓ selected
                                </div>
                            )}
                        </div>
                    );
                })}
            </div>
        </div>
    );
};
