:root {
    --bg: #05060a;
    --text: #c8d2de;
    --bright: #f4f8fc;
    --dim: #6b7a8d;
    --accent: #ff8a3d;
    --waypoint: #4fd1c5;
    --danger: #ff4d5e;
    --panel-bg: rgba(8, 12, 20, 0.62);
    /* Opaque, for controls that float over the play area and have to stay
       legible against whatever is behind them. */
    --panel-solid: #0e141d;
    --panel-border: rgba(120, 150, 190, 0.18);
}

* {
    box-sizing: border-box;
}

html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: var(--bg);
    color: var(--text);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    -webkit-font-smoothing: antialiased;
    /* Dragging the joystick must not scroll, zoom, pull-to-refresh, or select
       HUD text. */
    touch-action: none;
    overscroll-behavior: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
}

#canvas {
    display: block;
    width: 100vw;
    height: 100vh;
    /* Dynamic units so iOS Safari's collapsing URL bar cannot push the bottom
       of the canvas off-screen. */
    width: 100dvw;
    height: 100dvh;
}

#hud {
    position: fixed;
    inset: 0;
    pointer-events: none;
    user-select: none;
}

.panel {
    position: absolute;
    padding: 10px 12px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    letter-spacing: 0.08em;
}

.panel .heading {
    margin: -2px 0 7px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--panel-border);
    color: var(--accent);
    font-size: 10px;
    letter-spacing: 0.22em;
    text-transform: uppercase;
}

.panel .row {
    display: flex;
    justify-content: space-between;
    gap: 18px;
    line-height: 1.7;
}

.panel .label,
.panel .desc {
    color: var(--dim);
}

.panel .value {
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

#stats {
    top: 16px;
    left: 16px;
    min-width: 190px;
}

#stats #fuel {
    color: var(--accent);
}

#levelPanel {
    top: 16px;
    right: 16px;
    min-width: 190px;
    pointer-events: auto;
}

#levelPanel .nav {
    display: flex;
    gap: 4px;
    margin-bottom: 6px;
}

#levelPanel button {
    flex: 0 0 auto;
    width: 22px;
    padding: 0;
    background: rgba(0, 0, 0, 0.5);
    border: 1px solid var(--panel-border);
    border-radius: 3px;
    color: var(--text);
    font-family: inherit;
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
}

#levelPanel button:hover {
    border-color: var(--accent);
    color: var(--accent);
}

#levelSelect {
    flex: 1 1 auto;
    min-width: 0;
    padding: 4px 6px;
    background: rgba(0, 0, 0, 0.5);
    color: var(--text);
    border: 1px solid var(--panel-border);
    border-radius: 3px;
    font-family: inherit;
    font-size: 12px;
    letter-spacing: 0.06em;
}

#levelSelect:focus {
    outline: 1px solid var(--accent);
}

#help {
    bottom: 16px;
    left: 16px;
    min-width: 190px;
}

/* Dim the key list, but not the heading -- it should match the other two. */
#help .row {
    opacity: 0.72;
}

#help .key {
    color: var(--text);
}

#badges {
    position: absolute;
    bottom: 16px;
    right: 16px;
    display: flex;
    gap: 8px;
}

.badge {
    display: none;
    padding: 5px 9px;
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    color: var(--accent);
    letter-spacing: 0.12em;
}

.badge.visible {
    display: block;
}

/* The outcome lands wherever the ship happened to end up, usually on top of a
   planet, so it gets an opaque dialog rather than bare text. */
#status {
    position: absolute;
    z-index: 3;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    visibility: hidden;
    transition: opacity 200ms ease-out, visibility 200ms;
}

#status.visible {
    opacity: 1;
    visibility: visible;
}

#status .dialog {
    min-width: 330px;
    padding: 22px 34px 24px;
    background: rgba(6, 9, 15, 0.97);
    border: 1px solid var(--panel-border);
    border-top: 2px solid var(--dim);
    border-radius: 5px;
    box-shadow: 0 24px 70px rgba(0, 0, 0, 0.75);
    text-align: center;
    /* A full leaderboard plus the buttons can outgrow a short window -- a phone in
       landscape, or a desktop window dragged down to nothing. Laid out as a column
       so that the board is the only part that scrolls: whatever the height, the
       score, the title and above all the buttons that leave stay put. Needs the
       pointer back, since #hud gives it up for everything by default. */
    display: flex;
    flex-direction: column;
    max-height: calc(100% - 32px);
    overflow: hidden;
    pointer-events: auto;
}

#status .title {
    font-size: 22px;
    letter-spacing: 0.26em;
    text-transform: uppercase;
    color: var(--text);
}

#status .sub {
    margin-top: 11px;
    font-size: 12px;
    letter-spacing: 0.13em;
    color: var(--dim);
}

#status .sub.record {
    color: var(--accent);
}

#status .sub .keys {
    color: var(--text);
}

#status.crashed .dialog {
    border-top-color: var(--danger);
}

#status.crashed .title {
    color: var(--danger);
}

#status.lost .dialog {
    border-top-color: var(--accent);
}

#status.lost .title {
    color: var(--accent);
}

#status.complete .dialog {
    border-top-color: var(--waypoint);
}

#status.complete .title {
    color: var(--waypoint);
}

/* Passive text sits below the controls; see the z-index note by #actions. */
#hint {
    position: absolute;
    z-index: 1;
    bottom: 16%;
    left: 0;
    width: 100%;
    /* Keeps clear of the side panels on a narrow window: the text wraps
       rather than running underneath them. */
    padding: 0 232px;
    text-align: center;
    letter-spacing: 0.24em;
    color: var(--dim);
    opacity: 0;
    transition: opacity 220ms ease-out;
}

/* The instruction itself peaks at near-white, so the pulse actually reads as an
   invitation rather than a dim label. The line under it stays muted. */
#hint .primary {
    color: var(--bright);
}

#hint.visible {
    opacity: 1;
    animation: pulse 2.4s ease-in-out infinite;
}

#hint .sub {
    margin-top: 9px;
    font-size: 11px;
    letter-spacing: 0.18em;
    opacity: 0.8;
}

/* Touch controls ---------------------------------------------------------- */

/* Hidden until we know the pointer is coarse or a touch has actually arrived,
   so a desktop never shows thumb controls. */
#views,
#actions {
    display: none;
}

.pointer-touch {
    display: none;
}

/* Everything in the HUD is absolutely positioned inside one parent, so paint
   order would otherwise just be DOM order. Layering it explicitly: passive text
   at 1, controls above it at 2, the outcome dialog over everything at 3. */
#actions {
    position: absolute;
    z-index: 2;
    right: 14px;
    bottom: 14px;
    flex-direction: column;
    gap: 10px;
    pointer-events: auto;
}

.action {
    width: 46px;
    height: 46px;
    padding: 0;
    background: var(--panel-solid);
    border: 1px solid var(--panel-border);
    border-radius: 50%;
    color: var(--text);
    font-family: inherit;
    font-size: 17px;
    line-height: 1;
    cursor: pointer;
}

.action.on {
    border-color: var(--accent);
    color: var(--accent);
}

/* Press-and-hold fast-forward, bottom left so the left thumb works it while the
   right drives the joystick. Text glyph rather than an emoji codepoint, which
   iOS renders as a colour Apple emoji. */
#boostButton {
    display: none;
    position: absolute;
    z-index: 2;
    left: 16px;
    bottom: 16px;
    width: 68px;
    height: 68px;
    padding: 0;
    background: var(--panel-solid);
    border: 1px solid var(--panel-border);
    border-radius: 50%;
    color: var(--text);
    font-family: inherit;
    font-size: 24px;
    line-height: 1;
    pointer-events: auto;
    cursor: pointer;
}

#boostButton.on {
    border-color: var(--accent);
    color: var(--accent);
    background: rgba(255, 138, 61, 0.12);
}

body.touch #boostButton {
    display: block;
}

#views {
    position: absolute;
    z-index: 2;
    right: 70px;
    bottom: 14px;
    flex-direction: column;
    align-items: flex-end;
    gap: 7px;
    pointer-events: auto;
}

#views.open {
    display: flex;
}

.toggle {
    padding: 9px 13px;
    background: var(--panel-solid);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    color: var(--dim);
    font-family: inherit;
    font-size: 12px;
    letter-spacing: 0.08em;
    white-space: nowrap;
    cursor: pointer;
}

.toggle.on {
    border-color: var(--accent);
    color: var(--accent);
}

/* Buttons in the outcome dialog are the only way to retry or advance without a
   keyboard, so they are always present -- a mouse can use them too. */
#status .buttons {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 18px;
    pointer-events: auto;
}

#status .buttons button {
    padding: 11px 20px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    color: var(--text);
    font-family: inherit;
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    cursor: pointer;
}

#status .buttons button:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* The upload is the one action in the dialog that cannot be taken later, so it
   is the only button that reads as primary. */
#status .buttons button.primary {
    border-color: var(--waypoint);
    color: var(--waypoint);
}

#status .buttons button.primary:hover {
    background: rgba(79, 209, 197, 0.12);
}

#status .buttons button:disabled {
    color: var(--dim);
    cursor: default;
}

#status .buttons button:disabled:hover {
    border-color: var(--panel-border);
    color: var(--dim);
}

/* Leaderboard -------------------------------------------------------------- */

/* The controls that only mean something with a backend behind them. Hidden until
   the probe in js/sketch.js answers, so a static build with no leaderboard says
   nothing about one rather than offering a key that does nothing. */
.needs-leaderboard {
    display: none;
}

body.has-leaderboard #help .row.needs-leaderboard {
    display: flex;
}

/* Touch only, like the rest of #actions. */
body.has-leaderboard.touch #actions .needs-leaderboard {
    display: block;
}

/* Ruled off from the score above it: everything below the line leaves the
   machine, and everything above it already happened. */
#status .upload {
    margin-top: 18px;
    padding-top: 16px;
    border-top: 1px solid var(--panel-border);
    pointer-events: auto;
}

#status .upload .caption {
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--dim);
}

#status .upload.failed {
    color: var(--danger);
}

/* The reason, when there is one worth reading -- a timeout or a rejection. */
#status .upload .why {
    display: block;
    margin-top: 5px;
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: none;
    color: var(--dim);
}

#status .group {
    display: flex;
    gap: 6px;
    margin-top: 9px;
}

#status .field {
    flex: 1 1 auto;
    min-width: 0;
    padding: 8px 10px;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    color: var(--bright);
    font-family: inherit;
    font-size: 13px;
    letter-spacing: 0.08em;
    text-align: center;
}

#status .field:focus {
    outline: none;
    border-color: var(--accent);
}

#status .mini {
    flex: 0 0 auto;
    width: 34px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid var(--panel-border);
    border-radius: 4px;
    color: var(--dim);
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;
}

#status .mini:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* What is about to be sent, spelled out next to the button that sends it. */
#status .preview {
    display: flex;
    justify-content: center;
    gap: 18px;
    margin-top: 12px;
}

#status .preview .k {
    margin-right: 6px;
    font-size: 10px;
    letter-spacing: 0.14em;
    color: var(--dim);
}

#status .preview .v {
    font-size: 12px;
    color: var(--text);
}

#status .board {
    display: flex;
    flex-direction: column;
    /* Without this a flex item refuses to shrink below its content, and the board
       would push the buttons out of the dialog instead of scrolling. */
    min-height: 0;
    margin-top: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--panel-border);
}

#status .board .rows {
    min-height: 0;
    overflow-y: auto;
}

#status .board table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    text-align: left;
}

#status .board td {
    padding: 3px 0;
    color: var(--dim);
    /* Zalgo is trimmed rather than stripped in js/names.js, since combining
       marks are load-bearing in plenty of scripts. Two marks still stack taller
       than a line, so a row clips instead of climbing over the one above it. */
    overflow: hidden;
}

#status .board .rank {
    width: 2.4em;
    text-align: right;
    padding-right: 12px;
}

/* The one column that can be any length, so it is the one that gives way. */
#status .board .name {
    max-width: 0;
    color: var(--text);
    white-space: nowrap;
    text-overflow: ellipsis;
}

#status .board .fuel,
#status .board .time {
    width: 4.6em;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

#status .board .fuel {
    color: var(--text);
}

#status .board tr.mine td {
    color: var(--accent);
}

/* Where the ranks skip, because the player placed below the top of the board
   and arrived with a couple of neighbours. */
#status .board tr.gap td {
    padding: 1px 0;
    /* Dimmer than a name, brighter than the rules: at panel-border it read as an
       accidental blank line rather than as a deliberate omission. */
    color: rgba(120, 150, 190, 0.5);
    text-align: center;
    letter-spacing: 0.3em;
}

#status .board .place {
    margin-top: 12px;
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent);
}

/* Level editor ------------------------------------------------------------- */

/* Only ever visible while editing, which takes a keypress, so there is nothing
   further to do about keeping these off a phone. */
/* Matched widths, so the field between them reads as centered -- it is centered
   in the window rather than between the panels, so unequal ones would throw it
   visibly off. The left side is a column of three, which scrolls as one unit on
   a short window. */
#editorPanels,
#editorInspector {
    display: none;
    position: absolute;
    top: 16px;
    width: 250px;
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
    pointer-events: auto;
}

#editorPanels {
    left: 16px;
    flex-direction: column;
    gap: 10px;
}

/* The column places them, so they give up the absolute corners .panel assumes. */
#editorPanels > .panel {
    position: static;
    flex: 0 0 auto;
}

#editorInspector {
    right: 16px;
}

body.editing #editorPanels {
    display: flex;
}

body.editing #editorInspector {
    display: block;
}

/* The metrics read zero throughout an edit and the control list is replaced by
   the editor's own, so both give up their corner. The level picker goes because
   the panel it would sit above describes a level that may no longer be the one
   it names. The touch controls go too: a laptop with a touchscreen has them,
   and the boost button would launch the ship out from under the level being
   laid out. */
body.editing #stats,
body.editing #help,
body.editing #levelPanel,
body.editing #actions,
body.editing #views,
body.editing #boostButton {
    display: none;
}

#editor .heading {
    display: flex;
    justify-content: space-between;
}

#editorDirty {
    color: var(--danger);
    opacity: 0;
}

#editorDirty.visible {
    opacity: 1;
}

.edit .row {
    align-items: center;
    gap: 10px;
    line-height: 1.8;
}

.edit .field {
    flex: 1 1 auto;
    min-width: 0;
    padding: 2px 5px;
    background: rgba(0, 0, 0, 0.5);
    border: 1px solid var(--panel-border);
    border-radius: 3px;
    color: var(--text);
    font-family: inherit;
    font-size: 11px;
    letter-spacing: 0.06em;
}

.edit .field:focus {
    outline: 1px solid var(--accent);
}

.edit .swatch {
    height: 19px;
    padding: 0 1px;
}

.edit .check {
    margin: 0;
    accent-color: var(--accent);
}

/* An input paired with the button that resets or rerolls it. */
.edit .group {
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    gap: 4px;
    align-items: center;
}

.edit .mini {
    flex: 0 0 auto;
    width: 20px;
    padding: 0;
    background: rgba(0, 0, 0, 0.5);
    border: 1px solid var(--panel-border);
    border-radius: 3px;
    color: var(--dim);
    font-family: inherit;
    font-size: 12px;
    line-height: 17px;
    cursor: pointer;
}

.edit .mini:hover {
    border-color: var(--accent);
    color: var(--accent);
}

/* The clear button doubles as the indicator that a body carries a color of its
   own rather than the one its index gives it, so it is only there when it does
   -- but it keeps its space either way, so nothing shifts. */
.edit [data-action="autoColor"] {
    visibility: hidden;
}

.edit [data-action="autoColor"].visible {
    visibility: visible;
}

.edit .problem {
    line-height: 1.5;
    color: var(--danger);
}

.edit .problem.warn {
    color: var(--accent);
}

.edit .ok {
    color: var(--dim);
}

.edit .buttons {
    display: flex;
    gap: 5px;
    margin-top: 10px;
}

.edit .buttons button {
    flex: 1 1 0;
    min-width: 0;
    padding: 5px 4px;
    background: rgba(0, 0, 0, 0.5);
    border: 1px solid var(--panel-border);
    border-radius: 3px;
    color: var(--text);
    font-family: inherit;
    font-size: 11px;
    letter-spacing: 0.06em;
    cursor: pointer;
}

.edit .buttons button:hover:not(:disabled) {
    border-color: var(--accent);
    color: var(--accent);
}

.edit .buttons button:disabled {
    opacity: 0.4;
    cursor: default;
}

#editorToast {
    min-height: 15px;
    margin-top: 5px;
    color: var(--accent);
    font-size: 10px;
    letter-spacing: 0.1em;
    opacity: 0;
    transition: opacity 200ms ease-out;
}

#editorToast.visible {
    opacity: 1;
}

/* Dim the key list, but not its heading -- matching #help, which does the
   same. */
#editorKeys {
    opacity: 0.72;
}

/* Tighter than the rest of the panel: nine rows of key hints have to fit
   without either column wrapping. */
#editorKeys .row {
    gap: 10px;
    font-size: 11px;
    letter-spacing: 0.02em;
    line-height: 1.6;
    white-space: nowrap;
}

#editorKeys .key {
    flex: 0 0 auto;
    color: var(--text);
}

/* Touch layout ------------------------------------------------------------ */

body.touch #hud {
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
        env(safe-area-inset-bottom) env(safe-area-inset-left);
}

body.touch #actions {
    display: flex;
}

body.touch .pointer-keys,
body.touch #help,
body.touch .panel .heading,
body.touch #stats .row.secondary,
body.touch #status .keys {
    display: none;
}

body.touch .pointer-touch {
    display: block;
}

/* On desktop the wrapper generates no box at all, so #stats and #levelPanel
   position against #hud exactly as they did before it existed. */
#topbar {
    display: contents;
}

/* On touch it becomes a centered column: level on top, metrics beneath. There
   is far more vertical room than horizontal on a phone, so stacking always
   beats trying to fit the two side by side at some widths and not others. */
body.touch #topbar {
    position: absolute;
    top: 10px;
    left: 10px;
    right: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

body.touch #levelPanel {
    order: 0;
}

body.touch #stats {
    order: 1;
}

body.touch #topbar > .panel {
    position: static;
    flex: 0 0 auto;
}

/* Both boxes become three equal columns, each a label centred over its value.
   Equal thirds rather than content-sized so the numbers stay put as they change
   width -- a fuel figure crossing from 99.9 to 100.0 should not shove the clock
   sideways. Matching min-widths keep the two boxes aligned when stacked. */
body.touch #stats,
body.touch #levelPanel {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    min-width: 246px;
    padding: 6px 10px;
}

body.touch #stats .row,
body.touch #levelPanel .row {
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    gap: 2px;
    line-height: 1.15;
}

body.touch #stats .label,
body.touch #levelPanel .label {
    font-size: 9px;
    letter-spacing: 0.1em;
}

/* The level picker sits above its three stats, spanning the full width. */
body.touch #levelPanel .nav {
    grid-column: 1 / -1;
    margin-bottom: 5px;
}

body.touch #levelSelect {
    max-width: none;
    font-size: 11px;
}

body.touch #levelPanel button {
    width: 30px;
    height: 30px;
    font-size: 15px;
}

body.touch #badges {
    right: 74px;
    bottom: 72px;
}

/* Into the band between the bottom of the field and the joystick, and tight
   enough that neither line wraps at 375px. */
body.touch #hint {
    bottom: 20%;
    padding: 0 16px;
    font-size: 11px;
    letter-spacing: 0.14em;
}

body.touch #hint .sub {
    margin-top: 7px;
    font-size: 10px;
    letter-spacing: 0.1em;
}

body.touch #status .dialog {
    min-width: 0;
    width: min(340px, calc(100vw - 40px));
    padding: 20px 22px 22px;
}

/* Three actions rather than two once there is a score to post, which does not
   fit across a narrow phone. Wrapping is better than shrinking the type on the
   only controls a touch player has. */
body.touch #status .buttons {
    flex-wrap: wrap;
}

body.touch #status .buttons button {
    padding: 11px 16px;
}

/* Par gets its own line rather than wrapping mid-sentence, which is why its
   leading separator is a separate span to drop. */
body.touch #status .par {
    display: block;
}

body.touch #status .par .sep {
    display: none;
}

@keyframes pulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}
