{"TotalCount":45,"Files":[{"Ident":"gamah.splitclicker","Path":"styles/form/_colorproperty.scss","FileName":"_colorproperty.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"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\u003E .textentry:not( .a.b.c )\r\n\t{\r\n\t\tpadding-left: 36px;\r\n\t}\r\n}"},{"Ident":"gamah.splitclicker","Path":"ui/controls/vectorcontrol.cs.scss","FileName":"vectorcontrol.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"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":"gamah.splitclicker","Path":"ui/controlsheet/controlsheetrow.cs.scss","FileName":"controlsheetrow.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"\r\nControlSheetRow\r\n{\r\n\tflex-direction: row;\r\n\tflex-shrink: 0;\r\n\tmax-height: 32px;\r\n\tborder-radius: 4px;\r\n\r\n\t\u0026.hidden\r\n\t{\r\n\t\tdisplay: none;\r\n\t}\r\n}\r\n\r\nControlSheetRow \u003E .left\r\n{\r\n\twidth: 150px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\tpadding: 6px;\r\n}\r\n\r\nControlSheetRow \u003E .right\r\n{\r\n\tflex-grow: 1;\r\n}\r\n\r\nControlSheetRow \u003E .left \u003E .title\r\n{\r\n\twidth: 150px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\tfont-size: 13px;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n}\r\n\r\nControlSheetRow \u003E .right \u003E textentry,\r\nControlSheetRow \u003E .right numberentry\r\n{\r\n\tbackground-color: #000a;\r\n\tflex-grow: 1;\r\n\tpadding: 4px 8px;\r\n\tborder-radius: 2px;\r\n}\r\n\r\n"},{"Ident":"gamah.splitclicker","Path":"ui/components/packagelist.razor.scss","FileName":"packagelist.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":".package-list\r\n{\r\n    flex-shrink: 1;\r\n    flex-wrap: wrap;\r\n    flex-grow: 1;\r\n\r\n    h1\r\n    {\r\n        width: 100%;\r\n        margin-top: 50px;\r\n        font-size: 40px;\r\n    }\r\n\r\n    PackageCard\r\n    {\r\n        \u0026:hover\r\n        {\r\n            sound-in: \u0022ui.button.over\u0022;\r\n        }\r\n    }\r\n\r\n    VirtualGrid\r\n    {\r\n        width: 100%;\r\n        height: 100%;\r\n\r\n        .cell\r\n        {\r\n            \r\n        }\r\n    }\r\n}"},{"Ident":"gamah.splitclicker","Path":"ui/components/packagefilters.razor.scss","FileName":"packagefilters.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"\r\n@import \u0022/styles/_theme.scss\u0022;\r\n\r\n.packagefilter\r\n{\r\n\twidth: 100%;\r\n\tflex-shrink: 0;\r\n\r\n\t.left\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tflex-shrink: 0;\r\n\t\tgap: 16px;\r\n\t}\r\n\r\n\t.right\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tflex-shrink: 0;\r\n\t\tgap: 16px;\r\n\t}\r\n}\r\n\r\n.packagefilter textentry\r\n{\r\n\tflex-grow: 1;\r\n\tpadding: 8px 12px;\r\n\tpointer-events: all;\r\n\r\n\tLabel\r\n\t{\r\n\t\tmargin-left: 26px;\r\n\t}\r\n\r\n\ticonpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tleft: 12px;\r\n\t\topacity: 0.3;\r\n\t\tfont-size: $text-large;\r\n\t}\r\n}\r\n\r\n\t.packagefilterfacet, .packagefilter textentry\r\n\t{\r\n\t\tbackground-color: rgba($default-950, 0.3);\r\n\t\tcolor: $default-text;\r\n\t\toutline: 1px solid $default-border;\r\n\t\ttransition: all 150ms $easing;\r\n\t\tborder-radius: $rounding-default;\r\n\t\tpadding: 4px 12px;\r\n\t\tgap: 16px;\r\n\t\tflex-shrink: 0;\r\n\t\tfont-size: 14px;\r\n\t\tcursor: pointer;\r\n\t\talign-items: center;\r\n\r\n\t\t.clear-button\r\n\t\t{\r\n\t\t\theight: 100%;\r\n\t\t\tmargin-right: -8px;\r\n\t\t\talign-items: center;\r\n\t\t\tpadding: 0px 4px;\r\n\r\n\t\t\t\u0026:hover\r\n\t\t\t{\r\n\t\t\t\tcolor: red;\r\n\t\t\t\tborder-radius: 100%;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t\u0026.is-active\r\n\t\t{\r\n\t\t\tbackground-color: $default-700;\r\n\t\t}\r\n\r\n\t\t\u0026:not(.is-active)\r\n\t\t{\r\n\t\t\t\u0026:hover\r\n\t\t\t{\r\n\t\t\t\toutline: 1px solid $default-50;\r\n\t\t\t}\r\n\t\t}\r\n\t}"},{"Ident":"gamah.splitclicker","Path":"ui/components/packagecard.razor.scss","FileName":"packagecard.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"@import \u0022/styles/_theme.scss\u0022;\r\n$background-size: 8px;\r\n\r\nPackageCard\r\n{\r\n\tflex-shrink: 0;\r\n\r\n\t\u0026:hover\r\n\t{\r\n\t\tsound-in: \u0022ui.button.over\u0022;\r\n\t}\r\n\r\n\t\u0026:active\r\n\t{\r\n\t\tsound-in: \u0022ui.button.press\u0022;\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\u0026.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\u0026: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\u0026::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\u0026.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\u0026.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\u0026.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\u0026:before\r\n\t\t{\r\n\t\t\tcontent: \u0027\u25CF\u0027;\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\u0026:not(.list)\r\n\t{\r\n\t\t\u0026::after\r\n\t\t{\r\n\t\t\tcontent: \u0022\u0022;\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\u0026:hover\r\n\t\t{\r\n\t\t\ttransform: scale( 1.05 );\r\n\r\n\t\t\t\u0026::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":"gamah.splitclicker","Path":"ui/controls/color/colorsaturationvaluecontrol.cs.scss","FileName":"colorsaturationvaluecontrol.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"ColorSaturationValueControl\r\n{\r\n\twidth: 240px;\r\n\theight: 240px;\r\n\tbackground-color: red;\r\n\tposition: relative;\r\n\tborder-radius: 4px;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t\u0026:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t\u0026: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\twidth: 16px;\r\n\t\theight: 16px;\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% ) translateY( -50% );\r\n\t\tpointer-events: none;\r\n\t\tz-index: 100;\r\n\t\tz-index: 100;\r\n\t}\r\n\r\n\t.gradient\r\n\t{\r\n\t\tposition: absolute;\r\n\t\twidth: 100%;\r\n\t\theight: 100%;\r\n\t\tborder-radius: 4px;\r\n\t\tbackground: linear-gradient( to right, white, rgba( 255, 255, 255, 0 ) );\r\n\r\n\t\t\u0026:after\r\n\t\t{\r\n\t\t\tcontent: \u0022\u0022;\r\n\t\t\tposition: absolute;\r\n\t\t\twidth: 100%;\r\n\t\t\theight: 100%;\r\n\t\t\tborder-radius: 4px;\r\n\t\t\tbackground: linear-gradient( to top, black, rgba( 0, 0, 0, 0 ) );\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"gamah.splitclicker","Path":"Audio/SoundPlayer.cs","FileName":"SoundPlayer.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"using Sandbox;\n\nnamespace Splitclicker.Audio;\n\n// Thin wrapper over the five UI sound effects (see scripts/gen_sounds.py). Every voice\n// is a square \u002B triangle blend around G; together they form the round\u0027s audio motif:\n// arming heartbeat \u2192 armed \u0022go\u0022 \u2192 click blips \u2192 disarm bookend, with throttle as the\n// \u0022nope\u0022 for clicking while the button is dormant. Driven from ClickController.\npublic static class SoundPlayer\n{\n\tpublic static void PlayArming()   =\u003E Sound.Play( \u0022sounds/arming.sound\u0022 );\n\tpublic static void PlayArmed()    =\u003E Sound.Play( \u0022sounds/armed.sound\u0022 );\n\tpublic static void PlayClick()    =\u003E Sound.Play( \u0022sounds/click.sound\u0022 );\n\tpublic static void PlayDisarm()   =\u003E Sound.Play( \u0022sounds/disarm.sound\u0022 );\n\tpublic static void PlayThrottle() =\u003E Sound.Play( \u0022sounds/throttle.sound\u0022 );\n\n\t// An opponent\u0027s scoring click during the live window: the click blip a major\n\t// fifth up (3:2 ratio \u21D2 Pitch 1.5) at half volume, so the field of other\n\t// players\u0027 clicks reads as related-but-subordinate to your own click.\n\tpublic static void PlayPip()\n\t{\n\t\tvar h = Sound.Play( \u0022sounds/click.sound\u0022 );\n\t\tif ( h != null \u0026\u0026 h.IsValid )\n\t\t{\n\t\t\th.Pitch = 1.5f;\n\t\t\th.Volume = 0.5f;\n\t\t}\n\t}\n}\n"},{"Ident":"gamah.splitclicker","Path":"Game/AchievementTracker.cs","FileName":"AchievementTracker.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"using System;\nusing Sandbox;\n\nnamespace Splitclicker.Game;\n\n// Maps server-pushed \u0060you.*\u0060 facts to s\u0026box Services (Stats \u002B Achievements).\n// The server is the source of truth; we just record. Stat increments are NOT\n// idempotent, so guard them with the round_id/game_id the server stamps; manual\n// Unlock calls are idempotent and need no guard (PLAN \u00A77.1).\n//\n// Named *Tracker to avoid colliding with Sandbox.Services.Achievements.\npublic static class AchievementTracker\n{\n\t// round_result \u2192 bump the \u0060points\u0060 stat once per unseen round. Stat-threshold\n\t// achievements (first_point, points_50, points_100) then fire automatically.\n\tpublic static void OnRoundResult( int pointsDelta, string roundId )\n\t{\n\t\tif ( pointsDelta \u003C= 0 || string.IsNullOrEmpty( roundId ) ) return;\n\n\t\t// \u0022Ahead of the Curve\u0022: more than 5 scoring clicks in a single round. Manual\n\t\t// Unlock is idempotent, so it\u0027s safe to attempt on every delivery (no round_id\n\t\t// guard needed) \u2014 unlike the \u0060points\u0060 increment below.\n\t\tif ( pointsDelta \u003E 5 ) Unlock( \u0022aotc\u0022 );\n\n\t\tvar pd = PlayerData.Load();\n\t\tif ( pd.LastPointsRoundId == roundId ) return; // duplicate delivery / reconnect replay\n\t\tpd.LastPointsRoundId = roundId;\n\t\tpd.Save();\n\t\ttry { Sandbox.Services.Stats.Increment( \u0022points\u0022, pointsDelta ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] points stat failed: {e.Message}\u0022 ); }\n\t}\n\n\t// game_over \u2192 placement achievements (idempotent Unlock) \u002B the \u0060wins\u0060 stat\n\t// once per unseen game (drives first_win / wins_5 / wins_10).\n\tpublic static void OnGameOver( int placement, bool won, string gameId )\n\t{\n\t\tif ( placement \u003E= 1 \u0026\u0026 placement \u003C= 5 ) Unlock( \u0022top_5\u0022 );\n\t\tif ( placement \u003E= 1 \u0026\u0026 placement \u003C= 3 ) Unlock( \u0022top_3\u0022 );\n\t\t// \u0022Chicken Dinner\u0022: win a session (finish #1 in the game\u0027s final standings).\n\t\tif ( won ) Unlock( \u0022firstwin\u0022 );\n\n\t\tif ( !won || string.IsNullOrEmpty( gameId ) ) return;\n\t\tvar pd = PlayerData.Load();\n\t\tif ( pd.LastWinGameId == gameId ) return;\n\t\tpd.LastWinGameId = gameId;\n\t\tpd.Save();\n\t\ttry { Sandbox.Services.Stats.Increment( \u0022wins\u0022, 1 ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] wins stat failed: {e.Message}\u0022 ); }\n\t}\n\n\t// Out-of-band achievement push from the server (the \u0060achievement\u0060 WS frame):\n\t// fire the named unlock straight through. Used for feats the server detects off\n\t// the game socket \u2014 \u0060fart\u0060 (poked the backend into a 404) and \u0060hackerman\u0060\n\t// (fumbled the admin password) \u2014 matched back to this client by IP. The Unlock\n\t// is idempotent, so a repeat push is a harmless no-op.\n\tpublic static void OnAchievement( string ident )\n\t{\n\t\tif ( string.IsNullOrEmpty( ident ) ) return;\n\t\tUnlock( ident );\n\t}\n\n\t// Idempotent achievement unlock with a console trace. Manual unlocks give no\n\t// visible feedback in the editor (the ident must be defined on the published\n\t// package to actually pop), so the log line is how we confirm it fired.\n\tstatic void Unlock( string ident )\n\t{\n\t\tLog.Info( $\u0022[Splitclicker] fired achievement {ident}\u0022 );\n\t\ttry { Sandbox.Services.Achievements.Unlock( ident ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] unlock {ident} failed: {e.Message}\u0022 ); }\n\t}\n}\n"},{"Ident":"gamah.splitclicker","Path":"MusicGen.cs","FileName":"MusicGen.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Skafinity;\n\n/// \u003Csummary\u003E\n/// Deterministic procedural ska / reggae-rock track generator (issue: procedural music).\n///\n/// A player\u0027s 8-hex \u003Cc\u003Eplayer_tag\u003C/c\u003E seeds a portable PRNG (xmur3 \u2192 mulberry32);\n/// the PRNG drives every musical choice (tempo, key, progression, bass / skank /\n/// organ / lead / drum patterns) so the SAME tag always produces the SAME song.\n/// Output is interleaved stereo 16-bit PCM (for SoundStream) or a WAV (debug).\n///\n/// PORTABILITY (web parity): the *composition* is whatever the PRNG emits \u2014 a web\n/// client reproduces the same arrangement by mirroring xmur3 \u002B mulberry32 and this\n/// file\u0027s note-selection order, using the same \u003Csee cref=\u0022Config\u0022/\u003E values. Synthesis\n/// (oscillators/filters below) only needs matching for bit-identical audio.\n///\n/// Synthesis: subtractive \u2014 unison-detuned oscillators through a resonant low-pass\n/// state-variable filter with a cutoff envelope (warm, not \u00228-bit\u0022); full synth drum\n/// kit (kick/snare/toms/hats/crash \u002B fills). Default voicing aims for a Sublime vibe:\n/// laid-back reggae-rock tempo, bass-forward, prominent clean skank \u002B organ bubble.\n/// \u003C/summary\u003E\npublic sealed class MusicGen\n{\n\tpublic sealed class Config\n\t{\n\t\t// Genre \u2014 selects the instrument set \u002B arrangement. 0 = Ska (bass/skank/organ/lead/\n\t\t// horns/drums), 1 = Rock (drums/bass/keys/lead-gtr/rhythm-gtr), 2 = Country, 3 = Metal,\n\t\t// 4 = Punk (lean power-pop), 5 = Pop (synth/dance). New genres append here.\n\t\tpublic int Genre = 0;\n\n\t\t// Output\n\t\tpublic int SampleRate = 44100;\n\t\tpublic float TargetSeconds = 80f; // (legacy) song length now follows the structure\n\t\tpublic int Bars = 64;             // fallback if TargetSeconds \u003C= 0\n\n\t\t// Tempo \u2014 main is laid-back reggae-rock; Fast is an uptempo ska feel.\n\t\tpublic int BpmMin = 130, BpmMax = 185;\n\t\tpublic float FastChance = 0.30f;\n\t\tpublic int FastBpmMin = 150, FastBpmMax = 168;\n\t\tpublic float Swing = 0.14f, FastSwing = 0.05f;\n\n\t\t// Mix (per-voice gain pre-master) \u2014 the six \u0022volume\u0022 sliders are normalized:\n\t\t// same 0..1.5 range and the same 1.0 default (flat mix), tune from there.\n\t\tpublic float BassVol = 1.00f;\n\t\tpublic float SkankVol = 1.00f;\n\t\tpublic float OrganVol = 1.00f;\n\t\tpublic float MelodyVol = 1.00f;\n\t\tpublic float HornVol = 1.00f;\n\t\t// Per-part kit trims. The kit is balanced for EQUAL PEAK LEVEL in the file internally\n\t\t// (see *Balance consts below), so these knobs all share a 1.0 default \u2014 every piece\n\t\t// peaks at the same level out of the box and these are pure user trims around that.\n\t\tpublic float KickVol = 1.00f;\n\t\tpublic float SnareVol = 1.00f;\n\t\tpublic float TomVol = 1.00f;\n\t\tpublic float HatVol = 1.00f;\n\t\tpublic float CrashVol = 1.00f;\n\t\tpublic float DrumVol = 1.00f;         // master gain over the whole kit\n\t\t// Drum \u0022tone\u0022 \u2014 toms\u2194cymbals bias for the PART. 0 = boom (fills/decoration lean to\n\t\t// toms, cymbals pulled back), 0.5 = neutral, 1 = bright (fills/decoration lean to\n\t\t// cymbals, toms pulled back). Drives both what\u0027s played and a gentle per-voice gain lean.\n\t\tpublic float DrumTone = 0.5f;\n\t\t// Drum \u0022drive\u0022 \u2014 pull\u2194push timing feel (replaces the old DrumPush magnitude roll).\n\t\t// 0 = lay back behind the beat, 0.5 = dead-on, 1 = push ahead of the beat.\n\t\tpublic float DrumDrive = 0.5f;\n\n\t\t// Rock instruments (Genre 1). Bass \u002B drums reuse the shared knobs above.\n\t\t// KEYS \u2014 the offbeat-chord comp (was labelled \u0022rhythm guitar\u0022, but it always read as a\n\t\t// keyboard, so it\u0027s named for what it sounds like). Power-chord comping on every eighth.\n\t\tpublic float KeysVol = 1.00f;\n\t\tpublic float KeysCutoff = 1700f;      // Hz low-pass (darker wall)\n\t\tpublic float KeysDrive = 3.2f;        // distortion amount (tanh drive)\n\t\tpublic float KeysChug = 0.5f;         // 0 = ringing chords, 1 = tight palm-mute chug\n\t\t// RHYTHM GTR \u2014 twangy distorted power chords. Shares the lead guitar\u0027s voice but strums\n\t\t// chords at a lower base distortion than the lead.\n\t\tpublic float RhythmGtrVol = 1.00f;\n\t\tpublic float RhythmGtrCutoff = 2600f; // Hz low-pass on the rhythm guitar\n\t\tpublic float RhythmGtrDrive = 2.8f;   // distortion amount (tanh drive) \u2014 under the lead\n\t\tpublic float RhythmGtrChug = 0.5f;    // 0 = ringing chords, 1 = tight palm-mute chug\n\t\t// LEAD GTR \u2014 twangy, heavily distorted single-note lead.\n\t\tpublic float LeadGtrVol = 1.00f;\n\t\tpublic float LeadGtrCutoff = 2600f;   // Hz low-pass on the lead guitar\n\t\tpublic float LeadGtrDrive = 5.0f;     // distortion amount (tanh drive) \u2014 new floor of the DISTORTION knob\n\t\tpublic float LeadGtrBend = 0.30f;     // rock lead \u0022bendiness\u0022 0..1 \u2014 propensity to bend up into notes and scoop\n\n\t\t// Tone \u2014 low drives \u002B filtering for warmth; detune for width.\n\t\tpublic float Detune = 14f;        // cents, unison spread\n\t\tpublic float BassCutoff = 380f;   // Hz low-pass on bass\n\t\tpublic float SkankCutoff = 3000f; // Hz low-pass on skank\n\t\tpublic float SkankHighpass = 500f;// Hz high-pass to thin the skank (\u0022skank bite\u0022)\n\t\tpublic float SkankChop = 0.5f;    // skank chop length as a fraction of an eighth\n\t\tpublic float LeadCutoff = 3200f;  // Hz low-pass on lead\n\t\tpublic float OrganCutoff = 1400f; // Hz low-pass on the organ bubble\n\t\tpublic float OrganVibrato = 5.5f; // organ bubble vibrato depth\n\t\tpublic float HornCutoff = 3200f;  // Hz low-pass on the backing horns\n\t\tpublic float Resonance = 1.0f;    // SVF damping (lower = more resonant)\n\t\tpublic float BassDrive = 1.5f;\n\t\tpublic float SkankDrive = 1.3f;\n\t\tpublic float MelodyDrive = 1.3f;\n\t\tpublic float HornDrive = 1.4f;\n\t\tpublic float MasterDrive = 1.1f;\n\t\tpublic float MasterPeak = 0.95f;\n\n\t\t// Master room reverb \u2014 a touch of stereo space so the mix reads with depth\n\t\t// instead of dry/\u002216-bit\u0022. Wet = blend, Decay = tail length (0..1).\n\t\tpublic float MasterReverb = 0.5f;\n\t\tpublic float ReverbDecay = 0.5f;\n\n\t\t// Feel\n\t\tpublic float OctavePopChance = 0.30f;\n\t\tpublic float OrganBubbleChance = 0.55f;\n\t\tpublic float KickSyncChance = 0.25f;\n\t\tpublic float GhostSnareChance = 0.35f;\n\t\tpublic float FillChance = 0.6f;       // drum fill at phrase ends\n\t\tpublic float DrumBusy = 0.6f;         // 0..1 overall kit activity: 16th hats, ghosts, kick syncopation\n\t\tpublic float TripletChance = 0.06f;   // 0..0.2 chance of triplet/16th ornament in fills / lead runs (potent)\n\t\tpublic float BassTriplets = 0.06f;    // 0..0.1 bass-only 16th/triplet ornament rate (own knob)\n\t\tpublic float MelodyRestChance = 0.30f;\n\t\tpublic float MelodyLeapChance = 0.18f;\n\t\tpublic float MelodyVibrato = 5.0f;\n\n\t\t// STEREO WIDTH (vibe slider): master 0..1 stereo amount. Scales the lead/horn placement\n\t\t// AND the drum pan \u002B double-tracking spread/decorrelation (see _widthScale). 1 = full\n\t\t// design width (the \u0022100%\u0022 the rest of the mix is tuned for); 0 = mono.\n\t\tpublic float PanAmount = 1.0f;\n\n\t\t// Lead instrument weights (RNG picks one; ForceInstrument overrides:\n\t\t// -1 = RNG, 0=Trumpet 1=Sax 2=Organ 3=Trombone).\n\t\tpublic float TrumpetWeight = 1.0f;\n\t\tpublic float SaxWeight = 1.0f;\n\t\tpublic float OrganWeight = 0.8f;\n\t\tpublic float TromboneWeight = 0.4f;\n\t\tpublic int ForceInstrument = -1;\n\n\t\t// Backing horn section\n\t\tpublic float HornSectionChance = 0.5f;\n\t\tpublic float HornDensity = 0.35f;\n\n\t\t// \u2500\u2500 Advanced peak-balance / level tuning (NOT vibe knobs) \u2500\u2500\n\t\t// These were hardcoded consts; they\u0027re Config fields so the house mix can be retuned at\n\t\t// runtime (web: config.json) without a rebuild. They\u0027re deliberately kept OUT of the vibe\n\t\t// seed and the per-genre slider grid (see VibeCodec.AdvancedFields) \u2014 they shape the\n\t\t// baseline mix, not a song\u0027s shareable identity. Edit these to re-balance, not the *Vol\n\t\t// defaults (which stay a flat 1.0 so the knobs read as pure user trims around this).\n\t\t//\n\t\t// KitPresence: drums are short transients fighting a sustained melodic bed; this baseline\n\t\t// boost lets the kit sit in the mix at DRUMS = 1.0 (parity with the other voice sliders).\n\t\tpublic float KitPresence = 2.0f;\n\t\t// Kit per-voice level balance. The pieces synthesise at very different raw levels (a kick\n\t\t// is huge, a hat is thin noise). Starting point was EQUAL PRE-MASTER PEAK (kick 0.40 /\n\t\t// snare 0.476 / tom 0.496 / hat 0.582 / crash 0.515), then hand-tuned by ear: equal peak\n\t\t// buried the sparse kick/snare/toms under the constant hats, so drums were pushed back up\n\t\t// and the hats backed off.\n\t\tpublic float KickBalance = 0.800f;\n\t\tpublic float SnareBalance = 0.900f;\n\t\tpublic float TomBalance = 0.780f;     // tuned by ear\n\t\tpublic float HatBalance = 0.407f;     // equal-peak baseline (0.582) backed off 30% by ear\n\t\tpublic float CrashBalance = 0.515f;\n\t\t// Melodic-voice peak balance \u2014 the instrument analog of the kit balances above (same\n\t\t// equal-peak-then-tune workflow). Bass measured the same peak across genres, so one value.\n\t\tpublic float BassBalance = 0.733f;\n\t\tpublic float SkankBalance = 1.223f;\n\t\tpublic float OrganBalance = 1.237f;\n\t\tpublic float MelodyBalance = 0.896f;  // ska horn lead\n\t\tpublic float HornBalance = 1.142f;    // backing horn section\n\t\tpublic float KeysBalance = 1.065f;    // rock offbeat keys\n\t\tpublic float RhythmGtrBalance = 1.053f;\n\t\tpublic float LeadGtrBalance = 0.896f;\n\n\t\t// \u2500\u2500 Stereo double-tracking (width) \u2500\u2500 (config-only; see VibeCodec.AdvancedFields)\n\t\t// Widen the non-drum voices the way a band double-tracks guitars (bass excepted \u2014 it\n\t\t// stays centred for a tight, mono-safe low end): each widened note is rendered\n\t\t// as TWO decorrelated \u0022takes\u0022 panned apart \u2014 NOT one mono signal copied to both channels\n\t\t// (that just collapses to centre). The width comes entirely from the micro-differences\n\t\t// between the takes: a few cents of detune, a small constant timing offset \u002B per-note\n\t\t// jitter, independent oscillator start phase, and per-note amplitude/cutoff variation.\n\t\t// Tunable at runtime (no rebuild). Drums are panned separately (see DrumPan).\n\t\tpublic float DoubleTrack = 1f;        // master enable: \u003C0.5 = off (single centred take, as before)\n\t\tpublic float WidthBacking = 0.5f;     // pan spread (\u00B1) for backing voices \u2014 50% L/R\n\t\tpublic float WidthLead = 1.0f;        // pan spread (\u00B1) for a doubled lead. INERT by default: the\n\t\t                                      // lead is monophonic and rendered as a single clean take\n\t\t                                      // (see RenderLeadNote) \u2014 doubling a solo line smeared pitch\n\t\t                                      // (\u0022out of key\u0022). Only takes effect if the lead path is\n\t\t                                      // switched back to lead:true. Chordal voices use WidthBacking.\n\t\tpublic float WidthDetune = 6f;        // cents BETWEEN the two takes (split \u00B1half each)\n\t\tpublic float WidthDelayMs = 9f;       // constant timing offset of the 2nd take (ms)\n\t\tpublic float WidthJitterMs = 4f;      // extra random per-note timing jitter (ms, \u00B1 per take)\n\t\tpublic float WidthAmpVar = 0.08f;     // per-note amplitude variation between takes (0..1)\n\t\tpublic float WidthCutoffVar = 0.10f;  // per-note filter-cutoff variation between takes (0..1)\n\t}\n\n\treadonly Config _c;\n\treadonly int _sr;\n\treadonly float _drumGain;   // master kit gain \u2014 straight 0..1.5 slider \u00D7 Config.KitPresence baseline\n\tfloat[] _bufL, _bufR;\n\n\tMusicGen( Config c ) { _c = c ?? new Config(); _sr = _c.SampleRate; _drumGain = Math.Clamp( _c.DrumVol, 0f, 1.5f ) * _c.KitPresence; }\n\n\tpublic const int Channels = 2;\n\n\tpublic static byte[] Generate( string tag, Config cfg = null )\n\t{\n\t\tvar g = new MusicGen( cfg );\n\t\treturn g.EncodeWav( g.Compose( tag ) );\n\t}\n\n\tpublic static short[] GenerateSamples( string tag, Config cfg, out int sampleRate )\n\t{\n\t\tvar g = new MusicGen( cfg );\n\t\tfloat gain = g.Compose( tag );\n\t\tsampleRate = g._sr;\n\t\treturn g.ToShorts( gain );\n\t}\n\n\t// \u2500\u2500 Chunked generation (parallel synthesis) \u2500\u2500\n\t// Composition \u002B drum synthesis are sequential (RNG-bound); pitched-voice synthesis\n\t// pulls no RNG, so the caller can split it across worker threads. Flow:\n\t//   var g = MusicGen.BeginPlan( tag, cfg );            // sequential plan \u002B drums\n\t//   parallel-for window in 0..g.TotalSamples: g.RenderPitchedRange( from, to );\n\t//   short[] pcm = g.FinishStereo();                    // master \u002B interleave\n\tpublic static MusicGen BeginPlan( string tag, Config cfg )\n\t{\n\t\tvar g = new MusicGen( cfg );\n\t\tg.ComposePlan( tag );\n\t\treturn g;\n\t}\n\n\tpublic int TotalSamples =\u003E _bufL?.Length ?? 0;\n\tpublic int SampleRate =\u003E _sr;\n\n\t/// \u003Csummary\u003EMaster-normalize and interleave to stereo 16-bit PCM. Call after every\n\t/// \u003Csee cref=\u0022RenderPitchedRange\u0022/\u003E window has finished.\u003C/summary\u003E\n\tpublic short[] FinishStereo() =\u003E ToShorts( Master() );\n\n\tconst int EighthsPerBar = 8;\n\n\t// \u2500\u2500 Portable PRNG (mirror exactly in the web client) \u2500\u2500\n\tstatic uint Xmur3( string str )\n\t{\n\t\tuint h = 1779033703u ^ (uint)str.Length;\n\t\tfor ( int i = 0; i \u003C str.Length; i\u002B\u002B )\n\t\t{\n\t\t\th = unchecked( (h ^ str[i]) * 3432918353u );\n\t\t\th = (h \u003C\u003C 13) | (h \u003E\u003E 19);\n\t\t}\n\t\th = unchecked( (h ^ (h \u003E\u003E 16)) * 2246822507u );\n\t\th = unchecked( (h ^ (h \u003E\u003E 13)) * 3266489909u );\n\t\treturn h ^ (h \u003E\u003E 16);\n\t}\n\n\tsealed class Rng\n\t{\n\t\tuint _a;\n\t\tpublic Rng( uint seed ) { _a = seed; }\n\t\tpublic float Next()\n\t\t{\n\t\t\t_a = unchecked( _a \u002B 0x6D2B79F5u );\n\t\t\tuint t = _a;\n\t\t\tt = unchecked( (t ^ (t \u003E\u003E 15)) * (t | 1u) );\n\t\t\tt ^= unchecked( t \u002B (t ^ (t \u003E\u003E 7)) * (t | 61u) );\n\t\t\treturn (t ^ (t \u003E\u003E 14)) / 4294967296f;\n\t\t}\n\t\tpublic int Int( int n ) =\u003E n \u003C= 0 ? 0 : Math.Min( n - 1, (int)(Next() * n) );\n\t\tpublic bool Chance( float p ) =\u003E Next() \u003C p;\n\t\tpublic T Pick\u003CT\u003E( T[] arr ) =\u003E arr[Int( arr.Length )];\n\t}\n\n\t// \u2500\u2500 Harmony \u2500\u2500\n\t// Major-leaning scales (Sublime / reggae sit in major \u0026 mixolydian mostly).\n\tstatic readonly int[][] Scales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 10 }, // mixolydian\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted)\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted \u2014 was dorian; ska/reggae stays bright)\n\t};\n\n\tstatic readonly int[][] Progressions =\n\t{\n\t\tnew[] { 0, 4, 5, 3 }, // I\u2013V\u2013vi\u2013IV\n\t\tnew[] { 0, 3, 4, 4 }, // I\u2013IV\u2013V\u2013V\n\t\tnew[] { 0, 5, 3, 4 }, // I\u2013vi\u2013IV\u2013V\n\t\tnew[] { 0, 6, 3, 0 }, // I\u2013bVII\u2013IV\u2013I (mixolydian)\n\t\tnew[] { 5, 3, 0, 4 }, // vi\u2013IV\u2013I\u2013V\n\t\tnew[] { 0, 3, 0, 4 }, // I\u2013IV\u2013I\u2013V\n\t};\n\n\t// Bass patterns: semitone offsets from the chord root per eighth; -99 = rest\n\t// (note sustains to the next onset). Slot 7 is the \u0022approach\u0022 \u2192 walks to the\n\t// next chord. Mix of melodic / one-drop / rocking / busy ska.\n\tconst int Rest = -99, Approach = 99;\n\tstatic readonly int[][] BassPatterns =\n\t{\n\t\tnew[] { 0, Rest, 0, 12, Rest, 7, 5, Approach },   // sublime melodic\n\t\tnew[] { Rest, Rest, 0, Rest, 0, Rest, 7, Approach }, // one-drop spacey\n\t\tnew[] { 0, Rest, 7, Rest, 12, Rest, 7, Approach },   // rocking\n\t\tnew[] { 0, 12, 0, 7, 0, 12, 7, Approach },           // busy ska\n\t\tnew[] { 0, Rest, 0, Rest, 5, Rest, 7, Approach },    // root\u2013fifth\n\t};\n\n\t// \u2500\u2500 Rock harmony (Genre 1) \u2500\u2500\n\t// Darker, power-chord-friendly modes (minor / dorian / mixolydian) so rock doesn\u0027t\n\t// share ska\u0027s bright major themes. Picked with the SAME RNG draw as the ska tables, so\n\t// ska songs are byte-identical \u2014 only genre 1 reads these.\n\t// All minor-3rd modes: the RockProgressions are written/labelled as MINOR (i\u2013\u266DVII\u2013\u266DVI \u2026), so\n\t// pairing them with a major-3rd mode (e.g. mixolydian) flipped the tonic major and the dark\n\t// rock vamp evaporated. Aeolian/dorian/phrygian keep a minor tonic \u2014 the progression reads as\n\t// intended \u2014 while still giving modal variety (dorian\u0027s \u266E6, phrygian\u0027s \u266DII colour). 4 entries\n\t// kept so the genre Pick stays at the same draw and other genres are byte-identical.\n\tstatic readonly int[][] RockScales =\n\t{\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (aeolian)\n\t\tnew[] { 0, 2, 3, 5, 7, 9, 10 }, // dorian (minor, brighter \u266E6 \u2014 classic rock)\n\t\tnew[] { 0, 1, 3, 5, 7, 8, 10 }, // phrygian (dark)\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (weighted)\n\t};\n\n\t// Degrees are read against the (often minor) scale, so 5 = \u266DVI, 6 = \u266DVII, 3 = iv, 4 = v.\n\tstatic readonly int[][] RockProgressions =\n\t{\n\t\tnew[] { 0, 6, 5, 6 }, // i\u2013\u266DVII\u2013\u266DVI\u2013\u266DVII (driving rock vamp)\n\t\tnew[] { 0, 5, 6, 0 }, // i\u2013\u266DVI\u2013\u266DVII\u2013i\n\t\tnew[] { 0, 6, 3, 0 }, // i\u2013\u266DVII\u2013IV\u2013i (mixolydian rock)\n\t\tnew[] { 0, 3, 6, 0 }, // i\u2013iv\u2013\u266DVII\u2013i\n\t\tnew[] { 0, 0, 6, 6 }, // i / \u266DVII riff vamp\n\t\tnew[] { 0, 3, 4, 0 }, // i\u2013iv\u2013v\u2013i\n\t};\n\n\t// Driving root/octave eighths that lock to the kick \u2014 the rock engine room, vs ska\u0027s\n\t// syncopated off-beat one-drop bass.\n\tstatic readonly int[][] RockBassPatterns =\n\t{\n\t\tnew[] { 0, 0, 0, 0, 0, 0, 0, Approach },         // straight eighth chug\n\t\tnew[] { 0, Rest, 0, Rest, 0, Rest, 0, Approach },// quarter-note pulse\n\t\tnew[] { 0, 0, 12, 0, 0, 0, 12, Approach },       // root with octave pushes\n\t\tnew[] { 0, 0, 7, 0, 0, 0, 7, Approach },         // root\u2013fifth gallop\n\t\tnew[] { 0, Rest, 0, 0, Rest, 0, 12, Approach },  // syncopated driver\n\t};\n\n\t// \u2500\u2500 Country harmony (Genre 2) \u2500\u2500\n\t// Bright and major \u2014 country lives in major / mixolydian. Same RNG draws as the other\n\t// tables, so songs in other genres are untouched.\n\tstatic readonly int[][] CountryScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 10 }, // mixolydian\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted)\n\t};\n\n\tstatic readonly int[][] CountryProgressions =\n\t{\n\t\tnew[] { 0, 3, 4, 0 }, // I\u2013IV\u2013V\u2013I (the country backbone)\n\t\tnew[] { 0, 0, 4, 4 }, // I\u2013V vamp\n\t\tnew[] { 0, 3, 0, 4 }, // I\u2013IV\u2013I\u2013V\n\t\tnew[] { 0, 4, 5, 3 }, // I\u2013V\u2013vi\u2013IV\n\t\tnew[] { 0, 4, 0, 4 }, // I\u2013V two-chord\n\t};\n\n\t// \u0022Boom-chick\u0022 alternating root/fifth on the beats (the guitar/snare take the off \u0022chick\u0022),\n\t// walking up to the next chord on the approach.\n\tstatic readonly int[][] CountryBassPatterns =\n\t{\n\t\tnew[] { 0, Rest, 7, Rest, 0, Rest, 7, Approach },  // alternating root\u2013fifth\n\t\tnew[] { 0, Rest, 7, Rest, 12, Rest, 7, Approach }, // root\u2013fifth with the octave\n\t\tnew[] { 0, Rest, 7, Rest, 0, Rest, 5, Approach },  // root\u2013fifth, lean on the 4th\n\t\tnew[] { 0, Rest, 4, Rest, 7, Rest, 5, Approach },  // walking-ish\n\t};\n\n\t// \u2500\u2500 Metal harmony (Genre 3) \u2500\u2500\n\t// Dark and tight \u2014 natural minor / phrygian / harmonic minor for the menacing power-chord\n\t// riffs. Same RNG draws as the other tables.\n\tstatic readonly int[][] MetalScales =\n\t{\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (aeolian)\n\t\tnew[] { 0, 1, 3, 5, 7, 8, 10 }, // phrygian (the metal mode)\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 11 }, // harmonic minor\n\t\tnew[] { 0, 2, 3, 5, 7, 8, 10 }, // natural minor (weighted)\n\t};\n\n\t// Degrees read against the (minor) scale: 5 = \u266DVI, 6 = \u266DVII, 1 = \u266DII, 3 = iv.\n\tstatic readonly int[][] MetalProgressions =\n\t{\n\t\tnew[] { 0, 5, 6, 0 }, // i\u2013\u266DVI\u2013\u266DVII\u2013i\n\t\tnew[] { 0, 6, 5, 6 }, // i\u2013\u266DVII\u2013\u266DVI\u2013\u266DVII (driving)\n\t\tnew[] { 0, 1, 0, 6 }, // i\u2013\u266DII\u2013i\u2013\u266DVII (phrygian menace)\n\t\tnew[] { 0, 0, 5, 6 }, // i pedal \u2192 \u266DVI\u2013\u266DVII\n\t\tnew[] { 0, 6, 3, 0 }, // i\u2013\u266DVII\u2013iv\u2013i\n\t};\n\n\t// Driving roots locked to the double-kick; octave pushes for the gallop.\n\tstatic readonly int[][] MetalBassPatterns =\n\t{\n\t\tnew[] { 0, 0, 0, 0, 0, 0, 0, Approach },         // straight chug\n\t\tnew[] { 0, 0, 12, 0, 0, 0, 12, Approach },       // root with octave pushes\n\t\tnew[] { 0, 0, 0, 12, 0, 0, 0, Approach },        // syncopated octave\n\t\tnew[] { 0, Rest, 0, 0, Rest, 0, 0, Approach },   // syncopated driver\n\t};\n\n\t// \u2500\u2500 Punk harmony (Genre 4) \u2500\u2500\n\t// \u0022Lean punk\u0022 / power-pop: bright and major (the opposite pole to rock\u0027s dark minor), riding\n\t// the anthemic four-chord turnarounds at speed. Same RNG draws as the other tables.\n\tstatic readonly int[][] PunkScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (the pop-punk default)\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 10 }, // mixolydian (a little grit on the \u266D7)\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted)\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted)\n\t};\n\n\t// Major degrees: 3 = IV, 4 = V, 5 = vi \u2014 the anthem turnarounds.\n\tstatic readonly int[][] PunkProgressions =\n\t{\n\t\tnew[] { 0, 4, 5, 3 }, // I\u2013V\u2013vi\u2013IV (the pop-punk anthem)\n\t\tnew[] { 5, 3, 0, 4 }, // vi\u2013IV\u2013I\u2013V\n\t\tnew[] { 0, 5, 3, 4 }, // I\u2013vi\u2013IV\u2013V (50s changes, sped up)\n\t\tnew[] { 0, 3, 4, 4 }, // I\u2013IV\u2013V\u2013V\n\t\tnew[] { 0, 4, 3, 4 }, // I\u2013V\u2013IV\u2013V (three-chord drive)\n\t};\n\n\t// Driving root/octave eighths locked to the kick \u2014 same engine room as rock, pushed harder.\n\tstatic readonly int[][] PunkBassPatterns =\n\t{\n\t\tnew[] { 0, 0, 0, 0, 0, 0, 0, Approach },         // straight eighth chug\n\t\tnew[] { 0, 0, 12, 0, 0, 0, 12, Approach },       // root with octave pushes\n\t\tnew[] { 0, 0, 7, 0, 0, 0, 7, Approach },         // root\u2013fifth gallop\n\t\tnew[] { 0, Rest, 0, Rest, 0, Rest, 0, Approach },// quarter-note pulse\n\t};\n\n\t// \u2500\u2500 Pop harmony (Genre 5) \u2500\u2500\n\t// Modern synth/dance-pop: bright major / lydian over the ubiquitous four-chord loops, built\n\t// to sit on a four-on-the-floor kick. Same RNG draws as the other tables.\n\tstatic readonly int[][] PopScales =\n\t{\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major\n\t\tnew[] { 0, 2, 4, 6, 7, 9, 11 }, // lydian (the sparkly \u266F4 \u2014 synth-pop shimmer)\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted)\n\t\tnew[] { 0, 2, 4, 5, 7, 9, 11 }, // major (weighted)\n\t};\n\n\tstatic readonly int[][] PopProgressions =\n\t{\n\t\tnew[] { 0, 4, 5, 3 }, // I\u2013V\u2013vi\u2013IV\n\t\tnew[] { 5, 3, 0, 4 }, // vi\u2013IV\u2013I\u2013V (the \u0022Axis\u0022 loop)\n\t\tnew[] { 0, 5, 3, 4 }, // I\u2013vi\u2013IV\u2013V\n\t\tnew[] { 0, 3, 0, 4 }, // I\u2013IV\u2013I\u2013V (two-chord pulse)\n\t};\n\n\t// Four-on-the-floor synth bass: a steady root pulse with octave pops for bounce.\n\tstatic readonly int[][] PopBassPatterns =\n\t{\n\t\tnew[] { 0, Rest, 0, Rest, 0, Rest, 0, Approach },  // root on every beat (locks the floor)\n\t\tnew[] { 0, 0, 12, 0, 0, 0, 12, Approach },         // root with octave pops\n\t\tnew[] { 0, 0, 0, 0, 0, 0, 0, Approach },           // straight eighth pulse\n\t\tnew[] { 0, Rest, 0, 0, Rest, 0, 12, Approach },    // syncopated synth bass\n\t};\n\n\t// The genre\u0027s harmony tables (one RNG Pick each, so other genres stay byte-identical).\n\tstatic int[][] ScalesFor( int g ) =\u003E g switch\n\t{ 1 =\u003E RockScales, 2 =\u003E CountryScales, 3 =\u003E MetalScales, 4 =\u003E PunkScales, 5 =\u003E PopScales, _ =\u003E Scales };\n\tstatic int[][] ProgressionsFor( int g ) =\u003E g switch\n\t{ 1 =\u003E RockProgressions, 2 =\u003E CountryProgressions, 3 =\u003E MetalProgressions, 4 =\u003E PunkProgressions, 5 =\u003E PopProgressions, _ =\u003E Progressions };\n\tstatic int[][] BassPatternsFor( int g ) =\u003E g switch\n\t{ 1 =\u003E RockBassPatterns, 2 =\u003E CountryBassPatterns, 3 =\u003E MetalBassPatterns, 4 =\u003E PunkBassPatterns, 5 =\u003E PopBassPatterns, _ =\u003E BassPatterns };\n\n\tint[] _scale, _prog;\n\tint _rootMidi;\n\tInstrument _lead;\n\tfloat _leadPan;\n\tfloat _widthScale = 1f;  // STEREO WIDTH slider (PanAmount) as a 0..1 master: scales the drum\n\t                         // pan AND the double-tracking spread/decorrelation. 1 = full (design)\n\t                         // width; 0 = everything collapses to centre (mono).\n\tfloat _drumPan = DrumPan;// per-song effective drum spread = DrumPan * _widthScale\n\tbool _hasHorns;\n\tbool[] _hornMask;\n\tint[] _bassPat;\n\tint _drumStyle;          // 0 one-drop, 1 steppers, 2 straight backbeat\n\tint[] _kickAccents = Array.Empty\u003Cint\u003E(); // per-song backbeat kick accents (see BackbeatKickAccents)\n\tbool _ride;              // per-SECTION: ride cymbal drives the eighth pulse instead of closed hats (set in RenderSection from _ridePref)\n\tfloat _ridePref;         // per-song lean toward riding the ride vs the hats; each section rolls its own _ride against this\n\tbool _crashBrightLeft;   // per-song: which side the kit\u0027s two crashes sit on (bright crash left \u21C4 dark crash right, or flipped)\n\tbool _organBubble;\n\tbool _fast;\n\tint _genre;              // 0 ska, 1 rock, 2 country, 3 metal, 4 punk, 5 pop\n\tstring _tag;             // the per-song seed string, reused to seed per-section streams\n\tint _drumPush;           // per-song-constant kit timing bias in samples (\u2212 ahead / \u002B back)\n\tfloat _swing;            // per-song shuffle: offbeat eighths pushed late by this fraction of an eighth (whole band, via Swung)\n\tfloat _drumTone = 0.5f;  // DrumTone 0..1 \u2192 toms\u2194cymbals CONTENT bias in fills/groove decoration\n\tfloat _drumLowMul = 1f;  // DrumTone \u2192 kick/tom gain lean (gentle, on top of the content bias)\n\tfloat _drumHighMul = 1f; // DrumTone \u2192 hat/cymbal gain lean (gentle, on top of the content bias)\n\n\t// \u2500\u2500 Song structure \u2500\u2500\n\t// A song is an ordered list of sections. Hardcoded for now (will be RNG-generated once\n\t// there are more part types); the fixed run is intro \u2192 chorus \u2192 verse(0) \u2192 chorus \u2192\n\t// verse(1) \u2192 chorus \u2192 ending. Non-lead voices are seeded by section TYPE so every chorus\n\t// (and both verses) play identical backing; the lead is seeded by type \u002B verse index so\n\t// it evolves across the Nth verse; the section-end fill is seeded by absolute index so\n\t// every section closes with a different fill.\n\tenum Section { Intro, Chorus, Verse, Ending }\n\n\t// Extra seconds appended after the last bar so the ending\u0027s final tonic chord (and the\n\t// master reverb) can ring out naturally instead of being clipped at the buffer edge.\n\tconst float RingOutTail = 2.4f;\n\n\treadonly struct Part\n\t{\n\t\tpublic readonly Section Type; public readonly int Bars; public readonly int VerseIndex;\n\t\tpublic Part( Section t, int bars, int verse ) { Type = t; Bars = bars; VerseIndex = verse; }\n\t}\n\n\tstatic List\u003CPart\u003E BuildStructure() =\u003E new()\n\t{\n\t\tnew Part( Section.Intro,  4, 0 ),\n\t\tnew Part( Section.Chorus, 8, 0 ),\n\t\tnew Part( Section.Verse,  8, 0 ),\n\t\tnew Part( Section.Chorus, 8, 0 ),\n\t\tnew Part( Section.Verse,  8, 1 ),\n\t\tnew Part( Section.Chorus, 8, 0 ),\n\t\tnew Part( Section.Ending, 2, 0 ),\n\t};\n\n\tstatic string SectionKey( Section s ) =\u003E s switch\n\t{\n\t\tSection.Intro =\u003E \u0022intro\u0022,\n\t\tSection.Chorus =\u003E \u0022chorus\u0022,\n\t\tSection.Verse =\u003E \u0022verse\u0022,\n\t\t_ =\u003E \u0022ending\u0022,\n\t};\n\n\t// Single-threaded generation (used by Generate / GenerateSamples). The controller\n\t// uses the chunked path instead (BeginPlan \u2192 parallel RenderPitchedRange \u2192 FinishStereo).\n\tfloat Compose( string tag )\n\t{\n\t\tComposePlan( tag );\n\t\tRenderPitchedRange( 0, _bufL.Length );\n\t\treturn Master();\n\t}\n\n\t// Sequential planning pass: RNG composition \u002B drum synthesis written straight into\n\t// the buffer, while every pitched note is collected as an event (rendered later,\n\t// possibly in parallel). RNG draw order is identical to the old inline render \u2014\n\t// RenderPatch now only enqueues, and it never pulled RNG anyway.\n\tvoid ComposePlan( string tag )\n\t{\n\t\t_events.Clear();\n\t\t_tag = string.IsNullOrEmpty( tag ) ? \u0022rotaliate\u0022 : tag;\n\t\t_genre = Math.Clamp( _c.Genre, 0, 5 );\n\t\tvar rng = new Rng( Xmur3( _tag.ToLowerInvariant() ) );\n\n\t\t// TEMPO BIAS \u2014 punk always runs hot (it\u0027s the fast genre); the roll still consumes one\n\t\t// draw so every other genre\u0027s later picks stay byte-identical.\n\t\t_fast = rng.Chance( _c.FastChance ) || _genre == 4;\n\t\tint bpm = _fast\n\t\t\t? _c.FastBpmMin \u002B rng.Int( Math.Max( 1, _c.FastBpmMax - _c.FastBpmMin \u002B 1 ) )\n\t\t\t: _c.BpmMin \u002B rng.Int( Math.Max( 1, _c.BpmMax - _c.BpmMin \u002B 1 ) );\n\t\t_scale = rng.Pick( ScalesFor( _genre ) );\n\t\t_prog = rng.Pick( ProgressionsFor( _genre ) );\n\t\t_rootMidi = 28 \u002B rng.Int( 8 );                    // E1..B1 bass root\n\t\t_lead = Instrument.Trumpet;                       // ska lead is fixed; other genres use guitar\n\t\t_leadPan = (rng.Next() * 2f - 1f) * _c.PanAmount;\n\t\t_widthScale = Math.Clamp( _c.PanAmount, 0f, 1f );\n\t\t_drumPan = DrumPan * _widthScale;\n\t\t_bassPat = rng.Pick( BassPatternsFor( _genre ) );\n\t\t_drumStyle = _genre switch                        // 0 ska rolls a style; the rest are fixed\n\t\t{\n\t\t\t1 =\u003E 2,                                       // rock: straight backbeat\n\t\t\t2 =\u003E 2,                                       // country: train-beat backbeat\n\t\t\t3 =\u003E 3,                                       // metal: double-kick\n\t\t\t4 =\u003E 2,                                       // punk: straight backbeat (fast)\n\t\t\t5 =\u003E 4,                                       // pop: four-on-the-floor\n\t\t\t_ =\u003E _fast ? 2 : rng.Int( 2 ),\n\t\t};\n\t\t_organBubble = true;\n\t\t_hasHorns = true;\n\t\t_hornMask = new bool[EighthsPerBar];\n\t\t_hornMask[0] = true;\n\t\tfor ( int e = 1; e \u003C EighthsPerBar; e\u002B\u002B )\n\t\t\t_hornMask[e] = rng.Chance( _c.HornDensity * (e % 2 == 1 ? 1.3f : 0.5f) );\n\t\t// How much this song leans on the ride cymbal vs the closed hats for the main pulse.\n\t\t// Every song can do both \u2014 each SECTION rolls its own choice against this preference\n\t\t// (see RenderSection), so a song can hat the verse and ride the chorus. The lean is\n\t\t// genre-biased (rock/metal ride more) and spread per song so some strongly prefer one.\n\t\t// One rng.Next() (same draw count as the old single Chance), so later draws stay aligned.\n\t\tfloat rideBase = _genre switch { 1 =\u003E 0.55f, 3 =\u003E 0.65f, 2 =\u003E 0.30f, 4 =\u003E 0.20f, 5 =\u003E 0.30f, _ =\u003E 0.40f };\n\t\t_ridePref = Math.Clamp( rideBase - 0.25f \u002B 0.5f * rng.Next(), 0f, 1f );\n\t\t// Which side the two crashes sit on (\u00B125%); flips per song so the stereo image varies.\n\t\t_crashBrightLeft = rng.Chance( 0.5f );\n\t\t// This song\u0027s backbeat kick personality \u2014 which off-beat eighths the kick leans into\n\t\t// beyond the fixed beat-1 \u0026 3 anchors. Only the straight backbeat (rock/country/fast\n\t\t// ska) reads it; drawn last so it shifts no earlier choice.\n\t\t_kickAccents = rng.Pick( BackbeatKickAccents );\n\n\t\t_swing = _fast ? _c.FastSwing : _c.Swing;\n\t\tif ( _genre == 5 ) _swing *= 0.25f;               // pop sits on a tight, straight dance grid\n\t\tdouble secPerEighth = 60.0 / bpm / 2.0;\n\t\tint spe = (int)Math.Round( _sr * secPerEighth );\n\n\t\t// Drum tone (toms\u2194cymbals) \u2192 per-voice gain split, and drive (pull\u2194push) \u2192 a constant\n\t\t// kit timing bias (\u2212 = ahead/push, \u002B = behind/lay back; 0.5 = dead on).\n\t\tfloat dt = Math.Clamp( _c.DrumTone, 0f, 1f );\n\t\t_drumTone = dt;\n\t\t// Gentle gain lean (neutral at 0.5 so the balanced kit is untouched there); the\n\t\t// bulk of the toms\u2194cymbals bias now comes from what the part actually plays.\n\t\t_drumLowMul = 1.2f - 0.4f * dt;\n\t\t_drumHighMul = 0.7f \u002B 0.6f * dt;\n\t\t_drumPush = (int)Math.Round( (0.5f - Math.Clamp( _c.DrumDrive, 0f, 1f )) * 2f * 0.13f * spe );\n\n\t\t// Lay out the structure and size the buffers to its total length.\n\t\tvar structure = BuildStructure();\n\t\tint totalBars = 0;\n\t\tforeach ( var p in structure ) totalBars \u002B= p.Bars;\n\t\t// Size to the structure plus a ring-out tail, so the ending\u0027s final chord and the\n\t\t// reverb decay into real silence past the last bar rather than being cut off.\n\t\tint structured = spe * EighthsPerBar * totalBars;\n\t\tint total = structured \u002B (int)(_sr * RingOutTail);\n\t\t_bufL = new float[total];\n\t\t_bufR = new float[total];\n\n\t\tint barCursor = 0;\n\t\tfor ( int si = 0; si \u003C structure.Count; si\u002B\u002B )\n\t\t{\n\t\t\tvar part = structure[si];\n\t\t\tRenderSection( part, si, barCursor * EighthsPerBar * spe, spe, secPerEighth );\n\t\t\tbarCursor \u002B= part.Bars;\n\t\t}\n\t}\n\n\t// Render one section. Each voice gets its own per-section RNG stream keyed so that repeats\n\t// of a section type reproduce identical backing, while the lead key folds in the verse\n\t// index (so the Nth verse\u0027s lead differs) and the fill key folds in the absolute section\n\t// index (so every section closes with a unique fill).\n\tvoid RenderSection( Part part, int absIndex, int sectionStart, int spe, double secPerEighth )\n\t{\n\t\tstring bk = SectionKey( part.Type );\n\t\tstring lk = part.Type == Section.Verse ? $\u0022verse:{part.VerseIndex}\u0022 : bk;\n\t\tvar bassRng = new Rng( Xmur3( $\u0022{_tag}:bass:{bk}\u0022 ) );\n\t\tvar bassOrn = new Rng( Xmur3( $\u0022{_tag}:bassorn:{bk}\u0022 ) );\n\t\tvar rhythmRng = new Rng( Xmur3( $\u0022{_tag}:rhythm:{bk}\u0022 ) );\n\t\tvar keysRng = new Rng( Xmur3( $\u0022{_tag}:keys:{bk}\u0022 ) );\n\t\tvar hornRng = new Rng( Xmur3( $\u0022{_tag}:horn:{bk}\u0022 ) );\n\t\tvar leadRng = new Rng( Xmur3( $\u0022{_tag}:lead:{lk}\u0022 ) );\n\t\t// Expression (vibrato/bend/glide/scoop) rolls off their own stream so adding them\n\t\t// leaves every voice\u0027s existing note CHOICES untouched \u2014 only pitch-shaping is layered on.\n\t\tvar exprRng = new Rng( Xmur3( $\u0022{_tag}:expr:{lk}\u0022 ) );\n\t\tvar noise = new Rng( Xmur3( $\u0022{_tag}:drums:{bk}\u0022 ) );\n\t\t// Hats vs ride is decided per section off its own stream (keyed by section TYPE, so every\n\t\t// chorus rides-or-hats the same, but a verse can differ). Rolled against the song\u0027s\n\t\t// _ridePref \u2014 independent stream, so it disturbs no other voice\u0027s draws.\n\t\t_ride = new Rng( Xmur3( $\u0022{_tag}:ride:{bk}\u0022 ) ).Chance( _ridePref );\n\t\tvar fillRng = new Rng( Xmur3( $\u0022{_tag}:fill:{absIndex}\u0022 ) );\n\t\tvar fillNoise = new Rng( Xmur3( $\u0022{_tag}:fillnoise:{absIndex}\u0022 ) );\n\n\t\tbool isIntro = part.Type == Section.Intro;\n\t\tbool isEnding = part.Type == Section.Ending;\n\n\t\tfor ( int bar = 0; bar \u003C part.Bars; bar\u002B\u002B )\n\t\t{\n\t\t\tint chord = (bar / 2) % _prog.Length;\n\t\t\tint nextChord = ((bar / 2) \u002B 1) % _prog.Length;\n\t\t\tint barStart = sectionStart \u002B bar * EighthsPerBar * spe;\n\n\t\t\t// The ending lands on a held tonic chord that rings out \u2014 the band stops on the\n\t\t\t// \u0022one\u0022, it doesn\u0027t roll forward as if looping. The bar before it fills to lead in.\n\t\t\tif ( isEnding \u0026\u0026 bar == part.Bars - 1 )\n\t\t\t{\n\t\t\t\tRenderEnding( barStart, spe, noise );\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Every section\u0027s last bar fills; for the ending that fill moves one bar earlier so\n\t\t\t// it sets up the final hit instead of pushing past the end into nothing.\n\t\t\tbool lastBar = bar == part.Bars - 1 || (isEnding \u0026\u0026 bar == part.Bars - 2);\n\n\t\t\t// Intro build-in: rather than slamming in at full band (which reads as looping back\n\t\t\t// into the middle of the song), the voices enter a layer at a time \u2014 bass \u002B drums\n\t\t\t// lay down the groove first, then the chordal voice, then the horns/lead on top. The\n\t\t\t// thresholds are derived from the intro length (not hardcoded bar numbers) so the build\n\t\t\t// always spans the whole intro and can\u0027t silently collapse if part.Bars changes: the\n\t\t\t// chord enters a quarter of the way in, the top half-way. (4-bar intro \u21D2 bars 1 and 2.)\n\t\t\tbool playChord = !isIntro || bar \u003E= part.Bars / 4;\n\t\t\tbool playTop = !isIntro || bar \u003E= part.Bars / 2;\n\n\t\t\tRenderBassBar( barStart, spe, secPerEighth, chord, nextChord, bassRng, bassOrn, exprRng );\n\t\t\tif ( playChord )\n\t\t\t\tswitch ( _genre )\n\t\t\t\t{\n\t\t\t\t\tcase 1: // rock: keys comp \u002B power-chord guitar\n\t\t\t\t\tcase 2: // country: honky-tonk piano comp \u002B strummed twang guitar\n\t\t\t\t\t\tRenderKeysBar( barStart, spe, secPerEighth, chord, keysRng, exprRng );\n\t\t\t\t\t\tRenderRhythmGuitarBar( barStart, spe, secPerEighth, chord, rhythmRng, exprRng );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 3: // metal: palm-muted gallop riff carries the bar\n\t\t\t\t\t\tRenderMetalRiffBar( barStart, spe, secPerEighth, chord, rhythmRng, exprRng );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 4: // punk: lean \u2014 power-chord guitar carries it, no keys\n\t\t\t\t\t\tRenderRhythmGuitarBar( barStart, spe, secPerEighth, chord, rhythmRng, exprRng );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase 5: // pop: synth comp (the keys voice, run clean \u002B bright)\n\t\t\t\t\t\tRenderKeysBar( barStart, spe, secPerEighth, chord, keysRng, exprRng );\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tdefault: // ska: skank chop \u002B horn stabs\n\t\t\t\t\t\tRenderRhythmBar( barStart, spe, secPerEighth, chord, rhythmRng, exprRng );\n\t\t\t\t\t\tif ( _hasHorns \u0026\u0026 playTop )\n\t\t\t\t\t\t\tRenderHornStabs( barStart, spe, secPerEighth, chord, hornRng, exprRng );\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\tRenderDrumBar( barStart, spe, lastBar, noise, fillRng, fillNoise );\n\n\t\t\t// No lead in the ending: a lead phrase starts every two bars and runs ~two bars, so in\n\t\t\t// the short outro it spilled melody notes across the held final chord \u2014 the band has\n\t\t\t// already resolved and stopped, so the lead must too (it read as \u0022random notes after\n\t\t\t// the hold\u0022). The ending is just the fill bar \u2192 the ringing tonic.\n\t\t\tif ( playTop \u0026\u0026 bar % 2 == 0 \u0026\u0026 !isEnding )\n\t\t\t\tRenderLeadPhrase( barStart, spe, secPerEighth, chord, leadRng, exprRng );\n\t\t}\n\t}\n\n\t// The song\u0027s final downbeat. The whole band resolves home to the tonic on the \u0022one\u0022 and\n\t// lets the chord ring out into the tail RingOutTail reserved \u2014 a landing, not a turnaround.\n\t// The previous bar\u0027s fill (see RenderSection) leads in and its terminal crash lands right\n\t// here, so the ending itself only adds the kick \u002B the sustained, decaying chord.\n\t// \u2500\u2500 Swing: warp the eighth-note grid so the whole band shuffles together \u2500\u2500\n\t// On-beat (even) eighths are anchors that stay put; off-beat (odd) eighths are pushed late\n\t// by _swing of an eighth, and positions between anchors interpolate. So a sixteenth (e\u002B0.5)\n\t// or a triplet subdivision lands on the SAME warped grid as the eighths \u2014 every voice swings\n\t// in lockstep instead of the skank chop alone. \u0060eighths\u0060 is a within-bar position measured in\n\t// eighth-notes (0.5 = a sixteenth); returns the absolute sample index.\n\tint Swung( int barStart, int spe, double eighths )\n\t{\n\t\tdouble baseE = Math.Floor( eighths );\n\t\tdouble frac = eighths - baseE;\n\t\tlong slot = (long)baseE;\n\t\tdouble startShift = (slot \u0026 1) == 1 ? _swing : 0.0;          // this eighth\u0027s onset shift\n\t\tdouble endShift   = ((slot \u002B 1) \u0026 1) == 1 ? _swing : 0.0;    // the next eighth\u0027s onset shift\n\t\tdouble pos = baseE \u002B startShift \u002B frac * (1.0 \u002B endShift - startShift);\n\t\treturn barStart \u002B (int)Math.Round( pos * spe );\n\t}\n\n\tvoid RenderEnding( int barStart, int spe, Rng noise )\n\t{\n\t\tint at = Math.Max( 0, barStart );\n\t\tRenderKick( at, noise );\n\n\t\t// Resolve to the progression\u0027s tonic (slot 0) wherever the chord cycle happened to\n\t\t// land, and ring it with a natural exponential tail (Sustained = false) long enough to\n\t\t// bloom into the reserved tail room. A held triad up top \u002B the root an octave below.\n\t\tint dur = (int)(_sr * RingOutTail * 0.92f);\n\t\tint baseMidi = _rootMidi \u002B 19;\n\t\tint[] degs = { _prog[0], _prog[0] \u002B 2, _prog[0] \u002B 4, _prog[0] \u002B 7 };\n\t\tforeach ( var d in degs )\n\t\t{\n\t\t\tvar pad = new Patch\n\t\t\t{\n\t\t\t\tOsc = 1, Voices = 3, Detune = _c.Detune,\n\t\t\t\tAmp = 0.7f / degs.Length, Attack = 0.006f, Decay = 1.2,\n\t\t\t\tSustain = 0f, Sustained = false,\n\t\t\t\tCutoff = _c.SkankCutoff, CutEnv = 700f, Reso = 0.8f,\n\t\t\t\tDrive = _genre == 3 ? 3.5f : 1.3f, Pan = 0f,\n\t\t\t};\n\t\t\tRenderPatch( at, dur, Midi( ScaleMidi( baseMidi, d ) ), pad );\n\t\t}\n\t\tvar low = new Patch\n\t\t{\n\t\t\tOsc = 3, Voices = 2, Detune = _c.Detune * 0.4f,\n\t\t\tAmp = _c.BassVol * _c.BassBalance, Attack = 0.004f, Decay = 1.4,\n\t\t\tSustain = 0f, Sustained = false,\n\t\t\tCutoff = _c.BassCutoff, CutEnv = 350f, Reso = 0.9f,\n\t\t\tDrive = _c.BassDrive, Pan = 0f,\n\t\t};\n\t\tRenderPatch( at, dur, Midi( ChordRoot( 0 ) ), low, mono: true );\n\t}\n\n\t// Master: gentle soft-clip \u002B normalize. The mix peak is first normalized to 1.0\n\t// BEFORE the soft-clipper so it always has headroom \u2014 otherwise a hot sustained\n\t// bed (all voices flat at 1.0) saturated the tanh and swallowed the drum\n\t// transients (kick/snare washed out). MasterDrive now sets how hard a\n\t// peak-normalized signal hits the clipper, so the dynamics stay intact.\n\t// Call only after every RenderPitchedRange window has completed.\n\tfloat Master()\n\t{\n\t\tint total = _bufL.Length;\n\t\tfloat rawPeak = 0f;\n\t\tfor ( int i = 0; i \u003C total; i\u002B\u002B )\n\t\t\trawPeak = Math.Max( rawPeak, Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) ) );\n\t\tfloat pre = rawPeak \u003E 0.0001f ? _c.MasterDrive / rawPeak : _c.MasterDrive;\n\n\t\tfor ( int i = 0; i \u003C total; i\u002B\u002B )\n\t\t{\n\t\t\t_bufL[i] = (float)Math.Tanh( _bufL[i] * pre );\n\t\t\t_bufR[i] = (float)Math.Tanh( _bufR[i] * pre );\n\t\t}\n\n\t\t// A touch of stereo room reverb \u2014 the dry mix alone read flat/\u002216-bit\u0022.\n\t\tApplyReverb();\n\n\t\tfloat peak = 0f;\n\t\tfor ( int i = 0; i \u003C total; i\u002B\u002B )\n\t\t{\n\t\t\tfloat a = Math.Max( MathF.Abs( _bufL[i] ), MathF.Abs( _bufR[i] ) );\n\t\t\tif ( a \u003E peak ) peak = a;\n\t\t}\n\t\treturn peak \u003E 0.0001f ? _c.MasterPeak / peak : 1f;\n\t}\n\n\t// \u2500\u2500 Master reverb \u2500\u2500\n\t// A Schroeder/Freeverb-style bank: several parallel damped comb filters (the dense\n\t// tail) feeding a chain of allpasses (diffusion). The two channels use slightly\n\t// different delay lengths so the room is decorrelated \u2192 real stereo width and depth.\n\tstatic readonly int[] CombBase = { 1116, 1188, 1277, 1356, 1422, 1491 }; // samples @ 44.1k\n\tstatic readonly int[] ApBase = { 556, 441, 341 };\n\tconst int ReverbStereoSpread = 23; // R-channel delay offset for decorrelation\n\n\tvoid ApplyReverb()\n\t{\n\t\tfloat wet = Math.Clamp( _c.MasterReverb, 0f, 1f );\n\t\tif ( wet \u003C= 0.0001f ) return;\n\t\tfloat feedback = 0.70f \u002B 0.28f * Math.Clamp( _c.ReverbDecay, 0f, 1f ); // tail length\n\t\tconst float damp = 0.25f, damp1 = 1f - damp;                            // HF damping in the tail\n\t\tconst float apg = 0.5f;                                                 // allpass coefficient\n\t\tconst float inGain = 0.25f;                                             // drive into the reverb\n\t\tdouble srk = _sr / 44100.0;                                             // scale delays to the rate\n\n\t\tfor ( int ch = 0; ch \u003C 2; ch\u002B\u002B )\n\t\t{\n\t\t\tvar buf = ch == 0 ? _bufL : _bufR;\n\t\t\tint off = ch == 0 ? 0 : ReverbStereoSpread;\n\t\t\tint nc = CombBase.Length, na = ApBase.Length;\n\t\t\tvar combBuf = new float[nc][];\n\t\t\tvar combIdx = new int[nc];\n\t\t\tvar combStore = new float[nc];\n\t\t\tfor ( int j = 0; j \u003C nc; j\u002B\u002B )\n\t\t\t\tcombBuf[j] = new float[Math.Max( 1, (int)Math.Round( (CombBase[j] \u002B off) * srk ) )];\n\t\t\tvar apBuf = new float[na][];\n\t\t\tvar apIdx = new int[na];\n\t\t\tfor ( int j = 0; j \u003C na; j\u002B\u002B )\n\t\t\t\tapBuf[j] = new float[Math.Max( 1, (int)Math.Round( (ApBase[j] \u002B off) * srk ) )];\n\n\t\t\tint n = buf.Length;\n\t\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat input = buf[i] * inGain;\n\t\t\t\tfloat acc = 0f;\n\t\t\t\tfor ( int j = 0; j \u003C nc; j\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar cb = combBuf[j];\n\t\t\t\t\tint idx = combIdx[j];\n\t\t\t\t\tfloat r = cb[idx];\n\t\t\t\t\tcombStore[j] = r * damp1 \u002B combStore[j] * damp;\n\t\t\t\t\tcb[idx] = input \u002B combStore[j] * feedback;\n\t\t\t\t\tif ( \u002B\u002Bidx \u003E= cb.Length ) idx = 0;\n\t\t\t\t\tcombIdx[j] = idx;\n\t\t\t\t\tacc \u002B= r;\n\t\t\t\t}\n\t\t\t\tacc /= nc;\n\t\t\t\tfor ( int j = 0; j \u003C na; j\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar ab = apBuf[j];\n\t\t\t\t\tint idx = apIdx[j];\n\t\t\t\t\tfloat r = ab[idx];\n\t\t\t\t\tfloat o = r - acc;\n\t\t\t\t\tab[idx] = acc \u002B r * apg;\n\t\t\t\t\tif ( \u002B\u002Bidx \u003E= ab.Length ) idx = 0;\n\t\t\t\t\tapIdx[j] = idx;\n\t\t\t\t\tacc = o;\n\t\t\t\t}\n\t\t\t\tbuf[i] \u002B= wet * acc;\n\t\t\t}\n\t\t}\n\t}\n\n\tint ScaleMidi( int baseMidi, int degree )\n\t{\n\t\tint len = _scale.Length;\n\t\tint oct = (int)Math.Floor( degree / (double)len );\n\t\treturn baseMidi \u002B _scale[degree - oct * len] \u002B 12 * oct;\n\t}\n\tint ChordRoot( int c ) =\u003E ScaleMidi( _rootMidi, _prog[c] );\n\n\t// \u2500\u2500 Instrument expression \u2500\u2500\n\t// Four expressive PROPERTIES every pitched voice can lean on (drums are excluded). Each\n\t// instrument gets a genre-specific PROPENSITY for each, \u0022based on what it is\u0022 \u2014 a brass\n\t// lead sings and scoops, a bass slides, a power-chord guitar stays dead straight. The\n\t// realization is the per-note pitch shaping in RenderEvent (vibrato depth \u002B bend envelope).\n\t//   Vib    \u2014 #1 vibrato depth (a constant lean, no per-note roll)\n\t//   BendIn \u2014 #2 bend up INTO the note from a step below (per-note chance)\n\t//   Glide  \u2014 #3 portamento from the previous note\u0027s pitch (per-note chance)\n\t//   Scoop  \u2014 #4 bend up-and-back within the note (per-note chance)\n\treadonly struct Expression\n\t{\n\t\tpublic readonly float Vib, BendIn, Glide, Scoop;\n\t\tpublic Expression( float vib, float bendIn, float glide, float scoop )\n\t\t{ Vib = vib; BendIn = bendIn; Glide = glide; Scoop = scoop; }\n\t}\n\n\tconst int NoPrev = int.MinValue; // \u0022no previous note\u0022 sentinel for glide\n\n\t// The per-instrument propensity table \u2014 genre-aware. Leads route here by genre\n\t// (\u0022LEAD\u0022 = ska brass, \u0022LEAD GTR\u0022 = rock guitar). Rock lead\u0027s BENDINESS knob drives its\n\t// bend-in \u002B scoop directly (that\u0027s what \u0022bendiness\u0022 is). Tune these by ear.\n\tExpression Expr( string voice )\n\t{\n\t\tswitch ( voice )\n\t\t{\n\t\t\tcase \u0022BASS\u0022:       return _genre switch\n\t\t\t{\n\t\t\t\t1 =\u003E new Expression( 0f, 0f, 0.10f, 0f ),     // rock: locked\n\t\t\t\t2 =\u003E new Expression( 0f, 0f, 0.12f, 0.03f ),  // country: a subtle slide\n\t\t\t\t3 =\u003E default,                                 // metal: dead straight, fast\n\t\t\t\t4 =\u003E default,                                 // punk: dead straight, fast\n\t\t\t\t5 =\u003E default,                                 // pop: tight synth bass, no slide\n\t\t\t\t_ =\u003E new Expression( 0f, 0f, 0.25f, 0.05f ),  // reggae bass slides\n\t\t\t};\n\t\t\tcase \u0022SKANK\u0022:      return default;                          // staccato chops \u2014 dead straight\n\t\t\tcase \u0022ORGAN\u0022:      return new Expression( 0.15f, 0f, 0f, 0f ); // gentle bubble vibrato (only blooms on held notes)\n\t\t\tcase \u0022LEAD\u0022:       return new Expression( 0.35f, 0.15f, 0.10f, 0.25f ); // brass sings \u002B scoops\n\t\t\tcase \u0022HORNS\u0022:      return new Expression( 0.20f, 0f, 0f, 0.20f ); // section stabs fall/scoop\n\t\t\tcase \u0022KEYS\u0022:       return default;                          // organ comp \u2014 locked, no wobble\n\t\t\tcase \u0022RHYTHM GTR\u0022: return default;                          // power chords \u2014 straight\n\t\t\tcase \u0022LEAD GTR\u0022:\n\t\t\t{\n\t\t\t\t// Country leans hard into bends (the telecaster twang); rock/metal ride the knob.\n\t\t\t\tfloat bend = _genre == 2 ? MathF.Max( _c.LeadGtrBend, 0.5f ) : _c.LeadGtrBend;\n\t\t\t\treturn new Expression( 0.30f, bend, 0.10f, bend );\n\t\t\t}\n\t\t\tdefault:           return default;\n\t\t}\n\t}\n\n\t// A rolled-per-note voicing: the concrete pitch-shaping a note will get. Vibrato is a\n\t// constant depth (no draw); bend-in/glide/scoop are rolled against their propensities, so\n\t// only voices that lean on them ever pull from the expression stream.\n\tstruct Voicing { public float VibDepth, BendSemis, BendTime, ScoopSemis; }\n\n\tVoicing Roll( in Expression ex, int midi, int prevMidi, Rng rng )\n\t{\n\t\tvar v = new Voicing();\n\t\t// Vibrato depth is a SMALL pitch fraction (lean 0.5 \u2248 \u00B110 cents) and it\u0027s delayed in\n\t\t// the synth, so notes read locked-on, not seasick. BendTime is in SECONDS \u2014 a quick\n\t\t// slide that resolves and locks, never a fraction of a long held note.\n\t\tif ( ex.Vib \u003E 0f ) v.VibDepth = 0.003f \u002B 0.006f * ex.Vib;\n\t\tif ( ex.Glide \u003E 0f \u0026\u0026 prevMidi != NoPrev \u0026\u0026 rng.Chance( ex.Glide ) )\n\t\t{\n\t\t\tv.BendSemis = Math.Clamp( (prevMidi - midi) * 0.3f, -2f, 2f ); // lean toward the prev pitch, not all the way\n\t\t\tv.BendTime = 0.13f;                                      // ~130 ms portamento\n\t\t}\n\t\telse if ( ex.BendIn \u003E 0f \u0026\u0026 rng.Chance( ex.BendIn ) )\n\t\t{\n\t\t\tv.BendSemis = rng.Chance( 0.5f ) ? -0.3f : -0.55f;       // a subtle lean up into pitch\n\t\t\tv.BendTime = 0.09f;                                      // ~90 ms bend up into pitch\n\t\t}\n\t\tif ( ex.Scoop \u003E 0f \u0026\u0026 rng.Chance( ex.Scoop ) )\n\t\t\tv.ScoopSemis = rng.Chance( 0.5f ) ? 0.15f : 0.3f;        // a slight attack hump\n\t\treturn v;\n\t}\n\n\t// Bake a rolled voicing onto a patch. VibDepth is harmless unless the patch carries a\n\t// vibrato RATE (p.Vibrato) \u2014 so a voice the user muted to 0 Hz stays dry \u2014 which means a\n\t// voice that wants expression-vibrato must set its own rate in its patch literal.\n\tstatic void ApplyVoicing( ref Patch p, in Voicing v )\n\t{\n\t\tif ( v.VibDepth \u003E 0f ) p.VibDepth = v.VibDepth;\n\t\tp.BendSemis = v.BendSemis; p.BendTime = v.BendTime; p.ScoopSemis = v.ScoopSemis;\n\t}\n\n\t// \u2500\u2500 Bass \u2500\u2500\n\tvoid RenderBassBar( int barStart, int spe, double secPerEighth, int chord, int nextChord, Rng rng, Rng bassOrn, Rng exprRng )\n\t{\n\t\tint root = ChordRoot( chord );\n\t\tvar ex = Expr( \u0022BASS\u0022 );\n\t\tint prevMidi = NoPrev;\n\t\t// Bass has its own ornament knob (BASS TRIPLETS), nudged up a touch by overall\n\t\t// kit busyness so a busy vibe gets a busier bass.\n\t\tfloat ornChance = _c.BassTriplets * 0.5f \u002B _c.DrumBusy * 0.05f;\n\t\tfor ( int e = 0; e \u003C EighthsPerBar; e\u002B\u002B )\n\t\t{\n\t\t\tint off = _bassPat[e];\n\t\t\tif ( off == Rest ) continue;\n\n\t\t\tint midi;\n\t\t\tif ( off == Approach )\n\t\t\t{\n\t\t\t\tint target = ChordRoot( nextChord );\n\t\t\t\tmidi = target - (rng.Chance( 0.5f ) ? 1 : 2); // chromatic/step lead-in\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tmidi = root \u002B off;\n\t\t\t\tif ( off == 0 \u0026\u0026 e \u003E 0 \u0026\u0026 rng.Chance( _c.OctavePopChance ) ) midi \u002B= 12;\n\t\t\t}\n\n\t\t\t// note runs until the next onset (legato reggae feel)\n\t\t\tint len = 1;\n\t\t\twhile ( e \u002B len \u003C EighthsPerBar \u0026\u0026 _bassPat[e \u002B len] == Rest ) len\u002B\u002B;\n\n\t\t\t// Chop a standalone (non-sustaining) note into a 16th pair or 16th-note\n\t\t\t// triplet so the line reads \u0022long long short short\u0022 / \u0022long short long long\u0022\n\t\t\t// instead of even eighths. Driven by a dedicated stream, so the main\n\t\t\t// composition RNG order \u2014 and every existing song \u2014 is left unchanged.\n\t\t\tvar vc = Roll( ex, midi, prevMidi, exprRng );\n\t\t\tprevMidi = midi;\n\n\t\t\tif ( off != Approach \u0026\u0026 len == 1 \u0026\u0026 bassOrn.Chance( ornChance ) )\n\t\t\t{\n\t\t\t\tint n = bassOrn.Chance( 0.65f ) ? 2 : 3;        // 16th pair / 16th triplet\n\t\t\t\tint step = spe / n;\n\t\t\t\tint[] moves = { 0, 7, 12 };                     // root / fifth / octave\n\t\t\t\tfor ( int k = 0; k \u003C n; k\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tint bm = midi \u002B (k == 0 ? 0 : moves[bassOrn.Int( moves.Length )]);\n\t\t\t\t\tEmitBass( Swung( barStart, spe, e \u002B (double)k / n ), (int)(step * 0.9f), bm, secPerEighth / n * 0.8, vc );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tEmitBass( Swung( barStart, spe, e ), (int)(spe * len * 0.95f), midi, secPerEighth * len * 0.8, vc );\n\t\t}\n\t}\n\n\tvoid EmitBass( int at, int dur, int midi, double decaySec, in Voicing vc )\n\t{\n\t\t// Triangle body for a round, deep reggae/dub bass (saw alone read as too\n\t\t// buzzy) \u2014 but triangle alone was too subtle, so layer a quieter square\n\t\t// underneath for presence/definition. The square\u0027s odd harmonics give the\n\t\t// bass its bite; both share the bass low-pass so the tone stays warm.\n\t\tvar body = new Patch\n\t\t{\n\t\t\tOsc = 3, Voices = 2, Detune = _c.Detune * 0.4f,\n\t\t\tAmp = _c.BassVol * _c.BassBalance, Attack = 0.004f, Decay = decaySec,\n\t\t\tSustain = 0.55f, Sustained = true,\n\t\t\tCutoff = _c.BassCutoff, CutEnv = 350f, Reso = 0.9f,\n\t\t\tDrive = _c.BassDrive, Pan = 0f,\n\t\t};\n\t\tvar sub = new Patch\n\t\t{\n\t\t\tOsc = 2, Voices = 1, Detune = 0f,\n\t\t\tAmp = _c.BassVol * 0.4f * _c.BassBalance, Attack = 0.004f, Decay = decaySec,\n\t\t\tSustain = 0.55f, Sustained = true,\n\t\t\tCutoff = _c.BassCutoff, CutEnv = 350f, Reso = 0.9f,\n\t\t\tDrive = _c.BassDrive, Pan = 0f,\n\t\t};\n\t\tApplyVoicing( ref body, vc ); ApplyVoicing( ref sub, vc );\n\t\tRenderPatch( at, dur, Midi( midi ), body, mono: true );\n\t\tRenderPatch( at, dur, Midi( midi ), sub, mono: true );\n\t}\n\n\t// \u2500\u2500 Skank guitar (the signature) \u002B reggae organ bubble \u2014 offbeats, centered \u2500\u2500\n\tvoid RenderRhythmBar( int barStart, int spe, double secPerEighth, int chord, Rng rng, Rng exprRng )\n\t{\n\t\t// \u002B24: skank/organ sit an octave above the old register \u2014 at \u002B12 (E2..B2) the\n\t\t// chop was too low/muddy to cut through and read as missing. Organ stays a\n\t\t// further octave down via the -12 below.\n\t\tint gBase = _rootMidi \u002B 24;\n\t\tint[] degs = { _prog[chord], _prog[chord] \u002B 2, _prog[chord] \u002B 4, _prog[chord] \u002B 7 };\n\t\t// Skank is dead straight (default); the organ bubble gets a gentle vibrato depth.\n\t\tvar organVc = Roll( Expr( \u0022ORGAN\u0022 ), 0, NoPrev, exprRng );\n\n\t\tfor ( int e = 1; e \u003C EighthsPerBar; e \u002B= 2 ) // offbeats\n\t\t{\n\t\t\tint at = Swung( barStart, spe, e );   // odd e = offbeat \u2192 Swung pushes it late by _swing\n\n\t\t\t// bright, thin, short guitar chop\n\t\t\tforeach ( var d in degs )\n\t\t\t\tRenderPatch( at, (int)(spe * Math.Clamp( _c.SkankChop, 0.15f, 1f )), Midi( ScaleMidi( gBase, d ) ), new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 1, Voices = 3, Detune = _c.Detune,\n\t\t\t\t\tAmp = _c.SkankVol * _c.SkankBalance / degs.Length, Attack = 0.002f, Decay = 0.10,\n\t\t\t\t\tSustain = 0f, Sustained = false,\n\t\t\t\t\tCutoff = _c.SkankCutoff, CutEnv = 1500f, Reso = 0.8f,\n\t\t\t\t\tHighpass = _c.SkankHighpass, Drive = _c.SkankDrive, Pan = 0f,\n\t\t\t\t} );\n\n\t\t\t// reggae organ \u0022bubble\u0022: a softer, rounder offbeat under the guitar\n\t\t\tif ( _organBubble )\n\t\t\t\tforeach ( var d in degs )\n\t\t\t\t{\n\t\t\t\t\tvar organ = new Patch\n\t\t\t\t\t{\n\t\t\t\t\t\tOsc = 0, Voices = 2, Detune = _c.Detune * 0.5f,\n\t\t\t\t\t\tAmp = _c.OrganVol * _c.OrganBalance / degs.Length, Attack = 0.004f, Decay = 0.16,\n\t\t\t\t\t\tSustain = 0.3f, Sustained = false,\n\t\t\t\t\t\tCutoff = _c.OrganCutoff, CutEnv = 0f, Reso = 1.0f, Drive = 1.1f, Pan = 0f,\n\t\t\t\t\t\tVibrato = _c.OrganVibrato,\n\t\t\t\t\t};\n\t\t\t\t\tApplyVoicing( ref organ, organVc );\n\t\t\t\t\tRenderPatch( at, (int)(spe * 0.55f), Midi( ScaleMidi( gBase, d ) - 12 ), organ );\n\t\t\t\t}\n\t\t}\n\t}\n\n\t// \u2500\u2500 Lead melody (chord-tone locked \u2192 consonant) \u2500\u2500\n\tvoid RenderLeadPhrase( int barStart, int spe, double secPerEighth, int chord, Rng rng, Rng exprRng )\n\t{\n\t\tint slots = EighthsPerBar * 2;\n\t\tint melBase = _rootMidi \u002B 24;\n\t\tint[] tones = { _prog[chord], _prog[chord] \u002B 2, _prog[chord] \u002B 4, _prog[chord] \u002B 6 }; // chord tones\n\t\tint degree = tones[rng.Int( 3 )];\n\t\tbool guitarLead = _genre != 0;                    // ska is the only horn lead\n\t\tfloat amp = guitarLead ? _c.LeadGtrVol * _c.LeadGtrBalance : _c.MelodyVol * _c.MelodyBalance;\n\t\tfloat drive = guitarLead ? _c.LeadGtrDrive : _c.MelodyDrive;\n\t\t// Rock lead trades fast RUNS for BENDINESS (handled via expression), so its run rate is\n\t\t// forced to 0; metal shreds (a high floor of runs); ska/country keep the TRIPLETS knob.\n\t\tfloat tripChance = _genre switch\n\t\t{\n\t\t\t1 =\u003E 0f,\n\t\t\t3 =\u003E MathF.Max( _c.TripletChance, 0.4f ),\n\t\t\t_ =\u003E _c.TripletChance,\n\t\t};\n\t\tvar ex = guitarLead ? Expr( \u0022LEAD GTR\u0022 ) : Expr( \u0022LEAD\u0022 );\n\t\tint prevMidi = NoPrev;\n\n\t\tint e = 0;\n\t\twhile ( e \u003C slots )\n\t\t{\n\t\t\tif ( rng.Chance( _c.MelodyRestChance ) ) { e\u002B\u002B; continue; }\n\n\t\t\t// ornament: a sixteenth pair, or a triplet at one of three rates \u2014 a tight\n\t\t\t// 16th-triplet (3 in an eighth), an eighth-note triplet (3 in a beat), or a\n\t\t\t// wide quarter-note triplet (3 over two beats). Wider spans give the lazy,\n\t\t\t// over-the-barline triplet feel, not just the fast run.\n\t\t\tif ( rng.Chance( tripChance ) )\n\t\t\t{\n\t\t\t\tfloat r = rng.Next();\n\t\t\t\tint n, spanE; // n notes evenly across spanE eighths\n\t\t\t\tif ( r \u003C 0.25f ) { n = 2; spanE = 1; }       // sixteenth pair\n\t\t\t\telse if ( r \u003C 0.50f ) { n = 3; spanE = 1; }  // 16th-note triplet\n\t\t\t\telse if ( r \u003C 0.80f ) { n = 3; spanE = 2; }  // eighth-note triplet (1 beat)\n\t\t\t\telse { n = 3; spanE = 4; }                   // quarter-note triplet (2 beats)\n\t\t\t\tif ( e \u002B spanE \u003E slots ) spanE = 1;\n\n\t\t\t\tint span = spanE * spe;\n\t\t\t\tint step = span / n;\n\t\t\t\tint firstMidi = ScaleMidi( melBase, Math.Clamp( degree - n / 2, _prog[chord] - 3, _prog[chord] \u002B 10 ) );\n\t\t\t\tvar runVc = Roll( ex, firstMidi, prevMidi, exprRng );\n\t\t\t\tfor ( int k = 0; k \u003C n; k\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tint d2 = Math.Clamp( degree \u002B (k - n / 2), _prog[chord] - 3, _prog[chord] \u002B 10 );\n\t\t\t\t\tint m2 = ScaleMidi( melBase, d2 );\n\t\t\t\t\tRenderLeadNote( Swung( barStart, spe, e \u002B (double)k * spanE / n ), (int)(step * 0.9f),\n\t\t\t\t\t\tm2, amp, secPerEighth * spanE / (double)n * 0.85, drive, runVc );\n\t\t\t\t\tprevMidi = m2;\n\t\t\t\t}\n\t\t\t\te \u002B= spanE;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tint len = 1 \u002B rng.Int( 3 );\n\t\t\tif ( e \u002B len \u003E slots ) len = slots - e;\n\t\t\tbool strong = (e % 2) == 0;\n\n\t\t\tif ( strong )\n\t\t\t{\n\t\t\t\t// land on a chord tone near the current degree\n\t\t\t\tint best = tones[0], bestD = 999;\n\t\t\t\tforeach ( var t in tones )\n\t\t\t\t{\n\t\t\t\t\tfor ( int oc = -7; oc \u003C= 14; oc \u002B= 7 )\n\t\t\t\t\t{\n\t\t\t\t\t\tint cand = t \u002B (oc / 7) * 7; // keep in degree space\n\t\t\t\t\t\tint dist = Math.Abs( cand - degree );\n\t\t\t\t\t\tif ( dist \u003C bestD ) { bestD = dist; best = cand; }\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdegree = best;\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tint step = rng.Chance( _c.MelodyLeapChance ) ? (rng.Chance( 0.5f ) ? 3 : -3) : (rng.Chance( 0.5f ) ? 1 : -1);\n\t\t\t\tdegree = Math.Clamp( degree \u002B step, _prog[chord] - 3, _prog[chord] \u002B 10 );\n\t\t\t}\n\n\t\t\tint midi = ScaleMidi( melBase, degree );\n\t\t\tvar vc = Roll( ex, midi, prevMidi, exprRng );\n\t\t\tRenderLeadNote( Swung( barStart, spe, e ), (int)(spe * len * 0.9f), midi,\n\t\t\t\tamp, secPerEighth * len * 0.7f, drive, vc );\n\t\t\tprevMidi = midi;\n\t\t\te \u002B= len;\n\t\t}\n\t}\n\n\t// Dispatch a lead note to the genre\u0027s lead voice: a distorted single-note guitar for rock,\n\t// otherwise the ska horn (RenderLead \u2192 trumpet).\n\tvoid RenderLeadNote( int at, int dur, int midi, float amp, double decaySec, float drive, in Voicing vc )\n\t{\n\t\tif ( _genre != 0 )\n\t\t{\n\t\t\t// Twang = a bright cutoff-envelope snap on each pick (high CutEnv, decays fast) through\n\t\t\t// a resonant SVF, plus a BASE distortion under the slider so it reads as an electric\n\t\t\t// guitar even at the slider minimum. The base is genre-set: rock = 3 (overdriven),\n\t\t\t// metal = 4 hot (heavy), country = clean (the bite comes from the twang snap \u002B bends,\n\t\t\t// not gain). The bends (BENDINESS knob \u2192 bend-in \u002B scoop) come in via the voicing.\n\t\t\tfloat driveAmt = _genre switch\n\t\t\t{\n\t\t\t\t3 =\u003E 4f \u002B MathF.Max( 1f, _c.LeadGtrDrive ),         // metal: heavy\n\t\t\t\t2 =\u003E 0.8f \u002B 0.3f * MathF.Max( 1f, _c.LeadGtrDrive ),// country: clean twang\n\t\t\t\t4 =\u003E 2f \u002B MathF.Max( 1f, _c.LeadGtrDrive ),         // punk: bright, lightly driven\n\t\t\t\t5 =\u003E 0.6f \u002B 0.2f * MathF.Max( 1f, _c.LeadGtrDrive ),// pop: clean synth pluck/lead\n\t\t\t\t_ =\u003E 3f \u002B MathF.Max( 1f, _c.LeadGtrDrive ),         // rock\n\t\t\t};\n\t\t\t// Country/pop get a brighter cutoff snap \u2014 country for telecaster twang, pop for a\n\t\t\t// plucky synth attack.\n\t\t\tfloat cutEnv = _genre == 2 ? 3000f : _genre == 5 ? 3500f : 2200f;\n\t\t\tvar gtr = new Patch\n\t\t\t{\n\t\t\t\tOsc = 1, Voices = 1, Detune = 0f, Amp = amp,\n\t\t\t\tAttack = 0.002f, Decay = decaySec, Sustain = 0.55f, Sustained = true,\n\t\t\t\tCutoff = _c.LeadGtrCutoff, CutEnv = cutEnv, Reso = 0.65f,\n\t\t\t\tDrive = driveAmt, Pan = _leadPan, Vibrato = _c.MelodyVibrato,\n\t\t\t};\n\t\t\tApplyVoicing( ref gtr, vc );\n\t\t\t// The lead is monophonic \u2014 a single clean take at its per-song pan (_leadPan). It is NOT\n\t\t\t// double-tracked: splitting a solo line into two detuned, hard-panned, time-offset takes\n\t\t\t// beats on sustained notes and overlaps adjacent pitches on runs, reading as \u0022out of key\u0022.\n\t\t\t// Double-tracking width is for the chordal/strummed voices, not the melody.\n\t\t\tRenderPatch( at, dur, Midi( midi ), gtr, mono: true );\n\t\t\treturn;\n\t\t}\n\t\tRenderLead( at, dur, midi, amp, decaySec, drive, vc );\n\t}\n\n\t// \u2500\u2500 Rock KEYS \u2014 their OWN part, not a double of the guitar. A syncopated organ comp (the\n\t// \u00221, \u0026-of-2, 3, \u0026-of-4\u0022 Charleston push) playing diatonic TRIADS (root/3rd/5th) in a high\n\t// keyboard register. Different notes (a real triad vs the guitar\u0027s bare power chord) AND a\n\t// different rhythm (a 4-hit syncopation vs the guitar\u0027s every-eighth chug), so the two\n\t// interlock instead of playing in lockstep. KeysChug rings the chords (0) or tightens them\n\t// toward short stabs (1).\n\tstatic readonly int[] KeysOnsets = { 0, 3, 4, 7 };    // eighth positions of the comp\u0027s hits\n\tvoid RenderKeysBar( int barStart, int spe, double secPerEighth, int chord, Rng rng, Rng exprRng )\n\t{\n\t\tint kBase = _rootMidi \u002B 24;                        // keyboard register, an octave over the rhythm guitar\n\t\tint[] degs = { _prog[chord], _prog[chord] \u002B 2, _prog[chord] \u002B 4 };  // diatonic triad\n\t\tfloat chug = Math.Clamp( _c.KeysChug, 0f, 1f );\n\t\t// Country reads this comp as a honky-tonk piano and pop as a clean bright synth: keep both\n\t\t// clean (rock drives it dirty).\n\t\tfloat keysDrive = _genre == 2 || _genre == 5 ? 1f \u002B 0.2f * MathF.Max( 1f, _c.KeysDrive )\n\t\t                              : MathF.Max( 1f, _c.KeysDrive );\n\t\tvar keysVc = Roll( Expr( \u0022KEYS\u0022 ), 0, NoPrev, exprRng ); // gentle vibrato only\n\t\tfor ( int oi = 0; oi \u003C KeysOnsets.Length; oi\u002B\u002B )\n\t\t{\n\t\t\tint e = KeysOnsets[oi];\n\t\t\tint nextE = oi \u002B 1 \u003C KeysOnsets.Length ? KeysOnsets[oi \u002B 1] : EighthsPerBar; // ring up to the next hit\n\t\t\tint gap = nextE - e;\n\t\t\tbool ring = chug \u003C 0.5f;\n\t\t\tint dur = (int)(gap * spe * Math.Max( 0.25f, 1f - 0.7f * chug ));\n\t\t\tdouble dec = secPerEighth * gap * (ring ? 0.9 : 0.4);\n\t\t\tforeach ( var d in degs )\n\t\t\t{\n\t\t\t\tvar keys = new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 1, Voices = 2, Detune = _c.Detune * 0.5f,\n\t\t\t\t\tAmp = _c.KeysVol * _c.KeysBalance / degs.Length,\n\t\t\t\t\tAttack = 0.004f, Decay = dec, Sustain = ring ? 0.6f : 0.2f, Sustained = ring,\n\t\t\t\t\tCutoff = _c.KeysCutoff, CutEnv = 250f, Reso = 1.0f,\n\t\t\t\t\tDrive = keysDrive, Pan = 0f,\n\t\t\t\t};\n\t\t\t\tApplyVoicing( ref keys, keysVc );\n\t\t\t\tRenderPatch( Swung( barStart, spe, e ), dur, Midi( ScaleMidi( kBase, d ) ), keys );\n\t\t\t}\n\t\t}\n\t}\n\n\t// \u2500\u2500 Rock rhythm guitar \u2014 twangy distorted power chords. Shares the lead guitar\u0027s voice (the\n\t// bright cutoff-envelope \u0022twang\u0022 through a resonant SVF) but strums root\u002Bfifth\u002Boctave and\n\t// runs a LOWER base distortion than the lead so the two layer instead of mush. Downbeats\n\t// ring; offbeats tighten toward a palm-muted chug as RhythmGtrChug rises.\n\t// exprRng is unused: power chords stay dead straight (Expr(\u0022RHYTHM GTR\u0022) is default), but the\n\t// param keeps every instrument\u0027s call site uniform.\n\tvoid RenderRhythmGuitarBar( int barStart, int spe, double secPerEighth, int chord, Rng rng, Rng exprRng )\n\t{\n\t\tbool country = _genre == 2;\n\t\tint root = ChordRoot( chord ) \u002B 12;               // chunky register, an octave up\n\t\t// Country strums a full DIATONIC triad (root/3rd/5th \u002B octave) clean and bright \u2014 built in\n\t\t// degree space (ScaleMidi) so the 3rd follows the mode, matching the keys/lead. A hardcoded\n\t\t// major 3rd clashed on the minor chords of a progression (e.g. the vi in {0,4,5,3}). Rock\n\t\t// chunks a bare, mode-neutral power chord (root/5th/octave) with more base distortion.\n\t\tint triBase = _rootMidi \u002B 12;                     // same register as \u0060root\u0060, in degree space\n\t\tint[] notes = country\n\t\t\t? new[] { ScaleMidi( triBase, _prog[chord] ),     ScaleMidi( triBase, _prog[chord] \u002B 2 ),\n\t\t\t          ScaleMidi( triBase, _prog[chord] \u002B 4 ), ScaleMidi( triBase, _prog[chord] \u002B 7 ) }\n\t\t\t: new[] { root, root \u002B 7, root \u002B 12 };\n\t\tfloat chug = Math.Clamp( _c.RhythmGtrChug, 0f, 1f );\n\t\tfloat cutEnv = country ? 2600f : 1400f;            // brighter twang for the clean strum\n\t\tfloat driveAmt = country ? 0.8f \u002B 0.3f * MathF.Max( 1f, _c.RhythmGtrDrive )\n\t\t                         : 1.5f \u002B MathF.Max( 1f, _c.RhythmGtrDrive ); // less base than lead\n\t\tfor ( int e = 0; e \u003C EighthsPerBar; e\u002B\u002B )\n\t\t{\n\t\t\tbool accent = (e % 2) == 0;                    // downbeats ring, offbeats chug\n\t\t\tfloat lenFrac = accent ? (1f - 0.5f * chug) : (0.35f - 0.2f * chug);\n\t\t\tint dur = (int)(spe * Math.Max( 0.12f, lenFrac ));\n\t\t\tdouble dec = secPerEighth * (accent ? 0.8 : 0.3);\n\t\t\tforeach ( var m in notes )\n\t\t\t\tRenderPatch( Swung( barStart, spe, e ), dur, Midi( m ), new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 1, Voices = 2, Detune = _c.Detune * 0.5f,\n\t\t\t\t\tAmp = _c.RhythmGtrVol * _c.RhythmGtrBalance / notes.Length * (accent ? 1f : 0.7f),\n\t\t\t\t\tAttack = 0.002f, Decay = dec, Sustain = accent ? 0.45f : 0f, Sustained = accent,\n\t\t\t\t\tCutoff = _c.RhythmGtrCutoff, CutEnv = cutEnv, Reso = 0.8f,   // twang\n\t\t\t\t\tDrive = driveAmt, Pan = 0f,\n\t\t\t\t} );\n\t\t}\n\t}\n\n\t// \u2500\u2500 Metal rhythm guitar \u2014 palm-muted 16th-note gallop on the low root with power-chord\n\t// accents. The relentless 16th chug (under the double-kick) is the \u0022fast riff\u0022 engine; the\n\t// downbeats and a few syncopated stabs ring a full power chord. Heavy base distortion, dark\n\t// and tight. rng (the rhythm stream) breaks up the accent placement so riffs vary by section.\n\tvoid RenderMetalRiffBar( int barStart, int spe, double secPerEighth, int chord, Rng rng, Rng exprRng )\n\t{\n\t\tint root = ChordRoot( chord );                    // low, chunky \u2014 no octave bump\n\t\tint[] power = { 0, 7, 12 };\n\t\tint six = spe / 2;\n\t\tif ( six \u003C= 0 ) return;\n\t\tfloat chug = Math.Clamp( _c.RhythmGtrChug, 0f, 1f );\n\t\tfloat driveAmt = 4f \u002B MathF.Max( 1f, _c.RhythmGtrDrive ); // heavy\n\t\tfor ( int s = 0; s \u003C EighthsPerBar * 2; s\u002B\u002B )     // 16 sixteenths\n\t\t{\n\t\t\tint at = Swung( barStart, spe, s * 0.5 );   // 16 sixteenths on the swung grid\n\t\t\tbool beat = s % 4 == 0;                        // quarter-note downbeats \u2192 ring a chord\n\t\t\tbool ring = beat || (s % 2 == 0 \u0026\u0026 rng.Chance( 0.3f )); // some offbeat eighths ring too\n\t\t\tint[] offs = ring ? power : new[] { 0 };       // accents = power chord, chugs = root only\n\t\t\tfloat gain = ring ? 1f : 0.6f;\n\t\t\t// Palm mute = short, tight; accents ring longer. Chug tightens the muted notes further.\n\t\t\tint dur = (int)(six * (ring ? 0.9f : Math.Max( 0.25f, 0.55f - 0.3f * chug )));\n\t\t\tdouble dec = secPerEighth * (ring ? 0.4 : 0.12);\n\t\t\tforeach ( var o in offs )\n\t\t\t\tRenderPatch( at, dur, Midi( root \u002B o ), new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 1, Voices = 2, Detune = _c.Detune * 0.5f,\n\t\t\t\t\tAmp = _c.RhythmGtrVol * _c.RhythmGtrBalance / offs.Length * gain,\n\t\t\t\t\tAttack = 0.002f, Decay = dec, Sustain = ring ? 0.35f : 0f, Sustained = ring,\n\t\t\t\t\tCutoff = _c.RhythmGtrCutoff, CutEnv = 1100f, Reso = 0.7f,\n\t\t\t\t\tDrive = driveAmt, Pan = 0f,\n\t\t\t\t} );\n\t\t}\n\t}\n\n\t// \u2500\u2500 Backing horns (panned spread) \u2500\u2500\n\t// Block stabs on the mask read \u0022samey\u0022 (only eighth-note chords). A dedicated\n\t// stream (horn:tag, so the main composition order is unchanged) breaks them up\n\t// with rolling arpeggios, 16th pairs, grace pickups and varied length. Kept\n\t// modest \u2014 the bass got over-busy when its ornament rate ran high.\n\tvoid RenderHornStabs( int barStart, int spe, double secPerEighth, int chord, Rng orn, Rng exprRng )\n\t{\n\t\tint baseMidi = _rootMidi \u002B 19;\n\t\tint[] degs = { _prog[chord], _prog[chord] \u002B 2, _prog[chord] \u002B 4 };\n\t\tfloat spread = _c.PanAmount * 0.7f;\n\t\tint six = spe / 2;\n\t\tfloat ornChance = 0.18f \u002B _c.TripletChance; // ~0.24 default; rides the same knob\n\n\t\t// Section expression \u2014 vibrato \u002B a chance of a scoop/fall, rolled once per onset (below)\n\t\t// and applied to every tone of that stab so the whole section bends together.\n\t\tvar ex = Expr( \u0022HORNS\u0022 );\n\t\tVoicing hornVc = default;\n\n\t\t// one chord-tone voice\n\t\tvoid Note( int at, int dur, int k, double dec, float gain )\n\t\t{\n\t\t\tvar horn = new Patch\n\t\t\t{\n\t\t\t\tOsc = 1, Voices = 3, Detune = _c.Detune,\n\t\t\t\tAmp = _c.HornVol * _c.HornBalance / degs.Length * gain, Attack = 0.008f, Decay = dec,\n\t\t\t\tSustain = 0.2f, Sustained = false,\n\t\t\t\tCutoff = _c.HornCutoff, CutEnv = 1200f, Reso = 1.0f,\n\t\t\t\tDrive = _c.HornDrive, Pan = spread * (k / (float)(degs.Length - 1) * 2f - 1f),\n\t\t\t\tVibrato = _c.MelodyVibrato,\n\t\t\t};\n\t\t\tApplyVoicing( ref horn, hornVc );\n\t\t\tRenderPatch( at, dur, Midi( ScaleMidi( baseMidi, degs[k] ) ), horn );\n\t\t}\n\n\t\t// full block chord stab\n\t\tvoid Stab( int at, int dur, double dec, float gain )\n\t\t{\n\t\t\tfor ( int k = 0; k \u003C degs.Length; k\u002B\u002B ) Note( at, dur, k, dec, gain );\n\t\t}\n\n\t\tfor ( int e = 0; e \u003C EighthsPerBar; e\u002B\u002B )\n\t\t{\n\t\t\tif ( !_hornMask[e] ) continue;\n\t\t\tint at = Swung( barStart, spe, e );\n\t\t\thornVc = Roll( ex, baseMidi, NoPrev, exprRng );\n\n\t\t\tif ( six \u003E 0 \u0026\u0026 orn.Chance( ornChance ) )\n\t\t\t{\n\t\t\t\tfloat r = orn.Next();\n\t\t\t\tif ( r \u003C 0.4f )\n\t\t\t\t{\n\t\t\t\t\t// rolling arpeggio: chord tones climb across a 16th-triplet\n\t\t\t\t\tint step = spe / 3;\n\t\t\t\t\tfor ( int k = 0; k \u003C degs.Length; k\u002B\u002B )\n\t\t\t\t\t\tNote( Swung( barStart, spe, e \u002B (double)k / 3 ), (int)(step * 0.9f), k, secPerEighth / 3 * 0.8, 1f );\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif ( r \u003C 0.75f )\n\t\t\t\t{\n\t\t\t\t\t// 16th pair: stab on the beat, softer echo on the \u0022e\u0022\n\t\t\t\t\tStab( at, (int)(six * 0.85f), secPerEighth * 0.5 * 0.8, 1f );\n\t\t\t\t\tStab( Swung( barStart, spe, e \u002B 0.5 ), (int)(six * 0.85f), secPerEighth * 0.5 * 0.7, 0.6f );\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// grace pickup: a soft single tone just before the block stab\n\t\t\t\tNote( Swung( barStart, spe, e - 0.5 ), (int)(six * 0.8f), 0, secPerEighth * 0.5 * 0.6, 0.5f );\n\t\t\t\tStab( at, (int)(spe * 0.6f), 0.22, 1f );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// plain stab \u2014 length varies a touch so even straight bars aren\u0027t identical\n\t\t\tfloat lenMul = 0.45f \u002B orn.Next() * 0.35f;\n\t\t\tStab( at, (int)(spe * lenMul), 0.22, 1f );\n\t\t}\n\t}\n\n\t// \u2500\u2500 Lead instrument voices \u2500\u2500\n\tenum Instrument { Trumpet, Sax, Organ, Trombone }\n\n\tInstrument PickInstrument( Rng rng )\n\t{\n\t\tif ( _c.ForceInstrument \u003E= 0 \u0026\u0026 _c.ForceInstrument \u003C= 3 ) return (Instrument)_c.ForceInstrument;\n\t\tfloat tw = MathF.Max( 0f, _c.TrumpetWeight ), sw = MathF.Max( 0f, _c.SaxWeight );\n\t\tfloat ow = MathF.Max( 0f, _c.OrganWeight ), bw = MathF.Max( 0f, _c.TromboneWeight );\n\t\tfloat sum = tw \u002B sw \u002B ow \u002B bw;\n\t\tif ( sum \u003C= 0f ) return Instrument.Trumpet;\n\t\tfloat r = rng.Next() * sum;\n\t\tif ( (r -= tw) \u003C 0f ) return Instrument.Trumpet;\n\t\tif ( (r -= sw) \u003C 0f ) return Instrument.Sax;\n\t\tif ( (r -= ow) \u003C 0f ) return Instrument.Organ;\n\t\treturn Instrument.Trombone;\n\t}\n\n\tvoid RenderLead( int at, int dur, int midi, float amp, double decaySec, float drive, in Voicing vc )\n\t{\n\t\tPatch p; int m = midi;\n\t\tswitch ( _lead )\n\t\t{\n\t\t\tcase Instrument.Trumpet:\n\t\t\t\tp = new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 1, Voices = 3, Detune = _c.Detune * 0.7f, Amp = amp,\n\t\t\t\t\tAttack = 0.01f, Decay = decaySec, Sustain = 0.7f, Sustained = true,\n\t\t\t\t\tCutoff = _c.LeadCutoff, CutEnv = 1800f, Reso = 1.0f, Drive = drive,\n\t\t\t\t\tPan = _leadPan, Vibrato = _c.MelodyVibrato,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase Instrument.Trombone:\n\t\t\t\tm = midi - 12;\n\t\t\t\tp = new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 1, Voices = 3, Detune = _c.Detune * 0.7f, Amp = amp * 1.1f,\n\t\t\t\t\tAttack = 0.02f, Decay = decaySec, Sustain = 0.7f, Sustained = true,\n\t\t\t\t\tCutoff = _c.LeadCutoff * 0.7f, CutEnv = 900f, Reso = 1.0f, Drive = MathF.Max( 1f, drive * 0.8f ),\n\t\t\t\t\tPan = _leadPan, Vibrato = _c.MelodyVibrato * 0.7f,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tcase Instrument.Sax:\n\t\t\t\tp = new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 3, Voices = 2, Detune = _c.Detune * 0.5f, Amp = amp * 1.15f,\n\t\t\t\t\tAttack = 0.014f, Decay = decaySec, Sustain = 0.75f, Sustained = true,\n\t\t\t\t\tCutoff = _c.LeadCutoff, CutEnv = 1400f, Reso = 0.7f, Drive = MathF.Max( 1.2f, drive ),\n\t\t\t\t\tPan = _leadPan, Vibrato = _c.MelodyVibrato, Breath = 0.03f,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\tdefault: // Organ\n\t\t\t\tp = new Patch\n\t\t\t\t{\n\t\t\t\t\tOsc = 0, Voices = 3, Detune = _c.Detune * 0.6f, Amp = amp,\n\t\t\t\t\tAttack = 0.006f, Decay = decaySec * 1.5, Sustain = 0.9f, Sustained = true,\n\t\t\t\t\tCutoff = 2600f, CutEnv = 0f, Reso = 1.0f, Drive = 1.15f,\n\t\t\t\t\tPan = _leadPan, Vibrato = _c.MelodyVibrato * 0.9f,\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t}\n\t\tApplyVoicing( ref p, vc );\n\t\t// Single clean take at _leadPan \u2014 the ska horn/organ lead is monophonic, so it is not\n\t\t// double-tracked (see the guitar-lead note in RenderLeadNote: doubling a solo line smears\n\t\t// pitch). Only chordal/strummed voices get the width.\n\t\tRenderPatch( at, dur, Midi( m ), p, mono: true );\n\t}\n\n\t// \u2500\u2500 Synth core: unison osc \u2192 optional high-pass \u2192 resonant low-pass (cutoff\n\t//    envelope) \u2192 soft drive \u2192 AD/sustain amp env. \u2500\u2500\n\tstruct Patch\n\t{\n\t\tpublic int Osc;        // 0 sine 1 saw 2 square 3 triangle\n\t\tpublic int Voices;\n\t\tpublic float Detune;   // cents\n\t\tpublic float Amp;\n\t\tpublic float Attack;   // sec\n\t\tpublic double Decay;   // sec (exp time constant)\n\t\tpublic float Sustain;  // 0..1 (only if Sustained)\n\t\tpublic bool Sustained;\n\t\tpublic float Cutoff;   // Hz low-pass\n\t\tpublic float CutEnv;   // Hz added at attack, decays with Decay\n\t\tpublic float Reso;     // SVF damping (lower = more resonance)\n\t\tpublic float Highpass; // Hz one-pole high-pass (0 = off)\n\t\tpublic float Drive;    // tanh\n\t\tpublic float Pan;      // -1..1\n\t\tpublic float Vibrato;  // Hz (rate of the pitch wobble)\n\t\tpublic float Breath;   // 0..1 noise mix (reeds)\n\t\t// \u2500\u2500 Expression (per-note pitch shaping; see Expression/Voicing) \u2500\u2500\n\t\tpublic float VibDepth;   // vibrato depth as a pitch fraction (0 \u2192 legacy 0.005 when Vibrato\u003E0)\n\t\tpublic float BendSemis;  // pitch offset in semitones at note START, glides to 0 (bend-in / glide); \u2212ve starts below\n\t\tpublic float BendTime;   // 0..1 fraction of the note over which BendSemis glides to 0\n\t\tpublic float ScoopSemis; // height (semitones) of a mid-note bend-up-and-back hump (0 = none)\n\t\tpublic float PhaseSeed;  // oscillator start phase (0..1); 0 = legacy in-phase start. Used to\n\t\t                         // decorrelate the two double-tracking takes (see RenderPatch).\n\t}\n\n\t// Pitched note events collected during ComposePlan, then synthesized by\n\t// RenderPitchedRange. Synthesis pulls no RNG, so windows parallelize across threads.\n\tstruct NoteEvent { public int Start, Dur; public float Freq; public Patch P; }\n\treadonly List\u003CNoteEvent\u003E _events = new();\n\n\t// During ComposePlan this only enqueues; the synthesis happens in RenderPitchedRange.\n\t// When DoubleTrack is on, every (non-drum) note is widened into two decorrelated takes\n\t// panned apart \u2014 see the Config \u0022width\u0022 block. \u0060lead\u0060 selects the wider lead spread.\n\tvoid RenderPatch( int start, int dur, float freq, Patch p, bool lead = false, bool mono = false )\n\t{\n\t\tif ( start \u003C 0 || dur \u003C= 0 || p.Voices \u003C 1 ) return;\n\t\t// Bass stays centred (mono): doubling/detuning low frequencies smears the low end and\n\t\t// cancels in mono, so the bass is the one voice kept dead-centre regardless of width.\n\t\tif ( mono || _c.DoubleTrack \u003C 0.5f )\n\t\t{\n\t\t\t_events.Add( new NoteEvent { Start = start, Dur = dur, Freq = freq, P = p } );\n\t\t\treturn;\n\t\t}\n\t\t// The STEREO WIDTH slider (_widthScale) scales the whole effect: pan AND the\n\t\t// decorrelation (detune / delay / jitter / phase / per-note variation, all scaled inside\n\t\t// AddTake). At 100% it\u0027s the full design width; at 0% both takes collapse onto each other\n\t\t// at centre \u2192 effectively mono.\n\t\tfloat width = Math.Clamp( lead ? _c.WidthLead : _c.WidthBacking, 0f, 1f ) * _widthScale;\n\t\tfloat halfCents = _c.WidthDetune * 0.5f * _widthScale;\n\t\tint delay = (int)(_c.WidthDelayMs * 0.001f * _sr * _widthScale);\n\t\tint jit = (int)(_c.WidthJitterMs * 0.001f * _sr * _widthScale);\n\t\t// Take A sits left and slightly flat; take B sits right, slightly sharp, and lags by the\n\t\t// constant offset. Both get independent phase \u002B per-note amp/cutoff/timing variation, so\n\t\t// the two channels are genuinely different performances (the source of the width) rather\n\t\t// than a phantom-centre mono copy.\n\t\tAddTake( start, dur, freq, p, 0, -width, -halfCents, 0, jit );\n\t\tAddTake( start, dur, freq, p, 1, \u002Bwidth, \u002BhalfCents, delay, jit );\n\t}\n\n\t// Enqueue one double-tracking take. All per-note variation is derived from a local hash of\n\t// (start, freq, take) so it stays deterministic and pulls no RNG (synthesis must remain a\n\t// pure function for the parallel RenderPitchedRange windows).\n\tvoid AddTake( int start, int dur, float freq, Patch p, int take, float pan, float cents, int delayBase, int jit )\n\t{\n\t\tuint s = unchecked( (uint)start * 2654435761u \u002B (uint)(int)freq * 40503u )\n\t\t\t^ (take == 0 ? 0x85EBCA6Bu : 0xC2B2AE35u);\n\t\tfloat r1 = Hash01( ref s );   // start phase\n\t\tfloat r2 = Hash01( ref s );   // amplitude\n\t\tfloat r3 = Hash01( ref s );   // cutoff\n\t\tfloat r4 = Hash01( ref s );   // timing jitter\n\t\tint st = start \u002B delayBase \u002B (int)((r4 * 2f - 1f) * jit);\n\t\tif ( st \u003C 0 ) st = 0;\n\t\tvar q = p;\n\t\tq.Pan = pan;\n\t\t// Phase \u002B per-note variation also scale with the width slider, so at 0% the two takes\n\t\t// become identical and centred (clean mono) instead of a flangy centred double.\n\t\tq.PhaseSeed = r1 * _widthScale;\n\t\tq.Amp = p.Amp * (1f \u002B (r2 * 2f - 1f) * _c.WidthAmpVar * _widthScale);\n\t\tq.Cutoff = p.Cutoff * (1f \u002B (r3 * 2f - 1f) * _c.WidthCutoffVar * _widthScale);\n\t\tfloat f = freq * (float)Math.Pow( 2.0, cents / 1200.0 );\n\t\t_events.Add( new NoteEvent { Start = st, Dur = dur, Freq = f, P = q } );\n\t}\n\n\t// Fast deterministic [0,1) hash step (xorshift32).\n\tstatic float Hash01( ref uint s )\n\t{\n\t\ts ^= s \u003C\u003C 13; s ^= s \u003E\u003E 17; s ^= s \u003C\u003C 5;\n\t\treturn (s \u0026 0xFFFFFFu) / 16777216f;\n\t}\n\n\t/// \u003Csummary\u003ESynthesize every pitched event whose span overlaps \u003Cc\u003E[from, to)\u003C/c\u003E,\n\t/// writing ONLY samples inside that window. Safe to call concurrently for disjoint\n\t/// windows: each output index is owned by exactly one window, a boundary-spanning\n\t/// note is re-rendered from its own start by each window (the SVF / high-pass state\n\t/// can\u0027t be resumed mid-stream), and each window walks \u003Cc\u003E_events\u003C/c\u003E in order, so\n\t/// writes never collide and the per-index sum order is deterministic.\u003C/summary\u003E\n\tpublic void RenderPitchedRange( int from, int to )\n\t{\n\t\tfrom = Math.Max( 0, from );\n\t\tto = Math.Min( _bufL.Length, to );\n\t\tif ( to \u003C= from ) return;\n\t\tvar ph = new double[8];\n\t\tvar inc = new double[8];\n\t\tvar events = _events;\n\t\tfor ( int k = 0; k \u003C events.Count; k\u002B\u002B )\n\t\t{\n\t\t\tvar ev = events[k];\n\t\t\tint end = Math.Min( _bufL.Length, ev.Start \u002B ev.Dur );\n\t\t\tif ( end \u003C= from || ev.Start \u003E= to ) continue; // no overlap with this window\n\t\t\tRenderEvent( ev, from, to, ph, inc );\n\t\t}\n\t}\n\n\t// One pitched note. Computes from the note\u0027s own start (the running filter / breath\n\t// state can\u0027t be resumed mid-note) but writes only within [clipFrom, clipTo), and\n\t// stops once past clipTo since later windows own those samples. ph/inc are caller-\n\t// owned scratch (per-thread \u2192 no shared state).\n\tvoid RenderEvent( in NoteEvent ev, int clipFrom, int clipTo, double[] ph, double[] inc )\n\t{\n\t\tint start = ev.Start, dur = ev.Dur;\n\t\tfloat freq = ev.Freq;\n\t\tvar p = ev.P;\n\t\tStereoGains( p.Pan, out float gL, out float gR );\n\t\tint atk = Math.Max( 1, (int)(p.Attack * _sr) );\n\t\tdouble decSamp = Math.Max( 1.0, p.Decay * _sr );\n\t\tint rel = Math.Max( 1, (int)(0.006f * _sr) );\n\t\tint voices = Math.Min( 8, p.Voices );\n\n\t\tfor ( int v = 0; v \u003C voices; v\u002B\u002B )\n\t\t{\n\t\t\tph[v] = p.PhaseSeed;   // 0 for un-doubled notes \u2192 identical to the old in-phase start\n\t\t\tfloat cents = voices == 1 ? 0f : (v - (voices - 1) * 0.5f) * p.Detune;\n\t\t\tinc[v] = freq * Math.Pow( 2, cents / 1200.0 ) / _sr;\n\t\t}\n\n\t\tfloat low = 0, band = 0;\n\t\tfloat reso = Math.Clamp( p.Reso, 0.2f, 2f );\n\t\tfloat dnorm = p.Drive \u003E 1f ? 1f / (float)Math.Tanh( p.Drive ) : 1f;\n\t\tfloat hpA = p.Highpass \u003E 0f ? (float)(1.0 / (1.0 \u002B 2 * Math.PI * p.Highpass / _sr)) : 0f;\n\t\tfloat hpInPrev = 0f, hpOutPrev = 0f;\n\t\tuint bn = 0x9E3779B9u;\n\n\t\tint end = Math.Min( Math.Min( _bufL.Length, start \u002B dur ), clipTo );\n\t\tint relStart = dur - rel;\n\t\t// Expression windows (samples): vibrato holds off then ramps in; the scoop is a quick\n\t\t// attack gesture. Kept fixed/absolute so a long held note locks on pitch after them.\n\t\tint vibDelay = (int)(0.18f * _sr);\n\t\tint vibRamp = Math.Max( 1, (int)(0.16f * _sr) );\n\t\tint scoopWin = Math.Max( 1, (int)(0.16f * _sr) );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat env;\n\t\t\tif ( i \u003C atk ) env = (float)i / atk;\n\t\t\telse if ( p.Sustained )\n\t\t\t{\n\t\t\t\tfloat d = (float)Math.Exp( -(i - atk) / decSamp );\n\t\t\t\tenv = p.Sustain \u002B (1f - p.Sustain) * d;\n\t\t\t}\n\t\t\telse env = (float)Math.Exp( -(i - atk) / decSamp );\n\t\t\tif ( i \u003E= relStart ) env *= Math.Max( 0f, (float)(dur - i) / rel );\n\t\t\tif ( env \u003C 0.0006f \u0026\u0026 i \u003E atk \u0026\u0026 !p.Sustained ) break;\n\n\t\t\tfloat s = 0f;\n\t\t\t// Vibrato: subtle, and DELAYED so the note locks on pitch first and only blooms a\n\t\t\t// wobble if it\u0027s held \u2014 short notes stay dead-on. Depth is a small pitch fraction.\n\t\t\tfloat vib = 1f;\n\t\t\tif ( p.Vibrato \u003E 0f \u0026\u0026 p.VibDepth \u003E 0f )\n\t\t\t{\n\t\t\t\tfloat ramp = MathF.Max( 0f, (i - vibDelay) / (float)vibRamp );\n\t\t\t\tif ( ramp \u003E 1f ) ramp = 1f;\n\t\t\t\tif ( ramp \u003E 0f )\n\t\t\t\t\tvib = (float)(1.0 \u002B p.VibDepth * ramp * Math.Sin( i / (double)_sr * p.Vibrato * 2 * Math.PI ));\n\t\t\t}\n\t\t\t// Pitch-bend envelope (semitones) on top of vibrato. Both are QUICK gestures over a\n\t\t\t// short fixed window so the note then sits locked on its target pitch (bendMul == 1):\n\t\t\t// BendSemis snaps to 0 over BendTime seconds (bend-in / glide); ScoopSemis is a fast\n\t\t\t// up-and-back hump confined to the attack (bend-and-release).\n\t\t\tfloat bendSemis = 0f;\n\t\t\tif ( p.BendSemis != 0f \u0026\u0026 p.BendTime \u003E 0f )\n\t\t\t{\n\t\t\t\tint bt = Math.Min( dur, Math.Max( 1, (int)(p.BendTime * _sr) ) );\n\t\t\t\tif ( i \u003C bt ) { float u = i / (float)bt; bendSemis \u002B= p.BendSemis * (1f - u * u * (3f - 2f * u)); }\n\t\t\t}\n\t\t\tif ( p.ScoopSemis != 0f \u0026\u0026 i \u003C scoopWin )\n\t\t\t\tbendSemis \u002B= p.ScoopSemis * MathF.Sin( (float)(i / (float)Math.Min( dur, scoopWin ) * Math.PI) );\n\t\t\tfloat bendMul = bendSemis != 0f ? (float)Math.Pow( 2.0, bendSemis / 12.0 ) : 1f;\n\t\t\tfor ( int v = 0; v \u003C voices; v\u002B\u002B )\n\t\t\t{\n\t\t\t\t\tdouble dt = inc[v] * vib * bendMul;\n\t\t\t\t\ts \u002B= BlepOsc( p.Osc, ph[v] - Math.Floor( ph[v] ), dt );\n\t\t\t\t\tph[v] \u002B= dt;\n\t\t\t}\n\t\t\ts /= voices;\n\t\t\tif ( p.Breath \u003E 0f )\n\t\t\t{\n\t\t\t\tbn = unchecked( bn * 1664525u \u002B 1013904223u );\n\t\t\t\ts \u002B= (bn / 4294967296f * 2f - 1f) * p.Breath;\n\t\t\t}\n\t\t\tif ( hpA \u003E 0f )\n\t\t\t{\n\t\t\t\tfloat hp = hpA * (hpOutPrev \u002B s - hpInPrev);\n\t\t\t\thpInPrev = s; hpOutPrev = hp; s = hp;\n\t\t\t}\n\n\t\t\t// resonant low-pass (Chamberlin SVF) with cutoff envelope.\n\t\t\t// Clamp to ~sr/6 to keep the SVF stable.\n\t\t\tfloat cut = p.Cutoff \u002B (p.CutEnv \u003E 0f ? p.CutEnv * (float)Math.Exp( -i / decSamp ) : 0f);\n\t\t\tfloat f = (float)(2 * Math.Sin( Math.PI * Math.Min( cut, _sr * 0.16f ) / _sr ));\n\t\t\tfloat high = s - low - reso * band;\n\t\t\tband \u002B= f * high;\n\t\t\tlow \u002B= f * band;\n\t\t\tfloat outp = low;\n\n\t\t\tif ( p.Drive \u003E 1f ) outp = (float)Math.Tanh( outp * p.Drive ) * dnorm;\n\t\t\tfloat val = outp * env * p.Amp;\n\t\t\tint idx = start \u002B i;\n\t\t\tif ( idx \u003E= clipFrom )\n\t\t\t{\n\t\t\t\t_bufL[idx] \u002B= val * gL;\n\t\t\t\t_bufR[idx] \u002B= val * gR;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Band-limited oscillator. Naive saw/square step instantaneously at the phase wrap,\n\t// and those discontinuities alias into harsh inharmonic tones \u2014 the core of the\n\t// \u00228-/16-bit\u0022 buzz. PolyBLEP rounds each discontinuity over one sample so the harmonics\n\t// fold back cleanly, for a warm analog edge instead. Sine is already band-limited;\n\t// triangle\u0027s corners roll off as 1/n\u00B2 so its aliasing is inaudible.\n\t// p = phase in [0,1), dt = phase increment per sample (cycles/sample).\n\tstatic float BlepOsc( int t, double p, double dt )\n\t{\n\t\tswitch ( t )\n\t\t{\n\t\t\tcase 0:\n\t\t\t\treturn MathF.Sin( (float)(p * 2 * Math.PI) );\n\t\t\tcase 1: // saw\n\t\t\t\treturn (float)(2 * p - 1) - PolyBlep( p, dt );\n\t\t\tcase 2: // square (50% duty = two opposed discontinuities)\n\t\t\t{\n\t\t\t\tfloat v = p \u003C 0.5 ? 1f : -1f;\n\t\t\t\tv \u002B= PolyBlep( p, dt );\n\t\t\t\tdouble p2 = p \u002B 0.5; if ( p2 \u003E= 1.0 ) p2 -= 1.0;\n\t\t\t\treturn v - PolyBlep( p2, dt );\n\t\t\t}\n\t\t\tdefault: // triangle\n\t\t\t\treturn 4f * MathF.Abs( (float)p - 0.5f ) - 1f;\n\t\t}\n\t}\n\n\t// PolyBLEP residual: the correction applied around a step discontinuity.\n\tstatic float PolyBlep( double t, double dt )\n\t{\n\t\tif ( dt \u003C= 0 ) return 0f;\n\t\tif ( t \u003C dt ) { t /= dt; return (float)(t \u002B t - t * t - 1.0); }\n\t\tif ( t \u003E 1.0 - dt ) { t = (t - 1.0) / dt; return (float)(t * t \u002B t \u002B t \u002B 1.0); }\n\t\treturn 0f;\n\t}\n\n\tstatic float Midi( int m ) =\u003E 440f * MathF.Pow( 2f, (m - 69) / 12f );\n\n\tconst float Sqrt2 = 1.41421356f;\n\t// Fixed (not randomized) stereo spread for the kit\u0027s off-centre voices: 25% each way.\n\tconst float DrumPan = 0.25f;\n\tstatic void StereoGains( float pan, out float gL, out float gR )\n\t{\n\t\tpan = Math.Clamp( pan, -1f, 1f );\n\t\tdouble ang = (pan \u002B 1) * 0.5 * (Math.PI / 2);\n\t\tgL = (float)Math.Cos( ang ) * Sqrt2;\n\t\tgR = (float)Math.Sin( ang ) * Sqrt2;\n\t}\n\n\t// \u2500\u2500 Drums \u2500\u2500\n\t// Render a bar of kit. On a section\u0027s last bar (fillEnd) the closing beat is replaced by a\n\t// fill \u2014 driven by its own RNG streams so every section\u0027s fill is different even when the\n\t// groove before it is identical.\n\tvoid RenderDrumBar( int barStart, int spe, bool fillEnd, Rng noise, Rng fillRng, Rng fillNoise )\n\t{\n\t\t// Knob ceiling was too frantic: scale so DRUM BUSY 100% reads as the old 75%.\n\t\tfloat busy = Math.Clamp( _c.DrumBusy, 0f, 1f ) * 0.75f;\n\t\tint six = spe / 2;\n\t\tint hatEnd = fillEnd ? 6 : EighthsPerBar;         // hats stop where the fill begins\n\n\t\t// closed hats on eighths (open on the \u0022and of 4\u0022); busy fills the gaps with\n\t\t// quieter sixteenth-note hats (constant 16th chatter at the top of the range). On\n\t\t// ride songs the ride cymbal carries the eighth pulse instead (bell on the beats), with\n\t\t// the open hat still punctuating the \u0022and of 4\u0022.\n\t\tfor ( int e = 0; e \u003C hatEnd; e\u002B\u002B )\n\t\t{\n\t\t\tint at = Swung( barStart, spe, e );\n\t\t\t// Pop pumps an open hat on every offbeat (the classic four-on-the-floor \u0022ts-ts-ts\u0022);\n\t\t\t// every other style opens only on the \u0022and of 4\u0022.\n\t\t\tbool open = e == 7 || (_drumStyle == 4 \u0026\u0026 e % 2 == 1);\n\t\t\tfloat amp = e % 2 == 1 ? _c.HatVol : _c.HatVol * 0.6f;\n\t\t\tif ( _ride \u0026\u0026 !open )\n\t\t\t\tRenderRide( at, e % 2 == 0, amp, noise );    // bell accent on the downbeats\n\t\t\telse\n\t\t\t\tRenderHat( at, open, amp, noise );\n\t\t\tif ( !open \u0026\u0026 six \u003E 0 \u0026\u0026 noise.Chance( busy ) )\n\t\t\t{\n\t\t\t\tint sixAt = Swung( barStart, spe, e \u002B 0.5 );\n\t\t\t\tif ( _ride ) RenderRide( sixAt, false, _c.HatVol * 0.4f, noise );\n\t\t\t\telse RenderHat( sixAt, false, _c.HatVol * 0.4f, noise );\n\t\t\t}\n\t\t}\n\n\t\tif ( fillEnd )\n\t\t{\n\t\t\tRenderKickSnareGroove( barStart, spe, 0, 6, busy, noise );   // first 3 beats normal\n\t\t\tRenderFill( barStart, spe, 6, fillNoise, fillRng );\n\t\t\treturn;\n\t\t}\n\t\tRenderKickSnareGroove( barStart, spe, 0, EighthsPerBar, busy, noise );\n\t}\n\n\t// Per-song kick accents for the straight backbeat: eighths (beyond the beat-1 \u0026 3 anchors)\n\t// the kick leans into. e1 = \u0022and of 1\u0022, e3 = \u0022and of 2\u0022, e5 = \u0022and of 3\u0022, e6 = beat 4,\n\t// e7 = \u0022and of 4\u0022. One set is picked per song, then each accent is rolled per bar so the\n\t// groove breathes instead of stamping the same kick pattern every bar \u2014 the main nuance\n\t// lever that keeps rock/country from all sharing one mechanical backbeat.\n\tstatic readonly int[][] BackbeatKickAccents =\n\t{\n\t\tnew int[0],          // bone-dry: just 1 \u0026 3\n\t\tnew[] { 3 },         // push into the snare (\u0022and of 2\u0022)\n\t\tnew[] { 7 },         // pickup into the next bar (\u0022and of 4\u0022)\n\t\tnew[] { 3, 7 },      // push \u002B pickup\n\t\tnew[] { 6 },         // driving beat-4 kick\n\t\tnew[] { 5, 7 },      // syncopated \u0022and of 3\u0022 \u002B pickup\n\t\tnew[] { 3, 6 },      // push into 3 \u002B beat-4 drive\n\t};\n\n\tvoid RenderKickSnareGroove( int barStart, int spe, int from, int to, float busy, Rng noise )\n\t{\n\t\tint six = spe / 2;\n\t\tfor ( int e = from; e \u003C to; e\u002B\u002B )\n\t\t{\n\t\t\tint at = Swung( barStart, spe, e );\n\t\t\tint sixAt = six \u003E 0 ? Swung( barStart, spe, e \u002B 0.5 ) : at;\n\t\t\tswitch ( _drumStyle )\n\t\t\t{\n\t\t\t\tcase 0: // one-drop: kick \u002B snare together on beat 3\n\t\t\t\t\tif ( e == 4 ) { RenderKick( at, noise ); RenderSnare( at, noise, false ); }\n\t\t\t\t\telse if ( e == 2 \u0026\u0026 noise.Chance( _c.GhostSnareChance * (0.4f \u002B busy) ) ) RenderSnare( at, noise, true );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 1: // steppers: kick every beat, snare on 2 \u0026 4\n\t\t\t\t\tif ( e % 2 == 0 ) RenderKick( at, noise );\n\t\t\t\t\tif ( e == 2 || e == 6 ) RenderSnare( at, noise, false );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 3: // metal double-kick: 16th-note kick gallop \u002B crashing, snare backbeat\n\t\t\t\t\tif ( e == 0 \u0026\u0026 noise.Chance( 0.55f ) ) RenderCrash( at, noise, noise.Chance( 0.35f ) );\n\t\t\t\t\tRenderKick( at, noise );\n\t\t\t\t\tif ( six \u003E 0 ) RenderKick( sixAt, noise ); // the second pedal \u2192 the 16th gallop\n\t\t\t\t\tif ( e == 2 || e == 6 ) RenderSnare( at, noise, false );\n\t\t\t\t\tbreak;\n\t\t\t\tcase 4: // pop four-on-the-floor: kick on every beat, backbeat snare/clap on 2 \u0026 4\n\t\t\t\t\tif ( e % 2 == 0 ) RenderKick( at, noise );\n\t\t\t\t\tif ( e == 2 || e == 6 ) RenderSnare( at, noise, false );\n\t\t\t\t\telse if ( noise.Chance( _c.GhostSnareChance * busy * 0.5f ) ) RenderSnare( at, noise, true );\n\t\t\t\t\tbreak;\n\t\t\t\tdefault: // straight backbeat \u2014 anchors on beats 1 \u0026 3, plus this song\u0027s kick\n\t\t\t\t\t     // accents, each humanised per bar so the groove breathes\n\t\t\t\t\tbool kick = e == 0 || e == 4;\n\t\t\t\t\tif ( !kick \u0026\u0026 Array.IndexOf( _kickAccents, e ) \u003E= 0 )\n\t\t\t\t\t\tkick = noise.Chance( 0.82f );                 // mostly play the accent, occasionally lay out\n\t\t\t\t\telse if ( !kick \u0026\u0026 e == 3 )\n\t\t\t\t\t\tkick = noise.Chance( _c.KickSyncChance * (0.4f \u002B busy) ); // stray push into beat 3\n\t\t\t\t\tif ( kick ) RenderKick( at, noise );\n\t\t\t\t\tif ( e == 2 || e == 6 ) RenderSnare( at, noise, false );\n\t\t\t\t\telse if ( noise.Chance( _c.GhostSnareChance * busy ) ) RenderSnare( at, noise, true );\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// Busy fills the \u0022e/a\u0022 sixteenths between hits: more snare as busy rises, and \u2014\n\t\t\t// when the tone leans low \u2014 toms too. (Busy \u2192 snare \u002B toms; tone \u2192 tom vs cymbal.)\n\t\t\t// Metal already fills every 16th with the double-kick, so it skips the ghost layer.\n\t\t\tif ( _drumStyle != 3 \u0026\u0026 six \u003E 0 \u0026\u0026 e != 4 \u0026\u0026 noise.Chance( _c.GhostSnareChance * busy ) )\n\t\t\t{\n\t\t\t\tif ( noise.Chance( (1f - _drumTone) * 0.5f ) )\n\t\t\t\t\tRenderTom( sixAt, 150f \u002B 40f * (e \u0026 1), noise );\n\t\t\t\telse\n\t\t\t\t\tRenderSnare( sixAt, noise, true );\n\t\t\t}\n\t\t}\n\t}\n\n\t// Tom/snare roll across the last beat (two eighths). Straight = four 16ths;\n\t// triplet (TripletChance) = six even subdivisions for a rolling shuffle feel.\n\t// The fill rolls across the last beat \u2014 the two eighths starting at baseE (6) up to the bar\n\t// line \u2014 on the same swung grid as the rest of the band.\n\tvoid RenderFill( int barStart, int spe, int baseE, Rng noise, Rng rng )\n\t{\n\t\t// straight = four 16ths; triplet = either an eighth-note triplet (3) or a\n\t\t// faster 16th-note triplet (6) across the beat.\n\t\tint n = rng.Chance( _c.TripletChance ) ? (rng.Chance( 0.5f ) ? 3 : 6) : 4;\n\t\tint step = (spe * 2) / n;\n\t\tfloat[] toms = { 260f, 215f, 175f, 145f, 120f, 100f };\n\t\t// Half the hits stay snare so it still reads as a drum fill; the rest are biased by\n\t\t// DrumTone \u2014 toms when the tone leans low, cymbals (ride hits) when it leans high.\n\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\n\t\t{\n\t\t\tint t = Swung( barStart, spe, baseE \u002B i * 2.0 / n );\n\t\t\tif ( rng.Chance( 0.5f ) ) RenderSnare( t, noise, false );\n\t\t\telse if ( rng.Chance( _drumTone ) ) RenderRide( t, false, _c.HatVol, noise );\n\t\t\telse RenderTom( t, toms[i], noise );  // pan derived from pitch inside RenderTom\n\t\t}\n\t\t// crash into the downbeat (lands on the bar line, an on-beat anchor \u2192 dead straight) \u2014 a\n\t\t// bright crash or a darker, washier crash, picked off the fill stream so the cymbal colour\n\t\t// varies section to section.\n\t\tRenderCrash( Swung( barStart, spe, baseE \u002B 2.0 ), noise, rng.Chance( 0.4f ) );\n\t}\n\n\tvoid RenderKick( int start, Rng noise )\n\t{\n\t\tstart = Math.Max( 0, start \u002B _drumPush );\n\t\tint dur = (int)(_sr * 0.17f);          // a little longer tail for thump (was 0.13)\n\t\tdouble decay = dur * 0.31;             // slightly slower decay = a touch more boom\n\t\tdouble subDecay = dur * 0.55;          // sub layer rings longer for weight\n\t\tdouble phase = 0, subPhase = 0;\n\t\t// noise.Next() only fires in the fixed 3ms click below, so changing dur/decay\n\t\t// here does NOT shift the drum RNG stream (patterns are preserved).\n\t\tint clickLen = (int)(_sr * 0.003f);\n\t\tint end = Math.Min( _bufL.Length, start \u002B dur );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat t = (float)i / dur;\n\t\t\tphase \u002B= (127f - 80f * MathF.Min( 1f, t * 2.6f )) / _sr;   // pitch drop 127\u219247\n\t\t\tsubPhase \u002B= 44f / _sr;                                     // steady sub fundamental\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat subEnv = (float)Math.Exp( -i / subDecay );\n\t\t\tfloat body = (float)Math.Tanh( MathF.Sin( (float)(phase * 2 * Math.PI) ) * 1.6f ) * env;\n\t\t\tfloat sub = MathF.Sin( (float)(subPhase * 2 * Math.PI) ) * 0.3f * subEnv;\n\t\t\tfloat click = i \u003C clickLen ? (noise.Next() * 2f - 1f) * 0.55f * (1f - i / (float)clickLen) : 0f;\n\t\t\tfloat v = (body \u002B sub \u002B click) * _c.KickVol * _c.KickBalance * _drumGain * _drumLowMul;\n\t\t\t_bufL[start \u002B i] \u002B= v; _bufR[start \u002B i] \u002B= v;\n\t\t}\n\t}\n\n\t// One-pole high-pass coefficient (unconditionally stable).\n\tfloat HpCoeff( float fc ) =\u003E (float)(1.0 / (1.0 \u002B 2 * Math.PI * fc / _sr));\n\n\tvoid RenderSnare( int start, Rng noise, bool ghost )\n\t{\n\t\tstart = Math.Max( 0, start \u002B _drumPush );\n\t\t// dur and the single noise.Next()/sample are kept exactly so the drum RNG stream\n\t\t// is unchanged \u2014 only the timbre (more shell body) is rerolled.\n\t\tint dur = (int)(_sr * (ghost ? 0.06f : 0.15f));\n\t\tdouble decay = dur * (ghost ? 0.3 : 0.32);\n\t\tdouble phase = 0, phase2 = 0;\n\t\tfloat amp = _c.SnareVol * _c.SnareBalance * (ghost ? 0.3f : 1f) * _drumGain;\n\t\tfloat a = HpCoeff( 1350f );           // slightly crisper wire crack (was 1200)\n\t\tfloat inPrev = 0f, outPrev = 0f;\n\t\tint end = Math.Min( _bufL.Length, start \u002B dur );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat t = (float)i / dur;\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat drop = 1f - 0.14f * t;       // shell pitch sags a touch \u2192 \u0022dow\u0022\n\t\t\tphase \u002B= 185f * drop / _sr;\n\t\t\tphase2 \u002B= 268f * drop / _sr;\n\t\t\tfloat n = noise.Next() * 2f - 1f;\n\t\t\tfloat hp = a * (outPrev \u002B n - inPrev); inPrev = n; outPrev = hp;\n\t\t\t// two-tone shell body, a bit fuller than before, vs the wire layer\n\t\t\tfloat body = (MathF.Sin( (float)(phase * 2 * Math.PI) ) \u002B MathF.Sin( (float)(phase2 * 2 * Math.PI) ) * 0.6f) * 0.375f;\n\t\t\tfloat v = ((float)Math.Tanh( hp * 1.2f ) * 0.6f \u002B body) * env * amp;\n\t\t\t_bufL[start \u002B i] \u002B= v; _bufR[start \u002B i] \u002B= v;\n\t\t}\n\t}\n\n\tvoid RenderTom( int start, float baseFreq, Rng noise )\n\t{\n\t\tstart = Math.Max( 0, start \u002B _drumPush );\n\t\t// Pan by PITCH so a given tom always sits in the same spot (rack/high left \u2192 floor/low\n\t\t// right). Mapped across the commonly-played fill range (~145 Hz floor .. 260 Hz rack) so a\n\t\t// normal fill sweeps the full field and the low toms read clearly to the right; anything\n\t\t// below the range clamps hard right. Scaled by the STEREO WIDTH slider via _drumPan.\n\t\tfloat u = Math.Clamp( (baseFreq - 145f) / (260f - 145f), 0f, 1f ); // 0 = low/floor, 1 = high/rack\n\t\tStereoGains( -_drumPan * (u * 2f - 1f), out float gL, out float gR );\n\t\tint dur = (int)(_sr * 0.18f);\n\t\tdouble decay = dur * 0.3;\n\t\tdouble attackDecay = dur * 0.06;        // fast-decaying upper partial \u2192 beater \u0022snap\u0022\n\t\tdouble phase = 0, phase2 = 0;\n\t\t// deterministic per-hit stick attack \u2014 a local LFSR so the shared drum RNG stream\n\t\t// (and therefore every other pattern) is left byte-identical.\n\t\tuint ns = (uint)start * 2654435761u | 1u;\n\t\tint clickLen = (int)(_sr * 0.006f);\n\t\tint end = Math.Min( _bufL.Length, start \u002B dur );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat t = (float)i / dur;\n\t\t\tfloat pf = baseFreq * (1f - 0.22f * t);   // less pitch sag \u2192 holds its note, more bite\n\t\t\tphase \u002B= pf / _sr;\n\t\t\tphase2 \u002B= pf * 2.5f / _sr;                 // inharmonic upper partial for attack snap\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat aenv = (float)Math.Exp( -i / attackDecay );\n\t\t\tfloat body = MathF.Sin( (float)(phase * 2 * Math.PI) ) * env;\n\t\t\tfloat snap = MathF.Sin( (float)(phase2 * 2 * Math.PI) ) * aenv * 0.5f;\n\t\t\tfloat click = 0f;\n\t\t\tif ( i \u003C clickLen )\n\t\t\t{\n\t\t\t\tns ^= ns \u003C\u003C 13; ns ^= ns \u003E\u003E 17; ns ^= ns \u003C\u003C 5;\n\t\t\t\tclick = ((ns \u0026 0xffff) / 32768f - 1f) * 0.45f * (1f - i / (float)clickLen);\n\t\t\t}\n\t\t\tfloat v = (body \u002B snap \u002B click) * _c.TomVol * _c.TomBalance * _drumGain * _drumLowMul;\n\t\t\t_bufL[start \u002B i] \u002B= v * gL; _bufR[start \u002B i] \u002B= v * gR;\n\t\t}\n\t}\n\n\tvoid RenderHat( int start, bool open, float amp, Rng noise )\n\t{\n\t\tstart = Math.Max( 0, start \u002B _drumPush );\n\t\t// Closed/open hats live on the left of the kit (the hi-hat stand); ride sits opposite.\n\t\tStereoGains( -_drumPan, out float gL, out float gR );\n\t\tint dur = (int)(_sr * (open ? 0.16f : 0.035f));\n\t\tdouble decay = dur * 0.4;\n\t\tfloat a = HpCoeff( 7000f );\n\t\tfloat inPrev = 0f, outPrev = 0f;\n\t\tint end = Math.Min( _bufL.Length, start \u002B dur );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat n = noise.Next() * 2f - 1f;\n\t\t\tfloat hp = a * (outPrev \u002B n - inPrev); inPrev = n; outPrev = hp;\n\t\t\tfloat v = hp * env * amp * _c.HatBalance * _drumGain * _drumHighMul;\n\t\t\t_bufL[start \u002B i] \u002B= v * gL; _bufR[start \u002B i] \u002B= v * gR;\n\t\t}\n\t}\n\n\t// Two crash colours off one voice: the bright crash (short-ish, high-passed high) and a\n\t// dark crash \u2014 lower cutoff, longer wash, a touch quieter \u2014 for a bigger china/ride-crash.\n\t// The kit has two crashes panned apart (\u00B125%); the bright/dark colour picks which physical\n\t// cymbal, and _crashBrightLeft (per song) decides which side that is.\n\tvoid RenderCrash( int start, Rng noise, bool dark = false )\n\t{\n\t\tstart = Math.Max( 0, start \u002B _drumPush );\n\t\tStereoGains( (dark == _crashBrightLeft ? _drumPan : -_drumPan), out float gL, out float gR );\n\t\tint dur = (int)(_sr * (dark ? 0.9f : 0.6f));\n\t\tdouble decay = dur * (dark ? 0.5 : 0.45);\n\t\tfloat a = HpCoeff( dark ? 2600f : 4000f );\n\t\tfloat amp = _c.CrashVol * _c.CrashBalance * (dark ? 0.85f : 1f);\n\t\tfloat inPrev = 0f, outPrev = 0f;\n\t\tint end = Math.Min( _bufL.Length, start \u002B dur );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat n = noise.Next() * 2f - 1f;\n\t\t\tfloat hp = a * (outPrev \u002B n - inPrev); inPrev = n; outPrev = hp;\n\t\t\tfloat v = hp * env * amp * _drumGain * _drumHighMul;\n\t\t\t_bufL[start \u002B i] \u002B= v * gL; _bufR[start \u002B i] \u002B= v * gR;\n\t\t}\n\t}\n\n\t// Ride cymbal \u2014 a sustained, high-passed noise wash, nothing more. A cymbal is mostly\n\t// filtered noise; we deliberately render *only* that body and no pitched component. Earlier\n\t// versions layered inharmonic sine partials for a metallic \u0022ping\u0022/bell, but any tonal layer\n\t// (however quiet or detuned) read as a pitched ring/ding, so it\u0027s gone entirely. The bell\n\t// hit (on the beat) just rings a touch longer than the bow hit (on the \u0022and\u0022). Tracks off\n\t// HatVol via the caller\u0027s amp so the existing DRUMS knobs still balance it.\n\tvoid RenderRide( int start, bool bell, float amp, Rng noise )\n\t{\n\t\tstart = Math.Max( 0, start \u002B _drumPush );\n\t\t// Ride sits opposite the hats, on the right of the kit.\n\t\tStereoGains( _drumPan, out float gL, out float gR );\n\t\tint dur = (int)(_sr * (bell ? 0.34f : 0.22f));\n\t\tdouble decay = dur * 0.42;\n\t\tfloat a = HpCoeff( 7000f );\n\t\tfloat inPrev = 0f, outPrev = 0f;\n\t\tint end = Math.Min( _bufL.Length, start \u002B dur );\n\t\tfor ( int i = 0; start \u002B i \u003C end; i\u002B\u002B )\n\t\t{\n\t\t\tfloat env = (float)Math.Exp( -i / decay );\n\t\t\tfloat n = noise.Next() * 2f - 1f;\n\t\t\tfloat hp = a * (outPrev \u002B n - inPrev); inPrev = n; outPrev = hp;\n\t\t\tfloat v = hp * 0.7f * env * amp * _c.HatBalance * _drumGain * _drumHighMul;\n\t\t\t_bufL[start \u002B i] \u002B= v * gL; _bufR[start \u002B i] \u002B= v * gR;\n\t\t}\n\t}\n\n\t// \u2500\u2500 Output \u2500\u2500\n\tshort[] ToShorts( float gain )\n\t{\n\t\tint n = _bufL.Length;\n\t\tvar s = new short[n * Channels];\n\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\n\t\t{\n\t\t\ts[i * 2] = ToS16( _bufL[i] * gain );\n\t\t\ts[i * 2 \u002B 1] = ToS16( _bufR[i] * gain );\n\t\t}\n\t\treturn s;\n\t}\n\n\tstatic short ToS16( float v ) =\u003E (short)(Math.Clamp( v, -1f, 1f ) * 32767f);\n\n\t/// \u003Csummary\u003EWrap already-rendered 16-bit samples in a WAV (for export). Mono or\n\t/// interleaved stereo per \u003Cparamref name=\u0022channels\u0022/\u003E.\u003C/summary\u003E\n\tpublic static byte[] WavFromSamples( short[] samples, int channels, int sampleRate )\n\t{\n\t\tint dataSize = samples.Length * 2;\n\t\tint blockAlign = channels * 2;\n\t\tvar bytes = new System.Collections.Generic.List\u003Cbyte\u003E( 44 \u002B dataSize );\n\t\tvoid Str( string s ) { foreach ( var ch in s ) bytes.Add( (byte)ch ); }\n\t\tvoid U32( uint v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v \u003E\u003E 8) ); bytes.Add( (byte)(v \u003E\u003E 16) ); bytes.Add( (byte)(v \u003E\u003E 24) ); }\n\t\tvoid U16( ushort v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v \u003E\u003E 8) ); }\n\t\tStr( \u0022RIFF\u0022 ); U32( (uint)(36 \u002B dataSize) ); Str( \u0022WAVE\u0022 );\n\t\tStr( \u0022fmt \u0022 ); U32( 16 ); U16( 1 ); U16( (ushort)channels );\n\t\tU32( (uint)sampleRate ); U32( (uint)(sampleRate * blockAlign) ); U16( (ushort)blockAlign ); U16( 16 );\n\t\tStr( \u0022data\u0022 ); U32( (uint)dataSize );\n\t\tforeach ( var s in samples ) { ushort u = (ushort)s; bytes.Add( (byte)u ); bytes.Add( (byte)(u \u003E\u003E 8) ); }\n\t\treturn bytes.ToArray();\n\t}\n\n\tbyte[] EncodeWav( float gain )\n\t{\n\t\tint n = _bufL.Length;\n\t\tint dataSize = n * 4;\n\t\tvar bytes = new System.Collections.Generic.List\u003Cbyte\u003E( 44 \u002B dataSize );\n\t\tvoid Str( string s ) { foreach ( var ch in s ) bytes.Add( (byte)ch ); }\n\t\tvoid U32( uint v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v \u003E\u003E 8) ); bytes.Add( (byte)(v \u003E\u003E 16) ); bytes.Add( (byte)(v \u003E\u003E 24) ); }\n\t\tvoid U16( ushort v ) { bytes.Add( (byte)v ); bytes.Add( (byte)(v \u003E\u003E 8) ); }\n\t\tStr( \u0022RIFF\u0022 ); U32( (uint)(36 \u002B dataSize) ); Str( \u0022WAVE\u0022 );\n\t\tStr( \u0022fmt \u0022 ); U32( 16 ); U16( 1 ); U16( 2 );\n\t\tU32( (uint)_sr ); U32( (uint)(_sr * 4) ); U16( 4 ); U16( 16 );\n\t\tStr( \u0022data\u0022 ); U32( (uint)dataSize );\n\t\tfor ( int i = 0; i \u003C n; i\u002B\u002B )\n\t\t{\n\t\t\tushort l = (ushort)ToS16( _bufL[i] * gain );\n\t\t\tushort r = (ushort)ToS16( _bufR[i] * gain );\n\t\t\tbytes.Add( (byte)l ); bytes.Add( (byte)(l \u003E\u003E 8) );\n\t\t\tbytes.Add( (byte)r ); bytes.Add( (byte)(r \u003E\u003E 8) );\n\t\t}\n\t\treturn bytes.ToArray();\n\t}\n}\n"},{"Ident":"gamah.splitclicker","Path":"UI/Hud.razor","FileName":"Hud.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"@using System\n@using System.Collections.Generic\n@using System.Linq\n@using System.Threading.Tasks\n@using Sandbox\n@using Sandbox.UI\n@using Splitclicker.Api\n@using Splitclicker.Game\n@using Skafinity\n@namespace Splitclicker.UI\n@inherits PanelComponent\n\n@*\n\tThe whole HUD as a SINGLE root PanelComponent. This matters: a ScreenPanel lays\n\tits child PanelComponent roots out in a flex row and ignores \u0060position\u0060 set on\n\tthose roots, so sibling panels can\u0027t pin/center themselves. The working pattern\n\t(see rotaliate\u0027s GameHud) is one root with \u0060width/height: 100%\u0060 that genuinely\n\tfills the screen, with every piece an absolutely-positioned CHILD of it:\n\n\t  \u2022 session board   \u2014 pinned top-left\n\t  \u2022 a bounding box inset between the left/right boards \u2014 holds the status HUD\n\t    (centred) and the global button, which is small and JUMPS to a random spot\n\t    inside the box each time the round arms (whack-a-mole); \u0022clicks sent\u0022 sits\n\t    pinned to the box bottom\n\t  \u2022 hourly board     \u2014 pinned top-right, just left of hours-won\n\t  \u2022 hours-won board  \u2014 pinned hard to the top-right edge\n*@\n\n\u003Croot\u003E\n\t@{ var c = ClickController.Instance; }\n\n\t@* \u2500\u2500 left column: the session board stacked above the all-time board, flowed in\n\t     a flex column so they can never overlap however many rows each grows to\n\t     (both are capped at DisplayLimit). Pinned top-left. \u2500\u2500 *@\n\t\u003Cdiv class=\u0022left-col\u0022\u003E\n\n\t@* \u2500\u2500 session board: current game\u0027s cumulative standings \u2500\u2500 *@\n\t\u003Cdiv class=\u0022board session\u0022\u003E\n\t\t\u003Cdiv class=\u0022title\u0022\u003ESESSION \u00B7 TOP CLICKERS\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t@if ( SessionEntries.Count == 0 )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003ENo scores yet\u003C/div\u003E\n\t\t}\n\t\telse\n\t\t{\n\t\t\t@for ( int i = 0; i \u003C SessionEntries.Count \u0026\u0026 i \u003C DisplayLimit; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar s = SessionEntries[i];\n\t\t\t\t\u003Cdiv class=\u0022row @(IsMe( s ) ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022rank\u0022\u003E@(i \u002B 1)\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022dot @StatusClass( s )\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022name clickable\u0022 onclick=\u0022@(() =\u003E CopyProfile( s ))\u0022\u003E@DisplayName( s )\u003C/div\u003E\n\t\t\t\t\t@if ( s.BehindMs \u003E 0 )\n\t\t\t\t\t{\n\t\t\t\t\t\t\u003Cdiv class=\u0022behind\u0022 title=\u0022lost the game by @(s.BehindMs)ms\u0022\u003E\u002B@(s.BehindMs)ms\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\t\u003Cdiv class=\u0022pts\u0022\u003E@s.Points\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 all-time clickers board: lifetime total scoring clicks across every skin\n\t     (never resets) \u2500\u2500 *@\n\t\u003Cdiv class=\u0022board alltime\u0022\u003E\n\t\t\u003Cdiv class=\u0022title\u0022\u003EALL-TIME \u00B7 TOP CLICKERS\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t@if ( _allTime.Count == 0 )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003E@(_allTimeLoaded ? \u0022No clicks recorded yet\u0022 : \u0022loading\u2026\u0022)\u003C/div\u003E\n\t\t}\n\t\telse\n\t\t{\n\t\t\t@for ( int i = 0; i \u003C _allTime.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar s = _allTime[i];\n\t\t\t\t\u003Cdiv class=\u0022row @(IsMe( s ) ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022rank\u0022\u003E@(i \u002B 1)\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022dot @StatusClass( s )\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022name clickable\u0022 onclick=\u0022@(() =\u003E CopyProfile( s ))\u0022\u003E@DisplayName( s )\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022pts\u0022\u003E@s.Points\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\u003C/div\u003E\n\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 GAME INFO opener: a wide labelled bar pinned bottom-left, mirroring the\n\t     MUSIC bar that sits beneath it. Toggles the floating \u0022How to Play\u0022 popup;\n\t     the open/closed choice is persisted to local storage (PlayerData) so it\n\t     survives across sessions. The popup itself is rendered near the end of the\n\t     root so it overlays everything. \u2500\u2500 *@\n\t@* \u2500\u2500 PAUSE control: lets a player step away without being flagged AFK. Pinned\n\t     bottom-left ABOVE the GAME INFO bar. Green \u0022PAUSE\u0022 at rest; red \u0022PAUSED\u0022 while\n\t     away. Pressing it parks the player (the server withholds frames \u002B drops them\n\t     from the round\u0027s N); pressing again rejoins on the next button. Also engaged\n\t     automatically when the server auto-parks an idle player. Hidden until connected. \u2500\u2500 *@\n\t@if ( C != null \u0026\u0026 C.Phase != GamePhase.Connecting \u0026\u0026 C.Phase != GamePhase.Disconnected )\n\t{\n\t\t\u003Cdiv class=\u0022pause-fab @PauseFabClass()\u0022 onclick=\u0022@TogglePause\u0022\u003E@PauseFabLabel()\u003C/div\u003E\n\t}\n\n\t\u003Cdiv class=\u0022info-fab\u0022 onclick=\u0022@ToggleHowTo\u0022\u003EGAME INFO\u003C/div\u003E\n\n\t@* \u2500\u2500 the music opener is the Skafinity library\u0027s own toggle button: it lives on the\n\t     MusicUI ScreenPanel and is styled (ShowFab/WideFab/FabLabel in the scene) into the\n\t     wide \u0022MUSIC\u0022 bar pinned bottom-left under the GAME INFO bar, opening/closing its\n\t     own board. This HUD only reads MusicOpen (the panel\u0027s IsOpen) to stop painting the\n\t     game while the board is up. \u2500\u2500 *@\n\n\t@* \u2500\u2500 the live board \u002B status HUD over it \u2500\u2500 *@\n\t@* While the music board is open (MusicOpen) the board \u002B status \u002B \u0022clicks sent\u0022 stop\n\t   drawing \u2014 the panel overlays from its own ScreenPanel, and the connection stays open\n\t   (we just don\u0027t paint the game underneath it). *@\n\t\u003Cdiv class=\u0022center\u0022 @ref=\u0022_box\u0022\u003E\n\t\t@if ( !MusicOpen )\n\t\t{\n\t\t\t@* The scoring surface: one clickable button per live board slot, at its\n\t\t\t   server-placed position (board-% space, same as the pips). The board is the\n\t\t\t   only surface and exists only while armed \u2014 NOTHING during the arming wait or\n\t\t\t   for stray bad clicks. *@\n\t\t\t@if ( c != null \u0026\u0026 c.HasBoard )\n\t\t\t{\n\t\t\t\t@foreach ( var btn in c.LiveButtons )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022board-btn\u0022 style=\u0022left: @(PipPct( btn.X ))%; top: @(PipPct( btn.Y ))%;\u0022\n\t\t\t\t\t     onclick=\u0022@(() =\u003E OnPressSlot( btn.Slot ))\u0022\u003E\u003C/div\u003E\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t@* Opponent cursors: a labelled amber dot per sampled opponent pointer. *@\n\t\t\t@foreach ( var cur in CursorsToShow() )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022opp-cursor\u0022 style=\u0022left: @(PipPct( cur.X ))%; top: @(PipPct( cur.Y ))%;\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022opp-cursor-name\u0022\u003E@cur.Name\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\n\t\t\t@* Opponent click pips: each a half-size button that fades quickly (CSS\n\t\t\t   keyframe) at the clicker\u0027s normalized position with their name over it.\n\t\t\t   Positioned in the same box-% space as the global button, so a pip lands\n\t\t\t   where that player\u0027s button was when they clicked. *@\n\t\t\t@foreach ( var pip in PipsToShow() )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022pip\u0022 style=\u0022left: @(PipPct( pip.X ))%; top: @(PipPct( pip.Y ))%;\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022pip-name\u0022\u003E@pip.Name\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\n\t\t\t@* Each line below is a SINGLE interpolated expression on purpose: mixing\n\t\t\t   literal text with razor expressions in one element emits several text nodes,\n\t\t\t   which s\u0026box\u0027s panel reconciler reorders across rebuilds (the \u0022arms in 2-6s\u0022\n\t\t\t   -\u003E \u0022- arms in 26s\u0022 / vanishing-penalty bug). One expression = one stable node. *@\n\t\t\t\u003Cdiv class=\u0022hud\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022meta\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022players\u0022\u003E@($\u0022{c?.Players ?? 0} online\u0022)\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022sep\u0022\u003E\u00B7\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022clicks\u0022\u003E@($\u0022{c?.ClicksToWin ?? 0} clicks exist for this round\u0022)\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022round\u0022\u003E@($\u0022Round {c?.Round ?? 0} / {c?.Of ?? 0}\u0022)\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state @PhaseClass()\u0022\u003E@PhaseText()\u003C/div\u003E\n\t\t\t\t@* live clicks-remaining counter: descends from N as the race fills,\n\t\t\t\t   driven by the server tick (clamped at 0). Only while the window is open. *@\n\t\t\t\t@if ( c?.Phase == GamePhase.Armed )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022remaining\u0022\u003E@($\u0022{RemainingShown( c )} left\u0022)\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\t@if ( c?.Phase == GamePhase.Pending \u0026\u0026 (c?.ArmMaxSec ?? 0) \u003E 0 )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022arm\u0022\u003E@($\u0022arms in {c.ArmMinSec}\u2013{c.ArmMaxSec}s\u0022)\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\t@* dev note: a host-pushed broadcast message, orange. Independent of round\n\t\t\t\t   phase; shows until the server sends an empty note. *@\n\t\t\t\t@if ( c != null \u0026\u0026 !string.IsNullOrEmpty( c.DevNote ) )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022dev-note\u0022\u003E@c.DevNote\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\u003C/div\u003E\n\n\t\t\t\u003Cdiv class=\u0022clicks-sent\u0022\u003E@($\u0022clicks sent: {c?.ClicksSent ?? 0}\u0022)\u003C/div\u003E\n\t\t}\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 top-clickers board: points scored during the current bounty window\n\t     (resets when the skin is won), pinned right (inner) \u2500\u2500 *@\n\t\u003Cdiv class=\u0022board hourly\u0022\u003E\n\t\t\u003Cdiv class=\u0022title\u0022\u003ETOP CLICKERS \u00B7 THIS BOUNTY\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022reset\u0022\u003Eresets when the bounty is won\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t@if ( !_hasBounty )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003ENo active bounty\u003C/div\u003E\n\t\t}\n\t\telse if ( _hourly.Count == 0 )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003E@(_hourlyLoaded ? \u0022No clicks for this bounty yet\u0022 : \u0022loading\u2026\u0022)\u003C/div\u003E\n\t\t}\n\t\telse\n\t\t{\n\t\t\t@for ( int i = 0; i \u003C _hourly.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar s = _hourly[i];\n\t\t\t\t\u003Cdiv class=\u0022row @(IsMe( s ) ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022rank\u0022\u003E@(i \u002B 1)\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022dot @StatusClass( s )\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022name clickable\u0022 onclick=\u0022@(() =\u003E CopyProfile( s ))\u0022\u003E@DisplayName( s )\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022pts\u0022\u003E@s.Points\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 hours-won board: UTC clock-hours topped during the current bounty window\n\t     (resets when the skin is won), pinned right (outer) \u2500\u2500 *@\n\t\u003Cdiv class=\u0022board hourswon\u0022\u003E\n\t\t\u003Cdiv class=\u0022title\u0022\u003EHOURS WON \u00B7 THIS BOUNTY\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t@if ( !_hasBounty )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003ENo active bounty\u003C/div\u003E\n\t\t}\n\t\telse if ( _hoursWon.Count == 0 )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003E@(_hoursWonLoaded ? \u0022No hours won for this bounty yet\u0022 : \u0022loading\u2026\u0022)\u003C/div\u003E\n\t\t}\n\t\telse\n\t\t{\n\t\t\t@for ( int i = 0; i \u003C _hoursWon.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar s = _hoursWon[i];\n\t\t\t\t\u003Cdiv class=\u0022row @(IsMe( s ) ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022rank\u0022\u003E@(i \u002B 1)\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022dot @StatusClass( s )\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022name clickable\u0022 onclick=\u0022@(() =\u003E CopyProfile( s ))\u0022\u003E@DisplayName( s )\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022pts\u0022\u003E@s.Points\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 games-won board: games topped during the current bounty window \u2014 this is\n\t     the board the bounty winner is read from (resets when the skin is won),\n\t     pinned bottom-right \u2500\u2500 *@\n\t\u003Cdiv class=\u0022board sessionswon\u0022\u003E\n\t\t\u003Cdiv class=\u0022title\u0022\u003EGAMES WON \u00B7 THIS BOUNTY\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t@if ( !_hasBounty )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003ENo active bounty\u003C/div\u003E\n\t\t}\n\t\telse if ( _sessionsWon.Count == 0 )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022empty\u0022\u003E@(_sessionsWonLoaded ? \u0022No games won for this bounty yet\u0022 : \u0022loading\u2026\u0022)\u003C/div\u003E\n\t\t}\n\t\telse\n\t\t{\n\t\t\t@for ( int i = 0; i \u003C _sessionsWon.Count; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tvar s = _sessionsWon[i];\n\t\t\t\t\u003Cdiv class=\u0022row @(IsMe( s ) ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022rank\u0022\u003E@(i \u002B 1)\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022dot @StatusClass( s )\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022name clickable\u0022 onclick=\u0022@(() =\u003E CopyProfile( s ))\u0022\u003E@DisplayName( s )\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022pts\u0022\u003E@s.Points\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 leader\u0027s \u0022skin\u0022: image served by the backend (selectable server-side),\n\t     pinned just left of the sessions-won board, captioned with the current\n\t     sessions-won leader\u0027s name. Above it, a countdown to when the winner is\n\t     locked in; once that passes, a prompt for Gamah to set a new skin. \u2500\u2500 *@\n\t\u003Cdiv class=\u0022skin-panel\u0022\u003E\n\t\t@if ( !_hasBounty )\n\t\t{\n\t\t\t@* No bounty is live: the config skin/countdown are a stale fallback, so show\n\t\t\t   nothing of it \u2014 just say there\u0027s no skin to win right now. *@\n\t\t\t\u003Cdiv class=\u0022no-bounty\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022no-bounty-title\u0022\u003ENo active bounty\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022no-bounty-sub\u0022\u003EGamah will set a new bounty to win soon \u2014 check back shortly\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t}\n\t\telse\n\t\t{\n\t\t@if ( HasWinnerLock )\n\t\t{\n\t\t\t@if ( WinnerLockPassed() )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022skin-locked\u0022\u003EGamah will set a new bounty to win when he sees this\u003C/div\u003E\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022skin-countdown\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022cd-label\u0022\u003ETime until winner is locked in\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022cd-time\u0022\u003E@WinnerCountdown()\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\t\u003Cdiv class=\u0022skin-img\u0022 style=\u0022background-image: url( \u0027@SkinImgUrl()\u0027 );\u0022\u003E\u003C/div\u003E\n\t\t@if ( _skin != null \u0026\u0026 _skin.DecodeOk )\n\t\t{\n\t\t\t@if ( _skin.ImageOk )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022skin-name\u0022\u003E@_skin.Name\u003C/div\u003E\n\t\t\t}\n\t\t\t\u003Cdiv class=\u0022skin-stats\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022stat\u0022\u003E\u003Cspan class=\u0022k\u0022\u003EWear\u003C/span\u003E\u003Cspan class=\u0022v\u0022\u003E@_skin.WearName\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022stat\u0022\u003E\u003Cspan class=\u0022k\u0022\u003EFloat\u003C/span\u003E\u003Cspan class=\u0022v\u0022\u003E@FloatStr()\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022stat\u0022\u003E\u003Cspan class=\u0022k\u0022\u003ESeed\u003C/span\u003E\u003Cspan class=\u0022v\u0022\u003E@_skin.PaintSeed\u003C/span\u003E\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022wear-bar\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022seg fn\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022seg mw\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022seg ft\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022seg ww\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022seg bs\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022wear-marker\u0022 style=\u0022left: @(MarkerPct())%;\u0022\u003E\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\t@if ( !_skin.ImageOk )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022skin-note\u0022\u003Eunable to fetch bounty from steam\u003C/div\u003E\n\t\t\t}\n\t\t}\n\t\telse if ( _skin != null )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022skin-note\u0022\u003Eunable to fetch bounty from steam\u003C/div\u003E\n\t\t}\n\t\t@if ( !string.IsNullOrWhiteSpace( _inspectLink ) )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022skin-copy clickable\u0022 onclick=\u0022@CopyInspect\u0022\u003ECopy inspect link\u003C/div\u003E\n\t\t}\n\t\t\u003Cdiv class=\u0022skin-caption\u0022\u003E@($\u0022{SkinLeaderName()}\u0027s Bounty\u0022)\u003C/div\u003E\n\t\t}\n\t\u003C/div\u003E\n\n\t@* \u2500\u2500 previous winner: a duplicate of the skin panel showing who won the last\n\t     bounty and the skin it was (decoded from its inspect link, same as the live\n\t     one). Shown once a bounty has settled; refreshed on a bounty_update push /\n\t     reconnect. The panel is pointer-events:none so it can\u0027t intercept a click\n\t     meant for the roaming button beneath it; only the winner line re-enables\n\t     clicks, to copy their Steam profile link. \u2500\u2500 *@\n\t@if ( _prevBounty != null )\n\t{\n\t\t\u003Cdiv class=\u0022skin-panel previous\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022prev-header\u0022\u003EPREVIOUS WINNER\u003C/div\u003E\n\t\t\t@* The winner is the panel\u0027s identity \u2014 bold and (when we have a steamid)\n\t\t\t   clickable to copy their profile link, the only interactive bit (the rest\n\t\t\t   of the panel is pointer-events:none so it can\u0027t steal a button click). *@\n\t\t\t@if ( !string.IsNullOrEmpty( _prevBounty.WinnerSteamId ) )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022prev-winner clickable\u0022 onclick=\u0022@CopyPrevWinner\u0022\u003E@PrevWinnerName()\u003C/div\u003E\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022prev-winner\u0022\u003E@PrevWinnerName()\u003C/div\u003E\n\t\t\t}\n\t\t\t\u003Cdiv class=\u0022prev-wins\u0022\u003E@($\u0022won {_prevBounty.WinnerWins} games\u0022)\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022skin-img\u0022 style=\u0022background-image: url( \u0027@PrevSkinImgUrl()\u0027 );\u0022\u003E\u003C/div\u003E\n\t\t\t@if ( _prevSkin != null \u0026\u0026 _prevSkin.DecodeOk )\n\t\t\t{\n\t\t\t\t@if ( _prevSkin.ImageOk )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022skin-name\u0022\u003E@_prevSkin.Name\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\t\u003Cdiv class=\u0022skin-stats\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022stat\u0022\u003E\u003Cspan class=\u0022k\u0022\u003EWear\u003C/span\u003E\u003Cspan class=\u0022v\u0022\u003E@_prevSkin.WearName\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022stat\u0022\u003E\u003Cspan class=\u0022k\u0022\u003EFloat\u003C/span\u003E\u003Cspan class=\u0022v\u0022\u003E@PrevFloatStr()\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022stat\u0022\u003E\u003Cspan class=\u0022k\u0022\u003ESeed\u003C/span\u003E\u003Cspan class=\u0022v\u0022\u003E@_prevSkin.PaintSeed\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022wear-bar\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022seg fn\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022seg mw\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022seg ft\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022seg ww\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022seg bs\u0022\u003E\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022wear-marker\u0022 style=\u0022left: @(PrevMarkerPct())%;\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t\telse if ( !string.IsNullOrEmpty( _prevBounty.Label ) )\n\t\t\t{\n\t\t\t\t@* Only as a skin-name fallback when the link didn\u0027t decode \u2014 never as the\n\t\t\t\t   headline (the winner is the headline). *@\n\t\t\t\t\u003Cdiv class=\u0022skin-caption\u0022\u003E@_prevBounty.Label\u003C/div\u003E\n\t\t\t}\n\t\t\u003C/div\u003E\n\t}\n\n\t@* \u2500\u2500 per-round scores flash: a transient panel listing who scored the round that\n\t     just ended (the round\u0027s winners, in first-click order, with the points each\n\t     grabbed THIS round), shown briefly after every round_result then fading out.\n\t     Non-interactive (pointer-events:none) so it never eats a click. Each text line\n\t     is a single interpolated expression (the mixed-text-node reorder bug). \u2500\u2500 *@\n\t@if ( FlashVisible )\n\t{\n\t\t\u003Cdiv class=\u0022round-flash\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022rf-panel\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022rf-title\u0022\u003E@($\u0022ROUND {_flashRound} SCORES\u0022)\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t\t\t@if ( _flashWinners.Count == 0 )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022rf-empty\u0022\u003ENobody scored this round\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t@for ( int i = 0; i \u003C _flashWinners.Count \u0026\u0026 i \u003C DisplayLimit; i\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar w = _flashWinners[i];\n\t\t\t\t\t\t\u003Cdiv class=\u0022rf-row @(IsMe( w ) ? \u0022me\u0022 : \u0022\u0022)\u0022\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022rf-rank\u0022\u003E@(i \u002B 1)\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022rf-name\u0022\u003E@DisplayName( w )\u003C/div\u003E\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022rf-pts\u0022\u003E@($\u0022\u002B{w.Points}\u0022)\u003C/div\u003E\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t}\n\n\t@* \u2500\u2500 transient \u0022copied profile link\u0022 toast, top-center for ~a second \u2500\u2500 *@\n\t@if ( ToastVisible )\n\t{\n\t\t\u003Cdiv class=\u0022toast\u0022\u003E@_toast\u003C/div\u003E\n\t}\n\n\t@* \u2500\u2500 \u0022How to Play\u0022 popup: a floating card centred over the screen (like the music\n\t     player), shown while _howtoOpen. A dim full-screen backdrop captures input so the\n\t     game pauses behind it; the \u2715 (or the GAME INFO bar) closes it. Two sections: the\n\t     round-state primer and a rundown of the server-side anticheat checks (see\n\t     ../server/internal/game). Every text line is a single literal or one interpolated\n\t     expression (the mixed-text-node reorder bug). \u2500\u2500 *@\n\t@if ( _howtoOpen )\n\t{\n\t\t\u003Cdiv class=\u0022howto-overlay\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022howto-card\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022howto-head\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022howto-title\u0022\u003EHOW TO PLAY\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022howto-close\u0022 onclick=\u0022@ToggleHowTo\u0022\u003E\u2715\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022line\u0022\u003EOne global button, limited clicks, shared by everyone.\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag wait\u0022\u003EWAIT\u2026\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EButton is arming - DO NOT CLICK just because it\u0027s green\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag go\u0022\u003ECLICK!\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EButton is live! One point per click.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003EROUND OVER\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EAll clicks used up, current score shown.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003EGAME OVER\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003ESession winner picked, new game next!\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022line penalty-note\u0022\u003EClicking before the button is live is allowed but scores nothing \u2014 just wait for it to go green.\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022howto-subtitle\u0022\u003ESTEPPING AWAY\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag wait\u0022\u003EPAUSE\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003ENeed to step away? Hit PAUSE (above GAME INFO). You drop out of the round and the leaderboard race and won\u0027t be flagged AFK. Hit it again to rejoin on the next button.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022line penalty-note\u0022\u003ESit still and idle through a round and the server pauses you automatically \u2014 just hit PAUSE to come back when you\u0027re ready.\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022howto-subtitle\u0022\u003EANTI-CHEAT\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022line\u0022\u003EEvery round, the server watches for tell-tale bot behaviour. The first flags just bench you behind a quick math question before the button arms again.\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003ETOO FAST\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003ETwo of your scoring clicks landed closer together than any human hand can manage.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003ETOO MANY\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EIn a busy round you took far more than your fair share of the clicks (solo rounds are exempt \u2014 there\u0027s no one to share with).\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003ESOLO FARM\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EYou\u0027re miles ahead on the bounty with nobody around to race. Nothing\u0027s wrong, just check back when it\u0027s busier, or bring some friends so it counts.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003ERUNAWAY\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EYou out-clicked a runner-up who was genuinely competing by an impossible margin (beating an idle player is fine).\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003EAFK\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EDon\u0027t sit still during the wait \u2014 move your cursor while the button is arming.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022state-row\u0022\u003E\u003Cspan class=\u0022tag over\u0022\u003EBUSTED\u003C/span\u003E\u003Cspan class=\u0022desc\u0022\u003EYou scored without ever moving your cursor.\u003C/span\u003E\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022line penalty-note\u0022\u003EKeep getting flagged in one bounty and it escalates: too many flags puts you on a timed cooldown, and a few more after that sidelines you until the bounty is won. Play fair and none of this ever fires \u2014 these only catch automation.\u003C/div\u003E\n\n\t\t\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t\t\t@* Single interpolated string (mixed literal \u002B @() text nodes reorder on\n\t\t\t\t   rebuild \u2014 see CLAUDE.md). Click copies the invite; toast confirms. *@\n\t\t\t\t\u003Cdiv class=\u0022discord-line clickable\u0022 onclick=\u0022@CopyDiscord\u0022\u003E@($\u0022Think a flag was wrong, or want a heads-up on upcoming changes? Click here to copy our Discord invite ({DiscordInvite}) and come say hi.\u0022)\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t}\n\n\t@* \u2500\u2500 parked / away gate: shown while the player has stepped away \u2014 either they hit\n\t     PAUSE or the server auto-parked them off an afk_idle verdict. A dim overlay\n\t     captures input so nothing underneath registers; the server withholds every game\n\t     frame until they rejoin, so there\u0027s nothing to play here. The PAUSE bar (which\n\t     stays visible above GAME INFO) is the single place to rejoin. \u2500\u2500 *@\n\t@* Suppressed while a panel is open: an open how-to/music panel is itself the \u0022away\u0022\n\t   surface, and the park-overlay (z:190) would otherwise stack its RESUME card on top of\n\t   the popup (z:160). Closing the panel auto-rejoins a panel-park, so the overlay only\n\t   ever shows for a real PAUSE / server afk-park with no panel up. \u2500\u2500 *@\n\t@if ( C?.Parked == true \u0026\u0026 !_howtoOpen \u0026\u0026 !MusicOpen )\n\t{\n\t\t\u003Cdiv class=\u0022park-overlay\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022park-card\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022park-title\u0022\u003EPAUSED \u2014 AWAY\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022park-sub\u0022\u003EYou\u0027ve stepped away, so you\u0027re out of the round and off the leaderboard race \u2014 no AFK flags while you\u0027re paused.\u003C/div\u003E\n\t\t\t\t@* While a RESUME is in flight the server defers the rejoin to the next arming\n\t\t\t\t   boundary, so we stay on this surface showing \u0022rejoining next round\u0022 until the\n\t\t\t\t   next round_pending drops us to STAND BY. Otherwise show the RESUME button \u2014\n\t\t\t\t   a child of the overlay card (painted above the backdrop) so it\u0027s reliably\n\t\t\t\t   clickable, unlike the bottom-left PAUSE bar, which s\u0026box hit-tests under this\n\t\t\t\t   full-screen overlay regardless of z-index. \u2500\u2500 *@\n\t\t\t\t@if ( C.Resuming )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022park-hint\u0022\u003ERejoining \u2014 you\u0027ll be back on the next button\u2026\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022park-hint\u0022\u003EPress RESUME to rejoin \u2014 you\u0027ll be back in on the next button.\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022park-resume\u0022 onclick=\u0022@TogglePause\u0022\u003ERESUME\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t}\n\n\t@* \u2500\u2500 anticheat test gate: shown when the player failed an end-of-round check and\n\t     is benched. A full-screen dim overlay (captures input) with the question and\n\t     an answer box; answering correctly un-benches them and the server arms them\n\t     again. Every text line is a single literal or one interpolated expression\n\t     (the mixed-text-node reorder bug). \u2500\u2500 *@\n\t@if ( C?.HasTest == true )\n\t{\n\t\t\u003Cdiv class=\u0022test-overlay\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022test-card\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022test-title\u0022\u003EGamah Anti-Cheat v0.67.69.420\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022test-sub\u0022\u003EYou probably know what you did so do some math now instead...\u003C/div\u003E\n\t\t\t\t@if ( !string.IsNullOrEmpty( C.TestMessage ) )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022test-reason\u0022\u003E@C.TestMessage\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\t\u003Cdiv class=\u0022test-prompt\u0022\u003E@($\u0022{C.TestPrompt} = ?\u0022)\u003C/div\u003E\n\t\t\t\t\u003CTextEntry @ref=\u0022_testEntry\u0022 placeholder=\u0022answer\u0022 class=\u0022test-input\u0022 maxlength=\u002212\u0022 /\u003E\n\t\t\t\t\u003Cdiv class=\u0022test-submit\u0022 onclick=\u0022@SubmitTest\u0022\u003ESUBMIT\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t}\n\n\t@* \u2500\u2500 anticheat sanction gate: shown when the player is past the test rung \u2014 on a\n\t     timed cooldown or ignored for the rest of the bounty. Same dim overlay, but no\n\t     test to answer: just the reason and a countdown to when the state lifts (reuses\n\t     the bounty-timer HH:MM:SS format). Each line is a single interpolated\n\t     expression (the mixed-text-node reorder bug). \u2500\u2500 *@\n\t@if ( !string.IsNullOrEmpty( C?.SanctionState ) )\n\t{\n\t\t\u003Cdiv class=\u0022test-overlay\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022test-card sanction\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022test-title\u0022\u003EGamah Anti-Cheat v0.67.69.420\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022test-reason\u0022\u003E@(C.SanctionMessage)\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022sanction-label\u0022\u003E@(C.SanctionState == \u0022ignored\u0022 ? \u0022Back in the game when the bounty is won\u0022 : \u0022Cooldown ends in\u0022)\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022sanction-time\u0022\u003E@SanctionCountdown()\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t}\n\u003C/root\u003E\n\n\u003Cstyle\u003E\n\t/* The root genuinely fills the screen, so every absolutely-positioned child\n\t   below anchors against the full screen rect. pointer-events:none here; only\n\t   the button re-enables them. */\n\troot {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tpointer-events: none;\n\t}\n\n\t/* \u2500\u2500 boards: shared look; each pinned via its own position class \u2500\u2500 */\n\t.board {\n\t\tposition: absolute;\n\t\ttop: 18px;\n\t\twidth: 280px;\n\t\tflex-direction: column;\n\t\tbackground-color: rgba(3, 13, 7, 0.85);\n\t\tborder: 1.5px solid rgba(255, 255, 255, 0.12);\n\t\tborder-radius: 10px;\n\t\tpadding: 12px 14px;\n\t\tgap: 3px;\n\t}\n\n\t/* left column: session board stacked above the all-time board, pinned top-left.\n\t   The two boards flow in this flex column (their own absolute pin is cancelled\n\t   below) so they grow downward independently and can never overlap each other. */\n\t.left-col {\n\t\tposition: absolute;\n\t\ttop: 18px;\n\t\tleft: 14px;\n\t\tflex-direction: column;\n\t\tgap: 14px;\n\t}\n\t.left-col .board { position: relative; top: auto; left: auto; right: auto; }\n\n\t/* GAME INFO opener: a wide labelled bar pinned bottom-left (top:auto cancels the\n\t   shared pins), lifted clear of the bottom edge so the MUSIC bar sits beneath it.\n\t   Mirrors the music FAB\u0027s wide-bar geometry/colours so the two read as a pair.\n\t   Re-enables pointer events against the pointer-events:none root. */\n\t.info-fab {\n\t\tposition: absolute;\n\t\tleft: 14px;\n\t\tbottom: 66px;\n\t\twidth: 300px;\n\t\theight: 44px;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tfont-size: 15px;\n\t\tfont-weight: bold;\n\t\tletter-spacing: 1px;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\tbackground-color: rgba(3, 13, 7, 0.85);\n\t\tborder: 1.5px solid rgba(255, 255, 255, 0.12);\n\t\tborder-radius: 8px;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t\tz-index: 150;\n\t}\n\n\t.info-fab:hover {\n\t\tcolor: #ffffff;\n\t\tborder-color: rgba(6, 214, 160, 0.6);\n\t}\n\n\t/* PAUSE control: same wide-bar geometry as the GAME INFO / MUSIC bars, pinned one\n\t   slot ABOVE the GAME INFO bar. Green \u0022PAUSE\u0022 at rest; red \u0022PAUSED\u0022 while away. */\n\t.pause-fab {\n\t\tposition: absolute;\n\t\tleft: 14px;\n\t\tbottom: 120px;\n\t\twidth: 300px;\n\t\theight: 44px;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tfont-size: 15px;\n\t\tfont-weight: bold;\n\t\tletter-spacing: 1px;\n\t\tcolor: #04130b;\n\t\tbackground-color: rgba(6, 214, 160, 0.9);\n\t\tborder: 1.5px solid rgba(6, 214, 160, 0.55);\n\t\tborder-radius: 8px;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t\t/* The PARK direction (not parked \u2192 away) happens here, with no overlay up. While\n\t\t   PARKED the full-screen overlay sits over this bar (s\u0026box hit-tests the overlay\n\t\t   on top regardless of z-index), so rejoin is the overlay\u0027s RESUME button, not this\n\t\t   bar \u2014 the bar then just reads \u0022PAUSED\u0022 as a status. */\n\t\tz-index: 150;\n\t}\n\n\t.pause-fab:hover { background-color: rgba(6, 214, 160, 1); }\n\n\t.pause-fab.paused {\n\t\tcolor: #ffffff;\n\t\tbackground-color: rgba(214, 40, 40, 0.9);\n\t\tborder-color: rgba(214, 40, 40, 0.65);\n\t}\n\n\t.pause-fab.paused:hover { background-color: rgba(214, 40, 40, 1); }\n\n\t/* \u0022PAUSING\u2026\u0022: a manual PAUSE pressed mid-armed, waiting for the next arming boundary\n\t   to take effect (amber, between the green rest and red paused states). */\n\t.pause-fab.pending {\n\t\tcolor: #04130b;\n\t\tbackground-color: rgba(255, 209, 102, 0.92);\n\t\tborder-color: rgba(255, 209, 102, 0.6);\n\t}\n\n\t.pause-fab.pending:hover { background-color: rgba(255, 209, 102, 1); }\n\n\t/* \u2500\u2500 parked / away overlay: a dim full-screen backdrop (captures input so nothing\n\t   underneath registers) with a centred card. Sits above the how-to popup, below the\n\t   anticheat test gate (z:200). The PAUSE bar is lifted above this backdrop (z:195) so\n\t   it stays clickable \u2014 it\u0027s the one way to rejoin. \u2500\u2500 */\n\t.park-overlay {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackground-color: rgba(0, 0, 0, 0.72);\n\t\tpointer-events: all;\n\t\tz-index: 190;\n\t}\n\n\t.park-card {\n\t\tflex-direction: column;\n\t\tflex-shrink: 0;\n\t\twidth: 520px;\n\t\tpadding: 26px 30px;\n\t\talign-items: center;\n\t\tbackground-color: rgba(3, 13, 7, 0.98);\n\t\tborder: 1.5px solid rgba(214, 40, 40, 0.55);\n\t\tborder-radius: 12px;\n\t\tgap: 10px;\n\t\tpointer-events: all;\n\t}\n\n\t.park-title {\n\t\tfont-size: 26px;\n\t\tfont-weight: bold;\n\t\tletter-spacing: 2px;\n\t\tcolor: #ff6b6b;\n\t}\n\n\t.park-sub {\n\t\tfont-size: 16px;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\ttext-align: center;\n\t}\n\n\t.park-hint {\n\t\tfont-size: 15px;\n\t\tcolor: rgba(6, 214, 160, 0.95);\n\t\ttext-align: center;\n\t}\n\n\t/* RESUME button on the parked overlay card \u2014 the reliable rejoin control. */\n\t.park-resume {\n\t\tmargin-top: 8px;\n\t\tpadding: 12px 40px;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tfont-size: 18px;\n\t\tfont-weight: bold;\n\t\tletter-spacing: 2px;\n\t\tcolor: #04130b;\n\t\tbackground-color: rgba(6, 214, 160, 0.92);\n\t\tborder: 1.5px solid rgba(6, 214, 160, 0.6);\n\t\tborder-radius: 8px;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t}\n\n\t.park-resume:hover { background-color: rgba(6, 214, 160, 1); }\n\n\t/* \u2500\u2500 \u0022How to Play\u0022 popup: a dim full-screen backdrop (captures input) with a\n\t   floating card centred over it, like the music player. Sits below the anticheat\n\t   test gate (z:200) but above everything else. \u2500\u2500 */\n\t.howto-overlay {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackground-color: rgba(0, 0, 0, 0.7);\n\t\tpointer-events: all;\n\t\tz-index: 160;\n\t}\n\n\t/* Sized to its content \u2014 NOT scrollable. An overflow:scroll range becomes\n\t   drag-pannable in s\u0026box (you can drag the card around inside the overlay), so\n\t   the card grows to fit instead, like the music board. The content is lean\n\t   enough to fit the 1080-tall reference ScreenPanel scales to. */\n\t.howto-card {\n\t\tflex-direction: column;\n\t\tflex-shrink: 0;\n\t\twidth: 560px;\n\t\tpadding: 22px 28px;\n\t\tbackground-color: rgba(3, 13, 7, 0.98);\n\t\tborder: 1.5px solid rgba(255, 255, 255, 0.12);\n\t\tborder-radius: 12px;\n\t\tgap: 3px;\n\t\tpointer-events: all;\n\t}\n\n\t.howto-card .howto-head {\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tjustify-content: space-between;\n\t\tmargin-bottom: 4px;\n\t}\n\n\t.howto-card .howto-title {\n\t\tfont-size: 22px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.9);\n\t\tletter-spacing: 1px;\n\t}\n\n\t.howto-card .howto-subtitle {\n\t\tfont-size: 16px;\n\t\tfont-weight: bold;\n\t\tcolor: #06d6a0;\n\t\tletter-spacing: 1px;\n\t\tmargin-top: 2px;\n\t\tmargin-bottom: 4px;\n\t}\n\n\t.howto-card .howto-close {\n\t\twidth: 34px;\n\t\theight: 34px;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tfont-size: 20px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\tborder-radius: 6px;\n\t\tcursor: pointer;\n\t}\n\n\t.howto-card .howto-close:hover {\n\t\tcolor: #ffffff;\n\t\tbackground-color: rgba(255, 255, 255, 0.08);\n\t}\n\n\t.howto-card .divider {\n\t\theight: 1px;\n\t\tbackground-color: rgba(255, 255, 255, 0.1);\n\t\tmargin-top: 6px;\n\t\tmargin-bottom: 8px;\n\t}\n\n\t.howto-card .line {\n\t\tfont-size: 14px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\tmargin-bottom: 6px;\n\t\twhite-space: normal;\n\t}\n\n\t.howto-card .penalty-note {\n\t\tmargin-bottom: 0;\n\t\tmargin-top: 2px;\n\t}\n\n\t.howto-card .state-row {\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tgap: 10px;\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.howto-card .tag {\n\t\twidth: 110px;\n\t\tflex-shrink: 0;\n\t\tfont-size: 13px;\n\t\tfont-weight: bold;\n\t\tletter-spacing: 0.5px;\n\t}\n\n\t.howto-card .tag.wait { color: #06d6a0; }\n\t.howto-card .tag.go { color: #ffffff; }\n\t.howto-card .tag.over { color: #e63946; }\n\n\t.howto-card .desc {\n\t\tflex-grow: 1;\n\t\tfont-size: 13px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\twhite-space: normal;\n\t}\n\n\t/* click-to-copy Discord invite blurb (there\u0027s no in-game URL open; Clipboard\n\t   works, so it copies). Discord blurple, underlines on hover. */\n\t.howto-card .discord-line {\n\t\tfont-size: 13px;\n\t\tfont-weight: bold;\n\t\tcolor: #8b93f5;\n\t\twhite-space: normal;\n\t\tpointer-events: all;\n\t\tcursor: pointer;\n\t}\n\t.howto-card .discord-line:hover { color: #ffffff; text-decoration: underline; }\n\n\t/* The music opener lives on the Skafinity panel\u0027s own ScreenPanel (see scene\n\t   ShowFab/WideFab/FabLabel), not here. */\n\t.board.hourly { right: 308px; } /* 14 (edge) \u002B 280 (hours-won) \u002B 14 (gap) */\n\t.board.hourswon { right: 14px; }\n\t/* sessions-won pins to the free bottom-right corner (top:auto cancels the\n\t   shared top:18px) so the three right-hand boards never crowd the button. */\n\t.board.sessionswon { top: auto; bottom: 18px; right: 14px; }\n\n\t.board .title {\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\tletter-spacing: 1px;\n\t\tjustify-content: center;\n\t\tmargin-bottom: 4px;\n\t}\n\n\t.board .reset {\n\t\tfont-size: 11px;\n\t\tcolor: rgba(255, 255, 255, 0.5);\n\t\tjustify-content: center;\n\t\tmargin-top: -2px;\n\t\tmargin-bottom: 4px;\n\t}\n\n\t.board .divider {\n\t\theight: 1px;\n\t\tbackground-color: rgba(255, 255, 255, 0.1);\n\t\tmargin-bottom: 5px;\n\t}\n\n\t.board .row {\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tfont-size: 13px;\n\t\tcolor: rgba(255, 255, 255, 0.78);\n\t\tpadding: 2px 5px;\n\t\tborder-radius: 4px;\n\t}\n\n\t.board .rank { width: 24px; color: rgba(255, 255, 255, 0.4); font-size: 12px; }\n\t/* anticheat status dot: green live / yellow cooldown / red ignored. */\n\t.board .dot {\n\t\twidth: 8px;\n\t\theight: 8px;\n\t\tflex-shrink: 0;\n\t\tmargin-right: 7px;\n\t\tborder-radius: 50%;\n\t\tbackground-color: #06d6a0;\n\t}\n\t.board .dot.live { background-color: #06d6a0; }\n\t.board .dot.cooldown { background-color: #ffd166; }\n\t.board .dot.ignored { background-color: #e63946; }\n\t.board .name { flex-grow: 1; overflow: hidden; white-space: nowrap; }\n\t.board .pts { color: #06d6a0; font-weight: bold; }\n\t.board .row.me { background-color: rgba(6, 214, 160, 0.15); }\n\n\t/* the tie-break margin (\u0022\u002B12ms\u0022): how far this player trailed the one above\n\t   them on the same score. Dim amber, just left of the points. */\n\t.board .behind { color: #ffb37a; font-size: 11px; margin-right: 6px; flex-shrink: 0; }\n\n\t/* names are clickable (copy the player\u0027s Steam profile link); re-enable\n\t   pointer events on just these against the pointer-events:none root. */\n\t.board .name.clickable { pointer-events: all; cursor: pointer; }\n\t.board .name.clickable:hover { color: #ffffff; text-decoration: underline; }\n\n\t/* hours-won is the gold board */\n\t.board.hourswon .pts { color: #ffd166; }\n\t.board.hourswon .row.me { background-color: rgba(255, 209, 102, 0.15); }\n\n\t/* sessions-won is the blue board */\n\t.board.sessionswon .pts { color: #4cc9f0; }\n\t.board.sessionswon .row.me { background-color: rgba(76, 201, 240, 0.15); }\n\n\t/* all-time clickers: the violet board (stacked under the session board in the\n\t   left column; positioning handled by .left-col). */\n\t.board.alltime .pts { color: #c77dff; }\n\t.board.alltime .row.me { background-color: rgba(199, 125, 255, 0.15); }\n\n\t/* the leader\u0027s \u0022skin\u0022: bundled image \u002B caption, pinned just left of the\n\t   sessions-won board (308 = 14 edge \u002B 280 board \u002B 14 gap), bottom-aligned. */\n\t.skin-panel {\n\t\tposition: absolute;\n\t\tbottom: 18px;\n\t\tright: 308px;\n\t\twidth: 200px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tbackground-color: rgba(3, 13, 7, 0.85);\n\t\tborder: 1.5px solid rgba(255, 255, 255, 0.12);\n\t\tborder-radius: 10px;\n\t\tpadding: 10px;\n\t\tgap: 6px;\n\t}\n\n\t/* countdown to the winner-lock time, sitting above the image. */\n\t.skin-countdown {\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tgap: 1px;\n\t}\n\n\t.skin-countdown .cd-label {\n\t\tfont-size: 12px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\tletter-spacing: 0.5px;\n\t}\n\n\t.skin-countdown .cd-time {\n\t\tfont-size: 22px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffd166;\n\t\tjustify-content: center;\n\t\tletter-spacing: 1px;\n\t}\n\n\t/* shown in place of the countdown once the winner-lock time has passed. */\n\t.skin-locked {\n\t\tfont-size: 13px;\n\t\tfont-weight: bold;\n\t\tcolor: #ff9f1c;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t/* shown in place of the whole skin/countdown when no bounty is active (the\n\t   config skin/countdown would otherwise be a stale fallback). */\n\t.no-bounty {\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpadding: 24px 12px;\n\t\tgap: 8px;\n\t}\n\n\t.no-bounty-title {\n\t\tfont-size: 20px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffd166;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\tletter-spacing: 0.5px;\n\t}\n\n\t.no-bounty-sub {\n\t\tfont-size: 12px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t/* the image (served by the backend) scales to fit the panel width\n\t   (contain keeps its aspect ratio); the url is bound inline from config. */\n\t.skin-img {\n\t\twidth: 100%;\n\t\theight: 240px;\n\t\tbackground-repeat: no-repeat;\n\t\tbackground-position: center;\n\t\tbackground-size: contain;\n\t}\n\n\t.skin-caption {\n\t\tfont-size: 15px;\n\t\tfont-weight: bold;\n\t\tcolor: #4cc9f0;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t}\n\n\t/* click-to-copy the bounty\u0027s CS2 inspect link (no in-game URL open; Clipboard\n\t   works, so it copies). Underlines on hover like the other clickable lines. */\n\t.skin-copy {\n\t\tfont-size: 12px;\n\t\tfont-weight: bold;\n\t\tcolor: #8b93f5;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tpointer-events: all;\n\t\tcursor: pointer;\n\t}\n\t.skin-copy:hover { color: #ffffff; text-decoration: underline; }\n\n\t/* the decoded skin\u0027s name (from the inspect link\u0027s dataset lookup). */\n\t.skin-name {\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffffff;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t/* wear / float / seed rows decoded from the inspect link. */\n\t.skin-stats {\n\t\twidth: 100%;\n\t\tflex-direction: column;\n\t\tgap: 2px;\n\t}\n\n\t.skin-stats .stat {\n\t\tflex-direction: row;\n\t\tjustify-content: space-between;\n\t\tfont-size: 12px;\n\t}\n\n\t.skin-stats .stat .k { color: rgba(255, 255, 255, 0.55); }\n\t.skin-stats .stat .v { color: #e0e0e0; font-weight: bold; }\n\n\t/* the FN\u2192BS wear bar: five segments sized to the wear thresholds with a\n\t   marker line at the float position. */\n\t.wear-bar {\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 8px;\n\t\tflex-direction: row;\n\t\tborder-radius: 3px;\n\t}\n\n\t.wear-bar .seg { height: 100%; }\n\t.wear-bar .seg.fn { width: 7%;  background-color: #4cd964; }\n\t.wear-bar .seg.mw { width: 8%;  background-color: #a8e05f; }\n\t.wear-bar .seg.ft { width: 23%; background-color: #ffd166; }\n\t.wear-bar .seg.ww { width: 7%;  background-color: #ff9f1c; }\n\t.wear-bar .seg.bs { width: 55%; background-color: #ff4d4d; }\n\n\t.wear-marker {\n\t\tposition: absolute;\n\t\ttop: -2px;\n\t\twidth: 2px;\n\t\theight: 12px;\n\t\tbackground-color: #ffffff;\n\t\tborder: 1px solid rgba(0, 0, 0, 0.6);\n\t}\n\n\t/* shown when the inspect link couldn\u0027t be decoded or its image fetched. */\n\t.skin-note {\n\t\tfont-size: 11px;\n\t\tfont-weight: bold;\n\t\tcolor: #ff9f1c;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t/* previous-winner panel: a duplicate skin panel pinned just left of the live one\n\t   (308 \u002B 200 \u002B 20). Display-only \u2014 pointer-events:none so the roaming button\n\t   beneath stays fully clickable; a gold edge \u002B slightly shorter image distinguish\n\t   it from the live \u0022skin to win\u0022. */\n\t.skin-panel.previous {\n\t\tright: 528px;\n\t\tpointer-events: none;\n\t\tborder-color: rgba(255, 209, 102, 0.35);\n\t}\n\n\t.skin-panel.previous .skin-img { height: 150px; }\n\n\t.prev-header {\n\t\tfont-size: 12px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffd166;\n\t\tletter-spacing: 1px;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t.prev-winner {\n\t\tfont-size: 16px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffffff;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t/* the winner line re-enables pointer events over the panel\u0027s pointer-events:none\n\t   (only this line is interactive \u2014 click to copy the winner\u0027s profile link). */\n\t.prev-winner.clickable { pointer-events: all; cursor: pointer; }\n\t.prev-winner.clickable:hover { color: #ffd166; text-decoration: underline; }\n\n\t.prev-wins {\n\t\tfont-size: 12px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t}\n\n\t.board .empty {\n\t\tjustify-content: center;\n\t\tfont-size: 12px;\n\t\tcolor: rgba(255, 255, 255, 0.35);\n\t\tmargin-top: 4px;\n\t}\n\n\t/* \u2500\u2500 center: the BOUNDING BOX the button roams inside, inset between the\n\t   left session board (right edge \u2248 308px) and the right board column (also\n\t   308px), with a little top/bottom breathing room. The button is positioned\n\t   absolutely within this box; the status HUD overlays it centred. \u2500\u2500\n\n\t   The box (and so the button \u002B status HUD \u002B music panel inside it) is kept ABOVE every\n\t   board at all times via z-index. The boards are declared after .center in the markup,\n\t   so without this they\u0027d paint \u2014 and take clicks \u2014 on top of anything in the box. The\n\t   box is normally inset clear of every board, but the roaming button (and the music\n\t   button it can overlap at the bottom-left) must always win paint AND click priority\n\t   when they do intersect, so the box outranks them unconditionally. */\n\t.center {\n\t\tposition: absolute;\n\t\ttop: 24px;\n\t\tbottom: 24px;\n\t\tleft: 308px;\n\t\tright: 308px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t\tz-index: 100;\n\t}\n\n\t/* Opponent click pip: a half-size (70px vs the button\u0027s 140px) green circle that\n\t   pops in and fades over ~0.45s (matched to PipLifetime so it\u0027s gone before\n\t   ClickController prunes it). Positioned in the box\u0027s % space (left/top set inline),\n\t   with margin offsets of \u2212half so the inline %, which is the clicker\u0027s button\n\t   CENTRE, places the pip centred there. pointer-events:none so it never eats a\n\t   click; below the penalty pops, above the button. */\n\t.pip {\n\t\tposition: absolute;\n\t\twidth: 70px;\n\t\theight: 70px;\n\t\tmargin-left: -35px;\n\t\tmargin-top: -35px;\n\t\tborder-radius: 50%;\n\t\tbackground-color: #06d6a0;\n\t\tborder: 2px solid rgba(255, 255, 255, 0.8);\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t\tz-index: 110;\n\t\tanimation-name: pip-fade;\n\t\tanimation-duration: 0.45s;\n\t\tanimation-timing-function: ease-out;\n\t\tanimation-fill-mode: forwards;\n\t}\n\n\t@@keyframes pip-fade {\n\t\t0%   { opacity: 0.9; transform: scale(0.6); }\n\t\t25%  { opacity: 0.85; transform: scale(1); }\n\t\t100% { opacity: 0; transform: scale(1.1); }\n\t}\n\n\t/* The clicker\u0027s name centred over their pip. */\n\t.pip-name {\n\t\tfont-size: 13px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffffff;\n\t\ttext-stroke: 1px #000000;\n\t\ttext-align: center;\n\t\twhite-space: nowrap;\n\t}\n\n\t/* v5 board button: one clickable green SQUARE per live slot. Positioned by its\n\t   server-placed centre (inline left/top %, same box-% space as the pips), centred via\n\t   \u2212half margins. Square (not a circle) so the visual matches the actual click hitbox\n\t   AND the \u0060touch\u0060 hitbox \u2014 s\u0026box hit-tests the panel\u0027s box, so a circle would leave the\n\t   corners clickable-but-untouched and false-trip the no_hover check; it also matches the\n\t   server\u0027s square non-overlap spacing model (board.go posMinDist). Smaller than the\n\t   legacy single button since up to X share the box. No teleport \u2014 positions are\n\t   server-authoritative, so left/top can transition-free. */\n\t.board-btn {\n\t\tposition: absolute;\n\t\twidth: 96px;\n\t\theight: 96px;\n\t\tmargin-left: -48px;\n\t\tmargin-top: -48px;\n\t\tborder-radius: 12px;\n\t\tbackground-color: #06d6a0; /* live green \u2014 the board only exists while armed */\n\t\tborder: 4px solid rgba(255, 255, 255, 0.9);\n\t\ttransition: transform 0.08s ease-out;\n\t\tpointer-events: all;\n\t\tcursor: pointer;\n\t\tz-index: 105;\n\t}\n\t.board-btn:active { transform: scale(0.9); }\n\n\t/* Opponent cursor: a small amber dot at another player\u0027s sampled pointer, with their\n\t   name beneath. Box-% space like the pips; never eats a click. */\n\t.opp-cursor {\n\t\tposition: absolute;\n\t\twidth: 16px;\n\t\theight: 16px;\n\t\tmargin-left: -8px;\n\t\tmargin-top: -8px;\n\t\tborder-radius: 50%;\n\t\tbackground-color: rgba(255, 209, 102, 0.9);\n\t\tborder: 2px solid rgba(0, 0, 0, 0.5);\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tpointer-events: none;\n\t\tz-index: 108;\n\t}\n\n\t.opp-cursor-name {\n\t\tposition: absolute;\n\t\ttop: 16px;\n\t\tfont-size: 13px;\n\t\tcolor: #ffd166;\n\t\ttext-stroke: 1px rgba(0, 0, 0, 0.8);\n\t\twhite-space: nowrap;\n\t\tpointer-events: none;\n\t}\n\n\t/* Live clicks-remaining counter, green, shown while the window is open. */\n\t.remaining {\n\t\tfont-size: 40px;\n\t\tfont-weight: bold;\n\t\tcolor: #06d6a0;\n\t\ttext-stroke: 1px rgba(0, 0, 0, 0.5);\n\t}\n\n\t/* HUD pinned to the top edge of the screen, centred horizontally over the box;\n\t   the button roams below/behind it and clicks fall through. */\n\t.hud {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: flex-start;\n\t\tgap: 10px;\n\t\tpointer-events: none;\n\t}\n\n\t.round {\n\t\tfont-size: 22px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\tletter-spacing: 1px;\n\t}\n\n\t.state {\n\t\tfont-size: 54px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.9);\n\t\tletter-spacing: 3px;\n\t}\n\n\t.state.armed { color: #ffffff; }\n\t.state.offline { color: #ffd166; }\n\n\t.meta {\n\t\tflex-direction: row;\n\t\tgap: 10px;\n\t\talign-items: center;\n\t\tfont-size: 18px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t}\n\n\t.meta .sep { color: rgba(255, 255, 255, 0.4); }\n\n\t.arm {\n\t\tfont-size: 16px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\tletter-spacing: 1px;\n\t}\n\n\t/* Host-pushed broadcast note: a large orange message centred in the status HUD. */\n\t.dev-note {\n\t\tfont-size: 32px;\n\t\tfont-weight: bold;\n\t\tcolor: #ff8c00;\n\t}\n\n\t/* The status HUD now floats centred on the dark box (the button roams away from\n\t   it), so the text stays white in every state \u2014 no on-button contrast override. */\n\n\t/* Pinned to the bottom of the bounding box, centred horizontally. */\n\t.clicks-sent {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tjustify-content: center;\n\t\tfont-size: 22px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.65);\n\t\tletter-spacing: 1px;\n\t\tpointer-events: none;\n\t}\n\n\t/* transient confirmation that a profile link was copied; top-center, fades by\n\t   simply un-rendering after ToastDuration (see BuildHash). */\n\t.toast {\n\t\tposition: absolute;\n\t\ttop: 40px;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tjustify-content: center;\n\t\tfont-size: 20px;\n\t\tfont-weight: bold;\n\t\tcolor: #4cc9f0;\n\t\tletter-spacing: 1px;\n\t\tpointer-events: none;\n\t\t/* Above every overlay (the .center box at 100, the how-to popup at 160, the\n\t\t   anticheat gate at 200) so the \u0022copied\u0022 confirmation is visible no matter what\u0027s\n\t\t   open \u2014 e.g. copying the Discord invite from inside the how-to popup. */\n\t\tz-index: 210;\n\t}\n\n\t/* \u2500\u2500 per-round scores flash: a card centred over the play area, shown for\n\t   FlashDuration after each round closes then fading out. The .round-flash layer\n\t   fills the screen and centres the card; the keyframe (pop in \u2192 hold \u2192 fade) runs\n\t   for the full 4s so the card unmounts as the fade ends. Non-interactive and above\n\t   the boards/button but below the how-to popup (160) and the anticheat gate (200).\n\t   The card is sized to its content (no scroll \u2014 an overflow range becomes\n\t   drag-pannable in s\u0026box). */\n\t.round-flash {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tpointer-events: none;\n\t\tz-index: 140;\n\t\tanimation-name: round-flash-anim;\n\t\tanimation-duration: 4s;\n\t\tanimation-timing-function: ease-out;\n\t\tanimation-fill-mode: forwards;\n\t}\n\n\t@@keyframes round-flash-anim {\n\t\t0%   { opacity: 0; }\n\t\t6%   { opacity: 1; }\n\t\t82%  { opacity: 1; }\n\t\t100% { opacity: 0; }\n\t}\n\n\t/* the visible card: same dark panel look as the boards. */\n\t.round-flash .rf-panel {\n\t\tflex-direction: column;\n\t\tflex-shrink: 0;\n\t\twidth: 360px;\n\t\tpadding: 16px 20px;\n\t\tgap: 3px;\n\t\tbackground-color: rgba(3, 13, 7, 0.95);\n\t\tborder: 1.5px solid rgba(255, 255, 255, 0.12);\n\t\tborder-radius: 12px;\n\t}\n\n\t.round-flash .rf-title {\n\t\tfont-size: 22px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.9);\n\t\tletter-spacing: 2px;\n\t\tjustify-content: center;\n\t\tmargin-bottom: 2px;\n\t}\n\n\t.round-flash .divider {\n\t\theight: 1px;\n\t\tbackground-color: rgba(255, 255, 255, 0.1);\n\t\tmargin-bottom: 6px;\n\t}\n\n\t.round-flash .rf-row {\n\t\tflex-direction: row;\n\t\talign-items: center;\n\t\tfont-size: 18px;\n\t\tcolor: rgba(255, 255, 255, 0.85);\n\t\tpadding: 3px 6px;\n\t\tborder-radius: 4px;\n\t}\n\n\t.round-flash .rf-rank { width: 30px; color: rgba(255, 255, 255, 0.4); font-size: 15px; }\n\t.round-flash .rf-name { flex-grow: 1; overflow: hidden; white-space: nowrap; }\n\t.round-flash .rf-pts { color: #06d6a0; font-weight: bold; }\n\t.round-flash .rf-row.me { background-color: rgba(6, 214, 160, 0.15); }\n\n\t.round-flash .rf-empty {\n\t\tjustify-content: center;\n\t\tfont-size: 15px;\n\t\tcolor: rgba(255, 255, 255, 0.5);\n\t\tpadding: 4px 0;\n\t}\n\n\t/* \u2500\u2500 anticheat test gate: a full-screen dim overlay that captures input (above\n\t   everything else) with a centred card holding the question \u002B answer box. \u2500\u2500 */\n\t.test-overlay {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackground-color: rgba(0, 0, 0, 0.75);\n\t\tpointer-events: all; /* block the game beneath while benched */\n\t\tz-index: 200;\n\t}\n\n\t.test-card {\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tgap: 14px;\n\t\twidth: 420px;\n\t\tpadding: 28px 32px;\n\t\tbackground-color: rgba(3, 13, 7, 0.98);\n\t\tborder: 2px solid #e63946;\n\t\tborder-radius: 14px;\n\t}\n\n\t.test-title {\n\t\tfont-size: 26px;\n\t\tfont-weight: bold;\n\t\tcolor: #e63946;\n\t\tletter-spacing: 2px;\n\t\tjustify-content: center;\n\t}\n\n\t.test-sub {\n\t\tfont-size: 14px;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\twhite-space: normal;\n\t}\n\n\t/* the specific check (or sanction) reason, in the anticheat red. */\n\t.test-reason {\n\t\tfont-size: 16px;\n\t\tfont-weight: bold;\n\t\tcolor: #ff8c69;\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\twhite-space: normal;\n\t}\n\n\t/* cooldown/ignored card: no answer box, just the reason \u002B a countdown. The\n\t   amber border separates it from the red math-test card. */\n\t.test-card.sanction { border-color: #ffd166; }\n\n\t.sanction-label {\n\t\tfont-size: 14px;\n\t\tfont-weight: bold;\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\tletter-spacing: 0.5px;\n\t}\n\n\t.sanction-time {\n\t\tfont-size: 40px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffd166;\n\t\tletter-spacing: 2px;\n\t\tjustify-content: center;\n\t}\n\n\t.test-prompt {\n\t\tfont-size: 40px;\n\t\tfont-weight: bold;\n\t\tcolor: #ffffff;\n\t\tletter-spacing: 2px;\n\t\tjustify-content: center;\n\t}\n\n\t/* Mirror the known-good TextEntry recipe (skafinity .tag-input / rotaliate\n\t   .chat-entry): an explicit height \u002B plain \u0060color: white\u0060. Without a height the\n\t   text box collapses and the typed text is clipped (it\u0027s still captured \u2014 the\n\t   answer sends \u2014 just not painted); text-align on a TextEntry also hides it. */\n\t.test-input {\n\t\twidth: 240px;\n\t\theight: 44px;\n\t\tpadding: 0 14px;\n\t\tfont-size: 24px;\n\t\tcolor: white;\n\t\tbackground-color: rgba(255, 255, 255, 0.08);\n\t\tborder: 1.5px solid rgba(255, 255, 255, 0.3);\n\t\tborder-radius: 8px;\n\t\tpointer-events: all;\n\t}\n\n\t.test-submit {\n\t\tfont-size: 18px;\n\t\tfont-weight: bold;\n\t\tcolor: #030d07;\n\t\tbackground-color: #06d6a0;\n\t\tletter-spacing: 1px;\n\t\tpadding: 10px 28px;\n\t\tborder-radius: 8px;\n\t\tcursor: pointer;\n\t\tpointer-events: all;\n\t}\n\n\t.test-submit:hover { background-color: #08f0b4; }\n\u003C/style\u003E\n\n@code {\n\tconst int DisplayLimit = 10;\n\tconst float RefreshInterval = 20f;\n\n\t// Touch-hitbox geometry (reference px, the 1080-tall ScreenPanel space). CenterRefH is\n\t// the .center box\u0027s reference height (1080 \u2212 top:24 \u2212 bottom:24); BoardBtnRefHalf is the\n\t// .board-btn half-extent (96px square \u21D2 48). TouchPadRef DELIBERATELY enlarges the touch\n\t// hitbox beyond the visual/click box so the \u0060touch\u0060 zone is a strict SUPERSET of the\n\t// clickable area: a click can land in the 4px border ring or a frame where the per-frame\n\t// sampler just missed the edge, and no_hover only targets EGREGIOUS cases (cursor nowhere\n\t// near the button), so erring bigger trades a hair of sensitivity for zero edge-click\n\t// false positives. Used to size the square hitbox in actual px when detecting a \u0060touch\u0060\n\t// enter (see OnUpdate).\n\tconst float CenterRefH = 1080f - 24f - 24f;\n\tconst float BoardBtnRefHalf = 48f;\n\tconst float TouchPadRef = 32f;\n\n\tClickController C =\u003E ClickController.Instance;\n\n\t// \u2500\u2500 session: live standings pushed inside round_result/game_over (never fetched) \u2500\u2500\n\tstatic List\u003CStanding\u003E SessionEntries =\u003E ClickController.Instance?.Standings ?? new List\u003CStanding\u003E();\n\n\t// \u2500\u2500 hourly \u002B hours-won: fetched over HTTP, deliberately infrequently (once on\n\t//    connect, then when a round closes, throttled) so neither becomes a fan-in\n\t//    GET stampede. The hot click path never touches HTTP. \u2500\u2500\n\tList\u003CStanding\u003E _hourly = new();\n\tbool _hourlyLoaded, _hourlyFetching;\n\tRealTimeSince _hourlyLast;\n\n\tList\u003CStanding\u003E _hoursWon = new();\n\tbool _hoursWonLoaded, _hoursWonFetching;\n\tRealTimeSince _hoursWonLast;\n\n\tList\u003CStanding\u003E _sessionsWon = new();\n\tbool _sessionsWonLoaded, _sessionsWonFetching;\n\tRealTimeSince _sessionsWonLast;\n\n\tList\u003CStanding\u003E _allTime = new();\n\tbool _allTimeLoaded, _allTimeFetching;\n\tRealTimeSince _allTimeLast;\n\n\tGamePhase _lastPhase = GamePhase.Connecting;\n\n\t// \u2500\u2500 the bounding box (.center), captured by @ref so the board buttons, opponent\n\t//    pips and cursor can be measured/normalized against it. The board buttons are\n\t//    server-placed (board-% space); the client draws no button of its own. \u2500\u2500\n\tPanel _box;\n\n\t// \u2500\u2500 server-driven skin \u002B winner-lock countdown (GET /api/v1/config) \u2500\u2500\n\t// _winnerLockMs is the winner-lock instant as Unix epoch ms (0 = unset, hides\n\t// the countdown); _skinUrl is the absolute URL the backend serves the current\n\t// \u0022skin to win\u0022 image from. Both come from config so they retune without a\n\t// client rebuild. Epoch-ms (not a parsed DateTime) because the s\u0026box sandbox\n\t// doesn\u0027t whitelist System.Globalization \u2014 the countdown is integer math.\n\tlong _winnerLockMs;\n\tstring _skinUrl = \u0022\u0022;\n\n\t// True only while a bounty is actually active (config\u0027s has_bounty). When false the\n\t// skin/countdown above is the host config.json/env fallback (a stale \u0022old\u0022 skin) and\n\t// the bounty-scoped boards have fallen back to all-time data \u2014 so the skin panel and\n\t// the \u0022THIS SKIN\u0022 boards show a \u0022no active bounty\u0022 state instead of that stale data.\n\tbool _hasBounty;\n\n\t// \u2500\u2500 inspect-link skin (decoded locally \u002B name/image resolved off the dataset) \u2500\u2500\n\t// Null until an inspect-linked bounty\u0027s config arrives and resolution completes;\n\t// its DecodeOk/ImageOk flags drive what the skin panel shows vs. the server image.\n\tSkinInspect.Skin _skin;\n\n\t// The active bounty\u0027s raw inspect link (\u0022\u0022 when the skin is an uploaded image\n\t// only). Kept verbatim so the \u0022copy inspect link\u0022 line can hand it to players to\n\t// paste into CS2, independent of whether it decoded/resolved locally.\n\tstring _inspectLink = \u0022\u0022;\n\n\t// \u2500\u2500 previous winner (the just-settled bounty) \u2500\u2500 a duplicate of the skin panel\n\t// showing who won the last skin and what it was. _prevBounty is the most recent\n\t// won bounty (null until fetched / when there\u0027s no history); _prevSkin is its\n\t// inspect link decoded locally (same as _skin); _prevSkinUrl is the per-bounty\n\t// image fallback. _lastBountyRefresh tracks ClickController.BountyRefreshSeq so a\n\t// hello/reconnect or a bounty_update push re-fetches both current and previous.\n\tPreviousBounty _prevBounty;\n\tSkinInspect.Skin _prevSkin;\n\tstring _prevSkinUrl = \u0022\u0022;\n\tint _lastBountyRefresh;\n\n\t// Image for every skin panel: a locally-bundled placeholder. Real CS2/Valve\n\t// skins are no longer surfaced and the image is no longer fetched from the\n\t// server \u2014 it ships with the client (see .sbproj Resources).\n\tconst string TempgunImg = \u0022media/tempgun.png\u0022;\n\tstring SkinImgUrl() =\u003E TempgunImg;\n\n\t// Wear-bar marker position as a 0..100 percentage at the float value, clamped.\n\tint MarkerPct()\n\t{\n\t\tint pct = (int)((_skin?.Float ?? 0f) * 100f \u002B 0.5f);\n\t\treturn pct \u003C 0 ? 0 : pct \u003E 100 ? 100 : pct;\n\t}\n\n\t// Float formatted to 10 dp without culture-sensitive APIs (the sandbox doesn\u0027t\n\t// whitelist System.Globalization): integer part \u002B zero-padded fractional digits.\n\tstring FloatStr()\n\t{\n\t\tdouble d = _skin?.Float ?? 0.0;\n\t\tif ( d \u003C 0 ) d = 0;\n\t\tconst long Scale = 10_000_000_000L; // 10 fractional digits, like the float checker\n\t\tlong scaled = (long)(d * Scale \u002B 0.5);\n\t\treturn $\u0022{scaled / Scale}.{(scaled % Scale).ToString().PadLeft( 10, \u00270\u0027 )}\u0022;\n\t}\n\n\t// \u2500\u2500 previous-winner panel helpers (mirror the current-skin ones, reading the\n\t//    previous bounty\u0027s decoded skin/image instead) \u2500\u2500\n\tstring PrevSkinImgUrl() =\u003E TempgunImg;\n\n\tint PrevMarkerPct()\n\t{\n\t\tint pct = (int)((_prevSkin?.Float ?? 0f) * 100f \u002B 0.5f);\n\t\treturn pct \u003C 0 ? 0 : pct \u003E 100 ? 100 : pct;\n\t}\n\n\tstring PrevFloatStr()\n\t{\n\t\tdouble d = _prevSkin?.Float ?? 0.0;\n\t\tif ( d \u003C 0 ) d = 0;\n\t\tconst long Scale = 10_000_000_000L;\n\t\tlong scaled = (long)(d * Scale \u002B 0.5);\n\t\treturn $\u0022{scaled / Scale}.{(scaled % Scale).ToString().PadLeft( 10, \u00270\u0027 )}\u0022;\n\t}\n\n\t// The previous winner\u0027s display name, tagged \u0022(you)\u0022 when the local player won\n\t// it (matched by the public tag, the only id the client knows itself by).\n\tstring PrevWinnerName()\n\t{\n\t\tif ( _prevBounty == null ) return \u0022\u0022;\n\t\tvar name = string.IsNullOrEmpty( _prevBounty.WinnerName ) ? \u0022\u2014\u0022 : _prevBounty.WinnerName;\n\t\tvar myTag = ClickController.Instance?.Tag;\n\t\tif ( !string.IsNullOrEmpty( myTag ) \u0026\u0026 _prevBounty.WinnerTag == myTag ) name \u002B= \u0022 (you)\u0022;\n\t\treturn name;\n\t}\n\n\t// \u2500\u2500 anticheat test gate: the answer box (captured by @ref so SubmitTest can read\n\t//    it) shown in the overlay while the player is benched. \u2500\u2500\n\tTextEntry _testEntry;\n\n\t// Send the typed answer to the server and clear the box. The server replies with a\n\t// \u0060test\u0060 cleared frame (correct \u2192 un-benched) or a fresh \u0060test\u0060 (wrong); either way\n\t// ClickController updates HasTest/TestPrompt and the overlay re-renders.\n\tvoid SubmitTest()\n\t{\n\t\tvar ans = _testEntry?.Text?.Trim();\n\t\tif ( string.IsNullOrEmpty( ans ) ) return;\n\t\tClickController.Instance?.SubmitTestAnswer( ans );\n\t\tif ( _testEntry != null ) _testEntry.Text = \u0022\u0022;\n\t}\n\n\t// \u2500\u2500 \u0022How to Play\u0022 popup: open state mirrors PlayerData.HowToPlayOpen (persisted to\n\t//    local storage), seeded once from it here so the player\u0027s choice survives across\n\t//    sessions. First-time players default to open (PlayerData default). \u2500\u2500\n\tbool _howtoOpen = PlayerData.Load().HowToPlayOpen;\n\n\t// True once WE auto-parked the player because a panel (the how-to popup or the music\n\t// board) is open \u2014 reading the rules or browsing music means they\u0027re not playing, so we\n\t// step them out of the round \u002B the AFK pass instead of letting a still cursor flag them.\n\t// Latched only when our own park took, so a server afk-park or a manual PAUSE that\n\t// happens to coincide with a panel is never auto-resumed when the panel closes. Driven\n\t// from OnUpdate (PanelParkSync).\n\tbool _panelParked;\n\n\t// Toggle the popup and persist the new state to local storage so it sticks.\n\tvoid ToggleHowTo()\n\t{\n\t\t_howtoOpen = !_howtoOpen;\n\t\tvar data = PlayerData.Load();\n\t\tdata.HowToPlayOpen = _howtoOpen;\n\t\tdata.Save();\n\t\tStateHasChanged();\n\t}\n\n\t// Keep the parked state in sync with the open panels (how-to popup / music board). Open\n\t// panel \u21D2 park; once they\u0027re all closed \u21D2 rejoin \u2014 but only if WE were the ones who\n\t// parked for a panel (_panelParked). A server afk-park or a manual PAUSE that overlaps a\n\t// panel is left alone (we never latched it), so closing the panel doesn\u0027t resume them.\n\t// Park is a no-op without a live socket (e.g. the first-launch popup before connect), so\n\t// we only latch once the park actually took, and retry every frame until it does.\n\tvoid PanelParkSync()\n\t{\n\t\tif ( C == null ) return;\n\t\tbool panelOpen = _howtoOpen || MusicOpen;\n\t\tif ( panelOpen )\n\t\t{\n\t\t\t// Request a park once. Don\u0027t re-issue while one is already parked OR pending: a\n\t\t\t// PAUSE pressed mid-armed is deferred to the next arming boundary (Parked stays\n\t\t\t// false, ParkPending true until then), and re-calling TogglePark would read that\n\t\t\t// as a RESUME and toggle it straight back off.\n\t\t\tif ( !C.Parked \u0026\u0026 !C.ParkPending )\n\t\t\t{\n\t\t\t\tC.TogglePark();\n\t\t\t\tif ( C.Parked || C.ParkPending ) _panelParked = true; // our park took (now or deferred)\n\t\t\t\tStateHasChanged();\n\t\t\t}\n\t\t}\n\t\telse if ( _panelParked )\n\t\t{\n\t\t\t// Panels closed: rejoin if we\u0027re parked or a park is still in flight.\n\t\t\tif ( C.Parked || C.ParkPending ) C.TogglePark();\n\t\t\t_panelParked = false;\n\t\t\tStateHasChanged();\n\t\t}\n\t}\n\n\t// Toggle the away/parked state from the PAUSE bar / parked-overlay RESUME. Delegates to\n\t// the controller (which parks immediately, defers a mid-armed park to the next arming\n\t// boundary, or requests a deferred rejoin) and re-renders so the bar \u002B overlay update.\n\tvoid TogglePause()\n\t{\n\t\tClickController.Instance?.TogglePark();\n\t\tStateHasChanged();\n\t}\n\n\t// PAUSE-bar label/class. \u0022PAUSING\u2026\u0022 (amber) while a mid-armed park waits for the next\n\t// arming boundary; \u0022PAUSED\u0022 (red) once parked; \u0022PAUSE\u0022 (green) at rest.\n\tstring PauseFabLabel()\n\t{\n\t\tvar c = C;\n\t\tif ( c == null ) return \u0022PAUSE\u0022;\n\t\tif ( c.ParkPending ) return \u0022PAUSING\u2026\u0022;\n\t\treturn c.Parked ? \u0022PAUSED\u0022 : \u0022PAUSE\u0022;\n\t}\n\n\tstring PauseFabClass()\n\t{\n\t\tvar c = C;\n\t\tif ( c == null ) return \u0022\u0022;\n\t\tif ( c.ParkPending ) return \u0022pending\u0022;\n\t\treturn c.Parked ? \u0022paused\u0022 : \u0022\u0022;\n\t}\n\n\t// \u2500\u2500 \u0022copied profile link\u0022 toast: shown for ToastDuration after a name click \u2500\u2500\n\tconst float ToastDuration = 1.2f;\n\tstring _toast = \u0022\u0022;\n\tRealTimeSince _toastSince = 1000f; // start expired\n\tbool ToastVisible =\u003E _toastSince \u003C ToastDuration;\n\n\t// \u2500\u2500 per-round scores flash: a transient panel of the round\u0027s scorers (the\n\t//    round_result \u0060winners\u0060, carrying the points each grabbed THAT round), shown\n\t//    for FlashDuration when a round closes. Triggered on the phase change into\n\t//    Result (the final round folds into game_over, whose GAME OVER standings stand\n\t//    in for its flash). A snapshot is taken so it survives the next round overwriting\n\t//    C.Winners. _flashSince starts expired so nothing shows until the first round. \u2500\u2500\n\tconst float FlashDuration = 4f;\n\tList\u003CStanding\u003E _flashWinners = new();\n\tint _flashRound;\n\tRealTimeSince _flashSince = 1000f;\n\tbool FlashVisible =\u003E _flashSince \u003C FlashDuration;\n\n\t// \u2500\u2500 post-game board refresh: the server credits the session win and refreshes\n\t//    the board cache asynchronously (afterGame) AFTER it broadcasts game_over, so\n\t//    a fetch fired the instant game_over lands races ahead of that write and shows\n\t//    the games/hours-won boards a game behind (\u0022lags by 1\u0022). When a game ends we\n\t//    arm a one-shot refresh a short delay later \u2014 past the async write (a sub-second\n\t//    local DB write; the 5s intermission easily covers it) \u2014 so the just-finished\n\t//    game is reflected while the GAME OVER standings are still up. \u2500\u2500\n\tconst float PostGameRefreshDelay = 2f;\n\tbool _postGamePending;\n\tRealTimeSince _postGameSince = 1000f;\n\n\tprotected override void OnTreeFirstBuilt()\n\t{\n\t\t_ = LoadConfig();\n\t\t_ = LoadPreviousBounty();\n\t\t_ = RefreshHourly();\n\t\t_ = RefreshHoursWon();\n\t\t_ = RefreshSessionsWon();\n\t\t_ = RefreshAllTime();\n\t}\n\n\t// Fetch the winner-lock time \u002B skin image URL (the active bounty). Re-run on\n\t// (re)connect and on a bounty_update push, so the panel always reflects the\n\t// current bounty. On failure the countdown simply doesn\u0027t show and the image\n\t// stays blank \u2014 no hard dependency.\n\tasync Task LoadConfig()\n\t{\n\t\tvar cfg = await ApiClient.GetConfig();\n\t\tif ( cfg == null ) return;\n\t\t_skinUrl = ApiClient.AbsoluteUrl( cfg.SkinUrl ) ?? \u0022\u0022;\n\t\t_winnerLockMs = cfg.WinnerLockMs;\n\t\t_inspectLink = cfg.InspectLink ?? \u0022\u0022;\n\t\t_hasBounty = cfg.HasBounty;\n\t\t// Clear the previous bounty\u0027s decoded skin so a re-fetch (rollover) doesn\u0027t\n\t\t// leave the old name/stats showing under the new skin \u2014 ResolveSkin refills it\n\t\t// when this bounty also has an inspect link; an image-only bounty keeps it null.\n\t\t_skin = null;\n\t\tStateHasChanged();\n\n\t\t// If the active bounty was set by an inspect link, decode it locally and\n\t\t// resolve its name/stats (cached dataset) in the background. We use the decoded\n\t\t// name \u002B wear/float/seed, but the displayed image is always the local tempgun\n\t\t// placeholder (SkinImgUrl), never the Valve weapon image.\n\t\tif ( !string.IsNullOrWhiteSpace( cfg.InspectLink ) )\n\t\t\t_ = ResolveSkin( cfg.InspectLink );\n\t}\n\n\t// Decode the bounty\u0027s inspect link \u002B resolve its name/stats, then re-render. On\n\t// any failure _skin is left with DecodeOk/ImageOk false so the panel shows the\n\t// \u0022unable to fetch bounty from steam\u0022 note; the image is the tempgun placeholder\n\t// either way.\n\tasync Task ResolveSkin( string link )\n\t{\n\t\t_skin = await SkinInspect.Resolve( link );\n\t\tStateHasChanged();\n\t}\n\n\t// Fetch the most recent settled bounty (the \u0022previous winner\u0022) and, if it was an\n\t// inspect-link skin, decode its name/stats for the render (the image stays the\n\t// tempgun placeholder). Re-run alongside LoadConfig so a rollover surfaces the\n\t// just-settled winner immediately. The resolve is guarded by id so a slow decode\n\t// can\u0027t clobber a newer previous bounty.\n\tasync Task LoadPreviousBounty()\n\t{\n\t\tvar list = await ApiClient.GetPreviousBounties();\n\t\tvar b = ( list != null \u0026\u0026 list.Count \u003E 0 ) ? list[0] : null;\n\t\t_prevBounty = b;\n\t\t_prevSkin = null;\n\t\t_prevSkinUrl = b != null ? ( ApiClient.AbsoluteUrl( b.SkinUrl ) ?? \u0022\u0022 ) : \u0022\u0022;\n\t\tStateHasChanged();\n\n\t\tif ( b != null \u0026\u0026 !string.IsNullOrWhiteSpace( b.InspectLink ) )\n\t\t{\n\t\t\tvar id = b.Id;\n\t\t\tvar skin = await SkinInspect.Resolve( b.InspectLink );\n\t\t\tif ( _prevBounty != null \u0026\u0026 _prevBounty.Id == id ) // still the same previous bounty\n\t\t\t{\n\t\t\t\t_prevSkin = skin;\n\t\t\t\tStateHasChanged();\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// The bounty rolled over (or we just (re)connected): re-fetch the active skin\n\t\t// and the previous winner so neither goes stale. Driven by the server push /\n\t\t// hello via ClickController.BountyRefreshSeq \u2014 no polling.\n\t\tint seq = C?.BountyRefreshSeq ?? 0;\n\t\tif ( seq != _lastBountyRefresh )\n\t\t{\n\t\t\t_lastBountyRefresh = seq;\n\t\t\t_ = LoadConfig();\n\t\t\t_ = LoadPreviousBounty();\n\t\t}\n\n\t\t// While a panel is open (the how-to popup \u2014 which pops up on first launch \u2014 or the\n\t\t// music board), the player is reading/browsing, not playing: their cursor sits still\n\t\t// and the server would flag them AFK. Auto-park them (drops them out of the round\u0027s N\n\t\t// \u002B the AFK pass) and rejoin them when the panels close. See _panelParked.\n\t\tPanelParkSync();\n\n\t\tvar phase = C?.Phase ?? GamePhase.Connecting;\n\n\t\t// Report our pointer to the server (throttled in SendCursor) so other players see our\n\t\t// roaming cursor AND so the server\u0027s AFK pass can tell a present player from an away\n\t\t// one. As of v7 this runs during BOTH the arming wait (Pending) and the live window\n\t\t// (Armed) \u2014 the server now watches arming-phase movement, not just armed. The .center\n\t\t// box (_box) is laid out throughout (it\u0027s a fixed absolutely-positioned container that\n\t\t// exists in every phase, with or without buttons), so its rect is valid during Pending\n\t\t// too. Mouse.Position and Box.Rect share the same screen-pixel space, so the pointer\n\t\t// normalizes to the box exactly like the buttons/pips (\u22121..1, 0 = centre). Re-render is\n\t\t// driven by BoardHash in BuildHash (board claims \u002B cursor moves), so no per-frame\n\t\t// StateHasChanged needed here.\n\t\tif ( ( phase == GamePhase.Armed || phase == GamePhase.Pending ) \u0026\u0026 C != null )\n\t\t{\n\t\t\tvar r = _box?.Box.Rect ?? default;\n\t\t\tif ( r.Width \u003E 0f \u0026\u0026 r.Height \u003E 0f )\n\t\t\t{\n\t\t\t\tvar m = Sandbox.Mouse.Position;\n\t\t\t\tfloat nx = ( m.x - r.Center.x ) / ( r.Width / 2f );\n\t\t\t\tfloat ny = ( m.y - r.Center.y ) / ( r.Height / 2f );\n\t\t\t\t// Only report the cursor while it\u0027s actually ON the board (within -1..1).\n\t\t\t\t// A pointer resting OUTSIDE the board (over the side leaderboards) used to\n\t\t\t\t// clamp to the \u002B-1 edge, and tiny hand jitter oscillating across that edge\n\t\t\t\t// made a physically-still cursor look like it moved a lot, defeating the\n\t\t\t\t// afk check. Off-board samples are simply not sent: a player parked off the\n\t\t\t\t// board reports no cursor (the server reads that as afk by the no-cursor\n\t\t\t\t// half), and on-board movement is measured cleanly with no edge artefact.\n\t\t\t\tif ( nx \u003E= -1f \u0026\u0026 nx \u003C= 1f \u0026\u0026 ny \u003E= -1f \u0026\u0026 ny \u003C= 1f )\n\t\t\t\t\tC.SendCursor( nx, ny );\n\n\t\t\t\t// Armed-only: fire a one-shot \u0060touch\u0060 the instant the pointer enters a live\n\t\t\t\t// button\u0027s hitbox (ClickController de-dupes per window). The button is a 96px\n\t\t\t\t// CSS SQUARE; the touch hitbox is an axis-aligned box of half-extent\n\t\t\t\t// (BoardBtnRefHalf \u002B TouchPadRef), deliberately a bit BIGGER than the visual/\n\t\t\t\t// click box so an edge click (the 4px border ring, or a frame the sampler just\n\t\t\t\t// missed) always counts as touched \u2014 no_hover only targets egregious \u0022cursor\n\t\t\t\t// nowhere near\u0022 cases. ScreenPanel scales uniformly, so the half-extent is the\n\t\t\t\t// same on both axes in actual px. .center spans top:24/bottom:24 of the 1080\n\t\t\t\t// ref, so its reference height is CenterRefH; scale = actual box height \u00F7 that.\n\t\t\t\tif ( phase == GamePhase.Armed )\n\t\t\t\t{\n\t\t\t\t\tfloat half = ( BoardBtnRefHalf \u002B TouchPadRef ) * ( r.Height / CenterRefH );\n\t\t\t\t\tforeach ( var btn in C.LiveButtons )\n\t\t\t\t\t{\n\t\t\t\t\t\tfloat cx = r.Center.x \u002B btn.X * ( r.Width / 2f );\n\t\t\t\t\t\tfloat cy = r.Center.y \u002B btn.Y * ( r.Height / 2f );\n\t\t\t\t\t\tif ( MathF.Abs( m.x - cx ) \u003C= half \u0026\u0026 MathF.Abs( m.y - cy ) \u003C= half )\n\t\t\t\t\t\t\tC.SendTouch( btn.Slot );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ( phase != _lastPhase )\n\t\t{\n\t\t\t// A round just closed: flash its scores. Snapshot the winners (the next\n\t\t\t// round overwrites C.Winners) and restart the flash timer. Only on Result \u2014\n\t\t\t// the final round folds into GAME OVER, whose standings are its own display.\n\t\t\tif ( phase == GamePhase.Result )\n\t\t\t{\n\t\t\t\t_flashWinners = C?.Winners != null ? new List\u003CStanding\u003E( C.Winners ) : new();\n\t\t\t\t_flashRound = C?.Round ?? 0;\n\t\t\t\t_flashSince = 0f;\n\t\t\t}\n\n\t\t\t// Refresh the boards when a round just closed (player is reading scores\n\t\t\t// between presses), throttled so rapid result\u2192pending churn can\u0027t spam.\n\t\t\t// GAME OVER is deliberately excluded: a fetch here races the server\u0027s async\n\t\t\t// post-game write (see below), so the games/hours-won boards would show a\n\t\t\t// game behind. Game-end refresh is handled by the delayed one-shot instead.\n\t\t\tif ( phase == GamePhase.Result )\n\t\t\t{\n\t\t\t\tif ( _hourlyLast \u003E RefreshInterval ) _ = RefreshHourly();\n\t\t\t\tif ( _hoursWonLast \u003E RefreshInterval ) _ = RefreshHoursWon();\n\t\t\t\tif ( _sessionsWonLast \u003E RefreshInterval ) _ = RefreshSessionsWon();\n\t\t\t\tif ( _allTimeLast \u003E RefreshInterval ) _ = RefreshAllTime();\n\t\t\t}\n\n\t\t\t// A game just ended: arm the one-shot post-game refresh so the boards pick\n\t\t\t// up this game\u0027s session win once the server\u0027s afterGame write has landed.\n\t\t\tif ( phase == GamePhase.GameOver )\n\t\t\t{\n\t\t\t\t_postGamePending = true;\n\t\t\t\t_postGameSince = 0f;\n\t\t\t}\n\n\t\t\t_lastPhase = phase;\n\t\t\tStateHasChanged();\n\t\t}\n\n\t\t// Fire the armed post-game refresh once the delay has elapsed (the server\u0027s\n\t\t// async session-win write is done by now), so the games/hours-won boards reflect\n\t\t// the just-finished game instead of lagging a game behind.\n\t\tif ( _postGamePending \u0026\u0026 _postGameSince \u003E= PostGameRefreshDelay )\n\t\t{\n\t\t\t_postGamePending = false;\n\t\t\t_ = RefreshHourly();\n\t\t\t_ = RefreshHoursWon();\n\t\t\t_ = RefreshSessionsWon();\n\t\t\t_ = RefreshAllTime();\n\t\t}\n\t}\n\n\tasync Task RefreshHourly()\n\t{\n\t\tif ( _hourlyFetching ) return;\n\t\t_hourlyFetching = true;\n\t\t_hourlyLast = 0;\n\t\ttry\n\t\t{\n\t\t\tvar board = await ApiClient.GetHourlyLeaderboard( DisplayLimit );\n\t\t\t_hourly = board ?? new List\u003CStanding\u003E();\n\t\t\t_hourlyLoaded = true;\n\t\t\tStateHasChanged();\n\t\t}\n\t\tfinally { _hourlyFetching = false; }\n\t}\n\n\tasync Task RefreshHoursWon()\n\t{\n\t\tif ( _hoursWonFetching ) return;\n\t\t_hoursWonFetching = true;\n\t\t_hoursWonLast = 0;\n\t\ttry\n\t\t{\n\t\t\tvar board = await ApiClient.GetHoursWonLeaderboard( DisplayLimit );\n\t\t\t_hoursWon = board ?? new List\u003CStanding\u003E();\n\t\t\t_hoursWonLoaded = true;\n\t\t\tStateHasChanged();\n\t\t}\n\t\tfinally { _hoursWonFetching = false; }\n\t}\n\n\tasync Task RefreshSessionsWon()\n\t{\n\t\tif ( _sessionsWonFetching ) return;\n\t\t_sessionsWonFetching = true;\n\t\t_sessionsWonLast = 0;\n\t\ttry\n\t\t{\n\t\t\tvar board = await ApiClient.GetSessionsWonLeaderboard( DisplayLimit );\n\t\t\t_sessionsWon = board ?? new List\u003CStanding\u003E();\n\t\t\t_sessionsWonLoaded = true;\n\t\t\tStateHasChanged();\n\t\t}\n\t\tfinally { _sessionsWonFetching = false; }\n\t}\n\n\tasync Task RefreshAllTime()\n\t{\n\t\tif ( _allTimeFetching ) return;\n\t\t_allTimeFetching = true;\n\t\t_allTimeLast = 0;\n\t\ttry\n\t\t{\n\t\t\tvar board = await ApiClient.GetAllTimeClickersLeaderboard( DisplayLimit );\n\t\t\t_allTime = board ?? new List\u003CStanding\u003E();\n\t\t\t_allTimeLoaded = true;\n\t\t\tStateHasChanged();\n\t\t}\n\t\tfinally { _allTimeFetching = false; }\n\t}\n\n\t// Copy the clicked player\u0027s public Steam community profile link to the\n\t// clipboard and flash a short confirmation toast. SteamID64 is public, so the\n\t// link works for anyone; no-op if the row carries no id.\n\tvoid CopyProfile( Standing s )\n\t{\n\t\tif ( s == null || string.IsNullOrEmpty( s.SteamId ) ) return;\n\t\tvar url = $\u0022https://steamcommunity.com/profiles/{s.SteamId}\u0022;\n\t\ttry { Clipboard.SetText( url ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] clipboard copy failed: {e.Message}\u0022 ); }\n\t\t_toast = $\u0022Copied {DisplayName( s )}\u0027s profile link\u0022;\n\t\t_toastSince = 0f;\n\t\tStateHasChanged();\n\t}\n\n\t// Copy the previous bounty winner\u0027s Steam profile link (same as CopyProfile, but\n\t// the winner is carried on the bounty record, not a Standing). No-op when the\n\t// settled bounty had no winner (empty window \u2192 no steamid).\n\tvoid CopyPrevWinner()\n\t{\n\t\tvar sid = _prevBounty?.WinnerSteamId;\n\t\tif ( string.IsNullOrEmpty( sid ) ) return;\n\t\tvar url = $\u0022https://steamcommunity.com/profiles/{sid}\u0022;\n\t\ttry { Clipboard.SetText( url ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] clipboard copy failed: {e.Message}\u0022 ); }\n\t\tvar who = string.IsNullOrEmpty( _prevBounty.WinnerName ) ? \u0022winner\u0022 : _prevBounty.WinnerName;\n\t\t_toast = $\u0022Copied {who}\u0027s profile link\u0022;\n\t\t_toastSince = 0f;\n\t\tStateHasChanged();\n\t}\n\n\t// Discord invite (shared with the rotaliate family). No in-game URL-open API, so\n\t// the how-to popup\u0027s blurb copies it to the clipboard instead. DiscordInvite is the\n\t// short display form; DiscordUrl is what lands on the clipboard.\n\tconst string DiscordInvite = \u0022discord.gg/GG8HWUfFpD\u0022;\n\tconst string DiscordUrl = \u0022https://discord.gg/GG8HWUfFpD\u0022;\n\n\t// Copy the Discord invite and flash the confirmation toast (visible above the\n\t// how-to popup \u2014 see the toast\u0027s z-index).\n\tvoid CopyDiscord()\n\t{\n\t\ttry { Clipboard.SetText( DiscordUrl ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] clipboard copy failed: {e.Message}\u0022 ); }\n\t\t_toast = \u0022Copied the Discord invite \u2014 see you there!\u0022;\n\t\t_toastSince = 0f;\n\t\tStateHasChanged();\n\t}\n\n\t// Copy the active bounty\u0027s CS2 inspect link to the clipboard so the player can\n\t// paste it into the game and inspect the exact skin. No-op when there\u0027s no link.\n\tvoid CopyInspect()\n\t{\n\t\tif ( string.IsNullOrWhiteSpace( _inspectLink ) ) return;\n\t\ttry { Clipboard.SetText( _inspectLink ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022[Splitclicker] clipboard copy failed: {e.Message}\u0022 ); }\n\t\t_toast = \u0022Copied the bounty\u0027s inspect link\u0022;\n\t\t_toastSince = 0f;\n\t\tStateHasChanged();\n\t}\n\n\tstatic bool IsMe( Standing s )\n\t{\n\t\tvar tag = ClickController.Instance?.Tag;\n\t\treturn !string.IsNullOrEmpty( tag ) \u0026\u0026 s.Tag == tag;\n\t}\n\n\t// Anticheat status dot class for a board row: green when live (the default),\n\t// yellow on a cooldown, red when ignored for the bounty. Empty/unknown \u21D2 live.\n\tstatic string StatusClass( Standing s ) =\u003E s?.Status switch\n\t{\n\t\t\u0022cooldown\u0022 =\u003E \u0022cooldown\u0022,\n\t\t\u0022ignored\u0022 =\u003E \u0022ignored\u0022,\n\t\t_ =\u003E \u0022live\u0022,\n\t};\n\n\t// Show the player\u0027s name (claimed username or Steam display name, resolved\n\t// server-side), never the opaque hex tag.\n\tstatic string DisplayName( Standing s ) =\u003E\n\t\t!string.IsNullOrWhiteSpace( s.Username ) ? s.Username : \u0022anonymous\u0022;\n\n\t// The \u0022current leader\u0022 for the skin caption: the sessions-won #1, falling back\n\t// to the live session leader, then to a placeholder when nobody has scored.\n\tstring SkinLeaderName()\n\t{\n\t\tif ( _sessionsWon.Count \u003E 0 ) return DisplayName( _sessionsWon[0] );\n\t\tvar session = SessionEntries;\n\t\tif ( session.Count \u003E 0 ) return DisplayName( session[0] );\n\t\treturn \u0022Nobody\u0022;\n\t}\n\n\t// Whether a winner-lock time is configured at all (0 = unset \u2192 hide countdown).\n\tbool HasWinnerLock =\u003E _winnerLockMs \u003E 0;\n\n\tstatic long NowMs() =\u003E DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();\n\n\t// True once the winner-lock instant has passed (config loaded and now \u2265 it).\n\tbool WinnerLockPassed() =\u003E HasWinnerLock \u0026\u0026 NowMs() \u003E= _winnerLockMs;\n\n\t// Time left until the winner locks in, as HH:MM:SS (clamped at zero). Pure\n\t// integer math on epoch-ms \u2014 no DateTime parsing (sandbox-safe).\n\tstring WinnerCountdown()\n\t{\n\t\tlong left = _winnerLockMs - NowMs();\n\t\tif ( left \u003C 0 ) left = 0;\n\t\tlong s = left / 1000;\n\t\treturn $\u0022{s / 3600:D2}:{(s % 3600) / 60:D2}:{s % 60:D2}\u0022;\n\t}\n\n\t// Time left on the active anticheat sanction (cooldown end, or the bounty resolve\n\t// time for \u0022ignored\u0022), as HH:MM:SS clamped at zero \u2014 same integer epoch-ms math\n\t// as the winner countdown (sandbox-safe).\n\tstring SanctionCountdown()\n\t{\n\t\tlong left = ( C?.SanctionUntilMs ?? 0 ) - NowMs();\n\t\tif ( left \u003C 0 ) left = 0;\n\t\tlong s = left / 1000;\n\t\treturn $\u0022{s / 3600:D2}:{(s % 3600) / 60:D2}:{s % 60:D2}\u0022;\n\t}\n\n\t// A click on a board button, addressed by its slot id (NOT a captured button\n\t// object): resolve the button currently live at that slot and score it via its nonce.\n\t// Slots are never reused within a round, so a stale render-closure firing an old slot\n\t// simply resolves to null and no-ops here \u2014 it can never send a consumed nonce at a\n\t// button that\u0027s already been claimed/replaced. No teleport; the board is server-placed.\n\tvoid OnPressSlot( ushort slot )\n\t{\n\t\tvar btn = ClickController.Instance?.FindButton( slot );\n\t\tif ( btn == null )\n\t\t\treturn; // already claimed/replaced (or a stale closure) \u2014 clean miss, nothing sent\n\n\t\tClickController.Instance?.SendButtonClick( btn );\n\t\tStateHasChanged();\n\t}\n\n\t// \u2500\u2500 opponent pips (read from ClickController, rendered \u002B faded by CSS) \u2500\u2500\n\tstatic readonly List\u003CClickController.PipButton\u003E NoPips = new();\n\tIReadOnlyList\u003CClickController.PipButton\u003E PipsToShow() =\u003E ClickController.Instance?.ActivePips ?? NoPips;\n\n\t// \u2500\u2500 opponent cursors (read from ClickController, rendered as labelled dots) \u2500\u2500\n\tstatic readonly List\u003CClickController.CursorDot\u003E NoCursors = new();\n\tIReadOnlyList\u003CClickController.CursorDot\u003E CursorsToShow() =\u003E ClickController.Instance?.Cursors ?? NoCursors;\n\n\t// A cheap hash of the live board \u002B opponent cursors so BuildHash re-renders when a\n\t// button is claimed/spawned or a cursor moves (positions quantized to ~box-% steps).\n\tint BoardHash()\n\t{\n\t\tvar c = C;\n\t\tif ( c == null ) return 0;\n\t\tvar h = new HashCode();\n\t\th.Add( c.LiveButtons.Count );\n\t\tforeach ( var b in c.LiveButtons ) h.Add( b.Slot );\n\t\th.Add( c.Cursors.Count );\n\t\tforeach ( var cur in c.Cursors )\n\t\t{\n\t\t\th.Add( (int)(cur.X * 200f) );\n\t\t\th.Add( (int)(cur.Y * 200f) );\n\t\t}\n\t\treturn h.ToHashCode();\n\t}\n\n\t// Normalized \u22121..1 position \u2192 box left/top percentage (0..100), clamped so an\n\t// off-screen pip pins to the box edge rather than overflowing. Integer math keeps\n\t// the inline style locale-safe (no culture-sensitive float formatting).\n\tstatic int PipPct( float n ) =\u003E (int)Math.Clamp( ( n \u002B 1f ) / 2f * 100f, 0f, 100f );\n\n\t// The live clicks-remaining counter, floored at 0 (a late tick can\u0027t show negative).\n\tstatic int RemainingShown( ClickController c ) =\u003E Math.Max( 0, c.RemainingThisRound );\n\n\t// \u2500\u2500 music panel toggle \u2500\u2500\n\t// The settings board is the Skafinity library\u0027s own SkafinityMusicPanel (the green\n\t// drop-in), mounted on its OWN ScreenPanel GameObject \u2014 a PanelComponent can\u0027t be a\n\t// sibling child of this HUD\u0027s single root (see CLAUDE.md ScreenPanel note). It owns its\n\t// own opener (the wide \u0022MUSIC\u0022 bar \u2014 ShowFab/WideFab/FabLabel in the scene) and its own\n\t// close, so the HUD never toggles it; it only READS IsOpen to stop painting the game\n\t// while the board is up. The optional inspector ref is just a fast-path; we fall back to\n\t// a scene lookup so a blank field can\u0027t leave the HUD blind to the board\u0027s state.\n\t[Property] public SkafinityMusicPanel MusicBoard { get; set; }\n\n\tSkafinityMusicPanel Board =\u003E MusicBoard ??= Scene?.GetAllComponents\u003CSkafinityMusicPanel\u003E().FirstOrDefault();\n\n\t// \u0022Open\u0022 = the panel\u0027s board is showing. MusicOpen feeds BuildHash, so the panel\u0027s own\n\t// open/close (which flips IsOpen) re-renders the HUD and shows/hides the game beneath it.\n\tbool MusicOpen =\u003E Board?.IsOpen ?? false;\n\n\tstring PhaseText() =\u003E C?.Phase switch\n\t{\n\t\tGamePhase.Armed =\u003E \u0022CLICK!\u0022,\n\t\tGamePhase.Pending =\u003E \u0022WAIT\u2026\u0022,\n\t\tGamePhase.Waiting =\u003E \u0022STAND BY\u2026\u0022,\n\t\tGamePhase.Result =\u003E \u0022ROUND OVER\u0022,\n\t\tGamePhase.GameOver =\u003E \u0022GAME OVER\u0022,\n\t\tGamePhase.Disconnected =\u003E \u0022RECONNECTING\u2026\u0022,\n\t\tGamePhase.Connecting =\u003E \u0022CONNECTING\u2026\u0022,\n\t\t_ =\u003E \u0022\u0022,\n\t};\n\n\tstring PhaseClass() =\u003E C?.Phase switch\n\t{\n\t\tGamePhase.Armed =\u003E \u0022armed\u0022,\n\t\tGamePhase.Disconnected =\u003E \u0022offline\u0022,\n\t\t_ =\u003E \u0022\u0022,\n\t};\n\n\t// Include the seconds-remaining so the countdown re-renders as it ticks, and\n\t// ToastVisible so the copied-link toast appears and clears itself on time.\n\tprotected override int BuildHash() =\u003E HashCode.Combine(\n\t\tHashCode.Combine( C?.Phase ?? GamePhase.Connecting, C?.Round ?? 0, C?.Of ?? 0,\n\t\t\tC?.Players ?? 0, C?.ClicksToWin ?? 0, C?.ClicksSent ?? 0 ),\n\t\tHashCode.Combine( SessionEntries.Count, _hourly.Count, _hourlyLoaded, _hoursWon.Count, _hoursWonLoaded,\n\t\t\tC?.ArmMaxSec ?? 0, C?.Tag, (int)(DateTime.UtcNow - DateTime.UtcNow.Date).TotalSeconds ),\n\t\tHashCode.Combine( _sessionsWon.Count, _sessionsWonLoaded, ToastVisible, SkinLeaderName(), MusicOpen,\n\t\t\t_skinUrl, _winnerLockMs, HashCode.Combine( WinnerLockPassed(), _hasBounty ) ),\n\t\tHashCode.Combine( _allTime.Count, _allTimeLoaded,\n\t\t\tC?.HasTest ?? false, C?.TestPrompt ),\n\t\t_howtoOpen,\n\t\t// sanction overlay state (the per-second countdown tick is already covered by\n\t\t// the seconds term in the second combine above); plus the parked/away state \u2014 and\n\t\t// the deferred park-pending / resuming flags \u2014 so the PAUSE bar \u002B parked overlay\n\t\t// repaint the instant any of them flips.\n\t\tHashCode.Combine( C?.TestMessage, C?.SanctionState, C?.SanctionUntilMs,\n\t\t\tC?.Parked ?? false, C?.ParkPending ?? false, C?.Resuming ?? false ),\n\t\t// round-scores flash: FlashVisible flips the panel off on time; round \u002B count\n\t\t// rebuild it when a new round\u0027s scores come up.\n\t\tHashCode.Combine( FlashVisible, _flashRound, _flashWinners.Count ),\n\t\t// live-window tick: the descending counter (changes ~tick-rate while armed) and\n\t\t// the on-screen pip count (a pip spawning/expiring changes it) drive re-render;\n\t\t// plus the previous-winner panel state (which bounty, and whether its skin has\n\t\t// resolved) so it repaints when a rollover/refresh loads a new one.\n\t\tHashCode.Combine( C?.RemainingThisRound ?? 0, PipsToShow().Count,\n\t\t\t_prevBounty?.Id ?? 0L, _prevSkin != null, _prevSkinUrl, BoardHash() ) );\n}\n"},{"Ident":"gamah.splitclicker","Path":"Api/Models.cs","FileName":"Models.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace Splitclicker.Api;\n\n// POST /api/v1/auth response: the public tag/username plus a single-use WS ticket\n// (mint immediately before connecting; expires after ttl_ms).\n// Username is the *claimed* handle only (empty when the player never set one) \u2014\n// persist/re-send only this, never DisplayName, or the Steam name gets bounced\n// back as a username and 422s on reconnect. DisplayName is the resolved string\n// to show (claimed handle, else Steam name, else hex tag).\npublic record AuthResponse(\n\t[property: JsonPropertyName( \u0022tag\u0022 )] string Tag,\n\t[property: JsonPropertyName( \u0022username\u0022 )] string Username,\n\t[property: JsonPropertyName( \u0022display_name\u0022 )] string DisplayName,\n\t[property: JsonPropertyName( \u0022ticket\u0022 )] string Ticket,\n\t[property: JsonPropertyName( \u0022ttl_ms\u0022 )] long TtlMs\n);\n\n// GET /api/v1/config response: server-driven bits the client needs at startup.\n// WinnerLockMs is the countdown target as Unix epoch milliseconds (0 = unset) \u2014\n// a plain number so no date parsing is needed (the s\u0026box sandbox doesn\u0027t\n// whitelist System.Globalization). SkinUrl is the server path the current\n// \u0022skin to win\u0022 image is served from (made absolute against BaseUrl).\n// InspectLink is the active bounty\u0027s CS2 inspect link, or \u0022\u0022 when the skin is an\n// uploaded image only. When set, the client decodes it locally and renders the\n// live float / seed / name / wear bar, falling back to SkinUrl\u0027s image on failure.\n// HasBounty is false when no bounty is active \u2014 the WinnerLockMs/SkinUrl are then the\n// host config.json/env fallback (a stale \u0022old\u0022 skin) and the bounty-scoped boards have\n// fallen back to all-time, so the client shows a \u0022no active bounty\u0022 state instead.\npublic record ConfigResponse(\n\t[property: JsonPropertyName( \u0022winner_lock_ms\u0022 )] long WinnerLockMs,\n\t[property: JsonPropertyName( \u0022skin_url\u0022 )] string SkinUrl,\n\t[property: JsonPropertyName( \u0022inspect_link\u0022 )] string InspectLink,\n\t[property: JsonPropertyName( \u0022has_bounty\u0022 )] bool HasBounty\n);\n\n// GET /api/v1/bounties/previous response row: a settled bounty \u002B its winner, for\n// the \u0022previous winner\u0022 panel. SkinUrl is the per-bounty image fallback (made\n// absolute against BaseUrl); InspectLink, when set, is decoded locally for the\n// live skin render exactly like the current bounty. WinnerTag matches the client\u0027s\n// own tag when the local player won (so the panel can say \u0022you\u0022); WinnerSteamId\n// opens the profile; WonAtMs is epoch ms (no date parsing in the sandbox).\npublic record PreviousBounty(\n\t[property: JsonPropertyName( \u0022id\u0022 )] long Id,\n\t[property: JsonPropertyName( \u0022label\u0022 )] string Label,\n\t[property: JsonPropertyName( \u0022skin_url\u0022 )] string SkinUrl,\n\t[property: JsonPropertyName( \u0022inspect_link\u0022 )] string InspectLink,\n\t[property: JsonPropertyName( \u0022winner_tag\u0022 )] string WinnerTag,\n\t[property: JsonPropertyName( \u0022winner_steam_id\u0022 )] string WinnerSteamId,\n\t[property: JsonPropertyName( \u0022winner_name\u0022 )] string WinnerName,\n\t[property: JsonPropertyName( \u0022winner_wins\u0022 )] int WinnerWins,\n\t[property: JsonPropertyName( \u0022won_at_ms\u0022 )] long WonAtMs\n);\n\n// One row of a leaderboard (GET /api/v1/leaderboard/*) and of the standings\n// embedded in round_result / game_over. SteamId is the public SteamID64, used to\n// open/copy the player\u0027s steamcommunity.com profile.\npublic record Standing(\n\t[property: JsonPropertyName( \u0022tag\u0022 )] string Tag,\n\t[property: JsonPropertyName( \u0022username\u0022 )] string Username,\n\t[property: JsonPropertyName( \u0022points\u0022 )] int Points,\n\t[property: JsonPropertyName( \u0022steam_id\u0022 )] string SteamId,\n\t// Anticheat status for the active bounty: \u0022live\u0022 / \u0022cooldown\u0022 / \u0022ignored\u0022.\n\t// Drives the coloured status dot on every board row. Empty \u21D2 treated as live.\n\t[property: JsonPropertyName( \u0022status\u0022 )] string Status,\n\t// BehindMs is how many ms this player\u0027s tie-deciding click landed after the\n\t// player ranked just above them with the SAME points \u2014 \u0022how much they lost the\n\t// game by\u0022. Only the final game_over standings carry it (the end-of-game tie is\n\t// the one that matters); 0/absent \u21D2 top of a tie group or a unique score.\n\t[property: JsonPropertyName( \u0022behind_ms\u0022 )] int BehindMs\n);\n\n// --- WebSocket server\u2192client frames (the \u0022t\u0022 field selects the shape) ---\n\npublic record HelloYou(\n\t[property: JsonPropertyName( \u0022tag\u0022 )] string Tag,\n\t[property: JsonPropertyName( \u0022username\u0022 )] string Username\n);\n\npublic record HelloGame(\n\t[property: JsonPropertyName( \u0022round\u0022 )] int Round,\n\t[property: JsonPropertyName( \u0022of\u0022 )] int Of,\n\t[property: JsonPropertyName( \u0022phase\u0022 )] string Phase,\n\t[property: JsonPropertyName( \u0022players\u0022 )] int Players,\n\t[property: JsonPropertyName( \u0022clicks\u0022 )] int Clicks,\n\t[property: JsonPropertyName( \u0022arm_min\u0022 )] int ArmMin,\n\t[property: JsonPropertyName( \u0022arm_max\u0022 )] int ArmMax,\n\t[property: JsonPropertyName( \u0022dev_note\u0022 )] string DevNote,\n\t// Live-window tick interval in ms (0 = ticking off). Sizes the pip jitter-buffer\n\t// playback delay so opponent pips replay at their true relative moment.\n\t[property: JsonPropertyName( \u0022tick_ms\u0022 )] int TickMs\n);\n\npublic record HelloMsg(\n\t[property: JsonPropertyName( \u0022you\u0022 )] HelloYou You,\n\t[property: JsonPropertyName( \u0022game\u0022 )] HelloGame Game\n);\n\n// roster is the full {tag \u2192 username} map of connected players (v5\u002B only), sent at\n// the arming stage so opponent pips \u2014 which carry only a 4-byte tag \u2014 can be\n// labelled with a name. Absent on older servers/clients (null).\npublic record RosterEntry(\n\t[property: JsonPropertyName( \u0022tag\u0022 )] string Tag,\n\t[property: JsonPropertyName( \u0022username\u0022 )] string Username\n);\n\npublic record PendingMsg(\n\t[property: JsonPropertyName( \u0022round\u0022 )] int Round,\n\t[property: JsonPropertyName( \u0022of\u0022 )] int Of,\n\t[property: JsonPropertyName( \u0022players\u0022 )] int Players,\n\t[property: JsonPropertyName( \u0022clicks\u0022 )] int Clicks,\n\t[property: JsonPropertyName( \u0022roster\u0022 )] List\u003CRosterEntry\u003E Roster\n);\n\n// One live button in the v5 armed board: id is the compact slot handle (referenced\n// by tick claim events), nonce the secret a scoring click on it must echo, x/y its\n// server-placed normalized position (int16, 0 = centre). Same shape the tick frame\u0027s\n// spawned replacements carry.\npublic record ButtonMsg(\n\t[property: JsonPropertyName( \u0022id\u0022 )] int Id,\n\t[property: JsonPropertyName( \u0022nonce\u0022 )] string Nonce,\n\t[property: JsonPropertyName( \u0022x\u0022 )] int X,\n\t[property: JsonPropertyName( \u0022y\u0022 )] int Y\n);\n\n// buttons is the initial multi-button board; each button carries a hex nonce (an\n// unguessable 64-bit token) echoed back verbatim in the click frame \u2014 never\n// parse/reformat it. (The old per-connection arm-delay penalty_ms was gutted in v7.)\npublic record ArmedMsg(\n\t[property: JsonPropertyName( \u0022round\u0022 )] int Round,\n\t[property: JsonPropertyName( \u0022seq\u0022 )] int Seq,\n\t[property: JsonPropertyName( \u0022buttons\u0022 )] List\u003CButtonMsg\u003E Buttons,\n\t[property: JsonPropertyName( \u0022players\u0022 )] int Players,\n\t[property: JsonPropertyName( \u0022clicks\u0022 )] int Clicks\n);\n\npublic record YouResult(\n\t[property: JsonPropertyName( \u0022points_delta\u0022 )] int PointsDelta,\n\t[property: JsonPropertyName( \u0022round_id\u0022 )] string RoundId\n);\n\npublic record RoundResultMsg(\n\t[property: JsonPropertyName( \u0022round\u0022 )] int Round,\n\t[property: JsonPropertyName( \u0022of\u0022 )] int Of,\n\t[property: JsonPropertyName( \u0022winners\u0022 )] List\u003CStanding\u003E Winners,\n\t[property: JsonPropertyName( \u0022standings\u0022 )] List\u003CStanding\u003E Standings,\n\t[property: JsonPropertyName( \u0022you\u0022 )] YouResult You\n);\n\n// points_delta/round_id carry the FINAL round\u0027s score (the last round folds into\n// game_over with no round_result of its own) so the client can drive its \u0060points\u0060\n// stat once, guarded by round_id like a normal round.\npublic record YouGameOver(\n\t[property: JsonPropertyName( \u0022placement\u0022 )] int Placement,\n\t[property: JsonPropertyName( \u0022won\u0022 )] bool Won,\n\t[property: JsonPropertyName( \u0022game_id\u0022 )] string GameId,\n\t[property: JsonPropertyName( \u0022points_delta\u0022 )] int PointsDelta,\n\t[property: JsonPropertyName( \u0022round_id\u0022 )] string RoundId\n);\n\npublic record GameOverMsg(\n\t[property: JsonPropertyName( \u0022standings\u0022 )] List\u003CStanding\u003E Standings,\n\t[property: JsonPropertyName( \u0022you\u0022 )] YouGameOver You\n);\n\n// note is the host-editable broadcast message (orange line under the throttle);\n// empty clears it. Pushed once per game and carried in hello for mid-game joiners.\npublic record DevNoteMsg(\n\t[property: JsonPropertyName( \u0022note\u0022 )] string Note\n);\n\n// An anticheat frame pushed to this client after it failed end-of-round checks.\n// state is the ladder rung:\n//   \u0022test\u0022     \u2014 answer prompt (echoing id) before the server will arm us again.\n//   \u0022cooldown\u0022 \u2014 sidelined until until_ms (a timed cooldown); no test to answer.\n//   \u0022ignored\u0022  \u2014 sidelined until until_ms (the bounty\u0027s resolve time); no test.\n// message is the player-facing explanation for every state; until_ms is the epoch\n// ms the cooldown/ignored state ends (the client shows a countdown to it).\n// cleared=true means we\u0027re back in play \u2014 dismiss any overlay. (an empty state is\n// treated as \u0022test\u0022.)\npublic record TestMsg(\n\t[property: JsonPropertyName( \u0022state\u0022 )] string State,\n\t[property: JsonPropertyName( \u0022id\u0022 )] string Id,\n\t[property: JsonPropertyName( \u0022kind\u0022 )] string Kind,\n\t[property: JsonPropertyName( \u0022prompt\u0022 )] string Prompt,\n\t[property: JsonPropertyName( \u0022message\u0022 )] string Message,\n\t[property: JsonPropertyName( \u0022until_ms\u0022 )] long UntilMs,\n\t[property: JsonPropertyName( \u0022cleared\u0022 )] bool Cleared\n);\n\n// ident is a server-pushed manual achievement unlock for an out-of-band feat\n// (e.g. poking the backend into a 404, fumbling the admin password); it must\n// match an achievement defined in the s\u0026box project.\npublic record AchievementMsg(\n\t[property: JsonPropertyName( \u0022ident\u0022 )] string Ident\n);\n\n// on=true parks us (auto-park off an afk_idle verdict); the server only ever sends\n// on:true (unpark is the client\u0027s call). Drives the parked/away state.\npublic record ParkMsg(\n\t[property: JsonPropertyName( \u0022on\u0022 )] bool On\n);\n"},{"Ident":"gamah.splitclicker","Path":"SkafinityPlayer.cs","FileName":"SkafinityPlayer.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"using System;\nusing System.Threading.Tasks;\nusing Sandbox;\n\nnamespace Skafinity;\n\n/// \u003Csummary\u003E\n/// Streams an endless, deterministic procedural ska / reggae-rock track (see\n/// \u003Csee cref=\u0022MusicGen\u0022/\u003E) through Web Audio-style scheduling over a \u003Csee cref=\u0022SoundStream\u0022/\u003E.\n///\n/// Drop this \u003Csee cref=\u0022Component\u0022/\u003E on any GameObject. It generates a ~80s loop from the\n/// seed \u003Cc\u003Etag:n\u003C/c\u003E, plays it \u003Csee cref=\u0022LoopsPerSong\u0022/\u003E times, then equal-power crossfades\n/// into the pre-generated next song (\u003Cc\u003Etag:n\u002B1\u003C/c\u003E), forever. Every generator knob is an\n/// inspector \u003Cc\u003E[Property]\u003C/c\u003E; with \u003Csee cref=\u0022LiveReload\u0022/\u003E on, tweaking one regenerates\n/// after a short settle so you can dial in a vibe in play mode.\n///\n/// A whole song is just its seed, so the arrangement is shareable: copy \u003Csee cref=\u0022CurrentSeed\u0022/\u003E\n/// (\u003Cc\u003Evibe:tag:n\u003C/c\u003E) and anyone who calls \u003Csee cref=\u0022PlaySeed\u0022/\u003E with it hears the same track.\n///\n/// This is a self-contained extraction of the Rotaliate music engine with no game-specific\n/// dependencies (no player data, networking, or UI). Persistence of the song index is opt-in\n/// via \u003Csee cref=\u0022PersistProgress\u0022/\u003E.\n/// \u003C/summary\u003E\npublic sealed class SkafinityPlayer : Component\n{\n\t// \u2500\u2500 Master \u2500\u2500\n\t/// \u003Csummary\u003EMusic master switch. \u0027new\u0027 so it\u0027s distinct from \u003Csee cref=\u0022Component.Enabled\u0022/\u003E.\u003C/summary\u003E\n\t[Property, Group( \u0022Music\u0022 )] public new bool Enabled { get; set; } = true;\n\t[Property, Group( \u0022Music\u0022 ), Range( 0f, 2f )] public float Volume { get; set; } = 0.7f;\n\t/// \u003Csummary\u003ERegenerate automatically a moment after any generator knob changes (editor tuning).\u003C/summary\u003E\n\t[Property, Group( \u0022Music\u0022 )] public bool LiveReload { get; set; } = true;\n\t/// \u003Csummary\u003EOptional mixer name to route the music to (e.g. \u0022Music\u0022). Empty = default mixer.\u003C/summary\u003E\n\t[Property, Group( \u0022Music\u0022 )] public string MixerName { get; set; } = \u0022\u0022;\n\t/// \u003Csummary\u003EBegin playing automatically in \u003Csee cref=\u0022OnStart\u0022/\u003E. Off = call \u003Csee cref=\u0022StartSequence\u0022/\u003E yourself.\u003C/summary\u003E\n\t[Property, Group( \u0022Music\u0022 )] public bool AutoPlay { get; set; } = true;\n\t/// \u003Csummary\u003EShuffle mode: re-randomise every knob (incl. genre \u002B volumes) as each new song\n\t/// begins, so the sequence keeps reinventing itself. Off = the seed\u0027s vibe stays put.\u003C/summary\u003E\n\t[Property, Group( \u0022Music\u0022 )] public bool RandomEverySong { get; set; } = false;\n\n\t// \u2500\u2500 Seed \u2500\u2500\n\t/// \u003Csummary\u003ESeed tag \u2014 any string (a name, a word). Empty falls back to \u0022skafinity\u0022.\u003C/summary\u003E\n\t[Property, Group( \u0022Seed\u0022 )] public string Tag { get; set; } = \u0022\u0022;\n\t/// \u003Csummary\u003ESong index in the infinite sequence (0,1,2\u2026). \u003Csee cref=\u0022StepN\u0022/\u003E/\u003Csee cref=\u0022NextSong\u0022/\u003E walk it.\u003C/summary\u003E\n\t[Property, Group( \u0022Seed\u0022 )] public int StartN { get; set; } = 0;\n\t/// \u003Csummary\u003EOptional base-36 vibe override (see \u003Csee cref=\u0022VibeCodec\u0022/\u003E). When set it overrides\n\t/// the matching inspector knobs, so a shared vibe reproduces the same voicing on any client.\u003C/summary\u003E\n\t[Property, Group( \u0022Seed\u0022 )] public string Vibe { get; set; } = \u0022\u0022;\n\t/// \u003Csummary\u003EPersist the current song index across sessions (FileSystem.Data, keyed by \u003Csee cref=\u0022SaveSlot\u0022/\u003E).\u003C/summary\u003E\n\t[Property, Group( \u0022Seed\u0022 )] public bool PersistProgress { get; set; } = false;\n\t[Property, Group( \u0022Seed\u0022 )] public string SaveSlot { get; set; } = \u0022default\u0022;\n\n\t// \u2500\u2500 Output \u2500\u2500\n\t[Property, Group( \u0022Output\u0022 ), Range( 8000, 48000 )] public int SampleRate { get; set; } = 32000;\n\t/// \u003Csummary\u003ETarget track length; bar count adapts to tempo to hit this.\u003C/summary\u003E\n\t[Property, Group( \u0022Output\u0022 ), Range( 30f, 180f )] public float TargetSeconds { get; set; } = 80f;\n\t/// \u003Csummary\u003EWorker threads the pitched-voice synthesis is split across (composition \u002B drums\n\t/// stay single-threaded). Keeps each worker burst under s\u0026amp;box\u0027s ~1000ms no-yield advisory.\u003C/summary\u003E\n\t[Property, Group( \u0022Output\u0022 ), Range( 1, 8 )] public int RenderThreads { get; set; } = 6;\n\n\t// \u2500\u2500 Crossfade / scheduling \u2500\u2500\n\t/// \u003Csummary\u003ECrossfade window (also the first song\u0027s fade-in from silence), seconds. The two\n\t/// songs only both-audible for \u003Csee cref=\u0022CrossfadeOverlap\u0022/\u003E of this, centred.\u003C/summary\u003E\n\t[Property, Group( \u0022Crossfade\u0022 ), Range( 0.5f, 8f )] public float Crossfade { get; set; } = 3.75f;\n\t[Property, Group( \u0022Crossfade\u0022 ), Range( 0f, 1f )] public float CrossfadeOverlap { get; set; } = 0.5f;\n\t/// \u003Csummary\u003EHow many times each loop plays before crossfading on (2 = play through, loop once, switch).\u003C/summary\u003E\n\t[Property, Group( \u0022Crossfade\u0022 ), Range( 1, 4 )] public int LoopsPerSong { get; set; } = 2;\n\t/// \u003Csummary\u003EHow many upcoming songs to keep pre-generated (built one-per-tick so the fill never stalls a frame).\u003C/summary\u003E\n\t[Property, Group( \u0022Crossfade\u0022 ), Range( 1, 8 )] public int AheadCount { get; set; } = 5;\n\n\t// \u2500\u2500 Tempo (main = laid-back reggae-rock; Fast = uptempo ska) \u2500\u2500\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 60, 200 )] public int BpmMin { get; set; } = 130;\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 60, 200 )] public int BpmMax { get; set; } = 185;\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 0f, 1f )] public float FastChance { get; set; } = 0.30f;\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 100, 220 )] public int FastBpmMin { get; set; } = 150;\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 100, 220 )] public int FastBpmMax { get; set; } = 168;\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 0f, 0.4f )] public float Swing { get; set; } = 0.14f;\n\t[Property, Group( \u0022Tempo\u0022 ), Range( 0f, 0.4f )] public float FastSwing { get; set; } = 0.05f;\n\n\t// \u2500\u2500 Mix \u2500\u2500\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float BassVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float SkankVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float OrganVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float MelodyVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float HornVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float KickVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float SnareVol { get; set; } = 0.70f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float TomVol { get; set; } = 0.60f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float HatVol { get; set; } = 0.22f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float CrashVol { get; set; } = 0.35f;\n\t[Property, Group( \u0022Mix\u0022 ), Range( 0f, 1.5f )] public float DrumVol { get; set; } = 1.00f;\n\n\t// \u2500\u2500 Tone \u2500\u2500\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0f, 40f )] public float Detune { get; set; } = 14f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 80f, 1200f )] public float BassCutoff { get; set; } = 380f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 500f, 8000f )] public float SkankCutoff { get; set; } = 3000f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0f, 2000f )] public float SkankHighpass { get; set; } = 500f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0.15f, 1f )] public float SkankChop { get; set; } = 0.5f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 500f, 8000f )] public float LeadCutoff { get; set; } = 3200f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 500f, 8000f )] public float OrganCutoff { get; set; } = 1400f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0f, 12f )] public float OrganVibrato { get; set; } = 5.5f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 500f, 8000f )] public float HornCutoff { get; set; } = 3200f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0.2f, 2f )] public float Resonance { get; set; } = 1.0f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 1f, 4f )] public float BassDrive { get; set; } = 1.5f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 1f, 4f )] public float SkankDrive { get; set; } = 1.3f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 1f, 4f )] public float MelodyDrive { get; set; } = 1.3f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 1f, 4f )] public float HornDrive { get; set; } = 1.4f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0.5f, 3f )] public float MasterDrive { get; set; } = 1.1f;\n\t[Property, Group( \u0022Tone\u0022 ), Range( 0.2f, 1f )] public float MasterPeak { get; set; } = 0.95f;\n\n\t// \u2500\u2500 Feel \u2500\u2500\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float OctavePopChance { get; set; } = 0.30f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float OrganBubbleChance { get; set; } = 0.55f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float KickSyncChance { get; set; } = 0.25f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float GhostSnareChance { get; set; } = 0.35f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float FillChance { get; set; } = 0.6f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float DrumBusy { get; set; } = 0.6f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float DrumTone { get; set; } = 0.5f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float DrumDrive { get; set; } = 0.5f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 0.2f )] public float TripletChance { get; set; } = 0.06f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 0.1f )] public float BassTriplets { get; set; } = 0.06f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float MelodyRestChance { get; set; } = 0.30f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 1f )] public float MelodyLeapChance { get; set; } = 0.18f;\n\t[Property, Group( \u0022Feel\u0022 ), Range( 0f, 12f )] public float MelodyVibrato { get; set; } = 5.0f;\n\n\t// \u2500\u2500 Stereo \u2500\u2500\n\t[Property, Group( \u0022Stereo\u0022 ), Range( 0f, 1f )] public float PanAmount { get; set; } = 0.4f;\n\n\t// \u2500\u2500 Lead instrument (RNG picks one per tag, weighted; Force overrides) \u2500\u2500\n\t[Property, Group( \u0022Instrument\u0022 ), Range( 0f, 4f )] public float TrumpetWeight { get; set; } = 1.0f;\n\t[Property, Group( \u0022Instrument\u0022 ), Range( 0f, 4f )] public float SaxWeight { get; set; } = 1.0f;\n\t[Property, Group( \u0022Instrument\u0022 ), Range( 0f, 4f )] public float OrganWeight { get; set; } = 0.8f;\n\t[Property, Group( \u0022Instrument\u0022 ), Range( 0f, 4f )] public float TromboneWeight { get; set; } = 0.4f;\n\t/// \u003Csummary\u003E-1 = RNG; 0=Trumpet 1=Sax 2=Organ 3=Trombone.\u003C/summary\u003E\n\t[Property, Group( \u0022Instrument\u0022 ), Range( -1, 3 )] public int ForceInstrument { get; set; } = -1;\n\n\t// \u2500\u2500 Backing horns \u2500\u2500\n\t[Property, Group( \u0022Horns\u0022 ), Range( 0f, 1f )] public float HornSectionChance { get; set; } = 0.5f;\n\t[Property, Group( \u0022Horns\u0022 ), Range( 0f, 1f )] public float HornDensity { get; set; } = 0.35f;\n\n\t// \u2500\u2500 Genre \u0026 rock instruments \u2500\u2500\n\t// Genre selects the instrument set: 0 = Ska, 1 = Rock (drums/bass/rhythm-gtr/lead-gtr).\n\t[Property, Group( \u0022Genre\u0022 ), Range( 0, 1 )] public int Genre { get; set; } = 0;\n\t// KEYS \u2014 the offbeat-chord comp (was the \u0022rhythm guitar\u0022; it reads as keys).\n\t[Property, Group( \u0022Rock\u0022 ), Range( 0f, 1.5f )] public float KeysVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 500f, 8000f )] public float KeysCutoff { get; set; } = 1700f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 1f, 5f )] public float KeysDrive { get; set; } = 3.2f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 0f, 1f )] public float KeysChug { get; set; } = 0.5f;\n\t// RHYTHM GTR \u2014 twangy distorted power chords (shares the lead voice, lower base distortion).\n\t[Property, Group( \u0022Rock\u0022 ), Range( 0f, 1.5f )] public float RhythmGtrVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 500f, 8000f )] public float RhythmGtrCutoff { get; set; } = 2600f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 1f, 5f )] public float RhythmGtrDrive { get; set; } = 2.8f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 0f, 1f )] public float RhythmGtrChug { get; set; } = 0.5f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 0f, 1.5f )] public float LeadGtrVol { get; set; } = 1.00f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 500f, 8000f )] public float LeadGtrCutoff { get; set; } = 2600f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 1f, 5f )] public float LeadGtrDrive { get; set; } = 3.6f;\n\t[Property, Group( \u0022Rock\u0022 ), Range( 0f, 1f )] public float LeadGtrBend { get; set; } = 0.30f;\n\n\tSoundStream _stream;\n\tSoundHandle _handle;\n\tint _sr;\n\n\tshort[] _curRaw;            // current song, full single loop (raw, for export)\n\treadonly System.Collections.Generic.List\u003Cshort[]\u003E _ahead = new(); // pre-generated n\u002B1, n\u002B2, \u2026\n\tint _curN;                 // index of the currently-playing song\n\t// Per-instrument volumes, keyed by voice NAME (BASS, DRUMS, \u2026) so the level follows the\n\t// instrument across genres. Pulled out of the vibe seed; persisted to FileSystem.Data and\n\t// overlaid onto every BuildConfig. See VibeCodec.ReadVolumes/ApplyVolumes.\n\tSystem.Collections.Generic.Dictionary\u003Cstring, float\u003E _vols = new();\n\t// Shared house-mix config (peak balances / kit presence) read from the addon\u0027s\n\t// skafinity.config.json \u2014 the SAME file the web toy uses. Overlaid onto every BuildConfig.\n\tSystem.Collections.Generic.Dictionary\u003Cstring, float\u003E _houseConfig = new();\n\tint _curReserve;           // samples of the current song\u0027s tail held back for the crossfade\n\tdouble _pushedSeconds;     // total audio pushed to the stream\n\tTimeSince _sinceStart;     // wall clock since playback started\n\tint _lastConfigHash;\n\tbool _dirty;\n\tTimeSince _dirtySince;\n\tbool _starting;            // StartSequenceAsync is in flight\n\tbool _fillingAhead;        // FillAhead is in flight\n\tbool Generating =\u003E _starting || _fillingAhead;\n\tint _seq;                  // bumped on each StartSequence; stale async results are discarded\n\tbool _flatConfigured;      // ConfigureFlat applied to the live handle\n\tbool _restartPending;      // a debounced restart (vibe edit) is queued\n\tTimeSince _restartPendingSince;\n\n\t/// \u003Csummary\u003ECurrently-playing song index.\u003C/summary\u003E\n\tpublic int N =\u003E _curN;\n\t/// \u003Csummary\u003EThe effective vibe \u2014 the override if set, else the encoded inspector knobs.\u003C/summary\u003E\n\tpublic string CurrentVibe =\u003E VibeCodec.Encode( BuildConfig() );\n\t/// \u003Csummary\u003EShareable seed for the playing song: \u003Cc\u003Evibe:tag:n\u003C/c\u003E.\u003C/summary\u003E\n\tpublic string CurrentSeed =\u003E $\u0022{CurrentVibe}:{SeedTag}:{_curN}\u0022;\n\t/// \u003Csummary\u003ETrue once a stream handle is live and audible.\u003C/summary\u003E\n\tpublic bool IsPlaying =\u003E _handle != null;\n\n\tstring SeedTag =\u003E string.IsNullOrEmpty( Tag ) ? \u0022\u0022 : Tag;\n\t// Build the PRNG seed string from a resolved tag, so worker code never re-reads state.\n\tstatic string SeedFor( string tag, int n ) =\u003E $\u0022{(string.IsNullOrEmpty( tag ) ? \u0022skafinity\u0022 : tag.ToLowerInvariant())}:{n}\u0022;\n\tstring Seed( int n ) =\u003E SeedFor( SeedTag, n );\n\n\tprotected override void OnStart()\n\t{\n\t\t_lastConfigHash = ConfigHash();\n\t\t_curN = Math.Max( 0, PersistProgress ? LoadN() ?? StartN : StartN );\n\t\t_vols = LoadVols();\n\t\t_houseConfig = LoadHouseConfig();\n\t\tif ( AutoPlay ) StartSequence();\n\t}\n\n\tprotected override void OnDestroy()\n\t{\n\t\t_seq\u002B\u002B;            // invalidate any in-flight worker generation\n\t\t_handle?.Stop();\n\t\t_handle = null;\n\t\t_stream = null;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( _handle != null )\n\t\t\t_handle.Volume = TargetVolume();\n\n\t\t// Keep the look-ahead buffer topped up. Generation runs on a worker thread so this\n\t\t// never blocks the frame.\n\t\tif ( !Generating \u0026\u0026 _curRaw != null \u0026\u0026 _ahead.Count \u003C Math.Max( 1, AheadCount ) )\n\t\t\t_ = FillAhead( _seq );\n\n\t\t// When the queued audio is about to run out, crossfade into the next song.\n\t\tif ( _stream != null \u0026\u0026 _ahead.Count \u003E 0 \u0026\u0026 _curRaw != null\n\t\t\t\u0026\u0026 _pushedSeconds - _sinceStart \u003C 2.0 )\n\t\t\tPushTransition();\n\n\t\tint h = ConfigHash();\n\t\tif ( h != _lastConfigHash )\n\t\t{\n\t\t\t_lastConfigHash = h;\n\t\t\t_dirty = true;\n\t\t\t_dirtySince = 0;\n\t\t}\n\t\tif ( _dirty \u0026\u0026 LiveReload \u0026\u0026 !Generating \u0026\u0026 _dirtySince \u003E 0.5f )\n\t\t{\n\t\t\t_dirty = false;\n\t\t\tStartSequence();\n\t\t}\n\n\t\t// Debounced restart for vibe edits: only regenerate once edits have settled.\n\t\tif ( _restartPending \u0026\u0026 !Generating \u0026\u0026 _restartPendingSince \u003E 0.35f )\n\t\t{\n\t\t\t_restartPending = false;\n\t\t\tStartSequence();\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EMake the stream play as flat 2D (SpacialBlend=0, parented to the camera with\n\t/// FollowParent so the listener can\u0027t pan/attenuate it). Optionally routes to a named mixer.\u003C/summary\u003E\n\tvoid ConfigureFlat()\n\t{\n\t\tif ( _handle == null || _flatConfigured ) return;\n\t\t_handle.SpacialBlend = 0f;\n\t\tvar camGo = Scene?.Camera?.GameObject;\n\t\tif ( camGo.IsValid() )\n\t\t{\n\t\t\t_handle.Parent = camGo;\n\t\t\t_handle.FollowParent = true;\n\t\t}\n\t\tif ( !string.IsNullOrEmpty( MixerName ) )\n\t\t{\n\t\t\tvar mixer = Sandbox.Audio.Mixer.FindMixerByName( MixerName );\n\t\t\tif ( mixer != null )\n\t\t\t\t_handle.TargetMixer = mixer;\n\t\t}\n\t\t_flatConfigured = true;\n\t}\n\n\tfloat TargetVolume() =\u003E Enabled ? Volume : 0f;\n\n\t/// \u003Csummary\u003EThe config currently in effect (inspector knobs with any \u003Csee cref=\u0022Vibe\u0022/\u003E applied).\u003C/summary\u003E\n\tpublic MusicGen.Config EffectiveConfig() =\u003E BuildConfig();\n\n\tMusicGen.Config BuildConfig()\n\t{\n\t\tvar cfg = BuildKnobConfig();\n\t\t// Shared house-mix baseline (peak balances / kit presence) from skafinity.config.json \u2014\n\t\t// the same file the web toy reads. Independent of the vibe/volume knobs below.\n\t\tVibeCodec.ApplyAdvanced( _houseConfig, cfg );\n\t\t// A vibe override sets the important knobs (so a shared vibe:tag:n reproduces the same\n\t\t// voicing regardless of this client\u0027s inspector knobs).\n\t\tif ( !string.IsNullOrEmpty( Vibe ) )\n\t\t\tVibeCodec.Apply( Vibe, cfg );\n\t\t// Per-instrument volumes are NOT in the seed \u2014 overlay the persisted per-voice mix on top.\n\t\tVibeCodec.ApplyVolumes( cfg.Genre, _vols, cfg );\n\t\treturn cfg;\n\t}\n\n\tMusicGen.Config BuildKnobConfig() =\u003E new()\n\t{\n\t\tSampleRate = SampleRate,\n\t\tTargetSeconds = TargetSeconds,\n\t\tBpmMin = BpmMin,\n\t\tBpmMax = BpmMax,\n\t\tFastChance = FastChance,\n\t\tFastBpmMin = FastBpmMin,\n\t\tFastBpmMax = FastBpmMax,\n\t\tSwing = Swing,\n\t\tFastSwing = FastSwing,\n\t\tBassVol = BassVol,\n\t\tSkankVol = SkankVol,\n\t\tOrganVol = OrganVol,\n\t\tMelodyVol = MelodyVol,\n\t\tHornVol = HornVol,\n\t\tKickVol = KickVol,\n\t\tSnareVol = SnareVol,\n\t\tTomVol = TomVol,\n\t\tHatVol = HatVol,\n\t\tCrashVol = CrashVol,\n\t\tDrumVol = DrumVol,\n\t\tDetune = Detune,\n\t\tBassCutoff = BassCutoff,\n\t\tSkankCutoff = SkankCutoff,\n\t\tSkankHighpass = SkankHighpass,\n\t\tSkankChop = SkankChop,\n\t\tLeadCutoff = LeadCutoff,\n\t\tOrganCutoff = OrganCutoff,\n\t\tOrganVibrato = OrganVibrato,\n\t\tHornCutoff = HornCutoff,\n\t\tResonance = Resonance,\n\t\tBassDrive = BassDrive,\n\t\tSkankDrive = SkankDrive,\n\t\tMelodyDrive = MelodyDrive,\n\t\tHornDrive = HornDrive,\n\t\tMasterDrive = MasterDrive,\n\t\tMasterPeak = MasterPeak,\n\t\tOctavePopChance = OctavePopChance,\n\t\tOrganBubbleChance = OrganBubbleChance,\n\t\tKickSyncChance = KickSyncChance,\n\t\tGhostSnareChance = GhostSnareChance,\n\t\tFillChance = FillChance,\n\t\tDrumBusy = DrumBusy,\n\t\tTripletChance = TripletChance,\n\t\tBassTriplets = BassTriplets,\n\t\tMelodyRestChance = MelodyRestChance,\n\t\tMelodyLeapChance = MelodyLeapChance,\n\t\tMelodyVibrato = MelodyVibrato,\n\t\tPanAmount = PanAmount,\n\t\tTrumpetWeight = TrumpetWeight,\n\t\tSaxWeight = SaxWeight,\n\t\tOrganWeight = OrganWeight,\n\t\tTromboneWeight = TromboneWeight,\n\t\tForceInstrument = ForceInstrument,\n\t\tHornSectionChance = HornSectionChance,\n\t\tHornDensity = HornDensity,\n\t\tGenre = Genre,\n\t\tDrumTone = DrumTone,\n\t\tDrumDrive = DrumDrive,\n\t\tKeysVol = KeysVol,\n\t\tKeysCutoff = KeysCutoff,\n\t\tKeysDrive = KeysDrive,\n\t\tKeysChug = KeysChug,\n\t\tRhythmGtrVol = RhythmGtrVol,\n\t\tRhythmGtrCutoff = RhythmGtrCutoff,\n\t\tRhythmGtrDrive = RhythmGtrDrive,\n\t\tRhythmGtrChug = RhythmGtrChug,\n\t\tLeadGtrVol = LeadGtrVol,\n\t\tLeadGtrCutoff = LeadGtrCutoff,\n\t\tLeadGtrDrive = LeadGtrDrive,\n\t\tLeadGtrBend = LeadGtrBend,\n\t};\n\n\tint ConfigHash()\n\t{\n\t\tvar h = new HashCode();\n\t\th.Add( SampleRate ); h.Add( TargetSeconds );\n\t\th.Add( BpmMin ); h.Add( BpmMax ); h.Add( FastChance );\n\t\th.Add( FastBpmMin ); h.Add( FastBpmMax ); h.Add( Swing ); h.Add( FastSwing );\n\t\th.Add( BassVol ); h.Add( SkankVol ); h.Add( OrganVol ); h.Add( MelodyVol ); h.Add( HornVol );\n\t\th.Add( KickVol ); h.Add( SnareVol ); h.Add( TomVol ); h.Add( HatVol ); h.Add( CrashVol ); h.Add( DrumVol );\n\t\th.Add( Detune ); h.Add( BassCutoff ); h.Add( SkankCutoff ); h.Add( SkankHighpass ); h.Add( SkankChop );\n\t\th.Add( LeadCutoff ); h.Add( OrganCutoff ); h.Add( OrganVibrato ); h.Add( HornCutoff ); h.Add( Resonance );\n\t\th.Add( BassDrive ); h.Add( SkankDrive ); h.Add( MelodyDrive ); h.Add( HornDrive );\n\t\th.Add( MasterDrive ); h.Add( MasterPeak );\n\t\th.Add( OctavePopChance ); h.Add( OrganBubbleChance ); h.Add( KickSyncChance );\n\t\th.Add( GhostSnareChance ); h.Add( FillChance );\n\t\th.Add( DrumBusy ); h.Add( DrumTone ); h.Add( DrumDrive ); h.Add( TripletChance ); h.Add( BassTriplets );\n\t\th.Add( MelodyRestChance ); h.Add( MelodyLeapChance ); h.Add( MelodyVibrato );\n\t\th.Add( PanAmount );\n\t\th.Add( TrumpetWeight ); h.Add( SaxWeight ); h.Add( OrganWeight ); h.Add( TromboneWeight );\n\t\th.Add( ForceInstrument );\n\t\th.Add( HornSectionChance ); h.Add( HornDensity );\n\t\th.Add( Genre );\n\t\th.Add( KeysVol ); h.Add( KeysCutoff ); h.Add( KeysDrive ); h.Add( KeysChug );\n\t\th.Add( RhythmGtrVol ); h.Add( RhythmGtrCutoff ); h.Add( RhythmGtrDrive ); h.Add( RhythmGtrChug );\n\t\th.Add( LeadGtrVol ); h.Add( LeadGtrCutoff ); h.Add( LeadGtrDrive ); h.Add( LeadGtrBend );\n\t\th.Add( Tag ); h.Add( Vibe );\n\t\treturn h.ToHashCode();\n\t}\n\n\t// Run the (pure, CPU-heavy) synthesis on worker threads so it never blocks the frame AND so\n\t// no single worker burst runs long enough to trip s\u0026box\u0027s ~1000ms no-yield advisory.\n\t// Composition \u002B drum synthesis are RNG-bound and stay sequential (BeginPlan, one worker); the\n\t// pitched voices pull no RNG, so they fan out across RenderThreads disjoint windows joined by\n\t// Task.WhenAll; the master\u002Binterleave runs on one worker. Result is interleaved stereo PCM.\n\tasync Task\u003Cshort[]\u003E GenerateStereoAsync( string seedStr, MusicGen.Config cfg )\n\t{\n\t\tMusicGen g = null;\n\t\tawait GameTask.RunInThreadAsync( () =\u003E { g = MusicGen.BeginPlan( seedStr, cfg ); return Task.CompletedTask; } );\n\n\t\tint total = g.TotalSamples;\n\t\tint k = Math.Clamp( RenderThreads, 1, 8 );\n\t\tif ( k \u003C= 1 )\n\t\t{\n\t\t\tawait GameTask.RunInThreadAsync( () =\u003E { g.RenderPitchedRange( 0, total ); return Task.CompletedTask; } );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tvar jobs = new Task[k];\n\t\t\tfor ( int i = 0; i \u003C k; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tint from = (int)((long)total * i / k);\n\t\t\t\tint to = (int)((long)total * (i \u002B 1) / k);\n\t\t\t\tjobs[i] = GameTask.RunInThreadAsync( () =\u003E { g.RenderPitchedRange( from, to ); return Task.CompletedTask; } );\n\t\t\t}\n\t\t\tawait Task.WhenAll( jobs );\n\t\t}\n\n\t\tshort[] pcm = null;\n\t\tawait GameTask.RunInThreadAsync( () =\u003E { pcm = g.FinishStereo(); return Task.CompletedTask; } );\n\t\t_sr = g.SampleRate;\n\t\treturn pcm;\n\t}\n\n\tint FadeFrames =\u003E Math.Max( 1, (int)(Math.Clamp( Crossfade, 0.25f, 8f ) * _sr) );\n\n\t// All look-ahead buffers are interleaved stereo PCM; lengths/offsets below are in frames.\n\tstatic int Frames( short[] pcm ) =\u003E pcm.Length / MusicGen.Channels;\n\n\t/// \u003Csummary\u003ETop the look-ahead buffer up to \u003Csee cref=\u0022AheadCount\u0022/\u003E, generating each song on\n\t/// a worker thread. Fire-and-forget from OnUpdate; \u003Cparamref name=\u0022seq\u0022/\u003E guards against a\n\t/// sequence restart landing a stale song in the buffer.\u003C/summary\u003E\n\tasync Task FillAhead( int seq )\n\t{\n\t\tif ( Generating ) return;\n\t\ttry\n\t\t{\n\t\t\t_fillingAhead = true;\n\t\t\tstring tag = SeedTag;\n\t\t\tvar cfg = BuildConfig();\n\t\t\twhile ( seq == _seq \u0026\u0026 _curRaw != null \u0026\u0026 _ahead.Count \u003C Math.Max( 1, AheadCount ) )\n\t\t\t{\n\t\t\t\tint n = _curN \u002B 1 \u002B _ahead.Count;\n\t\t\t\tvar song = await GenerateStereoAsync( SeedFor( tag, n ), cfg );\n\t\t\t\tif ( seq != _seq ) return;   // sequence restarted while we were generating\n\t\t\t\t_ahead.Add( song );\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022SkafinityPlayer: FillAhead failed: {e.Message}\u0022 ); }\n\t\tfinally { _fillingAhead = false; }\n\t}\n\n\t/// \u003Csummary\u003EWrite \u003Csee cref=\u0022LoopsPerSong\u0022/\u003E passes of interleaved-stereo \u003Cparamref name=\u0022raw\u0022/\u003E\n\t/// to the stream, given the first \u003Cparamref name=\u0022headConsumed\u0022/\u003E frames of pass 0 were already\n\t/// emitted, holding back the final \u003Cparamref name=\u0022reserve\u0022/\u003E frames for the next crossfade.\n\t/// Optional fade-in over the first \u003Cparamref name=\u0022fadeIn\u0022/\u003E frames of pass 0. Returns frames\n\t/// written.\u003C/summary\u003E\n\tint WriteSongBody( short[] raw, int headConsumed, int reserve, int fadeIn )\n\t{\n\t\tconst int ch = MusicGen.Channels;\n\t\tint loops = Math.Max( 1, LoopsPerSong );\n\t\tint rawFrames = raw.Length / ch;\n\t\tint total = 0;\n\t\tfor ( int loop = 0; loop \u003C loops; loop\u002B\u002B )\n\t\t{\n\t\t\tint start = loop == 0 ? headConsumed : 0;\n\t\t\tint end = loop == loops - 1 ? rawFrames - reserve : rawFrames;\n\t\t\tif ( end \u003C= start ) continue;\n\t\t\tint len = end - start;\n\t\t\tvar seg = new short[len * ch];\n\t\t\tfor ( int i = 0; i \u003C len; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tint frame = start \u002B i;\n\t\t\t\tfloat g = (loop == 0 \u0026\u0026 fadeIn \u003E 0 \u0026\u0026 frame \u003C fadeIn) ? (float)frame / fadeIn : 1f;\n\t\t\t\tfor ( int c = 0; c \u003C ch; c\u002B\u002B )\n\t\t\t\t\tseg[i * ch \u002B c] = (short)(raw[frame * ch \u002B c] * g);\n\t\t\t}\n\t\t\t_stream.WriteData( seg );\n\t\t\ttotal \u002B= len;\n\t\t}\n\t\treturn total;\n\t}\n\n\t/// \u003Csummary\u003E(Re)start the infinite sequence at the current tag/n. Bumps the sequence token\n\t/// (invalidating any in-flight generation), stops the current handle, then kicks the async\n\t/// (worker-thread) start so the caller never blocks.\u003C/summary\u003E\n\tpublic void StartSequence()\n\t{\n\t\tint seq = \u002B\u002B_seq;\n\t\t_ahead.Clear();\n\t\t_handle?.Stop();\n\t\t_handle = null;\n\t\t_stream = null;\n\t\t_flatConfigured = false;\n\t\t_curRaw = null;\n\t\t_ = StartSequenceAsync( seq );\n\t}\n\n\t// The first song fades in; thereafter songs play LoopsPerSong passes and crossfade into the\n\t// pre-generated next. Synthesis is offloaded; stream setup is on the main thread.\n\tasync Task StartSequenceAsync( int seq )\n\t{\n\t\ttry\n\t\t{\n\t\t\t_starting = true;\n\t\t\tint n = Math.Max( 0, _curN );\n\t\t\tstring tag = SeedTag;\n\t\t\tvar cfg = BuildConfig();\n\t\t\tvar raw = await GenerateStereoAsync( SeedFor( tag, n ), cfg );\n\t\t\tif ( seq != _seq ) return;   // superseded by a newer StartSequence\n\n\t\t\t_curN = n;\n\t\t\t_curRaw = raw;\n\t\t\tint fade = Math.Min( FadeFrames, Frames( _curRaw ) / 3 );\n\t\t\t_curReserve = fade;\n\n\t\t\t_stream = new SoundStream( _sr, MusicGen.Channels );\n\n\t\t\t// First song: LoopsPerSong passes, fade-in over the first \u0060fade\u0060, last \u0060fade\u0060 of the\n\t\t\t// final pass held back for the crossfade into the next song.\n\t\t\tint written = WriteSongBody( _curRaw, 0, _curReserve, fade );\n\t\t\t_pushedSeconds = written / (double)_sr;\n\n\t\t\t_handle = _stream.Play();\n\t\t\tif ( _handle != null )\n\t\t\t{\n\t\t\t\t_handle.Volume = TargetVolume();\n\t\t\t\tConfigureFlat();\n\t\t\t}\n\t\t\t_sinceStart = 0;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022SkafinityPlayer: StartSequence failed: {e.Message}\u0022 );\n\t\t}\n\t\tfinally { _starting = false; }\n\t}\n\n\t// Queue the crossfade from the current song\u0027s tail into the next song\u0027s head, then the next\n\t// song\u0027s body. Advances n (persisted if enabled); the following song is topped up by OnUpdate.\n\tvoid PushTransition()\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar next = _ahead[0];\n\t\t\t_ahead.RemoveAt( 0 );\n\n\t\t\t// Crossfade window = the current song\u0027s held-back tail (so there\u0027s no gap or overlap\n\t\t\t// even when songs differ in length). The two songs only overlap for CrossfadeOverlap\n\t\t\t// of this window, centred \u2014 the rest plays in the clear.\n\t\t\tconst int ch = MusicGen.Channels;\n\t\t\tint W = Math.Min( _curReserve, Frames( next ) / 3 );\n\t\t\tint curStart = Frames( _curRaw ) - W;\n\t\t\tint cross = Math.Clamp( (int)(W * CrossfadeOverlap), 1, W );\n\t\t\tint ws = (W - cross) / 2;     // overlap starts here\n\t\t\tint we = ws \u002B cross;          // overlap ends here\n\n\t\t\tvar xf = new short[W * ch];\n\t\t\tfor ( int i = 0; i \u003C W; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tfloat gOut, gIn;\n\t\t\t\tif ( i \u003C ws ) { gOut = 1f; gIn = 0f; }            // outgoing in the clear\n\t\t\t\telse if ( i \u003E= we ) { gOut = 0f; gIn = 1f; }      // incoming in the clear\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tdouble t = (i - ws \u002B 0.5) / cross * (Math.PI / 2); // equal-power cross\n\t\t\t\t\tgOut = (float)Math.Cos( t );\n\t\t\t\t\tgIn = (float)Math.Sin( t );\n\t\t\t\t}\n\t\t\t\tfor ( int c = 0; c \u003C ch; c\u002B\u002B )\n\t\t\t\t\txf[i * ch \u002B c] = (short)Math.Clamp( _curRaw[(curStart \u002B i) * ch \u002B c] * gOut \u002B next[i * ch \u002B c] * gIn, -32768, 32767 );\n\t\t\t}\n\t\t\t_stream.WriteData( xf );\n\n\t\t\t// next song: LoopsPerSong passes, first W of pass 0 already in the crossfade, last\n\t\t\t// \u0060nextReserve\u0060 of the final pass held back for the following crossfade.\n\t\t\tint nextReserve = Math.Min( FadeFrames, Frames( next ) / 3 );\n\t\t\tint written = WriteSongBody( next, W, nextReserve, 0 );\n\t\t\t_pushedSeconds \u002B= (W \u002B written) / (double)_sr;\n\n\t\t\t_curRaw = next;\n\t\t\t_curReserve = nextReserve;\n\t\t\t_curN\u002B\u002B;\n\t\t\tif ( PersistProgress ) SaveN( _curN );\n\n\t\t\t// Shuffle mode: each new song gets a fresh set of vibe knobs (NOT volumes \u2014 those are\n\t\t\t// a local mix preference, kept out of the seed \u2014 and NOT genre, matching the web toy).\n\t\t\t// Re-voice WITHOUT a restart (restart: false) \u2014 a restart would throw away the song we\n\t\t\t// just crossfaded into and replay _curN from scratch, stalling the n\u002B1 progression.\n\t\t\t// Instead drop the look-ahead so FillAhead (OnUpdate) regenerates upcoming songs with\n\t\t\t// the new vibe, and absorb the Vibe change into the config hash so LiveReload doesn\u0027t\n\t\t\t// fire its own restart either. The current crossfade keeps playing; n keeps advancing.\n\t\t\tif ( RandomEverySong )\n\t\t\t{\n\t\t\t\tRerollVibe( restart: false );\n\t\t\t\t_ahead.Clear();\n\t\t\t\t_lastConfigHash = ConfigHash();\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022SkafinityPlayer: PushTransition failed: {e.Message}\u0022 );\n\t\t}\n\t}\n\n\t// \u2500\u2500 Public control surface \u2500\u2500\n\n\t// Parse a shareable seed in any of vibe:tag:n / tag:n / tag. Missing parts stay null.\n\tstatic void ParseSeed( string seed, out string vibe, out string tag, out int? n )\n\t{\n\t\tvibe = null; tag = null; n = null;\n\t\tseed = seed?.Trim();\n\t\tif ( string.IsNullOrEmpty( seed ) ) return;\n\t\tvar p = seed.Split( \u0027:\u0027 );\n\t\tif ( p.Length \u003E= 3 ) { vibe = p[0]; tag = p[1]; if ( int.TryParse( p[2], out var v ) ) n = v; }\n\t\telse if ( p.Length == 2 )\n\t\t{\n\t\t\tif ( int.TryParse( p[1], out var v ) ) { tag = p[0]; n = v; }\n\t\t\telse if ( VibeCodec.LooksLikeVibe( p[0] ) ) { vibe = p[0]; tag = p[1]; }\n\t\t\telse tag = p[0];\n\t\t}\n\t\telse tag = p[0];\n\t}\n\n\t/// \u003Csummary\u003EPlay a shareable seed in any of the forms \u003Cc\u003Evibe:tag:n\u003C/c\u003E, \u003Cc\u003Etag:n\u003C/c\u003E, or\n\t/// \u003Cc\u003Etag\u003C/c\u003E. Missing components are left unchanged; a vibe is only applied when present.\n\t/// Restarts the sequence.\u003C/summary\u003E\n\tpublic void PlaySeed( string seed )\n\t{\n\t\tParseSeed( seed, out string vibe, out string tag, out int? n );\n\t\tif ( tag != null ) Tag = tag.Trim().ToLowerInvariant();\n\t\tif ( vibe != null ) Vibe = VibeCodec.LooksLikeVibe( vibe ) ? vibe.ToLowerInvariant() : \u0022\u0022;\n\t\tif ( n.HasValue ) _curN = Math.Max( 0, n.Value );\n\t\tif ( PersistProgress ) SaveN( _curN );\n\t\tStartSequence();\n\t}\n\n\t/// \u003Csummary\u003ESet just the seed tag (empty = the default \u0022skafinity\u0022 seed). Restarts.\u003C/summary\u003E\n\tpublic void SetTag( string tag )\n\t{\n\t\tTag = string.IsNullOrEmpty( tag ) ? \u0022\u0022 : tag.Trim().ToLowerInvariant();\n\t\tStartSequence();\n\t}\n\n\t/// \u003Csummary\u003EJump to song index n in the sequence (clamped \u2265 0). Restarts.\u003C/summary\u003E\n\tpublic void SetN( int n )\n\t{\n\t\t_curN = Math.Max( 0, n );\n\t\tif ( PersistProgress ) SaveN( _curN );\n\t\tStartSequence();\n\t}\n\n\t/// \u003Csummary\u003EStep the song index by \u003Cparamref name=\u0022delta\u0022/\u003E (e.g. \u002B1 / -1). Restarts.\u003C/summary\u003E\n\tpublic void StepN( int delta ) =\u003E SetN( _curN \u002B delta );\n\t/// \u003Csummary\u003ESkip to the next song in the sequence.\u003C/summary\u003E\n\tpublic void NextSong() =\u003E StepN( 1 );\n\t/// \u003Csummary\u003EStep back to the previous song in the sequence.\u003C/summary\u003E\n\tpublic void PrevSong() =\u003E StepN( -1 );\n\n\t/// \u003Csummary\u003ESet vibe field \u003Cparamref name=\u0022index\u0022/\u003E (see \u003Csee cref=\u0022VibeCodec.Fields(int)\u0022/\u003E) from\n\t/// a 0..1 fraction, store the re-encoded \u003Csee cref=\u0022Vibe\u0022/\u003E, and restart on a short debounce.\u003C/summary\u003E\n\tpublic void SetVibe( int index, float norm )\n\t{\n\t\tvar cfg = BuildConfig();\n\t\tvar fields = VibeCodec.Fields( cfg.Genre );\n\t\tif ( index \u003C 0 || index \u003E= fields.Count ) return;\n\t\tvar f = fields[index];\n\t\tf.SetNorm( cfg, norm );\n\t\tif ( VibeCodec.IsVolume( f ) )\n\t\t{\n\t\t\t// Volume is a local mix preference, not part of the seed \u2014 store per-voice \u002B persist.\n\t\t\t_vols[f.Voice] = norm;\n\t\t\tSaveVols();\n\t\t}\n\t\telse\n\t\t{\n\t\t\tVibe = VibeCodec.Encode( cfg );\n\t\t}\n\t\t_restartPending = true;\n\t\t_restartPendingSince = 0;\n\t}\n\n\t/// \u003Csummary\u003ESwitch genre (rides in the vibe\u0027s first char): re-encode the effective config\n\t/// with the new genre into \u003Csee cref=\u0022Vibe\u0022/\u003E so it sticks over the inspector knobs, then\n\t/// restart. Use this rather than setting \u003Csee cref=\u0022Genre\u0022/\u003E directly \u2014 an existing\n\t/// \u003Csee cref=\u0022Vibe\u0022/\u003E override otherwise wins and the change wouldn\u0027t take.\u003C/summary\u003E\n\tpublic void SetGenre( int genre )\n\t{\n\t\tvar cfg = BuildConfig();\n\t\tcfg.Genre = Math.Clamp( genre, 0, VibeCodec.GenreCount - 1 );\n\t\tVibe = VibeCodec.Encode( cfg );\n\t\tStartSequence();\n\t}\n\n\t/// \u003Csummary\u003ERandomize the vibe knobs and restart on a short debounce. By default the\n\t/// per-instrument volumes (and genre) are left alone so a reroll re-voices without upending\n\t/// the mix; pass \u003Cparamref name=\u0022includeVolumes\u0022/\u003E / \u003Cparamref name=\u0022includeGenre\u0022/\u003E for a\n\t/// full shuffle. Pass \u003Cparamref name=\u0022restart\u0022/\u003E = false to re-voice without yanking the\n\t/// playhead \u2014 the caller is then responsible for letting the change take effect (e.g. by\n\t/// clearing the look-ahead so upcoming songs regenerate with the new vibe).\u003C/summary\u003E\n\tpublic void RerollVibe( bool includeVolumes = false, bool includeGenre = false, bool restart = true )\n\t{\n\t\tvar cfg = BuildConfig();\n\t\tvar rng = System.Random.Shared;\n\t\tif ( includeGenre )\n\t\t\tcfg.Genre = rng.Next( VibeCodec.GenreCount );\n\t\tforeach ( var f in VibeCodec.Fields( cfg.Genre ) )\n\t\t{\n\t\t\tif ( !includeVolumes \u0026\u0026 f.Voice != null \u0026\u0026 f.Column == 0 ) continue; // skip per-instrument volumes\n\t\t\tf.SetNorm( cfg, rng.NextSingle() );\n\t\t}\n\t\tif ( cfg.BpmMin \u003E cfg.BpmMax ) (cfg.BpmMin, cfg.BpmMax) = (cfg.BpmMax, cfg.BpmMin);\n\t\tVibe = VibeCodec.Encode( cfg );\n\t\tif ( includeVolumes )\n\t\t{\n\t\t\t// Capture the freshly-randomized volumes into the persisted per-voice store (they\n\t\t\t// don\u0027t ride in the encoded vibe).\n\t\t\tforeach ( var kv in VibeCodec.ReadVolumes( cfg.Genre, cfg ) ) _vols[kv.Key] = kv.Value;\n\t\t\tSaveVols();\n\t\t}\n\t\tif ( restart )\n\t\t{\n\t\t\t_restartPending = true;\n\t\t\t_restartPendingSince = 0;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003EWrite the playing song\u0027s raw loop (no fade) to a WAV under FileSystem.Data.\n\t/// Returns the filename written, or null on failure.\u003C/summary\u003E\n\tpublic string SaveCurrentToFile()\n\t{\n\t\tif ( _curRaw == null || _sr \u003C= 0 ) return null;\n\t\tvar tag = string.IsNullOrEmpty( SeedTag ) ? \u0022skafinity\u0022 : SeedTag.ToLowerInvariant();\n\t\tvar name = $\u0022{tag}_{_curN}.wav\u0022;\n\t\ttry\n\t\t{\n\t\t\tFileSystem.Data.WriteAllBytes( name, MusicGen.WavFromSamples( _curRaw, 1, _sr ) );\n\t\t\treturn name;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022SkafinityPlayer: save failed: {e.Message}\u0022 );\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t// \u2500\u2500 Optional progress persistence (FileSystem.Data, see assets/file-system.md) \u2500\u2500\n\tstring ProgressFile =\u003E $\u0022skafinity_{(string.IsNullOrEmpty( SaveSlot ) ? \u0022default\u0022 : SaveSlot)}.n\u0022;\n\n\tvoid SaveN( int n )\n\t{\n\t\ttry { FileSystem.Data.WriteAllText( ProgressFile, n.ToString() ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022SkafinityPlayer: save progress failed: {e.Message}\u0022 ); }\n\t}\n\n\tint? LoadN()\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( FileSystem.Data.FileExists( ProgressFile )\n\t\t\t\t\u0026\u0026 int.TryParse( FileSystem.Data.ReadAllText( ProgressFile ), out var v ) )\n\t\t\t\treturn Math.Max( 0, v );\n\t\t}\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022SkafinityPlayer: load progress failed: {e.Message}\u0022 ); }\n\t\treturn null;\n\t}\n\n\t// \u2500\u2500 Per-instrument volume persistence (FileSystem.Data, keyed by SaveSlot) \u2500\u2500\n\t// Stored as JSON voice\u21920..1 level, separate from progress and from the (volume-free) seed,\n\t// so the mix is a local preference that survives sessions and follows each voice across genres.\n\tstring VolumeFile =\u003E $\u0022skafinity_{(string.IsNullOrEmpty( SaveSlot ) ? \u0022default\u0022 : SaveSlot)}.vol\u0022;\n\n\tvoid SaveVols()\n\t{\n\t\ttry { FileSystem.Data.WriteAllText( VolumeFile, Json.Serialize( _vols ) ); }\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022SkafinityPlayer: save volumes failed: {e.Message}\u0022 ); }\n\t}\n\n\tSystem.Collections.Generic.Dictionary\u003Cstring, float\u003E LoadVols()\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( FileSystem.Data.FileExists( VolumeFile ) )\n\t\t\t\treturn Json.Deserialize\u003CSystem.Collections.Generic.Dictionary\u003Cstring, float\u003E\u003E(\n\t\t\t\t\tFileSystem.Data.ReadAllText( VolumeFile ) ) ?? new();\n\t\t}\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022SkafinityPlayer: load volumes failed: {e.Message}\u0022 ); }\n\t\treturn new();\n\t}\n\n\t// \u2500\u2500 Shared house-mix config (read-only, shipped with the addon) \u2500\u2500\n\t// The SAME JSON the web toy uses (web/config.json is \u0060make\u0060-copied from the library\u0027s\n\t// skafinity.config.json). Its \u0022advanced\u0022 block overlays the baseline peak-balance / level\n\t// mix onto every BuildConfig, so the house mix is retuned by editing one file rather than\n\t// recompiling. Read-only addon content \u2192 FileSystem.Mounted. See VibeCodec.ApplyAdvanced.\n\tconst string HouseConfigFile = \u0022skafinity.config.json\u0022;\n\n\tclass HouseConfigDto { public System.Collections.Generic.Dictionary\u003Cstring, float\u003E advanced { get; set; } }\n\n\tSystem.Collections.Generic.Dictionary\u003Cstring, float\u003E LoadHouseConfig()\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( FileSystem.Mounted.FileExists( HouseConfigFile ) )\n\t\t\t{\n\t\t\t\tvar dto = Json.Deserialize\u003CHouseConfigDto\u003E( FileSystem.Mounted.ReadAllText( HouseConfigFile ) );\n\t\t\t\treturn dto?.advanced ?? new();\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception e ) { Log.Warning( $\u0022SkafinityPlayer: load house config failed: {e.Message}\u0022 ); }\n\t\treturn new();\n\t}\n}\n"},{"Ident":"gamah.splitclicker","Path":"UI/SkafinityMusicPanel.razor","FileName":"SkafinityMusicPanel.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"@using System\n@using System.Collections.Generic\n@using System.Linq\n@using Sandbox\n@using Sandbox.UI\n@namespace Skafinity\n@inherits PanelComponent\n\n@*\n\tThe optional drop-in settings panel for the Skafinity music engine.\n\n\tAdd this PanelComponent to a GameObject under a ScreenPanel (or WorldPanel). It finds a\n\tSkafinityPlayer in the scene (or set Player explicitly) and offers the whole knob surface\n\tas UI \u2014 you don\u0027t have to wire anything. A floating \u266A button toggles the board open/closed.\n\n\tThe engine needs nothing from this: SkafinityPlayer plays on its own. This panel is pure\n\tconvenience for players who want to tweak the vibe rather than tune it in the inspector.\n\n\tThe vibe editor is driven entirely from the library\u0027s VibeCodec field metadata for the\n\tcurrent genre: each field reports its voice (matrix row, or null for a GLOBAL knob) and\n\tcolumn (0 volume / 1 tone / 2 character / 3 extra). A new genre \u2014 or a new knob \u2014 is a pure\n\tengine change; there is no field table here. s\u0026box has no slider widget, so each knob is a\n\tstrip of tick cells (one per base-36 level the seed encodes); each change re-encodes the\n\tvibe and restarts the player on a short debounce.\n\n\tRe-theming: the palette lives as SCSS variables at the top of SkafinityMusicPanel.razor.scss.\n\tOverride those (or supply your own panel against the same SkafinityPlayer API) to restyle.\n*@\n\n\u003Croot class=\u0022@( IsOpen ? \u0022open\u0022 : \u0022\u0022 )\u0022\u003E\n\t@if ( !IsOpen \u0026\u0026 ShowFab )\n\t{\n\t\t\u003Cdiv class=\u0022fab @( WideFab ? \u0022wide\u0022 : \u0022\u0022 )\u0022 onclick=\u0022@Toggle\u0022\u003E@FabLabel\u003C/div\u003E\n\t}\n\telse\n\t{\n\t@{ var cfg = Player?.EffectiveConfig(); int genre = cfg?.Genre ?? 0; }\n\t\u003Cdiv class=\u0022music\u0022\u003E\n\t\t\u003Cdiv class=\u0022header\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022title\u0022\u003EMUSIC\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022close\u0022 onclick=\u0022@Toggle\u0022\u003E\u2715\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022top\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022label\u0022\u003ENOW PLAYING\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022play-row\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022seed-box\u0022\u003E@Seed()\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022btn\u0022 onclick=\u0022@CopySeed\u0022\u003E@_copyLabel\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\n\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022label\u0022\u003ESONG\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022play-row\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022btn wide\u0022 onclick=\u0022@( () =\u003E Step( -1 ) )\u0022\u003E\u25C0 Prev\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022num\u0022\u003E@( Player?.N ?? 0 )\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022btn wide\u0022 onclick=\u0022@( () =\u003E Step( 1 ) )\u0022\u003ENext \u25B6\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\n\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022label\u0022\u003EPLAY A SEED\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022play-row\u0022\u003E\n\t\t\t\t\t\u003CTextEntry @ref=\u0022_tagEntry\u0022 placeholder=\u0022Paste vibe:tag:n (or a tag \u2014 blank = default)\u0022 class=\u0022tag-input\u0022 /\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022btn\u0022 onclick=\u0022@Play\u0022\u003EPlay\u003C/div\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022btn\u0022 onclick=\u0022@UseDefault\u0022\u003EUse default\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\n\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022label\u0022\u003EMUTE\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022play-row\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022btn toggle @( Player?.Enabled == false ? \u0022on\u0022 : \u0022\u0022 )\u0022 onclick=\u0022@ToggleMute\u0022\u003E@( Player?.Enabled == false ? \u0022MUTED\u0022 : \u0022PLAYING\u0022 )\u003C/div\u003E\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\n\t\t\t\u003Cdiv class=\u0022row\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022label\u0022\u003EVOLUME\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022cells\u0022\u003E\n\t\t\t\t\t@foreach ( var v in VolumeSteps )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar vv = v;\n\t\t\t\t\t\t\u003Cdiv class=\u0022cell @( ( Player?.Volume ?? 1f ) \u003E= vv - 0.001f ? \u0022filled\u0022 : \u0022\u0022 )\u0022\n\t\t\t\t\t\t\t onclick=\u0022@( () =\u003E SetVolume( vv ) )\u0022\u003E@VolLabel( vv )\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022row genre-row\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022label\u0022\u003EGENRE\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022cells\u0022\u003E\n\t\t\t\t@for ( int g = 0; g \u003C VibeCodec.GenreCount; g\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar gg = g;\n\t\t\t\t\t\u003Cdiv class=\u0022cell choice @( g == genre ? \u0022selected\u0022 : \u0022\u0022 )\u0022\n\t\t\t\t\t\t onclick=\u0022@( () =\u003E SetGenre( gg ) )\u0022\u003E@VibeCodec.Genres[g]\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\t\u003Cdiv class=\u0022btn reroll\u0022 onclick=\u0022@Reroll\u0022\u003E\uD83C\uDFB2 Reroll\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022btn reroll toggle @( Player?.RandomEverySong == true ? \u0022on\u0022 : \u0022\u0022 )\u0022\n\t\t\t\t\t onclick=\u0022@ToggleRandomEverySong\u0022\u003E\uD83C\uDFB2 Random every song: @( Player?.RandomEverySong == true ? \u0022ON\u0022 : \u0022OFF\u0022 )\u003C/div\u003E\n\t\t\t\t\u003Cdiv class=\u0022btn\u0022 onclick=\u0022@Save\u0022\u003ESave .wav\u003C/div\u003E\n\t\t\t\u003C/div\u003E\n\t\t\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022label\u0022\u003EVIBE \u2014 per-instrument mixer (tweak, then share the seed)\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022matrix\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022mrow mhead\u0022\u003E\n\t\t\t\t\u003Cdiv class=\u0022mvoice\u0022\u003E\u003C/div\u003E\n\t\t\t\t@foreach ( var h in ColHeaders )\n\t\t\t\t{\n\t\t\t\t\t\u003Cdiv class=\u0022mcell mlabel\u0022\u003E@h\u003C/div\u003E\n\t\t\t\t}\n\t\t\t\u003C/div\u003E\n\t\t\t@foreach ( var (voice, cells) in InstrumentRows( genre ) )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022mrow\u0022\u003E\n\t\t\t\t\t\u003Cdiv class=\u0022mvoice\u0022\u003E@voice\u003C/div\u003E\n\t\t\t\t\t@for ( int col = 0; col \u003C ColHeaders.Length; col\u002B\u002B )\n\t\t\t\t\t{\n\t\t\t\t\t\tvar f = cells[col];\n\t\t\t\t\t\t\u003Cdiv class=\u0022mcell\u0022\u003E\n\t\t\t\t\t\t\t@if ( f != null )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t@Knob( f, cfg, f.Name == ColHeaders[col] ? \u0022\u0022 : f.Name )\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\u003C/div\u003E\n\t\t\t\t\t}\n\t\t\t\t\u003C/div\u003E\n\t\t\t}\n\t\t\u003C/div\u003E\n\n\t\t\u003Cdiv class=\u0022divider\u0022\u003E\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022label\u0022\u003EGLOBAL\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022vibe-grid\u0022\u003E\n\t\t\t@foreach ( var f in GlobalRows( genre ) )\n\t\t\t{\n\t\t\t\t\u003Cdiv class=\u0022vibe\u0022\u003E@Knob( f, cfg, f.Name )\u003C/div\u003E\n\t\t\t}\n\t\t\u003C/div\u003E\n\n\t\t@if ( _msg != null )\n\t\t{\n\t\t\t\u003Cdiv class=\u0022hint ok\u0022\u003E@_msg\u003C/div\u003E\n\t\t}\n\t\u003C/div\u003E\n\t}\n\u003C/root\u003E\n\n@code\n{\n\t/// \u003Csummary\u003EThe player this panel drives. Leave unset to auto-find a \u003Csee cref=\u0022SkafinityPlayer\u0022/\u003E\n\t/// in the scene on start.\u003C/summary\u003E\n\t[Property] public SkafinityPlayer Player { get; set; }\n\n\t// One tick cell per base-36 level the seed encodes (VibeCodec.Levels).\n\tstatic int UiTicks =\u003E VibeCodec.Levels;\n\tstatic readonly string[] ColHeaders = { \u0022VOLUME\u0022, \u0022TONE\u0022, \u0022CHARACTER\u0022, \u0022EXTRA\u0022 };\n\n\t// Volume control steps over the player\u0027s 0..2 range.\n\tstatic readonly float[] VolumeSteps = { 0f, 0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f };\n\n\t/// \u003Csummary\u003EWhether the settings board is showing (toggled by the \u266A button).\u003C/summary\u003E\n\tpublic bool IsOpen { get; private set; }\n\n\t/// \u003Csummary\u003EShow the built-in toggle button when the board is closed. Set false when a host\n\t/// UI provides its own opener and drives the board via \u003Csee cref=\u0022Toggle\u0022/\u003E.\u003C/summary\u003E\n\t[Property] public bool ShowFab { get; set; } = true;\n\n\t/// \u003Csummary\u003ELabel on the closed-state toggle button. Defaults to the \u266A glyph; a host can set\n\t/// e.g. \u0022MUSIC\u0022 to make it read as a labelled bar (pair with \u003Csee cref=\u0022WideFab\u0022/\u003E).\u003C/summary\u003E\n\t[Property] public string FabLabel { get; set; } = \u0022\u266A\u0022;\n\n\t/// \u003Csummary\u003ERender the toggle button as a wide labelled bar (host chrome) instead of the\n\t/// compact square glyph.\u003C/summary\u003E\n\t[Property] public bool WideFab { get; set; }\n\n\tTextEntry _tagEntry;\n\tbool _tagInit;\n\tstring _copyLabel = \u0022Copy\u0022;\n\tstring _msg;\n\n\tprotected override void OnStart()\n\t{\n\t\tPlayer ??= Scene.GetAllComponents\u003CSkafinityPlayer\u003E().FirstOrDefault();\n\t\tif ( Player == null )\n\t\t\tLog.Warning( \u0022SkafinityMusicPanel: no SkafinityPlayer found in the scene \u2014 add one (or set Player).\u0022 );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// One-time seed of the text entry with the current tag once the board opens.\n\t\tif ( !IsOpen ) { _tagInit = false; return; }\n\t\tif ( !_tagInit \u0026\u0026 _tagEntry != null ) { _tagEntry.Text = Player?.Tag ?? \u0022\u0022; _tagInit = true; }\n\t}\n\n\tstring Seed() =\u003E Player?.CurrentSeed ?? \u0022\u2014\u0022;\n\n\t/// \u003Csummary\u003EOpen/close the settings board.\u003C/summary\u003E\n\tpublic void Toggle()\n\t{\n\t\tIsOpen = !IsOpen;\n\t\tif ( !IsOpen ) _tagInit = false;\n\t}\n\n\tstatic string VolLabel( float v ) =\u003E v \u003C= 0f ? \u00220\u0022 : $\u0022{(int)Math.Round( v * 100 )}%\u0022;\n\n\t// Instrument rows for the genre: each voice with its [vol, tone, character, extra] cells\n\t// (null where a genre leaves a column empty). Order = the library\u0027s display/wire order.\n\tstatic IEnumerable\u003C(string Voice, VibeCodec.Field[] Cells)\u003E InstrumentRows( int genre )\n\t{\n\t\tvar order = new List\u003Cstring\u003E();\n\t\tvar byVoice = new Dictionary\u003Cstring, VibeCodec.Field[]\u003E();\n\t\tforeach ( var f in VibeCodec.Fields( genre ) )\n\t\t{\n\t\t\tif ( f.Voice == null ) continue;\n\t\t\tif ( !byVoice.TryGetValue( f.Voice, out var cells ) )\n\t\t\t{\n\t\t\t\tcells = new VibeCodec.Field[ColHeaders.Length];\n\t\t\t\tbyVoice[f.Voice] = cells;\n\t\t\t\torder.Add( f.Voice );\n\t\t\t}\n\t\t\tif ( f.Column \u003E= 0 \u0026\u0026 f.Column \u003C cells.Length ) cells[f.Column] = f;\n\t\t}\n\t\tforeach ( var v in order ) yield return (v, byVoice[v]);\n\t}\n\n\tstatic IEnumerable\u003CVibeCodec.Field\u003E GlobalRows( int genre ) =\u003E\n\t\tVibeCodec.Fields( genre ).Where( f =\u003E f.Voice == null );\n\n\t// Index of a field within the current genre\u0027s field list (what Player.SetVibe expects).\n\tstatic int FieldIndex( int genre, VibeCodec.Field field )\n\t{\n\t\tvar fields = VibeCodec.Fields( genre );\n\t\tfor ( int i = 0; i \u003C fields.Count; i\u002B\u002B )\n\t\t\tif ( ReferenceEquals( fields[i], field ) ) return i;\n\t\treturn -1;\n\t}\n\n\t// One knob: a name/value header over a strip of tick cells (or labeled choice cells).\n\tRenderFragment Knob( VibeCodec.Field f, MusicGen.Config cfg, string label )\n\t{\n\t\treturn @\u003Ctext\u003E\n\t\t\u003Cdiv class=\u0022vibe-head\u0022\u003E\n\t\t\t\u003Cdiv class=\u0022vibe-name\u0022\u003E@label\u003C/div\u003E\n\t\t\t\u003Cdiv class=\u0022vibe-val\u0022\u003E@( cfg != null ? f.Display( cfg ) : \u0022\u0022 )\u003C/div\u003E\n\t\t\u003C/div\u003E\n\t\t\u003Cdiv class=\u0022cells\u0022\u003E\n\t\t\t@{\n\t\t\t\tint genre = cfg?.Genre ?? 0;\n\t\t\t\tint idx = FieldIndex( genre, f );\n\t\t\t}\n\t\t\t@if ( f.Choices != null )\n\t\t\t{\n\t\t\t\tint sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (f.Choices.Length - 1) ) : 0;\n\t\t\t\t@for ( int k = 0; k \u003C f.Choices.Length; k\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar kk = k; var n = f.Choices.Length;\n\t\t\t\t\t\u003Cdiv class=\u0022cell choice @( k == sel ? \u0022selected\u0022 : \u0022\u0022 )\u0022\n\t\t\t\t\t\t onclick=\u0022@( () =\u003E SetVibe( idx, kk / (float)(n - 1) ) )\u0022\u003E@f.Choices[k]\u003C/div\u003E\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tint sel = cfg != null ? (int)MathF.Round( f.GetNorm( cfg ) * (UiTicks - 1) ) : 0;\n\t\t\t\t@for ( int k = 0; k \u003C UiTicks; k\u002B\u002B )\n\t\t\t\t{\n\t\t\t\t\tvar kk = k;\n\t\t\t\t\t\u003Cdiv class=\u0022cell tick @( k \u003C= sel ? \u0022filled\u0022 : \u0022\u0022 ) @( k == sel ? \u0022selected\u0022 : \u0022\u0022 )\u0022\n\t\t\t\t\t\t onclick=\u0022@( () =\u003E SetVibe( idx, kk / (float)(UiTicks - 1) ) )\u0022\u003E\u003C/div\u003E\n\t\t\t\t}\n\t\t\t}\n\t\t\u003C/div\u003E\n\t\u003C/text\u003E;\n\t}\n\n\tvoid Step( int d ) { Player?.StepN( d ); _msg = null; }\n\n\tvoid ToggleMute() { if ( Player != null ) Player.Enabled = !Player.Enabled; }\n\n\tvoid SetVolume( float v ) { if ( Player != null ) Player.Volume = v; }\n\n\tvoid SetVibe( int index, float norm )\n\t{\n\t\tif ( index \u003C 0 ) return;\n\t\tPlayer?.SetVibe( index, norm );\n\t\t_msg = null;\n\t}\n\n\tvoid SetGenre( int genre ) { Player?.SetGenre( genre ); _msg = null; }\n\n\tvoid Play()\n\t{\n\t\tPlayer?.PlaySeed( _tagEntry?.Text );\n\t\t_msg = Player != null ? $\u0022Playing {Player.CurrentSeed}\u0022 : null;\n\t}\n\n\tvoid UseDefault()\n\t{\n\t\tPlayer?.SetTag( \u0022\u0022 );\n\t\tif ( _tagEntry != null ) _tagEntry.Text = \u0022\u0022;\n\t\t_msg = \u0022Back to the default tag and vibe\u0022;\n\t}\n\n\tvoid CopySeed()\n\t{\n\t\ttry { Clipboard.SetText( Seed() ); _copyLabel = \u0022Copied!\u0022; }\n\t\tcatch { _copyLabel = \u0022\u2014\u0022; }\n\t}\n\n\tvoid Save()\n\t{\n\t\tvar name = Player?.SaveCurrentToFile();\n\t\t_msg = string.IsNullOrEmpty( name ) ? \u0022Couldn\u0027t save song\u0022 : $\u0022Saved {name} to your s\u0026box data folder\u0022;\n\t}\n\n\tvoid Reroll() { Player?.RerollVibe(); _msg = \u0022Rerolled every knob but the volumes\u0022; }\n\n\tvoid ToggleRandomEverySong()\n\t{\n\t\tif ( Player == null ) return;\n\t\tbool on = !Player.RandomEverySong;\n\t\tPlayer.RandomEverySong = on;\n\t\tif ( on ) Player.RerollVibe();\n\t\t_msg = on ? \u0022Shuffle: every new song re-rolls the vibe (keeps your volumes)\u0022 : \u0022Shuffle off\u0022;\n\t}\n\n\tprotected override int BuildHash() =\u003E\n\t\tHashCode.Combine(\n\t\t\tIsOpen, Player?.CurrentSeed, Player?.CurrentVibe, Player?.Enabled ?? true,\n\t\t\tPlayer?.Volume ?? 1f, Player?.RandomEverySong ?? false, _msg, _copyLabel );\n}\n"},{"Ident":"gamah.splitclicker","Path":"ui/button.cs.scss","FileName":"button.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":".button\r\n{\r\n\tposition: relative;\r\n\t\r\n\t\u003E .button-right-column\r\n\t{\r\n\t\tflex-direction: column;\r\n\t}\r\n}\r\n\r\n//  default menu position is below\r\n.button-hover-menu\r\n{\r\n\tposition: absolute;\r\n\ttop: 100%;\r\n\tflex-direction: column;\r\n\r\n\t\u0026.hidden\r\n\t{\r\n\t\topacity: 0;\r\n\t\tpointer-events: none;\r\n\t}\r\n}"},{"Ident":"gamah.splitclicker","Path":"ui/controls/color/colorpickercontrol.cs.scss","FileName":"colorpickercontrol.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"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":"gamah.splitclicker","Path":"styles/rootpanel.scss","FileName":"rootpanel.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"\r\n\r\nRootPanel\r\n{\r\n\tfont-family: Roboto;\r\n\tcolor: white;\r\n}\r\n\r\nIconPanel, i\r\n{\r\n\tfont-family: Material Icons;\r\n\tbackground-position: center;\r\n\tbackground-size: contain;\r\n}\r\n\r\n.tooltip\r\n{\r\n\tbackground-color: rgba( black, 0.8 );\r\n\tborder: 1px solid rgba( #555, 0.1 );\r\n\tpadding: 10px;\r\n\tborder-radius: 4px;\r\n\tcolor: white;\r\n\tfont-family: Inter;\r\n\tfont-weight: bold;\r\n\ttransition: opacity 0.09s ease;\r\n\topacity: 1;\r\n\tmargin: 0px;\r\n\r\n\t\u0026:intro, \u0026:outro\r\n\t{\r\n\t\topacity: 0;\r\n\t}\r\n}\r\n\r\n.textentry\r\n{\r\n\tcursor: text;\r\n\talign-items: center;\r\n\tjustify-content: center;\r\n\twhite-space: nowrap;\r\n\toverflow: hidden;\r\n\tflex-direction: row;\r\n\tposition: relative;\r\n\tpadding: 10px;\r\n\r\n\t\u0026.disabled\r\n\t{\r\n\t\tcursor: default;\r\n\t}\r\n\r\n\t\u0026.is-multiline\r\n\t{\r\n\t\talign-items: flex-start;\r\n\t\twhite-space: normal;\r\n\t}\r\n\r\n\t.content-label,\r\n\t.placeholder\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t}\r\n\r\n\t.placeholder\r\n\t{\r\n\t\topacity: 0.2;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t.iconpanel\r\n\t{\r\n\t\tposition: absolute;\r\n\t\tright: 0;\r\n\t\tpadding-right: 10px;\r\n\t\tpointer-events: none;\r\n\r\n\t\t\u0026.clearbutton\r\n\t\t{\r\n\t\t\tcolor: #fff;\r\n\t\t\tpointer-events: all;\r\n\t\t\tcursor: pointer;\r\n\t\t\topacity: 0.1;\r\n\r\n\t\t\t\u0026:hover\r\n\t\t\t{\r\n\t\t\t\topacity: 0.5;\r\n\t\t\t}\r\n\r\n\t\t\t\u0026:active\r\n\t\t\t{\r\n\t\t\t\topacity: 1;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026:empty\r\n\t{\r\n\t\t.clearbutton\r\n\t\t{\r\n\t\t\tdisplay: none;\r\n\t\t}\r\n\t}\r\n\r\n\t\u0026.has-icon\r\n\t{\r\n\t\tpadding-right: 30px;\r\n\t}\r\n\r\n\t.content-label,\r\n\t.prefix-label,\r\n\t.suffix-label,\r\n\t.placeholder\r\n\t{\r\n\t\toverflow: hidden;\r\n\t}\r\n\r\n\t.content-label\r\n\t{\r\n\t\torder: 1;\r\n\t}\r\n\r\n\t.suffix-label\r\n\t{\r\n\t\torder: 2;\r\n\t}\r\n\r\n\t.prefix-label,\r\n\t.suffix-label\r\n\t{\r\n\t\tflex-shrink: 0;\r\n\t\topacity: 0.5;\r\n\t}\r\n\r\n\t\u0026.invalid\r\n\t{\r\n\t\tbox-shadow: 0px 0px 1px 2px #f8356b;\r\n\t}\r\n}\r\n\r\n.popup-panel\r\n{\r\n\tbackground-color: #1b202c;\r\n\tflex-direction: column;\r\n\tcolor: #8e9ec2;\r\n\ttransition: all 0.1s ease-out;\r\n\tposition: absolute;\r\n\tz-index: 2000;\r\n\tmin-height: 20px;\r\n\tmin-width: 10px;\r\n\toverflow: scroll;\r\n\tpointer-events: all;\r\n\tbox-shadow: 3px 3px 64px #000;\r\n\tborder-radius: 4px;\r\n\r\n\t\u0026.medium\r\n\t{\r\n\t\tmax-width: 300px;\r\n\t}\r\n\r\n\t\u0026.center-horizontal\r\n\t{\r\n\t\ttransform-origin: top center;\r\n\t}\r\n\r\n\t\u0026.below-center\r\n\t{\r\n\t\ttransform: translateX( -50% );\r\n\t\ttransform-origin: 0% 0%;\r\n\t}\r\n\r\n\t\u0026.above-left\r\n\t{\r\n\t\ttransform-origin: 0% 100%;\r\n\t}\r\n\r\n\t\u0026.above-center\r\n\t{\r\n\t\ttransform: translateX( -50% );\r\n\t\ttransform-origin: 0% 100%;\r\n\t}\r\n\r\n\t\u0026.left\r\n\t{\r\n\t\ttransform-origin: 100% 50%;\r\n\t\ttransform: translate( 00% -50% );\r\n\t}\r\n\r\n\t\u0026.right\r\n\t{\r\n\t\ttransform-origin: 0 50%;\r\n\t\ttransform: translate( 0% -50% );\r\n\t}\r\n\r\n\t\u0026:intro\r\n\t{\r\n\t\ttransform: scale( 0 );\r\n\t\tpointer-events: none;\r\n\t\topacity: 0;\r\n\t}\r\n\r\n\t\u0026:outro\r\n\t{\r\n\t\topacity: 0;\r\n\t\ttransition: all 0.1s ease-in;\r\n\t\tmargin-top: 50px;\r\n\t\tpointer-events: none;\r\n\t}\r\n\r\n\t\u0026.below-stretch\r\n\t{\r\n\t\ttransform-origin: 0% 0%;\r\n\r\n\t\t\u0026:intro\r\n\t\t{\r\n\t\t\ttransform: scaleY( 0.1 );\r\n\t\t}\r\n\t}\r\n\r\n\t\u003E .canvas\r\n\t{\r\n\t\tflex-direction: column;\r\n\t}\r\n\r\n\t.information\r\n\t{\r\n\t\tpadding: 16px;\r\n\t\tfont-size: 14px;\r\n\t\topacity: 0.5;\r\n\t}\r\n\r\n\tbutton\r\n\t{\r\n\t\tpadding: 2px 8px;\r\n\t\tcursor: pointer;\r\n\t\tfont-family: Poppins;\r\n\t\tfont-weight: 600;\r\n\t\tflex-shrink: 0;\r\n\r\n\t\t\u0026:hover\r\n\t\t{\r\n\t\t\tbackground-color: #08f;\r\n\t\t\tcolor: #fff;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"gamah.splitclicker","Path":"styles/form/_coloreditor.scss","FileName":"_coloreditor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"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\u003E .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\u003E 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\u0026: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\u0026: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":"gamah.splitclicker","Path":"ui/menupanel.razor.scss","FileName":"menupanel.razor.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"\r\nmenupanel\r\n{\r\n    position: absolute;\r\n    z-index: 1000;\r\n    pointer-events: all;\r\n    font-size: 12px;\r\n    flex-shrink: 0;\r\n\r\n    .background\r\n    {\r\n        position: absolute;\r\n        left: -5000px;\r\n        right: -5000px;\r\n        top: -5000px;\r\n        bottom: -5000px;\r\n    }\r\n\r\n    \u003E .inner\r\n    {\r\n        min-width: 200px;\r\n        min-height: 20px;\r\n        flex-direction: column;\r\n        font-family: Poppins;\r\n        font-weight: bold;\r\n        border-radius: 10px;\r\n        box-shadow: 5px 5px 30px #000e;\r\n        background-color: #2a2a2a;\r\n        flex-shrink: 0;\r\n\r\n        .spacer\r\n        {\r\n            height: 1px;\r\n            background-color: #0005;\r\n        }\r\n\r\n        .option\r\n        {\r\n            color: #fffa;\r\n            padding: 0px 8px;\r\n            cursor: pointer;\r\n            flex-shrink: 0;\r\n            height: 32px;\r\n\r\n            \u0026:first-child\r\n            {\r\n                border-top-left-radius: 10px;\r\n                border-top-right-radius: 10px;\r\n            }\r\n\r\n            \u0026:last-child\r\n            {\r\n                border-bottom-left-radius: 10px;\r\n                border-bottom-right-radius: 10px;\r\n            }\r\n\r\n            .icon\r\n            {\r\n                padding: 8px;\r\n                font-family: Material Icons;\r\n                justify-content: center;\r\n                align-items: center;\r\n                flex-shrink: 0;\r\n            }\r\n\r\n            .text\r\n            {\r\n                padding: 8px;\r\n                flex-shrink: 0;\r\n            }\r\n\r\n            \u0026:hover\r\n            {\r\n                background-color: #3472e6;\r\n                color: #f5f8fe;\r\n            }\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"gamah.splitclicker","Path":"ui/controls/color/colorhuecontrol.cs.scss","FileName":"colorhuecontrol.cs.scss","PackageType":"game","CodeKind":"Game","AssetVersionId":301560,"Code":"ColorHueControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, red, yellow, lime, cyan, blue, magenta, red );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tmargin: 10px 0;\r\n\tborder: 1px solid #333;\r\n\r\n\t\u0026:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t\u0026: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"}]}