Adding skills & isac talent updates

This commit is contained in:
jb
2026-07-29 20:56:22 -04:00
parent f1b473ffa8
commit 65a4987cad
68 changed files with 5486 additions and 569 deletions
@@ -7,11 +7,21 @@
import type { AiMechanicContext } from '$lib/ai/mechanic-authoring';
import {
blankConditionalMechanicDraft,
currentHealthPercentBandPredicate,
eventFieldComparisonPredicate,
parseConditionalMechanicDraft,
readCurrentHealthPercentBand,
readEventFieldComparison,
readStatePoolThreshold,
statePoolThresholdPredicate,
type ConditionalMechanicClauseDraft,
type ConditionalMechanicDraftV1,
type ConditionalMechanicOutcomeDraft,
type ConditionalMechanicPoolDraft
type ConditionalMechanicPoolDraft,
type EditablePredicateScalar,
type EventFieldComparison,
type PredicateComparisonOperator,
type StatePoolThresholdComparison
} from '$lib/calculations/mechanic-editor';
import {
formatMechanicProgram,
@@ -59,6 +69,11 @@
let recipeCount = $state(9);
let recipeWindow = $state(3);
let recipeDistance = $state(10);
let recipeHealthBands = $state([
{ thresholdPercent: 25, outcomeKey: '15-percent-reduction' },
{ thresholdPercent: 50, outcomeKey: '12-percent-reduction' },
{ thresholdPercent: 75, outcomeKey: '9-percent-reduction' }
]);
let replaceConfirmation = $state(false);
let valueModelOverrides = $state<Record<string, OutcomeValueModel>>({});
@@ -90,17 +105,32 @@
{ value: 'direct_damage', label: 'DAMAGE' },
{ value: 'restoration', label: 'RESTORE' },
{ value: 'resource_cost', label: 'COST' },
{ value: 'weapon_reload', label: 'WEAPON RELOAD' },
{ value: 'marker', label: 'MARKER' }
];
const scopeOptions = $derived([
{ value: 'self', label: 'SELF' },
{ value: 'equipped_weapon', label: 'WEAPON' },
{ value: 'inactive_weapon', label: 'INACTIVE WEAPON' },
{ value: 'target', label: 'TARGET' },
{ value: 'allies', label: 'ALLIES' },
{ value: 'self_and_allies', label: 'SELF + ALLIES' },
...(allowAssignedAbility ? [{ value: 'assigned_ability', label: 'ABILITY' }] : []),
{ value: 'all_skills', label: 'ALL SKILLS' }
]);
const predicateOperatorOptions = [
{ value: 'eq', label: '=' },
{ value: 'ne', label: '≠' },
{ value: 'gt', label: '>' },
{ value: 'gte', label: '≥' },
{ value: 'lt', label: '<' },
{ value: 'lte', label: '≤' }
];
const predicateScalarTypeOptions = [
{ value: 'string', label: 'TEXT' },
{ value: 'number', label: 'NUMBER' },
{ value: 'boolean', label: 'BOOLEAN' }
];
const slug = (value: string) => value.toLocaleLowerCase()
.replace(/[^a-z0-9]+/g, '-')
@@ -114,10 +144,119 @@
if (recipeKey === 'consecutive_same_target_hits') return { hitsRequired: recipeCount };
if (recipeKey === 'continuous_firing_duration') return { seconds: recipeWindow };
if (recipeKey === 'target_hit_within_distance' || recipeKey === 'within_distance') return { meters: recipeDistance };
if (recipeKey === 'health_threshold_bands') return { bands: [{ thresholdPercent: 50, outcomeKey: 'active' }] };
if (recipeKey === 'health_threshold_bands') return { bands: recipeHealthBands };
return {};
}
function conditionModel(clause: ConditionalMechanicClauseDraft) {
if (!clause.if || clause.if.kind === 'true') return 'always';
if (readCurrentHealthPercentBand(clause.if)) return 'current_health_band';
if (readEventFieldComparison(clause.if)) return 'event_field';
if (readStatePoolThreshold(clause.if)) return 'state_pool_threshold';
return 'advanced';
}
function conditionOptions(clause: ConditionalMechanicClauseDraft) {
const options = [
{ value: 'always', label: 'ALWAYS' },
{ value: 'current_health_band', label: 'HEALTH BAND' },
{ value: 'event_field', label: 'EVENT FIELD' },
{ value: 'state_pool_threshold', label: 'STATE THRESHOLD', disabled: !rule.pools.length }
];
return conditionModel(clause) === 'advanced'
? [
...options.map((option) => ({ ...option, disabled: true })),
{ value: 'advanced', label: 'ADVANCED', disabled: true }
]
: options;
}
function setConditionModel(clauseIndex: number, model: string) {
if (model === 'always') updateClause(clauseIndex, { if: { kind: 'true' } });
if (model === 'current_health_band') {
updateClause(clauseIndex, {
if: currentHealthPercentBandPredicate({ maximum: 75 })
});
}
if (model === 'event_field') {
updateClause(clauseIndex, {
if: eventFieldComparisonPredicate({
path: 'values.state',
operator: 'eq',
value: 'moving'
})
});
}
if (model === 'state_pool_threshold' && rule.pools[0]) {
updateClause(clauseIndex, {
if: statePoolThresholdPredicate({
pool: rule.pools[0].key,
operator: 'gt',
value: 0
})
});
}
}
function updateHealthBand(
clauseIndex: number,
boundary: 'minimum' | 'maximum',
rawValue: string
) {
const current = readCurrentHealthPercentBand(rule.clauses[clauseIndex].if) ?? { maximum: 75 };
const numeric = rawValue === '' ? undefined : Number(rawValue);
const next = {
...(boundary === 'minimum'
? numeric === undefined ? {} : { minimum: numeric }
: current.minimum === undefined ? {} : { minimum: current.minimum }),
maximum: boundary === 'maximum' ? numeric ?? current.maximum : current.maximum
};
updateClause(clauseIndex, { if: currentHealthPercentBandPredicate(next) });
}
function updateEventFieldComparison(
clauseIndex: number,
patch: Partial<EventFieldComparison>
) {
const current = readEventFieldComparison(rule.clauses[clauseIndex].if) ?? {
path: 'values.state',
operator: 'eq',
value: 'moving'
} as const;
updateClause(clauseIndex, {
if: eventFieldComparisonPredicate({ ...current, ...patch })
});
}
function predicateScalarType(value: EditablePredicateScalar) {
return typeof value;
}
function setEventFieldScalarType(clauseIndex: number, type: string) {
const current = readEventFieldComparison(rule.clauses[clauseIndex].if);
if (!current) return;
const value: EditablePredicateScalar = type === 'number'
? (typeof current.value === 'number' ? current.value : 0)
: type === 'boolean'
? (typeof current.value === 'boolean' ? current.value : true)
: typeof current.value === 'string' ? current.value : String(current.value);
updateEventFieldComparison(clauseIndex, { value });
}
function updateStatePoolThreshold(
clauseIndex: number,
patch: Partial<StatePoolThresholdComparison>
) {
const current = readStatePoolThreshold(rule.clauses[clauseIndex].if) ?? {
pool: rule.pools[0]?.key ?? '',
operator: 'gt',
value: 0
} as const;
updateClause(clauseIndex, {
if: statePoolThresholdPredicate({ ...current, ...patch })
});
}
function applyRecipe(replace = false) {
if (!recipeKey) return;
const selectedRecipeKey = recipeKey;
@@ -647,6 +786,19 @@
{#if ['target_hit_within_distance', 'within_distance'].includes(recipeKey)}
<label class="field">METERS<input type="number" min="0" step="any" bind:value={recipeDistance}/></label>
{/if}
{#if recipeKey === 'health_threshold_bands'}
<div class="health-band-config">
<p>Define strict “below Health” bands from strongest to weakest. Exact thresholds enter the next weaker band.</p>
{#each recipeHealthBands as band, index}
<div>
<label class="field">BELOW · HEALTH %<input type="number" min="0.01" max="100" step="any" bind:value={band.thresholdPercent}/></label>
<label class="field">BAND LABEL<input maxlength="80" bind:value={band.outcomeKey}/></label>
<button type="button" class="remove" onclick={() => recipeHealthBands = recipeHealthBands.filter((_, current) => current !== index)} disabled={recipeHealthBands.length === 1}>REMOVE</button>
</div>
{/each}
<button type="button" class="secondary compact-action" onclick={() => recipeHealthBands = [...recipeHealthBands,{thresholdPercent:Math.min(100,(recipeHealthBands.at(-1)?.thresholdPercent ?? 0) + 25),outcomeKey:`health-band-${recipeHealthBands.length + 1}`}]} disabled={recipeHealthBands.length >= 5}>ADD BAND +</button>
</div>
{/if}
<button type="button" class="secondary" disabled={!recipeKey} onclick={() => applyRecipe(false)}>ADD RECIPE BRANCH +</button>
{#if rule.clauses.length && recipeKey}
<button type="button" class="danger-quiet" onclick={() => replaceConfirmation = true}>REPLACE PROGRAM</button>
@@ -669,7 +821,13 @@
<label class="field">INITIAL<input type="number" value={pool.initial} oninput={(event) => updatePool(index,{initial:Number(event.currentTarget.value)})}/></label>
<label class="field">MAX<input type="number" min="1" value={pool.max ?? 1} oninput={(event) => updatePool(index,{max:Number(event.currentTarget.value)})}/></label>
<label class="field">BEST CASE<input type="number" value={pool.bestCaseValue ?? pool.max ?? 1} oninput={(event) => updatePool(index,{bestCaseValue:Number(event.currentTarget.value)})}/></label>
<label class="field">EXPIRY · SECONDS<input type="number" min="0.01" step="any" value={pool.expiresAfterSeconds ?? ''} oninput={(event) => updatePool(index,{expiresAfterSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
{#if pool.expiresAfterSecondsVariants}
{#each valueColumns as column}
<label class="field">EXPIRY · {column.label}<input type="number" min="0.01" step="any" value={pool.expiresAfterSecondsVariants?.[column.variantKey] ?? ''} oninput={(event) => updatePool(index,{expiresAfterSecondsVariants:{...pool.expiresAfterSecondsVariants,[column.variantKey]:Number(event.currentTarget.value)}})}/></label>
{/each}
{:else}
<label class="field">EXPIRY · SECONDS<input type="number" min="0.01" step="any" value={pool.expiresAfterSeconds ?? ''} oninput={(event) => updatePool(index,{expiresAfterSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
{/if}
<button type="button" class="remove" onclick={() => setRule({...rule,pools:rule.pools.filter((_,current) => current !== index)})}>REMOVE</button>
</div>
{:else}
@@ -679,6 +837,9 @@
<div class="branch-stack">
{#each rule.clauses as clause, clauseIndex}
{@const conditionBand = readCurrentHealthPercentBand(clause.if)}
{@const eventFieldCondition = readEventFieldComparison(clause.if)}
{@const statePoolCondition = readStatePoolThreshold(clause.if)}
<section class="branch">
<header>
<div><span>{String(clauseIndex + 1).padStart(2, '0')}</span><label class="field">BRANCH LABEL<input value={clause.label ?? ''} oninput={(event) => updateClause(clauseIndex,{label:event.currentTarget.value,key:slug(event.currentTarget.value)})}/></label></div>
@@ -689,6 +850,75 @@
<div class="branch-readout"><span>MATCH POLICY</span><strong>{reducerItems.find((item) => item.id === clause.reducer.kind)?.name ?? clause.reducer.kind}</strong></div>
<label class="field">COOLDOWN · SECONDS<input type="number" min="0.01" step="any" value={clause.cooldownSeconds ?? ''} oninput={(event) => updateClause(clauseIndex,{cooldownSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
</div>
<div class="condition-controls">
<SegmentedChoice
legend="BRANCH CONDITION"
name={`${idPrefix}-condition-${clauseIndex}`}
options={conditionOptions(clause)}
value={conditionModel(clause)}
onchange={(values) => setConditionModel(clauseIndex,values[0])}
/>
{#if conditionBand}
<div class="health-boundaries">
<label class="field">FROM · HEALTH %<input type="number" min="0" max="100" step="any" value={conditionBand.minimum ?? ''} placeholder="0" oninput={(event) => updateHealthBand(clauseIndex,'minimum',event.currentTarget.value)}/><small>Inclusive; leave blank for zero.</small></label>
<label class="field">BELOW · HEALTH %<input type="number" min="0.01" max="100" step="any" value={conditionBand.maximum} oninput={(event) => updateHealthBand(clauseIndex,'maximum',event.currentTarget.value)}/><small>Exclusive; exact values do not match.</small></label>
</div>
{:else if eventFieldCondition}
<div class="predicate-fields event-predicate-fields">
<label class="field">EVENT FIELD PATH<input value={eventFieldCondition.path} placeholder="values.state" oninput={(event) => updateEventFieldComparison(clauseIndex,{path:event.currentTarget.value})}/></label>
<SegmentedChoice
legend="COMPARISON"
name={`${idPrefix}-event-operator-${clauseIndex}`}
options={predicateOperatorOptions}
value={eventFieldCondition.operator}
columns={6}
onchange={(values) => updateEventFieldComparison(clauseIndex,{operator:values[0] as PredicateComparisonOperator})}
/>
<SegmentedChoice
legend="VALUE TYPE"
name={`${idPrefix}-event-value-type-${clauseIndex}`}
options={predicateScalarTypeOptions}
value={predicateScalarType(eventFieldCondition.value)}
onchange={(values) => setEventFieldScalarType(clauseIndex,values[0])}
/>
{#if typeof eventFieldCondition.value === 'number'}
<label class="field">COMPARE VALUE<input type="number" step="any" value={eventFieldCondition.value} oninput={(event) => updateEventFieldComparison(clauseIndex,{value:Number(event.currentTarget.value)})}/></label>
{:else if typeof eventFieldCondition.value === 'boolean'}
<SegmentedChoice
legend="COMPARE VALUE"
name={`${idPrefix}-event-boolean-${clauseIndex}`}
options={[{value:'true',label:'TRUE'},{value:'false',label:'FALSE'}]}
value={String(eventFieldCondition.value)}
onchange={(values) => updateEventFieldComparison(clauseIndex,{value:values[0] === 'true'})}
/>
{:else}
<label class="field">COMPARE VALUE<input value={eventFieldCondition.value} placeholder="moving" oninput={(event) => updateEventFieldComparison(clauseIndex,{value:event.currentTarget.value})}/></label>
{/if}
</div>
{:else if statePoolCondition}
<div class="predicate-fields state-predicate-fields">
<div class="field"><span>STATE POOL</span><SearchableAssignment
items={rule.pools.map((pool) => ({id:pool.key,name:pool.label}))}
mode="single"
compact
subjectLabel="STATE POOL"
value={statePoolCondition.pool}
onvaluechange={(value) => updateStatePoolThreshold(clauseIndex,{pool:value})}
/></div>
<SegmentedChoice
legend="COMPARISON"
name={`${idPrefix}-state-operator-${clauseIndex}`}
options={predicateOperatorOptions}
value={statePoolCondition.operator}
columns={6}
onchange={(values) => updateStatePoolThreshold(clauseIndex,{operator:values[0] as PredicateComparisonOperator})}
/>
<label class="field">THRESHOLD<input type="number" step="any" value={statePoolCondition.value} oninput={(event) => updateStatePoolThreshold(clauseIndex,{value:Number(event.currentTarget.value)})}/></label>
</div>
{:else if conditionModel(clause) === 'advanced'}
<p class="advanced-condition-note">This saved condition remains active and is preserved unchanged. Use the generated mechanic preview to inspect it.</p>
{/if}
</div>
<div class="reducer-controls">
<SegmentedChoice
legend="MATCH POLICY"
@@ -760,10 +990,28 @@
<label class="field">DESCRIPTION SLOT<input type="number" min="1" step="1" value={outcome.descriptionSlot ?? ''} oninput={(event) => updateOutcome(clauseIndex,outcomeIndex,{descriptionSlot:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
</div>
<SegmentedChoice legend="TARGET SCOPE" name={`${idPrefix}-scope-${clauseIndex}-${outcomeIndex}`} options={scopeOptions} value={outcome.scope} columns={scopeOptions.length > 4 ? 4 : scopeOptions.length} onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{scope:values[0] as MechanicOutcomeScope})}/>
{#if outcome.scope === 'allies' || outcome.scope === 'self_and_allies' || outcome.scope === 'target'}
<div class="delivery-controls">
<SegmentedChoice
legend="AREA TARGETING"
name={`${idPrefix}-targeting-${clauseIndex}-${outcomeIndex}`}
options={[{value:'none',label:'NONE'},{value:'allies',label:'ALLIES IN RADIUS'},{value:'enemies',label:'ENEMIES IN RADIUS'}]}
value={outcome.targeting?.population ?? 'none'}
onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{
targeting: values[0] === 'none'
? undefined
: {kind:'within_radius',population:values[0] as 'allies'|'enemies',radiusMeters:outcome.targeting?.radiusMeters ?? 20}
})}
/>
{#if outcome.targeting}
<label class="field">RADIUS (METERS)<input type="number" min="0.01" step="any" value={outcome.targeting.radiusMeters} oninput={(event) => updateOutcome(clauseIndex,outcomeIndex,{targeting:{...outcome.targeting!,radiusMeters:Number(event.currentTarget.value)}})}/></label>
{/if}
</div>
{/if}
{#if outcome.kind === 'attribute_modifier'}
<SearchableAssignment items={attributes} mode="single" compact subjectLabel="ATTRIBUTE" bind:value={outcome.attributeId}/>
{:else if outcome.kind === 'restoration' || outcome.kind === 'resource_cost'}
<SegmentedChoice legend="RESOURCE" name={`${idPrefix}-resource-${clauseIndex}-${outcomeIndex}`} options={[{value:'health',label:'HEALTH'},{value:'armor',label:'ARMOR'},{value:'ammo',label:'AMMO'}]} value={outcome.resource ?? 'health'} onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{resource:values[0] as 'health'|'armor'|'ammo'})}/>
<SegmentedChoice legend="RESOURCE" name={`${idPrefix}-resource-${clauseIndex}-${outcomeIndex}`} options={[{value:'health',label:'HEALTH'},{value:'armor',label:'ARMOR'},{value:'ammo',label:'AMMO'},{value:'signature_ability_charge',label:'SIGNATURE CHARGE'}]} value={outcome.resource ?? 'health'} onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{resource:values[0] as 'health'|'armor'|'ammo'|'signature_ability_charge'})}/>
{/if}
{#if outcome.kind === 'restoration'}
<div class="delivery-controls">
@@ -876,6 +1124,9 @@
.scenario-row{display:grid;grid-template-columns:minmax(170px,1fr) minmax(260px,1.25fr) minmax(220px,1.3fr) repeat(4,minmax(85px,.45fr)) auto;align-items:end;gap:8px;padding:8px;border-bottom:1px solid var(--shd-border-soft)}
.scenario-row>.field{margin:0}.scenario-row>.description-field{min-width:0}
.recipe-grid{display:grid;grid-template-columns:minmax(260px,1fr) repeat(3,minmax(110px,.35fr)) auto auto;align-items:end;gap:var(--shd-gap-sm);padding:var(--shd-gap-sm)}
.health-band-config{grid-column:1/-1;display:grid;gap:var(--shd-gap-xs);padding:var(--shd-gap-sm);border:1px solid var(--shd-model-border);background:var(--shd-model-surface)}
.health-band-config>p,.advanced-condition-note{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
.health-band-config>div{display:grid;grid-template-columns:minmax(150px,.5fr) minmax(220px,1fr) auto;align-items:end;gap:var(--shd-gap-sm)}
.recipe-grid>.field,.pool-row>.field,.branch-grid>.field,.outcome-grid>.field,.variant-grid>.field{margin:0}
.replace-confirm{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm);padding:9px;border-top:1px solid var(--shd-danger);background:color-mix(in srgb,var(--shd-danger) 10%,var(--shd-surface))}
.replace-confirm p{display:grid;gap:2px;margin:0}.replace-confirm span{color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.replace-confirm>div{display:flex;gap:7px}
@@ -883,6 +1134,12 @@
.branch-stack{display:grid;gap:var(--shd-gap-sm)}
.branch>header>div{display:flex;align-items:end;gap:9px}.branch>header>div>span{align-self:center;color:var(--shd-orange);font:900 var(--shd-type-telemetry)/1 var(--shd-font-mono)}.branch>header .field{min-width:min(360px,55vw);margin:0}
.branch-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr)) minmax(160px,.45fr);align-items:end;gap:var(--shd-gap-sm);padding:var(--shd-gap-sm)}
.condition-controls{display:grid;grid-template-columns:minmax(260px,.7fr) minmax(320px,1.3fr);align-items:end;gap:var(--shd-gap-sm);padding:0 var(--shd-gap-sm) var(--shd-gap-sm)}
.health-boundaries{display:grid;grid-template-columns:repeat(2,minmax(150px,1fr));gap:var(--shd-gap-sm)}
.health-boundaries .field small{display:block;margin-top:4px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);line-height:1.3}
.predicate-fields{display:grid;grid-template-columns:minmax(170px,1fr) minmax(190px,.8fr);align-items:end;gap:var(--shd-gap-sm)}
.event-predicate-fields{grid-template-columns:minmax(180px,1fr) minmax(190px,.85fr) minmax(180px,.75fr) minmax(150px,.75fr)}
.predicate-fields>.field{margin:0}
.field>span{display:block;margin-bottom:5px;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
.branch-readout{display:grid;align-content:center;gap:3px;min-height:44px;padding:7px 9px;border:1px solid var(--shd-border);background:var(--shd-surface-raised)}.branch-readout span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.branch-readout strong{font-size:var(--shd-type-supporting)}
.reducer-controls{display:grid;grid-template-columns:minmax(0,1fr) repeat(2,minmax(130px,.25fr));align-items:end;gap:var(--shd-gap-sm);padding:0 var(--shd-gap-sm) var(--shd-gap-sm)}
@@ -899,7 +1156,7 @@
.compact-action{min-height:36px}.add-branch{min-height:44px}.empty-state{padding:10px}.script-preview{display:grid;grid-template-columns:minmax(190px,.28fr) minmax(0,1fr);gap:var(--shd-gap-sm);padding:var(--shd-gap-sm);border:1px solid var(--shd-border);border-top:2px solid var(--shd-success);background:var(--shd-surface)}
.script-preview>div{display:grid;align-content:start;gap:3px}.script-preview>div span{color:var(--shd-success);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}
@media(max-width:1300px){.scenario-row{grid-template-columns:repeat(2,minmax(0,1fr))}.scenario-row>.description-field{grid-column:1/-1}}
@media(max-width:1000px){.recipe-grid,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview{grid-template-columns:1fr 1fr}.recipe-grid>:first-child,.script-preview>:last-child{grid-column:1/-1}.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:1/-1}}
@media(max-width:620px){.mechanic-heading,.recipe-panel>header,.scenario-panel>header,.state-panel>header,.branch>header,.outcome-heading,.replace-confirm,.outcome-footer{align-items:stretch;flex-direction:column}.recipe-grid,.scenario-row,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview{grid-template-columns:1fr}.recipe-grid>:first-child,.script-preview>:last-child,.scenario-row>.description-field,.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:auto}.branch>header>div{align-items:stretch;flex-direction:column}.branch>header .field{min-width:0}.remove,.danger-quiet,.compact-action,.add-branch{width:100%;min-height:44px}}
@media(max-width:1000px){.recipe-grid,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview{grid-template-columns:1fr 1fr}.recipe-grid>:first-child,.script-preview>:last-child{grid-column:1/-1}.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:1/-1}.condition-controls{grid-template-columns:1fr}.event-predicate-fields{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:620px){.mechanic-heading,.recipe-panel>header,.scenario-panel>header,.state-panel>header,.branch>header,.outcome-heading,.replace-confirm,.outcome-footer{align-items:stretch;flex-direction:column}.recipe-grid,.scenario-row,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview,.health-band-config>div,.health-boundaries,.predicate-fields,.event-predicate-fields{grid-template-columns:1fr}.recipe-grid>:first-child,.script-preview>:last-child,.scenario-row>.description-field,.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:auto}.branch>header>div{align-items:stretch;flex-direction:column}.branch>header .field{min-width:0}.remove,.danger-quiet,.compact-action,.add-branch{width:100%;min-height:44px}}
@media(prefers-reduced-motion:reduce){.remove,.danger-quiet{transition:none}}
</style>