s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=playnplay.chain_reaction&take=20
Showing code results for query:
*
(95 total matches found)
Game
game
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Chain Reaction" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "chain_reaction" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "playnplay" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "playnplay.chain_reaction" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]
[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-08-25T16:50:49.1757035Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.120.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.120.0")]
Game
game
using Sandbox;
public sealed class MusicManager : Component
{
public static MusicManager Instance { get; private set; }
SoundHandle _menuMusic;
SoundHandle _windMusic;
SoundHandle _gameMusic;
// Tracked explicitly rather than inferred from SoundHandle.IsValid(), because
// a handle stopped with a fade is not reliably invalid straight away — and
// guessing wrong here means the menu comes back silent.
bool _menuPlaying;
protected override void OnStart()
{
Instance = this;
// Subscribe to game events
if (ChainReactionGame.Instance != null)
{
ChainReactionGame.Instance.OnGameStarted += OnGameStarted;
ChainReactionGame.Instance.OnGameReturned += OnGameReturned;
}
StartMenuLoops(.25f);
}
void StartMenuLoops(float volume)
{
_menuMusic = Sound.Play("phonk_menu_music", volume);
_windMusic = Sound.Play("wind_blowing_loop", volume);
_menuPlaying = true;
}
void StopMenuLoops(float fade)
{
if (_menuMusic.IsValid()) _menuMusic.Stop(fade);
if (_windMusic.IsValid()) _windMusic.Stop(fade);
_menuPlaying = false;
}
void OnGameStarted()
{
// Reactor mode is meant to be left running for a long time — the combat
// track would grate after ten minutes, so it keeps the calmer menu loop.
if (ChainReactionGame.Instance?.Mode == GameMode.Idle)
{
if (_gameMusic.IsValid()) _gameMusic.Stop(.5f);
if (!_menuPlaying) StartMenuLoops(.18f);
else if (_menuMusic.IsValid()) _menuMusic.Volume = .18f;
return;
}
StopMenuLoops(.5f);
// Only start game music if not already playing
if (!_gameMusic.IsValid())
_gameMusic = Sound.Play("abydos_game");
}
public void StopMenuMusic() => StopMenuLoops(0f);
public void PlayMenuMusic() => StartMenuLoops(.25f);
void OnGameReturned()
{
// Reactor mode never starts the combat track, so this handle can still be
// default-constructed here — calling Stop() on that throws.
if (_gameMusic.IsValid()) _gameMusic.Stop(.5f);
// Coming back from Reactor mode the menu loop never stopped — restore
// its volume instead of stacking a second copy on top of it.
if (_menuPlaying)
{
if (_menuMusic.IsValid()) _menuMusic.Volume = .3f;
return;
}
StartMenuLoops(.3f);
}
}
Game
game
@using System;
@using Sandbox;
@using Sandbox.Game.Menu
@using Sandbox.UI;
@inherits PanelComponent
@namespace Sandbox
<root>
@if(_active)
{
@* ── Debug card ── *@
@if(_debugMode)
{
<div class="tut-debug-card">
<div class="tut-debug-title">MM TUT — Step @_step</div>
<div class="tut-debug-row">
<span>BL: @_debugBL.ToString("F1")%</span>
<button class="tut-debug-btn" onclick=@(() => { _debugBL -= 1f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugBL += 1f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>BY: @_debugBY.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugBY -= 10f; StateHasChanged(); })>▲</button>
<button class="tut-debug-btn" onclick=@(() => { _debugBY += 10f; StateHasChanged(); })>▼</button>
</div>
<div class="tut-debug-row">
<span>BR: @_debugBR.ToString("F1")%</span>
<button class="tut-debug-btn" onclick=@(() => { _debugBR -= 1f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugBR += 1f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>HX: @_debugHX.ToString("F1")%</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHX -= 1f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHX += 1f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>HY: @_debugHY.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHY -= 10f; StateHasChanged(); })>▲</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHY += 10f; StateHasChanged(); })>▼</button>
</div>
<div class="tut-debug-row">
<span>HW: @_debugHW.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHW -= 10f; StateHasChanged(); })>◀</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHW += 10f; StateHasChanged(); })>▶</button>
</div>
<div class="tut-debug-row">
<span>HH: @_debugHH.ToString("F0")px</span>
<button class="tut-debug-btn" onclick=@(() => { _debugHH -= 10f; StateHasChanged(); })>▲</button>
<button class="tut-debug-btn" onclick=@(() => { _debugHH += 10f; StateHasChanged(); })>▼</button>
</div>
<button class="tut-debug-log" onclick=@LogCurrentValues>📋 Log</button>
</div>
}
@* ── Dim layer ── *@
<div class="tut-dim"></div>
@* ── Highlight ── *@
@if(_step < _steps.Length && _steps[_step].HW > 0)
{
var s = _steps[_step];
float hx = _debugHX != 0f ? _debugHX : s.HX;
float hy = _debugHY != 0f ? _debugHY : s.HY;
float hw = _debugHW != 0f ? _debugHW : s.HW;
float hh = _debugHH != 0f ? _debugHH : s.HH;
<div class="tut-highlight"
style="left:@hx.ToString("F1")%;top:@hy.ToString("F0")px;width:@hw.ToString("F0")px;height:@hh.ToString("F0")px;">
</div>
}
@* ── Instruction box ── *@
@if(_step < _steps.Length)
{
var s = _steps[_step];
float bl = _debugBL != 0f ? _debugBL : s.BL;
float br = _debugBR != 0f ? _debugBR : s.BR;
float by = _debugBY != 0f ? _debugBY : s.BY;
float boxWidthPct = s.BW > 0f ? (s.BW / 1920f * 100f) : (380f / 1920f * 100f);
string posStyle = br != 0f
? $"left:{(100f - br - boxWidthPct).ToString("F1")}%;"
: $"left:{bl.ToString("F1")}%;";
string boxStyle = $"{posStyle}top:{by.ToString("F0")}px;{(s.BW > 0f ? $"width:{s.BW.ToString("F0")}px;" : "")}";
<div class="tut-box" style="@boxStyle">
<div class="tut-step-count">@(_step + 1) / @_steps.Length</div>
<div class="tut-title">@s.Title</div>
<div class="tut-text">@s.Text</div>
@* Action hint — shown when player must act *@
@if(s.Trigger == MMTutTrigger.HeartPurchased)
{
<div class="tut-action-hint">👆 @s.ActionHint</div>
}
@* Next button — shown on Any steps *@
@if(s.Trigger == MMTutTrigger.Any)
{
<button class="tut-btn-next" onclick=@NextStep>Got it →</button>
}
@* Final step — Play button *@
@if(s.Trigger == MMTutTrigger.Done)
{
<button class="tut-btn-next" onclick=@OnPlayAndComplete>▶ Let's play!</button>
}
</div>
}
}
</root>
@code {
public static MainMenuTutorial Instance { get; private set; }
public bool IsActive => _active;
bool _active = false;
public int Step => _step;
int _step = 0;
// ── Debug mode ──────────────────────────────────────────────────── //
const bool _debugMode = false; // set false before release
float _debugBL = 0f;
float _debugBR = 0f;
float _debugBY = 0f;
float _debugHX = 0f;
float _debugHY = 0f;
float _debugHW = 0f;
float _debugHH = 0f;
void LogCurrentValues()
{
Log.Info($"// MM Step {_step} — {_steps[_step].Title}");
Log.Info($"BL={_debugBL:F1}f, BY={_debugBY:F0}f, BR={_debugBR:F1}f,");
Log.Info($"HX={_debugHX:F1}f, HY={_debugHY:F0}f, HW={_debugHW:F0}f, HH={_debugHH:F0}f");
}
// ── Trigger types ────────────────────────────────────────────────── //
public enum MMTutTrigger
{
Any, // advances via Got it button
HeartPurchased, // advances when player buys Heart consumable
Done, // final step — shows Play button
}
// ── Step data ────────────────────────────────────────────────────── //
public struct MMTutStep
{
public string Title;
public string Text;
public string ActionHint;
public MMTutTrigger Trigger;
public float HX, HY, HW, HH; // highlight
public float BL, BR, BY, BW; // box position
}
// ── Steps ────────────────────────────────────────────────────────── //
// Positions are placeholder — tune with debug card in-game
static readonly MMTutStep[] _steps = new MMTutStep[]
{
// Step 1 — Welcome
new MMTutStep {
Title = "Welcome to Chain Reaction!",
Text = "Here's a quick tour of how the game works. It'll take 30 seconds.",
Trigger = MMTutTrigger.Any,
HW = 0,
BL = 47f, BY = 370f
},
// Step 2 — Play button
new MMTutStep {
Title = "Play runs to earn 💠 Fragments",
Text = "Fragments are the game's currency. The further you go each run, the more you earn.",
Trigger = MMTutTrigger.Any,
HW = 0,
BL = 47f, BY = 370f
},
// Step 3 — Shop → Modifiers
new MMTutStep {
Title = "The Shop — Modifiers 🎲",
Text = "Spend fragments to unlock Modifiers. These permanently strengthen every run you play. Unlock once, keep forever.",
Trigger = MMTutTrigger.Any,
HX = 40f, HY = 200f, HW = 110f, HH = 40f,
BR = 61f, BY = 120f
},
// Step 4 — Consumables explanation
new MMTutStep {
Title = "Consumables 🎁",
Text = "Consumables are single-use items you bring into a run. Buy them once, use them at any time during a run. A bar will appear in-game when you own one.",
Trigger = MMTutTrigger.Any,
HX = 46f, HY = 200f, HW = 130f, HH = 40f,
BR = 61f, BY = 110
},
// Step 5 — Buy Heart (action step)
new MMTutStep {
Title = "Get yourself a Heart ❤️",
Text = "We've added 100 💠 to your balance. Buy the Heart consumable — it gives you an extra life mid-run when you need it most.",
ActionHint = "Click the buy button on the Heart card",
Trigger = MMTutTrigger.HeartPurchased,
HX = 65f, HY = 860f, HW = 80f, HH = 70f,
BR = 7f, BY = 800f
},
// Step 6 — Pet tab
new MMTutStep {
Title = "Your secret package 📦",
Text = "Play 3 runs to unlock what's inside. This section is for cosmetic skins — coming soon.",
Trigger = MMTutTrigger.Any,
HX = 53f, HY = 360f, HW = 70f, HH = 40f,
BR = 50f, BY = 340f
},
// Step 7 — Bombs
new MMTutStep {
Title = "Unlock new Bombs 💣",
Text = "Different bomb types open up new chain reaction strategies. Mix and match to find your playstyle.",
Trigger = MMTutTrigger.Any,
HX = 57f, HY = 220f, HW = 90f, HH = 40f,
BR = 5f, BY = 130f
},
// Step 8 — Leaderboard
new MMTutStep {
Title = "Compete Globally 🏅",
Text = "The leaderboard tracks your best score, chain reactions, and pet level. How far can you go?",
Trigger = MMTutTrigger.Any,
HX = 75f, HY = 400f, HW = 10f, HH = 10f,
BR = 5f, BY = 160f
},
// Step 9 — Done
new MMTutStep {
Title = "You're ready. 💥",
Text = "Play runs → earn fragments → unlock things → become stronger. Good luck!",
Trigger = MMTutTrigger.Done,
BR = 20f, BY = 370f
},
};
// ── Lifecycle ────────────────────────────────────────────────────── //
protected override void OnStart()
{
Instance = this;
_active = false;
}
public void Activate(PlayerSave save)
{
//var save = ChainReactionGame.Save ?? PlayerSave.Load();
if (save?.MenuTutorialCompleted == true) return;
_step = save?.MenuTutorialStep ?? 0;
_active = true;
ResetDebug();
// If resuming at step 4 (Buy Heart) but heart already purchased, skip it
if (_step == 4 && (save?.GetConsumableCount(ConsumableId.Heart) ?? 0) > 0)
_step = 5;
// Step 3 requires shop/mods to be visible — navigate there
ApplySideEffect(_step);
StateHasChanged();
}
// ── Called from MainMenu when Heart is bought ────────────────────── //
public void OnHeartPurchased()
{
if (!_active || _step >= _steps.Length) return;
if (_steps[_step].Trigger != MMTutTrigger.HeartPurchased) return;
NextStep();
}
// ── Step advancement ─────────────────────────────────────────────── //
void NextStep()
{
_step++;
ResetDebug();
if (_step >= _steps.Length)
{
Complete();
return;
}
// Persist current step so we can resume after crash - save tuto step
var save = PlayerSave.Load();
if (save != null)
{
save.MenuTutorialStep = _step;
save.Save();
}
ApplySideEffect(_step);
Sound.Play("UI_Button");
StateHasChanged();
}
// ── Side effects — navigate tabs automatically ───────────────────── //
void ApplySideEffect(int step)
{
var mm = MainMenu.Instance;
if (mm == null) return;
switch(step)
{
case 2: // Show shop modifiers
mm.SetTabPublic("shop");
mm.SetShopTabPublic("mods");
// Grant frags on step before heart purchase
break;
case 3: // Show consumables
mm.SetTabPublic("shop");
mm.SetShopTabPublic("consumables");
break;
case 4: // Buy Heart — shop consumables + grant frags if not already granted
var save = ChainReactionGame.Save ?? PlayerSave.Load();
if (save != null && !save.TutorialHeartFragsGranted)
{
save.TotalFragments += 100;
save.TutorialHeartFragsGranted = true;
save.Save();
}
mm.SetTabPublic("shop");
mm.SetShopTabPublic("consumables");
mm.ReloadSave();
break;
case 5: // Pet tab
mm.SetTabPublic("shop");
mm.SetShopTabPublic("pets");
break;
case 6: // Bombs
mm.SetTabPublic("shop");
mm.SetShopTabPublic("bombs");
break;
case 7: // Leaderboard
mm.SetTabPublic("lb");
break;
case 8: // Done — no navigation needed
break;
}
}
void Skip() => Complete();
void OnPlayAndComplete()
{
Complete();
MainMenu.Instance?.StartGamePublic();
}
void Complete()
{
_active = false;
// Get save from MainMenu directly
var mm = MainMenu.Instance;
// just use ChainReactionGame.Save or load fresh
var save = PlayerSave.Load();
if (save != null)
{
save.MenuTutorialCompleted = true;
save.MenuTutorialStep = 0;
save.Save();
}
StateHasChanged();
}
void ResetDebug()
{
_debugBL = 0f; _debugBR = 0f; _debugBY = 0f;
_debugHX = 0f; _debugHY = 0f; _debugHW = 0f; _debugHH = 0f;
}
public void ForceReset()
{
_active = false;
_step = 0;
ResetDebug();
StateHasChanged();
}
protected override int BuildHash() =>
System.HashCode.Combine(_active, _step, _debugBL, _debugBY, _debugHX, _debugHY, _debugHW, _debugHH);
}
Game
game
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@using System.Collections.Generic;
@inherits PanelComponent
@namespace Sandbox
@*
IMPORTANT — s&box text rendering quirk.
Razor compiles a literal containing non-ASCII characters (emoji, ×, ·, —)
into AddMarkupContent, and plain-ASCII literals into AddContent. The panel
renderer hoists every AddMarkupContent node ahead of every AddContent node,
so "@A × @B" comes out as "×AB".
The rule this file follows: an element holds EITHER pure literal text OR a
single @expression. Anything composed goes through a helper below.
*@
<root>
@if ( Visible )
{
<div class="idle-root">
@* ══════════════════════ AMBIENT BACKDROP ══════════════════════ *@
<div class="idle-bg"></div>
<div class="idle-bg-glow @GlowClass()"></div>
@for ( int i = 0; i < 12; i++ )
{
float pLeft = (i * 8.3f) % 100f;
float pDur = 10f + (i * 0.9f) % 8f;
float pDel = (i * 1.4f) % 12f;
int pSz = (i % 4 == 0) ? 4 : 2;
string pCol = (i % 3 == 0) ? "rgba(94,234,212,0.45)" : "rgba(96,165,250,0.30)";
<div class="idle-particle"
style="left:@pLeft.ToString("F1")%;width:@(pSz)px;height:@(pSz)px;background-color:@pCol;animation-duration:@pDur.ToString("F1")s;animation-delay:@pDel.ToString("F1")s;"></div>
}
@* ══════════════════════════ TOP BAR ══════════════════════════ *@
<div class="idle-top @TutDim("top")">
<button class="idle-back" onclick=@OnBack>⌂</button>
<button class="idle-help @HelpClass()" onclick=@OnStartTutorial>?</button>
<div class="idle-brand">
<div class="idle-brand-title">THE REACTOR</div>
<div class="idle-brand-sub">chain reactions, forever</div>
</div>
<div class="idle-currencies">
<div class="idle-cur cores">
<span class="idle-cur-icon">⚛️</span>
<span class="idle-cur-val">@CoresLabel()</span>
<span class="idle-cur-lbl">Cores</span>
</div>
<div class="idle-cur rate">
<span class="idle-cur-icon">📈</span>
<span class="idle-cur-val">@RateLabel()</span>
<span class="idle-cur-lbl">per sec</span>
</div>
<div class="idle-cur flux">
<span class="idle-cur-icon">🔷</span>
<span class="idle-cur-val">@FluxLabel()</span>
<span class="idle-cur-lbl">Flux</span>
</div>
<div class="idle-cur frag">
<span class="idle-cur-icon">💠</span>
<span class="idle-cur-val">@FragLabel()</span>
<span class="idle-cur-lbl">Frags</span>
</div>
</div>
</div>
@* ═══════════════════════════ BODY ════════════════════════════ *@
@* Hidden behind a modal — the panel renderer draws emoji glyphs above
overlay panels, so the board has to actually leave the tree. *@
@if ( !ModalOpen )
{
<div class="idle-body">
@* ─────────────────────── LEFT COLUMN ─────────────────────── *@
<div class="idle-col idle-left @TutDim("left")">
@* ── Daily contracts ──
First card in the column on purpose: it is the only part of
the mode with a deadline, so it has to be the first thing a
returning player's eye lands on. *@
<div class="idle-card idle-card-contracts @ContractsCardClass()">
<div class="idle-card-hd row">
<span class="idle-card-hd-txt">DAILY CONTRACTS</span>
<span class="idle-card-hd-side">@ContractResetLabel()</span>
</div>
@for ( int ci = 0; ci < IdleContracts.DailyCount; ci++ )
{
int slot = ci;
var contract = IdleContracts.At( S, slot );
if ( contract != null )
{
<div class="idle-contract @ContractClass( slot )" onclick=@(() => OnClaimContract( slot ))>
<span class="idle-contract-icon">@ContractIcon( slot )</span>
<div class="idle-contract-body">
<div class="idle-contract-top">
<span class="idle-contract-name">@contract.Name</span>
<span class="idle-contract-reward">@ContractRewardLabel( slot )</span>
</div>
<div class="idle-contract-desc">@ContractDescLabel( slot )</div>
<div class="idle-contract-track">
<div class="idle-contract-fill" style="width:@Pct( IdleContracts.Fraction( S, slot ) )%;"></div>
</div>
<div class="idle-contract-prog">@ContractProgLabel( slot )</div>
</div>
</div>
}
}
<div class="idle-card-note">@ContractFootLabel()</div>
</div>
<div class="idle-card idle-card-output">
<div class="idle-card-hd">OUTPUT</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Cores / sec</span>
<span class="idle-stat-val hot">@RateLabel()</span>
</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Global multiplier</span>
<span class="idle-stat-val">@GlobalMultLabel()</span>
</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Chain exponent</span>
<span class="idle-stat-val">@ExponentLabel()</span>
</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Last chain</span>
<span class="idle-stat-val">@LastChainLabel()</span>
</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Best chain</span>
<span class="idle-stat-val">@BestChainLabel()</span>
</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Board</span>
<span class="idle-stat-val">@BoardLabel()</span>
</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Feed line</span>
<span class="idle-stat-val @FeedClass()">@FeedLabel()</span>
</div>
</div>
@* ── Next goal ──
The Reactor Log is four clicks away in the right panel, which
means the long-tail goals are invisible exactly when they are
supposed to be pulling. This pins the closest one. *@
@if ( NextGoal != null )
{
<div class="idle-card idle-card-goal">
<div class="idle-card-hd">NEXT GOAL</div>
<div class="idle-goal-row">
<span class="idle-goal-icon">@NextGoal.Icon</span>
<div class="idle-goal-body">
<div class="idle-goal-name">@NextGoal.Name</div>
<div class="idle-goal-desc">@NextGoal.Desc</div>
</div>
</div>
<div class="idle-bar-track">
<div class="idle-bar-fill goal" style="width:@Pct( NextGoal.Fraction( S ) )%;"></div>
</div>
<div class="idle-goal-foot">
<span class="idle-goal-prog">@NextGoalProgLabel()</span>
<span class="idle-goal-reward">@NextGoalRewardLabel()</span>
</div>
</div>
}
@if ( ShowBoosts )
{
<div class="idle-card idle-card-active">
<div class="idle-card-hd">ACTIVE</div>
@if ( R.SurgeActive )
{
<div class="idle-boost surge">
<span class="idle-boost-icon">⚡</span>
<span class="idle-boost-lbl">@SurgeBoostLabel()</span>
<span class="idle-boost-time">@SecondsLabel( R.SurgeTimeLeft )</span>
</div>
}
@foreach ( var boost in R.Boosts )
{
var b = boost;
<div class="idle-boost">
<span class="idle-boost-icon">@b.Icon</span>
<span class="idle-boost-lbl">@BoostLabel( b )</span>
<span class="idle-boost-time">@SecondsLabel( b.SecondsLeft )</span>
</div>
}
</div>
}
<div class="idle-card idle-card-condenser">
<div class="idle-card-hd">CONDENSER</div>
<div class="idle-bar-track">
<div class="idle-bar-fill frag" style="width:@Pct( R.FragmentProgressFraction )%;"></div>
</div>
<div class="idle-card-note">@CondenserLabel()</div>
<div class="idle-stat-row">
<span class="idle-stat-lbl">Today</span>
<span class="idle-stat-val @CapClass()">@FragTodayLabel()</span>
</div>
</div>
<div class="idle-card">
<div class="idle-card-hd">MANIFEST</div>
@for ( int ti = 0; ti < IdleBalance.Tiers.Count; ti++ )
{
var tier = IdleBalance.Tiers[ti];
bool got = R.TierUnlocked( ti );
<div class="idle-tier @(got ? "on" : "")">
<span class="idle-tier-icon">@tier.Icon</span>
<div class="idle-tier-body">
<div class="idle-tier-name">@tier.Name</div>
<div class="idle-tier-blurb">@TierBlurb( ti, got )</div>
</div>
</div>
}
</div>
@* Rank card — neighbours, not a top-N. See RebuildRankRows(). *@
<div class="idle-card">
<div class="idle-card-hd">REACTOR RANK</div>
@if ( _rankRows.Count == 0 )
{
<div class="idle-card-note">@RankEmptyLabel()</div>
}
else
{
@foreach ( var row in _rankRows )
{
var rw = row;
<div class="idle-rank-row @RankRowClass( rw )">
<span class="idle-rank-pos">@rw.RankLabel</span>
<span class="idle-rank-name">@rw.Name</span>
<span class="idle-rank-val">@rw.ValueLabel</span>
</div>
}
<div class="idle-card-note">@GapLabel()</div>
@if ( _rankClimb > 0 )
{
<div class="idle-card-note up">@ClimbLabel()</div>
}
}
</div>
</div>
@* ────────────────────── CENTER COLUMN ────────────────────── *@
<div class="idle-col idle-center">
<div class="idle-board-wrap @TutDim("board")">
<div class="idle-board @SurgeClass() @CascadeClass() @BoardShake()">
@for ( int r = 0; r < R.Size; r++ )
{
<div class="idle-row">
@for ( int c = 0; c < R.Size; c++ )
{
int row = r;
int col = c;
var cell = R.Board[row, col];
bool golden = R.GoldenActive && R.GoldenRow == row && R.GoldenCol == col;
<div class="idle-cell @CellClass( cell, golden )"
style="width:@(CellPx)px;height:@(CellPx)px;"
onclick=@(() => OnCellClick( row, col ))>
@if ( golden )
{
<div class="idle-golden">🌟</div>
<div class="idle-golden-timer">@GoldenTimerLabel()</div>
}
@if ( !golden && cell.HasChest )
{
<div class="idle-chest" style="@ChestStyle( cell )">@ChestGlyph( cell )</div>
}
@if ( !golden && cell.HasChest && cell.MaxHits > 1 )
{
<div class="idle-hits">@cell.HitsLeft</div>
}
@if ( cell.HasDrone )
{
<div class="idle-drone" style="@DroneStyle( cell )">💣</div>
}
</div>
}
</div>
}
</div>
@foreach ( var floater in _floaters )
{
var f = floater;
<div class="idle-float @f.Cls"
style="left:@f.X.ToString("F1")%;top:@f.Y.ToString("F1")%;opacity:@f.Alpha.ToString("F2");transform:translateY(@f.Rise.ToString("F0")px) scale(@f.Scale.ToString("F2"));">
@f.Text
</div>
}
@if ( _calloutLife > 0f )
{
<div class="idle-callout @_calloutCls" style="opacity:@_calloutLife.ToString("F2");">@_calloutText</div>
}
</div>
<div class="idle-controls @TutDim("controls")">
<button class="idle-overload @OverloadClass()" onclick=@OnOverload>
<div class="idle-overload-fill" style="width:@OverloadFillPct()%;"></div>
<span class="idle-overload-label">@OverloadLabel()</span>
<span class="idle-overload-sub">@OverloadSubLabel()</span>
</button>
<div class="idle-surge-wrap">
<div class="idle-surge-track @SurgeReadyClass()">
<div class="idle-surge-fill" style="width:@Pct( R.Charge )%;"></div>
<span class="idle-surge-lbl">@SurgeLabel()</span>
</div>
@if ( R.SurgeReady )
{
<button class="idle-surge-btn" onclick=@OnSurge>@UnleashLabel()</button>
}
</div>
</div>
<div class="idle-meltdown-strip @MeltdownStripClass() @TutDim("meltdown")">
<span class="idle-melt-icon">☢️</span>
<span class="idle-melt-text">@MeltdownLabel()</span>
@if ( R.CanMeltdown )
{
<button class="idle-melt-btn" onclick=@OnOpenMeltdown>REVIEW</button>
}
@if ( !R.CanMeltdown )
{
<div class="idle-melt-bar">
<div class="idle-melt-fill" style="width:@Pct( R.MeltdownProgress )%;"></div>
</div>
}
</div>
@* Only appears once the perk tree is most of the way bought — the
layer is meaningless before that and just clutters the column. *@
@if ( R.SingularityVisible )
{
<div class="idle-sing-strip @SingStripClass() @TutDim("meltdown")">
<span class="idle-melt-icon">✴️</span>
<span class="idle-melt-text">@SingularityLabel()</span>
@if ( R.CanSingularity )
{
<button class="idle-sing-btn" onclick=@OnOpenSingularity>COLLAPSE</button>
}
@if ( !R.CanSingularity )
{
<div class="idle-melt-bar">
<div class="idle-sing-fill" style="width:@Pct( R.SingularityProgress )%;"></div>
</div>
}
</div>
}
</div>
@* ─────────────────────── RIGHT COLUMN ────────────────────── *@
<div class="idle-col idle-right @TutDim("right")">
<div class="idle-tabs">
<button class="idle-tab @TabClass("reactor")" onclick=@(() => SetTab("reactor"))>⚙️</button>
<button class="idle-tab @TabClass("chains")" onclick=@(() => SetTab("chains"))>⛓️</button>
<button class="idle-tab @TabClass("systems")" onclick=@(() => SetTab("systems"))>🛠️</button>
<button class="idle-tab fluxtab @TabClass("flux")" onclick=@(() => SetTab("flux"))>🔷</button>
@if ( R.SingularityVisible )
{
<button class="idle-tab singtab @TabClass("powers")" onclick=@(() => SetTab("powers"))>✴️</button>
}
<button class="idle-tab @TabClass("log")" onclick=@(() => SetTab("log"))>📜</button>
</div>
@if ( IsUpgradeTab )
{
<div class="idle-buyrow">
<span class="idle-buyrow-lbl">BUY</span>
@foreach ( var amount in BuyAmounts )
{
int amt = amount;
<button class="idle-buy-amt @BuyAmtClass( amt )" onclick=@(() => SetBuyAmount( amt ))>@BuyAmountLabel( amt )</button>
}
</div>
}
<div class="idle-panel">
@if ( IsUpgradeTab )
{
@foreach ( var upgrade in IdleUpgradeCatalog.InCategory( CurrentCategory ) )
{
var up = upgrade;
bool revealed = R.IsRevealed( up );
int lvl = S.Level( up.Id );
bool maxed = lvl >= up.MaxLevel;
int count = revealed ? BuyCount( up.Id ) : 0;
double cost = (!revealed || maxed) ? 0 : R.CostFor( up.Id, count );
bool afford = revealed && !maxed && count > 0 && Cores >= cost;
<div class="idle-up @UpgradeClass( revealed, maxed, afford )" onclick=@(() => OnBuy( up.Id ))>
<div class="idle-up-icon">@UpgradeIcon( up, revealed )</div>
<div class="idle-up-body">
<div class="idle-up-top">
<span class="idle-up-name">@UpgradeName( up, revealed )</span>
<span class="idle-up-lvl">@LevelLabel( up, lvl, revealed )</span>
</div>
<div class="idle-up-desc">@UpgradeDesc( up, revealed )</div>
@if ( revealed )
{
<div class="idle-up-bottom">
<span class="idle-up-effect">@up.Effect( lvl )</span>
<span class="idle-up-cost @MaxedClass( maxed )">@CostLabel( maxed, cost, count )</span>
</div>
}
</div>
</div>
}
}
@if ( _tab == "flux" )
{
<div class="idle-flux-head">
<div class="idle-flux-amount">@FluxHeadLabel()</div>
<div class="idle-flux-sub">@FluxSubLabel()</div>
</div>
@foreach ( var perk in VisiblePerks() )
{
var p = perk;
int lvl = S.PerkLevel( p.Id );
bool maxed = lvl >= p.MaxLevel;
int cost = p.CostAt( lvl );
bool afford = !maxed && S.Flux >= cost;
<div class="idle-up perk @UpgradeClass( true, maxed, afford )" onclick=@(() => OnBuyPerk( p.Id ))>
<div class="idle-up-icon">@p.Icon</div>
<div class="idle-up-body">
<div class="idle-up-top">
<span class="idle-up-name">@p.Name</span>
<span class="idle-up-lvl">@PerkLevelLabel( p, lvl )</span>
</div>
<div class="idle-up-desc">@p.Desc</div>
<div class="idle-up-bottom">
<span class="idle-up-effect">@p.Effect( lvl )</span>
<span class="idle-up-cost flux @MaxedClass( maxed )">@PerkCostLabel( maxed, cost )</span>
</div>
</div>
</div>
}
}
@if ( _tab == "powers" )
{
<div class="idle-flux-head sing">
<div class="idle-flux-amount">@ShardHeadLabel()</div>
<div class="idle-flux-sub">@ShardSubLabel()</div>
</div>
@foreach ( var power in IdleUpgradeCatalog.Powers )
{
var pw = power;
int lvl = S.PowerLevel( pw.Id );
bool maxed = lvl >= pw.MaxLevel;
int cost = pw.CostAt( lvl );
bool afford = !maxed && S.Shards >= cost;
<div class="idle-up power @UpgradeClass( true, maxed, afford )" onclick=@(() => OnBuyPower( pw.Id ))>
<div class="idle-up-icon">@pw.Icon</div>
<div class="idle-up-body">
<div class="idle-up-top">
<span class="idle-up-name">@pw.Name</span>
<span class="idle-up-lvl">@PowerLevelLabel( pw, lvl )</span>
</div>
<div class="idle-up-desc">@pw.Desc</div>
<div class="idle-up-bottom">
<span class="idle-up-effect">@pw.Effect( lvl )</span>
<span class="idle-up-cost shard @MaxedClass( maxed )">@PowerCostLabel( maxed, cost )</span>
</div>
</div>
</div>
}
}
@if ( _tab == "log" )
{
<div class="idle-flux-head">
<div class="idle-flux-amount">@LogHeadLabel()</div>
<div class="idle-flux-sub">Each one pays out once.</div>
</div>
@foreach ( var achievement in IdleAchievements.All )
{
var a = achievement;
bool done = S.HasAchievement( a.Id );
<div class="idle-up ach @(done ? "done" : "")">
<div class="idle-up-icon">@AchIcon( a, done )</div>
<div class="idle-up-body">
<div class="idle-up-top">
<span class="idle-up-name">@a.Name</span>
<span class="idle-ach-reward">@RewardLabel( a )</span>
</div>
<div class="idle-up-desc">@a.Desc</div>
@if ( !done )
{
<div class="idle-ach-bar">
<div class="idle-ach-fill" style="width:@Pct( a.Fraction( S ) )%;"></div>
</div>
<div class="idle-ach-prog">@ProgressLabel( a )</div>
}
</div>
</div>
}
}
</div>
</div>
</div>
}
@* ═════════════════════════ TOASTS ═════════════════════════ *@
<div class="idle-toasts">
@foreach ( var toast in _toasts )
{
var t = toast;
<div class="idle-toast @t.Cls" style="opacity:@t.Alpha.ToString("F2");">
<span class="idle-toast-icon">@t.Icon</span>
<span class="idle-toast-text">@t.Text</span>
</div>
}
</div>
@* ═══════════════════ WELCOME BACK OVERLAY ═══════════════════ *@
@if ( R.OfflinePending )
{
<div class="idle-modal-bg"></div>
<div class="idle-modal offline">
<div class="idle-modal-eyebrow">REACTOR REPORT</div>
<div class="idle-modal-title">Welcome back</div>
<div class="idle-modal-sub">@OfflineSubLabel()</div>
<div class="idle-offline-haul">
<div class="idle-offline-amount">@OfflineAmountLabel()</div>
<div class="idle-offline-lbl">CORES</div>
</div>
@if ( R.PendingOfflineFrags > 0 )
{
<div class="idle-offline-extra">@OfflineFragLabel()</div>
}
@if ( OfflineTankFull )
{
<div class="idle-offline-warn">@OfflineWarnLabel()</div>
}
<button class="idle-modal-btn" onclick=@OnClaimOffline>COLLECT</button>
</div>
}
@* ═══════════════════ DAILY STREAK OVERLAY ═══════════════════ *@
@if ( !R.OfflinePending && !_showIntro && R.DailyPending )
{
<div class="idle-modal-bg"></div>
<div class="idle-modal daily">
<div class="idle-modal-eyebrow">DAILY CYCLE</div>
<div class="idle-modal-title">@DailyTitleLabel()</div>
<div class="idle-modal-sub">Come back tomorrow to keep the streak climbing. Day 7 pays eight times.</div>
<div class="idle-streak-row">
@for ( int d = 1; d <= 7; d++ )
{
int day = d;
<div class="idle-streak-pip @StreakPipClass( day )">@day</div>
}
</div>
<div class="idle-daily-reward">
<div class="idle-daily-mult">@DailyMultLabel()</div>
<div class="idle-daily-lbl">@DailyRewardLabel()</div>
</div>
<button class="idle-modal-btn" onclick=@OnClaimDaily>CLAIM</button>
</div>
}
@* ══════════════════════ MELTDOWN MODAL ══════════════════════ *@
@if ( !R.OfflinePending && !_showIntro && !R.DailyPending && _showMeltdown )
{
<div class="idle-modal-bg"></div>
<div class="idle-modal meltdown">
<div class="idle-modal-eyebrow">PRESTIGE</div>
<div class="idle-modal-title">MELTDOWN</div>
<div class="idle-modal-sub">Collapse this cycle into permanent Flux. Every Flux you have ever earned adds income forever, and Flux buys perks that survive every reset.</div>
<div class="idle-melt-payout">
<div class="idle-melt-payout-val">@MeltPayoutLabel()</div>
<div class="idle-melt-payout-lbl">FLUX</div>
</div>
@* The Flux number alone does not answer the only question that
matters at this button — "am I actually going to be faster
afterwards". This does. *@
<div class="idle-melt-gain">
<span class="idle-melt-gain-from">@MeltMultBeforeLabel()</span>
<span class="idle-melt-gain-arrow">→</span>
<span class="idle-melt-gain-to">@MeltMultAfterLabel()</span>
<span class="idle-melt-gain-lbl">permanent income</span>
</div>
<div class="idle-melt-cols">
<div class="idle-melt-col keep">
<div class="idle-melt-col-hd">KEPT</div>
<div class="idle-melt-line">Flux and every perk</div>
<div class="idle-melt-line">Reactor Log entries</div>
<div class="idle-melt-line">Records and Fragments</div>
<div class="idle-melt-line">Kickstart head start</div>
</div>
<div class="idle-melt-col lose">
<div class="idle-melt-col-hd">BURNED</div>
<div class="idle-melt-line">@BurnedCoresLabel()</div>
<div class="idle-melt-line">All Core upgrades</div>
<div class="idle-melt-line">Reactor Frame size</div>
<div class="idle-melt-line">Unlocked chest tiers</div>
</div>
</div>
<div class="idle-modal-btns">
<button class="idle-modal-btn ghost" onclick=@OnCloseMeltdown>Not yet</button>
<button class="idle-modal-btn danger" onclick=@OnMeltdown>MELT DOWN</button>
</div>
</div>
}
@* ════════════════════ SINGULARITY MODAL ════════════════════ *@
@if ( !R.OfflinePending && !_showIntro && !R.DailyPending && !_showMeltdown && _showSingularity )
{
<div class="idle-modal-bg"></div>
<div class="idle-modal singularity">
<div class="idle-modal-eyebrow">SECOND PRESTIGE</div>
<div class="idle-modal-title">SINGULARITY</div>
<div class="idle-modal-sub">Collapse your entire Flux career into Shards. Shards buy Powers — and Powers change how a cycle runs, not just how much it pays.</div>
<div class="idle-melt-payout">
<div class="idle-sing-payout-val">@SingPayoutLabel()</div>
<div class="idle-melt-payout-lbl">SHARDS</div>
</div>
<div class="idle-melt-cols">
<div class="idle-melt-col keep">
<div class="idle-melt-col-hd">KEPT</div>
<div class="idle-melt-line">Shards and every Power</div>
<div class="idle-melt-line">Reactor Log entries</div>
<div class="idle-melt-line">Records, Fragments, pet</div>
<div class="idle-melt-line">@ShardKeepLabel()</div>
</div>
<div class="idle-melt-col lose">
<div class="idle-melt-col-hd">COLLAPSED</div>
<div class="idle-melt-line">@BurnedFluxLabel()</div>
<div class="idle-melt-line">Every Flux perk</div>
<div class="idle-melt-line">All Core upgrades</div>
<div class="idle-melt-line">This cycle's Cores</div>
</div>
</div>
<div class="idle-modal-btns">
<button class="idle-modal-btn ghost" onclick=@OnCloseSingularity>Not yet</button>
<button class="idle-modal-btn sing" onclick=@OnSingularity>COLLAPSE</button>
</div>
</div>
}
@* ════════════════════════ INTRO MODAL ═══════════════════════ *@
@if ( !R.OfflinePending && _showIntro )
{
<div class="idle-modal-bg"></div>
<div class="idle-modal intro">
<div class="idle-modal-eyebrow">HOW IT WORKS</div>
<div class="idle-modal-title">THE REACTOR</div>
<div class="idle-modal-sub">A board that plays itself. Chests drop, drones bomb them, chain reactions pay Cores. You never lose here — you only grow.</div>
<div class="idle-intro-steps">
<div class="idle-intro-step">
<span class="idle-intro-num">1</span>
<span class="idle-intro-txt">Spend Cores on the right. Longer chains are worth far more than richer chests.</span>
</div>
<div class="idle-intro-step">
<span class="idle-intro-num">2</span>
<span class="idle-intro-txt">Hit OVERLOAD, or press SPACE, for a burst. It charges the Surge meter — ten times output when you unleash it.</span>
</div>
<div class="idle-intro-step">
<span class="idle-intro-num">3</span>
<span class="idle-intro-txt">It keeps running while you are on the menu or another screen. Close the game and it stops.</span>
</div>
<div class="idle-intro-step">
<span class="idle-intro-num">4</span>
<span class="idle-intro-txt">Cores condense into Fragments, which feed your companion for a permanent output bonus. Meltdown turns a whole cycle into Flux.</span>
</div>
</div>
<div class="idle-modal-btns">
<button class="idle-modal-btn ghost" onclick=@OnCloseIntro>I'll figure it out</button>
<button class="idle-modal-btn" onclick=@OnIntroTutorial>SHOW ME HOW</button>
</div>
</div>
}
@* ═════════════════════ GUIDED WALKTHROUGH ═════════════════════
Coach marks over the live HUD. The board keeps running underneath —
an idle game that freezes for its own tutorial teaches the wrong
thing about what the game is. Everything here is measured from the
real panels at runtime, so it cannot drift out of sync with layout.
*@
@if ( TutorialActive )
{
<div class="rt-layer">
@* Four quads around the spotlight, rather than one dim sheet with
a hole — the panel renderer has no masking, so this is how you
get a real cut-out that still swallows clicks everywhere else. *@
<div class="rt-dim" style="@DimTopStyle()"></div>
<div class="rt-dim" style="@DimBottomStyle()"></div>
<div class="rt-dim" style="@DimLeftStyle()"></div>
<div class="rt-dim" style="@DimRightStyle()"></div>
@if ( TutHasSpot )
{
<div class="rt-ring" style="@SpotStyle()"></div>
<div class="rt-spot" style="@SpotStyle()">
<div class="rt-corner tl"></div>
<div class="rt-corner tr"></div>
<div class="rt-corner bl"></div>
<div class="rt-corner br"></div>
</div>
}
@* The class alternates per step purely so the entrance animation
replays — a stable class string would only ever play once. *@
<div class="rt-card @TutCardAnimClass()" style="@CardStyle()">
<div class="rt-card-top">
<span class="rt-eyebrow">HOW TO PLAY</span>
<span class="rt-count">@TutCountLabel()</span>
</div>
<div class="rt-title">@TutTitleLabel()</div>
<div class="rt-text">@TutTextLabel()</div>
<div class="rt-dots">
@for ( int di = 0; di < IdleTutorial.Count; di++ )
{
<div class="rt-dot @DotClass( di )"></div>
}
</div>
@if ( TutWaitingOnAction )
{
<div class="rt-hint">
<span class="rt-hint-pip">👆</span>
<span class="rt-hint-txt">@TutHintLabel()</span>
</div>
}
@if ( !TutWaitingOnAction )
{
<button class="rt-next" onclick=@OnTutNext>@TutNextLabel()</button>
}
<div class="rt-foot">
<button class="rt-back @TutBackClass()" onclick=@OnTutBack>Back</button>
<button class="rt-skip" onclick=@OnTutSkip>Skip the tour</button>
</div>
</div>
</div>
}
</div>
}
</root>
@code {
public static IdleHud Instance { get; private set; }
IdleReactor R => IdleReactor.Instance;
ChainReactionGame Game => ChainReactionGame.Instance;
IdleSave S => ChainReactionGame.Save?.Idle;
bool Visible => Game?.GameStarted == true
&& Game?.Mode == GameMode.Idle
&& R != null && S != null;
double Cores => S?.Cores ?? 0;
bool ModalOpen => R.OfflinePending || R.DailyPending || _showMeltdown || _showSingularity || _showIntro;
bool ShowBoosts => R.Boosts.Count > 0 || R.SurgeActive;
bool OfflineTankFull => R.PendingOfflineSeconds > R.OfflineCapHours * 3600f;
// ── UI state ──────────────────────────────────────────────────
string _tab = "reactor";
int _buyAmount = 1; // -1 == MAX
bool _showMeltdown;
bool _showSingularity;
bool _showIntro;
bool _subscribed;
bool _wasVisible;
static readonly int[] BuyAmounts = { 1, 10, 25, -1 };
bool IsUpgradeTab => _tab == "reactor" || _tab == "chains" || _tab == "systems";
IdleUpgradeCategory CurrentCategory => _tab switch
{
"reactor" => IdleUpgradeCategory.Reactor,
"chains" => IdleUpgradeCategory.Chains,
_ => IdleUpgradeCategory.Systems,
};
// ── Floating numbers ──────────────────────────────────────────
class Floater
{
public string Text;
public string Cls;
public float X, Y;
public float Life, MaxLife;
public float Alpha => MathF.Min( 1f, Life / 0.35f );
public float Rise => -(MaxLife - Life) * 42f;
public float Scale => 0.85f + MathF.Min( 0.35f, (MaxLife - Life) * 1.2f );
}
readonly List<Floater> _floaters = new();
class ToastMsg
{
public string Icon;
public string Text;
public string Cls;
public float Life;
public float Alpha => MathF.Min( 1f, Life / 0.4f );
}
readonly List<ToastMsg> _toasts = new();
string _calloutText = "";
string _calloutCls = "";
float _calloutLife;
// Board shake is kept local rather than going through ScreenShaker so the
// class string stays stable between rebuilds — a changing class would
// restart the animation on every repaint and read as a vibration.
string _shakeCls = "";
float _shakeUntil;
// ══════════════════════════════════════════════════════════════
// Guided walkthrough
//
// Coach marks measured off the real panels every frame, so the spotlight
// cannot drift when the board resizes, the window changes shape, or a card
// appears above the one being pointed at. The previous tutorial in this
// codebase hard-coded pixel rectangles and needed a debug nudge UI to keep
// them honest; this one has nothing to keep honest.
// ══════════════════════════════════════════════════════════════
bool _tutRunning;
int _tutStep;
float _tutStepAt; // Time.Now the step opened, for the action grace period
// Spotlight rect and card position, in design pixels relative to the root.
float _spotX, _spotY, _spotW, _spotH;
float _cardX, _cardY;
float _rootW = 1920f, _rootH = 1080f;
bool _spotFound;
// Measured on the frame after the card first renders; the fallback only
// ever shows for that single frame.
float _cardW = 380f, _cardH = 240f;
const float CardGap = 26f; // spotlight → card
const float EdgePad = 20f; // card → screen edge
const float ActionGrace = 0.45f; // the click that opened a step cannot also close it
/// <summary>
/// Set by the menu's "How to play" button. Consumed on the frame the HUD
/// becomes visible, since the tour points at panels that do not exist until
/// then.
/// </summary>
public static bool PendingTutorial { get; set; }
public bool TutorialActive => _tutRunning && Visible && !ModalOpen;
bool TutHasSpot => _spotFound && _spotW > 1f && _spotH > 1f;
IdleTutStep CurrentTutStep
=> _tutStep >= 0 && _tutStep < IdleTutorial.Count ? IdleTutorial.Steps[_tutStep] : null;
bool TutWaitingOnAction
=> CurrentTutStep != null && CurrentTutStep.Trigger != IdleTutTrigger.Button;
public void StartTutorial()
{
_tutRunning = true;
_tutStep = 0;
_tutStepAt = Time.Now;
_showIntro = false;
_spotFound = false;
MeasureTutorial();
StateHasChanged();
}
void EndTutorial( bool completed )
{
_tutRunning = false;
if ( S != null )
{
S.TutorialOffered = true;
if ( completed ) S.TutorialDone = true;
ChainReactionGame.Save?.Save();
}
StateHasChanged();
}
void OnTutNext()
{
Sound.Play( "UI_Button" );
AdvanceTutorial();
}
void OnTutBack()
{
if ( _tutStep <= 0 ) return;
Sound.Play( "UI_Button" );
_tutStep--;
_tutStepAt = Time.Now;
MeasureTutorial();
StateHasChanged();
}
void OnTutSkip()
{
Sound.Play( "UI_Button" );
EndTutorial( completed: true );
}
void AdvanceTutorial()
{
_tutStep++;
_tutStepAt = Time.Now;
if ( _tutStep >= IdleTutorial.Count )
{
Sound.Play( "PlayButton" );
EndTutorial( completed: true );
return;
}
MeasureTutorial();
StateHasChanged();
}
/// <summary>
/// Satisfies a step that is waiting on the player doing something. The grace
/// period stops the very click that advanced onto a step from also
/// advancing off it — buying an upgrade lands on the "buy an upgrade" step
/// otherwise, and the player never reads it.
/// </summary>
void TutSatisfy( IdleTutTrigger trigger )
{
if ( !_tutRunning ) return;
var step = CurrentTutStep;
if ( step == null || step.Trigger != trigger ) return;
if ( Time.Now - _tutStepAt < ActionGrace ) return;
Sound.Play( "Notification_1" );
AdvanceTutorial();
}
/// <summary>Recursive class lookup rather than Descendants, so the first match
/// found is the shallowest one — an inner panel sharing a class cannot win.</summary>
static Sandbox.UI.Panel FindByClass( Sandbox.UI.Panel from, string cls )
{
if ( from == null || string.IsNullOrEmpty( cls ) ) return null;
if ( from.HasClass( cls ) ) return from;
foreach ( var child in from.Children )
{
var hit = FindByClass( child, cls );
if ( hit != null ) return hit;
}
return null;
}
void MeasureTutorial()
{
var root = Panel;
if ( root == null ) return;
float scale = root.ScaleFromScreen;
var rr = root.Box.Rect;
_rootW = MathF.Max( 320f, rr.Width * scale );
_rootH = MathF.Max( 240f, rr.Height * scale );
var step = CurrentTutStep;
_spotFound = false;
if ( step != null && !string.IsNullOrEmpty( step.Target ) )
{
var target = FindByClass( root, step.Target );
if ( target != null )
{
var tr = target.Box.Rect;
if ( tr.Width > 1f && tr.Height > 1f )
{
_spotX = (tr.Left - rr.Left) * scale - step.Pad;
_spotY = (tr.Top - rr.Top ) * scale - step.Pad;
_spotW = tr.Width * scale + step.Pad * 2f;
_spotH = tr.Height * scale + step.Pad * 2f;
// A column that runs off the bottom of the viewport reports a
// rect taller than the screen; clamping keeps the dim quads
// from collapsing to nothing.
_spotX = Math.Clamp( _spotX, 0f, _rootW - 8f );
_spotY = Math.Clamp( _spotY, 0f, _rootH - 8f );
_spotW = Math.Clamp( _spotW, 8f, _rootW - _spotX );
_spotH = Math.Clamp( _spotH, 8f, _rootH - _spotY );
_spotFound = true;
}
}
}
// The card measures itself once it exists; until then the constructor
// defaults are close enough that nothing visibly jumps.
var card = FindByClass( root, "rt-card" );
if ( card != null )
{
var cr = card.Box.Rect;
if ( cr.Width > 1f && cr.Height > 1f )
{
_cardW = cr.Width * scale;
_cardH = cr.Height * scale;
}
}
PlaceCard( step );
}
void PlaceCard( IdleTutStep step )
{
if ( step == null || !TutHasSpot )
{
_cardX = (_rootW - _cardW) * 0.5f;
_cardY = (_rootH - _cardH) * 0.5f;
return;
}
float cx = _spotX + _spotW * 0.5f;
float cy = _spotY + _spotH * 0.5f;
var place = step.Place;
// Fall back to the other side when the requested one has no room. This
// is what makes the script survive an ultrawide or a small window
// without every step needing a per-aspect-ratio override.
if ( place == IdleTutPlace.Right && _spotX + _spotW + CardGap + _cardW > _rootW - EdgePad )
place = IdleTutPlace.Left;
else if ( place == IdleTutPlace.Left && _spotX - CardGap - _cardW < EdgePad )
place = IdleTutPlace.Right;
else if ( place == IdleTutPlace.Above && _spotY - CardGap - _cardH < EdgePad )
place = IdleTutPlace.Below;
else if ( place == IdleTutPlace.Below && _spotY + _spotH + CardGap + _cardH > _rootH - EdgePad )
place = IdleTutPlace.Above;
switch ( place )
{
case IdleTutPlace.Left:
_cardX = _spotX - CardGap - _cardW;
_cardY = cy - _cardH * 0.5f;
break;
case IdleTutPlace.Right:
_cardX = _spotX + _spotW + CardGap;
_cardY = cy - _cardH * 0.5f;
break;
case IdleTutPlace.Above:
_cardX = cx - _cardW * 0.5f;
_cardY = _spotY - CardGap - _cardH;
break;
case IdleTutPlace.Below:
_cardX = cx - _cardW * 0.5f;
_cardY = _spotY + _spotH + CardGap;
break;
default:
_cardX = (_rootW - _cardW) * 0.5f;
_cardY = (_rootH - _cardH) * 0.5f;
break;
}
_cardX = Math.Clamp( _cardX, EdgePad, MathF.Max( EdgePad, _rootW - _cardW - EdgePad ) );
_cardY = Math.Clamp( _cardY, EdgePad, MathF.Max( EdgePad, _rootH - _cardH - EdgePad ) );
}
// ── Inline styles ─────────────────────────────────────────────
// Ints only. A decimal separator from a French locale would be a comma and
// would silently kill the whole declaration.
static string Px( float v ) => ((int)MathF.Round( v )).ToString();
string SpotStyle()
=> $"left:{Px( _spotX )}px;top:{Px( _spotY )}px;width:{Px( _spotW )}px;height:{Px( _spotH )}px;";
string CardStyle() => $"left:{Px( _cardX )}px;top:{Px( _cardY )}px;";
// With no spotlight the top quad covers everything and the other three
// collapse — which is exactly the plain dim sheet a centred step wants.
string DimTopStyle()
=> TutHasSpot
? $"left:0px;top:0px;width:{Px( _rootW )}px;height:{Px( _spotY )}px;"
: $"left:0px;top:0px;width:{Px( _rootW )}px;height:{Px( _rootH )}px;";
string DimBottomStyle()
{
if ( !TutHasSpot ) return "width:0px;height:0px;";
float top = _spotY + _spotH;
return $"left:0px;top:{Px( top )}px;width:{Px( _rootW )}px;height:{Px( _rootH - top )}px;";
}
string DimLeftStyle()
=> TutHasSpot
? $"left:0px;top:{Px( _spotY )}px;width:{Px( _spotX )}px;height:{Px( _spotH )}px;"
: "width:0px;height:0px;";
string DimRightStyle()
{
if ( !TutHasSpot ) return "width:0px;height:0px;";
float left = _spotX + _spotW;
return $"left:{Px( left )}px;top:{Px( _spotY )}px;width:{Px( _rootW - left )}px;height:{Px( _spotH )}px;";
}
// ── Labels ────────────────────────────────────────────────────
string TutTitleLabel() => CurrentTutStep?.Title ?? "";
string TutTextLabel() => CurrentTutStep?.Text ?? "";
string TutHintLabel() => CurrentTutStep?.Hint ?? "";
string TutCountLabel() => $"{_tutStep + 1} / {IdleTutorial.Count}";
string TutNextLabel()
=> _tutStep >= IdleTutorial.Count - 1 ? "RUN THE REACTOR" : "GOT IT";
string TutBackClass() => _tutStep <= 0 ? "hidden" : "";
string TutCardAnimClass() => (_tutStep % 2) == 0 ? "in-a" : "in-b";
/// <summary>
/// Dims a HUD block that is not the subject of the current step.
///
/// The dim quads are click-blockers and a background scrim; they cannot do
/// this job on their own, because this renderer draws emoji above overlay
/// panels and every chest, icon and tab glyph would shine straight through
/// the scrim at full brightness. Ancestor opacity takes the glyphs with it.
/// </summary>
string TutDim( string region )
{
if ( !TutorialActive ) return "";
string want = CurrentTutStep?.Region ?? "";
if ( string.IsNullOrEmpty( want ) ) return ""; // centred step dims nothing
return region == want ? "" : "tut-dim";
}
string DotClass( int i )
{
if ( i == _tutStep ) return "on";
return i < _tutStep ? "done" : "";
}
string HelpClass() => _tutRunning ? "on" : "";
int CellPx => R == null ? 92 : R.Size switch
{
5 => 96,
6 => 84,
7 => 74,
8 => 66,
_ => 59,
};
// ══════════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════════
// Reactor rank card
//
// Deliberately NOT a top-N board. A top-N motivates whoever is already in
// it and is scenery for everyone else; the crossable goal is the gap to the
// player directly above, so the card shows a window around the local player
// (two up, you, one down).
//
// The board snapshot is stale between refreshes but S.AllTimeCores is live,
// so the local row and the gap are always computed from the local value.
// That makes the gap tick down while the player watches, which is the whole
// point of the card.
// ══════════════════════════════════════════════════════════════
class RankRow
{
public string RankLabel;
public string Name;
public string ValueLabel;
public bool IsMe;
}
readonly List<RankRow> _rankRows = new();
Sandbox.Services.Leaderboards.Board2 _rankBoard;
bool _rankLoading;
bool _rankFetched;
float _rankNextFetch;
int _myRank; // 1-based, 0 == not on the board
double _nextUpValue; // stat value of the player directly above, 0 == none above
int _rankClimb; // places gained since the last session
int _rankTotalPlayers; // Board2.TotalEntries — real population, not window size
const float RankRefreshSeconds = 90f;
// Same stat the main-menu leaderboard ranks on, so the two can never disagree.
static string RankStat => CommunityGoal.CoresStat;
/// <summary>
/// Network call — only ever driven by the throttle in OnUpdate, never from
/// the render path. BuildHash changes many times a second, so fetching from
/// there would hammer the backend into rate-limiting us.
/// </summary>
async System.Threading.Tasks.Task LoadRank()
{
if ( _rankLoading ) return;
_rankLoading = true;
try
{
var b = Sandbox.Services.Leaderboards.GetFromStat( RankStat );
b.SetAggregationMax();
b.SetSortDescending();
// CenterOnMe is exactly what this card wants: Board2 returns the
// entries surrounding a target player, so asking for a small window
// centred on us gives the neighbours directly. No manual slicing.
b.CenterOnMe();
b.MaxEntries = 5;
await b.Refresh();
_rankBoard = b;
_rankTotalPlayers = (int)b.TotalEntries;
Log.Info( $"Rank '{RankStat}' — window: {b.Entries?.Length ?? -1}, board total: {b.TotalEntries}" );
}
catch ( Exception e )
{
Log.Warning( $"Reactor rank: {e.Message}" );
}
_rankLoading = false;
_rankFetched = true;
RebuildRankRows();
StateHasChanged();
}
void RebuildRankRows()
{
_rankRows.Clear();
_myRank = 0;
_nextUpValue = 0;
_rankClimb = 0;
var entries = _rankBoard?.Entries;
if ( entries == null || entries.Length == 0 ) return;
// CenterOnMe means these indices are window-relative, NOT board-relative,
// so ranks must come from Entry.Rank. Using index+1 here would label the
// window "#1..#5" no matter where the player actually sits.
long myId = Sandbox.Connection.Local?.SteamId ?? 0;
int meIdx = -1;
for ( int i = 0; i < entries.Length; i++ )
{
if ( entries[i].SteamId == myId ) { meIdx = i; break; }
}
if ( meIdx < 0 )
{
// No entry for us yet — show the window we were given so there is
// still a named target to chase rather than an empty card.
for ( int i = 0; i < entries.Length; i++ )
_rankRows.Add( MakeRankRow( entries[i], false ) );
_nextUpValue = entries[entries.Length - 1].Value;
return;
}
_myRank = (int)entries[meIdx].Rank;
int start = Math.Max( 0, meIdx - 2 );
int end = Math.Min( entries.Length - 1, meIdx + 1 );
for ( int i = start; i <= end; i++ )
_rankRows.Add( MakeRankRow( entries[i], i == meIdx ) );
if ( meIdx > 0 ) _nextUpValue = entries[meIdx - 1].Value;
// Places gained since we last recorded a rank. Persisting this is what
// turns offline production into "I moved up while I was away".
int prev = S?.LastReactorRank ?? 0;
if ( prev > 0 && _myRank > 0 && _myRank < prev ) _rankClimb = prev - _myRank;
if ( S != null && _myRank > 0 && S.LastReactorRank != _myRank )
{
S.LastReactorRank = _myRank;
ChainReactionGame.Save?.Save();
}
}
RankRow MakeRankRow( Sandbox.Services.Leaderboards.Board2.Entry e, bool isMe )
{
// The snapshot lags our own earnings, so never show the player a number
// lower than what they have actually banked.
double val = isMe
? Math.Max( e.Value, S?.AllTimeCores ?? 0 )
: e.Value;
return new RankRow
{
RankLabel = "#" + e.Rank.ToString(),
Name = isMe ? "you" : RankName( e ),
ValueLabel = IdleFormat.Short( val ),
IsMe = isMe,
};
}
string RankName( Sandbox.Services.Leaderboards.Board2.Entry e )
{
if ( !string.IsNullOrWhiteSpace( e.DisplayName ) ) return e.DisplayName;
try
{
var friend = new Sandbox.Friend( e.SteamId );
if ( !string.IsNullOrWhiteSpace( friend.Name ) ) return friend.Name;
}
catch { }
return "Anonymous";
}
string RankRowClass( RankRow r ) => r.IsMe ? "me" : "";
string RankEmptyLabel()
{
if ( !_rankFetched ) return "Reading the board";
return "Produce Cores to enter the rankings";
}
string GapLabel()
{
if ( _nextUpValue <= 0 ) return "Top of the board";
double mine = S?.AllTimeCores ?? 0;
double gap = _nextUpValue - mine;
if ( gap <= 0 ) return "Ahead — board updates shortly";
return IdleFormat.Short( gap ) + " Cores to pass";
}
string ClimbLabel()
{
if ( _rankClimb == 1 ) return "Up 1 place since last time";
return "Up " + _rankClimb.ToString() + " places since last time";
}
// Lifecycle
// ══════════════════════════════════════════════════════════════
protected override void OnStart()
{
Instance = this;
}
protected override void OnUpdate()
{
if ( !_subscribed && R != null ) Subscribe();
bool vis = Visible;
if ( vis != _wasVisible )
{
_wasVisible = vis;
if ( vis )
{
_floaters.Clear();
_toasts.Clear();
_calloutLife = 0f;
_shakeUntil = 0f;
_showMeltdown = false;
_showSingularity = false;
// Offered on the walkthrough flag rather than IntroShown, so a
// save from before the walkthrough existed still gets asked
// once instead of being silently skipped past it.
_showIntro = S != null && !S.TutorialDone && !S.TutorialOffered;
_tutRunning = false;
_tab = "reactor";
_rankNextFetch = 0f; // refetch on entering idle mode
_nextGoalAt = 0f;
RefreshNextGoal();
if ( PendingTutorial )
{
PendingTutorial = false;
StartTutorial();
}
}
StateHasChanged();
}
if ( !vis ) return;
// Wall-clock throttle. Never move this into BuildHash or the render path.
if ( RealTime.Now >= _rankNextFetch )
{
_rankNextFetch = RealTime.Now + RankRefreshSeconds;
_ = LoadRank();
}
// SPACE is the detonate key everywhere else in the game, so it fires the
// reactor's manual burst here too.
if ( Input.Pressed( "Jump" ) && R.OverloadReady && !ModalOpen )
R.Overload();
if ( RealTime.Now >= _nextGoalAt )
{
_nextGoalAt = RealTime.Now + 0.5f;
RefreshNextGoal();
}
// Re-measured every frame rather than only on step change: the board
// resizes on a Reactor Frame purchase, cards appear and disappear in the
// left column, and the window can be dragged to another size mid-tour.
if ( TutorialActive ) MeasureTutorial();
float dt = Time.Delta;
for ( int i = _floaters.Count - 1; i >= 0; i-- )
{
_floaters[i].Life -= dt;
if ( _floaters[i].Life <= 0f ) _floaters.RemoveAt( i );
}
for ( int i = _toasts.Count - 1; i >= 0; i-- )
{
_toasts[i].Life -= dt;
if ( _toasts[i].Life <= 0f ) _toasts.RemoveAt( i );
}
if ( _calloutLife > 0f ) _calloutLife -= dt * 1.4f;
}
void Subscribe()
{
var r = R;
if ( r == null || _subscribed ) return;
r.OnPop += OnPop;
r.OnToast += OnToastEvent;
r.OnDetonation += OnDetonation;
// Walkthrough gates — a step that says "press OVERLOAD" only closes when
// the player actually presses OVERLOAD.
r.OnManualOverload += () => TutSatisfy( IdleTutTrigger.Overload );
r.OnUpgradeBought += () => TutSatisfy( IdleTutTrigger.Buy );
r.OnManualCrack += () => TutSatisfy( IdleTutTrigger.CellClick );
_subscribed = true;
}
/// <summary>
/// Rebuilds only when something on screen would actually differ.
///
/// The first version forced a repaint 15 times a second, which meant
/// rebuilding the board, both side columns and every upgrade row constantly
/// — that was the source of the stutter. Between detonations almost nothing
/// changes, so hashing real state instead of a clock cuts the repaint rate
/// by roughly an order of magnitude.
/// </summary>
protected override int BuildHash()
{
if ( !Visible ) return 0;
var hc = new HashCode();
hc.Add( _tab );
hc.Add( _buyAmount );
hc.Add( _showMeltdown );
hc.Add( _showSingularity );
hc.Add( R?.CascadeActive ?? false );
hc.Add( S?.Shards ?? 0 );
hc.Add( S?.ShardsEarned ?? 0 );
hc.Add( S?.Singularities ?? 0 );
hc.Add( R?.SingularityVisible ?? false );
hc.Add( _showIntro );
hc.Add( ModalOpen );
// Walkthrough — geometry is quantised to whole pixels, so a static
// spotlight leaves the hash alone and the panel is not rebuilt.
hc.Add( TutorialActive );
hc.Add( _tutStep );
hc.Add( TutHasSpot );
hc.Add( (int)_spotX ); hc.Add( (int)_spotY );
hc.Add( (int)_spotW ); hc.Add( (int)_spotH );
hc.Add( (int)_cardX ); hc.Add( (int)_cardY );
// Contracts — progress bars, claim state and the reset countdown
for ( int i = 0; i < IdleContracts.DailyCount; i++ )
{
hc.Add( (int)(IdleContracts.Fraction( S, i ) * 200f) );
hc.Add( IdleContracts.IsClaimed( S, i ) );
}
hc.Add( (int)(IdleContracts.SecondsUntilReset() / 60.0) );
var goal = NextGoal;
hc.Add( goal?.Id ?? "" );
hc.Add( goal == null ? 0 : (int)(goal.Fraction( S ) * 200f) );
// Currencies — quantised to what the labels actually show
hc.Add( (long)Cores );
hc.Add( (long)R.Cps );
hc.Add( S.Flux );
hc.Add( S.FluxEarned );
hc.Add( _rankRows.Count );
hc.Add( _myRank );
hc.Add( _rankClimb );
hc.Add( ChainReactionGame.Save?.TotalFragments ?? 0 );
hc.Add( S.TodayReactorFragments );
hc.Add( (int)(R.FragmentProgressFraction * 60f) );
// Live controls
hc.Add( (int)(R.OverloadCooldownLeft * 10f) );
hc.Add( (int)(R.Charge * 100f) );
hc.Add( R.SurgeActive );
hc.Add( (int)R.SurgeTimeLeft );
hc.Add( (int)R.GoldenTimeLeft );
hc.Add( R.Boosts.Count );
hc.Add( R.Boosts.Count > 0 ? (int)R.Boosts[0].SecondsLeft : 0 );
hc.Add( R.FeedStarved );
// Transient effects
hc.Add( _floaters.Count );
hc.Add( (int)(_calloutLife * 20f) );
hc.Add( Time.Now < _shakeUntil ? _shakeCls : "" );
hc.Add( _toasts.Count );
// Board — chest layout, damage, drones and any live flash
hc.Add( R.Size );
float now = Time.Now;
bool animating = _floaters.Count > 0 || _calloutLife > 0f || now < _shakeUntil;
for ( int r = 0; r < R.Size; r++ )
for ( int c = 0; c < R.Size; c++ )
{
var cell = R.Board[r, c];
hc.Add( (int)cell.Chest );
hc.Add( cell.HitsLeft );
hc.Add( cell.HasDrone );
hc.Add( cell.FlashUntil > now ? cell.FlashKind : 0 );
if ( cell.FlashUntil > now ) animating = true;
if ( cell.HasChest && now - cell.SpawnAt < 0.25f ) animating = true;
if ( cell.HasDrone && now - cell.DroneAt < 0.18f ) animating = true;
}
hc.Add( R.GoldenRow );
hc.Add( R.GoldenCol );
hc.Add( R.LastChain );
// Anything mid-flight is drawn from inline styles rather than keyframes,
// so it needs a clock — but only while it is actually moving. The rest of
// the time this hash is stable and the panel is never rebuilt at all.
if ( animating ) hc.Add( (int)(now * 20f) );
return hc.ToHashCode();
}
// ══════════════════════════════════════════════════════════════
// Reactor events
// ══════════════════════════════════════════════════════════════
void OnPop( int row, int col, double value, bool crit )
{
if ( R == null || _floaters.Count > 10 ) return;
float step = 100f / R.Size;
_floaters.Add( new Floater {
Text = "+" + IdleFormat.Short( value ),
Cls = crit ? "crit" : "",
X = step * col + step * 0.5f,
Y = step * row + step * 0.5f,
Life = 0.9f,
MaxLife = 0.9f,
} );
}
void OnToastEvent( string icon, string text, string cls )
{
_toasts.Insert( 0, new ToastMsg { Icon = icon, Text = text, Cls = cls, Life = 3.2f } );
while ( _toasts.Count > 4 ) _toasts.RemoveAt( _toasts.Count - 1 );
}
void OnDetonation( int chain, double payout, bool crit )
{
string shake = crit || chain >= 25 ? "idle-shake-3"
: chain >= 12 ? "idle-shake-2"
: chain >= 4 ? "idle-shake-1"
: "";
if ( shake != "" )
{
_shakeCls = shake;
_shakeUntil = Time.Now + (shake == "idle-shake-3" ? 0.45f : 0.28f);
}
if ( chain < 15 && !crit ) return;
double critPower = IdleBalance.CritPower( S?.Level( IdleUpgradeId.CritPower ) ?? 0 );
_calloutText = crit
? $"CRIT ×{critPower:F0} · +{IdleFormat.Short( payout )}"
: $"{chain} CHAIN · +{IdleFormat.Short( payout )}";
_calloutCls = crit ? "crit" : chain >= 35 ? "mega" : "big";
_calloutLife = 1f;
}
// ══════════════════════════════════════════════════════════════
// Input
// ══════════════════════════════════════════════════════════════
void SetTab( string tab )
{
bool changed = _tab != tab;
_tab = tab;
Sound.Play( "UI_Button" );
if ( changed ) TutSatisfy( IdleTutTrigger.TabChanged );
}
void SetBuyAmount( int amount ) { _buyAmount = amount; Sound.Play( "UI_Button" ); }
int BuyCount( IdleUpgradeId id )
=> _buyAmount < 0 ? Math.Max( 1, R.MaxAffordable( id ) ) : _buyAmount;
void OnBuy( IdleUpgradeId id )
{
if ( R == null ) return;
if ( !R.TryBuy( id, BuyCount( id ) ) ) Sound.Play( "Pop_Button_1" );
}
void OnBuyPerk( IdlePerkId id )
{
if ( R == null ) return;
if ( !R.TryBuyPerk( id ) ) Sound.Play( "Pop_Button_1" );
}
void OnOverload() => R?.Overload();
void OnSurge() => R?.ActivateSurge();
void OnCellClick( int r, int c ) => R?.ClickCell( r, c );
void OnClaimOffline() => R?.ClaimOffline();
void OnClaimDaily() => R?.ClaimDaily();
void OnOpenMeltdown() { _showMeltdown = true; Sound.Play( "UI_Button" ); }
void OnCloseMeltdown() { _showMeltdown = false; Sound.Play( "UI_Button" ); }
void OnOpenSingularity()
{
_showSingularity = true;
Sound.Play( "UI_Button" );
if ( S != null && !S.SingularitySeen )
{
S.SingularitySeen = true;
ChainReactionGame.Save?.Save();
}
}
void OnCloseSingularity() { _showSingularity = false; Sound.Play( "UI_Button" ); }
void OnSingularity()
{
_showSingularity = false;
R?.DoSingularity();
_tab = "powers";
}
void OnBuyPower( IdlePowerId id ) => R?.BuyPower( id );
void OnMeltdown()
{
_showMeltdown = false;
R?.DoMeltdown();
}
void OnCloseIntro()
{
_showIntro = false;
if ( S != null )
{
S.IntroShown = true;
S.TutorialOffered = true;
ChainReactionGame.Save?.Save();
}
Sound.Play( "PlayButton" );
}
void OnIntroTutorial()
{
if ( S != null )
{
S.IntroShown = true;
ChainReactionGame.Save?.Save();
}
Sound.Play( "PlayButton" );
StartTutorial();
}
/// <summary>The ? button in the top bar — replayable from anywhere, any time.</summary>
void OnStartTutorial()
{
Sound.Play( "UI_Button" );
if ( _tutRunning ) { EndTutorial( completed: true ); return; }
StartTutorial();
}
void OnClaimContract( int slot )
{
if ( R == null ) return;
if ( !R.ClaimContract( slot ) ) Sound.Play( "Pop_Button_1" );
}
void OnBack()
{
Sound.Play( "UI_Button" );
Game?.ReturnToMenu();
}
// ══════════════════════════════════════════════════════════════
// Class helpers
// ══════════════════════════════════════════════════════════════
string Pct( float f ) => (Math.Clamp( f, 0f, 1f ) * 100f).ToString( "F1" );
string GlowClass() => R.SurgeActive ? "surging" : "";
string SurgeClass() => R.SurgeActive ? "surging" : "";
// Surge is the louder event and its styles are declared first, so a cascade
// would otherwise paint over it. Let surge own the board while it lasts.
string CascadeClass() => R.CascadeActive && !R.SurgeActive ? "cascading" : "";
string SurgeReadyClass() => R.SurgeReady ? "ready" : "";
string OverloadClass() => R.OverloadReady ? "ready" : "cooling";
string BoardShake() => Time.Now < _shakeUntil ? _shakeCls : "";
string TabClass( string t ) => _tab == t ? "on" : "";
string BuyAmtClass( int amt ) => _buyAmount == amt ? "on" : "";
string MaxedClass( bool maxed ) => maxed ? "maxed" : "";
string CapClass() => R.FragmentsToday >= R.DailyFragmentCap ? "capped" : "";
string FeedClass() => R.FeedStarved ? "starved" : "";
string MeltdownStripClass() => R.CanMeltdown ? "ready" : "";
string StreakPipClass( int day )
{
int streak = S?.DailyStreak ?? 1;
if ( day == streak ) return "on today";
return day < streak ? "on" : "";
}
// ══════════════════════════════════════════════════════════════
// Text helpers — one composed string per element (see note at top)
// ══════════════════════════════════════════════════════════════
// ── Daily contracts ───────────────────────────────────────────
string ContractsCardClass() => R.ContractReady ? "ready" : "";
string ContractClass( int slot )
{
if ( IdleContracts.IsClaimed( S, slot ) ) return "claimed";
if ( IdleContracts.IsComplete( S, slot ) ) return "ready";
return "";
}
string ContractIcon( int slot )
{
if ( IdleContracts.IsClaimed( S, slot ) ) return "✅";
return IdleContracts.At( S, slot )?.Icon ?? "📋";
}
string ContractDescLabel( int slot )
{
var c = IdleContracts.At( S, slot );
if ( c == null ) return "";
return c.Desc( IdleContracts.Target( S, slot ) );
}
string ContractRewardLabel( int slot )
{
if ( IdleContracts.IsClaimed( S, slot ) ) return "CLAIMED";
if ( IdleContracts.IsComplete( S, slot ) ) return "CLAIM →";
return $"+{IdleContracts.Reward( S, slot )} 💠";
}
string ContractProgLabel( int slot )
{
var c = IdleContracts.At( S, slot );
if ( c == null ) return "";
double cur = Math.Min( IdleContracts.Progress( S, slot ), IdleContracts.Target( S, slot ) );
double tgt = IdleContracts.Target( S, slot );
if ( c.Id == "runtime" )
return $"{IdleFormat.Duration( cur )} / {IdleFormat.Duration( tgt )}";
return c.BigNumbers
? $"{IdleFormat.Short( cur )} / {IdleFormat.Short( tgt )}"
: $"{(long)cur} / {(long)tgt}";
}
string ContractResetLabel()
=> "resets in " + IdleFormat.Duration( IdleContracts.SecondsUntilReset() );
string ContractFootLabel()
{
int streak = S?.ContractStreak ?? 0;
if ( IdleContracts.AllClaimed( S ) )
return streak > 1
? $"All three cleared · {streak} days running"
: "All three cleared. Back tomorrow.";
return "Clear all three for a ×3 boost and double Fragments on the last one.";
}
// ── Next goal pin ─────────────────────────────────────────────
/// <summary>
/// The unfinished Reactor Log entry closest to completion. Picking by
/// fraction rather than by list order means the pin always shows something
/// nearly done, which is the only version of this card that pulls.
///
/// Cached: BuildHash runs every frame and the markup reads this five more
/// times per rebuild, so recomputing it on every access would walk the
/// achievement list a hundred-odd times a frame for a card that changes
/// once every few minutes.
/// </summary>
IdleAchievement _nextGoal;
float _nextGoalAt;
IdleAchievement NextGoal => _nextGoal;
void RefreshNextGoal()
{
var s = S;
if ( s == null ) { _nextGoal = null; return; }
IdleAchievement best = null;
float bestFrac = -1f;
foreach ( var a in IdleAchievements.All )
{
if ( s.HasAchievement( a.Id ) ) continue;
float f = a.Fraction( s );
if ( f > bestFrac ) { bestFrac = f; best = a; }
}
_nextGoal = best;
}
string NextGoalProgLabel()
{
var a = NextGoal;
if ( a == null ) return "";
return ProgressLabel( a );
}
string NextGoalRewardLabel()
{
var a = NextGoal;
if ( a == null ) return "";
return RewardLabel( a );
}
string CoresLabel() => IdleFormat.Short( Cores );
string RateLabel() => IdleFormat.Short( R.Cps );
string FluxLabel() => (S?.Flux ?? 0).ToString();
string FragLabel() => (ChainReactionGame.Save?.TotalFragments ?? 0).ToString();
string GlobalMultLabel() => "×" + IdleFormat.Short( R.GlobalMultiplier );
string ExponentLabel() => R.ChainExponent.ToString( "F2" );
string LastChainLabel() => $"{R.LastChain} chests";
string BestChainLabel() => $"{S.BestChain} chests";
string BoardLabel() => $"{R.Size} × {R.Size}";
string FeedLabel() => R.FeedStarved ? "starving" : "keeping up";
string SurgeBoostLabel() => $"SURGE ×{IdleBalance.SurgeMultiplier:F0}";
string BoostLabel( IdleReactor.Boost b ) => $"{b.Label} ×{b.Mult:F1}";
string SecondsLabel( float s ) => $"{s:F0}s";
string CondenserLabel()
=> $"next at {IdleFormat.Short( R.FragmentCost )} Cores · price climbs with each one";
string FragTodayLabel() => $"{R.FragmentsToday} / {R.DailyFragmentCap}";
string TierBlurb( int index, bool unlocked )
{
var tier = IdleBalance.Tiers[index];
if ( unlocked ) return tier.Blurb;
// Tier Memory can drop the gate to zero, so read the effective figure
// rather than the catalog one.
double need = IdleBalance.TierRequirement( index, S?.PowerLevel( IdlePowerId.TierSkip ) ?? 0 );
return $"unlocks at {IdleFormat.Short( need )} cycle Cores";
}
string GoldenTimerLabel() => R.GoldenTimeLeft.ToString( "F0" );
string OverloadLabel()
=> R.OverloadReady ? "⚡ OVERLOAD" : $"{R.OverloadCooldownLeft:F1}s";
string OverloadSubLabel() => $"×{IdleBalance.OverloadMultiplier:F1} burst · SPACE";
string OverloadFillPct() => ((1f - R.OverloadCooldownFraction) * 100f).ToString( "F0" );
string UnleashLabel() => $"UNLEASH ×{IdleBalance.SurgeMultiplier:F0}";
string SurgeLabel()
{
if ( R.SurgeActive ) return $"SURGE {R.SurgeTimeLeft:F0}s";
if ( R.SurgeReady ) return "SURGE READY";
return $"CHARGE {R.Charge * 100f:F0}%";
}
string MeltdownLabel()
=> R.CanMeltdown
? $"Meltdown available — +{R.PendingFlux} Flux"
: $"Meltdown unlocks at {IdleFormat.Short( R.MeltdownRequirement )} cycle Cores · {IdleFormat.Short( S.CycleCores )} banked";
string BuyAmountLabel( int amount ) => amount < 0 ? "MAX" : $"×{amount}";
string UpgradeClass( bool revealed, bool maxed, bool afford )
{
if ( !revealed ) return "locked";
if ( maxed ) return "maxed";
return afford ? "afford" : "poor";
}
string UpgradeIcon( IdleUpgrade up, bool revealed ) => revealed ? up.Icon : "🔒";
string UpgradeName( IdleUpgrade up, bool revealed ) => revealed ? up.Name : "Locked";
string UpgradeDesc( IdleUpgrade up, bool revealed )
=> revealed ? up.Desc : $"Reach {IdleFormat.Short( up.RevealAt )} lifetime Cores to reveal.";
string LevelLabel( IdleUpgrade up, int lvl, bool revealed )
{
if ( !revealed ) return "—";
return up.MaxLevel < 100 ? $"Lv {lvl} / {up.MaxLevel}" : $"Lv {lvl}";
}
string CostLabel( bool maxed, double cost, int count )
{
if ( maxed ) return "MAXED";
return count > 1
? $"{IdleFormat.Short( cost )} ×{count}"
: IdleFormat.Short( cost );
}
string PerkLevelLabel( IdlePerk p, int lvl ) => p.Uncapped ? $"Lv {lvl}" : $"Lv {lvl} / {p.MaxLevel}";
string PerkCostLabel( bool maxed, int cost ) => maxed ? "MAXED" : $"🔷 {cost}";
/// <summary>
/// Containment Field is deliberately hidden until the player has enough Flux
/// history to understand what a sink is for — offered at Flux 0 it just reads
/// as a bad first purchase.
/// </summary>
IEnumerable<IdlePerk> VisiblePerks()
=> IdleUpgradeCatalog.Perks.Where( p => p.RevealAtFluxEarned <= 0
|| (S?.FluxEarned ?? 0) >= p.RevealAtFluxEarned
|| (S?.PerkLevel( p.Id ) ?? 0) > 0 );
string PowerLevelLabel( IdlePower p, int lvl ) => $"Lv {lvl} / {p.MaxLevel}";
string PowerCostLabel( bool maxed, int cost ) => maxed ? "MAXED" : $"✴️ {cost}";
string ShardHeadLabel() => $"✴️ {S.Shards}";
string ShardSubLabel()
=> $"{S.ShardsEarned} earned · ×{IdleBalance.ShardMultiplier( S.ShardsEarned ):F2} permanent income";
string SingStripClass() => R.CanSingularity ? "ready" : "";
string SingularityLabel()
{
if ( R.CanSingularity )
return $"Singularity available — +{R.PendingShards} Shards";
return $"Singularity unlocks at {IdleBalance.SingularityRequirement} lifetime Flux · {S.FluxEarned} earned";
}
string SingPayoutLabel() => "+" + R.PendingShards.ToString();
string BurnedFluxLabel() => $"{S.FluxEarned} lifetime Flux";
string ShardKeepLabel()
{
int after = S.ShardsEarned + R.PendingShards;
return $"×{IdleBalance.ShardMultiplier( after ):F2} income from Shards";
}
string FluxHeadLabel() => $"🔷 {S.Flux}";
string FluxSubLabel()
=> $"{S.FluxEarned} earned · ×{IdleBalance.FluxMultiplier( S.FluxEarned ):F2} permanent income";
string LogHeadLabel() => $"{IdleAchievements.CompletedCount( S )} / {IdleAchievements.All.Count}";
string AchIcon( IdleAchievement a, bool done ) => done ? a.Icon : "🔘";
string RewardLabel( IdleAchievement a )
{
if ( a.RewardFlux > 0 && a.RewardFrags > 0 ) return $"+{a.RewardFlux}🔷 +{a.RewardFrags}💠";
if ( a.RewardFlux > 0 ) return $"+{a.RewardFlux}🔷";
return $"+{a.RewardFrags}💠";
}
string ProgressLabel( IdleAchievement a )
{
double cur = a.Progress( S );
return a.BigNumbers
? $"{IdleFormat.Short( cur )} / {IdleFormat.Short( a.Target )}"
: $"{(long)cur} / {(long)a.Target}";
}
// ── Modal labels ──────────────────────────────────────────────
string OfflineSubLabel()
=> $"The reactor ran for {IdleFormat.Duration( R.PendingOfflineSeconds )} at {IdleFormat.Pct( R.OfflineEfficiency )} efficiency.";
string OfflineAmountLabel() => "+" + IdleFormat.Short( R.PendingOfflineCores );
string OfflineFragLabel() => $"condensing {R.PendingOfflineFrags} Fragments";
string OfflineWarnLabel()
=> $"Storage filled up after {R.OfflineCapHours:F0}h. Upgrade Core Storage to bank longer trips.";
string DailyTitleLabel() => $"Day {S?.DailyStreak ?? 1}";
string DailyMultLabel() => $"×{IdleBalance.DailyReward( S?.DailyStreak ?? 1 ).mult:F1}";
string DailyRewardLabel()
{
var reward = IdleBalance.DailyReward( S?.DailyStreak ?? 1 );
return $"for {IdleFormat.Duration( reward.seconds )} · +{reward.frags} Fragments";
}
string MeltPayoutLabel() => "+" + R.PendingFlux;
string BurnedCoresLabel() => $"{IdleFormat.Short( Cores )} Cores";
string MeltMultBeforeLabel()
=> "×" + IdleBalance.FluxMultiplier( S?.FluxEarned ?? 0 ).ToString( "F2" );
string MeltMultAfterLabel()
=> "×" + IdleBalance.FluxMultiplier( (S?.FluxEarned ?? 0) + R.PendingFlux ).ToString( "F2" );
// ══════════════════════════════════════════════════════════════
// Cell rendering
// ══════════════════════════════════════════════════════════════
string CellClass( IdleReactor.Cell cell, bool golden )
{
if ( golden ) return "golden";
string cls = cell.HasChest ? "filled" : "empty";
if ( cell.FlashUntil > Time.Now )
{
cls += cell.FlashKind switch
{
3 => " fx-drone",
2 => " fx-kill",
_ => " fx-blast",
};
}
if ( cell.HasDrone ) cls += " armed";
if ( cell.HasChest )
{
cls += cell.Chest switch
{
GridManager.ChestType.Gem => " c-gem",
GridManager.ChestType.BombChest => " c-bomb",
GridManager.ChestType.Locked => " c-locked",
GridManager.ChestType.Cursed => " c-cursed",
GridManager.ChestType.Void => " c-void",
GridManager.ChestType.Boss => " c-boss",
_ => " c-normal",
};
}
return cls;
}
/// <summary>
/// Spawn pop, computed here rather than as a keyframe: the panel can rebuild
/// at any moment and a class-driven animation would restart mid-flight.
/// </summary>
string ChestStyle( IdleReactor.Cell cell )
{
float age = Time.Now - cell.SpawnAt;
if ( age >= 0.25f || age < 0f ) return "";
float scale = 0.4f + 0.6f * (age / 0.25f);
return $"transform:scale({scale.ToString( "F2" )});";
}
/// <summary>Drone drop — falls onto the cell as the volley arms.</summary>
string DroneStyle( IdleReactor.Cell cell )
{
float age = Time.Now - cell.DroneAt;
if ( age >= 0.18f || age < 0f ) return "";
float t = age / 0.18f;
float scale = 2.0f - 1.0f * t;
return $"transform:scale({scale.ToString( "F2" )});opacity:{t.ToString( "F2" )};";
}
string ChestGlyph( IdleReactor.Cell cell ) => cell.Chest switch
{
GridManager.ChestType.BombChest => "💣",
GridManager.ChestType.Locked => "🔒",
GridManager.ChestType.Cursed => "💀",
GridManager.ChestType.Void => "🌑",
GridManager.ChestType.Boss => "👑",
GridManager.ChestType.Gem => "💎",
_ => "🎁",
};
}
Game
game
$primary: red !default;
$primary-alt: white !default;
$switch-padding: 6px !default;
.checkbox.switch
{
cursor: pointer;
> .checkmark
{
font-size: 22px;
border: 0px solid $primary;
border-radius: 100px;
text-align: center;
justify-content: center;
align-items: center;
color: $primary-alt;
padding: $switch-padding;
padding-right: 32px;
padding-left: $switch-padding;
transition: all 0.3s ease;
background-color: rgba( $primary, 0.1 );
> .handle
{
background-color: $primary-alt;
width: 20px;
height: 20px;
border-radius: 100px;
box-shadow: 2px 2px 12px black;
}
}
&.checked
{
> .checkmark
{
background-color: $primary;
padding-left: 32px;
padding-right: $switch-padding;
}
}
&:active
{
transform: scale( 0.9 );
transform-origin: 20px 50%;
}
}
Game
game
VectorControl
{
gap: 2px;
flex-grow: 1;
}
VectorControl NumberEntry
{
flex-basis: 50%;
}
Game
game
EnumControl
{
gap: 2px;
flex-grow: 1;
}
EnumControl DropDown,
EnumControl ButtonGroup
{
border-radius: 8px;
background-color: #000a;
flex-grow: 1;
}
EnumControl DropDown
{
flex-grow: 1;
min-height: 32px;
}
EnumControl ButtonGroup
{
border-radius: 12px;
overflow: hidden;
min-height: 32px;
Button
{
flex-grow: 1;
justify-content: center;
align-items: center;
gap: 4px;
color: #aaa;
font-size: 1rem;
cursor: pointer;
.icon
{
color: #08f;
}
&:hover
{
color: #ddd;
.icon
{
color: #3af;
}
}
&:active
{
background-color: #04a;
color: white;
transform: translateX( 1px ) translateY( 1px );
.icon
{
color: #fff;
}
}
&.active
{
background-color: #08f;
color: white;
pointer-events: none;
.icon
{
color: #fff;
}
}
}
}
Game
game
ColorPickerControl
{
flex-direction: column;
flex-shrink: 0;
gap: 0.5rem;
margin: 1rem;
}
Game
game
/* ══════════════════════════════════════════════════════════════════════════
THE REACTOR — Idle mode HUD
Palette leans teal/amber against the same deep purple-black the rest of
10XPLOSION uses, so it reads as "the same game, different room".
══════════════════════════════════════════════════════════════════════════ */
root {
/* Declared so no panel in this tree ever claims the OS pointer -- a hovered
panel outranks Mouse.CursorType, and one that declares nothing still claims
the default arrow. GameCursor draws the cursor instead. */
cursor: none;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
overflow: hidden;
/* Sits above the wave HUD's layers — those hide themselves in this mode,
but ScreenShaker and PopupLayer share the same ScreenPanel. */
z-index: 200;
}
.idle-root {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
pointer-events: none;
}
/* ── Backdrop ─────────────────────────────────────────────────────────── */
.idle-bg {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #04070d;
}
.idle-bg-glow {
position: absolute;
top: -20%; left: 15%;
width: 70%;
height: 120%;
border-radius: 50%;
background-color: rgba(20, 130, 130, 0.10);
filter: blur(120px);
animation: idle-breathe 9s ease-in-out infinite alternate;
transition: background-color 0.5s;
}
.idle-bg-glow.surging {
background-color: rgba(251, 146, 60, 0.22);
}
@keyframes idle-breathe {
0% { transform: scale(1.00); opacity: 0.75; }
100% { transform: scale(1.18); opacity: 1.00; }
}
.idle-particle {
position: absolute;
bottom: -12px;
border-radius: 50%;
animation: idle-rise linear infinite;
}
@keyframes idle-rise {
0% { transform: translateY(0px); opacity: 0; }
12% { opacity: 1; }
100% { transform: translateY(-1180px); opacity: 0; }
}
/* ── Top bar ──────────────────────────────────────────────────────────── */
.idle-top {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding: 12px 22px;
flex-shrink: 0;
background-color: rgba(6, 12, 20, 0.82);
border-bottom: 2px solid rgba(45, 212, 191, 0.18);
pointer-events: all;
overflow: hidden;
}
.idle-back {
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: 10px;
border: 2px solid rgba(148, 163, 184, 0.28);
background-color: rgba(15, 23, 35, 0.9);
color: rgba(226, 232, 240, 0.85);
font-size: 22px;
font-weight: 900;
align-items: center;
justify-content: center;
cursor: none;
pointer-events: all;
transition: background-color 0.1s, border-color 0.1s;
}
.idle-back:hover {
background-color: rgba(45, 212, 191, 0.18);
border-color: rgba(45, 212, 191, 0.6);
}
/* Without an explicit no-shrink the currency row squeezes this to nothing and
the title wraps one letter per line. */
.idle-brand {
display: flex;
flex-direction: column;
flex-shrink: 0;
flex-grow: 0;
}
.idle-brand-title {
white-space: nowrap;
font-size: 22px;
font-weight: 900;
letter-spacing: 0.24em;
color: #5eead4;
text-shadow: 0px 0px 18px rgba(45, 212, 191, 0.45);
}
.idle-brand-sub {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(148, 163, 184, 0.55);
}
.idle-currencies {
display: flex;
flex-direction: row;
gap: 8px;
margin-left: auto;
flex-shrink: 0;
flex-grow: 0;
}
.idle-cur {
display: flex;
flex-direction: row;
align-items: center;
gap: 7px;
padding: 7px 13px;
border-radius: 10px;
background-color: rgba(12, 20, 30, 0.92);
border: 2px solid rgba(100, 116, 139, 0.22);
width: 172px;
flex-shrink: 0;
flex-grow: 0;
overflow: hidden;
}
.idle-cur-icon { font-size: 17px; flex-shrink: 0; }
.idle-cur-val {
font-size: 19px;
font-weight: 900;
color: #e2e8f0;
white-space: nowrap;
flex-shrink: 0;
}
.idle-cur-lbl {
font-size: 9px;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(148, 163, 184, 0.55);
margin-left: auto;
white-space: nowrap;
flex-shrink: 0;
}
.idle-cur.cores { border-color: rgba(45, 212, 191, 0.45); }
.idle-cur.cores .idle-cur-val { color: #5eead4; }
.idle-cur.rate .idle-cur-val { color: #a7f3d0; font-size: 17px; }
.idle-cur.flux { border-color: rgba(96, 165, 250, 0.42); width: 132px; }
.idle-cur.flux .idle-cur-val { color: #93c5fd; }
.idle-cur.frag { border-color: rgba(167, 139, 250, 0.42); width: 152px; }
.idle-cur.frag .idle-cur-val { color: #c4b5fd; }
/* ── Body layout ──────────────────────────────────────────────────────── */
.idle-body {
position: relative;
display: flex;
flex-direction: row;
align-items: stretch;
gap: 14px;
padding: 14px 20px 16px 20px;
flex-grow: 1;
flex-shrink: 1;
height: 0; /* lets flex-grow decide the height, so children can scroll */
overflow: hidden;
}
.idle-col { display: flex; flex-direction: column; gap: 12px; }
.idle-left {
width: 300px;
flex-shrink: 0;
overflow-y: scroll;
overflow-x: hidden;
pointer-events: all;
}
.idle-right {
width: 400px;
flex-shrink: 0;
overflow: hidden;
pointer-events: all;
}
.idle-center {
flex-grow: 1;
align-items: center;
justify-content: center;
gap: 16px;
pointer-events: all;
}
/* ── Info cards ───────────────────────────────────────────────────────── */
.idle-card {
background-color: rgba(10, 17, 26, 0.88);
border: 2px solid rgba(100, 116, 139, 0.18);
border-radius: 12px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 6px;
flex-shrink: 0;
}
.idle-card-hd {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(94, 234, 212, 0.75);
margin-bottom: 4px;
}
.idle-card-note {
font-size: 11px;
font-weight: 600;
color: rgba(148, 163, 184, 0.65);
}
.idle-card-note.dim { color: rgba(251, 191, 36, 0.75); }
.idle-stat-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.idle-stat-lbl {
font-size: 12px;
font-weight: 600;
color: rgba(148, 163, 184, 0.75);
}
.idle-stat-val {
font-size: 14px;
font-weight: 900;
color: #e2e8f0;
}
.idle-stat-val.hot { color: #5eead4; }
.idle-stat-val.capped { color: #fbbf24; }
.idle-stat-val.starved { color: #f87171; }
/* ── Reactor rank card ───────────────────────────────────────── */
.idle-rank-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 3px 6px;
border-radius: 6px;
border: 2px solid rgba(0, 0, 0, 0);
}
.idle-rank-row.me {
background-color: rgba(94, 234, 212, 0.10);
border-color: rgba(94, 234, 212, 0.35);
}
.idle-rank-pos {
font-size: 11px;
font-weight: 900;
color: rgba(148, 163, 184, 0.75);
width: 26px;
flex-shrink: 0;
}
.idle-rank-row.me .idle-rank-pos { color: #5eead4; }
.idle-rank-name {
font-size: 12px;
font-weight: 700;
color: rgba(226, 232, 240, 0.85);
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.idle-rank-row.me .idle-rank-name { color: #e2e8f0; }
.idle-rank-val {
font-size: 12px;
font-weight: 900;
color: rgba(148, 163, 184, 0.9);
flex-shrink: 0;
}
.idle-rank-row.me .idle-rank-val { color: #5eead4; }
.idle-card-note.up {
color: rgba(74, 222, 128, 0.9);
font-weight: 700;
}
.idle-bar-track {
width: 100%;
height: 10px;
border-radius: 5px;
background-color: rgba(30, 41, 59, 0.9);
overflow: hidden;
}
.idle-bar-fill {
height: 100%;
border-radius: 5px;
background-color: #5eead4;
transition: width 0.2s;
}
.idle-bar-fill.frag { background-color: #a78bfa; }
/* Boosts */
.idle-boost {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 8px;
background-color: rgba(30, 41, 59, 0.7);
border-left: 3px solid rgba(94, 234, 212, 0.7);
}
.idle-boost.surge {
border-left-color: #fb923c;
background-color: rgba(120, 53, 15, 0.45);
}
.idle-boost-icon { font-size: 15px; }
.idle-boost-lbl { font-size: 12px; font-weight: 800; color: #e2e8f0; }
.idle-boost-time {
margin-left: auto;
font-size: 11px;
font-weight: 900;
color: rgba(251, 191, 36, 0.9);
}
/* Chest tiers */
.idle-tier {
display: flex;
flex-direction: row;
align-items: center;
gap: 9px;
padding: 5px 0px;
opacity: 0.42;
}
.idle-tier.on { opacity: 1; }
.idle-tier-icon { font-size: 20px; width: 26px; text-align: center; }
.idle-tier-body { display: flex; flex-direction: column; flex-grow: 1; }
.idle-tier-name { font-size: 12px; font-weight: 900; color: #e2e8f0; }
.idle-tier-blurb {
font-size: 10px;
font-weight: 600;
color: rgba(148, 163, 184, 0.7);
}
.idle-tier.on .idle-tier-name { color: #5eead4; }
/* ── The board ────────────────────────────────────────────────────────── */
.idle-board-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.idle-board {
display: flex;
flex-direction: column;
gap: 5px;
padding: 14px;
border-radius: 16px;
background-color: rgba(6, 14, 22, 0.9);
border: 2px solid rgba(45, 212, 191, 0.22);
box-shadow: 0px 0px 40px rgba(13, 148, 136, 0.14);
transition: border-color 0.3s, box-shadow 0.3s;
}
.idle-board.surging {
border-color: rgba(251, 146, 60, 0.65);
box-shadow: 0px 0px 60px rgba(251, 146, 60, 0.35);
}
.idle-row { display: flex; flex-direction: row; gap: 5px; }
.idle-cell {
position: relative;
border-radius: 9px;
background-color: rgba(17, 27, 40, 0.85);
border: 2px solid rgba(71, 85, 105, 0.28);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: none;
pointer-events: all;
overflow: hidden;
}
.idle-cell.empty { background-color: rgba(13, 21, 32, 0.7); }
.idle-cell.filled { border-color: rgba(148, 163, 184, 0.35); }
.idle-chest {
font-size: 30px;
text-shadow: 0px 2px 6px rgba(0, 0, 0, 0.6);
}
/* chest type tinting */
.idle-cell.c-normal { background-color: rgba(59, 44, 20, 0.55); border-color: rgba(217, 160, 60, 0.45); }
.idle-cell.c-gem { background-color: rgba(14, 60, 78, 0.6); border-color: rgba(56, 189, 248, 0.55); }
.idle-cell.c-bomb { background-color: rgba(70, 24, 24, 0.62); border-color: rgba(248, 113, 113, 0.6); }
.idle-cell.c-locked { background-color: rgba(45, 39, 74, 0.65); border-color: rgba(167, 139, 250, 0.6); }
.idle-cell.c-cursed { background-color: rgba(56, 20, 52, 0.7); border-color: rgba(232, 121, 249, 0.65); }
.idle-cell.c-void { background-color: rgba(10, 10, 20, 0.95); border-color: rgba(129, 140, 248, 0.7); }
.idle-cell.c-boss { background-color: rgba(80, 55, 8, 0.75); border-color: rgba(251, 191, 36, 0.85); }
/* detonation feedback */
.idle-cell.fx-blast {
background-color: rgba(94, 234, 212, 0.30);
border-color: rgba(153, 246, 228, 0.85);
}
.idle-cell.fx-kill {
background-color: rgba(251, 191, 36, 0.55);
border-color: rgba(254, 240, 138, 0.95);
}
.idle-cell.fx-drone {
background-color: rgba(248, 113, 113, 0.6);
border-color: rgba(254, 202, 202, 0.95);
}
.idle-hits {
position: absolute;
bottom: 2px;
right: 4px;
font-size: 11px;
font-weight: 900;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.9);
}
/* A drone sits on its target for a beat before the volley goes off, so the
explosion has something to come from. */
.idle-drone {
position: absolute;
font-size: 26px;
text-shadow: 0px 0px 10px rgba(248, 113, 113, 0.9);
}
.idle-cell.armed {
border-color: rgba(248, 113, 113, 0.95);
background-color: rgba(80, 20, 20, 0.55);
}
/* golden chest */
.idle-cell.golden {
background-color: rgba(120, 82, 6, 0.85);
border-color: #fde047;
box-shadow: 0px 0px 26px rgba(253, 224, 71, 0.7);
animation: idle-golden-pulse 0.55s ease-in-out infinite alternate;
}
@keyframes idle-golden-pulse {
0% { transform: scale(1.00); }
100% { transform: scale(1.10); }
}
.idle-golden { font-size: 32px; }
.idle-golden-timer {
position: absolute;
bottom: 1px;
font-size: 11px;
font-weight: 900;
color: #fef9c3;
}
/* floating values */
.idle-float {
position: absolute;
font-size: 17px;
font-weight: 900;
color: #a7f3d0;
text-shadow: 0px 2px 6px rgba(0, 0, 0, 0.9);
pointer-events: none;
}
.idle-float.crit {
font-size: 22px;
color: #fde047;
}
.idle-callout {
position: absolute;
top: 40%;
font-size: 34px;
font-weight: 900;
letter-spacing: 0.06em;
color: #5eead4;
text-shadow: 0px 0px 22px rgba(45, 212, 191, 0.8), 0px 3px 8px rgba(0, 0, 0, 0.9);
pointer-events: none;
}
.idle-callout.big { color: #a7f3d0; }
.idle-callout.mega { color: #fbbf24; font-size: 40px; }
.idle-callout.crit { color: #fb923c; font-size: 44px; }
/* ── Controls ─────────────────────────────────────────────────────────── */
.idle-controls {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.idle-overload {
position: relative;
overflow: hidden;
width: 290px;
padding-top: 14px;
padding-bottom: 14px;
border-radius: 14px;
border: 3px solid rgba(248, 113, 113, 0.55);
background-color: rgba(60, 12, 12, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
cursor: none;
pointer-events: all;
transition: transform 0.06s, border-color 0.12s;
}
.idle-overload.ready {
border-color: #f87171;
animation: idle-overload-pulse 1.4s ease-in-out infinite alternate;
}
.idle-overload.cooling { opacity: 0.62; cursor: none; }
.idle-overload:active { transform: translateY(2px); }
@keyframes idle-overload-pulse {
0% { box-shadow: 0px 0px 12px rgba(248, 113, 113, 0.25); }
100% { box-shadow: 0px 0px 34px rgba(248, 113, 113, 0.65); }
}
.idle-overload-fill {
position: absolute;
left: 0; top: 0; bottom: 0;
background-color: rgba(248, 113, 113, 0.18);
pointer-events: none;
}
.idle-overload-label {
font-size: 20px;
font-weight: 900;
letter-spacing: 0.14em;
color: #fecaca;
}
.idle-overload-sub {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(254, 202, 202, 0.6);
}
.idle-surge-wrap {
display: flex;
flex-direction: column;
gap: 8px;
width: 300px;
}
.idle-surge-track {
position: relative;
height: 34px;
border-radius: 9px;
background-color: rgba(15, 23, 35, 0.92);
border: 2px solid rgba(148, 163, 184, 0.24);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.idle-surge-track.ready {
border-color: #fb923c;
animation: idle-overload-pulse 0.9s ease-in-out infinite alternate;
}
.idle-surge-fill {
position: absolute;
left: 0; top: 0; bottom: 0;
background-color: rgba(251, 146, 60, 0.42);
transition: width 0.2s;
}
.idle-surge-lbl {
position: relative;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.16em;
color: #fed7aa;
}
.idle-surge-btn {
padding-top: 10px;
padding-bottom: 10px;
border-radius: 9px;
border: 2px solid #fb923c;
background-color: rgba(154, 52, 18, 0.85);
color: #ffedd5;
font-size: 14px;
font-weight: 900;
letter-spacing: 0.12em;
align-items: center;
justify-content: center;
cursor: none;
pointer-events: all;
}
.idle-surge-btn:hover { background-color: rgba(194, 65, 12, 0.95); }
/* ── Meltdown strip ───────────────────────────────────────────────────── */
.idle-meltdown-strip {
display: flex;
flex-direction: row;
align-items: center;
gap: 14px;
width: 620px;
padding: 10px 16px;
border-radius: 12px;
background-color: rgba(10, 17, 26, 0.85);
border: 2px solid rgba(100, 116, 139, 0.18);
flex-shrink: 0;
}
.idle-meltdown-strip.ready {
border-color: rgba(251, 191, 36, 0.7);
background-color: rgba(69, 26, 3, 0.6);
}
.idle-melt-icon { font-size: 20px; flex-shrink: 0; }
.idle-melt-text {
font-size: 12px;
font-weight: 700;
color: rgba(203, 213, 225, 0.85);
flex-grow: 1;
}
.idle-meltdown-strip.ready .idle-melt-text { color: #fde68a; font-weight: 900; }
.idle-melt-bar {
margin-left: auto;
width: 180px;
height: 8px;
border-radius: 4px;
background-color: rgba(30, 41, 59, 0.9);
overflow: hidden;
}
.idle-melt-fill {
height: 100%;
background-color: #fbbf24;
border-radius: 4px;
}
.idle-melt-btn {
margin-left: auto;
padding: 8px 20px;
border-radius: 9px;
border: 2px solid #fbbf24;
background-color: rgba(120, 53, 15, 0.9);
color: #fef3c7;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.14em;
cursor: none;
pointer-events: all;
}
.idle-melt-btn:hover { background-color: rgba(180, 83, 9, 0.95); }
/* ── Right panel ──────────────────────────────────────────────────────── */
.idle-tabs { display: flex; flex-direction: row; gap: 6px; flex-shrink: 0; }
.idle-tab {
flex-grow: 1;
padding-top: 10px;
padding-bottom: 10px;
border-radius: 9px;
border: 2px solid rgba(100, 116, 139, 0.22);
background-color: rgba(12, 20, 30, 0.85);
font-size: 18px;
align-items: center;
justify-content: center;
cursor: none;
pointer-events: all;
opacity: 0.55;
transition: opacity 0.1s, border-color 0.1s;
}
.idle-tab:hover { opacity: 0.85; }
.idle-tab.on {
opacity: 1;
border-color: rgba(45, 212, 191, 0.7);
background-color: rgba(13, 40, 44, 0.9);
}
.idle-tab.fluxtab.on { border-color: rgba(96, 165, 250, 0.8); background-color: rgba(15, 32, 60, 0.9); }
.idle-buyrow {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.idle-buyrow-lbl {
font-size: 10px;
font-weight: 900;
letter-spacing: 0.16em;
color: rgba(148, 163, 184, 0.6);
margin-right: 4px;
}
.idle-buy-amt {
padding: 5px 14px;
border-radius: 7px;
border: 2px solid rgba(100, 116, 139, 0.22);
background-color: rgba(12, 20, 30, 0.85);
color: rgba(203, 213, 225, 0.7);
font-size: 11px;
font-weight: 900;
cursor: none;
pointer-events: all;
}
.idle-buy-amt.on {
border-color: rgba(45, 212, 191, 0.75);
color: #5eead4;
background-color: rgba(13, 40, 44, 0.9);
}
/* height:0 + flex-grow is what makes this actually scroll instead of pushing
its content off the bottom of the screen. */
.idle-panel {
flex-grow: 1;
flex-shrink: 1;
height: 0;
overflow-y: scroll;
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 8px;
padding-right: 8px;
}
/* ── Upgrade rows ─────────────────────────────────────────────────────── */
.idle-up {
display: flex;
flex-direction: row;
gap: 12px;
padding: 10px 12px;
border-radius: 11px;
background-color: rgba(10, 17, 26, 0.9);
border: 2px solid rgba(100, 116, 139, 0.16);
cursor: none;
pointer-events: all;
flex-shrink: 0;
transition: border-color 0.1s, background-color 0.1s, transform 0.06s;
}
.idle-up:hover { background-color: rgba(18, 28, 42, 0.95); }
.idle-up:active { transform: translateY(1px); }
.idle-up.afford { border-color: rgba(45, 212, 191, 0.55); }
.idle-up.afford:hover { border-color: rgba(94, 234, 212, 0.95); }
.idle-up.poor { opacity: 0.62; }
.idle-up.maxed { border-color: rgba(251, 191, 36, 0.45); opacity: 0.9; }
.idle-up.locked { opacity: 0.35; cursor: none; }
.idle-up.perk.afford { border-color: rgba(96, 165, 250, 0.7); }
.idle-up.ach { cursor: none; }
.idle-up.ach.done { border-color: rgba(52, 211, 153, 0.5); }
.idle-up-icon {
font-size: 26px;
width: 34px;
flex-shrink: 0;
text-align: center;
align-items: center;
justify-content: center;
}
.idle-up-body {
display: flex;
flex-direction: column;
gap: 3px;
flex-grow: 1;
}
.idle-up-top {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.idle-up-name {
font-size: 14px;
font-weight: 900;
color: #e2e8f0;
}
.idle-up-lvl {
font-size: 11px;
font-weight: 800;
color: rgba(148, 163, 184, 0.7);
}
.idle-up-desc {
font-size: 11px;
font-weight: 600;
color: rgba(148, 163, 184, 0.72);
}
.idle-up-bottom {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 3px;
}
.idle-up-effect {
font-size: 11px;
font-weight: 800;
color: rgba(94, 234, 212, 0.85);
}
.idle-up-cost {
font-size: 13px;
font-weight: 900;
color: #cbd5e1;
}
.idle-up.afford .idle-up-cost { color: #5eead4; }
.idle-up-cost.flux { color: #93c5fd; }
.idle-up-cost.maxed { color: #fbbf24; }
/* ── Flux / log headers ───────────────────────────────────────────────── */
.idle-flux-head {
display: flex;
flex-direction: column;
gap: 2px;
padding: 12px 14px;
border-radius: 11px;
background-color: rgba(13, 25, 46, 0.9);
border: 2px solid rgba(96, 165, 250, 0.3);
flex-shrink: 0;
}
.idle-flux-amount {
font-size: 22px;
font-weight: 900;
color: #93c5fd;
}
.idle-flux-sub {
font-size: 11px;
font-weight: 700;
color: rgba(148, 163, 184, 0.75);
}
.idle-ach-reward {
font-size: 11px;
font-weight: 900;
color: rgba(251, 191, 36, 0.9);
}
.idle-ach-bar {
width: 100%;
height: 6px;
border-radius: 3px;
background-color: rgba(30, 41, 59, 0.9);
overflow: hidden;
margin-top: 4px;
}
.idle-ach-fill {
height: 100%;
border-radius: 3px;
background-color: rgba(94, 234, 212, 0.8);
}
.idle-ach-prog {
font-size: 10px;
font-weight: 800;
color: rgba(148, 163, 184, 0.6);
margin-top: 2px;
}
/* ── Toasts ───────────────────────────────────────────────────────────── */
.idle-toasts {
position: absolute;
top: 96px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
pointer-events: none;
}
.idle-toast {
display: flex;
flex-direction: row;
align-items: center;
gap: 9px;
padding: 8px 18px;
border-radius: 10px;
background-color: rgba(8, 14, 22, 0.94);
border: 2px solid rgba(100, 116, 139, 0.35);
}
.idle-toast-icon { font-size: 17px; }
.idle-toast-text { font-size: 13px; font-weight: 800; color: #e2e8f0; }
.idle-toast.idle-toast-gold { border-color: rgba(253, 224, 71, 0.8); }
.idle-toast.idle-toast-chain { border-color: rgba(94, 234, 212, 0.75); }
.idle-toast.idle-toast-crit { border-color: rgba(251, 146, 60, 0.85); }
.idle-toast.idle-toast-frag { border-color: rgba(167, 139, 250, 0.8); }
.idle-toast.idle-toast-flux { border-color: rgba(96, 165, 250, 0.85); }
.idle-toast.idle-toast-surge { border-color: rgba(251, 146, 60, 0.9); }
.idle-toast.idle-toast-big { border-color: rgba(52, 211, 153, 0.85); }
.idle-toast.idle-toast-achievement { border-color: rgba(251, 191, 36, 0.9); }
.idle-toast.idle-toast-dim { opacity: 0.6; }
/* ── Modals ───────────────────────────────────────────────────────────── */
.idle-modal-bg {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(2, 4, 8, 0.86);
pointer-events: all;
}
.idle-modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 640px;
max-height: 88%;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 34px 40px;
border-radius: 18px;
background-color: rgba(9, 15, 24, 0.98);
border: 3px solid rgba(45, 212, 191, 0.45);
box-shadow: 0px 0px 70px rgba(13, 148, 136, 0.28);
pointer-events: all;
}
.idle-modal.meltdown { border-color: rgba(251, 191, 36, 0.6); box-shadow: 0px 0px 70px rgba(180, 83, 9, 0.35); }
.idle-modal.daily { border-color: rgba(167, 139, 250, 0.6); }
.idle-modal.intro { width: 720px; }
.idle-modal-eyebrow {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.30em;
text-transform: uppercase;
color: rgba(148, 163, 184, 0.55);
}
.idle-modal-title {
font-size: 34px;
font-weight: 900;
letter-spacing: 0.06em;
color: #5eead4;
text-shadow: 0px 0px 24px rgba(45, 212, 191, 0.4);
}
.idle-modal.meltdown .idle-modal-title { color: #fbbf24; }
.idle-modal.daily .idle-modal-title { color: #c4b5fd; }
.idle-modal-sub {
font-size: 13px;
font-weight: 600;
color: rgba(203, 213, 225, 0.75);
text-align: center;
max-width: 520px;
margin-bottom: 8px;
}
.idle-modal-btn {
margin-top: 14px;
padding: 14px 40px;
border-radius: 11px;
border: 2px solid #2dd4bf;
background-color: rgba(13, 78, 74, 0.9);
color: #ccfbf1;
font-size: 16px;
font-weight: 900;
letter-spacing: 0.16em;
align-items: center;
justify-content: center;
cursor: none;
pointer-events: all;
}
.idle-modal-btn:hover { background-color: rgba(15, 118, 110, 0.95); }
.idle-modal-btn.ghost {
border-color: rgba(148, 163, 184, 0.4);
background-color: rgba(30, 41, 59, 0.7);
color: rgba(203, 213, 225, 0.85);
}
.idle-modal-btn.danger {
border-color: #fbbf24;
background-color: rgba(146, 64, 14, 0.9);
color: #fef3c7;
}
.idle-modal-btn.danger:hover { background-color: rgba(180, 83, 9, 0.95); }
.idle-modal-btns { display: flex; flex-direction: row; gap: 14px; }
/* offline report */
.idle-offline-haul {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 18px 44px;
border-radius: 14px;
background-color: rgba(13, 40, 44, 0.7);
border: 2px solid rgba(45, 212, 191, 0.35);
margin-top: 6px;
}
.idle-offline-amount {
font-size: 46px;
font-weight: 900;
color: #5eead4;
text-shadow: 0px 0px 26px rgba(45, 212, 191, 0.5);
}
.idle-offline-lbl {
font-size: 12px;
font-weight: 900;
letter-spacing: 0.20em;
color: rgba(148, 163, 184, 0.7);
}
.idle-offline-extra {
font-size: 13px;
font-weight: 800;
color: #c4b5fd;
margin-top: 6px;
}
.idle-offline-warn {
font-size: 12px;
font-weight: 700;
color: rgba(251, 191, 36, 0.9);
text-align: center;
max-width: 480px;
margin-top: 8px;
}
/* daily */
.idle-streak-row { display: flex; flex-direction: row; gap: 8px; margin-top: 6px; }
.idle-streak-pip {
width: 44px;
height: 44px;
border-radius: 10px;
background-color: rgba(30, 41, 59, 0.8);
border: 2px solid rgba(100, 116, 139, 0.25);
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 900;
color: rgba(148, 163, 184, 0.6);
}
.idle-streak-pip.on {
background-color: rgba(76, 29, 149, 0.6);
border-color: rgba(167, 139, 250, 0.7);
color: #ddd6fe;
}
.idle-streak-pip.today {
border-color: #fbbf24;
color: #fde68a;
animation: idle-overload-pulse 1.1s ease-in-out infinite alternate;
}
.idle-daily-reward {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 14px;
}
.idle-daily-mult {
font-size: 44px;
font-weight: 900;
color: #fbbf24;
}
.idle-daily-lbl {
font-size: 13px;
font-weight: 800;
color: rgba(203, 213, 225, 0.8);
}
/* meltdown */
.idle-melt-payout {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 14px 38px;
border-radius: 14px;
background-color: rgba(30, 41, 90, 0.6);
border: 2px solid rgba(96, 165, 250, 0.45);
}
.idle-melt-payout-val { font-size: 44px; font-weight: 900; color: #93c5fd; }
.idle-melt-payout-lbl { font-size: 15px; font-weight: 900; color: rgba(148, 163, 184, 0.85); }
.idle-melt-cols {
display: flex;
flex-direction: row;
gap: 16px;
margin-top: 14px;
width: 100%;
}
.idle-melt-col {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 14px;
border-radius: 11px;
background-color: rgba(15, 23, 35, 0.85);
border: 2px solid rgba(100, 116, 139, 0.2);
}
.idle-melt-col.keep { border-color: rgba(52, 211, 153, 0.4); }
.idle-melt-col.lose { border-color: rgba(248, 113, 113, 0.4); }
.idle-melt-col-hd {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.16em;
color: rgba(203, 213, 225, 0.8);
margin-bottom: 4px;
}
.idle-melt-line {
font-size: 12px;
font-weight: 700;
color: rgba(148, 163, 184, 0.85);
}
/* intro */
.idle-intro-steps {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 10px;
width: 100%;
}
.idle-intro-step {
display: flex;
flex-direction: row;
align-items: center;
gap: 14px;
padding: 10px 14px;
border-radius: 11px;
background-color: rgba(15, 23, 35, 0.8);
border-left: 3px solid rgba(45, 212, 191, 0.6);
}
.idle-intro-num {
width: 28px;
height: 28px;
flex-shrink: 0;
border-radius: 50%;
background-color: rgba(13, 78, 74, 0.9);
border: 2px solid rgba(45, 212, 191, 0.6);
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 900;
color: #99f6e4;
}
.idle-intro-txt {
font-size: 13px;
font-weight: 600;
color: rgba(203, 213, 225, 0.9);
}
/* ── Board shake ──────────────────────────────────────────────────────── */
/* Own keyframes rather than reusing Gamehud's, so this stylesheet stands
on its own if that file ever changes. */
.idle-board.idle-shake-1 { animation: idle-shake-1 0.28s ease-out; }
.idle-board.idle-shake-2 { animation: idle-shake-2 0.28s ease-out; }
.idle-board.idle-shake-3 { animation: idle-shake-3 0.45s ease-out; }
@keyframes idle-shake-1 {
0% { transform: translate(0px, 0px); }
30% { transform: translate(-2px, 1px); }
60% { transform: translate(2px, -1px); }
100% { transform: translate(0px, 0px); }
}
@keyframes idle-shake-2 {
0% { transform: translate(0px, 0px) rotate(0deg); }
20% { transform: translate(-4px, 2px) rotate(-0.3deg); }
45% { transform: translate(4px, -3px) rotate(0.3deg); }
70% { transform: translate(-3px, -1px) rotate(-0.2deg); }
100% { transform: translate(0px, 0px) rotate(0deg); }
}
@keyframes idle-shake-3 {
0% { transform: translate(0px, 0px) rotate(0deg) scale(1.00); }
15% { transform: translate(-7px, 4px) rotate(-0.6deg) scale(1.01); }
35% { transform: translate(7px, -5px) rotate(0.6deg) scale(1.02); }
55% { transform: translate(-5px, -3px) rotate(-0.4deg) scale(1.01); }
78% { transform: translate(4px, 2px) rotate(0.3deg) scale(1.00); }
100% { transform: translate(0px, 0px) rotate(0deg) scale(1.00); }
}
/* ── Singularity — the second prestige layer ──────────────────────────── */
/* Violet throughout, so nothing on screen can be mistaken for Flux blue or
Meltdown amber. The layer is rare enough that it has to read instantly. */
.idle-sing-strip {
display: flex;
flex-direction: row;
align-items: center;
gap: 14px;
width: 620px;
padding: 10px 16px;
border-radius: 12px;
background-color: rgba(10, 17, 26, 0.85);
border: 2px solid rgba(100, 116, 139, 0.18);
flex-shrink: 0;
}
.idle-sing-strip.ready {
border-color: rgba(192, 132, 252, 0.75);
background-color: rgba(46, 16, 101, 0.6);
}
.idle-sing-strip.ready .idle-melt-text { color: #e9d5ff; font-weight: 900; }
.idle-sing-fill {
height: 100%;
background-color: #c084fc;
border-radius: 4px;
}
.idle-sing-btn {
margin-left: auto;
padding: 8px 20px;
border-radius: 9px;
border: 2px solid #c084fc;
background-color: rgba(76, 29, 149, 0.9);
color: #f3e8ff;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.14em;
cursor: none;
pointer-events: all;
}
.idle-sing-btn:hover { background-color: rgba(109, 40, 217, 0.95); }
.idle-tab.singtab.on {
border-color: rgba(192, 132, 252, 0.8);
background-color: rgba(38, 18, 62, 0.9);
}
.idle-up.power.afford { border-color: rgba(192, 132, 252, 0.7); }
.idle-up-cost.shard { color: #d8b4fe; }
.idle-flux-head.sing {
background-color: rgba(30, 15, 52, 0.9);
border-color: rgba(192, 132, 252, 0.35);
}
.idle-flux-head.sing .idle-flux-amount { color: #d8b4fe; }
.idle-sing-payout-val {
font-size: 44px;
font-weight: 900;
color: #d8b4fe;
}
.idle-modal.singularity {
border-color: rgba(192, 132, 252, 0.6);
box-shadow: 0px 0px 70px rgba(109, 40, 217, 0.4);
}
.idle-modal.singularity .idle-modal-title {
color: #d8b4fe;
text-shadow: 0px 0px 24px rgba(168, 85, 247, 0.45);
}
.idle-modal-btn.sing {
border-color: #c084fc;
background-color: rgba(76, 29, 149, 0.9);
color: #f3e8ff;
}
.idle-modal-btn.sing:hover { background-color: rgba(109, 40, 217, 0.95); }
/* Board glows violet while a Twin Reactor / Cascade follow-up is in the air,
so an extra volley reads as an event rather than as the board stuttering. */
.idle-board.cascading {
border-color: rgba(192, 132, 252, 0.65);
box-shadow: 0px 0px 46px rgba(168, 85, 247, 0.35);
}
/* ══════════════════════════════════════════════════════════════════════════
DAILY CONTRACTS
Violet, because contracts pay Fragments and Fragments are already violet
everywhere else in this HUD — the currency chip, the condenser bar, the
toast. Amber belongs to Meltdown and would have read as prestige.
Green marks a finished contract, matching the "collect" green the offline
and achievement toasts already use.
══════════════════════════════════════════════════════════════════════════ */
.idle-card-contracts { border-color: rgba(167, 139, 250, 0.28); }
.idle-card-contracts.ready { border-color: rgba(52, 211, 153, 0.5); }
.idle-card-contracts .idle-card-hd { color: rgba(196, 181, 253, 0.85); }
.idle-card-hd.row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.idle-card-hd-txt {
font-size: 11px;
font-weight: 900;
letter-spacing: 0.18em;
color: rgba(196, 181, 253, 0.85);
}
/* The parent .idle-card-hd is uppercased; a live countdown reading
"RESETS IN 5H 12M" is a lot louder than it deserves to be. */
.idle-card-hd-side {
font-size: 9px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: none;
color: rgba(148, 163, 184, 0.6);
}
.idle-contract {
display: flex;
flex-direction: row;
gap: 9px;
padding: 7px 8px;
border-radius: 9px;
background-color: rgba(10, 14, 22, 0.75);
border: 2px solid rgba(100, 116, 139, 0.16);
cursor: none;
pointer-events: all;
transition: border-color 0.12s, background-color 0.12s, transform 0.06s;
}
.idle-contract:hover { background-color: rgba(20, 26, 38, 0.85); }
.idle-contract.ready {
border-color: rgba(52, 211, 153, 0.85);
background-color: rgba(6, 46, 38, 0.6);
animation: idle-contract-ready 1.1s ease-in-out infinite alternate;
}
.idle-contract.ready:active { transform: translateY(1px); }
.idle-contract.claimed { opacity: 0.45; }
@keyframes idle-contract-ready {
0% { box-shadow: 0px 0px 6px rgba(52, 211, 153, 0.18); }
100% { box-shadow: 0px 0px 20px rgba(52, 211, 153, 0.50); }
}
.idle-contract-icon {
font-size: 19px;
width: 24px;
flex-shrink: 0;
text-align: center;
align-items: center;
justify-content: center;
}
.idle-contract-body {
display: flex;
flex-direction: column;
gap: 2px;
flex-grow: 1;
}
.idle-contract-top {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.idle-contract-name {
font-size: 12px;
font-weight: 900;
color: #e2e8f0;
}
.idle-contract-reward {
font-size: 10px;
font-weight: 900;
color: rgba(196, 181, 253, 0.9);
flex-shrink: 0;
}
.idle-contract.ready .idle-contract-reward { color: #6ee7b7; }
.idle-contract.claimed .idle-contract-reward { color: rgba(148, 163, 184, 0.7); }
.idle-contract-desc {
font-size: 10px;
font-weight: 600;
color: rgba(148, 163, 184, 0.72);
}
.idle-contract-track {
width: 100%;
height: 5px;
border-radius: 3px;
background-color: rgba(30, 41, 59, 0.9);
overflow: hidden;
margin-top: 3px;
}
/* Same violet as the condenser bar — both are the road to a Fragment. */
.idle-contract-fill {
height: 100%;
border-radius: 3px;
background-color: #a78bfa;
transition: width 0.25s;
}
.idle-contract.ready .idle-contract-fill { background-color: rgba(52, 211, 153, 0.85); }
.idle-contract.claimed .idle-contract-fill { background-color: rgba(52, 211, 153, 0.7); }
.idle-contract-prog {
font-size: 9px;
font-weight: 800;
color: rgba(148, 163, 184, 0.55);
}
.idle-toast.idle-toast-contract { border-color: rgba(167, 139, 250, 0.8); }
/* ══════════════════════════════════════════════════════════════════════════
NEXT GOAL PIN
══════════════════════════════════════════════════════════════════════════ */
.idle-card-goal { border-color: rgba(94, 234, 212, 0.22); }
.idle-goal-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 9px;
}
.idle-goal-icon {
font-size: 20px;
width: 26px;
flex-shrink: 0;
text-align: center;
}
.idle-goal-body { display: flex; flex-direction: column; flex-grow: 1; }
.idle-goal-name { font-size: 12px; font-weight: 900; color: #e2e8f0; }
.idle-goal-desc {
font-size: 10px;
font-weight: 600;
color: rgba(148, 163, 184, 0.7);
}
.idle-bar-fill.goal { background-color: rgba(94, 234, 212, 0.85); }
.idle-goal-foot {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.idle-goal-prog {
font-size: 10px;
font-weight: 800;
color: rgba(148, 163, 184, 0.6);
}
.idle-goal-reward {
font-size: 10px;
font-weight: 900;
color: rgba(251, 191, 36, 0.85);
}
/* ══════════════════════════════════════════════════════════════════════════
GUIDED WALKTHROUGH — coach marks over the live HUD
The spotlight is a real cut-out: four dim quads that stop at the target
rect, so the highlighted control stays fully lit AND fully clickable while
everything else eats the click. There is no masking in the panel renderer,
which is why it is built this way rather than as one sheet with a hole.
══════════════════════════════════════════════════════════════════════════ */
.rt-layer {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 400;
}
/* Background scrim and click blocker. The actual visual dimming of HUD content
is done by .tut-dim below — a scrim alone leaves every emoji punching
through it at full brightness. */
.rt-dim {
position: absolute;
/* Explicit origin: a collapsed quad is given only a width and a height,
and an absolute panel with no anchor falls back to its static position. */
left: 0;
top: 0;
background-color: rgba(2, 5, 10, 0.55);
pointer-events: all;
z-index: 1;
}
/* Applied to whichever HUD blocks are not the subject of the current step. */
.tut-dim {
opacity: 0.24;
transition: opacity 0.22s;
}
.rt-ring {
position: absolute;
border-radius: 18px;
border: 2px solid rgba(94, 234, 212, 0.75);
pointer-events: none;
z-index: 4;
animation: rt-ring 1.9s ease-out infinite;
}
@keyframes rt-ring {
0% { transform: scale(1.00); opacity: 0.85; }
65% { transform: scale(1.07); opacity: 0.00; }
100% { transform: scale(1.07); opacity: 0.00; }
}
.rt-spot {
position: absolute;
border-radius: 16px;
border: 3px solid rgba(94, 234, 212, 0.95);
pointer-events: none;
z-index: 5;
animation: rt-spot 1.5s ease-in-out infinite alternate;
}
@keyframes rt-spot {
0% {
border-color: rgba(94, 234, 212, 0.45);
box-shadow: 0px 0px 14px rgba(45, 212, 191, 0.25);
}
100% {
border-color: rgba(167, 243, 208, 1.00);
box-shadow: 0px 0px 46px rgba(45, 212, 191, 0.70);
}
}
/* Corner brackets — the bit that makes it read as a targeting reticle rather
than as a selected list item. */
.rt-corner {
position: absolute;
width: 20px;
height: 20px;
animation: rt-corner 1.5s ease-in-out infinite alternate;
}
.rt-corner.tl { top: -7px; left: -7px; border-top: 4px solid #fde047; border-left: 4px solid #fde047; }
.rt-corner.tr { top: -7px; right: -7px; border-top: 4px solid #fde047; border-right: 4px solid #fde047; }
.rt-corner.bl { bottom: -7px; left: -7px; border-bottom: 4px solid #fde047; border-left: 4px solid #fde047; }
.rt-corner.br { bottom: -7px; right: -7px; border-bottom: 4px solid #fde047; border-right: 4px solid #fde047; }
@keyframes rt-corner {
0% { opacity: 0.55; transform: scale(0.90); }
100% { opacity: 1.00; transform: scale(1.00); }
}
.rt-card {
position: absolute;
width: 380px;
padding: 16px 18px 14px 18px;
border-radius: 16px;
background-color: rgba(9, 15, 24, 0.98);
border: 2px solid rgba(45, 212, 191, 0.45);
box-shadow: 0px 0px 50px rgba(13, 148, 136, 0.28);
display: flex;
flex-direction: column;
gap: 7px;
pointer-events: all;
z-index: 10;
}
.rt-card.in-a { animation: rt-card-in 0.24s ease-out; }
.rt-card.in-b { animation: rt-card-in 0.24s ease-out; }
@keyframes rt-card-in {
0% { opacity: 0; transform: scale(0.94) translateY(8px); }
100% { opacity: 1; transform: scale(1.00) translateY(0px); }
}
.rt-card-top {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.rt-eyebrow {
font-size: 9px;
font-weight: 900;
letter-spacing: 0.28em;
color: rgba(94, 234, 212, 0.7);
}
.rt-count {
font-size: 10px;
font-weight: 900;
letter-spacing: 0.10em;
color: rgba(148, 163, 184, 0.55);
}
.rt-title {
font-size: 19px;
font-weight: 900;
color: #ccfbf1;
text-shadow: 0px 0px 18px rgba(45, 212, 191, 0.35);
}
.rt-text {
font-size: 13px;
font-weight: 600;
line-height: 1.45;
color: rgba(203, 213, 225, 0.9);
}
.rt-dots {
display: flex;
flex-direction: row;
gap: 4px;
margin-top: 4px;
margin-bottom: 2px;
}
.rt-dot {
width: 16px;
height: 4px;
border-radius: 2px;
background-color: rgba(100, 116, 139, 0.30);
transition: background-color 0.2s, width 0.2s;
}
.rt-dot.done { background-color: rgba(45, 212, 191, 0.45); }
.rt-dot.on { background-color: #5eead4; width: 24px; }
.rt-next {
margin-top: 4px;
padding-top: 11px;
padding-bottom: 11px;
border-radius: 10px;
border: 2px solid #2dd4bf;
background-color: rgba(13, 78, 74, 0.9);
color: #ccfbf1;
font-size: 13px;
font-weight: 900;
letter-spacing: 0.16em;
align-items: center;
justify-content: center;
cursor: none;
pointer-events: all;
transition: background-color 0.1s, transform 0.06s;
}
.rt-next:hover { background-color: rgba(15, 118, 110, 0.95); }
.rt-next:active { transform: translateY(1px); }
/* Shown instead of the button when the step is gated on an action — the two
are mutually exclusive, so there is no way to click past the doing. */
.rt-hint {
margin-top: 4px;
display: flex;
flex-direction: row;
align-items: center;
gap: 9px;
padding: 10px 12px;
border-radius: 10px;
background-color: rgba(69, 26, 3, 0.65);
border: 2px solid rgba(251, 191, 36, 0.65);
animation: rt-hint 1.0s ease-in-out infinite alternate;
}
@keyframes rt-hint {
0% { border-color: rgba(251, 191, 36, 0.35); }
100% { border-color: rgba(253, 224, 71, 0.95); }
}
.rt-hint-pip { font-size: 15px; flex-shrink: 0; }
.rt-hint-txt {
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
color: #fde68a;
}
.rt-foot {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 2px;
}
.rt-back, .rt-skip {
padding: 5px 10px;
border-radius: 7px;
border: 2px solid rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
font-size: 10px;
font-weight: 800;
letter-spacing: 0.08em;
color: rgba(148, 163, 184, 0.6);
cursor: none;
pointer-events: all;
transition: color 0.1s;
}
.rt-back:hover, .rt-skip:hover { color: rgba(226, 232, 240, 0.95); }
.rt-back.hidden { opacity: 0; pointer-events: none; }
/* ── Help button in the top bar ───────────────────────────────────────── */
.idle-help {
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: 10px;
border: 2px solid rgba(148, 163, 184, 0.28);
background-color: rgba(15, 23, 35, 0.9);
color: rgba(226, 232, 240, 0.85);
font-size: 20px;
font-weight: 900;
align-items: center;
justify-content: center;
cursor: none;
pointer-events: all;
transition: background-color 0.1s, border-color 0.1s;
}
.idle-help:hover {
background-color: rgba(45, 212, 191, 0.18);
border-color: rgba(45, 212, 191, 0.6);
}
.idle-help.on {
border-color: rgba(45, 212, 191, 0.9);
background-color: rgba(13, 78, 74, 0.9);
color: #5eead4;
}
/* ── Meltdown income preview ──────────────────────────────────────────── */
.idle-melt-gain {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 9px 18px;
border-radius: 11px;
background-color: rgba(15, 23, 35, 0.85);
border: 2px solid rgba(52, 211, 153, 0.35);
}
.idle-melt-gain-from {
font-size: 18px;
font-weight: 900;
color: rgba(148, 163, 184, 0.8);
}
.idle-melt-gain-arrow {
font-size: 16px;
font-weight: 900;
color: rgba(148, 163, 184, 0.6);
}
.idle-melt-gain-to {
font-size: 22px;
font-weight: 900;
color: #6ee7b7;
text-shadow: 0px 0px 16px rgba(52, 211, 153, 0.45);
}
.idle-melt-gain-lbl {
font-size: 11px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(148, 163, 184, 0.65);
}
Game
game
using Sandbox;
public enum GameMode
{
Wave,
Survival,
Idle, // Reactor mode — the self-running chain reaction economy
}
Game
game
using System;
/// <summary>
/// The pet is the Reactor's Fragment sink.
///
/// Cores buy upgrades that reset on Meltdown, and Flux buys perks that never do.
/// The pet sits outside both: Fragments condense slowly, on a daily ceiling, and
/// go into a permanent output multiplier that takes weeks rather than hours to
/// finish. It is the long-horizon goal that survives every reset.
/// </summary>
public static class IdlePet
{
public const int MaxLevel = 6;
/// <summary>Fragments needed to go from <paramref name="level"/> to the next one.</summary>
public static int CostToUpgrade( int level ) => level switch
{
1 => 120,
2 => 300,
3 => 700,
4 => 1_500,
5 => 3_200,
_ => 0,
};
/// <summary>Total Fragments to take a fresh pet all the way to level 6.</summary>
public static int TotalCost
{
get
{
int total = 0;
for ( int lvl = 1; lvl < MaxLevel; lvl++ ) total += CostToUpgrade( lvl );
return total;
}
}
public static bool IsMaxed( int level ) => level >= MaxLevel;
/// <summary>Reactor output multiplier granted by the pet, before Pet Link.</summary>
public static double OutputBonus( int level ) => 0.15 * Math.Max( 0, level - 1 );
public static string Name( int level ) => level switch
{
1 => "Mysterious Egg",
2 => "Cat",
3 => "Wolf",
4 => "Tiger",
5 => "Baby Drake",
_ => "Dragon",
};
public static string Emoji( int level ) => level switch
{
1 => "🥚",
2 => "🐱",
3 => "🐺",
4 => "🐯",
5 => "🐣",
_ => "🐉",
};
public static string Blurb( int level ) => level switch
{
1 => "Something is moving in there. Feed it Fragments and find out what.",
2 => "It watches the reactor and purrs at every chain.",
3 => "Faster, hungrier, and it has started organising the drones.",
4 => "Big enough now to shove chests into the blast radius itself.",
5 => "Wings, and the beginnings of a temper.",
_ => "Fully grown. The reactor runs hot around it.",
};
}
Game
game
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Persistent state for Reactor (Idle) mode.
/// Serialized as a nested object inside PlayerSave — see PlayerSave.Idle.
/// Everything here survives a Meltdown except what ResetForMeltdown() clears.
/// </summary>
public class IdleSave
{
// ── Unlock / onboarding ───────────────────────────────────────
public bool Unlocked { get; set; } = false; // granted after the first finished run
public bool IntroShown { get; set; } = false; // first-entry explainer
public bool MeltdownSeen { get; set; } = false; // has the prestige panel ever been opened
/// <summary>
/// The guided walkthrough has run to the end, or been skipped. Separate from
/// IntroShown so an existing save — which already has IntroShown set and has
/// therefore never seen the walkthrough — still gets offered it once.
/// </summary>
public bool TutorialDone { get; set; } = false;
/// <summary>The walkthrough has been offered and declined; do not re-prompt.</summary>
public bool TutorialOffered { get; set; } = false;
// ── Currencies ────────────────────────────────────────────────
public double Cores { get; set; } = 0; // spendable, wiped by Meltdown
public double CycleCores { get; set; } = 0; // earned since last Meltdown — drives Flux payout
public double AllTimeCores { get; set; } = 0; // never resets
public int Flux { get; set; } = 0; // unspent prestige currency
public int FluxEarned { get; set; } = 0; // lifetime, for milestones
public int Meltdowns { get; set; } = 0;
public int LastReactorRank { get; set; } = 0; // 0 == never ranked; drives the "up N places" note
// ── Singularity (second prestige) ─────────────────────────────
public int Shards { get; set; } = 0; // unspent, buys Powers
public int ShardsEarned { get; set; } = 0; // lifetime — drives the passive multiplier
public int Singularities { get; set; } = 0;
public bool SingularitySeen { get; set; } = false; // has the collapse panel ever been opened
// ── Upgrade levels ────────────────────────────────────────────
// Keys are IdleUpgradeId.ToString(); missing key == level 0.
public Dictionary<string, int> Upgrades { get; set; } = new();
public Dictionary<string, int> Perks { get; set; } = new();
public Dictionary<string, int> Powers { get; set; } = new();
// ── Offline bookkeeping ───────────────────────────────────────
public long LastSeenUnix { get; set; } = 0;
public double MeasuredCps { get; set; } = 0; // smoothed cores/sec, used for offline payout
public double BestCps { get; set; } = 0;
// Pending offline haul, staged so the welcome-back panel can show it
public double PendingOfflineCores { get; set; } = 0;
public double PendingOfflineSeconds { get; set; } = 0;
public int PendingOfflineFrags { get; set; } = 0;
// ── Daily loop ────────────────────────────────────────────────
public string LastDailyDate { get; set; } = "";
public int DailyStreak { get; set; } = 0;
public bool DailyClaimed { get; set; } = false;
// ── Fragment drip (own bucket — does not touch the pet farm cap) ──
public int TodayReactorFragments { get; set; } = 0;
public string LastFragDate { get; set; } = "";
public double FragProgress { get; set; } = 0; // cores banked toward the next fragment
// ── Records / milestones ──────────────────────────────────────
public int TotalDetonations { get; set; } = 0;
public long TotalChestsBlown { get; set; } = 0;
public int BestChain { get; set; } = 0;
public double BestSingleBlast { get; set; } = 0;
public int GoldenGrabbed { get; set; } = 0;
public double SecondsRun { get; set; } = 0;
public List<string> Achievements { get; set; } = new();
// Counters that exist purely so Daily Contracts have something to watch.
// They are lifetime totals; a contract stores its own baseline at issue.
public int TotalOverloads { get; set; } = 0;
public int TotalSurges { get; set; } = 0;
public int TotalUpgradesBought { get; set; } = 0;
/// <summary>Longest chain since the contract set was last issued. Reset with it.</summary>
public int BestChainToday { get; set; } = 0;
// ── Daily contracts ───────────────────────────────────────────
// Parallel lists rather than a list of objects: the save is plain JSON and
// a schema change to a nested type would break every existing file.
public string ContractDate { get; set; } = "";
public List<string> ContractIds { get; set; } = new();
public List<double> ContractTargets { get; set; } = new();
public List<double> ContractBaseline { get; set; } = new();
public List<bool> ContractClaimed { get; set; } = new();
/// <summary>Consecutive days on which all three contracts were cleared.</summary>
public int ContractStreak { get; set; } = 0;
public string ContractStreakDate { get; set; } = "";
// ── Upgrade helpers ───────────────────────────────────────────
public int Level( IdleUpgradeId id )
=> Upgrades.TryGetValue( id.ToString(), out int v ) ? v : 0;
public void SetLevel( IdleUpgradeId id, int level )
=> Upgrades[id.ToString()] = Math.Max( 0, level );
public int PerkLevel( IdlePerkId id )
=> Perks.TryGetValue( id.ToString(), out int v ) ? v : 0;
public void SetPerkLevel( IdlePerkId id, int level )
=> Perks[id.ToString()] = Math.Max( 0, level );
public int PowerLevel( IdlePowerId id )
=> Powers != null && Powers.TryGetValue( id.ToString(), out int v ) ? v : 0;
public void SetPowerLevel( IdlePowerId id, int level )
{
Powers ??= new();
Powers[id.ToString()] = Math.Max( 0, level );
}
public bool HasAchievement( string id ) => Achievements.Contains( id );
// ── Meltdown (prestige) ───────────────────────────────────────
/// <summary>Wipes the run-scoped economy. Flux, perks, records and achievements stay.</summary>
public void ResetForMeltdown()
{
Cores = 0;
CycleCores = 0;
Upgrades.Clear();
Meltdowns++;
// Kickstart perk hands back a head start so the early grind shrinks each time
int kick = PerkLevel( IdlePerkId.Kickstart );
if ( kick > 0 )
{
SetLevel( IdleUpgradeId.ChestValue, kick * 6 );
SetLevel( IdleUpgradeId.SpawnRate, kick * 4 );
SetLevel( IdleUpgradeId.CycleSpeed, kick * 4 );
SetLevel( IdleUpgradeId.DroneCount, Math.Min( kick, 3 ) );
}
}
// ── Singularity (second prestige) ─────────────────────────────
/// <summary>
/// Collapses everything a Meltdown normally protects — Flux, lifetime Flux
/// and the whole perk tree. Shards, Powers, records, achievements, Fragments
/// and the pet survive; those are what carry the run forward.
///
/// Meltdowns is deliberately kept: the Reactor Log counts it, and wiping a
/// lifetime tally the player earned honestly reads as a bug, not a cost.
/// </summary>
public void ResetForSingularity()
{
Flux = 0;
FluxEarned = 0;
Perks.Clear();
// Kickstart is gone with the perks, so ResetForMeltdown lands us at a
// genuine level-zero board — which is the point of the layer.
ResetForMeltdown();
Meltdowns--; // ResetForMeltdown counts a Meltdown that did not happen
Singularities++;
}
// ── Day rollover for the fragment bucket ──────────────────────
public void EnsureFragDay()
{
string today = DateTime.UtcNow.ToString( "yyyy-MM-dd" );
if ( LastFragDate == today ) return;
LastFragDate = today;
TodayReactorFragments = 0;
}
public int FragmentsToday
{
get { EnsureFragDay(); return TodayReactorFragments; }
}
}
Game
game
$primary: red !default;
$primary-alt: white !default;
$switch-padding: 6px !default;
.button.popupbutton.dropdown
{
cursor: pointer;
transition: all .1s ease-out;
position: relative;
> .dropdown_indicator
{
position: absolute;
right: 8px;
}
&.open
{
border-bottom-left-radius: 1px;
border-bottom-right-radius: 1px;
transition: border-radius 0.2s ease-out;
}
}
select
{
min-height: 40px;
> option
{
display: none;
}
}
Game
game
@import "/styles/_theme.scss";
$background-size: 8px;
PackageCard
{
flex-shrink: 0;
&:hover
{
sound-in: "ui.button.over";
}
&:active
{
sound-in: "ui.button.press";
}
flex-direction: column;
position: relative;
background-color: rgba( $default-950, 0 );
border-radius: $rounding-default;
transition: all 150ms ease;
cursor: pointer;
z-index: 0;
height: 200px;
.image
{
flex-grow: 1;
flex-shrink: 0;
transition: all 150ms ease;
border: 1px solid rgba( white, 0.025 );
aspect-ratio: 16 / 9;
background-position: center;
background-size: cover;
border-radius: $rounding-small;
position: relative;
}
column
{
padding: 8px 2px; // Optically aligned
}
&.list
{
flex-grow: 1;
height: 64px;
flex-direction: row;
gap: 12px;
padding: 4px;
.inner column
{
flex-grow: 1;
}
.image
{
height: 100%;
aspect-ratio: 1;
flex-grow: 0;
flex-shrink: 0;
}
column
{
gap: 3px;
justify-content: center;
}
.package-title
{
flex-shrink: 0;
max-width: 500px;
}
.package-users
{
top: 1px;
right: 1px;
}
&:hover
{
background-color: $default-800;
transform: none;
&::after
{
display: none;
}
}
}
&.wide
{
height: 250px;
.package-title
{
max-width: 300px;
}
}
&.small
{
height: 200px;
aspect-ratio: 3/4;
.image
{
aspect-ratio: 1;
}
.package-title
{
max-width: 140px;
}
}
&.tall
{
height: 400px;
.image
{
aspect-ratio: 9/16;
}
.package-title
{
max-width: 200px;
}
}
.package-title
{
text-overflow: ellipsis;
max-height: 24px;
flex-shrink: 1;
font-size: 14px;
}
.package-users
{
position: absolute;
bottom: 4px;
right: 4px;
background-color: rgba( 10, 40, 10, 0.95 );
padding: 3px 5px;
border-radius: 2px;
justify-content: center;
align-items: center;
gap: 3px;
font-size: 11px;
color: #def;
border: 1px solid #252;
color: #2f3;
&:before
{
content: '●';
font-size: 0.5rem;
}
}
// Hover effect (not for list)
&:not(.list)
{
&::after
{
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba( $default-950, 0 );
transition: all 150ms ease;
z-index: -10;
border-radius: $rounding-large;
pointer-events: none;
}
&:hover
{
transform: scale( 1.05 );
&::after
{
background-color: $default-800;
top: -$background-size;
left: -$background-size;
right: -$background-size;
bottom: -$background-size;
box-shadow: 0 0 25px rgba( black, 0.3 );
}
z-index: 100;
background-color: $default-900;
}
}
}
.package-card.list packageflairbar
{
display: none;
}
.package-card.list .package-users
{
display: none;
}
Game
game
root {
/* Declared so no panel in this tree ever claims the OS pointer -- a hovered
panel outranks Mouse.CursorType, and one that declares nothing still claims
the default arrow. GameCursor draws the cursor instead. */
cursor: none;
flex-grow:1; width:100%; height:100%;
pointer-events:none; position:relative;
overflow:visible;
}
.mini-hand {
position:absolute;
bottom:15px;
left:0; right:0px; /* right edge = sidebar width */
display:flex; flex-direction:row; gap:14px; align-items:flex-end;
justify-content:center; /* centers within the grid area, not full viewport */
pointer-events:all;
z-index:30;
}
.mini-card {
width:80px;
height: 80px;
background-color:rgba(31,27,83,0.85); /* more opaque — was barely visible */
border:2px solid rgba(255,255,255,0.18);
border-radius:16px; padding:16px 10px 12px;
display:flex; flex-direction:column; align-items:center; gap:10px;
cursor: none; position:relative;
transition:background-color 0.1s, border-color 0.1s, transform 0.1s;
}
.mini-card:hover { background-color:rgba(117,110,194,0.75); transform:translateY(-5px); }
.mini-card.active {
background-color:rgba(83,74,183,0.90);
border-color:#a78bfa;
border-width:3px;
transform:translateY(-16px);
}
.mini-card.used {
background-color:rgba(29,158,117,0.28);
border-color:rgba(29,158,117,0.60);
opacity:0.75;
cursor: none;
}
.mini-icon { font-size:30px; position: relative; bottom: 4px;} /* was 40px — easier to see */
.mini-name { font-size:13px; font-weight:800; color:rgba(255,255,255,0.80); }
.mini-card.active .mini-name { color:#fff; font-size:15px; }
.mini-badge {
position:absolute; top:5px; right:7px;
font-size:12px; font-weight:700;
color:rgba(80,220,140,0.95);
background-color:rgba(29,158,117,0.40);
border-radius:4px; padding:0 5px;
}
.kb-hint {
position:absolute; top:5px; left:7px;
font-size:12px; font-weight:800;
color:rgba(255,255,255,0.45);
background-color:rgba(255,255,255,0.10);
border-radius:4px; padding:0 6px;
}
.mini-card.active .kb-hint {
color:rgba(255,255,255,0.80);
background-color:rgba(255,255,255,0.18);
}
.mini-det-wrap { align-self:flex-end; }
.mini-det {
width:90px; /* was 90px */
background:linear-gradient(135deg,#7c3aed,#a855f7);
border:2px solid rgba(167,139,250,0.40);
border-radius:14px;
padding:12px 10px 5px;
display:flex; flex-direction:column; align-items:center; gap:5px;
cursor: none;
transition:opacity 0.15s;
}
.mini-det-wrap.ready .mini-det { border-color:#a78bfa; }
.mini-det-wrap.off .mini-det { opacity:0.25; pointer-events:none; }
.mini-det-icon { font-size:28px; }
.mini-det-label { font-size:13px; font-weight:800; color:#fff; letter-spacing:0.08em; }
/* SCREEN SHAKE */
.mini-hand.shake-small { animation: shake-small 0.25s ease-out; }
.mini-hand.shake-medium { animation: shake-medium 0.35s ease-out; }
.mini-hand.shake-strong { animation: shake-strong 0.45s ease-out; }
.mini-hand.shake-insane { animation: shake-insane 0.55s ease-out; }
@keyframes shake-small {
0%,100% { transform: translate(0,0); }
25% { transform: translate(-2px, 1px); }
50% { transform: translate(2px, -1px); }
75% { transform: translate(-1px, 2px); }
}
@keyframes shake-medium {
0%,100% { transform: translate(0,0); }
20% { transform: translate(-4px, 2px) rotate(-0.3deg); }
40% { transform: translate(4px,-3px) rotate(0.3deg); }
60% { transform: translate(-3px, 3px) rotate(-0.2deg); }
80% { transform: translate(3px,-2px) rotate(0.2deg); }
}
@keyframes shake-strong {
0%,100% { transform: translate(0,0); }
15% { transform: translate(-6px, 3px) rotate(-0.5deg); }
30% { transform: translate(6px,-4px) rotate(0.5deg); }
45% { transform: translate(-5px, 5px) rotate(-0.4deg); }
60% { transform: translate(5px,-4px) rotate(0.4deg); }
75% { transform: translate(-3px, 2px); }
90% { transform: translate(2px,-2px); }
}
@keyframes shake-insane {
0%,100% { transform: translate(0,0); }
10% { transform: translate(-9px, 5px) rotate(-0.7deg); }
20% { transform: translate(9px,-6px) rotate(0.7deg); }
30% { transform: translate(-8px, 7px) rotate(-0.6deg); }
40% { transform: translate(8px,-6px) rotate(0.6deg); }
50% { transform: translate(-6px, 5px) rotate(-0.4deg); }
60% { transform: translate(6px,-5px) rotate(0.4deg); }
70% { transform: translate(-4px, 3px); }
80% { transform: translate(3px,-3px); }
90% { transform: translate(-1px, 1px); }
}
/* Lock icon, if random event Bomb jam hit */
.mini-jam-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(239,68,68,0.25);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
pointer-events: none;
}
Game
game
root {
/* Declared so no panel in this tree ever claims the OS pointer -- a hovered
panel outranks Mouse.CursorType, and one that declares nothing still claims
the default arrow. GameCursor draws the cursor instead. */
cursor: none;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
overflow: hidden;
}
/* TOP HUD */
.topbar {
position: absolute;
top: 0; left: 0; right: 0;
height: 90px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
border-bottom: 1px solid rgba(167,139,250,0.15);
z-index: 10;
background-color: rgba(18,8,42,0.97);
animation: topbar-shift 20s ease-in-out infinite alternate;
overflow: hidden;
z-index: 11;
}
@keyframes topbar-shift {
0% { background-color: rgba(18,8,42,0.97); }
25% { background-color: rgba(8,24,48,0.97); }
50% { background-color: rgba(24,8,40,0.97); }
75% { background-color: rgba(8,18,50,0.97); }
100% { background-color: rgba(20,6,38,0.97); }
}
.hud-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.hud-lbl {
font-size: 22px;
font-weight: 700;
letter-spacing: 0.12em;
color: rgba(255,255,255,0.30);
text-transform: uppercase;
}
.hud-val {
font-size: 32px;
font-weight: 900;
color: white;
}
.hud-block.wave .hud-val { color: #a78bfa; }
.hud-block.score .hud-val { color: #34d399; }
.hud-block.best .hud-val { color: #fbbf24; }
.hud-sep {
width: 1px;
height: 120px;
background-color: rgba(128,95,128,0.07);
}
/* TIMER */
.timer-track {
position: absolute;
top: 90px; left: 0; right: 0;
height: 14px;
background-color: rgba(255,255,255,0.06);
z-index: 10;
}
.timer-fill {
position: absolute;
top: 0; left: 0; bottom: 0;
background-color: #34d399;
transition: width 0.25s linear, background-color 0.8s ease;
min-width: 4px;
}
.timer-fill.mid {
background-color: #a78bfa; /* half time: purple */
transition: width 0.25s linear, background-color 0.6s ease;
}
.timer-fill.hurry {
background-color: #f59e0b; /* 30%: amber */
transition: width 0.25s linear, background-color 0.5s ease;
}
.timer-fill.panic {
background-color: #ef4444; /* 15%: red */
transition: width 0.25s linear, background-color 0.3s ease;
animation: pulse-bar 0.55s ease-in-out infinite alternate;
}
@keyframes pulse-bar {
from { opacity: 1.0; }
to { opacity: 0.5; }
}
/*
Three-stage color shift:
full → hurry : purple → amber (feels warm, attention-grabbing)
hurry → panic : amber → red (urgent)
panic : red + pulse animation
*/
/* Challenge sentence — between timer and grid */
.challenge-bar {
position: absolute;
top: 104px;
left: 0;
right: 0px;
height: 36px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
background-color: rgba(13,8,32,0.80);
border-bottom: 1px solid rgba(167,139,250,0.12);
pointer-events: none;
z-index: 9;
transition: background-color 0.4s ease;
}
.challenge-bar.done {
background-color: rgba(16,42,16,0.85);
border-bottom-color: rgba(52,211,153,0.25);
}
.ch-icon {
font-size: 16px;
color: rgba(167,139,250,0.70);
}
.challenge-bar.done .ch-icon {
color: #34d399;
}
.ch-label {
font-size: 20px;
font-weight: 700;
color: rgba(255,255,255,0.55);
letter-spacing: 0.04em;
}
.challenge-bar.done .ch-label {
color: rgba(52,211,153,0.85);
}
.ch-bonus {
font-size: 13px;
font-weight: 800;
color: rgba(251,191,36,0.60);
background-color: rgba(251,191,36,0.08);
border-radius: 5px;
padding: 2px 7px;
}
.challenge-bar.done .ch-bonus {
color: rgba(52,211,153,0.80);
background-color: rgba(52,211,153,0.10);
}
/* CHAIN TRACK — above grid, pointer-events none */
.chain-track {
position: absolute;
top: 146px;
left: 0;
right: 0px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
pointer-events: none;
z-index: 15;
}
.chain-track-inner {
background-color: rgba(13,8,32,0.75);
border-radius: 14px;
padding: 10px 20px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.step {
background-color: rgba(255,255,255,0.05);
color: rgba(255,255,255,0.25);
border-radius: 10px;
padding: 6px 17px;
font-size: 15px;
border: 1.5px solid rgba(255,255,255,0.06);
transition: all 0.15s;
}
.step.lit {
background-color: rgba(167,139,250,0.2);
color: #c4b5fd;
border-color: #7c3aed;
transform: scale(1.15);
}
.arr { font-size: 15px; color: rgba(255,255,255,0.15); }
/* SIDEBAR */
.sidebar {
position: absolute;
top: 140px; right: 0; bottom: 0;
width: 260px;
background-color: rgba(18,8,42,0.97);
border-left: 2px solid rgba(167,139,250,0.18);
display: flex;
flex-direction: column;
gap: 14px;
padding: 16px;
pointer-events: all;
z-index: 10;
}
.divider {
height: 1px;
background-color: rgba(167,139,250,0.10);
}
.sbl {
font-size: 12px; font-weight: 800;
letter-spacing: 0.2em; text-transform: uppercase;
color: rgba(166,154,219,0.5);
}
/* Legend */
.legend { display: flex; flex-direction: column; gap: 8px; }
.leg { display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 14px; color: rgba(255,255,255,0.38); font-weight: 600; }
.dot { width: 10px; height: 10px; border-radius: 2px; }
.dot.p0 { background-color: #60a5fa; }
.dot.p1 { background-color: #34d399; }
.dot.p2 { background-color: #fb923c; }
.dot.red { background-color: #f87171; }
.dot.chain { background-color: #c4b5fd; }
/* Controls */
.controls-hint {
display: flex; flex-direction: column; gap: 6px;
font-size: 13px; color: rgba(255,255,255,0.30); font-weight: 500;
}
.spacer { flex-grow: 1; }
/* Message bottom bar — pointer-events none, no text-align */
.msg {
position: absolute;
bottom: 8px;
left: 12px;
background-color: rgba(4,1,12,1.0);
border: 1px solid rgba(167,139,250,0.15);
border-radius: 8px;
padding: 6px 14px;
font-size: 18px;
color: rgba(255,255,255,0.55);
pointer-events: none;
font-weight: 600;
z-index: 5;
}
/* GAME OVER */
/* ── Three column layout ── */
.ov-bg {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background-color: rgba(4,1,12,1.0);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 24px;
pointer-events: all;
z-index: 50;
overflow: hidden;
}
.ov-side-panel {
display: flex;
flex-direction: column;
gap: 16px;
width: 220px;
pointer-events: none;
z-index: 3;
max-height: 92vh;
overflow-y: auto;
}
.ov-side-card {
position: relative; /* needed for ::before */
background-color: rgba(8,3,22,0.90);
border: 1px solid rgba(139,92,246,0.18);
border-radius: 16px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
overflow: hidden;
}
.ov-side-card.journey {
gap: 4px;
}
/* Streak card — orange glow line */
.ov-side-card.streak::before {
background-color: rgba(251,146,60,0.55);
}
.ov-side-card::before {
content: "";
position: absolute;
top: -1px; left: 20%; right: 20%;
height: 1px;
background-color: rgba(139,92,246,0.40);
border-radius: 50%;
}
.ov-side-card.streak {
border-color: rgba(251,146,60,0.30);
background-color: rgba(251,146,60,0.05);
}
.ov-side-card-title {
font-size: 14px;
font-weight: 900;
color: rgb(255, 214, 161);
letter-spacing: 0.22em;
text-transform: uppercase;
width: 100%;
justify-content: center;
margin-bottom: 8px;
gap: 3px;
}
/* ── Personal best bar ── */
.ov-pb-bar-wrap { width: 100%; }
.ov-pb-bar-track {
width: 100%;
height: 6px;
background-color: rgba(255,255,255,0.06);
border-radius: 3px;
position: relative;
}
.ov-pb-bar-fill {
position: absolute;
top: 0; left: 0; bottom: 0;
background-color: #a78bfa;
border-radius: 3px;
min-width: 4px;
}
.ov-pb-marker {
position: absolute;
top: -3px;
width: 2px;
height: 12px;
background-color: rgba(251,191,36,0.80);
border-radius: 1px;
}
.ov-pb-scores {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
width: 100%;
}
.ov-pb-score-current {
font-size: 22px;
font-weight: 900;
color: #a78bfa;
}
.ov-pb-score-best {
font-size: 11px;
font-weight: 600;
color: rgba(255,255,255,0.30);
}
.ov-pb-newbest {
font-size: 12px;
font-weight: 900;
color: #fbbf24;
background-color: rgba(251,191,36,0.10);
border: 1px solid rgba(251,191,36,0.25);
border-radius: 8px;
padding: 3px 10px;
}
/* Game over background Squares */
@keyframes sq-float-1 {
0% { margin-top: 0px; margin-left: 0px; }
25% { margin-top: -14px; margin-left: 8px; }
50% { margin-top: -6px; margin-left: -10px; }
75% { margin-top: 10px; margin-left: 4px; }
100% { margin-top: 0px; margin-left: 0px; }
}
@keyframes sq-float-2 {
0% { margin-top: 0px; margin-left: 0px; }
30% { margin-top: 12px; margin-left: -8px; }
60% { margin-top: -8px; margin-left: 12px; }
100% { margin-top: 0px; margin-left: 0px; }
}
@keyframes sq-pulse {
0%,100% { opacity: 0.4; }
50% { opacity: 1.0; }
}
.ov-bg-squares {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.ov-bg-sq {
position: absolute;
border: 1.5px solid;
}
@keyframes sq-drift-a {
0% { margin-top: 0px; margin-left: 0px; opacity: 0.35; }
20% { margin-top: -12px; margin-left: 8px; opacity: 0.80; }
40% { margin-top: -20px; margin-left: -6px; opacity: 0.50; }
60% { margin-top: -8px; margin-left: -14px; opacity: 0.90; }
80% { margin-top: 6px; margin-left: 4px; opacity: 0.45; }
100% { margin-top: 0px; margin-left: 0px; opacity: 0.35; }
}
@keyframes sq-drift-b {
0% { margin-top: 0px; margin-left: 0px; opacity: 0.25; }
25% { margin-top: 14px; margin-left: -10px; opacity: 0.70; }
50% { margin-top: 6px; margin-left: 16px; opacity: 0.90; }
75% { margin-top: -10px; margin-left: 8px; opacity: 0.40; }
100% { margin-top: 0px; margin-left: 0px; opacity: 0.25; }
}
.ov-bg-sq.sq-a {
animation-name: sq-drift-a;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
.ov-bg-sq.sq-b {
animation-name: sq-drift-b;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
/* Alternate float pattern on even squares */
.ov-bg-sq:nth-child(even) {
animation-name: sq-float-2, sq-pulse;
}
/* ── Journey ── */
.ov-journey-wave {
font-size: 32px;
font-weight: 900;
color: white;
gap: 3px;
}
.ov-journey-sub {
font-size: 12px;
font-weight: 600;
color: rgba(167, 255, 179, 0.719);
word-spacing: 1px;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between; /* ← pushes label left, value right */
}
.ov-journey-tag {
font-size: 10px;
font-weight: 800;
color: rgba(255, 255, 255, 0.562);
background-color: rgba(255,255,255,0.04);
border-radius: 6px;
padding: 3px 10px;
}
.ov-journey-tag.good {
color: #34d399;
background-color: rgba(52,211,153,0.08);
}
/* ── Streak ── */
.ov-streak-count {
font-size: 48px;
font-weight: 900;
color: #fb923c;
}
.ov-streak-label {
font-size: 11px;
font-weight: 700;
color: rgba(255, 199, 125, 0.404);
}
/* ── Next unlock ── */
.ov-unlock-icon { font-size: 32px; }
.ov-unlock-name {
font-size: 13px;
font-weight: 800;
color: rgba(255,255,255,0.75);
width: 100%;
text-align: center;
justify-content: center;
margin-bottom: 2px;
}
.ov-unlock-cost {
font-size: 12px;
flex-direction: row;
font-weight: 700;
color: rgba(129,140,248,0.70);
width: 100%;
justify-content: center;
text-align: center;
gap: 3px; /* adds space b/w elements */
}
.ov-unlock-ready {
font-size: 12px;
font-weight: 900;
color: #34d399;
}
.ov-unlock-bar-track {
width: 100%;
height: 4px;
background-color: rgba(255,255,255,0.06);
border-radius: 2px;
position: relative;
margin-top: 4px;
}
.ov-unlock-bar-fill {
position: absolute;
top: 0; left: 0; bottom: 0;
background-color: #818cf8;
border-radius: 2px;
min-width: 4px;
}
.ov-particles {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
overflow: hidden;
z-index: 2;
}
.ov-particle {
position: absolute;
bottom: -20px;
opacity: 0;
animation-name: particle-rise;
animation-timing-function: ease-in;
animation-iteration-count: infinite;
}
@keyframes particle-rise {
0% { opacity: 0; transform: translateY(0px); }
15% { opacity: 1; }
80% { opacity: 0.7; transform: translateY(-65vh); }
100% { opacity: 0; transform: translateY(-105vh); }
}
.ov-box {
position: relative;
background-color: rgba(8,3,22,0.98);
border: 1px solid rgba(139,92,246,0.25);
border-radius: 24px;
padding: 2rem 2.2rem 2.2rem 2.2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 600px;
max-height: 92vh;
overflow-y: auto;
z-index: 3;
box-shadow: 0px 0px 0px 1px rgba(139,92,246,0.08),
0px 2px 0px rgba(139,92,246,0.15),
0px 24px 60px rgba(0s,0,0,0.80),
inset 0px 1px 0px rgba(255,255,255,0.04);
}
/* Subtle top glow on the box */
.ov-box::before {
content: "";
position: absolute;
top: -1px; left: 15%; right: 15%;
height: 1px;
background-color: rgba(167,139,250,0.50);
border-radius: 50%;
}
.ov-wave-big {
font-size: 64px;
font-weight: 900;
color: #a78bfa;
letter-spacing: 0.06em;
animation: ov-section-in 0.6s cubic-bezier(0.22,1,0.36,1) 0.3s both;
}
.ov-stats {
display: flex;
flex-direction: row;
gap: 8px;
flex-shrink: 0;
width: 100%;
justify-content: center;
}
.ov-stat {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
background-color: rgba(255,255,255,0.02);
border-radius: 14px;
padding: 16px 8px 12px 8px;
position: relative;
overflow: hidden;
}
.ov-stat::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
}
.ov-stat-icon { font-size: 18px; opacity: 0.70; }
.ov-stat-val {
font-size: 36px;
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1;
}
.ov-stat-lbl {
font-size: 11px;
font-weight: 700;
color: rgba(255, 255, 255, 0.575);
text-transform: uppercase;
letter-spacing: 0.14em;
}
.ov-stat:nth-child(1) {
border: 1px solid rgba(52,211,153,0.18);
background-color: rgba(52,211,153,0.06);
}
.ov-stat:nth-child(2) {
border: 1px solid rgba(167,139,250,0.18);
background-color: rgba(167,139,250,0.06);
}
.ov-stat:nth-child(3) {
border: 1px solid rgba(251,191,36,0.18);
background-color: rgba(251,191,36,0.06);
}
/* Color accent per stat — target each by position */
.ov-stat:nth-child(1)::before { background-color: #34d399; }
.ov-stat:nth-child(2)::before { background-color: #a78bfa; }
.ov-stat:nth-child(3)::before { background-color: #fbbf24; }
.ov-stat:nth-child(1) { border: 1px solid rgba(52,211,153,0.12); }
.ov-stat:nth-child(2) { border: 1px solid rgba(167,139,250,0.12); }
.ov-stat:nth-child(3) { border: 1px solid rgba(251,191,36,0.12); }
/* ── Stat label colors ── */
.ov-stat:nth-child(1) .ov-stat-val { color: #6ee7b7; }
.ov-stat:nth-child(2) .ov-stat-val { color: #d8b4fe; }
.ov-stat:nth-child(3) .ov-stat-val { color: #fde68a; }
/* Background orbs still game over (ov)*/
@keyframes orb-drift-1 {
0% { margin-left: 0px; margin-top: 0px; }
33% { margin-left: 18px; margin-top: -12px; }
66% { margin-left: -10px; margin-top: 20px; }
100% { margin-left: 0px; margin-top: 0px; }
}
@keyframes orb-drift-2 {
0% { margin-left: 0px; margin-top: 0px; }
40% { margin-left: -20px; margin-top: 14px; }
70% { margin-left: 12px; margin-top: -8px; }
100% { margin-left: 0px; margin-top: 0px; }
}
@keyframes orb-drift-3 {
0% { margin-left: 0px; margin-top: 0px; }
30% { margin-left: 14px; margin-top: 18px; }
60% { margin-left: -8px; margin-top: -14px; }
100% { margin-left: 0px; margin-top: 0px; }
}
.ov-orb {
position: absolute;
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.ov-orb-1 {
width: 280px;
height: 280px;
top: -60px;
left: -80px;
background-color: rgba(109,40,217,0.06);
animation: orb-drift-1 12s ease-in-out infinite;
}
.ov-orb-2 {
width: 200px;
height: 200px;
bottom: 40px;
right: -60px;
background-color: rgba(99,102,241,0.05);
animation: orb-drift-2 16s ease-in-out infinite;
}
.ov-orb-3 {
width: 150px;
height: 150px;
top: 40%;
left: 30%;
background-color: rgba(244,114,182,0.03);
animation: orb-drift-3 20s ease-in-out infinite;
}
/* Game over - secret */
.ov-secret-title {
font-size: 13px;
font-weight: 800;
color: rgba(244,114,182,0.70);
letter-spacing: 0.18em;
text-transform: uppercase;
animation: ov-in 0.5s ease-out 1.6s both;
}
.ov-secrets {
display: flex;
flex-direction: row;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.ov-secret {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
background-color: rgba(244,114,182,0.08);
border: 1px solid rgba(244,114,182,0.25);
border-radius: 14px;
padding: 14px 18px;
min-width: 80px;
overflow: hidden;
}
.ov-secret.locked {
background-color: rgba(255,255,255,0.03);
border-color: rgba(255,255,255,0.08);
animation: chest-idle 2s ease-in-out infinite;
}
.ov-secret.rare {
background-color: rgba(251,191,36,0.10);
border-color: rgba(251,191,36,0.45);
}
.ov-secret.pop { animation: chest-pop 0.7s cubic-bezier(0.22,1,0.36,1) forwards; }
.ov-secret.rare.pop { animation: chest-pop-rare 1.0s cubic-bezier(0.22,1,0.36,1) forwards; }
.ov-secret-icon { font-size: 28px; color: white; }
.ov-secret-lbl { font-size: 13px; font-weight: 800; color: rgba(255,255,255,0.80); }
.ov-secret.rare .ov-secret-lbl { color: #fbbf24; }
.ov-secret-shine {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,0.35) 50%, transparent 70%);
animation: shine-sweep 0.6s ease-out 0.1s forwards;
pointer-events: none;
}
.ov-section-enter {
animation: ov-section-in 0.45s cubic-bezier(0.22,1,0.36,1) forwards;
}
@keyframes ov-section-in {
0% { margin-top: 24px; }
100% { margin-top: 0px; }
}
/* Clickable secret chests during Game Over */
.ov-secret.clickable {
cursor: none;
border-color: rgba(167,139,250,0.55);
background-color: rgba(167,139,250,0.10);
animation: chest-idle 1.0s ease-in-out infinite;
pointer-events: all;
}
.ov-secret.clickable:hover {
background-color: rgba(167,139,250,0.20);
border-color: rgba(167,139,250,0.85);
}
@keyframes chest-idle {
0%,100% { transform: rotate(0deg); }
25% { transform: rotate(-2deg) scale(1.02); }
75% { transform: rotate(2deg) scale(1.02); }
}
@keyframes chest-pop {
0% { transform: scale(1.0); }
20% { transform: scale(0.88); }
55% { transform: scale(1.22); }
75% { transform: scale(0.96); }
100% { transform: scale(1.0); }
}
@keyframes chest-pop-rare {
0% { transform: scale(1.0) rotate(0deg); }
15% { transform: scale(0.82) rotate(-4deg); }
45% { transform: scale(1.35) rotate(3deg); }
65% { transform: scale(0.93) rotate(-1deg); }
82% { transform: scale(1.06) rotate(0deg); }
100% { transform: scale(1.0) rotate(0deg); }
}
@keyframes shine-sweep {
0% { transform: translateX(-200%); }
100% { transform: translateX(300%); }
}
/* Secret section - when clickicking */
.ov-secret-section {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.ov-secret-eyebrow {
font-size: 11px;
font-weight: 800;
color: rgba(244,114,182,0.70);
letter-spacing: 0.22em;
text-transform: uppercase;
}
.ov-secret-hint {
font-size: 13px;
font-weight: 600;
color: rgba(255,255,255,0.35);
}
/* ── Chest cards ── */
.ov-chest-cards {
display: flex;
flex-direction: row;
gap: 16px;
justify-content: center;
}
.ov-chest-card {
width: 130px;
min-height: 150px; /* was height: 150px */
border-radius: 16px;
border: 1.5px solid rgba(167,139,250,0.15);
background-color: rgba(13,6,32,0.90);
position: relative;
overflow: visible;
cursor: none;
box-shadow: 0px 4px 0px rgba(0,0,0,0.40);
}
.ov-chest-card.ready {
border-color: rgba(167,139,250,0.55);
background-color: rgba(60,20,140,0.25);
cursor: none;
pointer-events: all;
animation: chest-card-ready 1.4s ease-in-out infinite alternate;
box-shadow: 0px 4px 0px rgba(0,0,0,0.40),
0px 0px 20px rgba(167,139,250,0.15);
}
.ov-chest-card.ready:hover {
border-color: rgba(167,139,250,0.90);
background-color: rgba(109,40,217,0.30);
}
.ov-chest-card.waiting {
border-color: rgba(255,255,255,0.06);
background-color: rgba(255,255,255,0.02);
box-shadow: none;
}
.ov-chest-card.revealed {
border-color: rgba(129,140,248,0.35);
background-color: rgba(99,102,241,0.08);
box-shadow: 0px 4px 0px rgba(0,0,0,0.30),
0px 0px 16px rgba(129,140,248,0.10);
}
.ov-chest-card.revealed.rare {
border-color: rgba(251,191,36,0.50);
background-color: rgba(251,191,36,0.08);
box-shadow: 0px 4px 0px rgba(0,0,0,0.30),
0px 0px 24px rgba(251,191,36,0.12);
}
.ov-chest-card.pop-in {
animation: chest-card-pop 0.7s cubic-bezier(0.22,1,0.36,1) forwards;
}
.ov-chest-card-inner {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
pointer-events: none;
}
.ov-chest-emoji {
font-size: 48px;
animation: chest-card-idle 2.2s ease-in-out infinite alternate;
}
.ov-chest-card.waiting .ov-chest-emoji {
opacity: 0.25;
animation: none;
}
.ov-chest-tap {
font-size: 10px;
font-weight: 900;
color: white;
background-color: rgba(167,139,250,0.35);
border: 1px solid rgba(167,139,250,0.50);
border-radius: 6px;
padding: 3px 10px;
letter-spacing: 0.12em;
}
.ov-chest-tap.locked {
background-color: rgba(255,255,255,0.05);
border-color: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.20);
}
.ov-chest-glow {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border-radius: 14px;
animation: chest-glow-pulse 1.2s ease-in-out infinite alternate;
pointer-events: none;
}
.ov-chest-card.ready {
border-color: rgba(167,139,250,0.60);
background-color: rgba(109,40,217,0.18);
cursor: none;
pointer-events: all;
animation: chest-card-ready 1.2s ease-in-out infinite alternate;
}
.ov-chest-reward-icon {
font-size: 40px;
}
.ov-chest-reward-lbl {
font-size: 11px;
font-weight: 800;
color: rgba(255,255,255,0.75);
padding: 0px 8px;
}
/* rare badge */
.ov-chest-revealed-icon {
font-size: 40px;
}
.ov-chest-revealed-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.ov-chest-revealed-lbl {
font-size: 13px;
font-weight: 900;
color: white;
padding: 0px 4px;
}
.ov-chest-rare-badge {
font-size: 9px;
font-weight: 900;
color: #fbbf24;
background-color: rgba(251,191,36,0.10);
border: 1px solid rgba(251,191,36,0.30);
border-radius: 20px;
padding: 2px 8px;
letter-spacing: 0.10em;
}
.ov-confetti-bit {
position: absolute;
top: -20px;
width: 8px;
height: 8px;
border-radius: 2px;
animation-name: confetti-fly;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
pointer-events: none;
z-index: 10;
}
@keyframes confetti-fly {
0% { opacity: 1; transform: translateY(0px) rotate(0deg) scale(1.2); }
30% { opacity: 1; }
100% { opacity: 0; transform: translateY(-140px) rotate(520deg) scale(0.4); }
}
@keyframes chest-card-ready {
0% { transform: translateY(0px) scale(1.0); }
100% { transform: translateY(-5px) scale(1.02); }
}
@keyframes chest-card-idle {
0% { transform: translateY(0px) rotate(-1.5deg); }
100% { transform: translateY(-5px) rotate(1.5deg); }
}
@keyframes chest-card-pop {
0% { transform: scale(1.0); }
20% { transform: scale(0.85); }
55% { transform: scale(1.25); }
75% { transform: scale(0.96); }
100% { transform: scale(1.0); }
}
@keyframes chest-glow-pulse {
0% { opacity: 0.4; }
100% { opacity: 1.0; }
}
@keyframes chest-shine-sweep {
0% { transform: translateX(-100%); }
40% { transform: translateX(250%); }
100% { transform: translateX(250%); }
}
@keyframes confetti-fly {
0% { opacity: 1; transform: translateY(0px) rotate(0deg) scale(1.0); }
60% { opacity: 1; }
100% { opacity: 0; transform: translateY(-90px) rotate(380deg) scale(0.6); }
}
/* Frags section */
.ov-frag-section {
width: 100%;
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: center;
gap: 12px;
background-color: rgba(99,102,241,0.05);
border: 1px solid rgba(99,102,241,0.18);
border-radius: 18px;
padding: 20px 22px;
position: relative;
overflow: hidden;
}
/* Subtle shimmer across the top */
.ov-frag-section::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background-color: rgba(129,140,248,0.35);
}
.ov-frag-breakdown {
display: flex;
flex-direction: column;
gap: 5px;
width: 100%;
}
.ov-frag-line {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
background-color: rgba(255,255,255,0.025);
border-radius: 8px;
border-left: 2px solid rgba(129,140,248,0.20);
}
.ov-frag-line-lbl {
font-size: 12px;
font-weight: 600;
color: rgba(255,255,255,0.35);
}
.ov-frag-line-val {
font-size: 13px;
font-weight: 900;
color: #818cf8;
}
.ov-frag-label {
font-size: 10px;
font-weight: 900;
color: rgba(129,140,248,0.55);
letter-spacing: 0.22em;
text-transform: uppercase;
}
.ov-frag-val {
font-size: 52px;
font-weight: 900;
color: #818cf8;
letter-spacing: -0.02em;
line-height: 1;
}
.ov-frag-total {
font-size: 12px;
font-weight: 700;
color: rgba(129,140,248,0.50);
border-top: 1px solid rgba(129,140,248,0.10);
padding-top: 8px;
width: 100%;
text-align: center;
}
.ov-frag-hint {
font-size: 11px;
color: rgba(255,255,255,0.22);
font-weight: 600;
letter-spacing: 0.04em;
}
/**/
.ov-score-section {
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: center;
gap: 6px;
animation: ov-score-appear 0.6s ease-out forwards;
}
@keyframes ov-score-appear {
0% { opacity: 0; }
100% { opacity: 1; }
}
.ov-score-label { font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.35); letter-spacing: 0.18em; text-transform: uppercase; }
.ov-score { font-size: 62px; font-weight: 900; color: #a78bfa; }
.ov-score.gold { color: #fbbf24; }
.ov-newbest-badge {
font-size: 15px;
font-weight: 900;
color: #fbbf24;
background-color: rgba(251,191,36,0.12);
border: 1.5px solid rgba(251,191,36,0.40);
border-radius: 8px;
padding: 4px 14px;
letter-spacing: 0.12em;
animation: badge-slam 0.5s cubic-bezier(0.22,1,0.36,1) 0.6s both;
}
@keyframes badge-slam {
0% { opacity: 0; transform: scale(0.3); }
60% { transform: scale(1.15); }
100% { opacity: 1; transform: scale(1.0); }
}
.ov-btns {
display: flex;
flex-direction: row;
gap: 12px;
flex-shrink: 0; /* never let the buttons get squeezed */
}
/* ── Play Again button sweep ── */
.ov-btn-wrap {
position: relative;
overflow: hidden;
background-color: #3aed3a;
border: 1.5px solid #8bfa9a;
border-radius: 12px;
color: white;
font-size: 30px;
font-weight: 800;
padding: 15px 40px;
cursor: none;
pointer-events: all;
animation: btn-breathe 2.2s ease-in-out infinite alternate;
display: flex;
align-items: center;
justify-content: center;
}
@keyframes btn-breathe {
0% { transform: scale(1.0); }
100% { transform: scale(1.05); }
}
.ov-btn-shine {
position: absolute;
top: 0; bottom: 0;
left: -80%;
width: 40%;
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.25) 50%, transparent 100%);
animation: btn-shine-sweep 3.0s ease-in-out infinite;
pointer-events: none;
}
@keyframes btn-shine-sweep {
0% { left: -80%; }
45% { left: 140%; }
100% { left: 140%; }
}
@keyframes ov-in {
0% { opacity: 0; transform: translateY(12px) scale(0.97); }
100% { opacity: 1; transform: translateY(0) scale(1.0); }
}
.ov-reveal {
animation: ov-in 0.4s ease-out both;
}
/* Main Menu Button */
.ov-btn-menu {
background-color: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 12px;
color: rgba(255,255,255,0.40);
font-size: 14px;
font-weight: 700;
padding: 15px 20px;
cursor: none;
display: flex;
align-items: center;
justify-content: center;
}
.ov-btn-menu:hover {
background-color: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.65);
}
/* -------------------- Modifier strip -----------------------
* This shows the equipped modifiers as a vertical strip in the sidebar
* Each item has its icon, name, and a tooltip on hover showing the full description.
* The green tint matches the "active" color language used in the shop,
* creating visual consistency between menu and game.
* Players can now glance at the sidebar mid-run and see exactly what's buffing them
* Combined with the boss multiplier suppression warning I added,
* the player always knows what's affecting their score
*/
.mod-strip {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.mod-strip-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 8px 12px;
background-color: rgba(52,211,153,0.06);
border: 1px solid rgba(52,211,153,0.20);
border-radius: 8px;
transition: background-color 0.2s ease;
}
.mod-strip-item:hover {
background-color: rgba(52,211,153,0.12);
}
.mod-strip-icon {
font-size: 18px;
flex-shrink: 0;
}
.mod-strip-name {
font-size: 11px;
font-weight: 700;
color: rgba(255,255,255,0.75);
letter-spacing: 0.04em;
}
/* ── Game over reason ── */
.ov-reason {
animation: ov-section-in 0.5s cubic-bezier(0.22,1,0.36,1) 0.5s both;
font-size: 13px;
font-weight: 700;
color: rgba(255,255,255,0.55);
padding: 8px 16px;
background-color: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
width: 100%;
justify-content: center;
}
/* ---------------- SCREEN SHAKE ---------------- */
.hud-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 5;
}
.hud-overlay.shake-small { animation: shake-small 0.25s ease-out; }
.hud-overlay.shake-medium { animation: shake-medium 0.35s ease-out; }
.hud-overlay.shake-strong { animation: shake-strong 0.45s ease-out; }
.hud-overlay.shake-insane { animation: shake-insane 0.55s ease-out; }
.mini-hand.shake-small { animation: shake-small 0.25s ease-out; }
.mini-hand.shake-medium { animation: shake-medium 0.35s ease-out; }
.mini-hand.shake-strong { animation: shake-strong 0.45s ease-out; }
.mini-hand.shake-insane { animation: shake-insane 0.55s ease-out; }
@keyframes shake-small {
0%,100% { transform: translate(0,0); }
25% { transform: translate(-2px, 1px); }
50% { transform: translate(2px, -1px); }
75% { transform: translate(-1px, 2px); }
}
@keyframes shake-medium {
0%,100% { transform: translate(0,0); }
20% { transform: translate(-4px, 2px) rotate(-0.3deg); }
40% { transform: translate(4px,-3px) rotate(0.3deg); }
60% { transform: translate(-3px, 3px) rotate(-0.2deg); }
80% { transform: translate(3px,-2px) rotate(0.2deg); }
}
@keyframes shake-strong {
0%,100% { transform: translate(0,0); }
15% { transform: translate(-6px, 3px) rotate(-0.5deg); }
30% { transform: translate(6px,-4px) rotate(0.5deg); }
45% { transform: translate(-5px, 5px) rotate(-0.4deg); }
60% { transform: translate(5px,-4px) rotate(0.4deg); }
75% { transform: translate(-3px, 2px); }
90% { transform: translate(2px,-2px); }
}
@keyframes shake-insane {
0%,100% { transform: translate(0,0); }
10% { transform: translate(-9px, 5px) rotate(-0.7deg); }
20% { transform: translate(9px,-6px) rotate(0.7deg); }
30% { transform: translate(-8px, 7px) rotate(-0.6deg); }
40% { transform: translate(8px,-6px) rotate(0.6deg); }
50% { transform: translate(-6px, 5px) rotate(-0.4deg); }
60% { transform: translate(6px,-5px) rotate(0.4deg); }
70% { transform: translate(-4px, 3px); }
80% { transform: translate(3px,-3px); }
90% { transform: translate(-1px, 1px); }
}
/* ────────────────────────────────────────────────────────────────── */
/* ────── Consumable strip — horizontal bar below challenge bar ───── */
/* ────────────────────────────────────────────────────────────────── */
.consumable-strip {
position: absolute;
bottom: 120px; /* sits above the bomb wheel */
left: 0;
right: 150px; /* leave room for sidebar */
display: flex;
flex-direction: row;
justify-content: center;
gap: 8px;
pointer-events: all;
z-index: 20;
}
.consumable-slot {
position: relative;
left: 70px;
width: 54px;
height: 54px;
border-radius: 10px;
border: 1.5px solid rgba(255,255,255,0.10);
background-color: rgba(255,255,255,0.04);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: none;
position: relative;
}
.consumable-slot.available {
border-color: rgba(167,139,250,0.50);
background-color: rgba(109,40,217,0.15);
cursor: none;
}
.consumable-slot.available:hover {
background-color: rgba(109,40,217,0.30);
border-color: rgba(167,139,250,0.80);
}
.consumable-slot.empty {
opacity: 0.25;
cursor: none;
}
.consumable-icon {
font-size: 22px;
pointer-events: none;
}
/* Count badge — bottom right corner of slot */
.consumable-count {
position: absolute;
bottom: 2px;
right: 4px;
font-size: 10px;
font-weight: 900;
color: rgba(167,139,250,0.95);
pointer-events: none;
}
/* ── Screen Flash ── */
.screen-flash {
position: absolute;
top: 0; left: 0;
width: 2200px;
height: 1400px;
pointer-events: none;
z-index: 100;
}
.flash-text {
position: absolute;
top: 485px; /* roughly vertical center of 1080p */
left: 1055px; /* roughly horizontal center minus half text width */
font-size: 72px;
font-weight: 900;
color: rgba(255,255,255,0.90);
pointer-events: none;
animation: flash-text-pop 1.2s ease-out forwards;
text-shadow:
-3px -3px 0 #000,
3px -3px 0 #000,
-3px 3px 0 #000,
3px 3px 0 #000,
-4px 0 0 #000,
4px 0 0 #000,
0 -4px 0 #000,
0 4px 0 #000;
z-index: 10;
}
@keyframes flash-text-pop {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
12% { opacity: 1; transform: translate(-50%, -50%) scale(1.05); }
70% { opacity: 1; transform: translate(-50%, -50%) scale(1.0); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
}
@keyframes flash-fade {
0% { opacity: 0; }
15% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes flash-boss-spawn {
0% { opacity: 0; }
15% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes flash-wave {
0% { opacity: 0; }
10% { opacity: 1; }
60% { opacity: 1; }
100% { opacity: 0; }
}
.screen-flash.flash-boss-spawn {
background-color: rgba(220, 30, 30, 0.50);
animation: flash-boss-spawn 2.5s ease-out forwards;
}
.screen-flash.flash-boss-death {
background-color: rgba(251,191,36,0.55);
animation: flash-fade 0.6s ease-out forwards;
}
.screen-flash.flash-wave-start {
background-color: rgba(138, 92, 246, 0.082); /* was 0.18 — lighter */
animation: flash-wave .9s ease-out forwards; /* was 2.5s — faster */
}
.screen-flash.flash-new-best {
background-color: rgba(52,211,153,0.45);
animation: flash-fade 1.0s ease-out forwards;
}
.screen-flash.flash-game-over {
background-color: rgba(180,20,20,0.60);
animation: flash-fade 0.8s ease-out forwards;
}
/* ------------------------------------------- */
/* -------------- Pet Companion -------------- */
/* ------------------------------------------- */
.hud-pet-wrap {
position: absolute;
bottom: 55px;
left: 15px;
width: 220px;
height: 870px;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 14px 14px 0px 14px;
z-index: 6;
}
.hud-pet {
font-size: 110px;
animation: hud-pet-idle 2.5s ease-in-out infinite alternate;
opacity: 0.8;
margin-bottom: 10px;
margin-left: 0px;
}
.hud-pet image {
width: 110px;
height: 110px;
}
.hud-pet-egg-icon
{
width: 110px;
height: 110px;
}
/* Top section — modifiers + curse */
.hud-pet-top {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
/* Bottom section — pet + hearts, pinned to bottom */
.hud-pet-bottom {
margin-top: auto;
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
font-size: 64px;
}
@keyframes pet-bob {
from { transform: translateY(0px); }
to { transform: translateY(-8px); }
}
.pet-bob {
animation: pet-bob 1.4s ease-in-out infinite alternate;
}
.hud-pet-egg {
width: 120px;
height: 120px;
}
.hud-pet-bottom image {
width: 134px;
height: 134px;
margin-left: -10px;
}
.pet-sbl {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(166,154,219,0.5);
}
.hud-pet-card {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(18,8,42,0.92);
border: 1px solid rgba(167,139,250,0.18);
border-radius: 14px 14px 0px 0px;
z-index: -1;
}
@keyframes hud-pet-idle {
0% { transform: translateY(0px) scale(1.0); }
100% { transform: translateY(-6px) scale(1.05); }
}
/* -------------------------------------------------- */
/* -------------- Hearts/Lives Sidebar -------------- */
/* -------------------------------------------------- */
.hud-pet-lives {
position: absolute;
right: 0px;
left: 150px;
bottom: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.hud-pet-heart {
font-size: 40px;
}
.hud-pet-heart.empty {
opacity: 0.20;
}
/* -------------------------------------------------- */
/* -------------- BACKGROUND (grid-like) ------------ */
/* -------------------------------------------------- */
.game-bg-grid {
position: absolute;
top: 90px; left: 0; right: 0; bottom: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.game-bg-cell {
position: absolute;
width: 232px;
height: 232px;
border-radius: 10px;
border: 2px solid rgba(66, 54, 75, 0.04);
z-index: 0;
}
.game-bg-cell.gbg-idle { /* inline style from WaveGridColor — already very dark hex */ }
.game-bg-cell.gbg-expl { background-color: rgba(10,4,24,0.95); border-color: rgba(LoadPetLbLoadPetLb251,146,60,0.22); }
.game-bg-cell.gbg-expl2 { background-color: rgba(10,4,24,0.95); border-color: rgba(251,191,36,0.22); }
.game-bg-cell.gbg-gold { background-color: rgba(10,4,24,0.95); border-color: rgba(251,191,36,0.16); }
.game-bg-cell.gbg-red { background-color: rgba(10,4,24,0.95); border-color: rgba(248,113,113,0.16); }
.game-bg-cell.gbg-chain { background-color: rgba(10,4,24,0.95); border-color: rgba(196,181,253,0.14); }
/* SIDEBAR BUTTON (info) */
.sidebar-toggle {
position: absolute;
top: 155px;
right: 12px;
width: 44px;
height: 44px;
border-radius: 50px;
background-color: rgba(180,30,30,0.85);
border: 2px solid rgba(255,100,100,0.60);
color: white;
font-size: 20px;
font-weight: 900;
pointer-events: all;
z-index: 20;
cursor: none;
box-shadow: 2px 2px 0px rgba(0,0,0,0.60);
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-toggle:hover {
background-color: rgba(220,40,40,0.95);
border-color: rgba(255,140,140,0.80);
}
/**/
.pet-mod-strip {
display: flex;
flex-direction: row;
gap: 6px;
margin-top: 8px;
}
.pet-mod-item {
width: 32px;
height: 32px;
border-radius: 8px;
background-color: rgba(52,211,153,0.10);
border: 1px solid rgba(52,211,153,0.25);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.pet-curse-warning {
font-size: 13px;
font-weight: 800;
color: rgba(248,113,113,0.85);
margin-top: 6px;
padding: 4px 8px;
background-color: rgba(239,68,68,0.10);
border: 1px solid rgba(248,113,113,0.20);
border-radius: 6px;
}
/* Min chest requirement (displayed on left side bar)*/
.hud-min-chests {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 4px 23px;
background-color: rgba(0,0,0,0.35);
border-radius: 6px;
margin-top: 4px;
justify-content: center;
}
.hud-min-chests-lbl {
font-size: 18px;
font-weight: 700;
color: rgba(255,255,255,0.55);
}
.hud-min-chests-val {
font-size: 18px;
font-weight: 900;
color: rgba(251,191,36,0.90);
}
/* MUSIC DEBUG */
.mm-dev-music-panel {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
background-color: rgba(4,1,13,0.90);
border: 1px dashed rgba(244,114,182,0.30);
border-radius: 6px;
z-index: 99;
pointer-events: all;
}
.mm-dev-music-label {
font-size: 9px;
font-weight: 900;
color: rgba(244,114,182,0.60);
letter-spacing: 0.20em;
text-transform: uppercase;
margin-bottom: 2px;
}
.mm-dev-btn {
background-color: rgba(244,114,182,0.15);
border: 1px solid rgba(244,114,182,0.30);
border-radius: 4px;
color: rgba(244,114,182,0.85);
font-size: 10px;
font-weight: 700;
padding: 4px 8px;
cursor: none;
pointer-events: all;
letter-spacing: 0.05em;
}
.mm-dev-btn:hover {
background-color: rgba(244,114,182,0.25);
}
.mm-dev-btn-active {
background-color: rgba(52,211,153,0.20);
border-color: rgba(52,211,153,0.50);
color: rgba(52,211,153,0.90);
}
/* =============== New SURVIVAL mode =============== */
.survival-timer {
position: absolute;
top: 100px;
left: 50%;
transform: translateX(-50%);
width: 160px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
background-color: rgba(10,4,28,0.80);
border: 2px solid rgba(239,68,68,0.35);
border-radius: 10px;
box-shadow: 3px 3px 0px rgba(0,0,0,0.55);
z-index: 10;
}
.survival-timer-icon {
font-size: 16px;
}
.survival-timer-val {
font-size: 20px;
font-weight: 900;
color: rgba(251,191,36,0.90);
letter-spacing: 0.04em;
font-variant-numeric: tabular-nums;
}
.survival-spawn-track {
position: absolute;
top: 145px; /* just below the timer pill */
left: 50%;
transform: translateX(-50%);
width: 160px;
height: 6px;
background-color: rgba(255,255,255,0.08);
border-radius: 3px;
overflow: hidden;
z-index: 10;
}
.survival-spawn-fill {
height: 100%;
background-color: rgba(251,191,36,0.85);
transition: width 0.1s linear;
}
/* CAT */
.mm-pet-hero-emoji image {
width: 170px; /* bump this up to whatever size you want */
height: 170px;
image-rendering: pixelated; /* keep it crisp if it's pixel art */
margin-left: -10px;
}
.mm-pet-hero-emoji {
position: relative;
}
.cat-anchor { position: relative; }
.cat-speech-bubble {
position: absolute;
top: -50px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255,255,255,0.95);
color: #1a1030;
font-size: 14px;
font-weight: 800;
padding: 8px 14px;
border-radius: 12px;
white-space: nowrap;
box-shadow: 2px 2px 0px rgba(0,0,0,0.4);
z-index: 10;
animation: bubble-pop 0.2s ease-out both;
}
.cat-speech-bubble::after {
content: "";
position: absolute;
bottom: -6px;
left: 50%;
width: 12px;
height: 12px;
background-color: rgba(255,255,255,0.95);
transform: translateX(-50%) rotate(45deg);
}
@keyframes bubble-pop {
0% { opacity: 0; transform: translateX(-50%) scale(0.7); }
100% { opacity: 1; transform: translateX(-50%) scale(1.0); }
}
/* Back button */
.back-toggle {
top: 211px; /* 155 + 44 + 12 gap */
background-color: rgba(60,60,180,0.85);
border-color: rgba(140,140,255,0.60);
}
.back-toggle:hover {
background-color: rgba(80,80,220,0.95);
border-color: rgba(160,160,255,0.80);
}
.back-confirm-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(4,1,13,0.85);
z-index: 90;
pointer-events: all;
display: flex;
align-items: center;
justify-content: center;
}
.back-confirm-popup {
background-color: rgba(20,8,50,0.98);
border: 2px solid rgba(167,139,250,0.55);
border-radius: 18px;
padding: 32px 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
max-width: 380px;
text-align: center;
box-shadow: 6px 6px 0px rgba(20,0,60,0.80);
}
.back-confirm-title { font-size: 22px; font-weight: 900; color: white; }
.back-confirm-desc { font-size: 13px; color: rgba(255,255,255,0.55); line-height: 1.5; }
.back-confirm-btns { display: flex; flex-direction: row; gap: 12px; margin-top: 8px; }
.back-confirm-btn {
border-radius: 10px;
font-size: 14px;
font-weight: 800;
padding: 12px 20px;
cursor: none;
pointer-events: all;
border: 2px solid transparent;
}
.back-confirm-btn.cancel { background-color: rgba(52,211,153,0.15); border-color: rgba(52,211,153,0.45); color: #6ee7b7; }
.back-confirm-btn.confirm { background-color: rgba(239,68,68,0.15); border-color: rgba(239,68,68,0.45); color: #fca5a5; }
/* Survival hype track */
.survival-strip {
position: absolute;
top: 96px;
left: 0;
right: 0;
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
z-index: 10;
pointer-events: none;
}
.survival-timer {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 5px 12px;
background-color: rgba(10,4,28,0.85);
border: 2px solid rgba(239,68,68,0.35);
border-radius: 8px;
flex-shrink: 0;
}
.survival-timer-icon { font-size: 14px; }
.survival-timer-val {
font-size: 18px;
font-weight: 900;
color: rgba(251,191,36,0.9);
}
.survival-hype-track {
position: relative;
width: 130px;
height: 20px;
background-color: rgba(255,255,255,0.08);
border: 1px solid rgba(251,146,60,0.35);
border-radius: 6px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.survival-hype-fill {
position: absolute;
top: 0; left: 0; bottom: 0;
background-color: rgba(251,146,60,0.75);
transition: width 0.12s linear;
}
.survival-hype-track.maxed {
border-color: rgba(251,191,36,0.95);
animation: hype-pulse 0.5s ease-in-out infinite alternate;
}
.survival-hype-track.maxed .survival-hype-fill { background-color: rgba(251,191,36,0.9); }
.survival-hype-lbl {
font-size: 9px;
font-weight: 900;
letter-spacing: 0.12em;
color: rgba(255,255,255,0.9);
z-index: 1;
}
@keyframes hype-pulse {
from { border-color: rgba(251,191,36,0.5); }
to { border-color: rgba(251,191,36,1.0); }
}
.survival-goal {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 5px 12px;
background-color: rgba(10,4,28,0.85);
border: 1px solid rgba(167,139,250,0.35);
border-radius: 8px;
flex-shrink: 0;
}
.survival-goal-lbl { font-size: 11px; font-weight: 800; color: rgba(255,255,255,0.85); }
.survival-goal-prog { font-size: 12px; font-weight: 900; color: #a78bfa; }
.survival-goal-timer {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 2px;
background-color: rgba(255,255,255,0.06);
}
.survival-goal-timer-fill {
height: 100%;
background-color: rgba(167,139,250,0.7);
transition: width 0.2s linear;
}
.survival-spawn-track {
width: 90px;
height: 6px;
background-color: rgba(255,255,255,0.08);
border-radius: 3px;
overflow: hidden;
flex-shrink: 0;
}
.survival-spawn-fill {
height: 100%;
background-color: rgba(251,191,36,0.85);
transition: width 0.1s linear;
}
Game
game
.splitcontainer
{
flex-grow: 1;
> .split-left, > .split-right
{
flex-grow: 1;
}
> .split-left
{
height: 100%;
}
> .splitter
{
flex-grow: 1;
width: 8px;
cursor: ew-resize;
&:hover
{
}
}
&.dragging > .splitter
{
}
&.vertical
{
flex-direction: column;
> .splitter
{
height: 8px;
width: 100%;
cursor: ns-resize;
}
}
}
Game
game
$primary: red !default;
$primary-alt: white !default;
$switch-padding: 6px !default;
.coloreditor
{
flex-direction: column;
min-width: 250px;
max-width: 250px;
> .canvas
{
border-top: 1px solid rgba( white, 0.5 );
flex-grow: 1;
flex-direction: column;
padding: 10px 15px;
> textentry.textentry
{
flex-grow: 0;
flex-shrink: 0;
width: auto;
}
}
.slider
{
padding: 6.5px;
}
.slider .track
{
height: 14px;
border-radius: 14px;
left: 0px;
right: 0px;
transition: all 0.1s ease-out;
.inner
{
display: none;
}
}
.slider:hover, .slider:active
{
padding: 7px;
.track
{
height: 20px;
border-radius: 6px;
}
.thumb
{
width: 10px;
height: 16px;
margin-left: 2px;
}
}
.slider .thumb
{
width: 10px;
height: 10px;
box-shadow: 0px 0px 0px transparent;
background-color: white;
backdrop-filter: invert( 1 );
margin-left: 1px;
pointer-events: none;
}
.alpha_slider .track
{
background-image: linear-gradient( to right, rgba( #ccc, 0.05 ), rgba( #ccc, 0.9 ) );
}
.hue .track
{
background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
.inner
{
display: none;
}
}
.satval
{
background: linear-gradient(to right, white, transparent);
height: 200px;
position: relative;
&:active
{
z-index: 10;
cursor: none;
}
.value_gradient
{
background: linear-gradient(to bottom, black, white);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
mix-blend-mode: multiply;
}
.thumb
{
z-index: 10;
}
}
.presets
{
flex-wrap: wrap;
align-content: center;
justify-content: center;
padding: 15px;
border-top: 1px solid rgba( white, 0.5 );
.preset
{
background-color: white;
width: 16px;
height: 16px;
margin: 2px;
border-radius: 20px;
cursor: pointer;
&:hover
{
box-shadow: 0px 0px 1px 2px white;
}
}
}
}
.coloreditorrgba
{
flex-direction: column;
.slider
{
padding-right: 16px;
}
.slider .track
{
background-color: transparent;
height: 14px;
border-radius: 14px;
left: 0px;
right: 0px;
.inner
{
display: none;
}
}
.slider .thumb
{
width: 12px;
height: 12px;
box-shadow: 0px 0px 0px transparent;
background-color: white;
margin-left: 2px;
backdrop-filter: invert( 1 );
}
.red_slider .track
{
background-image: linear-gradient( to right, rgba( #e63439, 0.1 ), rgba( #e63439, 0.9 ) );
}
.green_slider .track
{
background-image: linear-gradient( to right, rgba( #54be35, 0.1 ), rgba( #54be35, 0.9 ) );
}
.blue_slider .track
{
background-image: linear-gradient( to right, rgba( #3472e6, 0.1 ), rgba( #3472e6, 0.9 ) );
}
.alpha_slider .track
{
background-image: linear-gradient( to right, rgba( #ccc, 0.05 ), rgba( #ccc, 0.9 ) );
}
}
.coloreditorhsva
{
flex-direction: column;
.slider
{
padding-right: 16px;
}
.slider .track
{
background-color: transparent;
height: 14px;
border-radius: 14px;
left: 0px;
right: 0px;
.inner
{
display: none;
}
}
.slider .thumb
{
width: 12px;
height: 12px;
box-shadow: 0px 0px 0px transparent;
background-color: white;
margin-left: 2px;
backdrop-filter: invert( 1 );
}
.hue_slider .track
{
background-image: linear-gradient( to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00 );
}
.alpha_slider .track
{
background-image: linear-gradient( to right, rgba( #ccc, 0.05 ), rgba( #ccc, 0.9 ) );
}
}
Game
game
.colorproperty
{
align-items: center;
position: relative;
.colorsquare
{
width: 20px;
height: 20px;
border-radius: 4px;
margin-right: 8px;
position: absolute;
left: 7px;
z-index: 1;
cursor: pointer;
}
> .textentry:not( .a.b.c )
{
padding-left: 36px;
}
}
Game
game
ColorAlphaControl
{
gap: 0.5rem;
flex-grow: 1;
pointer-events: all;
background: linear-gradient( to right, black, white );
border-radius: 4px;
padding: 2px;
height: 12px;
position: relative;
cursor: pointer;
border: 1px solid #333;
&:hover
{
border: 1px solid #08f;
}
&:active
{
border: 1px solid #fff;
}
.handle
{
top: -5px;
bottom: -5px;
aspect-ratio: 1;
border-radius: 100px;
border: 2px solid #444;
position: absolute;
background-color: white;
box-shadow: 2px 2px 16px #000a;
transform: translateX( -50% );
pointer-events: none;
}
}
Debug: View Raw JSON Response
{
"TotalCount": 95,
"Files": [
{
"Ident": "playnplay.chain_reaction",
"Path": ".obj/__compiler_extra.cs",
"FileName": "__compiler_extra.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Chain Reaction\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"chain_reaction\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"playnplay\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"playnplay.chain_reaction\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-08-25T16:50:49.1757035Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.120.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.120.0\")]"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "Game/2D/MusicManager.cs",
"FileName": "MusicManager.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\npublic sealed class MusicManager : Component\r\n{\r\n public static MusicManager Instance { get; private set; }\r\n\r\n SoundHandle _menuMusic;\r\n SoundHandle _windMusic;\r\n SoundHandle _gameMusic;\r\n\r\n // Tracked explicitly rather than inferred from SoundHandle.IsValid(), because\r\n // a handle stopped with a fade is not reliably invalid straight away \u2014 and\r\n // guessing wrong here means the menu comes back silent.\r\n bool _menuPlaying;\r\n\r\n protected override void OnStart()\r\n {\r\n Instance = this;\r\n\r\n // Subscribe to game events\r\n if (ChainReactionGame.Instance != null)\r\n {\r\n ChainReactionGame.Instance.OnGameStarted += OnGameStarted;\r\n ChainReactionGame.Instance.OnGameReturned += OnGameReturned;\r\n }\r\n\r\n StartMenuLoops(.25f);\r\n }\r\n\r\n void StartMenuLoops(float volume)\r\n {\r\n _menuMusic = Sound.Play(\"phonk_menu_music\", volume);\r\n _windMusic = Sound.Play(\"wind_blowing_loop\", volume);\r\n _menuPlaying = true;\r\n }\r\n\r\n void StopMenuLoops(float fade)\r\n {\r\n if (_menuMusic.IsValid()) _menuMusic.Stop(fade);\r\n if (_windMusic.IsValid()) _windMusic.Stop(fade);\r\n _menuPlaying = false;\r\n }\r\n\r\n void OnGameStarted()\r\n {\r\n // Reactor mode is meant to be left running for a long time \u2014 the combat\r\n // track would grate after ten minutes, so it keeps the calmer menu loop.\r\n if (ChainReactionGame.Instance?.Mode == GameMode.Idle)\r\n {\r\n if (_gameMusic.IsValid()) _gameMusic.Stop(.5f);\r\n\r\n if (!_menuPlaying) StartMenuLoops(.18f);\r\n else if (_menuMusic.IsValid()) _menuMusic.Volume = .18f;\r\n\r\n return;\r\n }\r\n\r\n StopMenuLoops(.5f);\r\n\r\n // Only start game music if not already playing\r\n if (!_gameMusic.IsValid())\r\n _gameMusic = Sound.Play(\"abydos_game\");\r\n }\r\n\r\n public void StopMenuMusic() => StopMenuLoops(0f);\r\n\r\n public void PlayMenuMusic() => StartMenuLoops(.25f);\r\n\r\n void OnGameReturned()\r\n {\r\n // Reactor mode never starts the combat track, so this handle can still be\r\n // default-constructed here \u2014 calling Stop() on that throws.\r\n if (_gameMusic.IsValid()) _gameMusic.Stop(.5f);\r\n\r\n // Coming back from Reactor mode the menu loop never stopped \u2014 restore\r\n // its volume instead of stacking a second copy on top of it.\r\n if (_menuPlaying)\r\n {\r\n if (_menuMusic.IsValid()) _menuMusic.Volume = .3f;\r\n return;\r\n }\r\n\r\n StartMenuLoops(.3f);\r\n }\r\n}\r\n"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "Game/2D/UI/MainMenuTutorial.razor",
"FileName": "MainMenuTutorial.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "@using System;\n@using Sandbox;\n@using Sandbox.Game.Menu\n@using Sandbox.UI;\n@inherits PanelComponent\n@namespace Sandbox\n\n<root>\n @if(_active)\n {\n\t\t@* \u2500\u2500 Debug card \u2500\u2500 *@\n\t\t@if(_debugMode)\n\t\t{\n\t\t\t<div class=\"tut-debug-card\">\n\t\t\t\t<div class=\"tut-debug-title\">MM TUT \u2014 Step @_step</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>BL: @_debugBL.ToString(\"F1\")%</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugBL -= 1f; StateHasChanged(); })>\u25c0</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugBL += 1f; StateHasChanged(); })>\u25b6</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>BY: @_debugBY.ToString(\"F0\")px</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugBY -= 10f; StateHasChanged(); })>\u25b2</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugBY += 10f; StateHasChanged(); })>\u25bc</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>BR: @_debugBR.ToString(\"F1\")%</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugBR -= 1f; StateHasChanged(); })>\u25c0</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugBR += 1f; StateHasChanged(); })>\u25b6</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>HX: @_debugHX.ToString(\"F1\")%</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHX -= 1f; StateHasChanged(); })>\u25c0</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHX += 1f; StateHasChanged(); })>\u25b6</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>HY: @_debugHY.ToString(\"F0\")px</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHY -= 10f; StateHasChanged(); })>\u25b2</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHY += 10f; StateHasChanged(); })>\u25bc</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>HW: @_debugHW.ToString(\"F0\")px</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHW -= 10f; StateHasChanged(); })>\u25c0</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHW += 10f; StateHasChanged(); })>\u25b6</button>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tut-debug-row\">\n\t\t\t\t\t<span>HH: @_debugHH.ToString(\"F0\")px</span>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHH -= 10f; StateHasChanged(); })>\u25b2</button>\n\t\t\t\t\t<button class=\"tut-debug-btn\" onclick=@(() => { _debugHH += 10f; StateHasChanged(); })>\u25bc</button>\n\t\t\t\t</div>\n\t\t\t\t<button class=\"tut-debug-log\" onclick=@LogCurrentValues>\ud83d\udccb Log</button>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2500\u2500 Dim layer \u2500\u2500 *@\n\t\t<div class=\"tut-dim\"></div>\n\n\t\t@* \u2500\u2500 Highlight \u2500\u2500 *@\n\t\t@if(_step < _steps.Length && _steps[_step].HW > 0)\n\t\t{\n\t\t\tvar s = _steps[_step];\n\t\t\tfloat hx = _debugHX != 0f ? _debugHX : s.HX;\n\t\t\tfloat hy = _debugHY != 0f ? _debugHY : s.HY;\n\t\t\tfloat hw = _debugHW != 0f ? _debugHW : s.HW;\n\t\t\tfloat hh = _debugHH != 0f ? _debugHH : s.HH;\n\t\t\t<div class=\"tut-highlight\"\n\t\t\t\t\tstyle=\"left:@hx.ToString(\"F1\")%;top:@hy.ToString(\"F0\")px;width:@hw.ToString(\"F0\")px;height:@hh.ToString(\"F0\")px;\">\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2500\u2500 Instruction box \u2500\u2500 *@\n\t\t@if(_step < _steps.Length)\n\t\t{\n\t\t\tvar s = _steps[_step];\n\t\t\tfloat bl = _debugBL != 0f ? _debugBL : s.BL;\n\t\t\tfloat br = _debugBR != 0f ? _debugBR : s.BR;\n\t\t\tfloat by = _debugBY != 0f ? _debugBY : s.BY;\n\n\t\t\tfloat boxWidthPct = s.BW > 0f ? (s.BW / 1920f * 100f) : (380f / 1920f * 100f);\n\n\t\t\tstring posStyle = br != 0f\n\t\t\t\t? $\"left:{(100f - br - boxWidthPct).ToString(\"F1\")}%;\"\n\t\t\t\t: $\"left:{bl.ToString(\"F1\")}%;\";\n\t\t\tstring boxStyle = $\"{posStyle}top:{by.ToString(\"F0\")}px;{(s.BW > 0f ? $\"width:{s.BW.ToString(\"F0\")}px;\" : \"\")}\";\n\n\t\t\t<div class=\"tut-box\" style=\"@boxStyle\">\n\t\t\t\t<div class=\"tut-step-count\">@(_step + 1) / @_steps.Length</div>\n\t\t\t\t<div class=\"tut-title\">@s.Title</div>\n\t\t\t\t<div class=\"tut-text\">@s.Text</div>\n\n\t\t\t\t@* Action hint \u2014 shown when player must act *@\n\t\t\t\t@if(s.Trigger == MMTutTrigger.HeartPurchased)\n\t\t\t\t{\n\t\t\t\t\t<div class=\"tut-action-hint\">\ud83d\udc46 @s.ActionHint</div>\n\t\t\t\t}\n\n\t\t\t\t@* Next button \u2014 shown on Any steps *@\n\t\t\t\t@if(s.Trigger == MMTutTrigger.Any)\n\t\t\t\t{\n\t\t\t\t\t<button class=\"tut-btn-next\" onclick=@NextStep>Got it \u2192</button>\n\t\t\t\t}\n\n\t\t\t\t@* Final step \u2014 Play button *@\n\t\t\t\t@if(s.Trigger == MMTutTrigger.Done)\n\t\t\t\t{\n\t\t\t\t\t<button class=\"tut-btn-next\" onclick=@OnPlayAndComplete>\u25b6 Let's play!</button>\n\t\t\t\t}\n\t\t\t</div>\n\t\t}\n }\n</root>\n\n@code {\n public static MainMenuTutorial Instance { get; private set; }\n public bool IsActive => _active;\n\n bool _active = false;\n\tpublic int Step => _step;\n int _step = 0;\n\n // \u2500\u2500 Debug mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n const bool _debugMode = false; // set false before release\n float _debugBL = 0f;\n float _debugBR = 0f;\n float _debugBY = 0f;\n float _debugHX = 0f;\n float _debugHY = 0f;\n float _debugHW = 0f;\n float _debugHH = 0f;\n\n void LogCurrentValues()\n {\n Log.Info($\"// MM Step {_step} \u2014 {_steps[_step].Title}\");\n Log.Info($\"BL={_debugBL:F1}f, BY={_debugBY:F0}f, BR={_debugBR:F1}f,\");\n Log.Info($\"HX={_debugHX:F1}f, HY={_debugHY:F0}f, HW={_debugHW:F0}f, HH={_debugHH:F0}f\");\n }\n\n // \u2500\u2500 Trigger types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n public enum MMTutTrigger\n {\n Any, // advances via Got it button\n HeartPurchased, // advances when player buys Heart consumable\n Done, // final step \u2014 shows Play button\n }\n\n // \u2500\u2500 Step data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n public struct MMTutStep\n {\n public string Title;\n public string Text;\n public string ActionHint;\n public MMTutTrigger Trigger;\n public float HX, HY, HW, HH; // highlight\n public float BL, BR, BY, BW; // box position\n }\n\n // \u2500\u2500 Steps \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n // Positions are placeholder \u2014 tune with debug card in-game\n static readonly MMTutStep[] _steps = new MMTutStep[]\n {\n // Step 1 \u2014 Welcome\n new MMTutStep {\n Title = \"Welcome to Chain Reaction!\",\n Text = \"Here's a quick tour of how the game works. It'll take 30 seconds.\",\n Trigger = MMTutTrigger.Any,\n HW = 0,\n BL = 47f, BY = 370f\n },\n\n // Step 2 \u2014 Play button\n new MMTutStep {\n Title = \"Play runs to earn \ud83d\udca0 Fragments\",\n Text = \"Fragments are the game's currency. The further you go each run, the more you earn.\",\n Trigger = MMTutTrigger.Any,\n HW = 0,\n BL = 47f, BY = 370f\n },\n\n // Step 3 \u2014 Shop \u2192 Modifiers\n new MMTutStep {\n Title = \"The Shop \u2014 Modifiers \ud83c\udfb2\",\n Text = \"Spend fragments to unlock Modifiers. These permanently strengthen every run you play. Unlock once, keep forever.\",\n Trigger = MMTutTrigger.Any,\n HX = 40f, HY = 200f, HW = 110f, HH = 40f,\n BR = 61f, BY = 120f\n },\n\n // Step 4 \u2014 Consumables explanation\n new MMTutStep {\n Title = \"Consumables \ud83c\udf81\",\n Text = \"Consumables are single-use items you bring into a run. Buy them once, use them at any time during a run. A bar will appear in-game when you own one.\",\n Trigger = MMTutTrigger.Any,\n HX = 46f, HY = 200f, HW = 130f, HH = 40f,\n BR = 61f, BY = 110\n },\n\n // Step 5 \u2014 Buy Heart (action step)\n new MMTutStep {\n Title = \"Get yourself a Heart \u2764\ufe0f\",\n Text = \"We've added 100 \ud83d\udca0 to your balance. Buy the Heart consumable \u2014 it gives you an extra life mid-run when you need it most.\",\n ActionHint = \"Click the buy button on the Heart card\",\n Trigger = MMTutTrigger.HeartPurchased,\n HX = 65f, HY = 860f, HW = 80f, HH = 70f,\n BR = 7f, BY = 800f\n },\n\n // Step 6 \u2014 Pet tab\n new MMTutStep {\n Title = \"Your secret package \ud83d\udce6\",\n Text = \"Play 3 runs to unlock what's inside. This section is for cosmetic skins \u2014 coming soon.\",\n \t\tTrigger = MMTutTrigger.Any,\n HX = 53f, HY = 360f, HW = 70f, HH = 40f,\n BR = 50f, BY = 340f\n },\n\n // Step 7 \u2014 Bombs\n new MMTutStep {\n Title = \"Unlock new Bombs \ud83d\udca3\",\n Text = \"Different bomb types open up new chain reaction strategies. Mix and match to find your playstyle.\",\n Trigger = MMTutTrigger.Any,\n HX = 57f, HY = 220f, HW = 90f, HH = 40f,\n BR = 5f, BY = 130f\n },\n\n // Step 8 \u2014 Leaderboard\n new MMTutStep {\n Title = \"Compete Globally \ud83c\udfc5\",\n Text = \"The leaderboard tracks your best score, chain reactions, and pet level. How far can you go?\",\n Trigger = MMTutTrigger.Any,\n HX = 75f, HY = 400f, HW = 10f, HH = 10f,\n BR = 5f, BY = 160f\n },\n\n // Step 9 \u2014 Done\n new MMTutStep {\n Title = \"You're ready. \ud83d\udca5\",\n Text = \"Play runs \u2192 earn fragments \u2192 unlock things \u2192 become stronger. Good luck!\",\n Trigger = MMTutTrigger.Done,\n\t\t\tBR = 20f, BY = 370f\n },\n };\n\n // \u2500\u2500 Lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n protected override void OnStart()\n {\n Instance = this;\n _active = false;\n }\n\n public void Activate(PlayerSave save)\n {\n //var save = ChainReactionGame.Save ?? PlayerSave.Load();\n if (save?.MenuTutorialCompleted == true) return;\n\n _step = save?.MenuTutorialStep ?? 0;\n _active = true;\n ResetDebug();\n\n\t\t// If resuming at step 4 (Buy Heart) but heart already purchased, skip it\n\t\tif (_step == 4 && (save?.GetConsumableCount(ConsumableId.Heart) ?? 0) > 0)\n\t\t\t_step = 5;\n\n // Step 3 requires shop/mods to be visible \u2014 navigate there\n ApplySideEffect(_step);\n StateHasChanged();\n }\n\n // \u2500\u2500 Called from MainMenu when Heart is bought \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n public void OnHeartPurchased()\n {\n if (!_active || _step >= _steps.Length) return;\n if (_steps[_step].Trigger != MMTutTrigger.HeartPurchased) return;\n NextStep();\n }\n\n // \u2500\u2500 Step advancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n \tvoid NextStep() \n\t{\n\t\t_step++;\n\t\tResetDebug();\n\n\t\tif (_step >= _steps.Length)\n\t\t{\n\t\t\tComplete();\n\t\t\treturn;\n\t\t}\n\n\t\t// Persist current step so we can resume after crash - save tuto step\n\t\tvar save = PlayerSave.Load();\n\t\tif (save != null) \n\t\t{ \n\t\t\tsave.MenuTutorialStep = _step; \n\t\t\tsave.Save(); \n\t\t}\n\n\t\tApplySideEffect(_step);\n\t\tSound.Play(\"UI_Button\");\n\t\tStateHasChanged();\n}\n\n // \u2500\u2500 Side effects \u2014 navigate tabs automatically \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 //\n void ApplySideEffect(int step)\n {\n var mm = MainMenu.Instance;\n if (mm == null) return;\n\n switch(step)\n {\n case 2: // Show shop modifiers\n mm.SetTabPublic(\"shop\");\n mm.SetShopTabPublic(\"mods\");\n // Grant frags on step before heart purchase\n break;\n\n case 3: // Show consumables\n mm.SetTabPublic(\"shop\");\n mm.SetShopTabPublic(\"consumables\");\n break;\n\n\t\t case 4: // Buy Heart \u2014 shop consumables + grant frags if not already granted\n var save = ChainReactionGame.Save ?? PlayerSave.Load();\n if (save != null && !save.TutorialHeartFragsGranted)\n {\n save.TotalFragments += 100;\n save.TutorialHeartFragsGranted = true;\n save.Save();\n }\n mm.SetTabPublic(\"shop\");\n mm.SetShopTabPublic(\"consumables\");\n mm.ReloadSave();\n break;\n\t\t\n case 5: // Pet tab\n\t\t\t mm.SetTabPublic(\"shop\");\n mm.SetShopTabPublic(\"pets\");\n break;\n\n case 6: // Bombs\n mm.SetTabPublic(\"shop\");\n mm.SetShopTabPublic(\"bombs\");\n break;\n\n case 7: // Leaderboard\n mm.SetTabPublic(\"lb\");\n break;\n\t\t\tcase 8: // Done \u2014 no navigation needed\n break;\n }\n }\n\n void Skip() => Complete();\n\n void OnPlayAndComplete()\n {\n Complete();\n MainMenu.Instance?.StartGamePublic();\n }\n\n void Complete()\n {\n _active = false;\n\n\t\t// Get save from MainMenu directly\n\t\tvar mm = MainMenu.Instance;\n\n\t\t// just use ChainReactionGame.Save or load fresh\n\t\tvar save = PlayerSave.Load();\n\n\t\tif (save != null)\n\t\t{\n\t\t\tsave.MenuTutorialCompleted = true;\n\t\t\tsave.MenuTutorialStep = 0;\n\t\t\tsave.Save();\n\t\t}\n\t\tStateHasChanged();\n }\n\n void ResetDebug()\n {\n _debugBL = 0f; _debugBR = 0f; _debugBY = 0f;\n _debugHX = 0f; _debugHY = 0f; _debugHW = 0f; _debugHH = 0f;\n }\n\n\tpublic void ForceReset()\n\t{\n\t\t_active = false;\n\t\t_step = 0;\n\t\tResetDebug();\n\t\tStateHasChanged();\n\t}\n\n protected override int BuildHash() =>\n System.HashCode.Combine(_active, _step, _debugBL, _debugBY, _debugHX, _debugHY, _debugHW, _debugHH);\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "Game/Idle/UI/IdleHud.razor",
"FileName": "IdleHud.razor",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "@using Sandbox;\n@using Sandbox.UI;\n@using System;\n@using System.Linq;\n@using System.Collections.Generic;\n@inherits PanelComponent\n@namespace Sandbox\n\n@*\n\tIMPORTANT \u2014 s&box text rendering quirk.\n\n\tRazor compiles a literal containing non-ASCII characters (emoji, \u00d7, \u00b7, \u2014)\n\tinto AddMarkupContent, and plain-ASCII literals into AddContent. The panel\n\trenderer hoists every AddMarkupContent node ahead of every AddContent node,\n\tso \"@A \u00d7 @B\" comes out as \"\u00d7AB\".\n\n\tThe rule this file follows: an element holds EITHER pure literal text OR a\n\tsingle @expression. Anything composed goes through a helper below.\n*@\n\n<root>\n@if ( Visible )\n{\n\t<div class=\"idle-root\">\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 AMBIENT BACKDROP \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t<div class=\"idle-bg\"></div>\n\t\t<div class=\"idle-bg-glow @GlowClass()\"></div>\n\n\t\t@for ( int i = 0; i < 12; i++ )\n\t\t{\n\t\t\tfloat pLeft = (i * 8.3f) % 100f;\n\t\t\tfloat pDur = 10f + (i * 0.9f) % 8f;\n\t\t\tfloat pDel = (i * 1.4f) % 12f;\n\t\t\tint pSz = (i % 4 == 0) ? 4 : 2;\n\t\t\tstring pCol = (i % 3 == 0) ? \"rgba(94,234,212,0.45)\" : \"rgba(96,165,250,0.30)\";\n\t\t\t<div class=\"idle-particle\"\n\t\t\t\t style=\"left:@pLeft.ToString(\"F1\")%;width:@(pSz)px;height:@(pSz)px;background-color:@pCol;animation-duration:@pDur.ToString(\"F1\")s;animation-delay:@pDel.ToString(\"F1\")s;\"></div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 TOP BAR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t<div class=\"idle-top @TutDim(\"top\")\">\n\t\t\t<button class=\"idle-back\" onclick=@OnBack>\u2302</button>\n\t\t\t<button class=\"idle-help @HelpClass()\" onclick=@OnStartTutorial>?</button>\n\n\t\t\t<div class=\"idle-brand\">\n\t\t\t\t<div class=\"idle-brand-title\">THE REACTOR</div>\n\t\t\t\t<div class=\"idle-brand-sub\">chain reactions, forever</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"idle-currencies\">\n\t\t\t\t<div class=\"idle-cur cores\">\n\t\t\t\t\t<span class=\"idle-cur-icon\">\u269b\ufe0f</span>\n\t\t\t\t\t<span class=\"idle-cur-val\">@CoresLabel()</span>\n\t\t\t\t\t<span class=\"idle-cur-lbl\">Cores</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"idle-cur rate\">\n\t\t\t\t\t<span class=\"idle-cur-icon\">\ud83d\udcc8</span>\n\t\t\t\t\t<span class=\"idle-cur-val\">@RateLabel()</span>\n\t\t\t\t\t<span class=\"idle-cur-lbl\">per sec</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"idle-cur flux\">\n\t\t\t\t\t<span class=\"idle-cur-icon\">\ud83d\udd37</span>\n\t\t\t\t\t<span class=\"idle-cur-val\">@FluxLabel()</span>\n\t\t\t\t\t<span class=\"idle-cur-lbl\">Flux</span>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"idle-cur frag\">\n\t\t\t\t\t<span class=\"idle-cur-icon\">\ud83d\udca0</span>\n\t\t\t\t\t<span class=\"idle-cur-val\">@FragLabel()</span>\n\t\t\t\t\t<span class=\"idle-cur-lbl\">Frags</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 BODY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t@* Hidden behind a modal \u2014 the panel renderer draws emoji glyphs above\n\t\t overlay panels, so the board has to actually leave the tree. *@\n\t\t@if ( !ModalOpen )\n\t\t{\n\t\t<div class=\"idle-body\">\n\n\t\t\t@* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 LEFT COLUMN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *@\n\t\t\t<div class=\"idle-col idle-left @TutDim(\"left\")\">\n\n\t\t\t\t@* \u2500\u2500 Daily contracts \u2500\u2500\n\t\t\t\t First card in the column on purpose: it is the only part of\n\t\t\t\t the mode with a deadline, so it has to be the first thing a\n\t\t\t\t returning player's eye lands on. *@\n\t\t\t\t<div class=\"idle-card idle-card-contracts @ContractsCardClass()\">\n\t\t\t\t\t<div class=\"idle-card-hd row\">\n\t\t\t\t\t\t<span class=\"idle-card-hd-txt\">DAILY CONTRACTS</span>\n\t\t\t\t\t\t<span class=\"idle-card-hd-side\">@ContractResetLabel()</span>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t@for ( int ci = 0; ci < IdleContracts.DailyCount; ci++ )\n\t\t\t\t\t{\n\t\t\t\t\t\tint slot = ci;\n\t\t\t\t\t\tvar contract = IdleContracts.At( S, slot );\n\t\t\t\t\t\tif ( contract != null )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"idle-contract @ContractClass( slot )\" onclick=@(() => OnClaimContract( slot ))>\n\t\t\t\t\t\t\t\t<span class=\"idle-contract-icon\">@ContractIcon( slot )</span>\n\t\t\t\t\t\t\t\t<div class=\"idle-contract-body\">\n\t\t\t\t\t\t\t\t\t<div class=\"idle-contract-top\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-contract-name\">@contract.Name</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-contract-reward\">@ContractRewardLabel( slot )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-contract-desc\">@ContractDescLabel( slot )</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-contract-track\">\n\t\t\t\t\t\t\t\t\t\t<div class=\"idle-contract-fill\" style=\"width:@Pct( IdleContracts.Fraction( S, slot ) )%;\"></div>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-contract-prog\">@ContractProgLabel( slot )</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t<div class=\"idle-card-note\">@ContractFootLabel()</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-card idle-card-output\">\n\t\t\t\t\t<div class=\"idle-card-hd\">OUTPUT</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Cores / sec</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val hot\">@RateLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Global multiplier</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val\">@GlobalMultLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Chain exponent</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val\">@ExponentLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Last chain</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val\">@LastChainLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Best chain</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val\">@BestChainLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Board</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val\">@BoardLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Feed line</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val @FeedClass()\">@FeedLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t@* \u2500\u2500 Next goal \u2500\u2500\n\t\t\t\t The Reactor Log is four clicks away in the right panel, which\n\t\t\t\t means the long-tail goals are invisible exactly when they are\n\t\t\t\t supposed to be pulling. This pins the closest one. *@\n\t\t\t\t@if ( NextGoal != null )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"idle-card idle-card-goal\">\n\t\t\t\t\t\t<div class=\"idle-card-hd\">NEXT GOAL</div>\n\t\t\t\t\t\t<div class=\"idle-goal-row\">\n\t\t\t\t\t\t\t<span class=\"idle-goal-icon\">@NextGoal.Icon</span>\n\t\t\t\t\t\t\t<div class=\"idle-goal-body\">\n\t\t\t\t\t\t\t\t<div class=\"idle-goal-name\">@NextGoal.Name</div>\n\t\t\t\t\t\t\t\t<div class=\"idle-goal-desc\">@NextGoal.Desc</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"idle-bar-track\">\n\t\t\t\t\t\t\t<div class=\"idle-bar-fill goal\" style=\"width:@Pct( NextGoal.Fraction( S ) )%;\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"idle-goal-foot\">\n\t\t\t\t\t\t\t<span class=\"idle-goal-prog\">@NextGoalProgLabel()</span>\n\t\t\t\t\t\t\t<span class=\"idle-goal-reward\">@NextGoalRewardLabel()</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@if ( ShowBoosts )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"idle-card idle-card-active\">\n\t\t\t\t\t\t<div class=\"idle-card-hd\">ACTIVE</div>\n\t\t\t\t\t\t@if ( R.SurgeActive )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"idle-boost surge\">\n\t\t\t\t\t\t\t\t<span class=\"idle-boost-icon\">\u26a1</span>\n\t\t\t\t\t\t\t\t<span class=\"idle-boost-lbl\">@SurgeBoostLabel()</span>\n\t\t\t\t\t\t\t\t<span class=\"idle-boost-time\">@SecondsLabel( R.SurgeTimeLeft )</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t\t@foreach ( var boost in R.Boosts )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar b = boost;\n\t\t\t\t\t\t\t<div class=\"idle-boost\">\n\t\t\t\t\t\t\t\t<span class=\"idle-boost-icon\">@b.Icon</span>\n\t\t\t\t\t\t\t\t<span class=\"idle-boost-lbl\">@BoostLabel( b )</span>\n\t\t\t\t\t\t\t\t<span class=\"idle-boost-time\">@SecondsLabel( b.SecondsLeft )</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t<div class=\"idle-card idle-card-condenser\">\n\t\t\t\t\t<div class=\"idle-card-hd\">CONDENSER</div>\n\t\t\t\t\t<div class=\"idle-bar-track\">\n\t\t\t\t\t\t<div class=\"idle-bar-fill frag\" style=\"width:@Pct( R.FragmentProgressFraction )%;\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-card-note\">@CondenserLabel()</div>\n\t\t\t\t\t<div class=\"idle-stat-row\">\n\t\t\t\t\t\t<span class=\"idle-stat-lbl\">Today</span>\n\t\t\t\t\t\t<span class=\"idle-stat-val @CapClass()\">@FragTodayLabel()</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-card\">\n\t\t\t\t\t<div class=\"idle-card-hd\">MANIFEST</div>\n\t\t\t\t\t@for ( int ti = 0; ti < IdleBalance.Tiers.Count; ti++ )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar tier = IdleBalance.Tiers[ti];\n\t\t\t\t\t\tbool got = R.TierUnlocked( ti );\n\t\t\t\t\t\t<div class=\"idle-tier @(got ? \"on\" : \"\")\">\n\t\t\t\t\t\t\t<span class=\"idle-tier-icon\">@tier.Icon</span>\n\t\t\t\t\t\t\t<div class=\"idle-tier-body\">\n\t\t\t\t\t\t\t\t<div class=\"idle-tier-name\">@tier.Name</div>\n\t\t\t\t\t\t\t\t<div class=\"idle-tier-blurb\">@TierBlurb( ti, got )</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t@* Rank card \u2014 neighbours, not a top-N. See RebuildRankRows(). *@\n\t\t\t\t<div class=\"idle-card\">\n\t\t\t\t\t<div class=\"idle-card-hd\">REACTOR RANK</div>\n\t\t\t\t\t@if ( _rankRows.Count == 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"idle-card-note\">@RankEmptyLabel()</div>\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\t@foreach ( var row in _rankRows )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar rw = row;\n\t\t\t\t\t\t\t<div class=\"idle-rank-row @RankRowClass( rw )\">\n\t\t\t\t\t\t\t\t<span class=\"idle-rank-pos\">@rw.RankLabel</span>\n\t\t\t\t\t\t\t\t<span class=\"idle-rank-name\">@rw.Name</span>\n\t\t\t\t\t\t\t\t<span class=\"idle-rank-val\">@rw.ValueLabel</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t\t<div class=\"idle-card-note\">@GapLabel()</div>\n\t\t\t\t\t\t@if ( _rankClimb > 0 )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"idle-card-note up\">@ClimbLabel()</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t</div>\n\n\t\t\t@* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 CENTER COLUMN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *@\n\t\t\t<div class=\"idle-col idle-center\">\n\n\t\t\t\t<div class=\"idle-board-wrap @TutDim(\"board\")\">\n\t\t\t\t\t<div class=\"idle-board @SurgeClass() @CascadeClass() @BoardShake()\">\n\t\t\t\t\t\t@for ( int r = 0; r < R.Size; r++ )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"idle-row\">\n\t\t\t\t\t\t\t\t@for ( int c = 0; c < R.Size; c++ )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tint row = r;\n\t\t\t\t\t\t\t\t\tint col = c;\n\t\t\t\t\t\t\t\t\tvar cell = R.Board[row, col];\n\t\t\t\t\t\t\t\t\tbool golden = R.GoldenActive && R.GoldenRow == row && R.GoldenCol == col;\n\n\t\t\t\t\t\t\t\t\t<div class=\"idle-cell @CellClass( cell, golden )\"\n\t\t\t\t\t\t\t\t\t\t style=\"width:@(CellPx)px;height:@(CellPx)px;\"\n\t\t\t\t\t\t\t\t\t\t onclick=@(() => OnCellClick( row, col ))>\n\n\t\t\t\t\t\t\t\t\t\t@if ( golden )\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"idle-golden\">\ud83c\udf1f</div>\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"idle-golden-timer\">@GoldenTimerLabel()</div>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t@if ( !golden && cell.HasChest )\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"idle-chest\" style=\"@ChestStyle( cell )\">@ChestGlyph( cell )</div>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t@if ( !golden && cell.HasChest && cell.MaxHits > 1 )\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"idle-hits\">@cell.HitsLeft</div>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t@if ( cell.HasDrone )\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"idle-drone\" style=\"@DroneStyle( cell )\">\ud83d\udca3</div>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\n\t\t\t\t\t@foreach ( var floater in _floaters )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar f = floater;\n\t\t\t\t\t\t<div class=\"idle-float @f.Cls\"\n\t\t\t\t\t\t\t style=\"left:@f.X.ToString(\"F1\")%;top:@f.Y.ToString(\"F1\")%;opacity:@f.Alpha.ToString(\"F2\");transform:translateY(@f.Rise.ToString(\"F0\")px) scale(@f.Scale.ToString(\"F2\"));\">\n\t\t\t\t\t\t\[email protected]\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\n\t\t\t\t\t@if ( _calloutLife > 0f )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"idle-callout @_calloutCls\" style=\"opacity:@_calloutLife.ToString(\"F2\");\">@_calloutText</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-controls @TutDim(\"controls\")\">\n\n\t\t\t\t\t<button class=\"idle-overload @OverloadClass()\" onclick=@OnOverload>\n\t\t\t\t\t\t<div class=\"idle-overload-fill\" style=\"width:@OverloadFillPct()%;\"></div>\n\t\t\t\t\t\t<span class=\"idle-overload-label\">@OverloadLabel()</span>\n\t\t\t\t\t\t<span class=\"idle-overload-sub\">@OverloadSubLabel()</span>\n\t\t\t\t\t</button>\n\n\t\t\t\t\t<div class=\"idle-surge-wrap\">\n\t\t\t\t\t\t<div class=\"idle-surge-track @SurgeReadyClass()\">\n\t\t\t\t\t\t\t<div class=\"idle-surge-fill\" style=\"width:@Pct( R.Charge )%;\"></div>\n\t\t\t\t\t\t\t<span class=\"idle-surge-lbl\">@SurgeLabel()</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t@if ( R.SurgeReady )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<button class=\"idle-surge-btn\" onclick=@OnSurge>@UnleashLabel()</button>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-meltdown-strip @MeltdownStripClass() @TutDim(\"meltdown\")\">\n\t\t\t\t\t<span class=\"idle-melt-icon\">\u2622\ufe0f</span>\n\t\t\t\t\t<span class=\"idle-melt-text\">@MeltdownLabel()</span>\n\t\t\t\t\t@if ( R.CanMeltdown )\n\t\t\t\t\t{\n\t\t\t\t\t\t<button class=\"idle-melt-btn\" onclick=@OnOpenMeltdown>REVIEW</button>\n\t\t\t\t\t}\n\t\t\t\t\t@if ( !R.CanMeltdown )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"idle-melt-bar\">\n\t\t\t\t\t\t\t<div class=\"idle-melt-fill\" style=\"width:@Pct( R.MeltdownProgress )%;\"></div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t@* Only appears once the perk tree is most of the way bought \u2014 the\n\t\t\t\t layer is meaningless before that and just clutters the column. *@\n\t\t\t\t@if ( R.SingularityVisible )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"idle-sing-strip @SingStripClass() @TutDim(\"meltdown\")\">\n\t\t\t\t\t\t<span class=\"idle-melt-icon\">\u2734\ufe0f</span>\n\t\t\t\t\t\t<span class=\"idle-melt-text\">@SingularityLabel()</span>\n\t\t\t\t\t\t@if ( R.CanSingularity )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<button class=\"idle-sing-btn\" onclick=@OnOpenSingularity>COLLAPSE</button>\n\t\t\t\t\t\t}\n\t\t\t\t\t\t@if ( !R.CanSingularity )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"idle-melt-bar\">\n\t\t\t\t\t\t\t\t<div class=\"idle-sing-fill\" style=\"width:@Pct( R.SingularityProgress )%;\"></div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t</div>\n\n\t\t\t@* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 RIGHT COLUMN \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *@\n\t\t\t<div class=\"idle-col idle-right @TutDim(\"right\")\">\n\n\t\t\t\t<div class=\"idle-tabs\">\n\t\t\t\t\t<button class=\"idle-tab @TabClass(\"reactor\")\" onclick=@(() => SetTab(\"reactor\"))>\u2699\ufe0f</button>\n\t\t\t\t\t<button class=\"idle-tab @TabClass(\"chains\")\" onclick=@(() => SetTab(\"chains\"))>\u26d3\ufe0f</button>\n\t\t\t\t\t<button class=\"idle-tab @TabClass(\"systems\")\" onclick=@(() => SetTab(\"systems\"))>\ud83d\udee0\ufe0f</button>\n\t\t\t\t\t<button class=\"idle-tab fluxtab @TabClass(\"flux\")\" onclick=@(() => SetTab(\"flux\"))>\ud83d\udd37</button>\n\t\t\t\t\t@if ( R.SingularityVisible )\n\t\t\t\t\t{\n\t\t\t\t\t\t<button class=\"idle-tab singtab @TabClass(\"powers\")\" onclick=@(() => SetTab(\"powers\"))>\u2734\ufe0f</button>\n\t\t\t\t\t}\n\t\t\t\t\t<button class=\"idle-tab @TabClass(\"log\")\" onclick=@(() => SetTab(\"log\"))>\ud83d\udcdc</button>\n\t\t\t\t</div>\n\n\t\t\t\t@if ( IsUpgradeTab )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"idle-buyrow\">\n\t\t\t\t\t\t<span class=\"idle-buyrow-lbl\">BUY</span>\n\t\t\t\t\t\t@foreach ( var amount in BuyAmounts )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tint amt = amount;\n\t\t\t\t\t\t\t<button class=\"idle-buy-amt @BuyAmtClass( amt )\" onclick=@(() => SetBuyAmount( amt ))>@BuyAmountLabel( amt )</button>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t<div class=\"idle-panel\">\n\n\t\t\t\t\t@if ( IsUpgradeTab )\n\t\t\t\t\t{\n\t\t\t\t\t\t@foreach ( var upgrade in IdleUpgradeCatalog.InCategory( CurrentCategory ) )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar up = upgrade;\n\t\t\t\t\t\t\tbool revealed = R.IsRevealed( up );\n\t\t\t\t\t\t\tint lvl = S.Level( up.Id );\n\t\t\t\t\t\t\tbool maxed = lvl >= up.MaxLevel;\n\t\t\t\t\t\t\tint count = revealed ? BuyCount( up.Id ) : 0;\n\t\t\t\t\t\t\tdouble cost = (!revealed || maxed) ? 0 : R.CostFor( up.Id, count );\n\t\t\t\t\t\t\tbool afford = revealed && !maxed && count > 0 && Cores >= cost;\n\n\t\t\t\t\t\t\t<div class=\"idle-up @UpgradeClass( revealed, maxed, afford )\" onclick=@(() => OnBuy( up.Id ))>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-icon\">@UpgradeIcon( up, revealed )</div>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-body\">\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-top\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-name\">@UpgradeName( up, revealed )</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-lvl\">@LevelLabel( up, lvl, revealed )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-desc\">@UpgradeDesc( up, revealed )</div>\n\t\t\t\t\t\t\t\t\t@if ( revealed )\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t<div class=\"idle-up-bottom\">\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-effect\">@up.Effect( lvl )</span>\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-cost @MaxedClass( maxed )\">@CostLabel( maxed, cost, count )</span>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t@if ( _tab == \"flux\" )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"idle-flux-head\">\n\t\t\t\t\t\t\t<div class=\"idle-flux-amount\">@FluxHeadLabel()</div>\n\t\t\t\t\t\t\t<div class=\"idle-flux-sub\">@FluxSubLabel()</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t@foreach ( var perk in VisiblePerks() )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar p = perk;\n\t\t\t\t\t\t\tint lvl = S.PerkLevel( p.Id );\n\t\t\t\t\t\t\tbool maxed = lvl >= p.MaxLevel;\n\t\t\t\t\t\t\tint cost = p.CostAt( lvl );\n\t\t\t\t\t\t\tbool afford = !maxed && S.Flux >= cost;\n\n\t\t\t\t\t\t\t<div class=\"idle-up perk @UpgradeClass( true, maxed, afford )\" onclick=@(() => OnBuyPerk( p.Id ))>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-icon\">@p.Icon</div>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-body\">\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-top\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-name\">@p.Name</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-lvl\">@PerkLevelLabel( p, lvl )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-desc\">@p.Desc</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-bottom\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-effect\">@p.Effect( lvl )</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-cost flux @MaxedClass( maxed )\">@PerkCostLabel( maxed, cost )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t@if ( _tab == \"powers\" )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"idle-flux-head sing\">\n\t\t\t\t\t\t\t<div class=\"idle-flux-amount\">@ShardHeadLabel()</div>\n\t\t\t\t\t\t\t<div class=\"idle-flux-sub\">@ShardSubLabel()</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t@foreach ( var power in IdleUpgradeCatalog.Powers )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar pw = power;\n\t\t\t\t\t\t\tint lvl = S.PowerLevel( pw.Id );\n\t\t\t\t\t\t\tbool maxed = lvl >= pw.MaxLevel;\n\t\t\t\t\t\t\tint cost = pw.CostAt( lvl );\n\t\t\t\t\t\t\tbool afford = !maxed && S.Shards >= cost;\n\n\t\t\t\t\t\t\t<div class=\"idle-up power @UpgradeClass( true, maxed, afford )\" onclick=@(() => OnBuyPower( pw.Id ))>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-icon\">@pw.Icon</div>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-body\">\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-top\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-name\">@pw.Name</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-lvl\">@PowerLevelLabel( pw, lvl )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-desc\">@pw.Desc</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-bottom\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-effect\">@pw.Effect( lvl )</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-cost shard @MaxedClass( maxed )\">@PowerCostLabel( maxed, cost )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t@if ( _tab == \"log\" )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"idle-flux-head\">\n\t\t\t\t\t\t\t<div class=\"idle-flux-amount\">@LogHeadLabel()</div>\n\t\t\t\t\t\t\t<div class=\"idle-flux-sub\">Each one pays out once.</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t@foreach ( var achievement in IdleAchievements.All )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar a = achievement;\n\t\t\t\t\t\t\tbool done = S.HasAchievement( a.Id );\n\n\t\t\t\t\t\t\t<div class=\"idle-up ach @(done ? \"done\" : \"\")\">\n\t\t\t\t\t\t\t\t<div class=\"idle-up-icon\">@AchIcon( a, done )</div>\n\t\t\t\t\t\t\t\t<div class=\"idle-up-body\">\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-top\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-up-name\">@a.Name</span>\n\t\t\t\t\t\t\t\t\t\t<span class=\"idle-ach-reward\">@RewardLabel( a )</span>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div class=\"idle-up-desc\">@a.Desc</div>\n\t\t\t\t\t\t\t\t\t@if ( !done )\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t<div class=\"idle-ach-bar\">\n\t\t\t\t\t\t\t\t\t\t\t<div class=\"idle-ach-fill\" style=\"width:@Pct( a.Fraction( S ) )%;\"></div>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t<div class=\"idle-ach-prog\">@ProgressLabel( a )</div>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 TOASTS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t<div class=\"idle-toasts\">\n\t\t\t@foreach ( var toast in _toasts )\n\t\t\t{\n\t\t\t\tvar t = toast;\n\t\t\t\t<div class=\"idle-toast @t.Cls\" style=\"opacity:@t.Alpha.ToString(\"F2\");\">\n\t\t\t\t\t<span class=\"idle-toast-icon\">@t.Icon</span>\n\t\t\t\t\t<span class=\"idle-toast-text\">@t.Text</span>\n\t\t\t\t</div>\n\t\t\t}\n\t\t</div>\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 WELCOME BACK OVERLAY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t@if ( R.OfflinePending )\n\t\t{\n\t\t\t<div class=\"idle-modal-bg\"></div>\n\t\t\t<div class=\"idle-modal offline\">\n\t\t\t\t<div class=\"idle-modal-eyebrow\">REACTOR REPORT</div>\n\t\t\t\t<div class=\"idle-modal-title\">Welcome back</div>\n\t\t\t\t<div class=\"idle-modal-sub\">@OfflineSubLabel()</div>\n\n\t\t\t\t<div class=\"idle-offline-haul\">\n\t\t\t\t\t<div class=\"idle-offline-amount\">@OfflineAmountLabel()</div>\n\t\t\t\t\t<div class=\"idle-offline-lbl\">CORES</div>\n\t\t\t\t</div>\n\n\t\t\t\t@if ( R.PendingOfflineFrags > 0 )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"idle-offline-extra\">@OfflineFragLabel()</div>\n\t\t\t\t}\n\n\t\t\t\t@if ( OfflineTankFull )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"idle-offline-warn\">@OfflineWarnLabel()</div>\n\t\t\t\t}\n\n\t\t\t\t<button class=\"idle-modal-btn\" onclick=@OnClaimOffline>COLLECT</button>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 DAILY STREAK OVERLAY \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t@if ( !R.OfflinePending && !_showIntro && R.DailyPending )\n\t\t{\n\t\t\t<div class=\"idle-modal-bg\"></div>\n\t\t\t<div class=\"idle-modal daily\">\n\t\t\t\t<div class=\"idle-modal-eyebrow\">DAILY CYCLE</div>\n\t\t\t\t<div class=\"idle-modal-title\">@DailyTitleLabel()</div>\n\t\t\t\t<div class=\"idle-modal-sub\">Come back tomorrow to keep the streak climbing. Day 7 pays eight times.</div>\n\n\t\t\t\t<div class=\"idle-streak-row\">\n\t\t\t\t\t@for ( int d = 1; d <= 7; d++ )\n\t\t\t\t\t{\n\t\t\t\t\t\tint day = d;\n\t\t\t\t\t\t<div class=\"idle-streak-pip @StreakPipClass( day )\">@day</div>\n\t\t\t\t\t}\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-daily-reward\">\n\t\t\t\t\t<div class=\"idle-daily-mult\">@DailyMultLabel()</div>\n\t\t\t\t\t<div class=\"idle-daily-lbl\">@DailyRewardLabel()</div>\n\t\t\t\t</div>\n\n\t\t\t\t<button class=\"idle-modal-btn\" onclick=@OnClaimDaily>CLAIM</button>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MELTDOWN MODAL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t@if ( !R.OfflinePending && !_showIntro && !R.DailyPending && _showMeltdown )\n\t\t{\n\t\t\t<div class=\"idle-modal-bg\"></div>\n\t\t\t<div class=\"idle-modal meltdown\">\n\t\t\t\t<div class=\"idle-modal-eyebrow\">PRESTIGE</div>\n\t\t\t\t<div class=\"idle-modal-title\">MELTDOWN</div>\n\t\t\t\t<div class=\"idle-modal-sub\">Collapse this cycle into permanent Flux. Every Flux you have ever earned adds income forever, and Flux buys perks that survive every reset.</div>\n\n\t\t\t\t<div class=\"idle-melt-payout\">\n\t\t\t\t\t<div class=\"idle-melt-payout-val\">@MeltPayoutLabel()</div>\n\t\t\t\t\t<div class=\"idle-melt-payout-lbl\">FLUX</div>\n\t\t\t\t</div>\n\n\t\t\t\t@* The Flux number alone does not answer the only question that\n\t\t\t\t matters at this button \u2014 \"am I actually going to be faster\n\t\t\t\t afterwards\". This does. *@\n\t\t\t\t<div class=\"idle-melt-gain\">\n\t\t\t\t\t<span class=\"idle-melt-gain-from\">@MeltMultBeforeLabel()</span>\n\t\t\t\t\t<span class=\"idle-melt-gain-arrow\">\u2192</span>\n\t\t\t\t\t<span class=\"idle-melt-gain-to\">@MeltMultAfterLabel()</span>\n\t\t\t\t\t<span class=\"idle-melt-gain-lbl\">permanent income</span>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-melt-cols\">\n\t\t\t\t\t<div class=\"idle-melt-col keep\">\n\t\t\t\t\t\t<div class=\"idle-melt-col-hd\">KEPT</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Flux and every perk</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Reactor Log entries</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Records and Fragments</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Kickstart head start</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-melt-col lose\">\n\t\t\t\t\t\t<div class=\"idle-melt-col-hd\">BURNED</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">@BurnedCoresLabel()</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">All Core upgrades</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Reactor Frame size</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Unlocked chest tiers</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-modal-btns\">\n\t\t\t\t\t<button class=\"idle-modal-btn ghost\" onclick=@OnCloseMeltdown>Not yet</button>\n\t\t\t\t\t<button class=\"idle-modal-btn danger\" onclick=@OnMeltdown>MELT DOWN</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 SINGULARITY MODAL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t@if ( !R.OfflinePending && !_showIntro && !R.DailyPending && !_showMeltdown && _showSingularity )\n\t\t{\n\t\t\t<div class=\"idle-modal-bg\"></div>\n\t\t\t<div class=\"idle-modal singularity\">\n\t\t\t\t<div class=\"idle-modal-eyebrow\">SECOND PRESTIGE</div>\n\t\t\t\t<div class=\"idle-modal-title\">SINGULARITY</div>\n\t\t\t\t<div class=\"idle-modal-sub\">Collapse your entire Flux career into Shards. Shards buy Powers \u2014 and Powers change how a cycle runs, not just how much it pays.</div>\n\n\t\t\t\t<div class=\"idle-melt-payout\">\n\t\t\t\t\t<div class=\"idle-sing-payout-val\">@SingPayoutLabel()</div>\n\t\t\t\t\t<div class=\"idle-melt-payout-lbl\">SHARDS</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-melt-cols\">\n\t\t\t\t\t<div class=\"idle-melt-col keep\">\n\t\t\t\t\t\t<div class=\"idle-melt-col-hd\">KEPT</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Shards and every Power</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Reactor Log entries</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Records, Fragments, pet</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">@ShardKeepLabel()</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-melt-col lose\">\n\t\t\t\t\t\t<div class=\"idle-melt-col-hd\">COLLAPSED</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">@BurnedFluxLabel()</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">Every Flux perk</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">All Core upgrades</div>\n\t\t\t\t\t\t<div class=\"idle-melt-line\">This cycle's Cores</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-modal-btns\">\n\t\t\t\t\t<button class=\"idle-modal-btn ghost\" onclick=@OnCloseSingularity>Not yet</button>\n\t\t\t\t\t<button class=\"idle-modal-btn sing\" onclick=@OnSingularity>COLLAPSE</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 INTRO MODAL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *@\n\t\t@if ( !R.OfflinePending && _showIntro )\n\t\t{\n\t\t\t<div class=\"idle-modal-bg\"></div>\n\t\t\t<div class=\"idle-modal intro\">\n\t\t\t\t<div class=\"idle-modal-eyebrow\">HOW IT WORKS</div>\n\t\t\t\t<div class=\"idle-modal-title\">THE REACTOR</div>\n\t\t\t\t<div class=\"idle-modal-sub\">A board that plays itself. Chests drop, drones bomb them, chain reactions pay Cores. You never lose here \u2014 you only grow.</div>\n\n\t\t\t\t<div class=\"idle-intro-steps\">\n\t\t\t\t\t<div class=\"idle-intro-step\">\n\t\t\t\t\t\t<span class=\"idle-intro-num\">1</span>\n\t\t\t\t\t\t<span class=\"idle-intro-txt\">Spend Cores on the right. Longer chains are worth far more than richer chests.</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-intro-step\">\n\t\t\t\t\t\t<span class=\"idle-intro-num\">2</span>\n\t\t\t\t\t\t<span class=\"idle-intro-txt\">Hit OVERLOAD, or press SPACE, for a burst. It charges the Surge meter \u2014 ten times output when you unleash it.</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-intro-step\">\n\t\t\t\t\t\t<span class=\"idle-intro-num\">3</span>\n\t\t\t\t\t\t<span class=\"idle-intro-txt\">It keeps running while you are on the menu or another screen. Close the game and it stops.</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"idle-intro-step\">\n\t\t\t\t\t\t<span class=\"idle-intro-num\">4</span>\n\t\t\t\t\t\t<span class=\"idle-intro-txt\">Cores condense into Fragments, which feed your companion for a permanent output bonus. Meltdown turns a whole cycle into Flux.</span>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"idle-modal-btns\">\n\t\t\t\t\t<button class=\"idle-modal-btn ghost\" onclick=@OnCloseIntro>I'll figure it out</button>\n\t\t\t\t\t<button class=\"idle-modal-btn\" onclick=@OnIntroTutorial>SHOW ME HOW</button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\n\t\t@* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 GUIDED WALKTHROUGH \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t\t Coach marks over the live HUD. The board keeps running underneath \u2014\n\t\t an idle game that freezes for its own tutorial teaches the wrong\n\t\t thing about what the game is. Everything here is measured from the\n\t\t real panels at runtime, so it cannot drift out of sync with layout.\n\t\t*@\n\t\t@if ( TutorialActive )\n\t\t{\n\t\t\t<div class=\"rt-layer\">\n\n\t\t\t\t@* Four quads around the spotlight, rather than one dim sheet with\n\t\t\t\t a hole \u2014 the panel renderer has no masking, so this is how you\n\t\t\t\t get a real cut-out that still swallows clicks everywhere else. *@\n\t\t\t\t<div class=\"rt-dim\" style=\"@DimTopStyle()\"></div>\n\t\t\t\t<div class=\"rt-dim\" style=\"@DimBottomStyle()\"></div>\n\t\t\t\t<div class=\"rt-dim\" style=\"@DimLeftStyle()\"></div>\n\t\t\t\t<div class=\"rt-dim\" style=\"@DimRightStyle()\"></div>\n\n\t\t\t\t@if ( TutHasSpot )\n\t\t\t\t{\n\t\t\t\t\t<div class=\"rt-ring\" style=\"@SpotStyle()\"></div>\n\t\t\t\t\t<div class=\"rt-spot\" style=\"@SpotStyle()\">\n\t\t\t\t\t\t<div class=\"rt-corner tl\"></div>\n\t\t\t\t\t\t<div class=\"rt-corner tr\"></div>\n\t\t\t\t\t\t<div class=\"rt-corner bl\"></div>\n\t\t\t\t\t\t<div class=\"rt-corner br\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\n\t\t\t\t@* The class alternates per step purely so the entrance animation\n\t\t\t\t replays \u2014 a stable class string would only ever play once. *@\n\t\t\t\t<div class=\"rt-card @TutCardAnimClass()\" style=\"@CardStyle()\">\n\t\t\t\t\t<div class=\"rt-card-top\">\n\t\t\t\t\t\t<span class=\"rt-eyebrow\">HOW TO PLAY</span>\n\t\t\t\t\t\t<span class=\"rt-count\">@TutCountLabel()</span>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div class=\"rt-title\">@TutTitleLabel()</div>\n\t\t\t\t\t<div class=\"rt-text\">@TutTextLabel()</div>\n\n\t\t\t\t\t<div class=\"rt-dots\">\n\t\t\t\t\t\t@for ( int di = 0; di < IdleTutorial.Count; di++ )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t<div class=\"rt-dot @DotClass( di )\"></div>\n\t\t\t\t\t\t}\n\t\t\t\t\t</div>\n\n\t\t\t\t\t@if ( TutWaitingOnAction )\n\t\t\t\t\t{\n\t\t\t\t\t\t<div class=\"rt-hint\">\n\t\t\t\t\t\t\t<span class=\"rt-hint-pip\">\ud83d\udc46</span>\n\t\t\t\t\t\t\t<span class=\"rt-hint-txt\">@TutHintLabel()</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t}\n\t\t\t\t\t@if ( !TutWaitingOnAction )\n\t\t\t\t\t{\n\t\t\t\t\t\t<button class=\"rt-next\" onclick=@OnTutNext>@TutNextLabel()</button>\n\t\t\t\t\t}\n\n\t\t\t\t\t<div class=\"rt-foot\">\n\t\t\t\t\t\t<button class=\"rt-back @TutBackClass()\" onclick=@OnTutBack>Back</button>\n\t\t\t\t\t\t<button class=\"rt-skip\" onclick=@OnTutSkip>Skip the tour</button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t}\n\t</div>\n}\n</root>\n\n@code {\n\tpublic static IdleHud Instance { get; private set; }\n\n\tIdleReactor R => IdleReactor.Instance;\n\tChainReactionGame Game => ChainReactionGame.Instance;\n\tIdleSave S => ChainReactionGame.Save?.Idle;\n\n\tbool Visible => Game?.GameStarted == true\n\t\t\t\t && Game?.Mode == GameMode.Idle\n\t\t\t\t && R != null && S != null;\n\n\tdouble Cores => S?.Cores ?? 0;\n\n\tbool ModalOpen => R.OfflinePending || R.DailyPending || _showMeltdown || _showSingularity || _showIntro;\n\tbool ShowBoosts => R.Boosts.Count > 0 || R.SurgeActive;\n\tbool OfflineTankFull => R.PendingOfflineSeconds > R.OfflineCapHours * 3600f;\n\n\t// \u2500\u2500 UI state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tstring _tab = \"reactor\";\n\tint _buyAmount = 1; // -1 == MAX\n\tbool _showMeltdown;\n\tbool _showSingularity;\n\tbool _showIntro;\n\tbool _subscribed;\n\tbool _wasVisible;\n\n\tstatic readonly int[] BuyAmounts = { 1, 10, 25, -1 };\n\n\tbool IsUpgradeTab => _tab == \"reactor\" || _tab == \"chains\" || _tab == \"systems\";\n\n\tIdleUpgradeCategory CurrentCategory => _tab switch\n\t{\n\t\t\"reactor\" => IdleUpgradeCategory.Reactor,\n\t\t\"chains\" => IdleUpgradeCategory.Chains,\n\t\t_ => IdleUpgradeCategory.Systems,\n\t};\n\n\t// \u2500\u2500 Floating numbers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tclass Floater\n\t{\n\t\tpublic string Text;\n\t\tpublic string Cls;\n\t\tpublic float X, Y;\n\t\tpublic float Life, MaxLife;\n\t\tpublic float Alpha => MathF.Min( 1f, Life / 0.35f );\n\t\tpublic float Rise => -(MaxLife - Life) * 42f;\n\t\tpublic float Scale => 0.85f + MathF.Min( 0.35f, (MaxLife - Life) * 1.2f );\n\t}\n\treadonly List<Floater> _floaters = new();\n\n\tclass ToastMsg\n\t{\n\t\tpublic string Icon;\n\t\tpublic string Text;\n\t\tpublic string Cls;\n\t\tpublic float Life;\n\t\tpublic float Alpha => MathF.Min( 1f, Life / 0.4f );\n\t}\n\treadonly List<ToastMsg> _toasts = new();\n\n\tstring _calloutText = \"\";\n\tstring _calloutCls = \"\";\n\tfloat _calloutLife;\n\n\t// Board shake is kept local rather than going through ScreenShaker so the\n\t// class string stays stable between rebuilds \u2014 a changing class would\n\t// restart the animation on every repaint and read as a vibration.\n\tstring _shakeCls = \"\";\n\tfloat _shakeUntil;\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Guided walkthrough\n\t//\n\t// Coach marks measured off the real panels every frame, so the spotlight\n\t// cannot drift when the board resizes, the window changes shape, or a card\n\t// appears above the one being pointed at. The previous tutorial in this\n\t// codebase hard-coded pixel rectangles and needed a debug nudge UI to keep\n\t// them honest; this one has nothing to keep honest.\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tbool _tutRunning;\n\tint _tutStep;\n\tfloat _tutStepAt; // Time.Now the step opened, for the action grace period\n\n\t// Spotlight rect and card position, in design pixels relative to the root.\n\tfloat _spotX, _spotY, _spotW, _spotH;\n\tfloat _cardX, _cardY;\n\tfloat _rootW = 1920f, _rootH = 1080f;\n\tbool _spotFound;\n\n\t// Measured on the frame after the card first renders; the fallback only\n\t// ever shows for that single frame.\n\tfloat _cardW = 380f, _cardH = 240f;\n\n\tconst float CardGap = 26f; // spotlight \u2192 card\n\tconst float EdgePad = 20f; // card \u2192 screen edge\n\tconst float ActionGrace = 0.45f; // the click that opened a step cannot also close it\n\n\t/// <summary>\n\t/// Set by the menu's \"How to play\" button. Consumed on the frame the HUD\n\t/// becomes visible, since the tour points at panels that do not exist until\n\t/// then.\n\t/// </summary>\n\tpublic static bool PendingTutorial { get; set; }\n\n\tpublic bool TutorialActive => _tutRunning && Visible && !ModalOpen;\n\n\tbool TutHasSpot => _spotFound && _spotW > 1f && _spotH > 1f;\n\n\tIdleTutStep CurrentTutStep\n\t\t=> _tutStep >= 0 && _tutStep < IdleTutorial.Count ? IdleTutorial.Steps[_tutStep] : null;\n\n\tbool TutWaitingOnAction\n\t\t=> CurrentTutStep != null && CurrentTutStep.Trigger != IdleTutTrigger.Button;\n\n\tpublic void StartTutorial()\n\t{\n\t\t_tutRunning = true;\n\t\t_tutStep = 0;\n\t\t_tutStepAt = Time.Now;\n\t\t_showIntro = false;\n\t\t_spotFound = false;\n\t\tMeasureTutorial();\n\t\tStateHasChanged();\n\t}\n\n\tvoid EndTutorial( bool completed )\n\t{\n\t\t_tutRunning = false;\n\t\tif ( S != null )\n\t\t{\n\t\t\tS.TutorialOffered = true;\n\t\t\tif ( completed ) S.TutorialDone = true;\n\t\t\tChainReactionGame.Save?.Save();\n\t\t}\n\t\tStateHasChanged();\n\t}\n\n\tvoid OnTutNext()\n\t{\n\t\tSound.Play( \"UI_Button\" );\n\t\tAdvanceTutorial();\n\t}\n\n\tvoid OnTutBack()\n\t{\n\t\tif ( _tutStep <= 0 ) return;\n\t\tSound.Play( \"UI_Button\" );\n\t\t_tutStep--;\n\t\t_tutStepAt = Time.Now;\n\t\tMeasureTutorial();\n\t\tStateHasChanged();\n\t}\n\n\tvoid OnTutSkip()\n\t{\n\t\tSound.Play( \"UI_Button\" );\n\t\tEndTutorial( completed: true );\n\t}\n\n\tvoid AdvanceTutorial()\n\t{\n\t\t_tutStep++;\n\t\t_tutStepAt = Time.Now;\n\n\t\tif ( _tutStep >= IdleTutorial.Count )\n\t\t{\n\t\t\tSound.Play( \"PlayButton\" );\n\t\t\tEndTutorial( completed: true );\n\t\t\treturn;\n\t\t}\n\n\t\tMeasureTutorial();\n\t\tStateHasChanged();\n\t}\n\n\t/// <summary>\n\t/// Satisfies a step that is waiting on the player doing something. The grace\n\t/// period stops the very click that advanced onto a step from also\n\t/// advancing off it \u2014 buying an upgrade lands on the \"buy an upgrade\" step\n\t/// otherwise, and the player never reads it.\n\t/// </summary>\n\tvoid TutSatisfy( IdleTutTrigger trigger )\n\t{\n\t\tif ( !_tutRunning ) return;\n\n\t\tvar step = CurrentTutStep;\n\t\tif ( step == null || step.Trigger != trigger ) return;\n\t\tif ( Time.Now - _tutStepAt < ActionGrace ) return;\n\n\t\tSound.Play( \"Notification_1\" );\n\t\tAdvanceTutorial();\n\t}\n\n\t/// <summary>Recursive class lookup rather than Descendants, so the first match\n\t/// found is the shallowest one \u2014 an inner panel sharing a class cannot win.</summary>\n\tstatic Sandbox.UI.Panel FindByClass( Sandbox.UI.Panel from, string cls )\n\t{\n\t\tif ( from == null || string.IsNullOrEmpty( cls ) ) return null;\n\t\tif ( from.HasClass( cls ) ) return from;\n\n\t\tforeach ( var child in from.Children )\n\t\t{\n\t\t\tvar hit = FindByClass( child, cls );\n\t\t\tif ( hit != null ) return hit;\n\t\t}\n\t\treturn null;\n\t}\n\n\tvoid MeasureTutorial()\n\t{\n\t\tvar root = Panel;\n\t\tif ( root == null ) return;\n\n\t\tfloat scale = root.ScaleFromScreen;\n\t\tvar rr = root.Box.Rect;\n\n\t\t_rootW = MathF.Max( 320f, rr.Width * scale );\n\t\t_rootH = MathF.Max( 240f, rr.Height * scale );\n\n\t\tvar step = CurrentTutStep;\n\t\t_spotFound = false;\n\n\t\tif ( step != null && !string.IsNullOrEmpty( step.Target ) )\n\t\t{\n\t\t\tvar target = FindByClass( root, step.Target );\n\t\t\tif ( target != null )\n\t\t\t{\n\t\t\t\tvar tr = target.Box.Rect;\n\t\t\t\tif ( tr.Width > 1f && tr.Height > 1f )\n\t\t\t\t{\n\t\t\t\t\t_spotX = (tr.Left - rr.Left) * scale - step.Pad;\n\t\t\t\t\t_spotY = (tr.Top - rr.Top ) * scale - step.Pad;\n\t\t\t\t\t_spotW = tr.Width * scale + step.Pad * 2f;\n\t\t\t\t\t_spotH = tr.Height * scale + step.Pad * 2f;\n\n\t\t\t\t\t// A column that runs off the bottom of the viewport reports a\n\t\t\t\t\t// rect taller than the screen; clamping keeps the dim quads\n\t\t\t\t\t// from collapsing to nothing.\n\t\t\t\t\t_spotX = Math.Clamp( _spotX, 0f, _rootW - 8f );\n\t\t\t\t\t_spotY = Math.Clamp( _spotY, 0f, _rootH - 8f );\n\t\t\t\t\t_spotW = Math.Clamp( _spotW, 8f, _rootW - _spotX );\n\t\t\t\t\t_spotH = Math.Clamp( _spotH, 8f, _rootH - _spotY );\n\n\t\t\t\t\t_spotFound = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// The card measures itself once it exists; until then the constructor\n\t\t// defaults are close enough that nothing visibly jumps.\n\t\tvar card = FindByClass( root, \"rt-card\" );\n\t\tif ( card != null )\n\t\t{\n\t\t\tvar cr = card.Box.Rect;\n\t\t\tif ( cr.Width > 1f && cr.Height > 1f )\n\t\t\t{\n\t\t\t\t_cardW = cr.Width * scale;\n\t\t\t\t_cardH = cr.Height * scale;\n\t\t\t}\n\t\t}\n\n\t\tPlaceCard( step );\n\t}\n\n\tvoid PlaceCard( IdleTutStep step )\n\t{\n\t\tif ( step == null || !TutHasSpot )\n\t\t{\n\t\t\t_cardX = (_rootW - _cardW) * 0.5f;\n\t\t\t_cardY = (_rootH - _cardH) * 0.5f;\n\t\t\treturn;\n\t\t}\n\n\t\tfloat cx = _spotX + _spotW * 0.5f;\n\t\tfloat cy = _spotY + _spotH * 0.5f;\n\n\t\tvar place = step.Place;\n\n\t\t// Fall back to the other side when the requested one has no room. This\n\t\t// is what makes the script survive an ultrawide or a small window\n\t\t// without every step needing a per-aspect-ratio override.\n\t\tif ( place == IdleTutPlace.Right && _spotX + _spotW + CardGap + _cardW > _rootW - EdgePad )\n\t\t\tplace = IdleTutPlace.Left;\n\t\telse if ( place == IdleTutPlace.Left && _spotX - CardGap - _cardW < EdgePad )\n\t\t\tplace = IdleTutPlace.Right;\n\t\telse if ( place == IdleTutPlace.Above && _spotY - CardGap - _cardH < EdgePad )\n\t\t\tplace = IdleTutPlace.Below;\n\t\telse if ( place == IdleTutPlace.Below && _spotY + _spotH + CardGap + _cardH > _rootH - EdgePad )\n\t\t\tplace = IdleTutPlace.Above;\n\n\t\tswitch ( place )\n\t\t{\n\t\t\tcase IdleTutPlace.Left:\n\t\t\t\t_cardX = _spotX - CardGap - _cardW;\n\t\t\t\t_cardY = cy - _cardH * 0.5f;\n\t\t\t\tbreak;\n\n\t\t\tcase IdleTutPlace.Right:\n\t\t\t\t_cardX = _spotX + _spotW + CardGap;\n\t\t\t\t_cardY = cy - _cardH * 0.5f;\n\t\t\t\tbreak;\n\n\t\t\tcase IdleTutPlace.Above:\n\t\t\t\t_cardX = cx - _cardW * 0.5f;\n\t\t\t\t_cardY = _spotY - CardGap - _cardH;\n\t\t\t\tbreak;\n\n\t\t\tcase IdleTutPlace.Below:\n\t\t\t\t_cardX = cx - _cardW * 0.5f;\n\t\t\t\t_cardY = _spotY + _spotH + CardGap;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t_cardX = (_rootW - _cardW) * 0.5f;\n\t\t\t\t_cardY = (_rootH - _cardH) * 0.5f;\n\t\t\t\tbreak;\n\t\t}\n\n\t\t_cardX = Math.Clamp( _cardX, EdgePad, MathF.Max( EdgePad, _rootW - _cardW - EdgePad ) );\n\t\t_cardY = Math.Clamp( _cardY, EdgePad, MathF.Max( EdgePad, _rootH - _cardH - EdgePad ) );\n\t}\n\n\t// \u2500\u2500 Inline styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t// Ints only. A decimal separator from a French locale would be a comma and\n\t// would silently kill the whole declaration.\n\n\tstatic string Px( float v ) => ((int)MathF.Round( v )).ToString();\n\n\tstring SpotStyle()\n\t\t=> $\"left:{Px( _spotX )}px;top:{Px( _spotY )}px;width:{Px( _spotW )}px;height:{Px( _spotH )}px;\";\n\n\tstring CardStyle() => $\"left:{Px( _cardX )}px;top:{Px( _cardY )}px;\";\n\n\t// With no spotlight the top quad covers everything and the other three\n\t// collapse \u2014 which is exactly the plain dim sheet a centred step wants.\n\tstring DimTopStyle()\n\t\t=> TutHasSpot\n\t\t\t? $\"left:0px;top:0px;width:{Px( _rootW )}px;height:{Px( _spotY )}px;\"\n\t\t\t: $\"left:0px;top:0px;width:{Px( _rootW )}px;height:{Px( _rootH )}px;\";\n\n\tstring DimBottomStyle()\n\t{\n\t\tif ( !TutHasSpot ) return \"width:0px;height:0px;\";\n\t\tfloat top = _spotY + _spotH;\n\t\treturn $\"left:0px;top:{Px( top )}px;width:{Px( _rootW )}px;height:{Px( _rootH - top )}px;\";\n\t}\n\n\tstring DimLeftStyle()\n\t\t=> TutHasSpot\n\t\t\t? $\"left:0px;top:{Px( _spotY )}px;width:{Px( _spotX )}px;height:{Px( _spotH )}px;\"\n\t\t\t: \"width:0px;height:0px;\";\n\n\tstring DimRightStyle()\n\t{\n\t\tif ( !TutHasSpot ) return \"width:0px;height:0px;\";\n\t\tfloat left = _spotX + _spotW;\n\t\treturn $\"left:{Px( left )}px;top:{Px( _spotY )}px;width:{Px( _rootW - left )}px;height:{Px( _spotH )}px;\";\n\t}\n\n\t// \u2500\u2500 Labels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tstring TutTitleLabel() => CurrentTutStep?.Title ?? \"\";\n\tstring TutTextLabel() => CurrentTutStep?.Text ?? \"\";\n\tstring TutHintLabel() => CurrentTutStep?.Hint ?? \"\";\n\tstring TutCountLabel() => $\"{_tutStep + 1} / {IdleTutorial.Count}\";\n\n\tstring TutNextLabel()\n\t\t=> _tutStep >= IdleTutorial.Count - 1 ? \"RUN THE REACTOR\" : \"GOT IT\";\n\n\tstring TutBackClass() => _tutStep <= 0 ? \"hidden\" : \"\";\n\n\tstring TutCardAnimClass() => (_tutStep % 2) == 0 ? \"in-a\" : \"in-b\";\n\n\t/// <summary>\n\t/// Dims a HUD block that is not the subject of the current step.\n\t///\n\t/// The dim quads are click-blockers and a background scrim; they cannot do\n\t/// this job on their own, because this renderer draws emoji above overlay\n\t/// panels and every chest, icon and tab glyph would shine straight through\n\t/// the scrim at full brightness. Ancestor opacity takes the glyphs with it.\n\t/// </summary>\n\tstring TutDim( string region )\n\t{\n\t\tif ( !TutorialActive ) return \"\";\n\n\t\tstring want = CurrentTutStep?.Region ?? \"\";\n\t\tif ( string.IsNullOrEmpty( want ) ) return \"\"; // centred step dims nothing\n\n\t\treturn region == want ? \"\" : \"tut-dim\";\n\t}\n\n\tstring DotClass( int i )\n\t{\n\t\tif ( i == _tutStep ) return \"on\";\n\t\treturn i < _tutStep ? \"done\" : \"\";\n\t}\n\n\tstring HelpClass() => _tutRunning ? \"on\" : \"\";\n\n\tint CellPx => R == null ? 92 : R.Size switch\n\t{\n\t\t5 => 96,\n\t\t6 => 84,\n\t\t7 => 74,\n\t\t8 => 66,\n\t\t_ => 59,\n\t};\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Reactor rank card\n\t//\n\t// Deliberately NOT a top-N board. A top-N motivates whoever is already in\n\t// it and is scenery for everyone else; the crossable goal is the gap to the\n\t// player directly above, so the card shows a window around the local player\n\t// (two up, you, one down).\n\t//\n\t// The board snapshot is stale between refreshes but S.AllTimeCores is live,\n\t// so the local row and the gap are always computed from the local value.\n\t// That makes the gap tick down while the player watches, which is the whole\n\t// point of the card.\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tclass RankRow\n\t{\n\t\tpublic string RankLabel;\n\t\tpublic string Name;\n\t\tpublic string ValueLabel;\n\t\tpublic bool IsMe;\n\t}\n\treadonly List<RankRow> _rankRows = new();\n\n\tSandbox.Services.Leaderboards.Board2 _rankBoard;\n\tbool _rankLoading;\n\tbool _rankFetched;\n\tfloat _rankNextFetch;\n\tint _myRank; // 1-based, 0 == not on the board\n\tdouble _nextUpValue; // stat value of the player directly above, 0 == none above\n\tint _rankClimb; // places gained since the last session\n\tint _rankTotalPlayers; // Board2.TotalEntries \u2014 real population, not window size\n\n\tconst float RankRefreshSeconds = 90f;\n\t// Same stat the main-menu leaderboard ranks on, so the two can never disagree.\n\tstatic string RankStat => CommunityGoal.CoresStat;\n\n\t/// <summary>\n\t/// Network call \u2014 only ever driven by the throttle in OnUpdate, never from\n\t/// the render path. BuildHash changes many times a second, so fetching from\n\t/// there would hammer the backend into rate-limiting us.\n\t/// </summary>\n\tasync System.Threading.Tasks.Task LoadRank()\n\t{\n\t\tif ( _rankLoading ) return;\n\t\t_rankLoading = true;\n\n\t\ttry\n\t\t{\n\t\t\tvar b = Sandbox.Services.Leaderboards.GetFromStat( RankStat );\n\t\t\tb.SetAggregationMax();\n\t\t\tb.SetSortDescending();\n\n\t\t\t// CenterOnMe is exactly what this card wants: Board2 returns the\n\t\t\t// entries surrounding a target player, so asking for a small window\n\t\t\t// centred on us gives the neighbours directly. No manual slicing.\n\t\t\tb.CenterOnMe();\n\t\t\tb.MaxEntries = 5;\n\n\t\t\tawait b.Refresh();\n\t\t\t_rankBoard = b;\n\t\t\t_rankTotalPlayers = (int)b.TotalEntries;\n\n\t\t\tLog.Info( $\"Rank '{RankStat}' \u2014 window: {b.Entries?.Length ?? -1}, board total: {b.TotalEntries}\" );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\"Reactor rank: {e.Message}\" );\n\t\t}\n\n\t\t_rankLoading = false;\n\t\t_rankFetched = true;\n\t\tRebuildRankRows();\n\t\tStateHasChanged();\n\t}\n\n\tvoid RebuildRankRows()\n\t{\n\t\t_rankRows.Clear();\n\t\t_myRank = 0;\n\t\t_nextUpValue = 0;\n\t\t_rankClimb = 0;\n\n\t\tvar entries = _rankBoard?.Entries;\n\t\tif ( entries == null || entries.Length == 0 ) return;\n\n\t\t// CenterOnMe means these indices are window-relative, NOT board-relative,\n\t\t// so ranks must come from Entry.Rank. Using index+1 here would label the\n\t\t// window \"#1..#5\" no matter where the player actually sits.\n\t\tlong myId = Sandbox.Connection.Local?.SteamId ?? 0;\n\t\tint meIdx = -1;\n\t\tfor ( int i = 0; i < entries.Length; i++ )\n\t\t{\n\t\t\tif ( entries[i].SteamId == myId ) { meIdx = i; break; }\n\t\t}\n\n\t\tif ( meIdx < 0 )\n\t\t{\n\t\t\t// No entry for us yet \u2014 show the window we were given so there is\n\t\t\t// still a named target to chase rather than an empty card.\n\t\t\tfor ( int i = 0; i < entries.Length; i++ )\n\t\t\t\t_rankRows.Add( MakeRankRow( entries[i], false ) );\n\n\t\t\t_nextUpValue = entries[entries.Length - 1].Value;\n\t\t\treturn;\n\t\t}\n\n\t\t_myRank = (int)entries[meIdx].Rank;\n\n\t\tint start = Math.Max( 0, meIdx - 2 );\n\t\tint end = Math.Min( entries.Length - 1, meIdx + 1 );\n\t\tfor ( int i = start; i <= end; i++ )\n\t\t\t_rankRows.Add( MakeRankRow( entries[i], i == meIdx ) );\n\n\t\tif ( meIdx > 0 ) _nextUpValue = entries[meIdx - 1].Value;\n\n\t\t// Places gained since we last recorded a rank. Persisting this is what\n\t\t// turns offline production into \"I moved up while I was away\".\n\t\tint prev = S?.LastReactorRank ?? 0;\n\t\tif ( prev > 0 && _myRank > 0 && _myRank < prev ) _rankClimb = prev - _myRank;\n\n\t\tif ( S != null && _myRank > 0 && S.LastReactorRank != _myRank )\n\t\t{\n\t\t\tS.LastReactorRank = _myRank;\n\t\t\tChainReactionGame.Save?.Save();\n\t\t}\n\t}\n\n\tRankRow MakeRankRow( Sandbox.Services.Leaderboards.Board2.Entry e, bool isMe )\n\t{\n\t\t// The snapshot lags our own earnings, so never show the player a number\n\t\t// lower than what they have actually banked.\n\t\tdouble val = isMe\n\t\t\t? Math.Max( e.Value, S?.AllTimeCores ?? 0 )\n\t\t\t: e.Value;\n\n\t\treturn new RankRow\n\t\t{\n\t\t\tRankLabel = \"#\" + e.Rank.ToString(),\n\t\t\tName = isMe ? \"you\" : RankName( e ),\n\t\t\tValueLabel = IdleFormat.Short( val ),\n\t\t\tIsMe = isMe,\n\t\t};\n\t}\n\n\tstring RankName( Sandbox.Services.Leaderboards.Board2.Entry e )\n\t{\n\t\tif ( !string.IsNullOrWhiteSpace( e.DisplayName ) ) return e.DisplayName;\n\t\ttry\n\t\t{\n\t\t\tvar friend = new Sandbox.Friend( e.SteamId );\n\t\t\tif ( !string.IsNullOrWhiteSpace( friend.Name ) ) return friend.Name;\n\t\t}\n\t\tcatch { }\n\t\treturn \"Anonymous\";\n\t}\n\n\tstring RankRowClass( RankRow r ) => r.IsMe ? \"me\" : \"\";\n\n\tstring RankEmptyLabel()\n\t{\n\t\tif ( !_rankFetched ) return \"Reading the board\";\n\t\treturn \"Produce Cores to enter the rankings\";\n\t}\n\n\tstring GapLabel()\n\t{\n\t\tif ( _nextUpValue <= 0 ) return \"Top of the board\";\n\n\t\tdouble mine = S?.AllTimeCores ?? 0;\n\t\tdouble gap = _nextUpValue - mine;\n\n\t\tif ( gap <= 0 ) return \"Ahead \u2014 board updates shortly\";\n\t\treturn IdleFormat.Short( gap ) + \" Cores to pass\";\n\t}\n\n\tstring ClimbLabel()\n\t{\n\t\tif ( _rankClimb == 1 ) return \"Up 1 place since last time\";\n\t\treturn \"Up \" + _rankClimb.ToString() + \" places since last time\";\n\t}\n\n\t// Lifecycle\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tprotected override void OnStart()\n\t{\n\t\tInstance = this;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !_subscribed && R != null ) Subscribe();\n\n\t\tbool vis = Visible;\n\t\tif ( vis != _wasVisible )\n\t\t{\n\t\t\t_wasVisible = vis;\n\t\t\tif ( vis )\n\t\t\t{\n\t\t\t\t_floaters.Clear();\n\t\t\t\t_toasts.Clear();\n\t\t\t\t_calloutLife = 0f;\n\t\t\t\t_shakeUntil = 0f;\n\t\t\t\t_showMeltdown = false;\n\t\t\t\t_showSingularity = false;\n\n\t\t\t\t// Offered on the walkthrough flag rather than IntroShown, so a\n\t\t\t\t// save from before the walkthrough existed still gets asked\n\t\t\t\t// once instead of being silently skipped past it.\n\t\t\t\t_showIntro = S != null && !S.TutorialDone && !S.TutorialOffered;\n\t\t\t\t_tutRunning = false;\n\t\t\t\t_tab = \"reactor\";\n\t\t\t\t_rankNextFetch = 0f; // refetch on entering idle mode\n\t\t\t\t_nextGoalAt = 0f;\n\t\t\t\tRefreshNextGoal();\n\n\t\t\t\tif ( PendingTutorial )\n\t\t\t\t{\n\t\t\t\t\tPendingTutorial = false;\n\t\t\t\t\tStartTutorial();\n\t\t\t\t}\n\t\t\t}\n\t\t\tStateHasChanged();\n\t\t}\n\n\t\tif ( !vis ) return;\n\n\t\t// Wall-clock throttle. Never move this into BuildHash or the render path.\n\t\tif ( RealTime.Now >= _rankNextFetch )\n\t\t{\n\t\t\t_rankNextFetch = RealTime.Now + RankRefreshSeconds;\n\t\t\t_ = LoadRank();\n\t\t}\n\n\t\t// SPACE is the detonate key everywhere else in the game, so it fires the\n\t\t// reactor's manual burst here too.\n\t\tif ( Input.Pressed( \"Jump\" ) && R.OverloadReady && !ModalOpen )\n\t\t\tR.Overload();\n\n\t\tif ( RealTime.Now >= _nextGoalAt )\n\t\t{\n\t\t\t_nextGoalAt = RealTime.Now + 0.5f;\n\t\t\tRefreshNextGoal();\n\t\t}\n\n\t\t// Re-measured every frame rather than only on step change: the board\n\t\t// resizes on a Reactor Frame purchase, cards appear and disappear in the\n\t\t// left column, and the window can be dragged to another size mid-tour.\n\t\tif ( TutorialActive ) MeasureTutorial();\n\n\t\tfloat dt = Time.Delta;\n\n\t\tfor ( int i = _floaters.Count - 1; i >= 0; i-- )\n\t\t{\n\t\t\t_floaters[i].Life -= dt;\n\t\t\tif ( _floaters[i].Life <= 0f ) _floaters.RemoveAt( i );\n\t\t}\n\n\t\tfor ( int i = _toasts.Count - 1; i >= 0; i-- )\n\t\t{\n\t\t\t_toasts[i].Life -= dt;\n\t\t\tif ( _toasts[i].Life <= 0f ) _toasts.RemoveAt( i );\n\t\t}\n\n\t\tif ( _calloutLife > 0f ) _calloutLife -= dt * 1.4f;\n\t}\n\n\tvoid Subscribe()\n\t{\n\t\tvar r = R;\n\t\tif ( r == null || _subscribed ) return;\n\n\t\tr.OnPop += OnPop;\n\t\tr.OnToast += OnToastEvent;\n\t\tr.OnDetonation += OnDetonation;\n\n\t\t// Walkthrough gates \u2014 a step that says \"press OVERLOAD\" only closes when\n\t\t// the player actually presses OVERLOAD.\n\t\tr.OnManualOverload += () => TutSatisfy( IdleTutTrigger.Overload );\n\t\tr.OnUpgradeBought += () => TutSatisfy( IdleTutTrigger.Buy );\n\t\tr.OnManualCrack += () => TutSatisfy( IdleTutTrigger.CellClick );\n\n\t\t_subscribed = true;\n\t}\n\n\t/// <summary>\n\t/// Rebuilds only when something on screen would actually differ.\n\t///\n\t/// The first version forced a repaint 15 times a second, which meant\n\t/// rebuilding the board, both side columns and every upgrade row constantly\n\t/// \u2014 that was the source of the stutter. Between detonations almost nothing\n\t/// changes, so hashing real state instead of a clock cuts the repaint rate\n\t/// by roughly an order of magnitude.\n\t/// </summary>\n\tprotected override int BuildHash()\n\t{\n\t\tif ( !Visible ) return 0;\n\n\t\tvar hc = new HashCode();\n\t\thc.Add( _tab );\n\t\thc.Add( _buyAmount );\n\t\thc.Add( _showMeltdown );\n\t\thc.Add( _showSingularity );\n\t\thc.Add( R?.CascadeActive ?? false );\n\t\thc.Add( S?.Shards ?? 0 );\n\t\thc.Add( S?.ShardsEarned ?? 0 );\n\t\thc.Add( S?.Singularities ?? 0 );\n\t\thc.Add( R?.SingularityVisible ?? false );\n\t\thc.Add( _showIntro );\n\t\thc.Add( ModalOpen );\n\n\t\t// Walkthrough \u2014 geometry is quantised to whole pixels, so a static\n\t\t// spotlight leaves the hash alone and the panel is not rebuilt.\n\t\thc.Add( TutorialActive );\n\t\thc.Add( _tutStep );\n\t\thc.Add( TutHasSpot );\n\t\thc.Add( (int)_spotX ); hc.Add( (int)_spotY );\n\t\thc.Add( (int)_spotW ); hc.Add( (int)_spotH );\n\t\thc.Add( (int)_cardX ); hc.Add( (int)_cardY );\n\n\t\t// Contracts \u2014 progress bars, claim state and the reset countdown\n\t\tfor ( int i = 0; i < IdleContracts.DailyCount; i++ )\n\t\t{\n\t\t\thc.Add( (int)(IdleContracts.Fraction( S, i ) * 200f) );\n\t\t\thc.Add( IdleContracts.IsClaimed( S, i ) );\n\t\t}\n\t\thc.Add( (int)(IdleContracts.SecondsUntilReset() / 60.0) );\n\n\t\tvar goal = NextGoal;\n\t\thc.Add( goal?.Id ?? \"\" );\n\t\thc.Add( goal == null ? 0 : (int)(goal.Fraction( S ) * 200f) );\n\n\t\t// Currencies \u2014 quantised to what the labels actually show\n\t\thc.Add( (long)Cores );\n\t\thc.Add( (long)R.Cps );\n\t\thc.Add( S.Flux );\n\t\thc.Add( S.FluxEarned );\n\t\thc.Add( _rankRows.Count );\n\t\thc.Add( _myRank );\n\t\thc.Add( _rankClimb );\n\t\thc.Add( ChainReactionGame.Save?.TotalFragments ?? 0 );\n\t\thc.Add( S.TodayReactorFragments );\n\t\thc.Add( (int)(R.FragmentProgressFraction * 60f) );\n\n\t\t// Live controls\n\t\thc.Add( (int)(R.OverloadCooldownLeft * 10f) );\n\t\thc.Add( (int)(R.Charge * 100f) );\n\t\thc.Add( R.SurgeActive );\n\t\thc.Add( (int)R.SurgeTimeLeft );\n\t\thc.Add( (int)R.GoldenTimeLeft );\n\t\thc.Add( R.Boosts.Count );\n\t\thc.Add( R.Boosts.Count > 0 ? (int)R.Boosts[0].SecondsLeft : 0 );\n\t\thc.Add( R.FeedStarved );\n\n\t\t// Transient effects\n\t\thc.Add( _floaters.Count );\n\t\thc.Add( (int)(_calloutLife * 20f) );\n\t\thc.Add( Time.Now < _shakeUntil ? _shakeCls : \"\" );\n\t\thc.Add( _toasts.Count );\n\n\t\t// Board \u2014 chest layout, damage, drones and any live flash\n\t\thc.Add( R.Size );\n\t\tfloat now = Time.Now;\n\t\tbool animating = _floaters.Count > 0 || _calloutLife > 0f || now < _shakeUntil;\n\n\t\tfor ( int r = 0; r < R.Size; r++ )\n\t\t\tfor ( int c = 0; c < R.Size; c++ )\n\t\t\t{\n\t\t\t\tvar cell = R.Board[r, c];\n\t\t\t\thc.Add( (int)cell.Chest );\n\t\t\t\thc.Add( cell.HitsLeft );\n\t\t\t\thc.Add( cell.HasDrone );\n\t\t\t\thc.Add( cell.FlashUntil > now ? cell.FlashKind : 0 );\n\n\t\t\t\tif ( cell.FlashUntil > now ) animating = true;\n\t\t\t\tif ( cell.HasChest && now - cell.SpawnAt < 0.25f ) animating = true;\n\t\t\t\tif ( cell.HasDrone && now - cell.DroneAt < 0.18f ) animating = true;\n\t\t\t}\n\n\t\thc.Add( R.GoldenRow );\n\t\thc.Add( R.GoldenCol );\n\t\thc.Add( R.LastChain );\n\n\t\t// Anything mid-flight is drawn from inline styles rather than keyframes,\n\t\t// so it needs a clock \u2014 but only while it is actually moving. The rest of\n\t\t// the time this hash is stable and the panel is never rebuilt at all.\n\t\tif ( animating ) hc.Add( (int)(now * 20f) );\n\n\t\treturn hc.ToHashCode();\n\t}\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Reactor events\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tvoid OnPop( int row, int col, double value, bool crit )\n\t{\n\t\tif ( R == null || _floaters.Count > 10 ) return;\n\n\t\tfloat step = 100f / R.Size;\n\t\t_floaters.Add( new Floater {\n\t\t\tText = \"+\" + IdleFormat.Short( value ),\n\t\t\tCls = crit ? \"crit\" : \"\",\n\t\t\tX = step * col + step * 0.5f,\n\t\t\tY = step * row + step * 0.5f,\n\t\t\tLife = 0.9f,\n\t\t\tMaxLife = 0.9f,\n\t\t} );\n\t}\n\n\tvoid OnToastEvent( string icon, string text, string cls )\n\t{\n\t\t_toasts.Insert( 0, new ToastMsg { Icon = icon, Text = text, Cls = cls, Life = 3.2f } );\n\t\twhile ( _toasts.Count > 4 ) _toasts.RemoveAt( _toasts.Count - 1 );\n\t}\n\n\tvoid OnDetonation( int chain, double payout, bool crit )\n\t{\n\t\tstring shake = crit || chain >= 25 ? \"idle-shake-3\"\n\t\t\t\t\t : chain >= 12 ? \"idle-shake-2\"\n\t\t\t\t\t : chain >= 4 ? \"idle-shake-1\"\n\t\t\t\t\t : \"\";\n\n\t\tif ( shake != \"\" )\n\t\t{\n\t\t\t_shakeCls = shake;\n\t\t\t_shakeUntil = Time.Now + (shake == \"idle-shake-3\" ? 0.45f : 0.28f);\n\t\t}\n\n\t\tif ( chain < 15 && !crit ) return;\n\n\t\tdouble critPower = IdleBalance.CritPower( S?.Level( IdleUpgradeId.CritPower ) ?? 0 );\n\n\t\t_calloutText = crit\n\t\t\t? $\"CRIT \u00d7{critPower:F0} \u00b7 +{IdleFormat.Short( payout )}\"\n\t\t\t: $\"{chain} CHAIN \u00b7 +{IdleFormat.Short( payout )}\";\n\n\t\t_calloutCls = crit ? \"crit\" : chain >= 35 ? \"mega\" : \"big\";\n\t\t_calloutLife = 1f;\n\t}\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Input\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tvoid SetTab( string tab )\n\t{\n\t\tbool changed = _tab != tab;\n\t\t_tab = tab;\n\t\tSound.Play( \"UI_Button\" );\n\t\tif ( changed ) TutSatisfy( IdleTutTrigger.TabChanged );\n\t}\n\tvoid SetBuyAmount( int amount ) { _buyAmount = amount; Sound.Play( \"UI_Button\" ); }\n\n\tint BuyCount( IdleUpgradeId id )\n\t\t=> _buyAmount < 0 ? Math.Max( 1, R.MaxAffordable( id ) ) : _buyAmount;\n\n\tvoid OnBuy( IdleUpgradeId id )\n\t{\n\t\tif ( R == null ) return;\n\t\tif ( !R.TryBuy( id, BuyCount( id ) ) ) Sound.Play( \"Pop_Button_1\" );\n\t}\n\n\tvoid OnBuyPerk( IdlePerkId id )\n\t{\n\t\tif ( R == null ) return;\n\t\tif ( !R.TryBuyPerk( id ) ) Sound.Play( \"Pop_Button_1\" );\n\t}\n\n\tvoid OnOverload() => R?.Overload();\n\tvoid OnSurge() => R?.ActivateSurge();\n\tvoid OnCellClick( int r, int c ) => R?.ClickCell( r, c );\n\n\tvoid OnClaimOffline() => R?.ClaimOffline();\n\tvoid OnClaimDaily() => R?.ClaimDaily();\n\n\tvoid OnOpenMeltdown() { _showMeltdown = true; Sound.Play( \"UI_Button\" ); }\n\tvoid OnCloseMeltdown() { _showMeltdown = false; Sound.Play( \"UI_Button\" ); }\n\n\tvoid OnOpenSingularity()\n\t{\n\t\t_showSingularity = true;\n\t\tSound.Play( \"UI_Button\" );\n\t\tif ( S != null && !S.SingularitySeen )\n\t\t{\n\t\t\tS.SingularitySeen = true;\n\t\t\tChainReactionGame.Save?.Save();\n\t\t}\n\t}\n\n\tvoid OnCloseSingularity() { _showSingularity = false; Sound.Play( \"UI_Button\" ); }\n\n\tvoid OnSingularity()\n\t{\n\t\t_showSingularity = false;\n\t\tR?.DoSingularity();\n\t\t_tab = \"powers\";\n\t}\n\n\tvoid OnBuyPower( IdlePowerId id ) => R?.BuyPower( id );\n\n\tvoid OnMeltdown()\n\t{\n\t\t_showMeltdown = false;\n\t\tR?.DoMeltdown();\n\t}\n\n\tvoid OnCloseIntro()\n\t{\n\t\t_showIntro = false;\n\t\tif ( S != null )\n\t\t{\n\t\t\tS.IntroShown = true;\n\t\t\tS.TutorialOffered = true;\n\t\t\tChainReactionGame.Save?.Save();\n\t\t}\n\t\tSound.Play( \"PlayButton\" );\n\t}\n\n\tvoid OnIntroTutorial()\n\t{\n\t\tif ( S != null )\n\t\t{\n\t\t\tS.IntroShown = true;\n\t\t\tChainReactionGame.Save?.Save();\n\t\t}\n\t\tSound.Play( \"PlayButton\" );\n\t\tStartTutorial();\n\t}\n\n\t/// <summary>The ? button in the top bar \u2014 replayable from anywhere, any time.</summary>\n\tvoid OnStartTutorial()\n\t{\n\t\tSound.Play( \"UI_Button\" );\n\t\tif ( _tutRunning ) { EndTutorial( completed: true ); return; }\n\t\tStartTutorial();\n\t}\n\n\tvoid OnClaimContract( int slot )\n\t{\n\t\tif ( R == null ) return;\n\t\tif ( !R.ClaimContract( slot ) ) Sound.Play( \"Pop_Button_1\" );\n\t}\n\n\tvoid OnBack()\n\t{\n\t\tSound.Play( \"UI_Button\" );\n\t\tGame?.ReturnToMenu();\n\t}\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Class helpers\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tstring Pct( float f ) => (Math.Clamp( f, 0f, 1f ) * 100f).ToString( \"F1\" );\n\n\tstring GlowClass() => R.SurgeActive ? \"surging\" : \"\";\n\tstring SurgeClass() => R.SurgeActive ? \"surging\" : \"\";\n\t// Surge is the louder event and its styles are declared first, so a cascade\n\t// would otherwise paint over it. Let surge own the board while it lasts.\n\tstring CascadeClass() => R.CascadeActive && !R.SurgeActive ? \"cascading\" : \"\";\n\tstring SurgeReadyClass() => R.SurgeReady ? \"ready\" : \"\";\n\tstring OverloadClass() => R.OverloadReady ? \"ready\" : \"cooling\";\n\tstring BoardShake() => Time.Now < _shakeUntil ? _shakeCls : \"\";\n\tstring TabClass( string t ) => _tab == t ? \"on\" : \"\";\n\tstring BuyAmtClass( int amt ) => _buyAmount == amt ? \"on\" : \"\";\n\tstring MaxedClass( bool maxed ) => maxed ? \"maxed\" : \"\";\n\tstring CapClass() => R.FragmentsToday >= R.DailyFragmentCap ? \"capped\" : \"\";\n\tstring FeedClass() => R.FeedStarved ? \"starved\" : \"\";\n\tstring MeltdownStripClass() => R.CanMeltdown ? \"ready\" : \"\";\n\n\tstring StreakPipClass( int day )\n\t{\n\t\tint streak = S?.DailyStreak ?? 1;\n\t\tif ( day == streak ) return \"on today\";\n\t\treturn day < streak ? \"on\" : \"\";\n\t}\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Text helpers \u2014 one composed string per element (see note at top)\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\t// \u2500\u2500 Daily contracts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tstring ContractsCardClass() => R.ContractReady ? \"ready\" : \"\";\n\n\tstring ContractClass( int slot )\n\t{\n\t\tif ( IdleContracts.IsClaimed( S, slot ) ) return \"claimed\";\n\t\tif ( IdleContracts.IsComplete( S, slot ) ) return \"ready\";\n\t\treturn \"\";\n\t}\n\n\tstring ContractIcon( int slot )\n\t{\n\t\tif ( IdleContracts.IsClaimed( S, slot ) ) return \"\u2705\";\n\t\treturn IdleContracts.At( S, slot )?.Icon ?? \"\ud83d\udccb\";\n\t}\n\n\tstring ContractDescLabel( int slot )\n\t{\n\t\tvar c = IdleContracts.At( S, slot );\n\t\tif ( c == null ) return \"\";\n\t\treturn c.Desc( IdleContracts.Target( S, slot ) );\n\t}\n\n\tstring ContractRewardLabel( int slot )\n\t{\n\t\tif ( IdleContracts.IsClaimed( S, slot ) ) return \"CLAIMED\";\n\t\tif ( IdleContracts.IsComplete( S, slot ) ) return \"CLAIM \u2192\";\n\t\treturn $\"+{IdleContracts.Reward( S, slot )} \ud83d\udca0\";\n\t}\n\n\tstring ContractProgLabel( int slot )\n\t{\n\t\tvar c = IdleContracts.At( S, slot );\n\t\tif ( c == null ) return \"\";\n\n\t\tdouble cur = Math.Min( IdleContracts.Progress( S, slot ), IdleContracts.Target( S, slot ) );\n\t\tdouble tgt = IdleContracts.Target( S, slot );\n\n\t\tif ( c.Id == \"runtime\" )\n\t\t\treturn $\"{IdleFormat.Duration( cur )} / {IdleFormat.Duration( tgt )}\";\n\n\t\treturn c.BigNumbers\n\t\t\t? $\"{IdleFormat.Short( cur )} / {IdleFormat.Short( tgt )}\"\n\t\t\t: $\"{(long)cur} / {(long)tgt}\";\n\t}\n\n\tstring ContractResetLabel()\n\t\t=> \"resets in \" + IdleFormat.Duration( IdleContracts.SecondsUntilReset() );\n\n\tstring ContractFootLabel()\n\t{\n\t\tint streak = S?.ContractStreak ?? 0;\n\t\tif ( IdleContracts.AllClaimed( S ) )\n\t\t\treturn streak > 1\n\t\t\t\t? $\"All three cleared \u00b7 {streak} days running\"\n\t\t\t\t: \"All three cleared. Back tomorrow.\";\n\n\t\treturn \"Clear all three for a \u00d73 boost and double Fragments on the last one.\";\n\t}\n\n\t// \u2500\u2500 Next goal pin \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\t/// <summary>\n\t/// The unfinished Reactor Log entry closest to completion. Picking by\n\t/// fraction rather than by list order means the pin always shows something\n\t/// nearly done, which is the only version of this card that pulls.\n\t///\n\t/// Cached: BuildHash runs every frame and the markup reads this five more\n\t/// times per rebuild, so recomputing it on every access would walk the\n\t/// achievement list a hundred-odd times a frame for a card that changes\n\t/// once every few minutes.\n\t/// </summary>\n\tIdleAchievement _nextGoal;\n\tfloat _nextGoalAt;\n\n\tIdleAchievement NextGoal => _nextGoal;\n\n\tvoid RefreshNextGoal()\n\t{\n\t\tvar s = S;\n\t\tif ( s == null ) { _nextGoal = null; return; }\n\n\t\tIdleAchievement best = null;\n\t\tfloat bestFrac = -1f;\n\n\t\tforeach ( var a in IdleAchievements.All )\n\t\t{\n\t\t\tif ( s.HasAchievement( a.Id ) ) continue;\n\t\t\tfloat f = a.Fraction( s );\n\t\t\tif ( f > bestFrac ) { bestFrac = f; best = a; }\n\t\t}\n\t\t_nextGoal = best;\n\t}\n\n\tstring NextGoalProgLabel()\n\t{\n\t\tvar a = NextGoal;\n\t\tif ( a == null ) return \"\";\n\t\treturn ProgressLabel( a );\n\t}\n\n\tstring NextGoalRewardLabel()\n\t{\n\t\tvar a = NextGoal;\n\t\tif ( a == null ) return \"\";\n\t\treturn RewardLabel( a );\n\t}\n\n\tstring CoresLabel() => IdleFormat.Short( Cores );\n\tstring RateLabel() => IdleFormat.Short( R.Cps );\n\tstring FluxLabel() => (S?.Flux ?? 0).ToString();\n\tstring FragLabel() => (ChainReactionGame.Save?.TotalFragments ?? 0).ToString();\n\n\tstring GlobalMultLabel() => \"\u00d7\" + IdleFormat.Short( R.GlobalMultiplier );\n\tstring ExponentLabel() => R.ChainExponent.ToString( \"F2\" );\n\tstring LastChainLabel() => $\"{R.LastChain} chests\";\n\tstring BestChainLabel() => $\"{S.BestChain} chests\";\n\tstring BoardLabel() => $\"{R.Size} \u00d7 {R.Size}\";\n\n\tstring FeedLabel() => R.FeedStarved ? \"starving\" : \"keeping up\";\n\n\tstring SurgeBoostLabel() => $\"SURGE \u00d7{IdleBalance.SurgeMultiplier:F0}\";\n\tstring BoostLabel( IdleReactor.Boost b ) => $\"{b.Label} \u00d7{b.Mult:F1}\";\n\tstring SecondsLabel( float s ) => $\"{s:F0}s\";\n\n\tstring CondenserLabel()\n\t\t=> $\"next at {IdleFormat.Short( R.FragmentCost )} Cores \u00b7 price climbs with each one\";\n\n\tstring FragTodayLabel() => $\"{R.FragmentsToday} / {R.DailyFragmentCap}\";\n\n\tstring TierBlurb( int index, bool unlocked )\n\t{\n\t\tvar tier = IdleBalance.Tiers[index];\n\t\tif ( unlocked ) return tier.Blurb;\n\n\t\t// Tier Memory can drop the gate to zero, so read the effective figure\n\t\t// rather than the catalog one.\n\t\tdouble need = IdleBalance.TierRequirement( index, S?.PowerLevel( IdlePowerId.TierSkip ) ?? 0 );\n\t\treturn $\"unlocks at {IdleFormat.Short( need )} cycle Cores\";\n\t}\n\n\tstring GoldenTimerLabel() => R.GoldenTimeLeft.ToString( \"F0\" );\n\n\tstring OverloadLabel()\n\t\t=> R.OverloadReady ? \"\u26a1 OVERLOAD\" : $\"{R.OverloadCooldownLeft:F1}s\";\n\n\tstring OverloadSubLabel() => $\"\u00d7{IdleBalance.OverloadMultiplier:F1} burst \u00b7 SPACE\";\n\tstring OverloadFillPct() => ((1f - R.OverloadCooldownFraction) * 100f).ToString( \"F0\" );\n\tstring UnleashLabel() => $\"UNLEASH \u00d7{IdleBalance.SurgeMultiplier:F0}\";\n\n\tstring SurgeLabel()\n\t{\n\t\tif ( R.SurgeActive ) return $\"SURGE {R.SurgeTimeLeft:F0}s\";\n\t\tif ( R.SurgeReady ) return \"SURGE READY\";\n\t\treturn $\"CHARGE {R.Charge * 100f:F0}%\";\n\t}\n\n\tstring MeltdownLabel()\n\t\t=> R.CanMeltdown\n\t\t\t? $\"Meltdown available \u2014 +{R.PendingFlux} Flux\"\n\t\t\t: $\"Meltdown unlocks at {IdleFormat.Short( R.MeltdownRequirement )} cycle Cores \u00b7 {IdleFormat.Short( S.CycleCores )} banked\";\n\n\tstring BuyAmountLabel( int amount ) => amount < 0 ? \"MAX\" : $\"\u00d7{amount}\";\n\n\tstring UpgradeClass( bool revealed, bool maxed, bool afford )\n\t{\n\t\tif ( !revealed ) return \"locked\";\n\t\tif ( maxed ) return \"maxed\";\n\t\treturn afford ? \"afford\" : \"poor\";\n\t}\n\n\tstring UpgradeIcon( IdleUpgrade up, bool revealed ) => revealed ? up.Icon : \"\ud83d\udd12\";\n\tstring UpgradeName( IdleUpgrade up, bool revealed ) => revealed ? up.Name : \"Locked\";\n\n\tstring UpgradeDesc( IdleUpgrade up, bool revealed )\n\t\t=> revealed ? up.Desc : $\"Reach {IdleFormat.Short( up.RevealAt )} lifetime Cores to reveal.\";\n\n\tstring LevelLabel( IdleUpgrade up, int lvl, bool revealed )\n\t{\n\t\tif ( !revealed ) return \"\u2014\";\n\t\treturn up.MaxLevel < 100 ? $\"Lv {lvl} / {up.MaxLevel}\" : $\"Lv {lvl}\";\n\t}\n\n\tstring CostLabel( bool maxed, double cost, int count )\n\t{\n\t\tif ( maxed ) return \"MAXED\";\n\t\treturn count > 1\n\t\t\t? $\"{IdleFormat.Short( cost )} \u00d7{count}\"\n\t\t\t: IdleFormat.Short( cost );\n\t}\n\n\tstring PerkLevelLabel( IdlePerk p, int lvl ) => p.Uncapped ? $\"Lv {lvl}\" : $\"Lv {lvl} / {p.MaxLevel}\";\n\tstring PerkCostLabel( bool maxed, int cost ) => maxed ? \"MAXED\" : $\"\ud83d\udd37 {cost}\";\n\n\t/// <summary>\n\t/// Containment Field is deliberately hidden until the player has enough Flux\n\t/// history to understand what a sink is for \u2014 offered at Flux 0 it just reads\n\t/// as a bad first purchase.\n\t/// </summary>\n\tIEnumerable<IdlePerk> VisiblePerks()\n\t\t=> IdleUpgradeCatalog.Perks.Where( p => p.RevealAtFluxEarned <= 0\n\t\t\t\t\t\t\t\t\t\t\t || (S?.FluxEarned ?? 0) >= p.RevealAtFluxEarned\n\t\t\t\t\t\t\t\t\t\t\t || (S?.PerkLevel( p.Id ) ?? 0) > 0 );\n\n\tstring PowerLevelLabel( IdlePower p, int lvl ) => $\"Lv {lvl} / {p.MaxLevel}\";\n\tstring PowerCostLabel( bool maxed, int cost ) => maxed ? \"MAXED\" : $\"\u2734\ufe0f {cost}\";\n\n\tstring ShardHeadLabel() => $\"\u2734\ufe0f {S.Shards}\";\n\n\tstring ShardSubLabel()\n\t\t=> $\"{S.ShardsEarned} earned \u00b7 \u00d7{IdleBalance.ShardMultiplier( S.ShardsEarned ):F2} permanent income\";\n\n\tstring SingStripClass() => R.CanSingularity ? \"ready\" : \"\";\n\n\tstring SingularityLabel()\n\t{\n\t\tif ( R.CanSingularity )\n\t\t\treturn $\"Singularity available \u2014 +{R.PendingShards} Shards\";\n\n\t\treturn $\"Singularity unlocks at {IdleBalance.SingularityRequirement} lifetime Flux \u00b7 {S.FluxEarned} earned\";\n\t}\n\n\tstring SingPayoutLabel() => \"+\" + R.PendingShards.ToString();\n\tstring BurnedFluxLabel() => $\"{S.FluxEarned} lifetime Flux\";\n\n\tstring ShardKeepLabel()\n\t{\n\t\tint after = S.ShardsEarned + R.PendingShards;\n\t\treturn $\"\u00d7{IdleBalance.ShardMultiplier( after ):F2} income from Shards\";\n\t}\n\n\tstring FluxHeadLabel() => $\"\ud83d\udd37 {S.Flux}\";\n\n\tstring FluxSubLabel()\n\t\t=> $\"{S.FluxEarned} earned \u00b7 \u00d7{IdleBalance.FluxMultiplier( S.FluxEarned ):F2} permanent income\";\n\n\tstring LogHeadLabel() => $\"{IdleAchievements.CompletedCount( S )} / {IdleAchievements.All.Count}\";\n\n\tstring AchIcon( IdleAchievement a, bool done ) => done ? a.Icon : \"\ud83d\udd18\";\n\n\tstring RewardLabel( IdleAchievement a )\n\t{\n\t\tif ( a.RewardFlux > 0 && a.RewardFrags > 0 ) return $\"+{a.RewardFlux}\ud83d\udd37 +{a.RewardFrags}\ud83d\udca0\";\n\t\tif ( a.RewardFlux > 0 ) return $\"+{a.RewardFlux}\ud83d\udd37\";\n\t\treturn $\"+{a.RewardFrags}\ud83d\udca0\";\n\t}\n\n\tstring ProgressLabel( IdleAchievement a )\n\t{\n\t\tdouble cur = a.Progress( S );\n\t\treturn a.BigNumbers\n\t\t\t? $\"{IdleFormat.Short( cur )} / {IdleFormat.Short( a.Target )}\"\n\t\t\t: $\"{(long)cur} / {(long)a.Target}\";\n\t}\n\n\t// \u2500\u2500 Modal labels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tstring OfflineSubLabel()\n\t\t=> $\"The reactor ran for {IdleFormat.Duration( R.PendingOfflineSeconds )} at {IdleFormat.Pct( R.OfflineEfficiency )} efficiency.\";\n\n\tstring OfflineAmountLabel() => \"+\" + IdleFormat.Short( R.PendingOfflineCores );\n\n\tstring OfflineFragLabel() => $\"condensing {R.PendingOfflineFrags} Fragments\";\n\n\tstring OfflineWarnLabel()\n\t\t=> $\"Storage filled up after {R.OfflineCapHours:F0}h. Upgrade Core Storage to bank longer trips.\";\n\n\tstring DailyTitleLabel() => $\"Day {S?.DailyStreak ?? 1}\";\n\tstring DailyMultLabel() => $\"\u00d7{IdleBalance.DailyReward( S?.DailyStreak ?? 1 ).mult:F1}\";\n\n\tstring DailyRewardLabel()\n\t{\n\t\tvar reward = IdleBalance.DailyReward( S?.DailyStreak ?? 1 );\n\t\treturn $\"for {IdleFormat.Duration( reward.seconds )} \u00b7 +{reward.frags} Fragments\";\n\t}\n\n\tstring MeltPayoutLabel() => \"+\" + R.PendingFlux;\n\tstring BurnedCoresLabel() => $\"{IdleFormat.Short( Cores )} Cores\";\n\n\tstring MeltMultBeforeLabel()\n\t\t=> \"\u00d7\" + IdleBalance.FluxMultiplier( S?.FluxEarned ?? 0 ).ToString( \"F2\" );\n\n\tstring MeltMultAfterLabel()\n\t\t=> \"\u00d7\" + IdleBalance.FluxMultiplier( (S?.FluxEarned ?? 0) + R.PendingFlux ).ToString( \"F2\" );\n\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\t// Cell rendering\n\t// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n\tstring CellClass( IdleReactor.Cell cell, bool golden )\n\t{\n\t\tif ( golden ) return \"golden\";\n\n\t\tstring cls = cell.HasChest ? \"filled\" : \"empty\";\n\n\t\tif ( cell.FlashUntil > Time.Now )\n\t\t{\n\t\t\tcls += cell.FlashKind switch\n\t\t\t{\n\t\t\t\t3 => \" fx-drone\",\n\t\t\t\t2 => \" fx-kill\",\n\t\t\t\t_ => \" fx-blast\",\n\t\t\t};\n\t\t}\n\n\t\tif ( cell.HasDrone ) cls += \" armed\";\n\n\t\tif ( cell.HasChest )\n\t\t{\n\t\t\tcls += cell.Chest switch\n\t\t\t{\n\t\t\t\tGridManager.ChestType.Gem => \" c-gem\",\n\t\t\t\tGridManager.ChestType.BombChest => \" c-bomb\",\n\t\t\t\tGridManager.ChestType.Locked => \" c-locked\",\n\t\t\t\tGridManager.ChestType.Cursed => \" c-cursed\",\n\t\t\t\tGridManager.ChestType.Void => \" c-void\",\n\t\t\t\tGridManager.ChestType.Boss => \" c-boss\",\n\t\t\t\t_ => \" c-normal\",\n\t\t\t};\n\t\t}\n\n\t\treturn cls;\n\t}\n\n\t/// <summary>\n\t/// Spawn pop, computed here rather than as a keyframe: the panel can rebuild\n\t/// at any moment and a class-driven animation would restart mid-flight.\n\t/// </summary>\n\tstring ChestStyle( IdleReactor.Cell cell )\n\t{\n\t\tfloat age = Time.Now - cell.SpawnAt;\n\t\tif ( age >= 0.25f || age < 0f ) return \"\";\n\n\t\tfloat scale = 0.4f + 0.6f * (age / 0.25f);\n\t\treturn $\"transform:scale({scale.ToString( \"F2\" )});\";\n\t}\n\n\t/// <summary>Drone drop \u2014 falls onto the cell as the volley arms.</summary>\n\tstring DroneStyle( IdleReactor.Cell cell )\n\t{\n\t\tfloat age = Time.Now - cell.DroneAt;\n\t\tif ( age >= 0.18f || age < 0f ) return \"\";\n\n\t\tfloat t = age / 0.18f;\n\t\tfloat scale = 2.0f - 1.0f * t;\n\t\treturn $\"transform:scale({scale.ToString( \"F2\" )});opacity:{t.ToString( \"F2\" )};\";\n\t}\n\n\tstring ChestGlyph( IdleReactor.Cell cell ) => cell.Chest switch\n\t{\n\t\tGridManager.ChestType.BombChest => \"\ud83d\udca3\",\n\t\tGridManager.ChestType.Locked => \"\ud83d\udd12\",\n\t\tGridManager.ChestType.Cursed => \"\ud83d\udc80\",\n\t\tGridManager.ChestType.Void => \"\ud83c\udf11\",\n\t\tGridManager.ChestType.Boss => \"\ud83d\udc51\",\n\t\tGridManager.ChestType.Gem => \"\ud83d\udc8e\",\n\t\t_ => \"\ud83c\udf81\",\n\t};\n}\n"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "styles/form/_switch.scss",
"FileName": "_switch.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "\r\n$primary: red !default;\r\n$primary-alt: white !default;\r\n\r\n$switch-padding: 6px !default;\r\n\r\n.checkbox.switch\r\n{\r\n\tcursor: pointer;\r\n\r\n\t> .checkmark\r\n\t{\r\n\t\tfont-size: 22px;\r\n\t\tborder: 0px solid $primary;\r\n\t\tborder-radius: 100px;\r\n\t\ttext-align: center;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tcolor: $primary-alt;\r\n\t\tpadding: $switch-padding;\r\n\t\tpadding-right: 32px;\r\n\t\tpadding-left: $switch-padding;\r\n\t\ttransition: all 0.3s ease;\r\n\t\tbackground-color: rgba( $primary, 0.1 );\r\n\r\n\t\t> .handle\r\n\t\t{\r\n\t\t\tbackground-color: $primary-alt;\r\n\t\t\twidth: 20px;\r\n\t\t\theight: 20px;\r\n\t\t\tborder-radius: 100px;\r\n\t\t\tbox-shadow: 2px 2px 12px black;\r\n\t\t}\r\n\t}\r\n\r\n\t&.checked\r\n\t{\r\n\t\t> .checkmark\r\n\t\t{\r\n\t\t\tbackground-color: $primary;\r\n\t\t\tpadding-left: 32px;\r\n\t\t\tpadding-right: $switch-padding;\r\n\t\t}\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\ttransform: scale( 0.9 );\r\n\t\ttransform-origin: 20px 50%;\r\n\t}\r\n}\r\n"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "ui/controls/vectorcontrol.cs.scss",
"FileName": "vectorcontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "VectorControl\r\n{\r\n\tgap: 2px;\r\n\tflex-grow: 1;\r\n}\r\n\r\nVectorControl NumberEntry\r\n{\r\n\tflex-basis: 50%;\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "ui/controls/enumcontrol.cs.scss",
"FileName": "enumcontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "EnumControl\r\n{\r\n\tgap: 2px;\r\n\tflex-grow: 1;\r\n}\r\n\r\nEnumControl DropDown,\r\nEnumControl ButtonGroup\r\n{\r\n\tborder-radius: 8px;\r\n\tbackground-color: #000a;\r\n\tflex-grow: 1;\r\n}\r\n\r\nEnumControl DropDown\r\n{\r\n\tflex-grow: 1;\r\n\tmin-height: 32px;\r\n}\r\n\r\nEnumControl ButtonGroup\r\n{\r\n\tborder-radius: 12px;\r\n\toverflow: hidden;\r\n\tmin-height: 32px;\r\n\r\n\tButton\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tgap: 4px;\r\n\t\tcolor: #aaa;\r\n\t\tfont-size: 1rem;\r\n\t\tcursor: pointer;\r\n\r\n\t\t.icon\r\n\t\t{\r\n\t\t\tcolor: #08f;\r\n\t\t}\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tcolor: #ddd;\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #3af;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&:active\r\n\t\t{\r\n\t\t\tbackground-color: #04a;\r\n\t\t\tcolor: white;\r\n\t\t\ttransform: translateX( 1px ) translateY( 1px );\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #fff;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&.active\r\n\t\t{\r\n\t\t\tbackground-color: #08f;\r\n\t\t\tcolor: white;\r\n\t\t\tpointer-events: none;\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #fff;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "ui/controls/color/colorpickercontrol.cs.scss",
"FileName": "colorpickercontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "ColorPickerControl\r\n{\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n\tgap: 0.5rem;\r\n\tmargin: 1rem;\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "game/idle/ui/idlehud.razor.scss",
"FileName": "idlehud.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n THE REACTOR \u2014 Idle mode HUD\n Palette leans teal/amber against the same deep purple-black the rest of\n 10XPLOSION uses, so it reads as \"the same game, different room\".\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\nroot {\n /* Declared so no panel in this tree ever claims the OS pointer -- a hovered\n panel outranks Mouse.CursorType, and one that declares nothing still claims\n the default arrow. GameCursor draws the cursor instead. */\n cursor: none;\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n pointer-events: none;\n overflow: hidden;\n /* Sits above the wave HUD's layers \u2014 those hide themselves in this mode,\n but ScreenShaker and PopupLayer share the same ScreenPanel. */\n z-index: 200;\n}\n\n.idle-root {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n display: flex;\n flex-direction: column;\n pointer-events: none;\n}\n\n/* \u2500\u2500 Backdrop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-bg {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background-color: #04070d;\n}\n\n.idle-bg-glow {\n position: absolute;\n top: -20%; left: 15%;\n width: 70%;\n height: 120%;\n border-radius: 50%;\n background-color: rgba(20, 130, 130, 0.10);\n filter: blur(120px);\n animation: idle-breathe 9s ease-in-out infinite alternate;\n transition: background-color 0.5s;\n}\n\n.idle-bg-glow.surging {\n background-color: rgba(251, 146, 60, 0.22);\n}\n\n@keyframes idle-breathe {\n 0% { transform: scale(1.00); opacity: 0.75; }\n 100% { transform: scale(1.18); opacity: 1.00; }\n}\n\n.idle-particle {\n position: absolute;\n bottom: -12px;\n border-radius: 50%;\n animation: idle-rise linear infinite;\n}\n\n@keyframes idle-rise {\n 0% { transform: translateY(0px); opacity: 0; }\n 12% { opacity: 1; }\n 100% { transform: translateY(-1180px); opacity: 0; }\n}\n\n/* \u2500\u2500 Top bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-top {\n position: relative;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 16px;\n padding: 12px 22px;\n flex-shrink: 0;\n background-color: rgba(6, 12, 20, 0.82);\n border-bottom: 2px solid rgba(45, 212, 191, 0.18);\n pointer-events: all;\n overflow: hidden;\n}\n\n.idle-back {\n width: 46px;\n height: 46px;\n flex-shrink: 0;\n border-radius: 10px;\n border: 2px solid rgba(148, 163, 184, 0.28);\n background-color: rgba(15, 23, 35, 0.9);\n color: rgba(226, 232, 240, 0.85);\n font-size: 22px;\n font-weight: 900;\n align-items: center;\n justify-content: center;\n cursor: none;\n pointer-events: all;\n transition: background-color 0.1s, border-color 0.1s;\n}\n.idle-back:hover {\n background-color: rgba(45, 212, 191, 0.18);\n border-color: rgba(45, 212, 191, 0.6);\n}\n\n/* Without an explicit no-shrink the currency row squeezes this to nothing and\n the title wraps one letter per line. */\n.idle-brand {\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n flex-grow: 0;\n}\n\n.idle-brand-title {\n white-space: nowrap;\n font-size: 22px;\n font-weight: 900;\n letter-spacing: 0.24em;\n color: #5eead4;\n text-shadow: 0px 0px 18px rgba(45, 212, 191, 0.45);\n}\n\n.idle-brand-sub {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: rgba(148, 163, 184, 0.55);\n}\n\n.idle-currencies {\n display: flex;\n flex-direction: row;\n gap: 8px;\n margin-left: auto;\n flex-shrink: 0;\n flex-grow: 0;\n}\n\n.idle-cur {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 7px;\n padding: 7px 13px;\n border-radius: 10px;\n background-color: rgba(12, 20, 30, 0.92);\n border: 2px solid rgba(100, 116, 139, 0.22);\n width: 172px;\n flex-shrink: 0;\n flex-grow: 0;\n overflow: hidden;\n}\n\n.idle-cur-icon { font-size: 17px; flex-shrink: 0; }\n\n.idle-cur-val {\n font-size: 19px;\n font-weight: 900;\n color: #e2e8f0;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.idle-cur-lbl {\n font-size: 9px;\n font-weight: 800;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: rgba(148, 163, 184, 0.55);\n margin-left: auto;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.idle-cur.cores { border-color: rgba(45, 212, 191, 0.45); }\n.idle-cur.cores .idle-cur-val { color: #5eead4; }\n.idle-cur.rate .idle-cur-val { color: #a7f3d0; font-size: 17px; }\n.idle-cur.flux { border-color: rgba(96, 165, 250, 0.42); width: 132px; }\n.idle-cur.flux .idle-cur-val { color: #93c5fd; }\n.idle-cur.frag { border-color: rgba(167, 139, 250, 0.42); width: 152px; }\n.idle-cur.frag .idle-cur-val { color: #c4b5fd; }\n\n/* \u2500\u2500 Body layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-body {\n position: relative;\n display: flex;\n flex-direction: row;\n align-items: stretch;\n gap: 14px;\n padding: 14px 20px 16px 20px;\n flex-grow: 1;\n flex-shrink: 1;\n height: 0; /* lets flex-grow decide the height, so children can scroll */\n overflow: hidden;\n}\n\n.idle-col { display: flex; flex-direction: column; gap: 12px; }\n\n.idle-left {\n width: 300px;\n flex-shrink: 0;\n overflow-y: scroll;\n overflow-x: hidden;\n pointer-events: all;\n}\n\n.idle-right {\n width: 400px;\n flex-shrink: 0;\n overflow: hidden;\n pointer-events: all;\n}\n\n.idle-center {\n flex-grow: 1;\n align-items: center;\n justify-content: center;\n gap: 16px;\n pointer-events: all;\n}\n\n/* \u2500\u2500 Info cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-card {\n background-color: rgba(10, 17, 26, 0.88);\n border: 2px solid rgba(100, 116, 139, 0.18);\n border-radius: 12px;\n padding: 12px 14px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n flex-shrink: 0;\n}\n\n.idle-card-hd {\n font-size: 11px;\n font-weight: 900;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: rgba(94, 234, 212, 0.75);\n margin-bottom: 4px;\n}\n\n.idle-card-note {\n font-size: 11px;\n font-weight: 600;\n color: rgba(148, 163, 184, 0.65);\n}\n.idle-card-note.dim { color: rgba(251, 191, 36, 0.75); }\n\n.idle-stat-row {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.idle-stat-lbl {\n font-size: 12px;\n font-weight: 600;\n color: rgba(148, 163, 184, 0.75);\n}\n\n.idle-stat-val {\n font-size: 14px;\n font-weight: 900;\n color: #e2e8f0;\n}\n.idle-stat-val.hot { color: #5eead4; }\n.idle-stat-val.capped { color: #fbbf24; }\n.idle-stat-val.starved { color: #f87171; }\n\n\n/* \u2500\u2500 Reactor rank card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.idle-rank-row {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n border-radius: 6px;\n border: 2px solid rgba(0, 0, 0, 0);\n}\n\n.idle-rank-row.me {\n background-color: rgba(94, 234, 212, 0.10);\n border-color: rgba(94, 234, 212, 0.35);\n}\n\n.idle-rank-pos {\n font-size: 11px;\n font-weight: 900;\n color: rgba(148, 163, 184, 0.75);\n width: 26px;\n flex-shrink: 0;\n}\n.idle-rank-row.me .idle-rank-pos { color: #5eead4; }\n\n.idle-rank-name {\n font-size: 12px;\n font-weight: 700;\n color: rgba(226, 232, 240, 0.85);\n flex-grow: 1;\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n}\n.idle-rank-row.me .idle-rank-name { color: #e2e8f0; }\n\n.idle-rank-val {\n font-size: 12px;\n font-weight: 900;\n color: rgba(148, 163, 184, 0.9);\n flex-shrink: 0;\n}\n.idle-rank-row.me .idle-rank-val { color: #5eead4; }\n\n.idle-card-note.up {\n color: rgba(74, 222, 128, 0.9);\n font-weight: 700;\n}\n.idle-bar-track {\n width: 100%;\n height: 10px;\n border-radius: 5px;\n background-color: rgba(30, 41, 59, 0.9);\n overflow: hidden;\n}\n\n.idle-bar-fill {\n height: 100%;\n border-radius: 5px;\n background-color: #5eead4;\n transition: width 0.2s;\n}\n.idle-bar-fill.frag { background-color: #a78bfa; }\n\n/* Boosts */\n.idle-boost {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 8px;\n background-color: rgba(30, 41, 59, 0.7);\n border-left: 3px solid rgba(94, 234, 212, 0.7);\n}\n.idle-boost.surge {\n border-left-color: #fb923c;\n background-color: rgba(120, 53, 15, 0.45);\n}\n.idle-boost-icon { font-size: 15px; }\n.idle-boost-lbl { font-size: 12px; font-weight: 800; color: #e2e8f0; }\n.idle-boost-time {\n margin-left: auto;\n font-size: 11px;\n font-weight: 900;\n color: rgba(251, 191, 36, 0.9);\n}\n\n/* Chest tiers */\n.idle-tier {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 9px;\n padding: 5px 0px;\n opacity: 0.42;\n}\n.idle-tier.on { opacity: 1; }\n\n.idle-tier-icon { font-size: 20px; width: 26px; text-align: center; }\n.idle-tier-body { display: flex; flex-direction: column; flex-grow: 1; }\n.idle-tier-name { font-size: 12px; font-weight: 900; color: #e2e8f0; }\n.idle-tier-blurb {\n font-size: 10px;\n font-weight: 600;\n color: rgba(148, 163, 184, 0.7);\n}\n.idle-tier.on .idle-tier-name { color: #5eead4; }\n\n/* \u2500\u2500 The board \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-board-wrap {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.idle-board {\n display: flex;\n flex-direction: column;\n gap: 5px;\n padding: 14px;\n border-radius: 16px;\n background-color: rgba(6, 14, 22, 0.9);\n border: 2px solid rgba(45, 212, 191, 0.22);\n box-shadow: 0px 0px 40px rgba(13, 148, 136, 0.14);\n transition: border-color 0.3s, box-shadow 0.3s;\n}\n\n.idle-board.surging {\n border-color: rgba(251, 146, 60, 0.65);\n box-shadow: 0px 0px 60px rgba(251, 146, 60, 0.35);\n}\n\n.idle-row { display: flex; flex-direction: row; gap: 5px; }\n\n.idle-cell {\n position: relative;\n border-radius: 9px;\n background-color: rgba(17, 27, 40, 0.85);\n border: 2px solid rgba(71, 85, 105, 0.28);\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n cursor: none;\n pointer-events: all;\n overflow: hidden;\n}\n\n.idle-cell.empty { background-color: rgba(13, 21, 32, 0.7); }\n.idle-cell.filled { border-color: rgba(148, 163, 184, 0.35); }\n\n.idle-chest {\n font-size: 30px;\n text-shadow: 0px 2px 6px rgba(0, 0, 0, 0.6);\n}\n\n/* chest type tinting */\n.idle-cell.c-normal { background-color: rgba(59, 44, 20, 0.55); border-color: rgba(217, 160, 60, 0.45); }\n.idle-cell.c-gem { background-color: rgba(14, 60, 78, 0.6); border-color: rgba(56, 189, 248, 0.55); }\n.idle-cell.c-bomb { background-color: rgba(70, 24, 24, 0.62); border-color: rgba(248, 113, 113, 0.6); }\n.idle-cell.c-locked { background-color: rgba(45, 39, 74, 0.65); border-color: rgba(167, 139, 250, 0.6); }\n.idle-cell.c-cursed { background-color: rgba(56, 20, 52, 0.7); border-color: rgba(232, 121, 249, 0.65); }\n.idle-cell.c-void { background-color: rgba(10, 10, 20, 0.95); border-color: rgba(129, 140, 248, 0.7); }\n.idle-cell.c-boss { background-color: rgba(80, 55, 8, 0.75); border-color: rgba(251, 191, 36, 0.85); }\n\n/* detonation feedback */\n.idle-cell.fx-blast {\n background-color: rgba(94, 234, 212, 0.30);\n border-color: rgba(153, 246, 228, 0.85);\n}\n.idle-cell.fx-kill {\n background-color: rgba(251, 191, 36, 0.55);\n border-color: rgba(254, 240, 138, 0.95);\n}\n.idle-cell.fx-drone {\n background-color: rgba(248, 113, 113, 0.6);\n border-color: rgba(254, 202, 202, 0.95);\n}\n\n.idle-hits {\n position: absolute;\n bottom: 2px;\n right: 4px;\n font-size: 11px;\n font-weight: 900;\n color: rgba(255, 255, 255, 0.85);\n text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.9);\n}\n\n/* A drone sits on its target for a beat before the volley goes off, so the\n explosion has something to come from. */\n.idle-drone {\n position: absolute;\n font-size: 26px;\n text-shadow: 0px 0px 10px rgba(248, 113, 113, 0.9);\n}\n\n.idle-cell.armed {\n border-color: rgba(248, 113, 113, 0.95);\n background-color: rgba(80, 20, 20, 0.55);\n}\n\n/* golden chest */\n.idle-cell.golden {\n background-color: rgba(120, 82, 6, 0.85);\n border-color: #fde047;\n box-shadow: 0px 0px 26px rgba(253, 224, 71, 0.7);\n animation: idle-golden-pulse 0.55s ease-in-out infinite alternate;\n}\n\n@keyframes idle-golden-pulse {\n 0% { transform: scale(1.00); }\n 100% { transform: scale(1.10); }\n}\n\n.idle-golden { font-size: 32px; }\n\n.idle-golden-timer {\n position: absolute;\n bottom: 1px;\n font-size: 11px;\n font-weight: 900;\n color: #fef9c3;\n}\n\n/* floating values */\n.idle-float {\n position: absolute;\n font-size: 17px;\n font-weight: 900;\n color: #a7f3d0;\n text-shadow: 0px 2px 6px rgba(0, 0, 0, 0.9);\n pointer-events: none;\n}\n.idle-float.crit {\n font-size: 22px;\n color: #fde047;\n}\n\n.idle-callout {\n position: absolute;\n top: 40%;\n font-size: 34px;\n font-weight: 900;\n letter-spacing: 0.06em;\n color: #5eead4;\n text-shadow: 0px 0px 22px rgba(45, 212, 191, 0.8), 0px 3px 8px rgba(0, 0, 0, 0.9);\n pointer-events: none;\n}\n.idle-callout.big { color: #a7f3d0; }\n.idle-callout.mega { color: #fbbf24; font-size: 40px; }\n.idle-callout.crit { color: #fb923c; font-size: 44px; }\n\n/* \u2500\u2500 Controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-controls {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 16px;\n flex-shrink: 0;\n}\n\n.idle-overload {\n position: relative;\n overflow: hidden;\n width: 290px;\n padding-top: 14px;\n padding-bottom: 14px;\n border-radius: 14px;\n border: 3px solid rgba(248, 113, 113, 0.55);\n background-color: rgba(60, 12, 12, 0.9);\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n cursor: none;\n pointer-events: all;\n transition: transform 0.06s, border-color 0.12s;\n}\n\n.idle-overload.ready {\n border-color: #f87171;\n animation: idle-overload-pulse 1.4s ease-in-out infinite alternate;\n}\n.idle-overload.cooling { opacity: 0.62; cursor: none; }\n.idle-overload:active { transform: translateY(2px); }\n\n@keyframes idle-overload-pulse {\n 0% { box-shadow: 0px 0px 12px rgba(248, 113, 113, 0.25); }\n 100% { box-shadow: 0px 0px 34px rgba(248, 113, 113, 0.65); }\n}\n\n.idle-overload-fill {\n position: absolute;\n left: 0; top: 0; bottom: 0;\n background-color: rgba(248, 113, 113, 0.18);\n pointer-events: none;\n}\n\n.idle-overload-label {\n font-size: 20px;\n font-weight: 900;\n letter-spacing: 0.14em;\n color: #fecaca;\n}\n\n.idle-overload-sub {\n font-size: 10px;\n font-weight: 800;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: rgba(254, 202, 202, 0.6);\n}\n\n.idle-surge-wrap {\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: 300px;\n}\n\n.idle-surge-track {\n position: relative;\n height: 34px;\n border-radius: 9px;\n background-color: rgba(15, 23, 35, 0.92);\n border: 2px solid rgba(148, 163, 184, 0.24);\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.idle-surge-track.ready {\n border-color: #fb923c;\n animation: idle-overload-pulse 0.9s ease-in-out infinite alternate;\n}\n\n.idle-surge-fill {\n position: absolute;\n left: 0; top: 0; bottom: 0;\n background-color: rgba(251, 146, 60, 0.42);\n transition: width 0.2s;\n}\n\n.idle-surge-lbl {\n position: relative;\n font-size: 12px;\n font-weight: 900;\n letter-spacing: 0.16em;\n color: #fed7aa;\n}\n\n.idle-surge-btn {\n padding-top: 10px;\n padding-bottom: 10px;\n border-radius: 9px;\n border: 2px solid #fb923c;\n background-color: rgba(154, 52, 18, 0.85);\n color: #ffedd5;\n font-size: 14px;\n font-weight: 900;\n letter-spacing: 0.12em;\n align-items: center;\n justify-content: center;\n cursor: none;\n pointer-events: all;\n}\n.idle-surge-btn:hover { background-color: rgba(194, 65, 12, 0.95); }\n\n/* \u2500\u2500 Meltdown strip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-meltdown-strip {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 14px;\n width: 620px;\n padding: 10px 16px;\n border-radius: 12px;\n background-color: rgba(10, 17, 26, 0.85);\n border: 2px solid rgba(100, 116, 139, 0.18);\n flex-shrink: 0;\n}\n.idle-meltdown-strip.ready {\n border-color: rgba(251, 191, 36, 0.7);\n background-color: rgba(69, 26, 3, 0.6);\n}\n\n.idle-melt-icon { font-size: 20px; flex-shrink: 0; }\n.idle-melt-text {\n font-size: 12px;\n font-weight: 700;\n color: rgba(203, 213, 225, 0.85);\n flex-grow: 1;\n}\n.idle-meltdown-strip.ready .idle-melt-text { color: #fde68a; font-weight: 900; }\n\n.idle-melt-bar {\n margin-left: auto;\n width: 180px;\n height: 8px;\n border-radius: 4px;\n background-color: rgba(30, 41, 59, 0.9);\n overflow: hidden;\n}\n.idle-melt-fill {\n height: 100%;\n background-color: #fbbf24;\n border-radius: 4px;\n}\n\n.idle-melt-btn {\n margin-left: auto;\n padding: 8px 20px;\n border-radius: 9px;\n border: 2px solid #fbbf24;\n background-color: rgba(120, 53, 15, 0.9);\n color: #fef3c7;\n font-size: 12px;\n font-weight: 900;\n letter-spacing: 0.14em;\n cursor: none;\n pointer-events: all;\n}\n.idle-melt-btn:hover { background-color: rgba(180, 83, 9, 0.95); }\n\n/* \u2500\u2500 Right panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-tabs { display: flex; flex-direction: row; gap: 6px; flex-shrink: 0; }\n\n.idle-tab {\n flex-grow: 1;\n padding-top: 10px;\n padding-bottom: 10px;\n border-radius: 9px;\n border: 2px solid rgba(100, 116, 139, 0.22);\n background-color: rgba(12, 20, 30, 0.85);\n font-size: 18px;\n align-items: center;\n justify-content: center;\n cursor: none;\n pointer-events: all;\n opacity: 0.55;\n transition: opacity 0.1s, border-color 0.1s;\n}\n.idle-tab:hover { opacity: 0.85; }\n.idle-tab.on {\n opacity: 1;\n border-color: rgba(45, 212, 191, 0.7);\n background-color: rgba(13, 40, 44, 0.9);\n}\n.idle-tab.fluxtab.on { border-color: rgba(96, 165, 250, 0.8); background-color: rgba(15, 32, 60, 0.9); }\n\n.idle-buyrow {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n}\n\n.idle-buyrow-lbl {\n font-size: 10px;\n font-weight: 900;\n letter-spacing: 0.16em;\n color: rgba(148, 163, 184, 0.6);\n margin-right: 4px;\n}\n\n.idle-buy-amt {\n padding: 5px 14px;\n border-radius: 7px;\n border: 2px solid rgba(100, 116, 139, 0.22);\n background-color: rgba(12, 20, 30, 0.85);\n color: rgba(203, 213, 225, 0.7);\n font-size: 11px;\n font-weight: 900;\n cursor: none;\n pointer-events: all;\n}\n.idle-buy-amt.on {\n border-color: rgba(45, 212, 191, 0.75);\n color: #5eead4;\n background-color: rgba(13, 40, 44, 0.9);\n}\n\n/* height:0 + flex-grow is what makes this actually scroll instead of pushing\n its content off the bottom of the screen. */\n.idle-panel {\n flex-grow: 1;\n flex-shrink: 1;\n height: 0;\n overflow-y: scroll;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding-right: 8px;\n}\n\n/* \u2500\u2500 Upgrade rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-up {\n display: flex;\n flex-direction: row;\n gap: 12px;\n padding: 10px 12px;\n border-radius: 11px;\n background-color: rgba(10, 17, 26, 0.9);\n border: 2px solid rgba(100, 116, 139, 0.16);\n cursor: none;\n pointer-events: all;\n flex-shrink: 0;\n transition: border-color 0.1s, background-color 0.1s, transform 0.06s;\n}\n.idle-up:hover { background-color: rgba(18, 28, 42, 0.95); }\n.idle-up:active { transform: translateY(1px); }\n\n.idle-up.afford { border-color: rgba(45, 212, 191, 0.55); }\n.idle-up.afford:hover { border-color: rgba(94, 234, 212, 0.95); }\n.idle-up.poor { opacity: 0.62; }\n.idle-up.maxed { border-color: rgba(251, 191, 36, 0.45); opacity: 0.9; }\n.idle-up.locked { opacity: 0.35; cursor: none; }\n.idle-up.perk.afford { border-color: rgba(96, 165, 250, 0.7); }\n.idle-up.ach { cursor: none; }\n.idle-up.ach.done { border-color: rgba(52, 211, 153, 0.5); }\n\n.idle-up-icon {\n font-size: 26px;\n width: 34px;\n flex-shrink: 0;\n text-align: center;\n align-items: center;\n justify-content: center;\n}\n\n.idle-up-body {\n display: flex;\n flex-direction: column;\n gap: 3px;\n flex-grow: 1;\n}\n\n.idle-up-top {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.idle-up-name {\n font-size: 14px;\n font-weight: 900;\n color: #e2e8f0;\n}\n\n.idle-up-lvl {\n font-size: 11px;\n font-weight: 800;\n color: rgba(148, 163, 184, 0.7);\n}\n\n.idle-up-desc {\n font-size: 11px;\n font-weight: 600;\n color: rgba(148, 163, 184, 0.72);\n}\n\n.idle-up-bottom {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-top: 3px;\n}\n\n.idle-up-effect {\n font-size: 11px;\n font-weight: 800;\n color: rgba(94, 234, 212, 0.85);\n}\n\n.idle-up-cost {\n font-size: 13px;\n font-weight: 900;\n color: #cbd5e1;\n}\n.idle-up.afford .idle-up-cost { color: #5eead4; }\n.idle-up-cost.flux { color: #93c5fd; }\n.idle-up-cost.maxed { color: #fbbf24; }\n\n/* \u2500\u2500 Flux / log headers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-flux-head {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 12px 14px;\n border-radius: 11px;\n background-color: rgba(13, 25, 46, 0.9);\n border: 2px solid rgba(96, 165, 250, 0.3);\n flex-shrink: 0;\n}\n\n.idle-flux-amount {\n font-size: 22px;\n font-weight: 900;\n color: #93c5fd;\n}\n\n.idle-flux-sub {\n font-size: 11px;\n font-weight: 700;\n color: rgba(148, 163, 184, 0.75);\n}\n\n.idle-ach-reward {\n font-size: 11px;\n font-weight: 900;\n color: rgba(251, 191, 36, 0.9);\n}\n\n.idle-ach-bar {\n width: 100%;\n height: 6px;\n border-radius: 3px;\n background-color: rgba(30, 41, 59, 0.9);\n overflow: hidden;\n margin-top: 4px;\n}\n\n.idle-ach-fill {\n height: 100%;\n border-radius: 3px;\n background-color: rgba(94, 234, 212, 0.8);\n}\n\n.idle-ach-prog {\n font-size: 10px;\n font-weight: 800;\n color: rgba(148, 163, 184, 0.6);\n margin-top: 2px;\n}\n\n/* \u2500\u2500 Toasts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-toasts {\n position: absolute;\n top: 96px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n pointer-events: none;\n}\n\n.idle-toast {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 9px;\n padding: 8px 18px;\n border-radius: 10px;\n background-color: rgba(8, 14, 22, 0.94);\n border: 2px solid rgba(100, 116, 139, 0.35);\n}\n\n.idle-toast-icon { font-size: 17px; }\n.idle-toast-text { font-size: 13px; font-weight: 800; color: #e2e8f0; }\n\n.idle-toast.idle-toast-gold { border-color: rgba(253, 224, 71, 0.8); }\n.idle-toast.idle-toast-chain { border-color: rgba(94, 234, 212, 0.75); }\n.idle-toast.idle-toast-crit { border-color: rgba(251, 146, 60, 0.85); }\n.idle-toast.idle-toast-frag { border-color: rgba(167, 139, 250, 0.8); }\n.idle-toast.idle-toast-flux { border-color: rgba(96, 165, 250, 0.85); }\n.idle-toast.idle-toast-surge { border-color: rgba(251, 146, 60, 0.9); }\n.idle-toast.idle-toast-big { border-color: rgba(52, 211, 153, 0.85); }\n.idle-toast.idle-toast-achievement { border-color: rgba(251, 191, 36, 0.9); }\n.idle-toast.idle-toast-dim { opacity: 0.6; }\n\n/* \u2500\u2500 Modals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-modal-bg {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background-color: rgba(2, 4, 8, 0.86);\n pointer-events: all;\n}\n\n.idle-modal {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 640px;\n max-height: 88%;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n padding: 34px 40px;\n border-radius: 18px;\n background-color: rgba(9, 15, 24, 0.98);\n border: 3px solid rgba(45, 212, 191, 0.45);\n box-shadow: 0px 0px 70px rgba(13, 148, 136, 0.28);\n pointer-events: all;\n}\n\n.idle-modal.meltdown { border-color: rgba(251, 191, 36, 0.6); box-shadow: 0px 0px 70px rgba(180, 83, 9, 0.35); }\n.idle-modal.daily { border-color: rgba(167, 139, 250, 0.6); }\n.idle-modal.intro { width: 720px; }\n\n.idle-modal-eyebrow {\n font-size: 11px;\n font-weight: 900;\n letter-spacing: 0.30em;\n text-transform: uppercase;\n color: rgba(148, 163, 184, 0.55);\n}\n\n.idle-modal-title {\n font-size: 34px;\n font-weight: 900;\n letter-spacing: 0.06em;\n color: #5eead4;\n text-shadow: 0px 0px 24px rgba(45, 212, 191, 0.4);\n}\n.idle-modal.meltdown .idle-modal-title { color: #fbbf24; }\n.idle-modal.daily .idle-modal-title { color: #c4b5fd; }\n\n.idle-modal-sub {\n font-size: 13px;\n font-weight: 600;\n color: rgba(203, 213, 225, 0.75);\n text-align: center;\n max-width: 520px;\n margin-bottom: 8px;\n}\n\n.idle-modal-btn {\n margin-top: 14px;\n padding: 14px 40px;\n border-radius: 11px;\n border: 2px solid #2dd4bf;\n background-color: rgba(13, 78, 74, 0.9);\n color: #ccfbf1;\n font-size: 16px;\n font-weight: 900;\n letter-spacing: 0.16em;\n align-items: center;\n justify-content: center;\n cursor: none;\n pointer-events: all;\n}\n.idle-modal-btn:hover { background-color: rgba(15, 118, 110, 0.95); }\n\n.idle-modal-btn.ghost {\n border-color: rgba(148, 163, 184, 0.4);\n background-color: rgba(30, 41, 59, 0.7);\n color: rgba(203, 213, 225, 0.85);\n}\n.idle-modal-btn.danger {\n border-color: #fbbf24;\n background-color: rgba(146, 64, 14, 0.9);\n color: #fef3c7;\n}\n.idle-modal-btn.danger:hover { background-color: rgba(180, 83, 9, 0.95); }\n\n.idle-modal-btns { display: flex; flex-direction: row; gap: 14px; }\n\n/* offline report */\n.idle-offline-haul {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n padding: 18px 44px;\n border-radius: 14px;\n background-color: rgba(13, 40, 44, 0.7);\n border: 2px solid rgba(45, 212, 191, 0.35);\n margin-top: 6px;\n}\n\n.idle-offline-amount {\n font-size: 46px;\n font-weight: 900;\n color: #5eead4;\n text-shadow: 0px 0px 26px rgba(45, 212, 191, 0.5);\n}\n\n.idle-offline-lbl {\n font-size: 12px;\n font-weight: 900;\n letter-spacing: 0.20em;\n color: rgba(148, 163, 184, 0.7);\n}\n\n.idle-offline-extra {\n font-size: 13px;\n font-weight: 800;\n color: #c4b5fd;\n margin-top: 6px;\n}\n\n.idle-offline-warn {\n font-size: 12px;\n font-weight: 700;\n color: rgba(251, 191, 36, 0.9);\n text-align: center;\n max-width: 480px;\n margin-top: 8px;\n}\n\n/* daily */\n.idle-streak-row { display: flex; flex-direction: row; gap: 8px; margin-top: 6px; }\n\n.idle-streak-pip {\n width: 44px;\n height: 44px;\n border-radius: 10px;\n background-color: rgba(30, 41, 59, 0.8);\n border: 2px solid rgba(100, 116, 139, 0.25);\n align-items: center;\n justify-content: center;\n font-size: 15px;\n font-weight: 900;\n color: rgba(148, 163, 184, 0.6);\n}\n.idle-streak-pip.on {\n background-color: rgba(76, 29, 149, 0.6);\n border-color: rgba(167, 139, 250, 0.7);\n color: #ddd6fe;\n}\n.idle-streak-pip.today {\n border-color: #fbbf24;\n color: #fde68a;\n animation: idle-overload-pulse 1.1s ease-in-out infinite alternate;\n}\n\n.idle-daily-reward {\n display: flex;\n flex-direction: column;\n align-items: center;\n margin-top: 14px;\n}\n.idle-daily-mult {\n font-size: 44px;\n font-weight: 900;\n color: #fbbf24;\n}\n.idle-daily-lbl {\n font-size: 13px;\n font-weight: 800;\n color: rgba(203, 213, 225, 0.8);\n}\n\n/* meltdown */\n.idle-melt-payout {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 10px;\n padding: 14px 38px;\n border-radius: 14px;\n background-color: rgba(30, 41, 90, 0.6);\n border: 2px solid rgba(96, 165, 250, 0.45);\n}\n.idle-melt-payout-val { font-size: 44px; font-weight: 900; color: #93c5fd; }\n.idle-melt-payout-lbl { font-size: 15px; font-weight: 900; color: rgba(148, 163, 184, 0.85); }\n\n.idle-melt-cols {\n display: flex;\n flex-direction: row;\n gap: 16px;\n margin-top: 14px;\n width: 100%;\n}\n\n.idle-melt-col {\n flex-grow: 1;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 12px 14px;\n border-radius: 11px;\n background-color: rgba(15, 23, 35, 0.85);\n border: 2px solid rgba(100, 116, 139, 0.2);\n}\n.idle-melt-col.keep { border-color: rgba(52, 211, 153, 0.4); }\n.idle-melt-col.lose { border-color: rgba(248, 113, 113, 0.4); }\n\n.idle-melt-col-hd {\n font-size: 11px;\n font-weight: 900;\n letter-spacing: 0.16em;\n color: rgba(203, 213, 225, 0.8);\n margin-bottom: 4px;\n}\n\n.idle-melt-line {\n font-size: 12px;\n font-weight: 700;\n color: rgba(148, 163, 184, 0.85);\n}\n\n/* intro */\n.idle-intro-steps {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin-top: 10px;\n width: 100%;\n}\n\n.idle-intro-step {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 14px;\n padding: 10px 14px;\n border-radius: 11px;\n background-color: rgba(15, 23, 35, 0.8);\n border-left: 3px solid rgba(45, 212, 191, 0.6);\n}\n\n.idle-intro-num {\n width: 28px;\n height: 28px;\n flex-shrink: 0;\n border-radius: 50%;\n background-color: rgba(13, 78, 74, 0.9);\n border: 2px solid rgba(45, 212, 191, 0.6);\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 900;\n color: #99f6e4;\n}\n\n.idle-intro-txt {\n font-size: 13px;\n font-weight: 600;\n color: rgba(203, 213, 225, 0.9);\n}\n\n\n/* \u2500\u2500 Board shake \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* Own keyframes rather than reusing Gamehud's, so this stylesheet stands\n on its own if that file ever changes. */\n\n.idle-board.idle-shake-1 { animation: idle-shake-1 0.28s ease-out; }\n.idle-board.idle-shake-2 { animation: idle-shake-2 0.28s ease-out; }\n.idle-board.idle-shake-3 { animation: idle-shake-3 0.45s ease-out; }\n\n@keyframes idle-shake-1 {\n 0% { transform: translate(0px, 0px); }\n 30% { transform: translate(-2px, 1px); }\n 60% { transform: translate(2px, -1px); }\n 100% { transform: translate(0px, 0px); }\n}\n\n@keyframes idle-shake-2 {\n 0% { transform: translate(0px, 0px) rotate(0deg); }\n 20% { transform: translate(-4px, 2px) rotate(-0.3deg); }\n 45% { transform: translate(4px, -3px) rotate(0.3deg); }\n 70% { transform: translate(-3px, -1px) rotate(-0.2deg); }\n 100% { transform: translate(0px, 0px) rotate(0deg); }\n}\n\n@keyframes idle-shake-3 {\n 0% { transform: translate(0px, 0px) rotate(0deg) scale(1.00); }\n 15% { transform: translate(-7px, 4px) rotate(-0.6deg) scale(1.01); }\n 35% { transform: translate(7px, -5px) rotate(0.6deg) scale(1.02); }\n 55% { transform: translate(-5px, -3px) rotate(-0.4deg) scale(1.01); }\n 78% { transform: translate(4px, 2px) rotate(0.3deg) scale(1.00); }\n 100% { transform: translate(0px, 0px) rotate(0deg) scale(1.00); }\n}\n\n/* \u2500\u2500 Singularity \u2014 the second prestige layer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* Violet throughout, so nothing on screen can be mistaken for Flux blue or\n Meltdown amber. The layer is rare enough that it has to read instantly. */\n\n.idle-sing-strip {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 14px;\n width: 620px;\n padding: 10px 16px;\n border-radius: 12px;\n background-color: rgba(10, 17, 26, 0.85);\n border: 2px solid rgba(100, 116, 139, 0.18);\n flex-shrink: 0;\n}\n.idle-sing-strip.ready {\n border-color: rgba(192, 132, 252, 0.75);\n background-color: rgba(46, 16, 101, 0.6);\n}\n.idle-sing-strip.ready .idle-melt-text { color: #e9d5ff; font-weight: 900; }\n\n.idle-sing-fill {\n height: 100%;\n background-color: #c084fc;\n border-radius: 4px;\n}\n\n.idle-sing-btn {\n margin-left: auto;\n padding: 8px 20px;\n border-radius: 9px;\n border: 2px solid #c084fc;\n background-color: rgba(76, 29, 149, 0.9);\n color: #f3e8ff;\n font-size: 12px;\n font-weight: 900;\n letter-spacing: 0.14em;\n cursor: none;\n pointer-events: all;\n}\n.idle-sing-btn:hover { background-color: rgba(109, 40, 217, 0.95); }\n\n.idle-tab.singtab.on {\n border-color: rgba(192, 132, 252, 0.8);\n background-color: rgba(38, 18, 62, 0.9);\n}\n\n.idle-up.power.afford { border-color: rgba(192, 132, 252, 0.7); }\n.idle-up-cost.shard { color: #d8b4fe; }\n\n.idle-flux-head.sing {\n background-color: rgba(30, 15, 52, 0.9);\n border-color: rgba(192, 132, 252, 0.35);\n}\n.idle-flux-head.sing .idle-flux-amount { color: #d8b4fe; }\n\n.idle-sing-payout-val {\n font-size: 44px;\n font-weight: 900;\n color: #d8b4fe;\n}\n\n.idle-modal.singularity {\n border-color: rgba(192, 132, 252, 0.6);\n box-shadow: 0px 0px 70px rgba(109, 40, 217, 0.4);\n}\n.idle-modal.singularity .idle-modal-title {\n color: #d8b4fe;\n text-shadow: 0px 0px 24px rgba(168, 85, 247, 0.45);\n}\n\n.idle-modal-btn.sing {\n border-color: #c084fc;\n background-color: rgba(76, 29, 149, 0.9);\n color: #f3e8ff;\n}\n.idle-modal-btn.sing:hover { background-color: rgba(109, 40, 217, 0.95); }\n\n/* Board glows violet while a Twin Reactor / Cascade follow-up is in the air,\n so an extra volley reads as an event rather than as the board stuttering. */\n.idle-board.cascading {\n border-color: rgba(192, 132, 252, 0.65);\n box-shadow: 0px 0px 46px rgba(168, 85, 247, 0.35);\n}\n\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n DAILY CONTRACTS\n\n Violet, because contracts pay Fragments and Fragments are already violet\n everywhere else in this HUD \u2014 the currency chip, the condenser bar, the\n toast. Amber belongs to Meltdown and would have read as prestige.\n Green marks a finished contract, matching the \"collect\" green the offline\n and achievement toasts already use.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.idle-card-contracts { border-color: rgba(167, 139, 250, 0.28); }\n\n.idle-card-contracts.ready { border-color: rgba(52, 211, 153, 0.5); }\n\n.idle-card-contracts .idle-card-hd { color: rgba(196, 181, 253, 0.85); }\n\n.idle-card-hd.row {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.idle-card-hd-txt {\n font-size: 11px;\n font-weight: 900;\n letter-spacing: 0.18em;\n color: rgba(196, 181, 253, 0.85);\n}\n\n/* The parent .idle-card-hd is uppercased; a live countdown reading\n \"RESETS IN 5H 12M\" is a lot louder than it deserves to be. */\n.idle-card-hd-side {\n font-size: 9px;\n font-weight: 800;\n letter-spacing: 0.04em;\n text-transform: none;\n color: rgba(148, 163, 184, 0.6);\n}\n\n.idle-contract {\n display: flex;\n flex-direction: row;\n gap: 9px;\n padding: 7px 8px;\n border-radius: 9px;\n background-color: rgba(10, 14, 22, 0.75);\n border: 2px solid rgba(100, 116, 139, 0.16);\n cursor: none;\n pointer-events: all;\n transition: border-color 0.12s, background-color 0.12s, transform 0.06s;\n}\n.idle-contract:hover { background-color: rgba(20, 26, 38, 0.85); }\n\n.idle-contract.ready {\n border-color: rgba(52, 211, 153, 0.85);\n background-color: rgba(6, 46, 38, 0.6);\n animation: idle-contract-ready 1.1s ease-in-out infinite alternate;\n}\n.idle-contract.ready:active { transform: translateY(1px); }\n\n.idle-contract.claimed { opacity: 0.45; }\n\n@keyframes idle-contract-ready {\n 0% { box-shadow: 0px 0px 6px rgba(52, 211, 153, 0.18); }\n 100% { box-shadow: 0px 0px 20px rgba(52, 211, 153, 0.50); }\n}\n\n.idle-contract-icon {\n font-size: 19px;\n width: 24px;\n flex-shrink: 0;\n text-align: center;\n align-items: center;\n justify-content: center;\n}\n\n.idle-contract-body {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex-grow: 1;\n}\n\n.idle-contract-top {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n}\n\n.idle-contract-name {\n font-size: 12px;\n font-weight: 900;\n color: #e2e8f0;\n}\n\n.idle-contract-reward {\n font-size: 10px;\n font-weight: 900;\n color: rgba(196, 181, 253, 0.9);\n flex-shrink: 0;\n}\n.idle-contract.ready .idle-contract-reward { color: #6ee7b7; }\n.idle-contract.claimed .idle-contract-reward { color: rgba(148, 163, 184, 0.7); }\n\n.idle-contract-desc {\n font-size: 10px;\n font-weight: 600;\n color: rgba(148, 163, 184, 0.72);\n}\n\n.idle-contract-track {\n width: 100%;\n height: 5px;\n border-radius: 3px;\n background-color: rgba(30, 41, 59, 0.9);\n overflow: hidden;\n margin-top: 3px;\n}\n\n/* Same violet as the condenser bar \u2014 both are the road to a Fragment. */\n.idle-contract-fill {\n height: 100%;\n border-radius: 3px;\n background-color: #a78bfa;\n transition: width 0.25s;\n}\n.idle-contract.ready .idle-contract-fill { background-color: rgba(52, 211, 153, 0.85); }\n.idle-contract.claimed .idle-contract-fill { background-color: rgba(52, 211, 153, 0.7); }\n\n.idle-contract-prog {\n font-size: 9px;\n font-weight: 800;\n color: rgba(148, 163, 184, 0.55);\n}\n\n.idle-toast.idle-toast-contract { border-color: rgba(167, 139, 250, 0.8); }\n\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n NEXT GOAL PIN\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.idle-card-goal { border-color: rgba(94, 234, 212, 0.22); }\n\n.idle-goal-row {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 9px;\n}\n\n.idle-goal-icon {\n font-size: 20px;\n width: 26px;\n flex-shrink: 0;\n text-align: center;\n}\n\n.idle-goal-body { display: flex; flex-direction: column; flex-grow: 1; }\n\n.idle-goal-name { font-size: 12px; font-weight: 900; color: #e2e8f0; }\n\n.idle-goal-desc {\n font-size: 10px;\n font-weight: 600;\n color: rgba(148, 163, 184, 0.7);\n}\n\n.idle-bar-fill.goal { background-color: rgba(94, 234, 212, 0.85); }\n\n.idle-goal-foot {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.idle-goal-prog {\n font-size: 10px;\n font-weight: 800;\n color: rgba(148, 163, 184, 0.6);\n}\n\n.idle-goal-reward {\n font-size: 10px;\n font-weight: 900;\n color: rgba(251, 191, 36, 0.85);\n}\n\n\n/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n GUIDED WALKTHROUGH \u2014 coach marks over the live HUD\n\n The spotlight is a real cut-out: four dim quads that stop at the target\n rect, so the highlighted control stays fully lit AND fully clickable while\n everything else eats the click. There is no masking in the panel renderer,\n which is why it is built this way rather than as one sheet with a hole.\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */\n\n.rt-layer {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n pointer-events: none;\n z-index: 400;\n}\n\n/* Background scrim and click blocker. The actual visual dimming of HUD content\n is done by .tut-dim below \u2014 a scrim alone leaves every emoji punching\n through it at full brightness. */\n.rt-dim {\n position: absolute;\n /* Explicit origin: a collapsed quad is given only a width and a height,\n and an absolute panel with no anchor falls back to its static position. */\n left: 0;\n top: 0;\n background-color: rgba(2, 5, 10, 0.55);\n pointer-events: all;\n z-index: 1;\n}\n\n/* Applied to whichever HUD blocks are not the subject of the current step. */\n.tut-dim {\n opacity: 0.24;\n transition: opacity 0.22s;\n}\n\n.rt-ring {\n position: absolute;\n border-radius: 18px;\n border: 2px solid rgba(94, 234, 212, 0.75);\n pointer-events: none;\n z-index: 4;\n animation: rt-ring 1.9s ease-out infinite;\n}\n\n@keyframes rt-ring {\n 0% { transform: scale(1.00); opacity: 0.85; }\n 65% { transform: scale(1.07); opacity: 0.00; }\n 100% { transform: scale(1.07); opacity: 0.00; }\n}\n\n.rt-spot {\n position: absolute;\n border-radius: 16px;\n border: 3px solid rgba(94, 234, 212, 0.95);\n pointer-events: none;\n z-index: 5;\n animation: rt-spot 1.5s ease-in-out infinite alternate;\n}\n\n@keyframes rt-spot {\n 0% {\n border-color: rgba(94, 234, 212, 0.45);\n box-shadow: 0px 0px 14px rgba(45, 212, 191, 0.25);\n }\n 100% {\n border-color: rgba(167, 243, 208, 1.00);\n box-shadow: 0px 0px 46px rgba(45, 212, 191, 0.70);\n }\n}\n\n/* Corner brackets \u2014 the bit that makes it read as a targeting reticle rather\n than as a selected list item. */\n.rt-corner {\n position: absolute;\n width: 20px;\n height: 20px;\n animation: rt-corner 1.5s ease-in-out infinite alternate;\n}\n\n.rt-corner.tl { top: -7px; left: -7px; border-top: 4px solid #fde047; border-left: 4px solid #fde047; }\n.rt-corner.tr { top: -7px; right: -7px; border-top: 4px solid #fde047; border-right: 4px solid #fde047; }\n.rt-corner.bl { bottom: -7px; left: -7px; border-bottom: 4px solid #fde047; border-left: 4px solid #fde047; }\n.rt-corner.br { bottom: -7px; right: -7px; border-bottom: 4px solid #fde047; border-right: 4px solid #fde047; }\n\n@keyframes rt-corner {\n 0% { opacity: 0.55; transform: scale(0.90); }\n 100% { opacity: 1.00; transform: scale(1.00); }\n}\n\n.rt-card {\n position: absolute;\n width: 380px;\n padding: 16px 18px 14px 18px;\n border-radius: 16px;\n background-color: rgba(9, 15, 24, 0.98);\n border: 2px solid rgba(45, 212, 191, 0.45);\n box-shadow: 0px 0px 50px rgba(13, 148, 136, 0.28);\n display: flex;\n flex-direction: column;\n gap: 7px;\n pointer-events: all;\n z-index: 10;\n}\n\n.rt-card.in-a { animation: rt-card-in 0.24s ease-out; }\n.rt-card.in-b { animation: rt-card-in 0.24s ease-out; }\n\n@keyframes rt-card-in {\n 0% { opacity: 0; transform: scale(0.94) translateY(8px); }\n 100% { opacity: 1; transform: scale(1.00) translateY(0px); }\n}\n\n.rt-card-top {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.rt-eyebrow {\n font-size: 9px;\n font-weight: 900;\n letter-spacing: 0.28em;\n color: rgba(94, 234, 212, 0.7);\n}\n\n.rt-count {\n font-size: 10px;\n font-weight: 900;\n letter-spacing: 0.10em;\n color: rgba(148, 163, 184, 0.55);\n}\n\n.rt-title {\n font-size: 19px;\n font-weight: 900;\n color: #ccfbf1;\n text-shadow: 0px 0px 18px rgba(45, 212, 191, 0.35);\n}\n\n.rt-text {\n font-size: 13px;\n font-weight: 600;\n line-height: 1.45;\n color: rgba(203, 213, 225, 0.9);\n}\n\n.rt-dots {\n display: flex;\n flex-direction: row;\n gap: 4px;\n margin-top: 4px;\n margin-bottom: 2px;\n}\n\n.rt-dot {\n width: 16px;\n height: 4px;\n border-radius: 2px;\n background-color: rgba(100, 116, 139, 0.30);\n transition: background-color 0.2s, width 0.2s;\n}\n.rt-dot.done { background-color: rgba(45, 212, 191, 0.45); }\n.rt-dot.on { background-color: #5eead4; width: 24px; }\n\n.rt-next {\n margin-top: 4px;\n padding-top: 11px;\n padding-bottom: 11px;\n border-radius: 10px;\n border: 2px solid #2dd4bf;\n background-color: rgba(13, 78, 74, 0.9);\n color: #ccfbf1;\n font-size: 13px;\n font-weight: 900;\n letter-spacing: 0.16em;\n align-items: center;\n justify-content: center;\n cursor: none;\n pointer-events: all;\n transition: background-color 0.1s, transform 0.06s;\n}\n.rt-next:hover { background-color: rgba(15, 118, 110, 0.95); }\n.rt-next:active { transform: translateY(1px); }\n\n/* Shown instead of the button when the step is gated on an action \u2014 the two\n are mutually exclusive, so there is no way to click past the doing. */\n.rt-hint {\n margin-top: 4px;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 9px;\n padding: 10px 12px;\n border-radius: 10px;\n background-color: rgba(69, 26, 3, 0.65);\n border: 2px solid rgba(251, 191, 36, 0.65);\n animation: rt-hint 1.0s ease-in-out infinite alternate;\n}\n\n@keyframes rt-hint {\n 0% { border-color: rgba(251, 191, 36, 0.35); }\n 100% { border-color: rgba(253, 224, 71, 0.95); }\n}\n\n.rt-hint-pip { font-size: 15px; flex-shrink: 0; }\n\n.rt-hint-txt {\n font-size: 12px;\n font-weight: 900;\n letter-spacing: 0.04em;\n color: #fde68a;\n}\n\n.rt-foot {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n margin-top: 2px;\n}\n\n.rt-back, .rt-skip {\n padding: 5px 10px;\n border-radius: 7px;\n border: 2px solid rgba(0, 0, 0, 0);\n background-color: rgba(0, 0, 0, 0);\n font-size: 10px;\n font-weight: 800;\n letter-spacing: 0.08em;\n color: rgba(148, 163, 184, 0.6);\n cursor: none;\n pointer-events: all;\n transition: color 0.1s;\n}\n.rt-back:hover, .rt-skip:hover { color: rgba(226, 232, 240, 0.95); }\n.rt-back.hidden { opacity: 0; pointer-events: none; }\n\n\n/* \u2500\u2500 Help button in the top bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-help {\n width: 46px;\n height: 46px;\n flex-shrink: 0;\n border-radius: 10px;\n border: 2px solid rgba(148, 163, 184, 0.28);\n background-color: rgba(15, 23, 35, 0.9);\n color: rgba(226, 232, 240, 0.85);\n font-size: 20px;\n font-weight: 900;\n align-items: center;\n justify-content: center;\n cursor: none;\n pointer-events: all;\n transition: background-color 0.1s, border-color 0.1s;\n}\n.idle-help:hover {\n background-color: rgba(45, 212, 191, 0.18);\n border-color: rgba(45, 212, 191, 0.6);\n}\n.idle-help.on {\n border-color: rgba(45, 212, 191, 0.9);\n background-color: rgba(13, 78, 74, 0.9);\n color: #5eead4;\n}\n\n\n/* \u2500\u2500 Meltdown income preview \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.idle-melt-gain {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 10px;\n margin-top: 10px;\n padding: 9px 18px;\n border-radius: 11px;\n background-color: rgba(15, 23, 35, 0.85);\n border: 2px solid rgba(52, 211, 153, 0.35);\n}\n\n.idle-melt-gain-from {\n font-size: 18px;\n font-weight: 900;\n color: rgba(148, 163, 184, 0.8);\n}\n\n.idle-melt-gain-arrow {\n font-size: 16px;\n font-weight: 900;\n color: rgba(148, 163, 184, 0.6);\n}\n\n.idle-melt-gain-to {\n font-size: 22px;\n font-weight: 900;\n color: #6ee7b7;\n text-shadow: 0px 0px 16px rgba(52, 211, 153, 0.45);\n}\n\n.idle-melt-gain-lbl {\n font-size: 11px;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: rgba(148, 163, 184, 0.65);\n}\n"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "Game/2D/GameMode.cs",
"FileName": "GameMode.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "using Sandbox;\r\n\r\npublic enum GameMode\r\n{\r\n Wave,\r\n Survival,\r\n Idle, // Reactor mode \u2014 the self-running chain reaction economy\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "Game/Idle/IdlePet.cs",
"FileName": "IdlePet.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "using System;\n\n/// <summary>\n/// The pet is the Reactor's Fragment sink.\n///\n/// Cores buy upgrades that reset on Meltdown, and Flux buys perks that never do.\n/// The pet sits outside both: Fragments condense slowly, on a daily ceiling, and\n/// go into a permanent output multiplier that takes weeks rather than hours to\n/// finish. It is the long-horizon goal that survives every reset.\n/// </summary>\npublic static class IdlePet\n{\n\tpublic const int MaxLevel = 6;\n\n\t/// <summary>Fragments needed to go from <paramref name=\"level\"/> to the next one.</summary>\n\tpublic static int CostToUpgrade( int level ) => level switch\n\t{\n\t\t1 => 120,\n\t\t2 => 300,\n\t\t3 => 700,\n\t\t4 => 1_500,\n\t\t5 => 3_200,\n\t\t_ => 0,\n\t};\n\n\t/// <summary>Total Fragments to take a fresh pet all the way to level 6.</summary>\n\tpublic static int TotalCost\n\t{\n\t\tget\n\t\t{\n\t\t\tint total = 0;\n\t\t\tfor ( int lvl = 1; lvl < MaxLevel; lvl++ ) total += CostToUpgrade( lvl );\n\t\t\treturn total;\n\t\t}\n\t}\n\n\tpublic static bool IsMaxed( int level ) => level >= MaxLevel;\n\n\t/// <summary>Reactor output multiplier granted by the pet, before Pet Link.</summary>\n\tpublic static double OutputBonus( int level ) => 0.15 * Math.Max( 0, level - 1 );\n\n\tpublic static string Name( int level ) => level switch\n\t{\n\t\t1 => \"Mysterious Egg\",\n\t\t2 => \"Cat\",\n\t\t3 => \"Wolf\",\n\t\t4 => \"Tiger\",\n\t\t5 => \"Baby Drake\",\n\t\t_ => \"Dragon\",\n\t};\n\n\tpublic static string Emoji( int level ) => level switch\n\t{\n\t\t1 => \"\ud83e\udd5a\",\n\t\t2 => \"\ud83d\udc31\",\n\t\t3 => \"\ud83d\udc3a\",\n\t\t4 => \"\ud83d\udc2f\",\n\t\t5 => \"\ud83d\udc23\",\n\t\t_ => \"\ud83d\udc09\",\n\t};\n\n\tpublic static string Blurb( int level ) => level switch\n\t{\n\t\t1 => \"Something is moving in there. Feed it Fragments and find out what.\",\n\t\t2 => \"It watches the reactor and purrs at every chain.\",\n\t\t3 => \"Faster, hungrier, and it has started organising the drones.\",\n\t\t4 => \"Big enough now to shove chests into the blast radius itself.\",\n\t\t5 => \"Wings, and the beginnings of a temper.\",\n\t\t_ => \"Fully grown. The reactor runs hot around it.\",\n\t};\n}\n"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "Game/Idle/IdleSave.cs",
"FileName": "IdleSave.cs",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\n/// <summary>\n/// Persistent state for Reactor (Idle) mode.\n/// Serialized as a nested object inside PlayerSave \u2014 see PlayerSave.Idle.\n/// Everything here survives a Meltdown except what ResetForMeltdown() clears.\n/// </summary>\npublic class IdleSave\n{\n\t// \u2500\u2500 Unlock / onboarding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic bool Unlocked { get; set; } = false; // granted after the first finished run\n\tpublic bool IntroShown { get; set; } = false; // first-entry explainer\n\tpublic bool MeltdownSeen { get; set; } = false; // has the prestige panel ever been opened\n\n\t/// <summary>\n\t/// The guided walkthrough has run to the end, or been skipped. Separate from\n\t/// IntroShown so an existing save \u2014 which already has IntroShown set and has\n\t/// therefore never seen the walkthrough \u2014 still gets offered it once.\n\t/// </summary>\n\tpublic bool TutorialDone { get; set; } = false;\n\n\t/// <summary>The walkthrough has been offered and declined; do not re-prompt.</summary>\n\tpublic bool TutorialOffered { get; set; } = false;\n\n\t// \u2500\u2500 Currencies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic double Cores { get; set; } = 0; // spendable, wiped by Meltdown\n\tpublic double CycleCores { get; set; } = 0; // earned since last Meltdown \u2014 drives Flux payout\n\tpublic double AllTimeCores { get; set; } = 0; // never resets\n\tpublic int Flux { get; set; } = 0; // unspent prestige currency\n\tpublic int FluxEarned { get; set; } = 0; // lifetime, for milestones\n\tpublic int Meltdowns { get; set; } = 0;\n\tpublic int LastReactorRank { get; set; } = 0; // 0 == never ranked; drives the \"up N places\" note\n\n\t// \u2500\u2500 Singularity (second prestige) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic int Shards { get; set; } = 0; // unspent, buys Powers\n\tpublic int ShardsEarned { get; set; } = 0; // lifetime \u2014 drives the passive multiplier\n\tpublic int Singularities { get; set; } = 0;\n\tpublic bool SingularitySeen { get; set; } = false; // has the collapse panel ever been opened\n\n\t// \u2500\u2500 Upgrade levels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t// Keys are IdleUpgradeId.ToString(); missing key == level 0.\n\tpublic Dictionary<string, int> Upgrades { get; set; } = new();\n\tpublic Dictionary<string, int> Perks { get; set; } = new();\n\tpublic Dictionary<string, int> Powers { get; set; } = new();\n\n\t// \u2500\u2500 Offline bookkeeping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic long LastSeenUnix { get; set; } = 0;\n\tpublic double MeasuredCps { get; set; } = 0; // smoothed cores/sec, used for offline payout\n\tpublic double BestCps { get; set; } = 0;\n\n\t// Pending offline haul, staged so the welcome-back panel can show it\n\tpublic double PendingOfflineCores { get; set; } = 0;\n\tpublic double PendingOfflineSeconds { get; set; } = 0;\n\tpublic int PendingOfflineFrags { get; set; } = 0;\n\n\t// \u2500\u2500 Daily loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic string LastDailyDate { get; set; } = \"\";\n\tpublic int DailyStreak { get; set; } = 0;\n\tpublic bool DailyClaimed { get; set; } = false;\n\n\t// \u2500\u2500 Fragment drip (own bucket \u2014 does not touch the pet farm cap) \u2500\u2500\n\tpublic int TodayReactorFragments { get; set; } = 0;\n\tpublic string LastFragDate { get; set; } = \"\";\n\tpublic double FragProgress { get; set; } = 0; // cores banked toward the next fragment\n\n\t// \u2500\u2500 Records / milestones \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic int TotalDetonations { get; set; } = 0;\n\tpublic long TotalChestsBlown { get; set; } = 0;\n\tpublic int BestChain { get; set; } = 0;\n\tpublic double BestSingleBlast { get; set; } = 0;\n\tpublic int GoldenGrabbed { get; set; } = 0;\n\tpublic double SecondsRun { get; set; } = 0;\n\tpublic List<string> Achievements { get; set; } = new();\n\n\t// Counters that exist purely so Daily Contracts have something to watch.\n\t// They are lifetime totals; a contract stores its own baseline at issue.\n\tpublic int TotalOverloads { get; set; } = 0;\n\tpublic int TotalSurges { get; set; } = 0;\n\tpublic int TotalUpgradesBought { get; set; } = 0;\n\n\t/// <summary>Longest chain since the contract set was last issued. Reset with it.</summary>\n\tpublic int BestChainToday { get; set; } = 0;\n\n\t// \u2500\u2500 Daily contracts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t// Parallel lists rather than a list of objects: the save is plain JSON and\n\t// a schema change to a nested type would break every existing file.\n\tpublic string ContractDate { get; set; } = \"\";\n\tpublic List<string> ContractIds { get; set; } = new();\n\tpublic List<double> ContractTargets { get; set; } = new();\n\tpublic List<double> ContractBaseline { get; set; } = new();\n\tpublic List<bool> ContractClaimed { get; set; } = new();\n\n\t/// <summary>Consecutive days on which all three contracts were cleared.</summary>\n\tpublic int ContractStreak { get; set; } = 0;\n\tpublic string ContractStreakDate { get; set; } = \"\";\n\n\t// \u2500\u2500 Upgrade helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic int Level( IdleUpgradeId id )\n\t\t=> Upgrades.TryGetValue( id.ToString(), out int v ) ? v : 0;\n\n\tpublic void SetLevel( IdleUpgradeId id, int level )\n\t\t=> Upgrades[id.ToString()] = Math.Max( 0, level );\n\n\tpublic int PerkLevel( IdlePerkId id )\n\t\t=> Perks.TryGetValue( id.ToString(), out int v ) ? v : 0;\n\n\tpublic void SetPerkLevel( IdlePerkId id, int level )\n\t\t=> Perks[id.ToString()] = Math.Max( 0, level );\n\n\tpublic int PowerLevel( IdlePowerId id )\n\t\t=> Powers != null && Powers.TryGetValue( id.ToString(), out int v ) ? v : 0;\n\n\tpublic void SetPowerLevel( IdlePowerId id, int level )\n\t{\n\t\tPowers ??= new();\n\t\tPowers[id.ToString()] = Math.Max( 0, level );\n\t}\n\n\tpublic bool HasAchievement( string id ) => Achievements.Contains( id );\n\n\t// \u2500\u2500 Meltdown (prestige) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/// <summary>Wipes the run-scoped economy. Flux, perks, records and achievements stay.</summary>\n\tpublic void ResetForMeltdown()\n\t{\n\t\tCores = 0;\n\t\tCycleCores = 0;\n\t\tUpgrades.Clear();\n\t\tMeltdowns++;\n\n\t\t// Kickstart perk hands back a head start so the early grind shrinks each time\n\t\tint kick = PerkLevel( IdlePerkId.Kickstart );\n\t\tif ( kick > 0 )\n\t\t{\n\t\t\tSetLevel( IdleUpgradeId.ChestValue, kick * 6 );\n\t\t\tSetLevel( IdleUpgradeId.SpawnRate, kick * 4 );\n\t\t\tSetLevel( IdleUpgradeId.CycleSpeed, kick * 4 );\n\t\t\tSetLevel( IdleUpgradeId.DroneCount, Math.Min( kick, 3 ) );\n\t\t}\n\t}\n\n\t// \u2500\u2500 Singularity (second prestige) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\t/// <summary>\n\t/// Collapses everything a Meltdown normally protects \u2014 Flux, lifetime Flux\n\t/// and the whole perk tree. Shards, Powers, records, achievements, Fragments\n\t/// and the pet survive; those are what carry the run forward.\n\t///\n\t/// Meltdowns is deliberately kept: the Reactor Log counts it, and wiping a\n\t/// lifetime tally the player earned honestly reads as a bug, not a cost.\n\t/// </summary>\n\tpublic void ResetForSingularity()\n\t{\n\t\tFlux = 0;\n\t\tFluxEarned = 0;\n\t\tPerks.Clear();\n\n\t\t// Kickstart is gone with the perks, so ResetForMeltdown lands us at a\n\t\t// genuine level-zero board \u2014 which is the point of the layer.\n\t\tResetForMeltdown();\n\t\tMeltdowns--; // ResetForMeltdown counts a Meltdown that did not happen\n\n\t\tSingularities++;\n\t}\n\n\t// \u2500\u2500 Day rollover for the fragment bucket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\tpublic void EnsureFragDay()\n\t{\n\t\tstring today = DateTime.UtcNow.ToString( \"yyyy-MM-dd\" );\n\t\tif ( LastFragDate == today ) return;\n\t\tLastFragDate = today;\n\t\tTodayReactorFragments = 0;\n\t}\n\n\tpublic int FragmentsToday\n\t{\n\t\tget { EnsureFragDay(); return TodayReactorFragments; }\n\t}\n}\n"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "styles/form/_dropdown.scss",
"FileName": "_dropdown.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "\r\n$primary: red !default;\r\n$primary-alt: white !default;\r\n\r\n$switch-padding: 6px !default;\r\n\r\n.button.popupbutton.dropdown\r\n{\r\n\tcursor: pointer;\r\n\ttransition: all .1s ease-out;\r\n\tposition: relative;\r\n\r\n\t> .dropdown_indicator\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tright: 8px;\r\n\t}\r\n\r\n\t&.open\r\n\t{\r\n\t\tborder-bottom-left-radius: 1px;\r\n\t\tborder-bottom-right-radius: 1px;\r\n\t\ttransition: border-radius 0.2s ease-out;\r\n\t}\r\n}\r\n\r\nselect\r\n{\r\n\tmin-height: 40px;\r\n\r\n\t> option\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "ui/components/packagecard.razor.scss",
"FileName": "packagecard.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "@import \"/styles/_theme.scss\";\r\n$background-size: 8px;\r\n\r\nPackageCard\r\n{\r\n\tflex-shrink: 0;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tsound-in: \"ui.button.over\";\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tsound-in: \"ui.button.press\";\r\n\t}\r\n\r\n\tflex-direction: column;\r\n\tposition: relative;\r\n\tbackground-color: rgba( $default-950, 0 );\r\n\tborder-radius: $rounding-default;\r\n\ttransition: all 150ms ease;\r\n\tcursor: pointer;\r\n\tz-index: 0;\r\n\theight: 200px;\r\n\r\n\t.image\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tflex-shrink: 0;\r\n\t\ttransition: all 150ms ease;\r\n\t\tborder: 1px solid rgba( white, 0.025 );\r\n\t\taspect-ratio: 16 / 9;\r\n\t\tbackground-position: center;\r\n\t\tbackground-size: cover;\r\n\t\tborder-radius: $rounding-small;\r\n\t\tposition: relative;\r\n\t}\r\n\r\n\tcolumn\r\n\t{\r\n\t\tpadding: 8px 2px; // Optically aligned\r\n\t}\r\n\r\n\t\r\n\r\n\t&.list\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\theight: 64px;\r\n\t\tflex-direction: row;\r\n\t\tgap: 12px;\r\n\t\tpadding: 4px;\r\n\r\n\t\t.inner column\r\n\t\t{\r\n\t\t\tflex-grow: 1;\r\n\t\t}\r\n\r\n\t\t.image\r\n\t\t{\r\n\t\t\theight: 100%;\r\n\t\t\taspect-ratio: 1;\r\n\t\t\tflex-grow: 0;\r\n\t\t\tflex-shrink: 0;\r\n\t\t}\r\n\r\n\t\tcolumn\r\n\t\t{\r\n\t\t\tgap: 3px;\r\n\t\t\tjustify-content: center;\r\n\t\t}\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tflex-shrink: 0;\r\n\t\t\tmax-width: 500px;\r\n\t\t}\r\n\r\n\t\t.package-users\r\n\t\t{\r\n\t\t\ttop: 1px;\r\n\t\t\tright: 1px;\r\n\t\t}\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tbackground-color: $default-800;\r\n\t\t\ttransform: none;\r\n\r\n\t\t\t&::after\r\n\t\t\t{\r\n\t\t\t\tdisplay: none;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t&.wide\r\n\t{\r\n\t\theight: 250px;\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tmax-width: 300px;\r\n\t\t}\r\n\t}\r\n\r\n\t&.small\r\n\t{\r\n\t\theight: 200px;\r\n\t\taspect-ratio: 3/4;\r\n\r\n\t\t.image\r\n\t\t{\r\n\t\t\taspect-ratio: 1;\r\n\t\t}\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tmax-width: 140px;\r\n\t\t}\r\n\t}\r\n\r\n\t&.tall\r\n\t{\r\n\t\theight: 400px;\r\n\r\n\t\t.image\r\n\t\t{\r\n\t\t\taspect-ratio: 9/16;\r\n\t\t}\r\n\r\n\t\t.package-title\r\n\t\t{\r\n\t\t\tmax-width: 200px;\r\n\t\t}\r\n\t}\r\n\r\n\t.package-title\r\n\t{\r\n\t\ttext-overflow: ellipsis;\r\n\t\tmax-height: 24px;\r\n\t\tflex-shrink: 1;\r\n\t\tfont-size: 14px;\r\n\t}\r\n\r\n\t.package-users\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tbottom: 4px;\r\n\t\tright: 4px;\r\n\t\tbackground-color: rgba( 10, 40, 10, 0.95 );\r\n\t\tpadding: 3px 5px;\r\n\t\tborder-radius: 2px;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tgap: 3px;\r\n\t\tfont-size: 11px;\r\n\t\tcolor: #def;\r\n\t\tborder: 1px solid #252;\r\n\t\tcolor: #2f3;\r\n\r\n\t\t&:before\r\n\t\t{\r\n\t\t\tcontent: '\u25cf';\r\n\t\t\tfont-size: 0.5rem;\r\n\t\t}\r\n\t}\r\n\t// Hover effect (not for list)\r\n\t&:not(.list)\r\n\t{\r\n\t\t&::after\r\n\t\t{\r\n\t\t\tcontent: \"\";\r\n\t\t\tposition: absolute;\r\n\t\t\ttop: 0;\r\n\t\t\tleft: 0;\r\n\t\t\tbottom: 0;\r\n\t\t\tright: 0;\r\n\t\t\tbackground-color: rgba( $default-950, 0 );\r\n\t\t\ttransition: all 150ms ease;\r\n\t\t\tz-index: -10;\r\n\t\t\tborder-radius: $rounding-large;\r\n\t\t\tpointer-events: none;\r\n\t\t}\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\ttransform: scale( 1.05 );\r\n\r\n\t\t\t&::after\r\n\t\t\t{\r\n\t\t\t\tbackground-color: $default-800;\r\n\t\t\t\ttop: -$background-size;\r\n\t\t\t\tleft: -$background-size;\r\n\t\t\t\tright: -$background-size;\r\n\t\t\t\tbottom: -$background-size;\r\n\t\t\t\tbox-shadow: 0 0 25px rgba( black, 0.3 );\r\n\t\t\t}\r\n\r\n\t\t\tz-index: 100;\r\n\t\t\tbackground-color: $default-900;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n\r\n.package-card.list packageflairbar\r\n{\r\n\tdisplay: none;\r\n}\r\n\r\n.package-card.list .package-users\r\n{\r\n\tdisplay: none;\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "game/2d/ui/bombwheel.razor.scss",
"FileName": "bombwheel.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "root {\n /* Declared so no panel in this tree ever claims the OS pointer -- a hovered\n panel outranks Mouse.CursorType, and one that declares nothing still claims\n the default arrow. GameCursor draws the cursor instead. */\n cursor: none;\n flex-grow:1; width:100%; height:100%;\n pointer-events:none; position:relative;\n overflow:visible;\n}\n\n.mini-hand {\n position:absolute;\n bottom:15px;\n left:0; right:0px; /* right edge = sidebar width */\n display:flex; flex-direction:row; gap:14px; align-items:flex-end;\n justify-content:center; /* centers within the grid area, not full viewport */\n pointer-events:all;\n z-index:30;\n}\n\n.mini-card {\n width:80px;\n height: 80px;\n background-color:rgba(31,27,83,0.85); /* more opaque \u2014 was barely visible */\n border:2px solid rgba(255,255,255,0.18);\n border-radius:16px; padding:16px 10px 12px;\n display:flex; flex-direction:column; align-items:center; gap:10px;\n cursor: none; position:relative;\n transition:background-color 0.1s, border-color 0.1s, transform 0.1s;\n}\n.mini-card:hover { background-color:rgba(117,110,194,0.75); transform:translateY(-5px); }\n.mini-card.active {\n background-color:rgba(83,74,183,0.90);\n border-color:#a78bfa;\n border-width:3px;\n transform:translateY(-16px);\n}\n.mini-card.used {\n background-color:rgba(29,158,117,0.28);\n border-color:rgba(29,158,117,0.60);\n opacity:0.75;\n cursor: none;\n}\n\n.mini-icon { font-size:30px; position: relative; bottom: 4px;} /* was 40px \u2014 easier to see */\n.mini-name { font-size:13px; font-weight:800; color:rgba(255,255,255,0.80); }\n.mini-card.active .mini-name { color:#fff; font-size:15px; }\n\n.mini-badge {\n position:absolute; top:5px; right:7px;\n font-size:12px; font-weight:700;\n color:rgba(80,220,140,0.95);\n background-color:rgba(29,158,117,0.40);\n border-radius:4px; padding:0 5px;\n}\n\n.kb-hint {\n position:absolute; top:5px; left:7px;\n font-size:12px; font-weight:800;\n color:rgba(255,255,255,0.45);\n background-color:rgba(255,255,255,0.10);\n border-radius:4px; padding:0 6px;\n}\n.mini-card.active .kb-hint {\n color:rgba(255,255,255,0.80);\n background-color:rgba(255,255,255,0.18);\n}\n\n.mini-det-wrap { align-self:flex-end; }\n.mini-det {\n width:90px; /* was 90px */\n background:linear-gradient(135deg,#7c3aed,#a855f7);\n border:2px solid rgba(167,139,250,0.40);\n border-radius:14px; \n padding:12px 10px 5px;\n display:flex; flex-direction:column; align-items:center; gap:5px;\n cursor: none;\n transition:opacity 0.15s;\n}\n.mini-det-wrap.ready .mini-det { border-color:#a78bfa; }\n.mini-det-wrap.off .mini-det { opacity:0.25; pointer-events:none; }\n.mini-det-icon { font-size:28px; }\n.mini-det-label { font-size:13px; font-weight:800; color:#fff; letter-spacing:0.08em; }\n\n/* SCREEN SHAKE */\n.mini-hand.shake-small { animation: shake-small 0.25s ease-out; }\n.mini-hand.shake-medium { animation: shake-medium 0.35s ease-out; }\n.mini-hand.shake-strong { animation: shake-strong 0.45s ease-out; }\n.mini-hand.shake-insane { animation: shake-insane 0.55s ease-out; }\n\n@keyframes shake-small {\n 0%,100% { transform: translate(0,0); }\n 25% { transform: translate(-2px, 1px); }\n 50% { transform: translate(2px, -1px); }\n 75% { transform: translate(-1px, 2px); }\n}\n@keyframes shake-medium {\n 0%,100% { transform: translate(0,0); }\n 20% { transform: translate(-4px, 2px) rotate(-0.3deg); }\n 40% { transform: translate(4px,-3px) rotate(0.3deg); }\n 60% { transform: translate(-3px, 3px) rotate(-0.2deg); }\n 80% { transform: translate(3px,-2px) rotate(0.2deg); }\n}\n@keyframes shake-strong {\n 0%,100% { transform: translate(0,0); }\n 15% { transform: translate(-6px, 3px) rotate(-0.5deg); }\n 30% { transform: translate(6px,-4px) rotate(0.5deg); }\n 45% { transform: translate(-5px, 5px) rotate(-0.4deg); }\n 60% { transform: translate(5px,-4px) rotate(0.4deg); }\n 75% { transform: translate(-3px, 2px); }\n 90% { transform: translate(2px,-2px); }\n}\n@keyframes shake-insane {\n 0%,100% { transform: translate(0,0); }\n 10% { transform: translate(-9px, 5px) rotate(-0.7deg); }\n 20% { transform: translate(9px,-6px) rotate(0.7deg); }\n 30% { transform: translate(-8px, 7px) rotate(-0.6deg); }\n 40% { transform: translate(8px,-6px) rotate(0.6deg); }\n 50% { transform: translate(-6px, 5px) rotate(-0.4deg); }\n 60% { transform: translate(6px,-5px) rotate(0.4deg); }\n 70% { transform: translate(-4px, 3px); }\n 80% { transform: translate(3px,-3px); }\n 90% { transform: translate(-1px, 1px); }\n}\n\n/* Lock icon, if random event Bomb jam hit */\n.mini-jam-overlay {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background-color: rgba(239,68,68,0.25);\n border-radius: 14px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 28px;\n pointer-events: none;\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "game/2d/ui/gamehud.razor.scss",
"FileName": "gamehud.razor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "root {\n /* Declared so no panel in this tree ever claims the OS pointer -- a hovered\n panel outranks Mouse.CursorType, and one that declares nothing still claims\n the default arrow. GameCursor draws the cursor instead. */\n cursor: none;\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n pointer-events: none;\n overflow: hidden;\n}\n\n/* TOP HUD */\n.topbar {\n position: absolute;\n top: 0; left: 0; right: 0;\n height: 90px;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-evenly;\n border-bottom: 1px solid rgba(167,139,250,0.15);\n z-index: 10;\n background-color: rgba(18,8,42,0.97);\n animation: topbar-shift 20s ease-in-out infinite alternate;\n overflow: hidden;\n z-index: 11;\n}\n\n@keyframes topbar-shift {\n 0% { background-color: rgba(18,8,42,0.97); }\n 25% { background-color: rgba(8,24,48,0.97); }\n 50% { background-color: rgba(24,8,40,0.97); }\n 75% { background-color: rgba(8,18,50,0.97); }\n 100% { background-color: rgba(20,6,38,0.97); }\n}\n.hud-block {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n}\n.hud-lbl {\n font-size: 22px;\n font-weight: 700;\n letter-spacing: 0.12em;\n color: rgba(255,255,255,0.30);\n text-transform: uppercase;\n}\n.hud-val {\n font-size: 32px;\n font-weight: 900;\n color: white;\n}\n.hud-block.wave .hud-val { color: #a78bfa; }\n.hud-block.score .hud-val { color: #34d399; }\n.hud-block.best .hud-val { color: #fbbf24; }\n.hud-sep {\n width: 1px;\n height: 120px;\n background-color: rgba(128,95,128,0.07);\n}\n\n/* TIMER */\n.timer-track {\n position: absolute;\n top: 90px; left: 0; right: 0;\n height: 14px;\n background-color: rgba(255,255,255,0.06);\n z-index: 10;\n}\n.timer-fill {\n position: absolute;\n top: 0; left: 0; bottom: 0;\n background-color: #34d399;\n transition: width 0.25s linear, background-color 0.8s ease;\n min-width: 4px;\n}\n\n.timer-fill.mid {\n background-color: #a78bfa; /* half time: purple */\n transition: width 0.25s linear, background-color 0.6s ease;\n}\n.timer-fill.hurry {\n background-color: #f59e0b; /* 30%: amber */\n transition: width 0.25s linear, background-color 0.5s ease;\n}\n.timer-fill.panic {\n background-color: #ef4444; /* 15%: red */\n transition: width 0.25s linear, background-color 0.3s ease;\n animation: pulse-bar 0.55s ease-in-out infinite alternate;\n}\n@keyframes pulse-bar {\n from { opacity: 1.0; }\n to { opacity: 0.5; }\n}\n/* \n Three-stage color shift:\n full \u2192 hurry : purple \u2192 amber (feels warm, attention-grabbing)\n hurry \u2192 panic : amber \u2192 red (urgent)\n panic : red + pulse animation\n*/\n\n/* Challenge sentence \u2014 between timer and grid */\n\n.challenge-bar {\n position: absolute;\n top: 104px; \n left: 0;\n right: 0px;\n height: 36px;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 10px;\n background-color: rgba(13,8,32,0.80);\n border-bottom: 1px solid rgba(167,139,250,0.12);\n pointer-events: none;\n z-index: 9;\n transition: background-color 0.4s ease;\n}\n.challenge-bar.done {\n background-color: rgba(16,42,16,0.85);\n border-bottom-color: rgba(52,211,153,0.25);\n}\n\n.ch-icon {\n font-size: 16px;\n color: rgba(167,139,250,0.70);\n}\n.challenge-bar.done .ch-icon {\n color: #34d399;\n}\n\n.ch-label {\n font-size: 20px;\n font-weight: 700;\n color: rgba(255,255,255,0.55);\n letter-spacing: 0.04em;\n}\n.challenge-bar.done .ch-label {\n color: rgba(52,211,153,0.85);\n}\n\n.ch-bonus {\n font-size: 13px;\n font-weight: 800;\n color: rgba(251,191,36,0.60);\n background-color: rgba(251,191,36,0.08);\n border-radius: 5px;\n padding: 2px 7px;\n}\n.challenge-bar.done .ch-bonus {\n color: rgba(52,211,153,0.80);\n background-color: rgba(52,211,153,0.10);\n}\n\n/* CHAIN TRACK \u2014 above grid, pointer-events none */\n.chain-track {\n position: absolute;\n top: 146px;\n left: 0;\n right: 0px;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 8px;\n pointer-events: none;\n z-index: 15;\n}\n.chain-track-inner {\n background-color: rgba(13,8,32,0.75);\n border-radius: 14px;\n padding: 10px 20px;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n}\n.step {\n background-color: rgba(255,255,255,0.05);\n color: rgba(255,255,255,0.25);\n border-radius: 10px;\n padding: 6px 17px;\n font-size: 15px;\n border: 1.5px solid rgba(255,255,255,0.06);\n transition: all 0.15s;\n}\n.step.lit {\n background-color: rgba(167,139,250,0.2);\n color: #c4b5fd;\n border-color: #7c3aed;\n transform: scale(1.15);\n}\n.arr { font-size: 15px; color: rgba(255,255,255,0.15); }\n\n/* SIDEBAR */\n.sidebar {\n position: absolute;\n top: 140px; right: 0; bottom: 0;\n width: 260px;\n background-color: rgba(18,8,42,0.97);\n border-left: 2px solid rgba(167,139,250,0.18);\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 16px;\n pointer-events: all;\n z-index: 10;\n}\n.divider {\n height: 1px;\n background-color: rgba(167,139,250,0.10);\n}\n.sbl {\n font-size: 12px; font-weight: 800;\n letter-spacing: 0.2em; text-transform: uppercase;\n color: rgba(166,154,219,0.5);\n}\n\n/* Legend */\n.legend { display: flex; flex-direction: column; gap: 8px; }\n.leg { display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 14px; color: rgba(255,255,255,0.38); font-weight: 600; }\n.dot { width: 10px; height: 10px; border-radius: 2px; }\n.dot.p0 { background-color: #60a5fa; }\n.dot.p1 { background-color: #34d399; }\n.dot.p2 { background-color: #fb923c; }\n.dot.red { background-color: #f87171; }\n.dot.chain { background-color: #c4b5fd; }\n\n/* Controls */\n.controls-hint {\n display: flex; flex-direction: column; gap: 6px;\n font-size: 13px; color: rgba(255,255,255,0.30); font-weight: 500;\n}\n\n.spacer { flex-grow: 1; }\n\n/* Message bottom bar \u2014 pointer-events none, no text-align */\n.msg {\n position: absolute;\n bottom: 8px;\n left: 12px;\n background-color: rgba(4,1,12,1.0);\n border: 1px solid rgba(167,139,250,0.15);\n border-radius: 8px;\n padding: 6px 14px;\n font-size: 18px;\n color: rgba(255,255,255,0.55);\n pointer-events: none;\n font-weight: 600;\n z-index: 5;\n}\n/* GAME OVER */\n/* \u2500\u2500 Three column layout \u2500\u2500 */\n.ov-bg {\n position: absolute;\n top: 0; left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(4,1,12,1.0);\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 24px;\n pointer-events: all;\n z-index: 50;\n overflow: hidden;\n}\n\n.ov-side-panel {\n display: flex;\n flex-direction: column;\n gap: 16px;\n width: 220px;\n pointer-events: none;\n z-index: 3;\n max-height: 92vh;\n overflow-y: auto;\n}\n\n.ov-side-card {\n position: relative; /* needed for ::before */\n background-color: rgba(8,3,22,0.90);\n border: 1px solid rgba(139,92,246,0.18);\n border-radius: 16px;\n padding: 16px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n overflow: hidden;\n}\n\n.ov-side-card.journey {\n gap: 4px;\n}\n/* Streak card \u2014 orange glow line */\n.ov-side-card.streak::before {\n background-color: rgba(251,146,60,0.55);\n}\n\n.ov-side-card::before {\n content: \"\";\n position: absolute;\n top: -1px; left: 20%; right: 20%;\n height: 1px;\n background-color: rgba(139,92,246,0.40);\n border-radius: 50%;\n}\n\n.ov-side-card.streak {\n border-color: rgba(251,146,60,0.30);\n background-color: rgba(251,146,60,0.05);\n}\n\n.ov-side-card-title {\n font-size: 14px;\n font-weight: 900;\n color: rgb(255, 214, 161);\n letter-spacing: 0.22em;\n text-transform: uppercase;\n width: 100%;\n justify-content: center;\n margin-bottom: 8px;\n gap: 3px;\n}\n\n/* \u2500\u2500 Personal best bar \u2500\u2500 */\n.ov-pb-bar-wrap { width: 100%; }\n\n.ov-pb-bar-track {\n width: 100%;\n height: 6px;\n background-color: rgba(255,255,255,0.06);\n border-radius: 3px;\n position: relative;\n}\n\n.ov-pb-bar-fill {\n position: absolute;\n top: 0; left: 0; bottom: 0;\n background-color: #a78bfa;\n border-radius: 3px;\n min-width: 4px;\n}\n\n.ov-pb-marker {\n position: absolute;\n top: -3px;\n width: 2px;\n height: 12px;\n background-color: rgba(251,191,36,0.80);\n border-radius: 1px;\n}\n\n.ov-pb-scores {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 2px;\n width: 100%;\n}\n\n.ov-pb-score-current {\n font-size: 22px;\n font-weight: 900;\n color: #a78bfa;\n}\n\n.ov-pb-score-best {\n font-size: 11px;\n font-weight: 600;\n color: rgba(255,255,255,0.30);\n}\n\n.ov-pb-newbest {\n font-size: 12px;\n font-weight: 900;\n color: #fbbf24;\n background-color: rgba(251,191,36,0.10);\n border: 1px solid rgba(251,191,36,0.25);\n border-radius: 8px;\n padding: 3px 10px;\n}\n\n/* Game over background Squares */\n@keyframes sq-float-1 {\n 0% { margin-top: 0px; margin-left: 0px; }\n 25% { margin-top: -14px; margin-left: 8px; }\n 50% { margin-top: -6px; margin-left: -10px; }\n 75% { margin-top: 10px; margin-left: 4px; }\n 100% { margin-top: 0px; margin-left: 0px; }\n}\n\n@keyframes sq-float-2 {\n 0% { margin-top: 0px; margin-left: 0px; }\n 30% { margin-top: 12px; margin-left: -8px; }\n 60% { margin-top: -8px; margin-left: 12px; }\n 100% { margin-top: 0px; margin-left: 0px; }\n}\n\n@keyframes sq-pulse {\n 0%,100% { opacity: 0.4; }\n 50% { opacity: 1.0; }\n}\n\n.ov-bg-squares {\n position: absolute;\n top: 0; left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n z-index: 1;\n}\n\n.ov-bg-sq {\n position: absolute;\n border: 1.5px solid;\n}\n\n@keyframes sq-drift-a {\n 0% { margin-top: 0px; margin-left: 0px; opacity: 0.35; }\n 20% { margin-top: -12px; margin-left: 8px; opacity: 0.80; }\n 40% { margin-top: -20px; margin-left: -6px; opacity: 0.50; }\n 60% { margin-top: -8px; margin-left: -14px; opacity: 0.90; }\n 80% { margin-top: 6px; margin-left: 4px; opacity: 0.45; }\n 100% { margin-top: 0px; margin-left: 0px; opacity: 0.35; }\n}\n\n@keyframes sq-drift-b {\n 0% { margin-top: 0px; margin-left: 0px; opacity: 0.25; }\n 25% { margin-top: 14px; margin-left: -10px; opacity: 0.70; }\n 50% { margin-top: 6px; margin-left: 16px; opacity: 0.90; }\n 75% { margin-top: -10px; margin-left: 8px; opacity: 0.40; }\n 100% { margin-top: 0px; margin-left: 0px; opacity: 0.25; }\n}\n\n.ov-bg-sq.sq-a {\n animation-name: sq-drift-a;\n animation-timing-function: ease-in-out;\n animation-iteration-count: infinite;\n}\n\n.ov-bg-sq.sq-b {\n animation-name: sq-drift-b;\n animation-timing-function: ease-in-out;\n animation-iteration-count: infinite;\n}\n\n/* Alternate float pattern on even squares */\n.ov-bg-sq:nth-child(even) {\n animation-name: sq-float-2, sq-pulse;\n}\n\n/* \u2500\u2500 Journey \u2500\u2500 */\n.ov-journey-wave {\n font-size: 32px;\n font-weight: 900;\n color: white;\n gap: 3px;\n}\n\n.ov-journey-sub {\n font-size: 12px;\n font-weight: 600;\n color: rgba(167, 255, 179, 0.719);\n word-spacing: 1px;\n width: 100%;\n display: flex;\n flex-direction: row;\n justify-content: space-between; /* \u2190 pushes label left, value right */\n}\n\n.ov-journey-tag {\n font-size: 10px;\n font-weight: 800;\n color: rgba(255, 255, 255, 0.562);\n background-color: rgba(255,255,255,0.04);\n border-radius: 6px;\n padding: 3px 10px;\n}\n\n.ov-journey-tag.good {\n color: #34d399;\n background-color: rgba(52,211,153,0.08);\n}\n\n/* \u2500\u2500 Streak \u2500\u2500 */\n.ov-streak-count {\n font-size: 48px;\n font-weight: 900;\n color: #fb923c;\n}\n\n.ov-streak-label {\n font-size: 11px;\n font-weight: 700;\n color: rgba(255, 199, 125, 0.404);\n}\n\n/* \u2500\u2500 Next unlock \u2500\u2500 */\n.ov-unlock-icon { font-size: 32px; }\n\n.ov-unlock-name {\n font-size: 13px;\n font-weight: 800;\n color: rgba(255,255,255,0.75);\n width: 100%;\n text-align: center;\n justify-content: center;\n margin-bottom: 2px;\n}\n\n.ov-unlock-cost {\n font-size: 12px;\n flex-direction: row;\n font-weight: 700;\n color: rgba(129,140,248,0.70);\n width: 100%;\n justify-content: center;\n text-align: center;\n gap: 3px; /* adds space b/w elements */\n}\n\n.ov-unlock-ready {\n font-size: 12px;\n font-weight: 900;\n color: #34d399;\n}\n\n.ov-unlock-bar-track {\n width: 100%;\n height: 4px;\n background-color: rgba(255,255,255,0.06);\n border-radius: 2px;\n position: relative;\n margin-top: 4px;\n}\n\n.ov-unlock-bar-fill {\n position: absolute;\n top: 0; left: 0; bottom: 0;\n background-color: #818cf8;\n border-radius: 2px;\n min-width: 4px;\n}\n\n.ov-particles {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n pointer-events: none;\n overflow: hidden;\n z-index: 2;\n}\n.ov-particle {\n position: absolute;\n bottom: -20px;\n opacity: 0;\n animation-name: particle-rise;\n animation-timing-function: ease-in;\n animation-iteration-count: infinite;\n}\n@keyframes particle-rise {\n 0% { opacity: 0; transform: translateY(0px); }\n 15% { opacity: 1; }\n 80% { opacity: 0.7; transform: translateY(-65vh); }\n 100% { opacity: 0; transform: translateY(-105vh); }\n}\n\n.ov-box {\n position: relative;\n background-color: rgba(8,3,22,0.98);\n border: 1px solid rgba(139,92,246,0.25);\n border-radius: 24px;\n padding: 2rem 2.2rem 2.2rem 2.2rem;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n width: 600px;\n max-height: 92vh;\n overflow-y: auto;\n z-index: 3;\n box-shadow: 0px 0px 0px 1px rgba(139,92,246,0.08),\n 0px 2px 0px rgba(139,92,246,0.15),\n 0px 24px 60px rgba(0s,0,0,0.80),\n inset 0px 1px 0px rgba(255,255,255,0.04);\n}\n\n/* Subtle top glow on the box */\n.ov-box::before {\n content: \"\";\n position: absolute;\n top: -1px; left: 15%; right: 15%;\n height: 1px;\n background-color: rgba(167,139,250,0.50);\n border-radius: 50%;\n}\n\n.ov-wave-big {\n font-size: 64px;\n font-weight: 900;\n color: #a78bfa;\n letter-spacing: 0.06em;\n animation: ov-section-in 0.6s cubic-bezier(0.22,1,0.36,1) 0.3s both;\n}\n\n.ov-stats {\n display: flex;\n flex-direction: row;\n gap: 8px;\n flex-shrink: 0;\n width: 100%;\n justify-content: center;\n}\n\n.ov-stat {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 5px;\n background-color: rgba(255,255,255,0.02);\n border-radius: 14px;\n padding: 16px 8px 12px 8px;\n position: relative;\n overflow: hidden;\n}\n\n.ov-stat::before {\n content: \"\";\n position: absolute;\n top: 0; left: 0; right: 0;\n height: 2px;\n}\n\n.ov-stat-icon { font-size: 18px; opacity: 0.70; }\n\n.ov-stat-val {\n font-size: 36px;\n font-weight: 900;\n letter-spacing: -0.03em;\n line-height: 1;\n}\n\n.ov-stat-lbl {\n font-size: 11px;\n font-weight: 700;\n color: rgba(255, 255, 255, 0.575);\n text-transform: uppercase;\n letter-spacing: 0.14em;\n}\n\n.ov-stat:nth-child(1) {\n border: 1px solid rgba(52,211,153,0.18);\n background-color: rgba(52,211,153,0.06);\n}\n.ov-stat:nth-child(2) {\n border: 1px solid rgba(167,139,250,0.18);\n background-color: rgba(167,139,250,0.06);\n}\n.ov-stat:nth-child(3) {\n border: 1px solid rgba(251,191,36,0.18);\n background-color: rgba(251,191,36,0.06);\n}\n\n/* Color accent per stat \u2014 target each by position */\n.ov-stat:nth-child(1)::before { background-color: #34d399; }\n.ov-stat:nth-child(2)::before { background-color: #a78bfa; }\n.ov-stat:nth-child(3)::before { background-color: #fbbf24; }\n\n.ov-stat:nth-child(1) { border: 1px solid rgba(52,211,153,0.12); }\n.ov-stat:nth-child(2) { border: 1px solid rgba(167,139,250,0.12); }\n.ov-stat:nth-child(3) { border: 1px solid rgba(251,191,36,0.12); }\n\n/* \u2500\u2500 Stat label colors \u2500\u2500 */\n.ov-stat:nth-child(1) .ov-stat-val { color: #6ee7b7; }\n.ov-stat:nth-child(2) .ov-stat-val { color: #d8b4fe; }\n.ov-stat:nth-child(3) .ov-stat-val { color: #fde68a; }\n\n\n/* Background orbs still game over (ov)*/\n@keyframes orb-drift-1 {\n 0% { margin-left: 0px; margin-top: 0px; }\n 33% { margin-left: 18px; margin-top: -12px; }\n 66% { margin-left: -10px; margin-top: 20px; }\n 100% { margin-left: 0px; margin-top: 0px; }\n}\n\n@keyframes orb-drift-2 {\n 0% { margin-left: 0px; margin-top: 0px; }\n 40% { margin-left: -20px; margin-top: 14px; }\n 70% { margin-left: 12px; margin-top: -8px; }\n 100% { margin-left: 0px; margin-top: 0px; }\n}\n\n@keyframes orb-drift-3 {\n 0% { margin-left: 0px; margin-top: 0px; }\n 30% { margin-left: 14px; margin-top: 18px; }\n 60% { margin-left: -8px; margin-top: -14px; }\n 100% { margin-left: 0px; margin-top: 0px; }\n}\n\n.ov-orb {\n position: absolute;\n border-radius: 50%;\n pointer-events: none;\n z-index: 0;\n}\n\n.ov-orb-1 {\n width: 280px;\n height: 280px;\n top: -60px;\n left: -80px;\n background-color: rgba(109,40,217,0.06);\n animation: orb-drift-1 12s ease-in-out infinite;\n}\n\n.ov-orb-2 {\n width: 200px;\n height: 200px;\n bottom: 40px;\n right: -60px;\n background-color: rgba(99,102,241,0.05);\n animation: orb-drift-2 16s ease-in-out infinite;\n}\n\n.ov-orb-3 {\n width: 150px;\n height: 150px;\n top: 40%;\n left: 30%;\n background-color: rgba(244,114,182,0.03);\n animation: orb-drift-3 20s ease-in-out infinite;\n}\n\n/* Game over - secret */\n.ov-secret-title {\n font-size: 13px;\n font-weight: 800;\n color: rgba(244,114,182,0.70);\n letter-spacing: 0.18em;\n text-transform: uppercase;\n animation: ov-in 0.5s ease-out 1.6s both;\n}\n.ov-secrets {\n display: flex;\n flex-direction: row;\n gap: 10px;\n flex-wrap: wrap;\n justify-content: center;\n}\n.ov-secret {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n background-color: rgba(244,114,182,0.08);\n border: 1px solid rgba(244,114,182,0.25);\n border-radius: 14px;\n padding: 14px 18px;\n min-width: 80px;\n overflow: hidden;\n}\n.ov-secret.locked {\n background-color: rgba(255,255,255,0.03);\n border-color: rgba(255,255,255,0.08);\n animation: chest-idle 2s ease-in-out infinite;\n}\n.ov-secret.rare {\n background-color: rgba(251,191,36,0.10);\n border-color: rgba(251,191,36,0.45);\n}\n.ov-secret.pop { animation: chest-pop 0.7s cubic-bezier(0.22,1,0.36,1) forwards; }\n.ov-secret.rare.pop { animation: chest-pop-rare 1.0s cubic-bezier(0.22,1,0.36,1) forwards; }\n.ov-secret-icon { font-size: 28px; color: white; }\n.ov-secret-lbl { font-size: 13px; font-weight: 800; color: rgba(255,255,255,0.80); }\n.ov-secret.rare .ov-secret-lbl { color: #fbbf24; }\n.ov-secret-shine {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,0.35) 50%, transparent 70%);\n animation: shine-sweep 0.6s ease-out 0.1s forwards;\n pointer-events: none;\n}\n\n.ov-section-enter {\n animation: ov-section-in 0.45s cubic-bezier(0.22,1,0.36,1) forwards;\n}\n\n@keyframes ov-section-in {\n 0% { margin-top: 24px; }\n 100% { margin-top: 0px; }\n}\n\n/* Clickable secret chests during Game Over */\n.ov-secret.clickable {\n cursor: none;\n border-color: rgba(167,139,250,0.55);\n background-color: rgba(167,139,250,0.10);\n animation: chest-idle 1.0s ease-in-out infinite;\n pointer-events: all;\n}\n\n.ov-secret.clickable:hover {\n background-color: rgba(167,139,250,0.20);\n border-color: rgba(167,139,250,0.85);\n}\n\n@keyframes chest-idle {\n 0%,100% { transform: rotate(0deg); }\n 25% { transform: rotate(-2deg) scale(1.02); }\n 75% { transform: rotate(2deg) scale(1.02); }\n}\n@keyframes chest-pop {\n 0% { transform: scale(1.0); }\n 20% { transform: scale(0.88); }\n 55% { transform: scale(1.22); }\n 75% { transform: scale(0.96); }\n 100% { transform: scale(1.0); }\n}\n@keyframes chest-pop-rare {\n 0% { transform: scale(1.0) rotate(0deg); }\n 15% { transform: scale(0.82) rotate(-4deg); }\n 45% { transform: scale(1.35) rotate(3deg); }\n 65% { transform: scale(0.93) rotate(-1deg); }\n 82% { transform: scale(1.06) rotate(0deg); }\n 100% { transform: scale(1.0) rotate(0deg); }\n}\n@keyframes shine-sweep {\n 0% { transform: translateX(-200%); }\n 100% { transform: translateX(300%); }\n}\n\n/* Secret section - when clickicking */\n.ov-secret-section {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n}\n\n.ov-secret-eyebrow {\n font-size: 11px;\n font-weight: 800;\n color: rgba(244,114,182,0.70);\n letter-spacing: 0.22em;\n text-transform: uppercase;\n}\n\n.ov-secret-hint {\n font-size: 13px;\n font-weight: 600;\n color: rgba(255,255,255,0.35);\n}\n\n/* \u2500\u2500 Chest cards \u2500\u2500 */\n\n.ov-chest-cards {\n display: flex;\n flex-direction: row;\n gap: 16px;\n justify-content: center;\n}\n\n.ov-chest-card {\n width: 130px;\n min-height: 150px; /* was height: 150px */\n border-radius: 16px;\n border: 1.5px solid rgba(167,139,250,0.15);\n background-color: rgba(13,6,32,0.90);\n position: relative;\n overflow: visible;\n cursor: none;\n box-shadow: 0px 4px 0px rgba(0,0,0,0.40);\n}\n\n.ov-chest-card.ready {\n border-color: rgba(167,139,250,0.55);\n background-color: rgba(60,20,140,0.25);\n cursor: none;\n pointer-events: all;\n animation: chest-card-ready 1.4s ease-in-out infinite alternate;\n box-shadow: 0px 4px 0px rgba(0,0,0,0.40),\n 0px 0px 20px rgba(167,139,250,0.15);\n}\n\n.ov-chest-card.ready:hover {\n border-color: rgba(167,139,250,0.90);\n background-color: rgba(109,40,217,0.30);\n}\n\n.ov-chest-card.waiting {\n border-color: rgba(255,255,255,0.06);\n background-color: rgba(255,255,255,0.02);\n box-shadow: none;\n}\n\n.ov-chest-card.revealed {\n border-color: rgba(129,140,248,0.35);\n background-color: rgba(99,102,241,0.08);\n box-shadow: 0px 4px 0px rgba(0,0,0,0.30),\n 0px 0px 16px rgba(129,140,248,0.10);\n}\n\n.ov-chest-card.revealed.rare {\n border-color: rgba(251,191,36,0.50);\n background-color: rgba(251,191,36,0.08);\n box-shadow: 0px 4px 0px rgba(0,0,0,0.30),\n 0px 0px 24px rgba(251,191,36,0.12);\n}\n\n.ov-chest-card.pop-in {\n animation: chest-card-pop 0.7s cubic-bezier(0.22,1,0.36,1) forwards;\n}\n\n.ov-chest-card-inner {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n pointer-events: none;\n}\n\n.ov-chest-emoji {\n font-size: 48px;\n animation: chest-card-idle 2.2s ease-in-out infinite alternate;\n}\n\n.ov-chest-card.waiting .ov-chest-emoji {\n opacity: 0.25;\n animation: none;\n}\n\n.ov-chest-tap {\n font-size: 10px;\n font-weight: 900;\n color: white;\n background-color: rgba(167,139,250,0.35);\n border: 1px solid rgba(167,139,250,0.50);\n border-radius: 6px;\n padding: 3px 10px;\n letter-spacing: 0.12em;\n}\n\n.ov-chest-tap.locked {\n background-color: rgba(255,255,255,0.05);\n border-color: rgba(255,255,255,0.08);\n color: rgba(255,255,255,0.20);\n}\n\n.ov-chest-glow {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n border-radius: 14px;\n animation: chest-glow-pulse 1.2s ease-in-out infinite alternate;\n pointer-events: none;\n}\n\n.ov-chest-card.ready {\n border-color: rgba(167,139,250,0.60);\n background-color: rgba(109,40,217,0.18);\n cursor: none;\n pointer-events: all;\n animation: chest-card-ready 1.2s ease-in-out infinite alternate;\n}\n\n.ov-chest-reward-icon {\n font-size: 40px;\n}\n\n.ov-chest-reward-lbl {\n font-size: 11px;\n font-weight: 800;\n color: rgba(255,255,255,0.75);\n padding: 0px 8px;\n}\n\n/* rare badge */\n.ov-chest-revealed-icon {\n font-size: 40px;\n}\n\n.ov-chest-revealed-info {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n}\n\n.ov-chest-revealed-lbl {\n font-size: 13px;\n font-weight: 900;\n color: white;\n padding: 0px 4px;\n}\n\n.ov-chest-rare-badge {\n font-size: 9px;\n font-weight: 900;\n color: #fbbf24;\n background-color: rgba(251,191,36,0.10);\n border: 1px solid rgba(251,191,36,0.30);\n border-radius: 20px;\n padding: 2px 8px;\n letter-spacing: 0.10em;\n}\n\n.ov-confetti-bit {\n position: absolute;\n top: -20px;\n width: 8px;\n height: 8px;\n border-radius: 2px;\n animation-name: confetti-fly;\n animation-timing-function: ease-out;\n animation-fill-mode: forwards;\n pointer-events: none;\n z-index: 10;\n}\n\n@keyframes confetti-fly {\n 0% { opacity: 1; transform: translateY(0px) rotate(0deg) scale(1.2); }\n 30% { opacity: 1; }\n 100% { opacity: 0; transform: translateY(-140px) rotate(520deg) scale(0.4); }\n}\n\n@keyframes chest-card-ready {\n 0% { transform: translateY(0px) scale(1.0); }\n 100% { transform: translateY(-5px) scale(1.02); }\n}\n\n@keyframes chest-card-idle {\n 0% { transform: translateY(0px) rotate(-1.5deg); }\n 100% { transform: translateY(-5px) rotate(1.5deg); }\n}\n\n@keyframes chest-card-pop {\n 0% { transform: scale(1.0); }\n 20% { transform: scale(0.85); }\n 55% { transform: scale(1.25); }\n 75% { transform: scale(0.96); }\n 100% { transform: scale(1.0); }\n}\n\n@keyframes chest-glow-pulse {\n 0% { opacity: 0.4; }\n 100% { opacity: 1.0; }\n}\n\n@keyframes chest-shine-sweep {\n 0% { transform: translateX(-100%); }\n 40% { transform: translateX(250%); }\n 100% { transform: translateX(250%); }\n}\n\n@keyframes confetti-fly {\n 0% { opacity: 1; transform: translateY(0px) rotate(0deg) scale(1.0); }\n 60% { opacity: 1; }\n 100% { opacity: 0; transform: translateY(-90px) rotate(380deg) scale(0.6); }\n}\n\n/* Frags section */\n.ov-frag-section {\n width: 100%;\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n background-color: rgba(99,102,241,0.05);\n border: 1px solid rgba(99,102,241,0.18);\n border-radius: 18px;\n padding: 20px 22px;\n position: relative;\n overflow: hidden;\n}\n\n/* Subtle shimmer across the top */\n.ov-frag-section::before {\n content: \"\";\n position: absolute;\n top: 0; left: 0; right: 0;\n height: 1px;\n background-color: rgba(129,140,248,0.35);\n}\n\n.ov-frag-breakdown {\n display: flex;\n flex-direction: column;\n gap: 5px;\n width: 100%;\n}\n\n.ov-frag-line {\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n padding: 5px 10px;\n background-color: rgba(255,255,255,0.025);\n border-radius: 8px;\n border-left: 2px solid rgba(129,140,248,0.20);\n}\n\n.ov-frag-line-lbl {\n font-size: 12px;\n font-weight: 600;\n color: rgba(255,255,255,0.35);\n}\n\n.ov-frag-line-val {\n font-size: 13px;\n font-weight: 900;\n color: #818cf8;\n}\n\n.ov-frag-label {\n font-size: 10px;\n font-weight: 900;\n color: rgba(129,140,248,0.55);\n letter-spacing: 0.22em;\n text-transform: uppercase;\n}\n\n.ov-frag-val {\n font-size: 52px;\n font-weight: 900;\n color: #818cf8;\n letter-spacing: -0.02em;\n line-height: 1;\n}\n\n.ov-frag-total {\n font-size: 12px;\n font-weight: 700;\n color: rgba(129,140,248,0.50);\n border-top: 1px solid rgba(129,140,248,0.10);\n padding-top: 8px;\n width: 100%;\n text-align: center;\n}\n\n.ov-frag-hint {\n font-size: 11px;\n color: rgba(255,255,255,0.22);\n font-weight: 600;\n letter-spacing: 0.04em;\n}\n\n/**/\n\n.ov-score-section {\n display: flex;\n flex-shrink: 0;\n flex-direction: column;\n align-items: center;\n gap: 6px;\n animation: ov-score-appear 0.6s ease-out forwards;\n}\n\n@keyframes ov-score-appear {\n 0% { opacity: 0; }\n 100% { opacity: 1; }\n}\n\n.ov-score-label { font-size: 12px; font-weight: 700; color: rgba(255,255,255,0.35); letter-spacing: 0.18em; text-transform: uppercase; }\n.ov-score { font-size: 62px; font-weight: 900; color: #a78bfa; }\n.ov-score.gold { color: #fbbf24; }\n.ov-newbest-badge {\n font-size: 15px;\n font-weight: 900;\n color: #fbbf24;\n background-color: rgba(251,191,36,0.12);\n border: 1.5px solid rgba(251,191,36,0.40);\n border-radius: 8px;\n padding: 4px 14px;\n letter-spacing: 0.12em;\n animation: badge-slam 0.5s cubic-bezier(0.22,1,0.36,1) 0.6s both;\n}\n@keyframes badge-slam {\n 0% { opacity: 0; transform: scale(0.3); }\n 60% { transform: scale(1.15); }\n 100% { opacity: 1; transform: scale(1.0); }\n}\n.ov-btns {\n display: flex;\n flex-direction: row;\n gap: 12px;\n flex-shrink: 0; /* never let the buttons get squeezed */\n}\n\n/* \u2500\u2500 Play Again button sweep \u2500\u2500 */\n.ov-btn-wrap {\n position: relative;\n overflow: hidden;\n background-color: #3aed3a;\n border: 1.5px solid #8bfa9a;\n border-radius: 12px;\n color: white;\n font-size: 30px;\n font-weight: 800;\n padding: 15px 40px;\n cursor: none;\n pointer-events: all;\n animation: btn-breathe 2.2s ease-in-out infinite alternate;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n@keyframes btn-breathe {\n 0% { transform: scale(1.0); }\n 100% { transform: scale(1.05); }\n}\n\n.ov-btn-shine {\n position: absolute;\n top: 0; bottom: 0;\n left: -80%;\n width: 40%;\n background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.25) 50%, transparent 100%);\n animation: btn-shine-sweep 3.0s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes btn-shine-sweep {\n 0% { left: -80%; }\n 45% { left: 140%; }\n 100% { left: 140%; }\n}\n\n@keyframes ov-in {\n 0% { opacity: 0; transform: translateY(12px) scale(0.97); }\n 100% { opacity: 1; transform: translateY(0) scale(1.0); }\n}\n\n.ov-reveal {\n animation: ov-in 0.4s ease-out both;\n}\n\n/* Main Menu Button */\n.ov-btn-menu {\n background-color: rgba(255,255,255,0.04);\n border: 1px solid rgba(255,255,255,0.10);\n border-radius: 12px;\n color: rgba(255,255,255,0.40);\n font-size: 14px;\n font-weight: 700;\n padding: 15px 20px;\n cursor: none;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.ov-btn-menu:hover {\n background-color: rgba(255,255,255,0.08);\n color: rgba(255,255,255,0.65);\n}\n\n/* -------------------- Modifier strip -----------------------\n* This shows the equipped modifiers as a vertical strip in the sidebar\n* Each item has its icon, name, and a tooltip on hover showing the full description. \n* The green tint matches the \"active\" color language used in the shop, \n* creating visual consistency between menu and game.\n\n* Players can now glance at the sidebar mid-run and see exactly what's buffing them\n* Combined with the boss multiplier suppression warning I added, \n* the player always knows what's affecting their score\n*/\n.mod-strip {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-bottom: 12px;\n}\n\n.mod-strip-item {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n background-color: rgba(52,211,153,0.06);\n border: 1px solid rgba(52,211,153,0.20);\n border-radius: 8px;\n transition: background-color 0.2s ease;\n}\n\n.mod-strip-item:hover {\n background-color: rgba(52,211,153,0.12);\n}\n\n.mod-strip-icon {\n font-size: 18px;\n flex-shrink: 0;\n}\n\n.mod-strip-name {\n font-size: 11px;\n font-weight: 700;\n color: rgba(255,255,255,0.75);\n letter-spacing: 0.04em;\n}\n\n/* \u2500\u2500 Game over reason \u2500\u2500 */\n.ov-reason {\n animation: ov-section-in 0.5s cubic-bezier(0.22,1,0.36,1) 0.5s both;\n font-size: 13px;\n font-weight: 700;\n color: rgba(255,255,255,0.55);\n padding: 8px 16px;\n background-color: rgba(255,255,255,0.04);\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 10px;\n width: 100%;\n justify-content: center;\n}\n\n/* ---------------- SCREEN SHAKE ---------------- */\n.hud-overlay {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n pointer-events: none;\n z-index: 5;\n}\n\n.hud-overlay.shake-small { animation: shake-small 0.25s ease-out; }\n.hud-overlay.shake-medium { animation: shake-medium 0.35s ease-out; }\n.hud-overlay.shake-strong { animation: shake-strong 0.45s ease-out; }\n.hud-overlay.shake-insane { animation: shake-insane 0.55s ease-out; }\n\n.mini-hand.shake-small { animation: shake-small 0.25s ease-out; }\n.mini-hand.shake-medium { animation: shake-medium 0.35s ease-out; }\n.mini-hand.shake-strong { animation: shake-strong 0.45s ease-out; }\n.mini-hand.shake-insane { animation: shake-insane 0.55s ease-out; }\n\n@keyframes shake-small {\n 0%,100% { transform: translate(0,0); }\n 25% { transform: translate(-2px, 1px); }\n 50% { transform: translate(2px, -1px); }\n 75% { transform: translate(-1px, 2px); }\n}\n@keyframes shake-medium {\n 0%,100% { transform: translate(0,0); }\n 20% { transform: translate(-4px, 2px) rotate(-0.3deg); }\n 40% { transform: translate(4px,-3px) rotate(0.3deg); }\n 60% { transform: translate(-3px, 3px) rotate(-0.2deg); }\n 80% { transform: translate(3px,-2px) rotate(0.2deg); }\n}\n@keyframes shake-strong {\n 0%,100% { transform: translate(0,0); }\n 15% { transform: translate(-6px, 3px) rotate(-0.5deg); }\n 30% { transform: translate(6px,-4px) rotate(0.5deg); }\n 45% { transform: translate(-5px, 5px) rotate(-0.4deg); }\n 60% { transform: translate(5px,-4px) rotate(0.4deg); }\n 75% { transform: translate(-3px, 2px); }\n 90% { transform: translate(2px,-2px); }\n}\n@keyframes shake-insane {\n 0%,100% { transform: translate(0,0); }\n 10% { transform: translate(-9px, 5px) rotate(-0.7deg); }\n 20% { transform: translate(9px,-6px) rotate(0.7deg); }\n 30% { transform: translate(-8px, 7px) rotate(-0.6deg); }\n 40% { transform: translate(8px,-6px) rotate(0.6deg); }\n 50% { transform: translate(-6px, 5px) rotate(-0.4deg); }\n 60% { transform: translate(6px,-5px) rotate(0.4deg); }\n 70% { transform: translate(-4px, 3px); }\n 80% { transform: translate(3px,-3px); }\n 90% { transform: translate(-1px, 1px); }\n}\n\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* \u2500\u2500\u2500\u2500\u2500\u2500 Consumable strip \u2014 horizontal bar below challenge bar \u2500\u2500\u2500\u2500\u2500 */\n/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.consumable-strip {\n position: absolute;\n bottom: 120px; /* sits above the bomb wheel */\n left: 0;\n right: 150px; /* leave room for sidebar */\n display: flex;\n flex-direction: row;\n justify-content: center;\n gap: 8px;\n pointer-events: all;\n z-index: 20;\n}\n\n.consumable-slot {\n position: relative;\n left: 70px;\n width: 54px;\n height: 54px;\n border-radius: 10px;\n border: 1.5px solid rgba(255,255,255,0.10);\n background-color: rgba(255,255,255,0.04);\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n cursor: none;\n position: relative;\n}\n\n.consumable-slot.available {\n border-color: rgba(167,139,250,0.50);\n background-color: rgba(109,40,217,0.15);\n cursor: none;\n}\n\n.consumable-slot.available:hover {\n background-color: rgba(109,40,217,0.30);\n border-color: rgba(167,139,250,0.80);\n}\n\n.consumable-slot.empty {\n opacity: 0.25;\n cursor: none;\n}\n\n.consumable-icon {\n font-size: 22px;\n pointer-events: none;\n}\n\n/* Count badge \u2014 bottom right corner of slot */\n.consumable-count {\n position: absolute;\n bottom: 2px;\n right: 4px;\n font-size: 10px;\n font-weight: 900;\n color: rgba(167,139,250,0.95);\n pointer-events: none;\n}\n\n/* \u2500\u2500 Screen Flash \u2500\u2500 */\n.screen-flash {\n position: absolute;\n top: 0; left: 0;\n width: 2200px;\n height: 1400px;\n pointer-events: none;\n z-index: 100;\n}\n\n.flash-text {\n position: absolute;\n top: 485px; /* roughly vertical center of 1080p */\n left: 1055px; /* roughly horizontal center minus half text width */\n font-size: 72px;\n font-weight: 900;\n color: rgba(255,255,255,0.90);\n pointer-events: none;\n animation: flash-text-pop 1.2s ease-out forwards;\n text-shadow:\n -3px -3px 0 #000,\n 3px -3px 0 #000,\n -3px 3px 0 #000,\n 3px 3px 0 #000,\n -4px 0 0 #000,\n 4px 0 0 #000,\n 0 -4px 0 #000,\n 0 4px 0 #000;\n\tz-index: 10;\n}\n\n@keyframes flash-text-pop {\n 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }\n 12% { opacity: 1; transform: translate(-50%, -50%) scale(1.05); }\n 70% { opacity: 1; transform: translate(-50%, -50%) scale(1.0); }\n 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }\n}\n\n@keyframes flash-fade {\n 0% { opacity: 0; }\n 15% { opacity: 1; }\n 100% { opacity: 0; }\n}\n\n@keyframes flash-boss-spawn {\n 0% { opacity: 0; }\n 15% { opacity: 1; }\n 100% { opacity: 0; }\n}\n\n@keyframes flash-wave {\n 0% { opacity: 0; }\n 10% { opacity: 1; }\n 60% { opacity: 1; }\n 100% { opacity: 0; }\n}\n\n.screen-flash.flash-boss-spawn {\n background-color: rgba(220, 30, 30, 0.50);\n animation: flash-boss-spawn 2.5s ease-out forwards;\n}\n\n.screen-flash.flash-boss-death {\n background-color: rgba(251,191,36,0.55);\n animation: flash-fade 0.6s ease-out forwards;\n}\n\n.screen-flash.flash-wave-start {\n background-color: rgba(138, 92, 246, 0.082); /* was 0.18 \u2014 lighter */\n animation: flash-wave .9s ease-out forwards; /* was 2.5s \u2014 faster */\n}\n\n.screen-flash.flash-new-best {\n background-color: rgba(52,211,153,0.45);\n animation: flash-fade 1.0s ease-out forwards;\n}\n\n.screen-flash.flash-game-over {\n background-color: rgba(180,20,20,0.60);\n animation: flash-fade 0.8s ease-out forwards;\n}\n\n/* ------------------------------------------- */\n/* -------------- Pet Companion -------------- */\n/* ------------------------------------------- */\n\n.hud-pet-wrap {\n position: absolute;\n bottom: 55px;\n left: 15px;\n width: 220px;\n height: 870px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n padding: 14px 14px 0px 14px;\n z-index: 6;\n}\n\n.hud-pet {\n font-size: 110px;\n animation: hud-pet-idle 2.5s ease-in-out infinite alternate;\n opacity: 0.8;\n margin-bottom: 10px;\n margin-left: 0px;\n}\n.hud-pet image {\n width: 110px;\n height: 110px;\n}\n\n.hud-pet-egg-icon\n{\n width: 110px;\n height: 110px;\n}\n\n/* Top section \u2014 modifiers + curse */\n.hud-pet-top {\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: 100%;\n}\n\n/* Bottom section \u2014 pet + hearts, pinned to bottom */\n.hud-pet-bottom {\n margin-top: auto;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n font-size: 64px; \n\n}\n\n@keyframes pet-bob {\n from { transform: translateY(0px); }\n to { transform: translateY(-8px); }\n}\n\n.pet-bob {\n animation: pet-bob 1.4s ease-in-out infinite alternate;\n}\n\n.hud-pet-egg {\n width: 120px;\n height: 120px;\n}\n\n.hud-pet-bottom image {\n width: 134px;\n height: 134px;\n margin-left: -10px;\n}\n\n.pet-sbl {\n font-size: 10px;\n font-weight: 800;\n letter-spacing: 0.2em;\n text-transform: uppercase;\n color: rgba(166,154,219,0.5);\n}\n\n.hud-pet-card {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background-color: rgba(18,8,42,0.92);\n border: 1px solid rgba(167,139,250,0.18);\n border-radius: 14px 14px 0px 0px;\n z-index: -1;\n}\n\n@keyframes hud-pet-idle {\n 0% { transform: translateY(0px) scale(1.0); }\n 100% { transform: translateY(-6px) scale(1.05); }\n}\n\n/* -------------------------------------------------- */\n/* -------------- Hearts/Lives Sidebar -------------- */\n/* -------------------------------------------------- */\n\n.hud-pet-lives {\n position: absolute;\n right: 0px;\n left: 150px;\n bottom: 10px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.hud-pet-heart {\n font-size: 40px;\n}\n\n.hud-pet-heart.empty {\n opacity: 0.20;\n}\n\n/* -------------------------------------------------- */\n/* -------------- BACKGROUND (grid-like) ------------ */\n/* -------------------------------------------------- */\n\n.game-bg-grid {\n position: absolute;\n top: 90px; left: 0; right: 0; bottom: 0;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n}\n\n.game-bg-cell {\n position: absolute;\n width: 232px;\n height: 232px;\n border-radius: 10px;\n border: 2px solid rgba(66, 54, 75, 0.04);\n z-index: 0;\n}\n\n.game-bg-cell.gbg-idle { /* inline style from WaveGridColor \u2014 already very dark hex */ }\n.game-bg-cell.gbg-expl { background-color: rgba(10,4,24,0.95); border-color: rgba(LoadPetLbLoadPetLb251,146,60,0.22); }\n.game-bg-cell.gbg-expl2 { background-color: rgba(10,4,24,0.95); border-color: rgba(251,191,36,0.22); }\n.game-bg-cell.gbg-gold { background-color: rgba(10,4,24,0.95); border-color: rgba(251,191,36,0.16); }\n.game-bg-cell.gbg-red { background-color: rgba(10,4,24,0.95); border-color: rgba(248,113,113,0.16); }\n.game-bg-cell.gbg-chain { background-color: rgba(10,4,24,0.95); border-color: rgba(196,181,253,0.14); }\n\n/* SIDEBAR BUTTON (info) */\n.sidebar-toggle {\n position: absolute;\n top: 155px;\n right: 12px;\n width: 44px;\n height: 44px;\n border-radius: 50px;\n background-color: rgba(180,30,30,0.85);\n border: 2px solid rgba(255,100,100,0.60);\n color: white;\n font-size: 20px;\n font-weight: 900;\n pointer-events: all;\n z-index: 20;\n cursor: none;\n box-shadow: 2px 2px 0px rgba(0,0,0,0.60);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.sidebar-toggle:hover {\n background-color: rgba(220,40,40,0.95);\n border-color: rgba(255,140,140,0.80);\n}\n\n/**/\n.pet-mod-strip {\n display: flex;\n flex-direction: row;\n gap: 6px;\n margin-top: 8px;\n}\n\n.pet-mod-item {\n width: 32px;\n height: 32px;\n border-radius: 8px;\n background-color: rgba(52,211,153,0.10);\n border: 1px solid rgba(52,211,153,0.25);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n}\n\n.pet-curse-warning {\n font-size: 13px;\n font-weight: 800;\n color: rgba(248,113,113,0.85);\n margin-top: 6px;\n padding: 4px 8px;\n background-color: rgba(239,68,68,0.10);\n border: 1px solid rgba(248,113,113,0.20);\n border-radius: 6px;\n}\n\n/* Min chest requirement (displayed on left side bar)*/\n.hud-min-chests {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 6px;\n padding: 4px 23px;\n background-color: rgba(0,0,0,0.35);\n border-radius: 6px;\n margin-top: 4px;\n justify-content: center;\n}\n\n.hud-min-chests-lbl {\n font-size: 18px;\n font-weight: 700;\n color: rgba(255,255,255,0.55);\n}\n\n.hud-min-chests-val {\n font-size: 18px;\n font-weight: 900;\n color: rgba(251,191,36,0.90);\n}\n\n/* MUSIC DEBUG */\n.mm-dev-music-panel {\n position: absolute;\n bottom: 20px;\n right: 20px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 8px;\n background-color: rgba(4,1,13,0.90);\n border: 1px dashed rgba(244,114,182,0.30);\n border-radius: 6px;\n z-index: 99;\n pointer-events: all;\n}\n\n.mm-dev-music-label {\n font-size: 9px;\n font-weight: 900;\n color: rgba(244,114,182,0.60);\n letter-spacing: 0.20em;\n text-transform: uppercase;\n margin-bottom: 2px;\n}\n\n.mm-dev-btn {\n background-color: rgba(244,114,182,0.15);\n border: 1px solid rgba(244,114,182,0.30);\n border-radius: 4px;\n color: rgba(244,114,182,0.85);\n font-size: 10px;\n font-weight: 700;\n padding: 4px 8px;\n cursor: none;\n pointer-events: all;\n letter-spacing: 0.05em;\n}\n\n.mm-dev-btn:hover {\n background-color: rgba(244,114,182,0.25);\n}\n\n.mm-dev-btn-active {\n background-color: rgba(52,211,153,0.20);\n border-color: rgba(52,211,153,0.50);\n color: rgba(52,211,153,0.90);\n}\n\n/* =============== New SURVIVAL mode =============== */\n.survival-timer {\n position: absolute;\n top: 100px;\n left: 50%;\n transform: translateX(-50%);\n width: 160px;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 8px;\n padding: 8px 16px;\n background-color: rgba(10,4,28,0.80);\n border: 2px solid rgba(239,68,68,0.35);\n border-radius: 10px;\n box-shadow: 3px 3px 0px rgba(0,0,0,0.55);\n z-index: 10;\n}\n\n.survival-timer-icon {\n font-size: 16px;\n}\n\n.survival-timer-val {\n font-size: 20px;\n font-weight: 900;\n color: rgba(251,191,36,0.90);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n\n.survival-spawn-track {\n position: absolute;\n top: 145px; /* just below the timer pill */\n left: 50%;\n transform: translateX(-50%);\n width: 160px;\n height: 6px;\n background-color: rgba(255,255,255,0.08);\n border-radius: 3px;\n overflow: hidden;\n z-index: 10;\n}\n\n.survival-spawn-fill {\n height: 100%;\n background-color: rgba(251,191,36,0.85);\n transition: width 0.1s linear;\n}\n\n/* CAT */\n.mm-pet-hero-emoji image {\n width: 170px; /* bump this up to whatever size you want */\n height: 170px;\n image-rendering: pixelated; /* keep it crisp if it's pixel art */\n margin-left: -10px;\n}\n\n.mm-pet-hero-emoji {\n position: relative;\n}\n.cat-anchor { position: relative; }\n\n\n\n.cat-speech-bubble {\n position: absolute;\n top: -50px;\n left: 50%;\n transform: translateX(-50%);\n background-color: rgba(255,255,255,0.95);\n color: #1a1030;\n font-size: 14px;\n font-weight: 800;\n padding: 8px 14px;\n border-radius: 12px;\n white-space: nowrap;\n box-shadow: 2px 2px 0px rgba(0,0,0,0.4);\n z-index: 10;\n animation: bubble-pop 0.2s ease-out both;\n}\n\n.cat-speech-bubble::after {\n content: \"\";\n position: absolute;\n bottom: -6px;\n left: 50%;\n width: 12px;\n height: 12px;\n background-color: rgba(255,255,255,0.95);\n transform: translateX(-50%) rotate(45deg);\n}\n\n@keyframes bubble-pop {\n 0% { opacity: 0; transform: translateX(-50%) scale(0.7); }\n 100% { opacity: 1; transform: translateX(-50%) scale(1.0); }\n}\n\n/* Back button */\n.back-toggle {\n top: 211px; /* 155 + 44 + 12 gap */\n background-color: rgba(60,60,180,0.85);\n border-color: rgba(140,140,255,0.60);\n}\n\n.back-toggle:hover {\n background-color: rgba(80,80,220,0.95);\n border-color: rgba(160,160,255,0.80);\n}\n\n.back-confirm-overlay {\n position: absolute;\n top: 0; left: 0; right: 0; bottom: 0;\n background-color: rgba(4,1,13,0.85);\n z-index: 90;\n pointer-events: all;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.back-confirm-popup {\n background-color: rgba(20,8,50,0.98);\n border: 2px solid rgba(167,139,250,0.55);\n border-radius: 18px;\n padding: 32px 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n max-width: 380px;\n text-align: center;\n box-shadow: 6px 6px 0px rgba(20,0,60,0.80);\n}\n\n.back-confirm-title { font-size: 22px; font-weight: 900; color: white; }\n.back-confirm-desc { font-size: 13px; color: rgba(255,255,255,0.55); line-height: 1.5; }\n.back-confirm-btns { display: flex; flex-direction: row; gap: 12px; margin-top: 8px; }\n\n.back-confirm-btn {\n border-radius: 10px;\n font-size: 14px;\n font-weight: 800;\n padding: 12px 20px;\n cursor: none;\n pointer-events: all;\n border: 2px solid transparent;\n}\n\n.back-confirm-btn.cancel { background-color: rgba(52,211,153,0.15); border-color: rgba(52,211,153,0.45); color: #6ee7b7; }\n.back-confirm-btn.confirm { background-color: rgba(239,68,68,0.15); border-color: rgba(239,68,68,0.45); color: #fca5a5; }\n\n\n/* Survival hype track */\n.survival-strip {\n position: absolute;\n top: 96px;\n left: 0;\n right: 0;\n height: 44px;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 10px;\n z-index: 10;\n pointer-events: none;\n}\n\n.survival-timer {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 6px;\n padding: 5px 12px;\n background-color: rgba(10,4,28,0.85);\n border: 2px solid rgba(239,68,68,0.35);\n border-radius: 8px;\n flex-shrink: 0;\n}\n.survival-timer-icon { font-size: 14px; }\n.survival-timer-val {\n font-size: 18px;\n font-weight: 900;\n color: rgba(251,191,36,0.9);\n}\n\n.survival-hype-track {\n position: relative;\n width: 130px;\n height: 20px;\n background-color: rgba(255,255,255,0.08);\n border: 1px solid rgba(251,146,60,0.35);\n border-radius: 6px;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n.survival-hype-fill {\n position: absolute;\n top: 0; left: 0; bottom: 0;\n background-color: rgba(251,146,60,0.75);\n transition: width 0.12s linear;\n}\n.survival-hype-track.maxed {\n border-color: rgba(251,191,36,0.95);\n animation: hype-pulse 0.5s ease-in-out infinite alternate;\n}\n.survival-hype-track.maxed .survival-hype-fill { background-color: rgba(251,191,36,0.9); }\n.survival-hype-lbl {\n font-size: 9px;\n font-weight: 900;\n letter-spacing: 0.12em;\n color: rgba(255,255,255,0.9);\n z-index: 1;\n}\n@keyframes hype-pulse {\n from { border-color: rgba(251,191,36,0.5); }\n to { border-color: rgba(251,191,36,1.0); }\n}\n\n.survival-goal {\n position: relative;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 5px 12px;\n background-color: rgba(10,4,28,0.85);\n border: 1px solid rgba(167,139,250,0.35);\n border-radius: 8px;\n flex-shrink: 0;\n}\n.survival-goal-lbl { font-size: 11px; font-weight: 800; color: rgba(255,255,255,0.85); }\n.survival-goal-prog { font-size: 12px; font-weight: 900; color: #a78bfa; }\n.survival-goal-timer {\n position: absolute;\n bottom: 0; left: 0; right: 0;\n height: 2px;\n background-color: rgba(255,255,255,0.06);\n}\n.survival-goal-timer-fill {\n height: 100%;\n background-color: rgba(167,139,250,0.7);\n transition: width 0.2s linear;\n}\n\n.survival-spawn-track {\n width: 90px;\n height: 6px;\n background-color: rgba(255,255,255,0.08);\n border-radius: 3px;\n overflow: hidden;\n flex-shrink: 0;\n}\n.survival-spawn-fill {\n height: 100%;\n background-color: rgba(251,191,36,0.85);\n transition: width 0.1s linear;\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "styles/base/_splitcontainer.scss",
"FileName": "_splitcontainer.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "\r\n.splitcontainer\r\n{\r\n\tflex-grow: 1;\r\n\r\n\t> .split-left, > .split-right\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t> .split-left\r\n\t{\r\n\t\theight: 100%;\r\n\t}\r\n\r\n\t> .splitter\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\twidth: 8px;\r\n\t\tcursor: ew-resize;\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t}\r\n\t}\r\n\r\n\t&.dragging > .splitter\r\n\t{\r\n\t}\r\n\r\n\t&.vertical\r\n\t{\r\n\t\tflex-direction: column;\r\n\r\n\t\t> .splitter\r\n\t\t{\r\n\t\t\theight: 8px;\r\n\t\t\twidth: 100%;\r\n\t\t\tcursor: ns-resize;\r\n\t\t}\r\n\t}\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "styles/form/_coloreditor.scss",
"FileName": "_coloreditor.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "\r\n$primary: red !default;\r\n$primary-alt: white !default;\r\n\r\n$switch-padding: 6px !default;\r\n\r\n.coloreditor\r\n{\r\n\tflex-direction: column;\r\n\tmin-width: 250px;\r\n\tmax-width: 250px;\r\n\r\n\t> .canvas\r\n\t{\r\n\t\tborder-top: 1px solid rgba( white, 0.5 );\r\n\t\tflex-grow: 1;\r\n\t\tflex-direction: column;\r\n\t\tpadding: 10px 15px;\r\n\r\n\r\n\t\t> textentry.textentry\r\n\t\t{\r\n\t\t\tflex-grow: 0;\r\n\t\t\tflex-shrink: 0;\r\n\t\t\twidth: auto;\r\n\t\t}\r\n\t}\r\n\r\n\t.slider\r\n\t{\r\n\t\tpadding: 6.5px;\r\n\t}\r\n\r\n\t.slider .track\r\n\t{\r\n\t\theight: 14px;\r\n\t\tborder-radius: 14px;\r\n\t\tleft: 0px;\r\n\t\tright: 0px;\r\n\t\ttransition: all 0.1s ease-out;\r\n\r\n\t\t.inner\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t.slider:hover, .slider:active\r\n\t{\r\n\t\tpadding: 7px;\r\n\r\n\t\t.track\r\n\t\t{\r\n\t\t\theight: 20px;\r\n\t\t\tborder-radius: 6px;\r\n\t\t}\r\n\r\n\t\t.thumb\r\n\t\t{\r\n\t\t\twidth: 10px;\r\n\t\t\theight: 16px;\r\n\t\t\tmargin-left: 2px;\r\n\t\t}\r\n\t}\r\n\r\n\t.slider .thumb\r\n\t{\r\n\t\twidth: 10px;\r\n\t\theight: 10px;\r\n\t\tbox-shadow: 0px 0px 0px transparent;\r\n\t\tbackground-color: white;\r\n\t\tbackdrop-filter: invert( 1 );\r\n\t\tmargin-left: 1px;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t.alpha_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, rgba( #ccc, 0.05 ), rgba( #ccc, 0.9 ) );\r\n\t}\r\n\r\n\t.hue .track\r\n\t{\r\n\t\tbackground: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);\r\n\r\n\t\t.inner\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t.satval\r\n\t{\r\n\t\tbackground: linear-gradient(to right, white, transparent);\r\n\t\theight: 200px;\r\n\t\tposition: relative;\r\n\r\n\t\t&:active\r\n\t\t{\r\n\t\t\tz-index: 10;\r\n\t\t\tcursor: none;\r\n\t\t}\r\n\r\n\t\t.value_gradient\r\n\t\t{\r\n\t\t\tbackground: linear-gradient(to bottom, black, white);\r\n\t\t\tposition: absolute;\r\n\t\t\ttop: 0;\r\n\t\t\tleft: 0;\r\n\t\t\tright: 0;\r\n\t\t\tbottom: 0;\r\n\t\t\tpointer-events: none;\r\n\t\t\tmix-blend-mode: multiply;\r\n\t\t}\r\n\r\n\t\t.thumb\r\n\t\t{\r\n\t\t\tz-index: 10;\r\n\t\t}\r\n\t}\r\n\r\n\t.presets\r\n\t{\r\n\t\tflex-wrap: wrap;\r\n\t\talign-content: center;\r\n\t\tjustify-content: center;\r\n\t\tpadding: 15px;\r\n\t\tborder-top: 1px solid rgba( white, 0.5 );\r\n\r\n\t\t.preset\r\n\t\t{\r\n\t\t\tbackground-color: white;\r\n\t\t\twidth: 16px;\r\n\t\t\theight: 16px;\r\n\t\t\tmargin: 2px;\r\n\t\t\tborder-radius: 20px;\r\n\t\t\tcursor: pointer;\r\n\t\t\t\r\n\t\t\t&:hover\r\n\t\t\t{\r\n\t\t\t\tbox-shadow: 0px 0px 1px 2px white;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n\r\n\r\n.coloreditorrgba\r\n{\r\n\tflex-direction: column;\r\n\r\n\t.slider\r\n\t{\r\n\t\tpadding-right: 16px;\r\n\t}\r\n\r\n\t.slider .track\r\n\t{\r\n\t\tbackground-color: transparent;\r\n\t\theight: 14px;\r\n\t\tborder-radius: 14px;\r\n\t\tleft: 0px;\r\n\t\tright: 0px;\r\n\r\n\t\t.inner\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t.slider .thumb\r\n\t{\r\n\t\twidth: 12px;\r\n\t\theight: 12px;\r\n\t\tbox-shadow: 0px 0px 0px transparent;\r\n\t\tbackground-color: white;\r\n\t\tmargin-left: 2px;\r\n\t\tbackdrop-filter: invert( 1 );\r\n\t}\r\n\r\n\t.red_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, rgba( #e63439, 0.1 ), rgba( #e63439, 0.9 ) );\r\n\t}\r\n\r\n\t.green_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, rgba( #54be35, 0.1 ), rgba( #54be35, 0.9 ) );\r\n\t}\r\n\r\n\t.blue_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, rgba( #3472e6, 0.1 ), rgba( #3472e6, 0.9 ) );\r\n\t}\r\n\r\n\t.alpha_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, rgba( #ccc, 0.05 ), rgba( #ccc, 0.9 ) );\r\n\t}\r\n}\r\n\r\n\r\n.coloreditorhsva\r\n{\r\n\tflex-direction: column;\r\n\r\n\t.slider\r\n\t{\r\n\t\tpadding-right: 16px;\r\n\t}\r\n\r\n\t.slider .track\r\n\t{\r\n\t\tbackground-color: transparent;\r\n\t\theight: 14px;\r\n\t\tborder-radius: 14px;\r\n\t\tleft: 0px;\r\n\t\tright: 0px;\r\n\r\n\t\t.inner\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t.slider .thumb\r\n\t{\r\n\t\twidth: 12px;\r\n\t\theight: 12px;\r\n\t\tbox-shadow: 0px 0px 0px transparent;\r\n\t\tbackground-color: white;\r\n\t\tmargin-left: 2px;\r\n\t\tbackdrop-filter: invert( 1 );\r\n\t}\r\n\t\t\r\n\t.hue_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00 );\r\n\t}\r\n\r\n\t.alpha_slider .track\r\n\t{\r\n\t\tbackground-image: linear-gradient( to right, rgba( #ccc, 0.05 ), rgba( #ccc, 0.9 ) );\r\n\t}\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "styles/form/_colorproperty.scss",
"FileName": "_colorproperty.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "\r\n\r\n.colorproperty\r\n{\r\n\talign-items: center;\r\n\tposition: relative;\r\n\r\n\t.colorsquare\r\n\t{\r\n\t\twidth: 20px;\r\n\t\theight: 20px;\r\n\t\tborder-radius: 4px;\r\n\t\tmargin-right: 8px;\r\n\t\tposition: absolute;\r\n\t\tleft: 7px;\r\n\t\tz-index: 1;\r\n\t\tcursor: pointer;\r\n\t}\r\n\r\n\t> .textentry:not( .a.b.c )\r\n\t{\r\n\t\tpadding-left: 36px;\r\n\t}\r\n}"
},
{
"Ident": "playnplay.chain_reaction",
"Path": "ui/controls/color/coloralphacontrol.cs.scss",
"FileName": "coloralphacontrol.cs.scss",
"PackageType": "game",
"CodeKind": "Game",
"AssetVersionId": 354834,
"IsPrivate": false,
"Code": "ColorAlphaControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, black, white );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\ttop: -5px;\r\n\t\tbottom: -5px;\r\n\t\taspect-ratio: 1;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% );\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n"
}
]
}