3750 lines
82 KiB
CSS
3750 lines
82 KiB
CSS
/* Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 */
|
||
|
|
|
||
|
|
/* Volumen admin · design system 2026
|
||
|
|
The interface sheet of the admin UI. It carries the same tokens, the same
|
||
|
|
components and the same calm as the author's website: colours are oklch
|
||
|
|
neutrals and exact matplotlib colour-map stops, derived further with
|
||
|
|
light-dark() and color-mix(); the cascade is ordered by @layer, so no
|
||
|
|
!important and no specificity war; type is rem on the user's browser
|
||
|
|
setting; motion respects prefers-reduced-motion; the popover, the dialog
|
||
|
|
and :has() replace the JavaScript they used to need. The palette of the
|
||
|
|
account rides on the html data-palette attribute, the pinned colour
|
||
|
|
scheme on data-scheme, and served as written nothing is compiled. */
|
||
|
|
|
||
|
|
@property --bg {
|
||
|
|
syntax: "<color>";
|
||
|
|
inherits: true;
|
||
|
|
initial-value: oklch(98.2% 0.008 280);
|
||
|
|
}
|
||
|
|
|
||
|
|
@layer reset, tokens, base, layout, components, utilities;
|
||
|
|
|
||
|
|
@layer tokens {
|
||
|
|
:root {
|
||
|
|
/* The scheme follows the system until the account pins one with the
|
||
|
|
toggle: the light-dark() tokens read the resolved colour scheme, and
|
||
|
|
a data-scheme attribute exists only to pin it. */
|
||
|
|
color-scheme: light dark;
|
||
|
|
|
||
|
|
--font-ui: "Ubuntu", ui-sans-serif, system-ui, "DejaVu Sans", sans-serif;
|
||
|
|
--font-mono:
|
||
|
|
"Ubuntu Mono",
|
||
|
|
ui-monospace,
|
||
|
|
"Cascadia Mono",
|
||
|
|
"DejaVu Sans Mono",
|
||
|
|
monospace;
|
||
|
|
|
||
|
|
/* VIRIDIS: the default palette, the official colour map. Every accent
|
||
|
|
is an exact entry of the table matplotlib publishes
|
||
|
|
(lib/matplotlib/_cm_listed.py, _viridis_data): #440154 is entry 0,
|
||
|
|
#26828e is 112, #5ec962 is 192 and #fde725 is 255. The neutrals are
|
||
|
|
tinted towards the map's dark end, hue 280 in the light scheme and
|
||
|
|
300 in the dark one. The other palettes below restate every token. */
|
||
|
|
--bg: light-dark(oklch(98.2% 0.008 280), oklch(17% 0.02 300));
|
||
|
|
--surface: light-dark(oklch(100% 0 0), oklch(21.5% 0.024 300));
|
||
|
|
--surface-2: light-dark(oklch(96.2% 0.01 280), oklch(25.5% 0.028 300));
|
||
|
|
--ink: light-dark(oklch(21% 0.03 300), oklch(94% 0.01 280));
|
||
|
|
--ink-2: light-dark(oklch(45% 0.025 300), oklch(78% 0.02 280));
|
||
|
|
--ink-3: light-dark(oklch(57% 0.02 300), oklch(64% 0.02 280));
|
||
|
|
--line: light-dark(oklch(91% 0.015 300), oklch(31% 0.025 300));
|
||
|
|
--line-strong: light-dark(oklch(83% 0.02 300), oklch(39% 0.03 300));
|
||
|
|
--accent: light-dark(#26828e, #5ec962);
|
||
|
|
--accent-2: light-dark(#440154, #fde725);
|
||
|
|
--on-accent: light-dark(oklch(99% 0.005 300), oklch(22% 0.04 130));
|
||
|
|
|
||
|
|
/* What derives from the palette, and what no palette owns. */
|
||
|
|
--accent-wash: light-dark(
|
||
|
|
color-mix(in oklab, var(--accent) 8%, transparent),
|
||
|
|
color-mix(in oklab, var(--accent) 16%, transparent)
|
||
|
|
);
|
||
|
|
--gradient: linear-gradient(
|
||
|
|
100deg,
|
||
|
|
var(--accent) 8%,
|
||
|
|
color-mix(in oklab, var(--accent) 45%, var(--accent-2)) 55%,
|
||
|
|
var(--accent-2) 100%
|
||
|
|
);
|
||
|
|
--glow: light-dark(
|
||
|
|
color-mix(in oklab, var(--accent) 26%, transparent),
|
||
|
|
color-mix(in oklab, var(--accent) 40%, transparent)
|
||
|
|
);
|
||
|
|
--danger: light-dark(oklch(50% 0.19 26), oklch(74% 0.14 26));
|
||
|
|
--ok: light-dark(oklch(48% 0.13 155), oklch(78% 0.13 155));
|
||
|
|
--warn: light-dark(oklch(50% 0.13 70), oklch(82% 0.14 85));
|
||
|
|
|
||
|
|
--text-xs: 0.8125rem;
|
||
|
|
--text-sm: 0.9062rem;
|
||
|
|
--text-base: 1.0312rem;
|
||
|
|
--text-lg: clamp(1.12rem, 0.3vw + 1.05rem, 1.25rem);
|
||
|
|
--text-xl: clamp(1.25rem, 0.6vw + 1.1rem, 1.5rem);
|
||
|
|
--text-2xl: clamp(1.5rem, 1.3vw + 1.16rem, 2rem);
|
||
|
|
--text-display: clamp(2.5rem, 5vw + 0.8rem, 4.35rem);
|
||
|
|
|
||
|
|
--space-3xs: 0.25rem;
|
||
|
|
--space-2xs: 0.4rem;
|
||
|
|
--space-xs: 0.6rem;
|
||
|
|
--space-sm: 0.9rem;
|
||
|
|
--space-md: 1.25rem;
|
||
|
|
--space-lg: 1.8rem;
|
||
|
|
--space-xl: 2.8rem;
|
||
|
|
--space-2xl: 5rem;
|
||
|
|
--space-3xl: 7.5rem;
|
||
|
|
|
||
|
|
--radius-sm: 10px;
|
||
|
|
--radius: 16px;
|
||
|
|
--radius-lg: 22px;
|
||
|
|
--content: 74rem;
|
||
|
|
--measure: 66ch;
|
||
|
|
--gutter: clamp(1.25rem, 4vw, 2.75rem);
|
||
|
|
--header-h: 4.25rem;
|
||
|
|
--rail-w: 15.5rem;
|
||
|
|
|
||
|
|
--shadow-sm: 0 1px 2px light-dark(
|
||
|
|
oklch(20% 0.02 268 / 0.05),
|
||
|
|
oklch(0% 0 0 / 0.3)
|
||
|
|
);
|
||
|
|
--shadow-md:
|
||
|
|
0 1px 2px light-dark(oklch(20% 0.02 268 / 0.05), oklch(0% 0 0 / 0.32)),
|
||
|
|
0 10px 30px -12px light-dark(oklch(20% 0.02 268 / 0.14), oklch(0% 0 0 / 0.5));
|
||
|
|
--shadow-lg:
|
||
|
|
0 2px 4px light-dark(oklch(20% 0.02 268 / 0.05), oklch(0% 0 0 / 0.34)),
|
||
|
|
0 22px 50px -16px light-dark(oklch(20% 0.02 268 / 0.2), oklch(0% 0 0 / 0.6));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* PLASMA: the official colour map. The accents are exact entries
|
||
|
|
(#0d0887 is 0, #bc3587 is 112, #f89540 is 192, #f0f921 is 255) and the
|
||
|
|
neutrals tint to hue 267.5, the map's dark end. */
|
||
|
|
:root[data-palette="plasma"] {
|
||
|
|
--bg: light-dark(oklch(98.2% 0.008 267.5), oklch(17% 0.02 267.5));
|
||
|
|
--surface: light-dark(oklch(100% 0 0), oklch(21.5% 0.024 267.5));
|
||
|
|
--surface-2: light-dark(oklch(96.2% 0.01 267.5), oklch(25.5% 0.028 267.5));
|
||
|
|
--ink: light-dark(oklch(21% 0.03 267.5), oklch(94% 0.01 267.5));
|
||
|
|
--ink-2: light-dark(oklch(45% 0.025 267.5), oklch(78% 0.02 267.5));
|
||
|
|
--ink-3: light-dark(oklch(57% 0.02 267.5), oklch(64% 0.02 267.5));
|
||
|
|
--line: light-dark(oklch(91% 0.015 267.5), oklch(31% 0.025 267.5));
|
||
|
|
--line-strong: light-dark(oklch(83% 0.02 267.5), oklch(39% 0.03 267.5));
|
||
|
|
--accent: light-dark(#bc3587, #f89540);
|
||
|
|
--accent-2: light-dark(#0d0887, #f0f921);
|
||
|
|
--on-accent: light-dark(oklch(99% 0.005 267.5), oklch(22% 0.04 112));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* MAGMA: the official colour map. The accents are exact entries
|
||
|
|
(#000004 is 0, #9c2e7f is 112, #fc8961 is 192, #fcfdbf is 255); the
|
||
|
|
neutrals tint to hue 306, the purple region, because entry 0 is black
|
||
|
|
and carries no hue of its own. */
|
||
|
|
:root[data-palette="magma"] {
|
||
|
|
--bg: light-dark(oklch(98.2% 0.008 306), oklch(17% 0.02 306));
|
||
|
|
--surface: light-dark(oklch(100% 0 0), oklch(21.5% 0.024 306));
|
||
|
|
--surface-2: light-dark(oklch(96.2% 0.01 306), oklch(25.5% 0.028 306));
|
||
|
|
--ink: light-dark(oklch(21% 0.03 306), oklch(94% 0.01 306));
|
||
|
|
--ink-2: light-dark(oklch(45% 0.025 306), oklch(78% 0.02 306));
|
||
|
|
--ink-3: light-dark(oklch(57% 0.02 306), oklch(64% 0.02 306));
|
||
|
|
--line: light-dark(oklch(91% 0.015 306), oklch(31% 0.025 306));
|
||
|
|
--line-strong: light-dark(oklch(83% 0.02 306), oklch(39% 0.03 306));
|
||
|
|
--accent: light-dark(#9c2e7f, #fc8961);
|
||
|
|
--accent-2: light-dark(#000004, #fcfdbf);
|
||
|
|
--on-accent: light-dark(oklch(99% 0.005 306), oklch(22% 0.04 108));
|
||
|
|
}
|
||
|
|
|
||
|
|
:root[data-scheme="light"] {
|
||
|
|
color-scheme: light;
|
||
|
|
}
|
||
|
|
|
||
|
|
:root[data-scheme="dark"] {
|
||
|
|
color-scheme: dark;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@layer reset {
|
||
|
|
*,
|
||
|
|
*::before,
|
||
|
|
*::after {
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
* {
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
html {
|
||
|
|
-webkit-text-size-adjust: 100%;
|
||
|
|
scrollbar-width: thin;
|
||
|
|
scrollbar-color: var(--accent) transparent;
|
||
|
|
scrollbar-gutter: stable;
|
||
|
|
}
|
||
|
|
|
||
|
|
img,
|
||
|
|
svg,
|
||
|
|
video {
|
||
|
|
display: block;
|
||
|
|
max-width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
input,
|
||
|
|
button,
|
||
|
|
textarea,
|
||
|
|
select {
|
||
|
|
font: inherit;
|
||
|
|
color: inherit;
|
||
|
|
}
|
||
|
|
|
||
|
|
ul[class],
|
||
|
|
ol[class] {
|
||
|
|
list-style: none;
|
||
|
|
padding: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
fieldset {
|
||
|
|
border: 0;
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
[hidden] {
|
||
|
|
display: none !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@layer base {
|
||
|
|
body {
|
||
|
|
min-height: 100svh;
|
||
|
|
background: var(--bg);
|
||
|
|
color: var(--ink);
|
||
|
|
font-family: var(--font-ui);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
line-height: 1.6;
|
||
|
|
-webkit-font-smoothing: antialiased;
|
||
|
|
text-rendering: optimizeLegibility;
|
||
|
|
transition:
|
||
|
|
background-color 0.3s ease,
|
||
|
|
color 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1,
|
||
|
|
h2,
|
||
|
|
h3,
|
||
|
|
h4 {
|
||
|
|
font-weight: 700;
|
||
|
|
line-height: 1.12;
|
||
|
|
letter-spacing: -0.022em;
|
||
|
|
text-wrap: balance;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
font-size: var(--text-2xl);
|
||
|
|
letter-spacing: -0.028em;
|
||
|
|
}
|
||
|
|
|
||
|
|
h2 {
|
||
|
|
font-size: var(--text-xl);
|
||
|
|
letter-spacing: -0.024em;
|
||
|
|
}
|
||
|
|
|
||
|
|
h3 {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
letter-spacing: -0.015em;
|
||
|
|
}
|
||
|
|
|
||
|
|
p {
|
||
|
|
text-wrap: pretty;
|
||
|
|
}
|
||
|
|
|
||
|
|
a {
|
||
|
|
color: var(--accent);
|
||
|
|
text-decoration: underline;
|
||
|
|
text-decoration-color: color-mix(in oklab, var(--accent) 38%, transparent);
|
||
|
|
text-decoration-thickness: 1.5px;
|
||
|
|
text-underline-offset: 0.2em;
|
||
|
|
transition:
|
||
|
|
color 0.18s ease,
|
||
|
|
text-decoration-color 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
a:hover {
|
||
|
|
text-decoration-color: currentColor;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
strong,
|
||
|
|
b {
|
||
|
|
font-weight: 650;
|
||
|
|
}
|
||
|
|
|
||
|
|
code,
|
||
|
|
kbd,
|
||
|
|
samp {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.875em;
|
||
|
|
}
|
||
|
|
|
||
|
|
:not(pre) > code {
|
||
|
|
padding: 0.12em 0.4em;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: 6px;
|
||
|
|
background: var(--surface-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
kbd {
|
||
|
|
display: inline-block;
|
||
|
|
padding: 0.1rem 0.45rem;
|
||
|
|
border: 1px solid var(--line-strong);
|
||
|
|
border-bottom-width: 2px;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface-2);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
line-height: 1.3;
|
||
|
|
}
|
||
|
|
|
||
|
|
::selection {
|
||
|
|
background: color-mix(in oklab, var(--accent) 26%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
:focus-visible {
|
||
|
|
outline: 2px solid var(--accent);
|
||
|
|
outline-offset: 2px;
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@layer layout {
|
||
|
|
/* The shell: a full-width topbar on the first row, the rail and the
|
||
|
|
content column beneath it. */
|
||
|
|
.shell {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: var(--rail-w) minmax(0, 1fr);
|
||
|
|
grid-template-rows: var(--header-h) minmax(0, 1fr);
|
||
|
|
min-height: 100svh;
|
||
|
|
}
|
||
|
|
|
||
|
|
.main {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content {
|
||
|
|
width: 100%;
|
||
|
|
max-width: var(--content);
|
||
|
|
margin-inline: auto;
|
||
|
|
padding: var(--space-xl) var(--gutter) var(--space-3xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-head {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: flex-end;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: var(--space-md);
|
||
|
|
margin-bottom: var(--space-xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-head__text {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-head__lede {
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-head__actions {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.grid-2 {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (min-width: 56rem) {
|
||
|
|
.grid-2 {
|
||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The dashboard strips: two cards ride side by side, one card takes the
|
||
|
|
whole row, the empty track collapses. */
|
||
|
|
.grid--fit {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
|
||
|
|
margin-bottom: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The page head following a card row earns its own distance. */
|
||
|
|
.card + .page-head,
|
||
|
|
.grid--fit + .page-head {
|
||
|
|
margin-top: var(--space-xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A full-page empty state (the post-update wait) wears the centred
|
||
|
|
column, so one lone message never floats across the wide container. */
|
||
|
|
.narrow {
|
||
|
|
max-width: 46rem;
|
||
|
|
margin-inline: auto;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@layer components {
|
||
|
|
/* --- Chrome: rail, topbar, menu, skip link --------------------------- */
|
||
|
|
|
||
|
|
.skip-link {
|
||
|
|
position: absolute;
|
||
|
|
z-index: 30;
|
||
|
|
top: 0.6rem;
|
||
|
|
left: 0.6rem;
|
||
|
|
padding: 0.6rem 1rem;
|
||
|
|
translate: 0 -5rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 600;
|
||
|
|
text-decoration: none;
|
||
|
|
transition: translate 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.skip-link:focus-visible {
|
||
|
|
translate: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
position: sticky;
|
||
|
|
top: var(--header-h);
|
||
|
|
height: calc(100svh - var(--header-h));
|
||
|
|
border-inline-end: 1px solid var(--line);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The rail is the wide-screen chrome: below that width it gives way to
|
||
|
|
the popover menu, and the shell drops to one column. */
|
||
|
|
@media (max-width: 55.99rem) {
|
||
|
|
.shell {
|
||
|
|
grid-template-columns: minmax(0, 1fr);
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The brand rides the topbar in the shape the site header wears it:
|
||
|
|
the icon badge beside a name stack, the name heavy and tight, the
|
||
|
|
role quiet beneath it. No wordmark image, no mono eyebrow. */
|
||
|
|
.brand {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
text-decoration: none;
|
||
|
|
color: var(--ink);
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.brand__mark {
|
||
|
|
width: 1.9rem;
|
||
|
|
height: 1.9rem;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.brand__text {
|
||
|
|
display: grid;
|
||
|
|
gap: 0.05rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.brand__name {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
font-weight: 750;
|
||
|
|
letter-spacing: -0.025em;
|
||
|
|
line-height: 1.15;
|
||
|
|
}
|
||
|
|
|
||
|
|
.brand__role {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__nav {
|
||
|
|
flex: 1;
|
||
|
|
overflow-x: hidden;
|
||
|
|
overflow-y: auto;
|
||
|
|
display: grid;
|
||
|
|
align-content: start;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: var(--space-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__label {
|
||
|
|
padding: var(--space-xs) var(--space-xs) 0;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.66rem;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.12em;
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__link {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: 0.5rem 0.8rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
font-weight: 520;
|
||
|
|
color: var(--ink-2);
|
||
|
|
text-decoration: none;
|
||
|
|
transition:
|
||
|
|
color 0.18s ease,
|
||
|
|
background-color 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__link svg {
|
||
|
|
width: 1.05rem;
|
||
|
|
height: 1.05rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.rail__link:hover {
|
||
|
|
color: var(--ink);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__link[aria-current="page"] {
|
||
|
|
color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The account anchors the topbar's right end: who this is, with the way
|
||
|
|
out standing apart to its left. The site header puts its controls
|
||
|
|
right, and so does this. */
|
||
|
|
.topbar__user {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.topbar__user-text {
|
||
|
|
display: grid;
|
||
|
|
gap: 0.05rem;
|
||
|
|
min-width: 0;
|
||
|
|
text-align: end;
|
||
|
|
}
|
||
|
|
|
||
|
|
.topbar__user-name {
|
||
|
|
font-weight: 620;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.topbar__user-role {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
text-transform: capitalize;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 55.99rem) {
|
||
|
|
.topbar__user-text {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__version {
|
||
|
|
padding: var(--space-2xs) var(--space-md) var(--space-sm);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The fold. The button lives at the head of the rail, and the head
|
||
|
|
script reads the device pin and sets data-rail before the first
|
||
|
|
paint; the attribute's very presence is the signal, so with no
|
||
|
|
JavaScript the toggle never shows and the rail stays full. Below
|
||
|
|
the rail's breakpoint the popover menu is the navigation and the
|
||
|
|
rail, toggle and all, is gone. */
|
||
|
|
.rail-toggle {
|
||
|
|
display: none;
|
||
|
|
flex-shrink: 0;
|
||
|
|
margin: var(--space-2xs) var(--space-sm) 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail] .rail-toggle {
|
||
|
|
display: grid;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The icon points where clicking takes the rail: its 180-degree turn
|
||
|
|
in the compact state says so without a second icon. */
|
||
|
|
.rail-toggle svg {
|
||
|
|
transition: rotate 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The column animates its whole width, so the fold slides. */
|
||
|
|
.shell {
|
||
|
|
transition: grid-template-columns 0.22s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] {
|
||
|
|
--rail-w: 5.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] .rail-toggle {
|
||
|
|
margin-inline: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] .rail-toggle svg {
|
||
|
|
rotate: 180deg;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] .rail__nav {
|
||
|
|
padding-inline: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] .rail__link {
|
||
|
|
justify-content: center;
|
||
|
|
padding: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The words outlive the last frame of the slide: display is only
|
||
|
|
surrendered after the fade, and @starting-style makes the fade run
|
||
|
|
in both directions. */
|
||
|
|
.rail__text,
|
||
|
|
.rail__version {
|
||
|
|
transition:
|
||
|
|
opacity 0.18s ease,
|
||
|
|
display 0.18s ease allow-discrete;
|
||
|
|
}
|
||
|
|
|
||
|
|
.rail__text {
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] .rail__text,
|
||
|
|
html[data-rail="compact"] .rail__version {
|
||
|
|
display: none;
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
@starting-style {
|
||
|
|
html[data-rail="compact"] .rail__text,
|
||
|
|
html[data-rail="compact"] .rail__version {
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The group eyebrow becomes the rule that separates the groups: its
|
||
|
|
words stay in the accessibility tree behind a zeroed font. The
|
||
|
|
first one has no group above it, so it folds away entirely, the
|
||
|
|
topbar's edge being the divider it would otherwise repeat. */
|
||
|
|
html[data-rail="compact"] .rail__label {
|
||
|
|
height: 0;
|
||
|
|
margin: var(--space-xs) var(--space-md) 0;
|
||
|
|
padding: 0;
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
font-size: 0;
|
||
|
|
line-height: 0;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
html[data-rail="compact"] .rail__label:first-child {
|
||
|
|
margin: 0;
|
||
|
|
border: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.avatar {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
width: 2.1rem;
|
||
|
|
height: 2.1rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
font-weight: 650;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.avatar img {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The topbar is an opaque surface band: it reads as one continuous
|
||
|
|
piece of chrome with the rail below it, and it stands apart from
|
||
|
|
the tinted canvas the content sits on. Glass would melt it into
|
||
|
|
the page background, which is the opposite of the point. */
|
||
|
|
.topbar {
|
||
|
|
position: sticky;
|
||
|
|
z-index: 20;
|
||
|
|
top: 0;
|
||
|
|
grid-column: 1 / -1;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
min-height: var(--header-h);
|
||
|
|
padding-inline: var(--gutter);
|
||
|
|
border-bottom: 1px solid var(--line);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The path rides the top of the page as a quiet line, not as a second
|
||
|
|
title in the topbar; only pages deeper than one section carry it. */
|
||
|
|
.crumbs {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
min-width: 0;
|
||
|
|
overflow: hidden;
|
||
|
|
white-space: nowrap;
|
||
|
|
margin-bottom: var(--space-md);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.crumbs a {
|
||
|
|
color: var(--ink-3);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.crumbs a:hover {
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.crumbs__current {
|
||
|
|
color: var(--ink-2);
|
||
|
|
font-weight: 550;
|
||
|
|
}
|
||
|
|
|
||
|
|
.topbar__tools {
|
||
|
|
margin-inline-start: auto;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The rule that parts the device controls from the account group: the
|
||
|
|
scheme is this device's, the session is this account's, and the chrome
|
||
|
|
reads calmer when the two are never mistaken for one cluster. */
|
||
|
|
.topbar__sep {
|
||
|
|
flex-shrink: 0;
|
||
|
|
width: 1px;
|
||
|
|
height: 1.5rem;
|
||
|
|
border-radius: 1px;
|
||
|
|
background: var(--line-strong);
|
||
|
|
}
|
||
|
|
|
||
|
|
.icon-btn {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
width: 2.3rem;
|
||
|
|
height: 2.3rem;
|
||
|
|
padding: 0;
|
||
|
|
border: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: transparent;
|
||
|
|
color: var(--ink-2);
|
||
|
|
cursor: pointer;
|
||
|
|
text-decoration: none;
|
||
|
|
transition:
|
||
|
|
color 0.18s ease,
|
||
|
|
background-color 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.icon-btn svg {
|
||
|
|
width: 1.15rem;
|
||
|
|
height: 1.15rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.icon-btn:hover {
|
||
|
|
color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.icon-btn--danger {
|
||
|
|
color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.icon-btn--danger:hover {
|
||
|
|
background: color-mix(in oklab, var(--danger) 12%, transparent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The menu toggle only earns its place when the rail is gone. */
|
||
|
|
.menu-toggle {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 55.99rem) {
|
||
|
|
.menu-toggle {
|
||
|
|
display: grid;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* One icon per mode: the monitor stands for following the system, and
|
||
|
|
the sun and moon for the two pins. Exactly one shows at a time. */
|
||
|
|
.theme-toggle svg {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
:root:not([data-scheme]) .theme-toggle .icon--system {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
:root[data-scheme="light"] .theme-toggle .icon--sun {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
:root[data-scheme="dark"] .theme-toggle .icon--moon {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The mobile navigation sheet: a popover in the top layer, so no
|
||
|
|
z-index war and no class juggling. */
|
||
|
|
.menu {
|
||
|
|
position: fixed;
|
||
|
|
inset: auto 0.6rem auto 0.6rem;
|
||
|
|
top: calc(var(--header-h) + 0.5rem);
|
||
|
|
width: auto;
|
||
|
|
margin: 0;
|
||
|
|
padding: var(--space-xs);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
opacity: 0;
|
||
|
|
translate: 0 -0.5rem;
|
||
|
|
transition:
|
||
|
|
opacity 0.18s ease,
|
||
|
|
translate 0.18s ease,
|
||
|
|
display 0.18s allow-discrete,
|
||
|
|
overlay 0.18s allow-discrete;
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu:popover-open {
|
||
|
|
opacity: 1;
|
||
|
|
translate: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
@starting-style {
|
||
|
|
.menu:popover-open {
|
||
|
|
opacity: 0;
|
||
|
|
translate: 0 -0.5rem;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu::backdrop {
|
||
|
|
background: color-mix(in oklab, oklch(18% 0.02 268) 40%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu ul {
|
||
|
|
display: grid;
|
||
|
|
gap: 2px;
|
||
|
|
list-style: none;
|
||
|
|
padding-inline-start: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu a {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: 0.7rem 0.9rem;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
font-weight: 550;
|
||
|
|
color: var(--ink);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu a svg {
|
||
|
|
width: 1.1rem;
|
||
|
|
height: 1.1rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.menu a:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu a[aria-current="page"] {
|
||
|
|
color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Primitives: buttons, chips, badges, cards ----------------------- */
|
||
|
|
|
||
|
|
.btn {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.6rem 1.2rem;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface-2);
|
||
|
|
color: var(--ink);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 620;
|
||
|
|
text-decoration: none;
|
||
|
|
cursor: pointer;
|
||
|
|
transition:
|
||
|
|
translate 0.18s ease,
|
||
|
|
box-shadow 0.18s ease,
|
||
|
|
background-color 0.18s ease,
|
||
|
|
border-color 0.18s ease,
|
||
|
|
color 0.18s ease,
|
||
|
|
filter 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn svg {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 2;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.btn:hover {
|
||
|
|
translate: 0 -1px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn--primary {
|
||
|
|
background: var(--gradient);
|
||
|
|
color: var(--on-accent);
|
||
|
|
border-color: transparent;
|
||
|
|
box-shadow: 0 8px 26px -8px var(--glow);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.btn--primary:hover {
|
||
|
|
filter: brightness(1.08);
|
||
|
|
box-shadow: 0 12px 32px -8px var(--glow);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn--ghost {
|
||
|
|
border-color: var(--line-strong);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.btn--ghost:hover {
|
||
|
|
border-color: var(--accent);
|
||
|
|
color: var(--accent);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn--danger {
|
||
|
|
color: var(--danger);
|
||
|
|
border-color: color-mix(in oklab, var(--danger) 35%, transparent);
|
||
|
|
background: transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.btn--danger:hover {
|
||
|
|
background: var(--danger);
|
||
|
|
border-color: var(--danger);
|
||
|
|
color: var(--surface);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn--sm {
|
||
|
|
padding: 0.32rem 0.8rem;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn[disabled] {
|
||
|
|
opacity: 0.55;
|
||
|
|
cursor: not-allowed;
|
||
|
|
translate: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The filter chip: a pill that toggles a list, and carries its weight in
|
||
|
|
a count when it has one. */
|
||
|
|
.chip {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.35rem;
|
||
|
|
padding: 0.32rem 0.85rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface);
|
||
|
|
box-shadow: var(--shadow-sm);
|
||
|
|
color: var(--ink-2);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
font-weight: 560;
|
||
|
|
cursor: pointer;
|
||
|
|
text-decoration: none;
|
||
|
|
transition:
|
||
|
|
border-color 0.15s ease,
|
||
|
|
background-color 0.15s ease,
|
||
|
|
color 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.chip:hover {
|
||
|
|
color: var(--ink);
|
||
|
|
border-color: var(--line-strong);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip[aria-pressed="true"] {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
border-color: color-mix(in oklab, var(--accent) 35%, transparent);
|
||
|
|
color: var(--accent);
|
||
|
|
font-weight: 650;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip__count {
|
||
|
|
color: var(--ink-3);
|
||
|
|
font-size: 0.72em;
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The language switch inside a card: the chip in the small mono form
|
||
|
|
the site's tags wear. */
|
||
|
|
.post__langs {
|
||
|
|
display: inline-flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 3px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip--lang {
|
||
|
|
padding: 0.1rem 0.55rem;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.7rem;
|
||
|
|
text-transform: lowercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The status badge: a tiny pill of meaning, mono where it is data. */
|
||
|
|
.tag {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.3rem;
|
||
|
|
padding: 0.16rem 0.6rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface-2);
|
||
|
|
color: var(--ink-2);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.7rem;
|
||
|
|
font-weight: 520;
|
||
|
|
letter-spacing: 0.03em;
|
||
|
|
text-transform: lowercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag--warn {
|
||
|
|
border-color: color-mix(in oklab, var(--warn) 40%, transparent);
|
||
|
|
background: color-mix(in oklab, var(--warn) 12%, var(--surface));
|
||
|
|
color: var(--warn);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag--accent {
|
||
|
|
border-color: color-mix(in oklab, var(--accent) 40%, transparent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag--ok {
|
||
|
|
border-color: color-mix(in oklab, var(--ok) 40%, transparent);
|
||
|
|
background: color-mix(in oklab, var(--ok) 12%, var(--surface));
|
||
|
|
color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The card: the one surface every panel, tile and dialog body wears. */
|
||
|
|
.card {
|
||
|
|
position: relative;
|
||
|
|
display: grid;
|
||
|
|
align-content: start;
|
||
|
|
gap: var(--space-md);
|
||
|
|
padding: var(--space-lg);
|
||
|
|
border: 1px solid color-mix(in oklab, var(--line) 88%, transparent);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: color-mix(in oklab, var(--surface) 88%, transparent);
|
||
|
|
box-shadow: var(--shadow-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card + .card {
|
||
|
|
margin-top: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card--flush {
|
||
|
|
padding: 0;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The flush card's head is inset like in every other card; only the rows
|
||
|
|
beneath it run edge to edge. Without this the head sits on the border
|
||
|
|
and its note is clipped by the card's own overflow. */
|
||
|
|
.card--flush > .card__head {
|
||
|
|
padding: var(--space-lg) var(--space-lg) 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.card__head {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: baseline;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card__head h2 {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card__note {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card__icon {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
width: 2.4rem;
|
||
|
|
height: 2.4rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
border-radius: 12px;
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card__icon svg {
|
||
|
|
width: 1.25rem;
|
||
|
|
height: 1.25rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The status strip: a sentence about what just happened. */
|
||
|
|
.status {
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
align-items: flex-start;
|
||
|
|
padding: var(--space-sm) var(--space-md);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-left: 3px solid var(--line-strong);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status svg {
|
||
|
|
width: 1.15rem;
|
||
|
|
height: 1.15rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
margin-top: 0.15rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A status separates from whatever follows it, not only from another
|
||
|
|
status or a form: the dashboard grid and the settings panels land
|
||
|
|
right beneath it too. */
|
||
|
|
.status + *,
|
||
|
|
form + .status {
|
||
|
|
margin-top: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status--ok {
|
||
|
|
border-color: color-mix(in oklab, var(--ok) 45%, transparent);
|
||
|
|
border-left-color: var(--ok);
|
||
|
|
background: color-mix(in oklab, var(--ok) 8%, var(--surface));
|
||
|
|
}
|
||
|
|
|
||
|
|
.status--error {
|
||
|
|
border-color: color-mix(in oklab, var(--danger) 45%, transparent);
|
||
|
|
border-left-color: var(--danger);
|
||
|
|
background: color-mix(in oklab, var(--danger) 8%, var(--surface));
|
||
|
|
}
|
||
|
|
|
||
|
|
.status--warn {
|
||
|
|
border-color: color-mix(in oklab, var(--warn) 45%, transparent);
|
||
|
|
border-left-color: var(--warn);
|
||
|
|
background: color-mix(in oklab, var(--warn) 10%, var(--surface));
|
||
|
|
}
|
||
|
|
|
||
|
|
.status--info {
|
||
|
|
border-color: color-mix(in oklab, var(--accent) 45%, transparent);
|
||
|
|
border-left-color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status__actions {
|
||
|
|
margin-inline-start: auto;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Forms ------------------------------------------------------------ */
|
||
|
|
|
||
|
|
/* A field packs to the top of whatever cell it sits in: the label
|
||
|
|
line, the control line and the notes below never drift, however
|
||
|
|
tall the neighbouring field grew (a stretched grid otherwise
|
||
|
|
distributes its free space into the rows and pushes the control
|
||
|
|
down). */
|
||
|
|
.field {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
align-content: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.field__label {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
.field__hint {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The wizard's hints sit on the login panel over the dotted lattice,
|
||
|
|
where the quiet grey reads below the contrast floor: they take the
|
||
|
|
second ink there. */
|
||
|
|
.setup .field__hint {
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.control {
|
||
|
|
width: 100%;
|
||
|
|
padding: 0.62rem 0.9rem;
|
||
|
|
border: 1px solid var(--line-strong);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
/* One leading for every control face: an input and a select of the
|
||
|
|
same class must stand at the same height side by side, which the
|
||
|
|
engines' own metrics do not give them. */
|
||
|
|
line-height: 1.5;
|
||
|
|
transition:
|
||
|
|
border-color 0.18s ease,
|
||
|
|
box-shadow 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.control:hover {
|
||
|
|
border-color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.control:focus {
|
||
|
|
border-color: var(--accent);
|
||
|
|
outline: none;
|
||
|
|
box-shadow: 0 0 0 3px var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
.control::placeholder {
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* One face height for the single-line controls: the engines give an
|
||
|
|
input and a select of the same class different boxes, and a mono
|
||
|
|
input stands taller than its proportional neighbour. A fixed height
|
||
|
|
is the only thing that stays equal across them, so a form's controls
|
||
|
|
and its button sit on one line. */
|
||
|
|
select.control,
|
||
|
|
input.control:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
|
||
|
|
height: 2.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
select.control.control--sm,
|
||
|
|
input.control.control--sm:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
|
||
|
|
height: 2.1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.control[readonly],
|
||
|
|
.control:disabled {
|
||
|
|
background: var(--surface-2);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
textarea.control {
|
||
|
|
min-height: 7rem;
|
||
|
|
resize: vertical;
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
textarea.control--mono {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.control[aria-invalid="true"],
|
||
|
|
.control.is-invalid {
|
||
|
|
border-color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.control.is-ok {
|
||
|
|
border-color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.field:has(.control.is-ok, .control.is-invalid) .field__hint.is-live {
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.control--sm {
|
||
|
|
padding: 0.3rem 0.6rem;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
width: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: auto 1fr;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
align-items: start;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.check input {
|
||
|
|
width: 1.05rem;
|
||
|
|
height: 1.05rem;
|
||
|
|
margin-top: 0.22rem;
|
||
|
|
accent-color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check span {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.check-row {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
input[type="file"] {
|
||
|
|
padding: 0.5rem 0.7rem;
|
||
|
|
width: 100%;
|
||
|
|
border: 1px dashed var(--line-strong);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink-2);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
input[type="file"]::file-selector-button {
|
||
|
|
margin-inline-end: var(--space-xs);
|
||
|
|
padding: 0.3rem 0.8rem;
|
||
|
|
border: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface-2);
|
||
|
|
color: var(--ink);
|
||
|
|
font: inherit;
|
||
|
|
font-weight: 620;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Tables ------------------------------------------------------------ */
|
||
|
|
|
||
|
|
.table {
|
||
|
|
width: 100%;
|
||
|
|
border-collapse: collapse;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.table th,
|
||
|
|
.table td {
|
||
|
|
padding: var(--space-2xs) var(--space-sm);
|
||
|
|
border-bottom: 1px solid var(--line);
|
||
|
|
text-align: start;
|
||
|
|
vertical-align: middle;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table th {
|
||
|
|
background: var(--surface-2);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
font-weight: 620;
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.table tbody tr:last-child td {
|
||
|
|
border-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.table tbody tr:hover td {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.table__actions {
|
||
|
|
text-align: end;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.table__actions form {
|
||
|
|
display: inline;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The dashboard ------------------------------------------------------ */
|
||
|
|
|
||
|
|
.stats {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
|
gap: 1px;
|
||
|
|
overflow: hidden;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--line);
|
||
|
|
box-shadow: var(--shadow-sm);
|
||
|
|
margin-bottom: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (min-width: 48rem) {
|
||
|
|
.stats {
|
||
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats > div {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
padding: var(--space-md);
|
||
|
|
background: color-mix(in oklab, var(--surface) 92%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats dd {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: clamp(1.6rem, 2vw + 0.9rem, 2.1rem);
|
||
|
|
font-weight: 700;
|
||
|
|
line-height: 1.1;
|
||
|
|
background: var(--gradient);
|
||
|
|
background-clip: text;
|
||
|
|
color: transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stats dt {
|
||
|
|
order: 2;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
font-weight: 560;
|
||
|
|
/* The caption sits on the washed cell, so it takes the ink the body
|
||
|
|
text takes: the quiet grey reads below the contrast floor there. */
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The quick list: recent and upcoming posts on the dashboard. */
|
||
|
|
.quick {
|
||
|
|
display: grid;
|
||
|
|
gap: 1px;
|
||
|
|
overflow: hidden;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.quick__row {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding: var(--space-sm) var(--space-md);
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink);
|
||
|
|
text-decoration: none;
|
||
|
|
min-width: 0;
|
||
|
|
transition: background-color 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.quick__row:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.quick__title {
|
||
|
|
font-weight: 550;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.quick__when {
|
||
|
|
flex-shrink: 0;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The toolbar over a list -------------------------------------------- */
|
||
|
|
|
||
|
|
.toolbar {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
margin-block: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.search {
|
||
|
|
position: relative;
|
||
|
|
flex: 1;
|
||
|
|
max-width: 24rem;
|
||
|
|
min-width: 12rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search .control {
|
||
|
|
padding-inline-start: 2.4rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search > svg {
|
||
|
|
position: absolute;
|
||
|
|
left: 0.85rem;
|
||
|
|
top: 50%;
|
||
|
|
translate: 0 -50%;
|
||
|
|
width: 1.05rem;
|
||
|
|
height: 1.05rem;
|
||
|
|
color: var(--ink-3);
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filters {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The filter groups carry a leading glyph naming what they filter by. */
|
||
|
|
.filters > svg {
|
||
|
|
width: 1.05rem;
|
||
|
|
height: 1.05rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
color: var(--ink-3);
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toolbar__count {
|
||
|
|
margin-inline-start: auto;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toolbar__count.is-empty {
|
||
|
|
color: var(--warn);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The posts grid ------------------------------------------------------ */
|
||
|
|
|
||
|
|
.post-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.post {
|
||
|
|
position: relative;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
border: 1px solid color-mix(in oklab, var(--line) 88%, transparent);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: color-mix(in oklab, var(--surface) 88%, transparent);
|
||
|
|
box-shadow: var(--shadow-sm);
|
||
|
|
overflow: hidden;
|
||
|
|
transition:
|
||
|
|
translate 0.22s ease,
|
||
|
|
box-shadow 0.22s ease,
|
||
|
|
border-color 0.22s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.post:hover {
|
||
|
|
translate: 0 -3px;
|
||
|
|
border-color: color-mix(in oklab, var(--accent) 30%, var(--line));
|
||
|
|
box-shadow: var(--shadow-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.post:hover .post__title {
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.post.is-hidden {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__link {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
flex: 1;
|
||
|
|
min-height: 0;
|
||
|
|
color: inherit;
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__cover {
|
||
|
|
aspect-ratio: 16 / 9;
|
||
|
|
background: var(--surface-2) var(--cover-url, none) center / cover;
|
||
|
|
border-bottom: 1px solid var(--line);
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__cover svg {
|
||
|
|
width: 1.9rem;
|
||
|
|
height: 1.9rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.5;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__cover[data-cover] svg {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__body {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
align-content: start;
|
||
|
|
padding: var(--space-md);
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__title {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
letter-spacing: -0.015em;
|
||
|
|
transition: color 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__excerpt {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--ink-2);
|
||
|
|
display: -webkit-box;
|
||
|
|
-webkit-line-clamp: 2;
|
||
|
|
-webkit-box-orient: vertical;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__meta {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
align-items: center;
|
||
|
|
margin-top: auto;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__date {
|
||
|
|
margin-inline-start: auto;
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
/* The card's washed surface costs the quiet grey its contrast: the
|
||
|
|
date is text, so it takes the second ink to clear the floor. */
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__check {
|
||
|
|
position: absolute;
|
||
|
|
top: var(--space-2xs);
|
||
|
|
left: var(--space-2xs);
|
||
|
|
z-index: 2;
|
||
|
|
display: inline-flex;
|
||
|
|
padding: 0.32rem 0.42rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: color-mix(in oklab, var(--surface) 85%, transparent);
|
||
|
|
backdrop-filter: blur(6px);
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__check input {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
margin: 0;
|
||
|
|
accent-color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.post:hover .post__check,
|
||
|
|
.post:focus-within .post__check {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__check:has(input:checked) {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Touch devices have no hover: keep the bulk checkboxes reachable. */
|
||
|
|
@media (hover: none) {
|
||
|
|
.post__check {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__foot {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
padding: var(--space-2xs) var(--space-xs);
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
background: color-mix(in oklab, var(--surface-2) 60%, var(--surface));
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__foot .icon-btn {
|
||
|
|
width: 2rem;
|
||
|
|
height: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.post__foot .icon-btn svg {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The select-all rides at the start of the toolbar. */
|
||
|
|
.bulk-all {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 0.45rem 0.55rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bulk-all input {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
margin: 0;
|
||
|
|
accent-color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bulk-all.is-empty {
|
||
|
|
visibility: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The bulk bar: what the selection can do. */
|
||
|
|
.bulk-bar {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: var(--space-2xs) var(--space-sm);
|
||
|
|
border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--accent-wash);
|
||
|
|
margin-bottom: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.bulk-bar__label {
|
||
|
|
padding-inline: var(--space-2xs);
|
||
|
|
color: var(--accent);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
.bulk-bar__spacer {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The empty state ----------------------------------------------------- */
|
||
|
|
|
||
|
|
.empty {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
padding: var(--space-2xl) var(--space-md);
|
||
|
|
border: 1px dashed var(--line-strong);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
color: var(--ink-2);
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty svg {
|
||
|
|
width: 2.2rem;
|
||
|
|
height: 2.2rem;
|
||
|
|
color: var(--ink-3);
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.5;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty__note {
|
||
|
|
max-width: 42ch;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A no-results state inside a grid spans the whole row. */
|
||
|
|
.empty--span {
|
||
|
|
grid-column: 1 / -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The editor ----------------------------------------------------------- */
|
||
|
|
|
||
|
|
/* The tab strip: a pill track with the raised active face, the same
|
||
|
|
control language as the language switch. */
|
||
|
|
.tabs {
|
||
|
|
display: inline-flex;
|
||
|
|
gap: 2px;
|
||
|
|
align-self: start;
|
||
|
|
padding: 3px;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tabs button {
|
||
|
|
padding: 0.35rem 0.95rem;
|
||
|
|
border: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: transparent;
|
||
|
|
color: var(--ink-3);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 560;
|
||
|
|
cursor: pointer;
|
||
|
|
transition:
|
||
|
|
color 0.15s ease,
|
||
|
|
background-color 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.tabs button:hover {
|
||
|
|
color: var(--ink);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.tabs button[aria-selected="true"] {
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink);
|
||
|
|
box-shadow: var(--shadow-sm);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The writing column: one wide pane unless the live preview needs both. */
|
||
|
|
.editor:not(.is-preview) {
|
||
|
|
max-width: 68rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.narrow-start {
|
||
|
|
max-width: 20rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor.is-preview {
|
||
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 70rem) {
|
||
|
|
.editor.is-preview {
|
||
|
|
grid-template-columns: minmax(0, 1fr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__pane {
|
||
|
|
position: relative;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
min-width: 0;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__head {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: 2px;
|
||
|
|
padding: var(--space-3xs) var(--space-xs);
|
||
|
|
border-bottom: 1px solid var(--line);
|
||
|
|
background: color-mix(in oklab, var(--surface-2) 60%, var(--surface));
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__label {
|
||
|
|
padding-inline: var(--space-2xs);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.66rem;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.12em;
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__head .flex {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
min-width: 1.9rem;
|
||
|
|
height: 1.9rem;
|
||
|
|
padding: 0 0.5rem;
|
||
|
|
border: 0;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: transparent;
|
||
|
|
color: var(--ink-2);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
cursor: pointer;
|
||
|
|
transition:
|
||
|
|
background-color 0.12s ease,
|
||
|
|
color 0.12s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool svg {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.tool:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--ink);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool.is-active {
|
||
|
|
background: var(--accent);
|
||
|
|
color: var(--on-accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool--bold {
|
||
|
|
font-weight: 700;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool--italic {
|
||
|
|
font-style: italic;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tool--wide {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.06em;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__input {
|
||
|
|
flex: 1;
|
||
|
|
border: 0;
|
||
|
|
border-radius: 0;
|
||
|
|
min-height: 32rem;
|
||
|
|
padding: var(--space-lg) var(--space-xl);
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
line-height: 1.7;
|
||
|
|
resize: vertical;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__input:focus {
|
||
|
|
outline: none;
|
||
|
|
box-shadow: inset 0 0 0 2px var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__preview {
|
||
|
|
min-height: 32rem;
|
||
|
|
overflow: auto;
|
||
|
|
padding: var(--space-lg) var(--space-xl);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__status {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding: var(--space-2xs) var(--space-xs);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
letter-spacing: 0.03em;
|
||
|
|
color: var(--ink-3);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__status .flex {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__saved {
|
||
|
|
color: var(--ok);
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity 0.4s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.editor__pane.is-drop .editor__input {
|
||
|
|
box-shadow: inset 0 0 0 2px var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.drop-overlay {
|
||
|
|
position: absolute;
|
||
|
|
inset: 0;
|
||
|
|
z-index: 5;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
background: color-mix(in oklab, var(--accent-wash) 92%, transparent);
|
||
|
|
border: 2px dashed var(--accent);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
color: var(--accent);
|
||
|
|
font-weight: 700;
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.drop-overlay svg {
|
||
|
|
width: 1.2rem;
|
||
|
|
height: 1.2rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 2;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The cover field: a row of control and actions, and the pick. */
|
||
|
|
.cover-row {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.cover-row .control {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 12rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cover-preview {
|
||
|
|
max-height: 12rem;
|
||
|
|
width: auto;
|
||
|
|
max-width: 100%;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The bibliography card rides the standard field, tag and button
|
||
|
|
vocabulary; the only thing it adds is the bounded list, which scrolls
|
||
|
|
inside itself so a volume with a hundred references never stretches
|
||
|
|
the editor page past its own content. */
|
||
|
|
.refs-scroll {
|
||
|
|
max-height: 32rem;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding-block-end: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.ref-row {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.ref-row + .ref-row {
|
||
|
|
margin-top: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.ref-row__head {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The authors and the title run the full width inside the structured
|
||
|
|
grid; everything else pairs up in the two columns. */
|
||
|
|
.ref-row .field--wide {
|
||
|
|
grid-column: 1 / -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The rendered Markdown: one reading column, the article set. */
|
||
|
|
.prose {
|
||
|
|
max-width: var(--measure);
|
||
|
|
line-height: 1.75;
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose > * + * {
|
||
|
|
margin-top: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose h1,
|
||
|
|
.prose h2,
|
||
|
|
.prose h3,
|
||
|
|
.prose h4 {
|
||
|
|
margin-top: var(--space-xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose h2 {
|
||
|
|
font-size: var(--text-2xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose h3 {
|
||
|
|
font-size: var(--text-xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose ul,
|
||
|
|
.prose ol {
|
||
|
|
padding-left: 1.4rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose li + li {
|
||
|
|
margin-top: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose blockquote {
|
||
|
|
padding: var(--space-sm) var(--space-md);
|
||
|
|
border-left: 3px solid var(--accent);
|
||
|
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose pre {
|
||
|
|
overflow-x: auto;
|
||
|
|
padding: var(--space-md);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface-2);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose pre code {
|
||
|
|
padding: 0;
|
||
|
|
border: 0;
|
||
|
|
background: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose table {
|
||
|
|
width: 100%;
|
||
|
|
border-collapse: collapse;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose th,
|
||
|
|
.prose td {
|
||
|
|
padding: var(--space-2xs) var(--space-sm);
|
||
|
|
border-bottom: 1px solid var(--line);
|
||
|
|
text-align: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose th {
|
||
|
|
background: var(--surface-2);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose img {
|
||
|
|
max-width: 100%;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose hr {
|
||
|
|
border: 0;
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.prose [contenteditable]:focus {
|
||
|
|
outline: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Bibliography -------------------------------------------------------- */
|
||
|
|
/* The reference list the engine splices at the [[refs]] marker, and the
|
||
|
|
inline [n] citations that point into it. */
|
||
|
|
|
||
|
|
.refs {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.refs ol {
|
||
|
|
padding-left: 1.6rem;
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.refs li {
|
||
|
|
text-indent: -0.2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.ref-cite {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.82em;
|
||
|
|
text-decoration: none;
|
||
|
|
color: var(--accent);
|
||
|
|
vertical-align: 0.2em;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.ref-cite:hover {
|
||
|
|
text-decoration: underline;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.refs-link {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.86em;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The drop zone -------------------------------------------------------- */
|
||
|
|
|
||
|
|
.dropzone {
|
||
|
|
position: relative;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: var(--space-2xl) var(--space-lg) var(--space-xl);
|
||
|
|
border: 2px dashed var(--line-strong);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
text-align: center;
|
||
|
|
cursor: pointer;
|
||
|
|
transition:
|
||
|
|
border-color 0.16s ease,
|
||
|
|
background-color 0.16s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone--row {
|
||
|
|
flex-direction: row;
|
||
|
|
text-align: start;
|
||
|
|
padding: var(--space-md) var(--space-lg);
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__input {
|
||
|
|
position: absolute;
|
||
|
|
width: 1px;
|
||
|
|
height: 1px;
|
||
|
|
opacity: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.dropzone:hover {
|
||
|
|
border-color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone:focus-visible {
|
||
|
|
border-color: var(--accent);
|
||
|
|
outline: none;
|
||
|
|
box-shadow: 0 0 0 3px var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone.is-over {
|
||
|
|
border-style: solid;
|
||
|
|
border-color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone.has-file {
|
||
|
|
border-style: solid;
|
||
|
|
border-color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone.is-rejected {
|
||
|
|
border-style: solid;
|
||
|
|
border-color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__icon {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
width: 3.2rem;
|
||
|
|
height: 3.2rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__icon svg {
|
||
|
|
width: 1.5rem;
|
||
|
|
height: 1.5rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone--row .dropzone__icon {
|
||
|
|
width: 2.4rem;
|
||
|
|
height: 2.4rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The drop zone's text stack: spans, so they need the block flow. */
|
||
|
|
.dropzone > span:not(.dropzone__icon) {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__title {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
font-weight: 650;
|
||
|
|
letter-spacing: -0.015em;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__sub {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__hint {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
letter-spacing: 0.04em;
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dropzone__error {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 600;
|
||
|
|
color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The file the drop zone accepted. */
|
||
|
|
.file-row {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding: var(--space-sm) var(--space-md);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-row__ok {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
width: 2rem;
|
||
|
|
height: 2rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: color-mix(in oklab, var(--ok) 14%, var(--surface));
|
||
|
|
color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-row__ok svg {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 2.5;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-row__info {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-row__name {
|
||
|
|
font-weight: 620;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.file-row__meta {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
.kv {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: auto minmax(0, 1fr);
|
||
|
|
gap: var(--space-3xs) var(--space-md);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.kv dt {
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.kv dd {
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.break-all {
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The hint that reports a state: the slug check, the schedule warning. */
|
||
|
|
.field__hint.hint--ok {
|
||
|
|
color: var(--ok);
|
||
|
|
font-weight: 550;
|
||
|
|
}
|
||
|
|
|
||
|
|
.field__hint.hint--error {
|
||
|
|
color: var(--danger);
|
||
|
|
font-weight: 550;
|
||
|
|
}
|
||
|
|
|
||
|
|
.field__hint.hint--warn {
|
||
|
|
color: var(--warn);
|
||
|
|
font-weight: 550;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Media ----------------------------------------------------------------- */
|
||
|
|
|
||
|
|
.media-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The tiles are rows of this grid, not stacked cards: the sibling
|
||
|
|
margin the plain card flow earns would drop every row but the first
|
||
|
|
below its neighbours, and the grid gap already spaces them. */
|
||
|
|
.media-grid > .card + .card {
|
||
|
|
margin-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: var(--space-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile.is-hidden {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__thumb {
|
||
|
|
aspect-ratio: 4 / 3;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
overflow: hidden;
|
||
|
|
background: var(--surface-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__thumb img {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__name {
|
||
|
|
font-weight: 560;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__meta {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__actions {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 2px;
|
||
|
|
margin-top: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__actions .icon-btn {
|
||
|
|
width: 1.9rem;
|
||
|
|
height: 1.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.media-tile__actions .icon-btn svg {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Settings ---------------------------------------------------------------- */
|
||
|
|
|
||
|
|
.settings {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-lg);
|
||
|
|
align-items: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (min-width: 56rem) {
|
||
|
|
.settings {
|
||
|
|
grid-template-columns: 15rem minmax(0, 1fr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The panels switch one at a time, so the card rule's sibling margin
|
||
|
|
would push every section but the first 24 px below the nav even while
|
||
|
|
its hidden siblings carry no weight. The grid gap spaces the no-JS
|
||
|
|
page, where all the sections show at once. */
|
||
|
|
.settings__panels {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.settings__panels > .card + .card {
|
||
|
|
margin-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.settings__nav {
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
overflow-x: auto;
|
||
|
|
padding: var(--space-3xs);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (min-width: 56rem) {
|
||
|
|
.settings__nav {
|
||
|
|
display: grid;
|
||
|
|
position: sticky;
|
||
|
|
top: calc(var(--header-h) + var(--space-sm));
|
||
|
|
overflow: visible;
|
||
|
|
padding: var(--space-2xs);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.settings__link {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: 0.5rem 0.9rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
color: var(--ink-2);
|
||
|
|
font-weight: 550;
|
||
|
|
text-decoration: none;
|
||
|
|
white-space: nowrap;
|
||
|
|
transition:
|
||
|
|
color 0.15s ease,
|
||
|
|
background-color 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.settings__link svg {
|
||
|
|
width: 1.05rem;
|
||
|
|
height: 1.05rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.settings__link:hover {
|
||
|
|
color: var(--ink);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.settings__link[aria-current="true"] {
|
||
|
|
color: var(--accent);
|
||
|
|
background: var(--accent-wash);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* One setting: the icon square, the body, and its controls. */
|
||
|
|
.setting {
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-md);
|
||
|
|
padding: var(--space-md) 0;
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.card > .setting:first-of-type {
|
||
|
|
border-top: 0;
|
||
|
|
padding-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 40rem) {
|
||
|
|
.setting {
|
||
|
|
flex-direction: column;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting__body {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
max-width: 46rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting__title {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting__desc {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A settings form is the site's form contract: fields stack, their
|
||
|
|
notes hang under the control they belong to, and the action closes
|
||
|
|
the form on its own line. Nothing ever sits beside a labelled
|
||
|
|
field, so no hint, meter or any element yet to come can push an
|
||
|
|
action off the control's line. */
|
||
|
|
.setting__row {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.setting__row > button {
|
||
|
|
justify-self: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The template list: one row per template, its name and pre-filled
|
||
|
|
fields on the left and the delete action on the right, parted by the
|
||
|
|
row rule the user rows wear. */
|
||
|
|
.kv-stack {
|
||
|
|
display: grid;
|
||
|
|
}
|
||
|
|
|
||
|
|
.kv-row {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding: var(--space-sm) 0;
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.kv-row:first-child {
|
||
|
|
border-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.kv-row > span {
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The config example wears the code-block face the prose pre wears:
|
||
|
|
mono on the quiet surface, scrolling sideways rather than wrapping. */
|
||
|
|
.webhook-example {
|
||
|
|
overflow-x: auto;
|
||
|
|
padding: var(--space-md);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface-2);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
line-height: 1.6;
|
||
|
|
}
|
||
|
|
|
||
|
|
.webhook-example code {
|
||
|
|
padding: 0;
|
||
|
|
border: 0;
|
||
|
|
background: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pill-group {
|
||
|
|
display: inline-flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A user of the instance, one row in the card. */
|
||
|
|
.user-row {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding: var(--space-sm) var(--space-lg);
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row:first-child {
|
||
|
|
border-top: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__info {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
display: grid;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__name {
|
||
|
|
font-weight: 620;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__username {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__actions {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__reset {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__reset summary {
|
||
|
|
list-style: none;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__reset summary::-webkit-details-marker {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__reset[open] summary {
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The reset popover wears the same form contract as every other
|
||
|
|
form: the field, its meter, then the action on its own line. */
|
||
|
|
.user-row__reset-form {
|
||
|
|
position: absolute;
|
||
|
|
inset-inline-end: 0;
|
||
|
|
top: calc(100% + var(--space-3xs));
|
||
|
|
z-index: 5;
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
min-width: 20rem;
|
||
|
|
padding: var(--space-xs);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-row__reset-form > button {
|
||
|
|
justify-self: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The second factor block inside Security: the QR is a white card
|
||
|
|
whatever the scheme, because a camera refuses a symbol that
|
||
|
|
follows the page's colours. The recovery codes sit on it once. */
|
||
|
|
.totp {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding-top: var(--space-md);
|
||
|
|
border-top: 1px dashed var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__title {
|
||
|
|
font-size: var(--text-base);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__setup {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__qr {
|
||
|
|
justify-self: start;
|
||
|
|
padding: var(--space-xs);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: #fff;
|
||
|
|
box-shadow: var(--shadow-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__qr svg {
|
||
|
|
display: block;
|
||
|
|
width: 11rem;
|
||
|
|
height: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__setup-side {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
justify-items: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__secret-label {
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 620;
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__secret {
|
||
|
|
padding: 0.3rem 0.6rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface-2);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
letter-spacing: 0.05em;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__form {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
justify-items: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__forms {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (min-width: 40rem) {
|
||
|
|
.totp__setup {
|
||
|
|
grid-template-columns: auto minmax(0, 1fr);
|
||
|
|
align-items: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.totp__forms {
|
||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.recovery {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (min-width: 30rem) {
|
||
|
|
.recovery {
|
||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.recovery__code {
|
||
|
|
padding: 0.35rem 0.6rem;
|
||
|
|
border: 1px dashed var(--line-strong);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
letter-spacing: 0.08em;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The add-user form closes the flush users card: it carries the row's
|
||
|
|
own padding and divider, so it reads as the last row of the list
|
||
|
|
rather than a form pasted to the card's edge. */
|
||
|
|
.user-add {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
padding: var(--space-sm) var(--space-lg) var(--space-lg);
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The button is a button, not a bar: it keeps its own width. */
|
||
|
|
.user-add > button {
|
||
|
|
justify-self: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The photo pick. */
|
||
|
|
.photo-row {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.photo-row__preview {
|
||
|
|
width: 5rem;
|
||
|
|
height: 5rem;
|
||
|
|
flex-shrink: 0;
|
||
|
|
border: 2px solid var(--line);
|
||
|
|
border-radius: 999px;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
.photo-row__placeholder {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
font-size: var(--text-xl);
|
||
|
|
font-weight: 650;
|
||
|
|
}
|
||
|
|
|
||
|
|
.photo-row__fields {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 14rem;
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.photo-row__buttons {
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- History and diff ------------------------------------------------------- */
|
||
|
|
|
||
|
|
.diff {
|
||
|
|
padding: var(--space-sm) 0;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
line-height: 1.6;
|
||
|
|
overflow-x: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff__line {
|
||
|
|
padding: 0.05rem var(--space-md);
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-word;
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff__sign {
|
||
|
|
display: inline-block;
|
||
|
|
width: 1.4em;
|
||
|
|
user-select: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff__line--removed {
|
||
|
|
background: color-mix(in oklab, var(--danger) 10%, transparent);
|
||
|
|
color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff__line--added {
|
||
|
|
background: color-mix(in oklab, var(--ok) 10%, transparent);
|
||
|
|
color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff__line--equal {
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff__line--skipped {
|
||
|
|
background: var(--surface-2);
|
||
|
|
color: var(--ink-3);
|
||
|
|
text-align: center;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.diff-legend {
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
margin-bottom: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- Toasts, dialog, spinner, date picker ------------------------------------ */
|
||
|
|
|
||
|
|
.toast-stack {
|
||
|
|
position: fixed;
|
||
|
|
inset-block-end: var(--space-md);
|
||
|
|
inset-inline-end: var(--space-md);
|
||
|
|
z-index: 100;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
max-width: min(22rem, calc(100% - 2rem));
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
padding: var(--space-2xs) var(--space-sm);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-inline-start: 3px solid var(--accent);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
opacity: 0;
|
||
|
|
translate: 0 0.5rem;
|
||
|
|
pointer-events: auto;
|
||
|
|
transition:
|
||
|
|
opacity 0.18s ease,
|
||
|
|
translate 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast.is-visible {
|
||
|
|
opacity: 1;
|
||
|
|
translate: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast--error {
|
||
|
|
border-inline-start-color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast--warn {
|
||
|
|
border-inline-start-color: var(--warn);
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast--ok {
|
||
|
|
border-inline-start-color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast__text {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.toast__action {
|
||
|
|
padding: 0.2rem 0.7rem;
|
||
|
|
border: 1px solid var(--line-strong);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: transparent;
|
||
|
|
color: var(--accent);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
font-weight: 620;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.toast__action:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The native modal: focus handling, Escape and the top layer for free.
|
||
|
|
The reset zeroes every element's margin, which would also cancel the
|
||
|
|
user agent's dialog centring and park the modal in the corner, so the
|
||
|
|
centring is restated here. */
|
||
|
|
dialog {
|
||
|
|
width: min(100% - 2rem, 30rem);
|
||
|
|
margin: auto;
|
||
|
|
padding: 0;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-lg);
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
}
|
||
|
|
|
||
|
|
dialog::backdrop {
|
||
|
|
background: color-mix(in oklab, oklch(18% 0.02 268) 45%, transparent);
|
||
|
|
backdrop-filter: blur(4px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dialog__head {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
padding: var(--space-md) var(--space-lg);
|
||
|
|
border-bottom: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dialog__head h2,
|
||
|
|
.dialog__head h3 {
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dialog__body {
|
||
|
|
padding: var(--space-lg);
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.dialog__foot {
|
||
|
|
display: flex;
|
||
|
|
justify-content: flex-end;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
padding: var(--space-md) var(--space-lg);
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.spinner {
|
||
|
|
width: 1.75rem;
|
||
|
|
height: 1.75rem;
|
||
|
|
margin-inline: auto;
|
||
|
|
border: 3px solid var(--line);
|
||
|
|
border-top-color: var(--accent);
|
||
|
|
border-radius: 999px;
|
||
|
|
animation: spin 0.8s linear infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes spin {
|
||
|
|
to {
|
||
|
|
transform: rotate(360deg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The custom date picker: a trigger that opens a calendar popover built
|
||
|
|
by the page script, with the native input kept in the DOM. */
|
||
|
|
.date-input {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__native {
|
||
|
|
display: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__btn {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
width: 100%;
|
||
|
|
padding: 0.62rem 0.9rem;
|
||
|
|
border: 1px solid var(--line-strong);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: var(--surface);
|
||
|
|
color: var(--ink);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
text-align: start;
|
||
|
|
cursor: pointer;
|
||
|
|
transition:
|
||
|
|
border-color 0.18s ease,
|
||
|
|
box-shadow 0.18s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__btn:hover {
|
||
|
|
border-color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__btn:focus-visible {
|
||
|
|
outline: none;
|
||
|
|
border-color: var(--accent);
|
||
|
|
box-shadow: 0 0 0 3px var(--accent-wash);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__btn[data-empty="1"] {
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__label {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__icon {
|
||
|
|
display: inline-flex;
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__icon svg {
|
||
|
|
width: 1rem;
|
||
|
|
height: 1rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__popover {
|
||
|
|
position: absolute;
|
||
|
|
z-index: 20;
|
||
|
|
inset-block-start: calc(100% + 6px);
|
||
|
|
inset-inline-start: 0;
|
||
|
|
min-width: 18rem;
|
||
|
|
padding: var(--space-sm);
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius);
|
||
|
|
background: var(--surface);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
user-select: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__nav {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3xs);
|
||
|
|
margin-bottom: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__nav-title {
|
||
|
|
flex: 1;
|
||
|
|
text-align: center;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-weight: 620;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__nav-btn {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
width: 1.8rem;
|
||
|
|
height: 1.8rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: transparent;
|
||
|
|
color: var(--ink-2);
|
||
|
|
cursor: pointer;
|
||
|
|
transition:
|
||
|
|
background-color 0.12s ease,
|
||
|
|
color 0.12s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.date-input__nav-btn:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__nav-btn svg {
|
||
|
|
width: 0.9rem;
|
||
|
|
height: 0.9rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 2;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__weekdays,
|
||
|
|
.date-input__days {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(7, 1fr);
|
||
|
|
gap: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__weekday {
|
||
|
|
padding-block: var(--space-3xs);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__day {
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
height: 2.1rem;
|
||
|
|
border: 0;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: transparent;
|
||
|
|
color: var(--ink);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background-color 0.1s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.date-input__day:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__day[data-other-month="1"] {
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__day[data-today="1"] {
|
||
|
|
box-shadow: inset 0 0 0 1px var(--line-strong);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__day[data-selected="1"] {
|
||
|
|
background: var(--accent);
|
||
|
|
color: var(--on-accent);
|
||
|
|
font-weight: 650;
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__footer {
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
margin-top: var(--space-2xs);
|
||
|
|
padding-top: var(--space-2xs);
|
||
|
|
border-top: 1px solid var(--line);
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__footer-btn {
|
||
|
|
flex: 1;
|
||
|
|
padding: 0.4rem 0.6rem;
|
||
|
|
border: 1px solid var(--line);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: transparent;
|
||
|
|
color: var(--ink-2);
|
||
|
|
font: inherit;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
font-weight: 620;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.date-input__footer-btn:hover {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.date-input__footer-btn--primary {
|
||
|
|
background: var(--accent);
|
||
|
|
border-color: var(--accent);
|
||
|
|
color: var(--on-accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.date-input__footer-btn--primary:hover {
|
||
|
|
filter: brightness(1.08);
|
||
|
|
color: var(--on-accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --- The login page ----------------------------------------------------------- */
|
||
|
|
|
||
|
|
.login {
|
||
|
|
position: relative;
|
||
|
|
min-height: 100svh;
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
padding: var(--space-xl) var(--gutter);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The printed dot lattice the site wears across the top of its pages. */
|
||
|
|
.login::before {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
inset: 0 0 auto 0;
|
||
|
|
height: min(46rem, 90svh);
|
||
|
|
z-index: -1;
|
||
|
|
pointer-events: none;
|
||
|
|
background: radial-gradient(
|
||
|
|
light-dark(color-mix(in oklab, var(--ink-3) 62%, transparent), color-mix(in oklab, var(--ink-2) 42%, transparent)) 1.3px,
|
||
|
|
transparent 1.5px
|
||
|
|
);
|
||
|
|
background-size: 26px 26px;
|
||
|
|
mask-image: linear-gradient(to bottom, black 60%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__tools {
|
||
|
|
position: fixed;
|
||
|
|
top: var(--space-md);
|
||
|
|
inset-inline-end: var(--space-md);
|
||
|
|
display: flex;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__panel {
|
||
|
|
width: 100%;
|
||
|
|
max-width: 27rem;
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-lg);
|
||
|
|
padding: var(--space-xl);
|
||
|
|
border: 1px solid color-mix(in oklab, var(--line) 88%, transparent);
|
||
|
|
border-radius: var(--radius-lg);
|
||
|
|
background: color-mix(in oklab, var(--surface) 92%, transparent);
|
||
|
|
box-shadow: var(--shadow-lg);
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__brand {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
justify-items: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__mark {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
font-size: var(--text-xl);
|
||
|
|
font-weight: 750;
|
||
|
|
letter-spacing: -0.025em;
|
||
|
|
color: var(--ink);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__mark img {
|
||
|
|
width: 2rem;
|
||
|
|
height: 2rem;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__pitch {
|
||
|
|
color: var(--ink-2);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
}
|
||
|
|
|
||
|
|
.eyebrow {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
padding: 0.35rem 0.9rem;
|
||
|
|
border: 1px solid var(--line-strong);
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.7rem;
|
||
|
|
font-weight: 560;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.08em;
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.eyebrow::before {
|
||
|
|
content: "";
|
||
|
|
width: 0.45rem;
|
||
|
|
height: 0.45rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.grad {
|
||
|
|
background: var(--gradient);
|
||
|
|
background-clip: text;
|
||
|
|
color: transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__title {
|
||
|
|
font-size: var(--text-2xl);
|
||
|
|
letter-spacing: -0.028em;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__card {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__submit {
|
||
|
|
width: 100%;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__foot {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
gap: var(--space-xs);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__foot a {
|
||
|
|
color: var(--ink-2);
|
||
|
|
text-decoration: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.login__foot a:hover {
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.login__version {
|
||
|
|
text-align: center;
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
letter-spacing: 0.04em;
|
||
|
|
/* The panel sits on the dotted lattice, so the quiet grey reads
|
||
|
|
below the contrast floor: the version takes the second ink. */
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The password reveal rides inside its field. */
|
||
|
|
.pw {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw .control {
|
||
|
|
padding-inline-end: 2.7rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw__toggle {
|
||
|
|
position: absolute;
|
||
|
|
inset-block-start: 50%;
|
||
|
|
inset-inline-end: 0.4rem;
|
||
|
|
translate: 0 -50%;
|
||
|
|
width: 2rem;
|
||
|
|
height: 2rem;
|
||
|
|
display: grid;
|
||
|
|
place-items: center;
|
||
|
|
border: 0;
|
||
|
|
border-radius: var(--radius-sm);
|
||
|
|
background: none;
|
||
|
|
color: var(--ink-3);
|
||
|
|
cursor: pointer;
|
||
|
|
transition: color 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (hover: hover) {
|
||
|
|
.pw__toggle:hover {
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw__toggle svg {
|
||
|
|
width: 1.05rem;
|
||
|
|
height: 1.05rem;
|
||
|
|
fill: none;
|
||
|
|
stroke: currentColor;
|
||
|
|
stroke-width: 1.7;
|
||
|
|
stroke-linecap: round;
|
||
|
|
stroke-linejoin: round;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The first-run wizard: fields gathered into groups, each named by a
|
||
|
|
quiet mono legend the way the rail labels its sections. The card
|
||
|
|
grows a little wider than the login's, its fields warrant it. */
|
||
|
|
.login__panel:has(.setup) {
|
||
|
|
max-width: 32rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setup__group {
|
||
|
|
display: grid;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
border: 0;
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The two columns of the account row keep their inputs on one axis
|
||
|
|
whatever the language: the field is stretched by the row and its
|
||
|
|
label-and-input stack sits at the bottom, so a label that wraps
|
||
|
|
to a second line grows upward and the inputs stay level. */
|
||
|
|
.setup .grid-2 .field {
|
||
|
|
align-content: end;
|
||
|
|
}
|
||
|
|
|
||
|
|
.setup__group > legend {
|
||
|
|
padding: 0 0 var(--space-2xs);
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.7rem;
|
||
|
|
font-weight: 560;
|
||
|
|
text-transform: uppercase;
|
||
|
|
letter-spacing: 0.08em;
|
||
|
|
/* The legend names its group on the panel surface, so it takes the
|
||
|
|
second ink: the quiet grey reads below the contrast floor there. */
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The theme chips wear their palette: the gradient stops are the
|
||
|
|
official entries of each colour map, viridis 0/192/255, plasma
|
||
|
|
0/192/255 and magma 0/192/255, the same values the palette token
|
||
|
|
blocks above cite. */
|
||
|
|
.chip--theme {
|
||
|
|
gap: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.pill-group--themes {
|
||
|
|
gap: var(--space-xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip__swatch {
|
||
|
|
flex: none;
|
||
|
|
width: 2rem;
|
||
|
|
height: 0.75rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--ink) 18%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip__swatch--viridis {
|
||
|
|
background: linear-gradient(100deg, #440154, #5ec962, #fde725);
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip__swatch--plasma {
|
||
|
|
background: linear-gradient(100deg, #0d0887, #f89540, #f0f921);
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip__swatch--magma {
|
||
|
|
background: linear-gradient(100deg, #000004, #fc8961, #fcfdbf);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* The strength meter of a password field: four segments and a word.
|
||
|
|
The bar is decorative, the label carries the news for assistive
|
||
|
|
tech, and the server's policy remains the gate. */
|
||
|
|
.pw-level {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-sm);
|
||
|
|
margin-block-start: var(--space-2xs);
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level__bar {
|
||
|
|
display: flex;
|
||
|
|
flex: 1;
|
||
|
|
gap: 0.3rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level__bar i {
|
||
|
|
flex: 1;
|
||
|
|
height: 0.32rem;
|
||
|
|
border-radius: 999px;
|
||
|
|
background: var(--surface-2);
|
||
|
|
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--line) 60%, transparent);
|
||
|
|
transition: background 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level[data-level="1"] .pw-level__bar i:nth-child(-n+1) {
|
||
|
|
background: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level[data-level="2"] .pw-level__bar i:nth-child(-n+2) {
|
||
|
|
background: var(--warn);
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level[data-level="3"] .pw-level__bar i:nth-child(-n+3) {
|
||
|
|
background: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level[data-level="4"] .pw-level__bar i {
|
||
|
|
background: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.pw-level__label {
|
||
|
|
flex: none;
|
||
|
|
min-width: 5.5rem;
|
||
|
|
text-align: end;
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
font-weight: 560;
|
||
|
|
color: var(--ink-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A native radio wearing the chip face: the input stays in the form
|
||
|
|
so the submit is the same shape without JavaScript, the checked
|
||
|
|
state answers through :has(). */
|
||
|
|
.chip--radio {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip--radio input {
|
||
|
|
position: absolute;
|
||
|
|
width: 1px;
|
||
|
|
height: 1px;
|
||
|
|
margin: -1px;
|
||
|
|
overflow: hidden;
|
||
|
|
clip-path: inset(50%);
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip--radio:has(input:checked) {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
border-color: color-mix(in oklab, var(--accent) 35%, transparent);
|
||
|
|
color: var(--accent);
|
||
|
|
font-weight: 650;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip--radio:has(input:focus-visible) {
|
||
|
|
outline: 2px solid var(--accent);
|
||
|
|
outline-offset: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A chip that is a link choosing the page's language: the current
|
||
|
|
choice wears the same face the checked radios do, announced by
|
||
|
|
aria-current which the script moves with the swap. */
|
||
|
|
.chip--switch[aria-current="true"] {
|
||
|
|
background: var(--accent-wash);
|
||
|
|
border-color: color-mix(in oklab, var(--accent) 35%, transparent);
|
||
|
|
color: var(--accent);
|
||
|
|
font-weight: 650;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip--switch:focus-visible {
|
||
|
|
outline: 2px solid var(--accent);
|
||
|
|
outline-offset: 2px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@layer utilities {
|
||
|
|
.visually-hidden {
|
||
|
|
position: absolute;
|
||
|
|
width: 1px;
|
||
|
|
height: 1px;
|
||
|
|
margin: -1px;
|
||
|
|
padding: 0;
|
||
|
|
overflow: hidden;
|
||
|
|
clip-path: inset(50%);
|
||
|
|
white-space: nowrap;
|
||
|
|
border: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mono {
|
||
|
|
font-family: var(--font-mono);
|
||
|
|
font-size: 0.9em;
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
}
|
||
|
|
|
||
|
|
.muted {
|
||
|
|
color: var(--ink-2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.danger-text {
|
||
|
|
color: var(--danger);
|
||
|
|
}
|
||
|
|
|
||
|
|
.ok-text {
|
||
|
|
color: var(--ok);
|
||
|
|
}
|
||
|
|
|
||
|
|
.flex {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stack > * + * {
|
||
|
|
margin-top: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.gap-top {
|
||
|
|
margin-top: var(--space-md);
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-inline {
|
||
|
|
display: inline;
|
||
|
|
}
|
||
|
|
|
||
|
|
.select-all {
|
||
|
|
user-select: all;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (prefers-reduced-motion: reduce) {
|
||
|
|
*,
|
||
|
|
*::before,
|
||
|
|
*::after {
|
||
|
|
animation-duration: 0.001ms !important;
|
||
|
|
animation-iteration-count: 1 !important;
|
||
|
|
transition-duration: 0.001ms !important;
|
||
|
|
}
|
||
|
|
}
|