Code/UI/SkafinityMusicPanel.razor.scss
// ── Static tokens — layout, type, radii, borders ──────────────────────────────
// The palette is RUNTIME, not compile-time: SkafinityTheme.cs derives it from one accent
// colour and the .razor binds it as inline style= values, so a consuming game re-themes the
// board without editing (or forking) this file. Nothing here sets a fill or a text colour
// that the theme owns.
//
// Two rules keep the split clean:
// * The stylesheet owns every BORDER and the razor sets none, so a :hover / .on
// border rule is never in competition with an inline style.
// * Hover feedback is therefore a border, not a fill — the fill is themed.
// Both are neutral white alphas, which read against any accent the host passes in.
//
// The split has one edge worth naming: a control this library did not write cannot be dressed
// from here AT ALL — not by a rule, which does not cross a component boundary, and not by an
// inline style, which cannot reach a panel it did not create. That is why the board's slider is
// its own (SkafinitySlider) and is painted from C# rather than from this file.
//
// web/palette.js reads the six alphas below out of THIS FILE (test/palette.mjs asserts it), so the
// widget and the board cannot drift into two schemes. Keep the `$name: rgba(255,255,255,a)` shape.
$radius: 2px;
$font: Consolas, 'Roboto Mono', monospace;
// Type scale in one place, because "a bit smaller" is a tuning pass and should be one edit.
$fs-title: 20px;
$fs-body: 12px; // buttons, labels, the seed box
$fs-small: 11px; // times, playlist rows, the jump line
$fs-tiny: 10px; // column headers, knob names, section headings
$edge: rgba(255,255,255,0.12); // the board's own outline
$rule: rgba(255,255,255,0.10); // dividers, panel outlines
$hover: rgba(255,255,255,0.35); // hover border
$pick: rgba(255,255,255,0.85); // selected / current border
$busy: rgba(255,255,255,0.55); // a playlist entry mid-render
$surface: rgba(255,255,255,0.07); // neutral inputs (seed box, jump box), progress-bar troughs
// ────────────────────────────────────────────────────────────────────────────────
skafinitymusicpanel {
// Fill the ScreenPanel; flex-centre the board. pointer-events:none so a closed panel
// never blocks anything — only the board itself takes pointer events.
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
justify-content: center; align-items: center;
pointer-events: none;
font-family: $font;
font-size: $fs-body;
// The board — sized to its content (no scroll range to drag-pan). Narrower than the old
// wall-of-cells layout, because the sliders stack rather than spreading.
.board {
flex-shrink: 0;
width: 94%; max-width: 1180px;
flex-direction: column;
align-items: stretch;
padding: 14px;
border: 1.5px solid $edge;
border-radius: 4px;
gap: 14px;
pointer-events: all;
}
.board > div { flex-shrink: 0; }
.header { flex-direction: row; align-items: center; justify-content: space-between; }
.title {
justify-content: center; white-space: nowrap;
font-size: $fs-title; font-weight: bold; letter-spacing: 6px; color: white;
}
.close {
width: 30px; height: 30px; justify-content: center; align-items: center;
font-size: 16px; font-weight: bold; color: rgba(255,255,255,0.6);
background-color: rgba(255,255,255,0.08); border-radius: $radius;
cursor: pointer; pointer-events: all;
&:hover { color: #ffffff; background-color: rgba(255,255,255,0.2); }
}
// ── Rows ──
.row { flex-direction: row; align-items: center; flex-wrap: wrap; gap: 8px; }
.grow { flex-grow: 1; }
// Pushes a control to the far end of its row (volume, tinker, save).
.right { margin-left: auto; }
.label { font-size: $fs-body; letter-spacing: 2px; white-space: nowrap; }
// ── Buttons ──
.btn {
flex-direction: row;
justify-content: center; align-items: center; height: 30px; padding: 0 14px;
font-size: $fs-body; white-space: nowrap;
border: 2px solid transparent; border-radius: $radius; cursor: pointer; pointer-events: all;
// A square button for a glyph — the three transport keys.
&.big { width: 40px; height: 32px; padding: 0; font-size: 16px; }
&.primary { font-weight: bold; }
&:hover { border-color: $hover; }
&.toggle.on { border-color: $pick; }
// Nothing to hand back / nothing to do: dimmed rather than hidden, so the control keeps
// its place and its explanation.
&.off { opacity: 0.45; }
&.off:hover { border-color: transparent; }
}
// ── Transport ──
.transport { flex-direction: column; gap: 8px; }
// flex-grow, not the volume's margin-left:auto, is what puts the volume at the far end of the
// row: an auto margin held the button in the row below out there but did nothing for the
// volume, and a spacer that grows needs no such adjudication.
.now { flex-direction: row; align-items: center; gap: 5px; flex-grow: 1; font-size: $fs-body; white-space: nowrap; }
.now b { font-weight: bold; }
.bufstate { font-size: $fs-body; white-space: nowrap; }
.vol { flex-direction: row; align-items: center; gap: 6px; font-size: $fs-body; }
.seek { flex-direction: row; align-items: center; gap: 10px; }
.time {
font-size: $fs-small; min-width: 40px; white-space: nowrap;
justify-content: flex-start;
}
.time.total { justify-content: flex-end; }
// ── Sliders ──
// There are none here. SkafinitySlider builds and paints its own three panels from C#, because
// a stylesheet cannot reach into another component's insides and a static class attribute never
// arrived on one — see the remarks on that class for what the live panel tree showed.
// ── Text entries ──
.seed-input, .jump-input {
height: 28px; padding: 0 10px; font-size: $fs-body; color: white;
background-color: $surface;
border: 2px solid rgba(255,255,255,0.15); border-radius: $radius; pointer-events: all;
&:focus { border-color: $hover; }
}
.jump-input { width: 70px; }
// ── Dropdowns ──
.genre, .knob-select {
height: 28px; padding: 0 10px; font-size: $fs-body;
border: 2px solid transparent; border-radius: $radius; cursor: pointer; pointer-events: all;
&:hover { border-color: $hover; }
}
.genre { min-width: 120px; }
.knob-select { height: 22px; font-size: $fs-small; }
// ── Panels (the vibe box, the playlist box) ──
.panel {
flex-direction: column;
padding: 12px;
border: 1px solid $rule; border-radius: $radius;
gap: 8px;
}
.h2 { font-size: $fs-tiny; font-weight: bold; letter-spacing: 2px; white-space: nowrap; }
// ── The mixer matrix — rows are voices, VOLUME leftmost ──
// Five columns across the board is not much per knob, so the cells are given room in the only
// two ways that do not cost a column: a wider gutter between them, and a header that reads as
// one thing (below).
.matrix { flex-direction: column; gap: 10px; }
.mrow { flex-direction: row; align-items: flex-start; gap: 16px; }
.mhead { align-items: center; }
.mvoice {
width: 68px; flex-shrink: 0; align-items: flex-start; justify-content: flex-start;
font-size: $fs-small; font-weight: bold; letter-spacing: 1px; color: white;
}
.mhlabel { font-size: 9px; letter-spacing: 2px; }
.mcell { width: 0; flex-grow: 1; flex-direction: column; }
// GLOBAL knobs: three compact columns under a rule.
.glabel { font-size: $fs-tiny; letter-spacing: 2px; padding-top: 8px; border-top: 1px solid $rule; }
.global-grid { flex-direction: row; flex-wrap: wrap; gap: 8px 14px; }
.knob-cell { width: 30%; }
.knob { flex-direction: column; gap: 2px; }
// Name and value TOGETHER at the left, not pushed to opposite ends. Spread across a narrow cell
// they stop reading as one label and start reading as two: a value hard against the right edge
// sits closer to the next column's name than to its own, so "40%" and "BUSY" — one the tone of
// the drums, the other the name of their character knob — look like a single caption.
.knob-head { flex-direction: row; justify-content: flex-start; align-items: baseline; gap: 5px; }
// Empty in the columns whose header already names the knob (VOLUME, TONE) — the value then
// simply starts the line rather than floating mid-cell.
.knob-name { font-size: 9px; letter-spacing: 1px; white-space: nowrap; }
.knob-val { font-size: 9px; white-space: nowrap; }
// ── The playlist ──
.playlist { flex-direction: column; gap: 2px; max-height: 260px; overflow: scroll; }
.plrow {
flex-direction: row; align-items: center; gap: 8px;
padding: 4px 6px;
border-radius: $radius;
border-left: 2px solid transparent;
&.cached { border-left-color: $busy; }
&.gen { border-left-color: $hover; }
&.now { border-left-color: $pick; }
}
// Three panels rather than one string, and the hash gets its own for a reason that is not
// layout: a label of more than one character beginning with `#` is read as a localisation token
// (SkafinityBoard.Copy.Hash). Alone in a panel it is one character, and survives.
.pllabel {
flex-direction: row; align-items: center;
min-width: 58px; font-size: $fs-small; color: white; white-space: nowrap;
cursor: pointer; pointer-events: all;
&:hover { color: #ffffff; text-decoration: underline; }
}
// A column, so every row's number starts at the same x whether or not it is the one playing.
.plcaret { width: 12px; flex-shrink: 0; }
.plhash { flex-shrink: 0; }
.plgenre { flex-grow: 1; font-size: $fs-tiny; letter-spacing: 1px; white-space: nowrap; }
.plstatus { min-width: 52px; font-size: $fs-tiny; white-space: nowrap; justify-content: flex-end; }
.pldl {
width: 24px; height: 22px; justify-content: center; align-items: center;
font-size: $fs-small; border-radius: $radius; cursor: pointer; pointer-events: all;
&:hover { background-color: rgba(255,255,255,0.12); }
}
.jump { font-size: $fs-small; }
// ── Progress bar (a playlist entry mid-render) ──
.bar {
width: 46px; height: 5px; background-color: $surface;
border-radius: $radius; overflow: hidden;
}
.bar-fill { height: 5px; border-radius: $radius; }
.msg { font-size: $fs-small; letter-spacing: 1px; }
}