ui/gamehud.razor.scss
// In-game HUD. Designed in 1080-reference space. The play square fills the full screen height and
// is centred horizontally, so the left/right letterbox is empty — the gear lives there (top-right).
@import "Theme.scss";
$teal-dark: #00262a; // letterbox / outside-arena colour (ClearColor)
// First ~0.1s punches the scale; the rest holds scale steady while fading away. Durations live on
// the .dismissing rules below — keep them in sync with GameStage.TUTORIAL_PROMPT_DISMISS_TIME.
@keyframes tutorial-instruction-dismiss {
0% { opacity: 1; transform: scale( 1 ); }
12% { opacity: 1; transform: scale( 1.1 ); }
25% { opacity: 1; transform: scale( 1 ); }
100% { opacity: 0; transform: scale( 1 ); }
}
@keyframes tutorial-instruction-backdrop-dismiss {
0%, 25% { background-color: rgba( $black, 0.6 ); }
100% { background-color: rgba( $black, 0 ); }
}
// "The help lives here" nudge: the character picker's .new-badge pulse (same colours, scale and
// 0.5s beat — see CharacterPicker.razor.scss) transplanted onto the square button. Both phases
// carry the square-button drop shadow so it never pops in/out mid-beat.
@keyframes character-help-button-pulse {
0%, 100% {
transform: scale( 0.94 );
background-color: #b86f6f;
border-color: #e7dfd2;
box-shadow: 6px 6px 0 $square-btn-shadow, 0 0 2px rgba( #c98a8a, 0.22 );
}
50% {
transform: scale( 1.08 );
background-color: #e2a2a2;
border-color: #fff3c4;
box-shadow: 6px 6px 0 $square-btn-shadow, 0 0 9px rgba( #e2a2a2, 0.62 );
}
}
@keyframes character-help-dismiss {
0% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes character-help-pose-primary {
0%, 44.444% { opacity: 1; }
50%, 94.444% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes character-help-pose-alternate {
0%, 44.444% { opacity: 0; }
50%, 94.444% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes character-help-marker-primary {
0%, 44.444% { opacity: 1; }
50%, 94.444% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes character-help-marker-alternate {
0%, 44.444% { opacity: 0; }
50%, 94.444% { opacity: 1; }
100% { opacity: 0; }
}
GameHud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
.top-buttons {
position: absolute;
top: 40px;
right: 40px;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
align-items: flex-start;
pointer-events: all;
// Only ever set during the character help overlay's auto-dismiss FADE — while the help panel is
// actually up the whole row is dropped from the tree (see GameHud.razor). Lifts the full row (the
// pulsing "?" included) above the fading overlay, and kills clicks for that beat so a press
// lands on the dismiss layer instead of a button the frozen sim can't act on yet.
&.help-open {
z-index: 3100;
pointer-events: none;
.hud-button {
pointer-events: none;
}
}
// Visuals come from the shared .square-btn (Theme.scss); this only spaces the row and
// colours the per-button accents.
.hud-button {
margin-left: 12px;
margin-bottom: 12px;
&.replay-save {
flex-direction: column;
.img {
margin-top: 3px;
}
.caption {
font-family: $font;
font-size: 13px;
color: $white;
margin-top: -2px;
}
&.saved {
border-color: rgba($white, 0.65);
&:hover {
background-color: rgba(255, 123, 123, 0.35);
border-color: #ff7b7b;
}
}
}
&.replay-favorite {
border-color: rgba($white, 0.75);
&.active {
border-color: #ff9b9b;
.img { background-tint: #ff9b9b; }
}
}
&.help {
position: relative;
// The "?" art spans the whole 64x64 face inside the 4px border (not the shared 40x40
// icon box) so it can share the square with the corner character icon.
.img {
width: 64px;
height: 64px;
}
// The character this button explains, tucked into the lower-left corner over the "?"
// glyph (the icon PNG leaves that corner clear). Same glow-behind-sprite stack as the
// help header. The clip box spans the button face so the baked glow oval can't spill
// past the border — the clip lives on this child, NOT the button, so the pulse's
// box-shadow is left alone.
.character-icon-clip {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
z-index: 1;
pointer-events: none;
}
// Corner anchor (left/bottom) is inline: HelpButtonIconStyle adds the per-character nudge.
.character-icon {
position: absolute;
pointer-events: none;
.icon-glow {
position: absolute;
left: -61%;
top: -60.5%;
width: 222%;
height: 221%;
z-index: 0;
pointer-events: none;
opacity: 0.3;
background-image: url( "sprites/ui/icon_glow.png" );
background-repeat: no-repeat;
background-size: 100% 100%;
}
.sprite-layer {
position: absolute;
z-index: 1;
pointer-events: none;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
image-rendering: pixelated;
}
&.paired {
.rear { transform: translate( 11%, -9% ) scale( 0.94 ); }
.front { transform: translate( -11%, 9% ); }
}
}
&.disabled {
cursor: blank-arrow;
pointer-events: none;
border-color: #727985;
background-color: rgba( $black, 0.35 );
.img { background-tint: #727985; }
.character-icon { opacity: 0.45; }
&:hover {
background-color: rgba( $black, 0.35 );
}
}
// Pulses until the player opens the help manually once (GameStage.ShouldPulseHelpButton).
// The icon tints dark for the whole run of the pulse — the face never returns to dark —
// so it stays readable without a lock-step tint animation.
&.pulsing {
transform-origin: 50% 50%;
animation: character-help-button-pulse 0.5s ease-in-out infinite;
.img { background-tint: $black; }
}
}
}
}
// Square button in the bottom-right letterbox that abandons the run/replay and returns to the
// title screen. Shared .square-btn visuals; the red accent marks it as a hard exit.
.quit-button {
position: absolute;
bottom: 40px;
right: 40px;
border-color: #ff7b7b;
.img { background-tint: #ff9b9b; }
}
// Square back button, sitting immediately left of the quit/home button (which is at right:40px,
// 72px wide). Returns to the launching menu rather than the title screen; keeps the neutral
// white accent so it doesn't read as a hard exit.
.back-button {
position: absolute;
bottom: 40px;
right: 124px;
}
// Hold-to-restart overlay: dims the game square while R is held during an ACTIVE run (post-death R
// restarts instantly and never shows this), with a small progress bar that fills over the hold.
// The whole overlay's opacity is driven per-frame from GameManager.RestartHoldAlpha (rapid fade in
// while held, fade back out from the current level on release) — no CSS animation here. It shows
// during LIVE gameplay, under the retro 240-grid quantize, so the bar is three nested plain rects
// (frame / trough / fill, no CSS borders) whose geometry GameHud computes on whole grid cells.
.restart-hold-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2000;
pointer-events: none;
// Dims ONLY the centred arena square (the sim keeps running under it), not the letterbox —
// same left-offset maths as the tutorial/help backdrops (ArenaSquareBackdropStyle).
.dim {
position: absolute;
top: 0;
width: 1080px;
height: 100%;
background-color: rgba( $black, 0.55 );
}
// "RESTARTING..." above the bar, fading with the whole overlay. Grid-snapped 81px pixel font
// (position comes from GameHud.RestartHoldTextStyle) so it stays crisp under the quantize.
.caption {
position: absolute;
font-family: $font;
font-smooth: never;
font-size: 81px;
color: $white;
white-space: nowrap;
}
.bar {
position: absolute;
background-color: $white; // the 1-cell frame, revealed around the inset trough
.inner {
position: absolute;
background-color: rgba( $black, 0.9 );
.fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: $white;
}
}
}
}
// Restart button, sitting immediately left of the back button (back is at right:124px, 72px wide;
// the restart button only appears after a player death in a normal run, where the back button is
// always present too). A counter-clockwise "replay" arrow with an R overlaid dead-centre.
.restart-button {
position: absolute;
bottom: 40px;
right: 208px;
// The R is laid over the arrow, centred within the full button rect.
.letter {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
font-family: $font;
font-size: 18px;
color: $white;
pointer-events: none;
}
}
// Full-screen transparent hit-catcher, present only while the timeline is being scrubbed. It exists
// so the mouse-release is processed wherever the cursor is (the arena / letterbox have no panel, so
// without it the release isn't seen and the drag sticks). Sits behind the transport panel so it
// never intercepts the controls themselves.
.scrub-catcher {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: all;
}
// Editor test-play exit: a square button at the bottom-RIGHT of the LEFT letterbox (just left of
// the centred play square — same spot as the editor's Test Play button). Anchored to screen centre
// so it hugs the square edge on any aspect: right:50% puts its right edge at centre, then
// margin-right pushes it left past the square half-width (540) + a gap. Back = BACKSPACE also exits.
.test-exit-button {
position: absolute;
bottom: 40px;
right: 50%;
// 580 = square half-width (540) + 40, so the gap to the square edge matches the 40px bottom inset.
margin-right: 580px;
border-color: #ff7b7b;
.img { background-tint: #ff9b9b; }
}
// Square reload-characters button in the bottom-right of the LEFT letterbox (same screen-centre
// anchor as the test-exit button, 40px inset from the square edge to match the bottom inset). Runs
// the reload_characters ConCmd. In live play it sits in the very corner (bottom:40px); in an editor
// test run the exit button owns that corner, so `.above-exit` lifts it one button-height above it.
.reload-characters-button {
position: absolute;
bottom: 40px;
right: 50%;
margin-right: 580px;
border-color: #c79bff;
&.above-exit { bottom: 124px; }
.img { background-tint: #d9bcff; }
}
// Replay-mode transport panel, shown instead of the gear while watching a recorded run. Lives in
// the LEFT letterbox (outside the play square, which fills the screen height and is centred), so
// it never covers the arena on standard wide aspect ratios.
// Left-letterbox dock holding the replay transport panel and (beneath it) the HIJACK button, so the
// button always sits centred right under the variable-height panel.
.replay-dock {
z-index: 2; // above the arena-only reaction placement surface
position: absolute;
top: 40px;
left: 32px;
width: 320px;
flex-direction: column;
align-items: stretch;
pointer-events: none; // children opt back in
}
.replay-panel {
flex-direction: column;
align-items: stretch;
padding: 18px 18px 16px 18px;
pointer-events: none; // children opt back in
background-color: rgba($black, 0.62);
border: 3px solid rgba($white, 0.85);
font-size: 22px;
color: $white;
font-family: $font; // pixel font for ALL panel text (icons override with Material Icons)
.reaction-status {
font-size: 18px;
color: #dddd99;
margin-top: 10px;
}
// The level being watched, shown at the top of the panel. Tag (map ordinal) in a muted colour
// before the white name, mirroring the level-select header.
.replay-level {
flex-direction: row;
align-items: center;
gap: 5px;
margin-bottom: 12px;
.tag {
font-size: 26px;
color: rgba(#8a97a5, 0.65);
}
.name {
font-size: 26px;
color: $white;
}
}
// Who set this run: avatar + name, with the submission date and country underneath.
.submitter {
flex-direction: row;
align-items: center;
margin-bottom: 14px;
// 48px face comes from the component's Size; click opens the Steam profile.
SteamAvatar {
margin-right: 12px;
border: 2px solid rgba($white, 0.6);
}
.who {
flex-direction: column;
justify-content: center;
flex-grow: 1;
.nameline {
flex-direction: row;
align-items: center;
.name {
font-size: 20px;
color: $white;
}
// Country flag (flagcdn 4:3 image) sitting beside the name.
.flag {
pointer-events: all;
// cover+center comes from FlagSheet.Style's inline background-size/position
// (flags live in one CSS-sprite sheet); box size feeds FlagSheet.Style( code, 28, 20 ).
width: 28px;
height: 20px;
margin-left: 10px;
flex-shrink: 0;
border: 1px solid rgba($black, 0.8);
}
}
.meta {
margin-top: 2px;
font-size: 15px;
color: rgba($white, 0.6);
}
}
}
.replay-view-counts {
flex-direction: column;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 2px solid rgba($white, 0.22);
font-size: 20px;
color: rgba($white, 0.8);
// Keep the empty row mounted so loading the counts cannot move the controls below.
.counts-row { width: 100%; height: 24px; flex-shrink: 0; flex-direction: row; gap: 20px; }
.count { height: 24px; flex-shrink: 0; align-items: center; gap: 8px; pointer-events: all; }
.reaction-count { margin-left: auto; }
label { height: 24px; line-height: 24px; white-space: nowrap; pointer-events: none; }
.icon { font-family: "Material Icons"; font-size: 22px; flex-shrink: 0; pointer-events: none; }
}
// Title row: the "▶ REPLAY" / "■ FINISHED" / "● HIJACKED" label, plus (when hijacked) the
// "PRACTICE RUN" note on the same horizontal line, right after it.
.title-row {
flex-direction: row;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 14px;
}
.title {
justify-content: center;
font-size: 26px;
color: #ff5b5b; // record-red accent
// Live-control (hijack) state: a distinct green accent so it never reads as plain playback.
&.hijack { color: #5bff8b; }
}
// "PRACTICE RUN" note shown beside the title while hijacked.
.hijack-note {
justify-content: center;
text-align: center;
font-size: 16px;
color: rgba($white, 0.5);
// The pixel font's tall line-box sits the smaller note glyphs high; nudge down to visually
// centre it against the larger title on the same row.
margin-top: 5px;
}
.speed-head {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 2px;
.caption { color: rgba($white, 0.7); font-size: 18px; }
.value {
font-family: $font-mono;
color: $white;
&.slow { color: #6bd0ff; }
&.fast { color: #ffd23f; }
}
}
.speed-slider {
pointer-events: all; // draggable
height: 6px;
}
.ends {
flex-direction: row;
justify-content: space-between;
margin-top: 4px;
margin-bottom: 14px;
color: rgba($white, 0.5);
font-size: 15px;
font-family: $font-mono;
}
.ctl {
flex-direction: row;
align-items: center;
pointer-events: all;
cursor: blank-hand;
margin-bottom: 8px;
padding: 8px 12px;
background-color: rgba($black, 0.7);
border: 2px solid rgba($white, 0.7);
color: $white;
transition: background-color 0.1s ease;
.icon {
font-family: "Material Icons";
font-size: 26px;
margin-right: 10px;
}
label { color: $white; }
&:hover { background-color: rgba($black, 0.95); }
&.disabled {
cursor: none;
border-color: #667075;
color: #667075;
background-color: rgba($black, 0.7);
label, .icon { color: #667075; }
}
// STOP (end hijack) — amber accent, distinct from the red BACK/exit.
&.stop {
border-color: #ffc24b;
label, .icon { color: #ffd584; }
}
}
// Scrubbable replay timeline (ReplayScrubBar self-styles its internals; this just spaces it).
.timeline {
margin-top: 8px;
}
.clock-row {
position: relative;
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin-top: 8px;
.clock {
justify-content: flex-start;
font-size: 18px;
font-family: $font-mono;
color: rgba($white, 0.85);
}
// Frame counter pinned to the right edge of the row, right-justified, while the clock stays
// centred under the progress bar.
.frame {
position: absolute;
right: 0;
justify-content: flex-end;
font-size: 18px;
font-family: $font-mono;
color: rgba($white, 0.85);
}
}
}
// Per-tick input readout in the bottom-left letterbox, arrow-key layout (U on top, L/D/R below).
// Each square brightens while its direction is held on the current replayed tick.
.input-display {
position: absolute;
bottom: 40px;
left: 32px;
flex-direction: column;
align-items: center;
pointer-events: none;
.row {
flex-direction: row;
}
.key {
width: 64px;
height: 64px;
margin: 3px;
justify-content: center;
align-items: center;
font-size: 30px;
color: rgba($white, 0.25);
background-color: rgba($black, 0.5);
border: 3px solid rgba($white, 0.2);
opacity: 0.2;
&.on {
color: $white;
background-color: rgba($black, 0.9);
border-color: $white;
opacity: 1;
}
// Flashes on the single tick the press fires (rising edge). A bright cyan accent +
// glow makes frame-precise taps (jump/dash/wall-kick timing) pop out from a plain hold.
&.just {
color: #6bd0ff;
border-color: #6bd0ff;
background-color: rgba(107, 208, 255, 0.35);
box-shadow: 0 0 16px 2px rgba(107, 208, 255, 0.8);
}
}
}
// Replay actions sit below the left control panel; green matches the HIJACKED title.
.hijack-btn {
align-self: center; // centred under the replay panel
margin-top: 12px;
flex-direction: row;
align-items: center;
justify-content: center;
pointer-events: all;
cursor: blank-hand;
padding: 10px 16px;
background-color: rgba($black, 0.7);
border: 3px solid #5bff8b;
color: #5bff8b;
font-family: $font; // pixel font for the label (icon overrides with Material Icons)
transition: background-color 0.1s ease;
.icon {
font-family: "Material Icons";
font-size: 28px;
margin-right: 10px;
color: #5bff8b;
}
label {
font-size: 22px;
color: #5bff8b;
}
&:hover { background-color: rgba($black, 0.95); }
&.disabled {
cursor: none;
border-color: #667075;
color: #667075;
background-color: rgba($black, 0.7);
.icon, label { color: #667075; }
}
&.react-btn {
border-color: #e7ba72;
color: #e7ba72;
.icon, label { color: #e7ba72; }
}
}
// Big "THE END" card centred over the arena once a replay has played out. Full-screen, but the
// play square fills screen HEIGHT and is centred, so a centred child lands on the arena centre.
// Cosmetic only (pointer-events:none) — the transport panel still owns restart/back.
.end-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
.end-text {
font-family: $font;
font-size: 200px;
color: rgba($white, 0.3);
text-align: center;
text-shadow: 6px 6px 0 rgba($black, 0.25);
}
// "PLAYBACK DESYNCED" readout under THE END. Shows under the 240-grid quantize (the retro
// shader keeps quantizing at replay end), so like the tutorial popup every metric is a whole
// number of grid cells: 81px = the 18-cell em (one art pixel per cell), and the box + per-line
// indents are snapped onto exact cells in code (GameHud.DesyncWarningStyle / GridLineStyle).
// No text-shadow — a soft shadow just quantizes to mud.
.desync-warning {
position: absolute;
font-family: $font;
font-smooth: never;
flex-direction: column;
align-items: flex-start;
// Fixed 18-cell stack pitch, top-anchored labels — same trick as .tutorial-instruction:
// the engine's natural line box isn't exactly the em, and auto-height stacking would
// drift every following line half a cell off the grid.
.line {
height: 81px;
flex-direction: row;
align-items: flex-start;
}
label {
font-size: 81px;
color: rgba(245, 21, 21, 0.8); // record-red, matching the title accent
white-space: nowrap;
}
}
// Retry hint shown (instead of "THE END") when a hijacked practice run ends. Renders under the
// end-of-replay quantize like .desync-warning, so the same rules apply: 81px = the 18-cell
// grid em (one art pixel per cell), box + per-line indents snapped onto exact cells in code
// (GameHud.PracticeHintStyle / GridLineStyle). No text-shadow — it just quantizes to mud.
.practice-hint {
position: absolute;
font-family: $font;
font-smooth: never;
flex-direction: column;
align-items: flex-start;
// Fixed 18-cell stack pitch, top-anchored labels — see .desync-warning.
.line {
height: 81px;
flex-direction: row;
align-items: flex-start;
}
label {
font-size: 81px;
color: rgba($white, 0.85);
white-space: nowrap;
}
}
}
// Covers the whole screen; the centred panel sits over the arena square.
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
pointer-events: all;
background-color: rgba($teal-dark, 0.6);
.panel {
flex-direction: column;
align-items: center;
justify-content: center;
// Positioned + clipped so the absolutely-filled OptionsBackdrop (drifting blocks) anchors to
// THIS square panel — not the full-screen .overlay — and its blocks stay square and confined.
position: relative;
overflow: hidden;
// Square (not a tall rectangle): the option rows are ~800px wide and the full list a touch
// taller, so lock a fixed square comfortably larger than the content and let the flex
// centring sit the rows in the middle of the checkerboard field.
width: 880px;
height: 880px;
padding: 48px 56px;
// Redraw the main-menu options look in UI space (the arena is paused behind us): the same
// dithered teal checkerboard field the menu shows as the live world. The world maps the
// 240px texture 1:1 onto the 240-unit arena that fills the 1080-ref screen height, i.e.
// 4.5px per texture cell -> background-size 1080px keeps the identical cell scale, tiled to
// fill the panel and point-sampled so the dither stays crisp. $teal-dark backs it as a fallback.
background-color: $teal-dark;
background-image: url( "sprites/background.png" );
background-repeat: repeat;
background-size: 1080px 1080px;
image-rendering: pixelated;
border: 6px solid $white;
}
}
// Leave-daily-run confirm (GameStage.LeaveConfirmOpen): the options overlay's dim with a compact
// dialog in the ReplayCodeDialog style. .selected mirrors :hover so keyboard/controller focus
// reads the same as the mouse. Above the hold-to-restart dim, below the character help.
.leave-confirm {
z-index: 2500;
.dialog {
width: 820px;
flex-direction: column;
align-items: center;
padding: 36px;
background-color: #024a54;
border: 6px solid $white;
font-family: $font;
font-smooth: never;
}
.title {
font-size: 48px;
color: $white;
text-shadow: 3px 3px 0 $black;
margin-bottom: 20px;
}
.attempts {
font-size: 28px;
color: #ff9b9b;
}
.buttons {
flex-direction: row;
justify-content: center;
margin-top: 28px;
button {
min-width: 260px;
height: 58px;
justify-content: center;
align-items: center;
margin: 0 10px;
font-size: 28px;
color: $black;
background-color: $white;
cursor: blank-hand;
pointer-events: all;
// Always-present transparent border so the controller focus ring below never resizes
// the button when it appears.
border: 4px solid transparent;
&:hover { background-color: #dddd99; }
&.selected { background-color: #dddd99; }
&.leave {
background-color: #c98a8a;
&:hover { background-color: #d99a9a; }
&.selected { background-color: #d99a9a; }
}
}
// Gamepad focus ring: a hard white frame with a dark inset so it reads on both button tints.
&.controller button.selected {
border-color: $white;
box-shadow: inset 0 0 0 3px $black;
}
}
}
.character-help-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 3000;
pointer-events: none;
font-family: $font;
font-smooth: never;
.dismiss-layer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
pointer-events: all;
cursor: blank-hand;
&.fading {
animation: character-help-dismiss 1.1s linear forwards;
}
}
.arena-backdrop {
position: absolute;
top: 0;
width: 1080px;
height: 100%;
background-color: rgba( $black, 0.78 );
}
.help-panel {
position: relative;
width: 820px;
max-width: 90%;
max-height: 930px;
padding: 40px 48px 44px 48px;
flex-direction: column;
align-items: stretch;
pointer-events: all;
cursor: blank-arrow;
background-color: $black;
border: 6px solid $white;
box-shadow: 0 12px 40px rgba( #000000, 0.55 );
.help-header {
height: 128px;
flex-direction: row;
align-items: center;
justify-content: center;
border-bottom: 4px solid rgba( $white, 0.35 );
margin-bottom: 28px;
.help-character {
position: relative;
flex-shrink: 0;
margin-right: 44px;
.icon-glow {
position: absolute;
left: -61%;
top: -60.5%;
width: 222%;
height: 221%;
z-index: 0;
pointer-events: none;
opacity: 0.3;
background-image: url( "sprites/ui/icon_glow.png" );
background-repeat: no-repeat;
background-size: 100% 100%;
}
.sprite-layer {
position: absolute;
z-index: 1;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
image-rendering: pixelated;
}
&.paired {
.rear { transform: translate( 11%, -9% ) scale( 0.94 ); }
.front { transform: translate( -11%, 9% ); }
}
}
.character-name {
font-size: 64px;
color: $white;
white-space: nowrap;
}
}
.ability-list {
flex-direction: column;
align-items: stretch;
}
.ability-row {
min-height: 116px;
padding: 14px 8px;
flex-direction: column;
align-items: center;
border-bottom: 3px solid rgba( $white, 0.16 );
&:last-child {
border-bottom: 0;
}
.ability-name {
width: 100%;
font-size: 32px;
color: $white;
margin-bottom: 8px;
text-align: center;
}
.instruction {
width: 100%;
min-width: 0;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
.row-illustration {
position: relative;
width: 60px;
height: 60px;
margin-right: 18px;
flex-shrink: 0;
.icon-glow {
position: absolute;
left: -61%;
top: -60.5%;
width: 222%;
height: 221%;
z-index: 0;
pointer-events: none;
opacity: 0.3;
background-image: url( "sprites/ui/icon_glow.png" );
background-repeat: no-repeat;
background-size: 100% 100%;
}
.pose {
position: absolute;
left: 0;
top: 0;
width: 60px;
height: 60px;
z-index: 1;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
image-rendering: pixelated;
&.alternating.primary {
animation: character-help-pose-primary 1.8s linear infinite;
}
&.alternating.alternate {
animation: character-help-pose-alternate 1.8s linear infinite;
}
}
}
.direction-keys {
flex-direction: row;
flex-shrink: 0;
margin-right: 16px;
&.terminal {
margin-right: 0;
.direction-key:last-child, .button-key:last-child {
margin-right: 0;
}
}
&.compact {
.direction-key {
margin-right: -8px;
}
// A face button following a compact (overlapped) chord must not be overlapped
// itself — restore a normal gap after the chord's -8px pull.
.button-key {
margin-left: 8px;
}
}
&.terminal.compact {
.direction-key:last-child, .button-key:last-child {
margin-right: 0;
}
}
.direction-key {
width: 42px;
height: 42px;
margin-right: 6px;
flex-shrink: 0;
background-repeat: no-repeat;
background-size: 100% 100%;
image-rendering: pixelated;
// One baked keycap per direction (tools/bake.py, bake_arrow_keys) instead of
// rotating a single image: the cap is lit from above, so rotating it carried
// its shaded bottom edge round to the side. Only the arrow turns. Every key
// gets one of these four classes (see CharacterHelpDirectionClass).
&.up { background-image: url( "sprites/ui/arrow_key_up.png" ); }
&.right { background-image: url( "sprites/ui/arrow_key_right.png" ); }
&.down { background-image: url( "sprites/ui/arrow_key_down.png" ); }
&.left { background-image: url( "sprites/ui/arrow_key_left.png" ); }
}
// Controller face-button glyphs, shown in place of keyboard patterns while a
// controller is in use: X (blue) fires the scripted double tap, Y (yellow) the
// all-four-directions chord, A (green) is jump. Drawn in CSS (round face + letter) so no baked art
// is needed; sized to sit beside the 42px keycaps.
.button-key {
width: 42px;
height: 42px;
margin-right: 6px;
flex-shrink: 0;
border-radius: 21px;
font-size: 30px;
line-height: 40px;
text-align: center;
justify-content: center;
// The pixel font's letter box carries a right-side advance gap, which reads as
// the glyph sitting left of the circle's centre — nudge it back.
padding-left: 1px;
&.x {
background-color: #2f5faf;
border: 3px solid #6b9be8;
color: #e8f0ff;
}
&.y {
background-color: #d8ac10;
border: 3px solid #f2d95e;
color: #453400;
}
&.a {
background-color: #2f8a3c;
border: 3px solid #6fcc7a;
color: #e6ffe9;
}
}
}
.direction-separator {
font-size: 32px;
line-height: 42px;
color: rgba( $white, 0.82 );
margin: 0 8px 0 2px;
&.comma {
font-size: 42px;
margin: 0 9px 0 -2px;
}
}
.teleport-controls {
flex-direction: row;
align-items: center;
flex-shrink: 0;
.teleport-origin-chords {
flex-direction: row;
.teleport-origin-chord:first-child {
margin-right: 6px;
}
}
.direction-separator {
flex-shrink: 0;
}
}
.thread-controls {
flex-direction: row;
align-items: center;
flex-shrink: 0;
.thread-chord:first-child {
margin-right: 14px;
}
}
.direction-prefix {
font-size: 27px;
color: rgba( $white, 0.82 );
margin-right: 1px;
white-space: nowrap;
}
.row-character-sprites {
flex-direction: row;
align-items: center;
flex-shrink: 0;
&.compact {
.row-character-preview {
margin-right: -12px;
&:last-child {
margin-right: 12px;
}
}
}
&.overlap {
.row-character-preview {
margin-right: -42px;
&:last-child {
margin-right: 0;
}
}
}
.row-character-preview {
position: relative;
margin-right: 10px;
flex-shrink: 0;
.icon-glow {
position: absolute;
left: -61%;
top: -60.5%;
width: 222%;
height: 221%;
z-index: 0;
pointer-events: none;
opacity: 0.3;
background-image: url( "sprites/ui/icon_glow.png" );
background-repeat: no-repeat;
background-size: 100% 100%;
}
.row-character-sprite {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
image-rendering: pixelated;
}
.control-marker {
position: absolute;
left: 50%;
top: -8px;
width: 20px;
height: 8px;
margin-left: -10px;
z-index: 2;
pointer-events: none;
&.alternating.primary {
animation: character-help-marker-primary 1.8s linear infinite;
}
&.alternating.alternate {
animation: character-help-marker-alternate 1.8s linear infinite;
}
.bar {
position: absolute;
left: 0;
top: 0;
width: 20px;
height: 4px;
background-color: $white;
}
.tip {
position: absolute;
left: 8px;
top: 4px;
width: 4px;
height: 4px;
background-color: $white;
}
}
}
}
// Gesture note: sits on the input line right after the keys, dimmer and smaller than
// the hint below so it reads as an aside rather than an instruction.
.gesture-note {
flex-direction: row;
align-items: center;
font-size: 21px;
color: rgba( $white, 0.38 );
margin-left: 12px;
white-space: nowrap;
flex-shrink: 0;
.gesture-alternative {
margin-left: 6px;
}
.gesture-glyph {
width: 28px;
height: 28px;
margin: 0 4px;
flex-shrink: 0;
opacity: 0.7;
}
}
.hint {
width: 100%;
min-width: 0;
margin-top: 4px;
font-size: 23px;
color: rgba( $white, 0.62 );
white-space: normal;
text-align: center;
&.extra-top-padding {
padding-top: 6px;
}
}
}
}
}
}
.tutorial-instruction-dismiss {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 3000;
pointer-events: all;
align-items: center;
justify-content: center;
// Dims the 240px game screen (the centred 1080-ref arena square) behind the popup — NOT the
// letterbox, so the click-to-dismiss container above stays transparent. Left edge comes from
// GameHud.TutorialBackdropStyle() (aspect-dependent).
.backdrop {
position: absolute;
top: 0;
width: 1080px;
height: 100%;
background-color: rgba( $black, 0.6 );
}
// 0.4s = GameStage.TUTORIAL_PROMPT_DISMISS_TIME. The sim is already unfrozen for this fade —
// the hold completing hands input back immediately (see GameStage.BlocksSimulation).
&.dismissing {
.backdrop {
animation: tutorial-instruction-backdrop-dismiss 0.4s linear forwards;
}
.tutorial-instruction,
.hold-bar {
animation: tutorial-instruction-dismiss 0.4s ease-out forwards;
}
}
// Hold-to-dismiss progress bar floating above the popup box: frame / trough / fill as three
// nested plain rects, same construction as the hold-to-restart bar. All geometry comes
// grid-snapped in absolute px from GameHud's TutorialHold*Style helpers so it stays crisp
// under the quantize.
.hold-bar {
position: absolute;
background-color: $white; // the 1-cell frame, revealed around the inset trough
.inner {
position: absolute;
background-color: rgba( $black, 0.9 );
.fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: $white;
}
}
}
// Shown DURING gameplay, i.e. under the 240-grid quantize pass — so unlike the level select's
// popup twin, every metric here must be a whole number of grid cells (1 cell = 4.5px in this
// 1080-ref space) or the text mushes. Position + size come from GameHud.TutorialPopupStyle(),
// which snaps the box onto exact cell boundaries (flex centring would land it between cells);
// keep these paddings/borders in sync with its POPUP_*_CELLS constants. No soft box-shadow:
// a smooth glow just quantizes into muddy rings.
.tutorial-instruction {
position: absolute;
padding: 9px 18px; // 2 / 4 cells
border: 9px solid $white; // 2 cells
background-color: $black;
pointer-events: all;
font-family: $font;
font-smooth: never;
// One .line row per prompt line, stacked; each line is indented onto whole cells by
// TutorialLineStyle (NOT centred here — flex centring can land on half-cell offsets).
flex-direction: column;
align-items: flex-start;
// The engine's natural label line box is NOT exactly the 81px em, so auto-height
// stacking drifts every following line off the grid (≈half a cell per line: the
// crisp/blurry/crisp pattern). Fix the stack pitch at exactly 18 cells and top-anchor
// the label, so every line renders with the same on-grid offset as the first.
.line {
height: 81px;
flex-direction: row;
align-items: flex-start;
}
label {
// 81px = 18-cell em: m6x11plus draws each art pixel as exactly one grid cell.
font-size: 81px;
color: $white;
white-space: nowrap;
}
}
}
}