542 lines
11 KiB
CSS
542 lines
11 KiB
CSS
/* crabidy web client — pure modern CSS (architecture/web-client.md).
|
|
One accent variable (crab orange-red), light and dark themes via
|
|
color-scheme + light-dark(); the theme toggle stamps data-theme on
|
|
<html>, otherwise the OS decides. No frameworks, no external
|
|
requests — everything ships in the bundle. */
|
|
|
|
:root {
|
|
color-scheme: light dark;
|
|
|
|
/* The crab. Every accent tone derives from this one value. */
|
|
--accent: oklch(0.62 0.19 35);
|
|
--accent-strong: color-mix(in oklch, var(--accent) 85%, black);
|
|
--accent-soft: color-mix(in oklch, var(--accent) 14%, transparent);
|
|
--on-accent: oklch(0.99 0.005 60);
|
|
|
|
--bg: light-dark(oklch(0.98 0.005 60), oklch(0.17 0.01 260));
|
|
--bg-raised: light-dark(oklch(1 0 0), oklch(0.21 0.012 260));
|
|
--fg: light-dark(oklch(0.25 0.015 260), oklch(0.92 0.005 60));
|
|
--fg-dim: light-dark(oklch(0.52 0.012 260), oklch(0.68 0.008 60));
|
|
--danger: light-dark(oklch(0.54 0.2 25), oklch(0.68 0.19 25));
|
|
--border: color-mix(in oklch, var(--fg) 14%, transparent);
|
|
--shadow: 0 8px 32px light-dark(rgb(0 0 0 / 0.14), rgb(0 0 0 / 0.55));
|
|
}
|
|
|
|
:root[data-theme="light"] {
|
|
color-scheme: light;
|
|
}
|
|
:root[data-theme="dark"] {
|
|
color-scheme: dark;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
font: 15px/1.45 system-ui, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
button {
|
|
font: inherit;
|
|
color: inherit;
|
|
background: var(--accent);
|
|
color: var(--on-accent);
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 0.3rem 0.75rem;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
background: var(--accent-strong);
|
|
}
|
|
|
|
&:disabled {
|
|
opacity: 0.35;
|
|
cursor: default;
|
|
}
|
|
|
|
&.ghost {
|
|
background: transparent;
|
|
color: var(--fg-dim);
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
&:hover:not(:disabled) {
|
|
background: var(--accent-soft);
|
|
color: var(--fg);
|
|
}
|
|
|
|
&.active {
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
}
|
|
}
|
|
|
|
&.ghost.danger {
|
|
background: transparent;
|
|
color: var(--danger);
|
|
|
|
&:hover:not(:disabled) {
|
|
background: color-mix(in oklch, var(--danger) 15%, transparent);
|
|
}
|
|
}
|
|
}
|
|
|
|
input {
|
|
font: inherit;
|
|
color: var(--fg);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.4rem 0.6rem;
|
|
|
|
&:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 1px;
|
|
}
|
|
}
|
|
|
|
/* ---- frame ---------------------------------------------------------- */
|
|
|
|
.shell {
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto;
|
|
block-size: 100dvh;
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.4rem 0.9rem;
|
|
border-block-end: 1px solid var(--border);
|
|
background: var(--bg-raised);
|
|
|
|
& .brand {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
font-size: 1.05rem;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
& .conn {
|
|
font-size: 0.85rem;
|
|
color: var(--fg-dim);
|
|
/* Truncate rather than push the pane tabs off a narrow screen. */
|
|
min-inline-size: 0;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
|
|
&.offline {
|
|
color: var(--danger);
|
|
}
|
|
}
|
|
|
|
& .tabs {
|
|
display: flex;
|
|
gap: 0.1rem;
|
|
padding: 0.1rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
|
|
/* Comfortable for a thumb — this is the phone's only pane switch. */
|
|
& button {
|
|
padding: 0.3rem 0.7rem;
|
|
}
|
|
}
|
|
|
|
& .topbar-actions {
|
|
margin-inline-start: auto;
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
}
|
|
|
|
/* ---- panes ----------------------------------------------------------- */
|
|
|
|
.panes {
|
|
display: grid;
|
|
grid-template-columns: 3fr 2fr;
|
|
min-block-size: 0;
|
|
}
|
|
|
|
.pane {
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto;
|
|
min-block-size: 0;
|
|
border-inline-end: 1px solid var(--border);
|
|
/* The focused pane shows it like the TUI's highlighted border. */
|
|
box-shadow: inset 0 2px 0 transparent;
|
|
|
|
&:last-child {
|
|
border-inline-end: none;
|
|
}
|
|
|
|
&.focused {
|
|
box-shadow: inset 0 2px 0 var(--accent);
|
|
}
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.15rem;
|
|
padding: 0.35rem 0.6rem;
|
|
border-block-end: 1px solid var(--border);
|
|
overflow-x: auto;
|
|
|
|
& .path {
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
& .spacer {
|
|
flex: 1;
|
|
}
|
|
}
|
|
|
|
.list {
|
|
margin: 0;
|
|
padding: 0.25rem 0;
|
|
list-style: none;
|
|
overflow-y: auto;
|
|
min-block-size: 0;
|
|
|
|
& li {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
padding: 0.28rem 0.75rem;
|
|
cursor: pointer;
|
|
border-inline-start: 3px solid transparent;
|
|
|
|
& .title {
|
|
flex: 1;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
& .badge {
|
|
color: var(--fg-dim);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
&:hover {
|
|
background: var(--accent-soft);
|
|
}
|
|
|
|
&.selected {
|
|
background: var(--accent-soft);
|
|
border-inline-start-color: var(--accent);
|
|
}
|
|
|
|
&.marked .title {
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
|
|
&::before {
|
|
content: "* ";
|
|
}
|
|
}
|
|
|
|
/* Skipped tracks carry no audio (incremental captures). */
|
|
&.skipped .title {
|
|
color: var(--danger);
|
|
}
|
|
|
|
&.node .title {
|
|
color: color-mix(in oklch, var(--fg) 80%, var(--accent));
|
|
}
|
|
|
|
&.current .title {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
& .row-action {
|
|
visibility: hidden;
|
|
}
|
|
|
|
&:hover .row-action,
|
|
&.selected .row-action {
|
|
visibility: visible;
|
|
}
|
|
}
|
|
}
|
|
|
|
.capture-lines {
|
|
padding: 0.2rem 0.75rem 0.4rem;
|
|
font-size: 0.85rem;
|
|
color: var(--fg-dim);
|
|
|
|
& .capture-line.error {
|
|
color: var(--danger);
|
|
}
|
|
}
|
|
|
|
/* ---- transport -------------------------------------------------------- */
|
|
|
|
.transport {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0.5rem 0.9rem;
|
|
border-block-start: 1px solid var(--border);
|
|
background: var(--bg-raised);
|
|
|
|
& .controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.1rem;
|
|
/* The prev/next/pause marks carry emoji presentation too, so a browser
|
|
with a color emoji font renders them as pictures rather than type.
|
|
Ask for the text form; ignored where unsupported, which costs
|
|
nothing — the glyphs are the fallback either way. */
|
|
font-variant-emoji: text;
|
|
|
|
& .big {
|
|
font-size: 1.3rem;
|
|
color: var(--accent);
|
|
}
|
|
}
|
|
|
|
& .now-playing {
|
|
min-inline-size: 0;
|
|
|
|
& .np-title {
|
|
display: block;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Frequency-spectrum bars (architecture/spectrum.md): a row of
|
|
accent-colored columns whose heights track the streamed bins. */
|
|
& .spectrum {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 1px;
|
|
block-size: 1.5rem;
|
|
margin-block-end: 0.15rem;
|
|
|
|
& .bar {
|
|
flex: 1;
|
|
min-block-size: 1px;
|
|
background: var(--accent);
|
|
border-radius: 1px;
|
|
transition: height 0.08s linear;
|
|
}
|
|
}
|
|
}
|
|
|
|
& .progress {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
|
|
& .time {
|
|
font-size: 0.8rem;
|
|
color: var(--fg-dim);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
& .gauge {
|
|
flex: 1;
|
|
block-size: 6px;
|
|
border-radius: 3px;
|
|
background: var(--accent-soft);
|
|
overflow: hidden;
|
|
/* Click-to-seek: `manipulation` drops the touch double-tap delay so a
|
|
tap seeks immediately. */
|
|
cursor: pointer;
|
|
touch-action: manipulation;
|
|
|
|
& .gauge-fill {
|
|
block-size: 100%;
|
|
background: var(--accent);
|
|
border-radius: 3px;
|
|
transition: width 0.4s linear;
|
|
}
|
|
}
|
|
}
|
|
|
|
& .volume {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
|
|
& input[type="range"] {
|
|
inline-size: 7rem;
|
|
accent-color: var(--accent);
|
|
padding: 0;
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---- overlays ---------------------------------------------------------- */
|
|
|
|
.overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
background: rgb(0 0 0 / 0.4);
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.dialog {
|
|
display: grid;
|
|
gap: 0.7rem;
|
|
min-inline-size: min(26rem, 90vw);
|
|
max-block-size: 85dvh;
|
|
overflow-y: auto;
|
|
padding: 1.1rem 1.3rem;
|
|
border-radius: 10px;
|
|
background: var(--bg-raised);
|
|
box-shadow: var(--shadow);
|
|
|
|
& label {
|
|
color: var(--fg-dim);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
& .dialog-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
|
|
/* Visual (paint-select) mode badge in a pane toolbar — the web counterpart of
|
|
the TUI's "— VISUAL" pane title. */
|
|
.mode {
|
|
padding: 0.05rem 0.4rem;
|
|
border: 1px solid var(--accent);
|
|
border-radius: 0.6rem;
|
|
color: var(--accent);
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.help {
|
|
min-inline-size: min(52rem, 94vw);
|
|
|
|
& h2 {
|
|
margin: 0;
|
|
color: var(--accent);
|
|
}
|
|
|
|
& .help-columns {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
|
|
gap: 0.5rem 2rem;
|
|
|
|
& h3 {
|
|
margin: 0.4rem 0 0.2rem;
|
|
font-size: 0.9rem;
|
|
color: var(--fg-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
& table {
|
|
border-collapse: collapse;
|
|
inline-size: 100%;
|
|
|
|
& td {
|
|
padding: 0.12rem 0.4rem 0.12rem 0;
|
|
vertical-align: top;
|
|
}
|
|
|
|
& .key {
|
|
font-family: ui-monospace, monospace;
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* The queue sort menu: one row per strategy, pickable by click as well as by
|
|
its letter (architecture/queue-order.md D16). */
|
|
.sort-choice {
|
|
cursor: pointer;
|
|
|
|
& td {
|
|
padding: 0.25rem 0.6rem 0.25rem 0;
|
|
}
|
|
|
|
& .key {
|
|
font-family: ui-monospace, monospace;
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
&:hover {
|
|
background: var(--accent-soft);
|
|
}
|
|
}
|
|
|
|
.hint {
|
|
margin: 0;
|
|
color: var(--fg-dim);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
inset-block-end: 4.5rem;
|
|
inset-inline-start: 50%;
|
|
translate: -50% 0;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 8px;
|
|
background: var(--danger);
|
|
color: var(--on-accent);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
/* ---- phone ------------------------------------------------------------- */
|
|
|
|
@media (max-width: 700px) {
|
|
/* One pane at a time, switched by the topbar tabs (or `Tab`). The other
|
|
pane is gone rather than collapsed to a strip: a strip is a row of
|
|
truncated rows and toolbar buttons that still take taps, so aiming at
|
|
it hit the wrong pane's list or its "play". The tabs replace it. */
|
|
.panes {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr;
|
|
}
|
|
|
|
.pane {
|
|
/* No second column to divide, so the border would sit on the screen
|
|
edge — and `:last-child` only spares the queue. */
|
|
border-inline-end: none;
|
|
}
|
|
|
|
.pane:not(.focused) {
|
|
display: none;
|
|
}
|
|
|
|
.transport {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.4rem;
|
|
|
|
& .volume {
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
}
|