Files
kawaandClaude Haiku 4.5 a7a1b05817 fix: theme toggle now properly handles auto mode
Remove data-theme attribute when mode="auto" instead of setting it to
"auto". Pico's OS-dark selector is :root:not([data-theme]), so any
value (even "auto") prevents dark-mode Pico styles. Add Pico form
element background vars and color-scheme: light to match active
theme.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-05 17:18:58 +02:00

1534 lines
40 KiB
CSS

/* ==========================================================================
ImpTune — application shell + component layer on top of Pico CSS.
Design direction: a workbench for a build pipeline (driver -> printer ->
package), not a dashboard. Prose is set in the system UI face; every value
the machine cares about (IPs, port names, INF driver names, PowerShell
commands) is set in monospace. That split is the visual identity.
Theme selectors mirror Pico's own so equal specificity + later source order
wins: light = :root:not([data-theme=dark]) ; dark = the two dark selectors.
The "system" state renders with **no** data-theme attribute (base.html
removes it) — Pico's OS-dark block is `:root:not([data-theme])`, so a literal
data-theme="auto" would keep Pico light while this file went dark.
========================================================================== */
/* --- Tokens: light (paper) ----------------------------------------------- */
:root:not([data-theme="dark"]) {
--im-bg: #f2f2ef;
--im-surface: #ffffff;
--im-surface-2: #f8f8f5;
--im-surface-3: #eeeeea;
--im-line: #e2e3de;
--im-line-strong: #cdcfc8;
--im-text: #191f26;
--im-text-dim: #59626c;
--im-text-faint: #868f99;
--im-accent: #0d6f79;
--im-accent-hover: #0a575f;
--im-accent-soft: #dff0f1;
--im-accent-line: #a9d5d9;
--im-ok: #2b7048;
--im-ok-soft: #e2f0e7;
--im-warn: #8c5a0c;
--im-warn-soft: #fbeed7;
--im-danger: #a32f2f;
--im-danger-soft: #f8e4e2;
--im-shadow: 0 1px 2px rgba(20, 26, 32, .05), 0 8px 24px -12px rgba(20, 26, 32, .18);
--im-ring: rgba(13, 111, 121, .35);
/* Pico remap */
--pico-background-color: var(--im-bg);
--pico-color: var(--im-text);
--pico-muted-color: var(--im-text-dim);
--pico-muted-border-color: var(--im-line);
--pico-border-color: var(--im-line);
--pico-h1-color: var(--im-text);
--pico-h2-color: var(--im-text);
--pico-h3-color: var(--im-text);
--pico-h4-color: var(--im-text);
--pico-h5-color: var(--im-text);
--pico-h6-color: var(--im-text-dim);
--pico-primary: var(--im-accent);
--pico-primary-background: var(--im-accent);
--pico-primary-border: var(--im-accent);
--pico-primary-hover: var(--im-accent-hover);
--pico-primary-hover-background: var(--im-accent-hover);
--pico-primary-hover-border: var(--im-accent-hover);
--pico-primary-underline: var(--im-accent-line);
--pico-primary-focus: var(--im-ring);
--pico-primary-inverse: #ffffff;
--pico-secondary: var(--im-text-dim);
--pico-secondary-background: var(--im-surface-3);
--pico-secondary-border: var(--im-line-strong);
--pico-secondary-hover: var(--im-text);
--pico-secondary-hover-background: var(--im-line);
--pico-secondary-hover-border: var(--im-line-strong);
--pico-secondary-inverse: var(--im-text);
--pico-card-background-color: var(--im-surface);
--pico-card-border-color: var(--im-line);
--pico-card-sectioning-background-color: var(--im-surface-2);
--pico-card-box-shadow: none;
--pico-form-element-background-color: var(--im-surface);
--pico-form-element-border-color: var(--im-line-strong);
--pico-form-element-color: var(--im-text);
--pico-form-element-placeholder-color: var(--im-text-faint);
--pico-form-element-active-border-color: var(--im-accent);
--pico-form-element-focus-color: var(--im-accent);
/* Pico swaps the *background* on :focus/:active. Remap it too, or a focused
field falls back to Pico's own palette instead of ours. */
--pico-form-element-active-background-color: var(--im-surface);
--pico-form-element-selected-background-color: var(--im-surface-3);
--pico-code-background-color: var(--im-surface-3);
--pico-code-color: var(--im-text);
--pico-modal-overlay-background-color: rgba(24, 30, 36, .42);
/* Native widgets: <select> popup, scrollbars, autofill. */
color-scheme: light;
}
/* --- Tokens: dark (ink) -------------------------------------------------- */
@media only screen and (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--im-bg: #0f151b;
--im-surface: #161d24;
--im-surface-2: #1b232b;
--im-surface-3: #222c35;
--im-line: #26313a;
--im-line-strong: #35434e;
--im-text: #e5eaee;
--im-text-dim: #98a5b0;
--im-text-faint: #6c7a86;
--im-accent: #33bec6;
--im-accent-hover: #5ad3d9;
--im-accent-soft: #10333a;
--im-accent-line: #1f5d66;
--im-ok: #58c185;
--im-ok-soft: #14301f;
--im-warn: #dfa757;
--im-warn-soft: #33260f;
--im-danger: #e8746c;
--im-danger-soft: #35191a;
--im-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 32px -14px rgba(0, 0, 0, .7);
--im-ring: rgba(51, 190, 198, .4);
--pico-background-color: var(--im-bg);
--pico-color: var(--im-text);
--pico-muted-color: var(--im-text-dim);
--pico-muted-border-color: var(--im-line);
--pico-border-color: var(--im-line);
--pico-h1-color: var(--im-text);
--pico-h2-color: var(--im-text);
--pico-h3-color: var(--im-text);
--pico-h4-color: var(--im-text);
--pico-h5-color: var(--im-text);
--pico-h6-color: var(--im-text-dim);
--pico-primary: var(--im-accent);
--pico-primary-background: var(--im-accent);
--pico-primary-border: var(--im-accent);
--pico-primary-hover: var(--im-accent-hover);
--pico-primary-hover-background: var(--im-accent-hover);
--pico-primary-hover-border: var(--im-accent-hover);
--pico-primary-underline: var(--im-accent-line);
--pico-primary-focus: var(--im-ring);
--pico-primary-inverse: #06232a;
--pico-secondary: var(--im-text-dim);
--pico-secondary-background: var(--im-surface-3);
--pico-secondary-border: var(--im-line-strong);
--pico-secondary-hover: var(--im-text);
--pico-secondary-hover-background: var(--im-line-strong);
--pico-secondary-hover-border: var(--im-line-strong);
--pico-secondary-inverse: var(--im-text);
--pico-card-background-color: var(--im-surface);
--pico-card-border-color: var(--im-line);
--pico-card-sectioning-background-color: var(--im-surface-2);
--pico-card-box-shadow: none;
--pico-form-element-background-color: var(--im-surface-2);
--pico-form-element-border-color: var(--im-line-strong);
--pico-form-element-color: var(--im-text);
--pico-form-element-placeholder-color: var(--im-text-faint);
--pico-form-element-active-border-color: var(--im-accent);
--pico-form-element-focus-color: var(--im-accent);
--pico-form-element-active-background-color: var(--im-surface-3);
--pico-form-element-selected-background-color: var(--im-surface-3);
--pico-code-background-color: var(--im-surface-3);
--pico-code-color: var(--im-text);
--pico-modal-overlay-background-color: rgba(4, 8, 11, .62);
color-scheme: dark;
}
}
[data-theme="dark"] {
--im-bg: #0f151b;
--im-surface: #161d24;
--im-surface-2: #1b232b;
--im-surface-3: #222c35;
--im-line: #26313a;
--im-line-strong: #35434e;
--im-text: #e5eaee;
--im-text-dim: #98a5b0;
--im-text-faint: #6c7a86;
--im-accent: #33bec6;
--im-accent-hover: #5ad3d9;
--im-accent-soft: #10333a;
--im-accent-line: #1f5d66;
--im-ok: #58c185;
--im-ok-soft: #14301f;
--im-warn: #dfa757;
--im-warn-soft: #33260f;
--im-danger: #e8746c;
--im-danger-soft: #35191a;
--im-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 32px -14px rgba(0, 0, 0, .7);
--im-ring: rgba(51, 190, 198, .4);
--pico-background-color: var(--im-bg);
--pico-color: var(--im-text);
--pico-muted-color: var(--im-text-dim);
--pico-muted-border-color: var(--im-line);
--pico-border-color: var(--im-line);
--pico-h1-color: var(--im-text);
--pico-h2-color: var(--im-text);
--pico-h3-color: var(--im-text);
--pico-h4-color: var(--im-text);
--pico-h5-color: var(--im-text);
--pico-h6-color: var(--im-text-dim);
--pico-primary: var(--im-accent);
--pico-primary-background: var(--im-accent);
--pico-primary-border: var(--im-accent);
--pico-primary-hover: var(--im-accent-hover);
--pico-primary-hover-background: var(--im-accent-hover);
--pico-primary-hover-border: var(--im-accent-hover);
--pico-primary-underline: var(--im-accent-line);
--pico-primary-focus: var(--im-ring);
--pico-primary-inverse: #06232a;
--pico-secondary: var(--im-text-dim);
--pico-secondary-background: var(--im-surface-3);
--pico-secondary-border: var(--im-line-strong);
--pico-secondary-hover: var(--im-text);
--pico-secondary-hover-background: var(--im-line-strong);
--pico-secondary-hover-border: var(--im-line-strong);
--pico-secondary-inverse: var(--im-text);
--pico-card-background-color: var(--im-surface);
--pico-card-border-color: var(--im-line);
--pico-card-sectioning-background-color: var(--im-surface-2);
--pico-card-box-shadow: none;
--pico-form-element-background-color: var(--im-surface-2);
--pico-form-element-border-color: var(--im-line-strong);
--pico-form-element-color: var(--im-text);
--pico-form-element-placeholder-color: var(--im-text-faint);
--pico-form-element-active-border-color: var(--im-accent);
--pico-form-element-focus-color: var(--im-accent);
--pico-form-element-active-background-color: var(--im-surface-3);
--pico-form-element-selected-background-color: var(--im-surface-3);
--pico-code-background-color: var(--im-surface-3);
--pico-code-color: var(--im-text);
--pico-modal-overlay-background-color: rgba(4, 8, 11, .62);
color-scheme: dark;
}
/* --- Typography + global rhythm ----------------------------------------- */
:root {
--im-mono: ui-monospace, "Cascadia Mono", "Cascadia Code", "SF Mono",
"JetBrains Mono", Consolas, "Liberation Mono", monospace;
--im-r: 8px;
--im-r-sm: 5px;
--im-sidebar-w: 236px;
--pico-font-family-sans-serif: system-ui, -apple-system, "Segoe UI Variable Text",
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--pico-font-family-monospace: var(--im-mono);
--pico-font-size: 93.75%; /* 15px base */
--pico-line-height: 1.55;
--pico-border-radius: var(--im-r-sm);
--pico-spacing: 1rem;
--pico-form-element-spacing-vertical: .55rem;
--pico-form-element-spacing-horizontal: .7rem;
--pico-typography-spacing-vertical: .9rem;
--pico-outline-width: 2px;
font-variant-numeric: tabular-nums;
}
body {
background: var(--im-bg);
color: var(--im-text);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 { letter-spacing: -.011em; font-weight: 600; }
/* Utility label — the recurring "field label" voice of a control panel. */
.eyebrow {
font-size: .688rem;
font-weight: 600;
letter-spacing: .085em;
text-transform: uppercase;
color: var(--im-text-faint);
margin: 0;
}
.mono { font-family: var(--im-mono); font-size: .84em; }
.dim { color: var(--im-text-dim); }
.faint { color: var(--im-text-faint); }
.nowrap { white-space: nowrap; }
.ico { flex: 0 0 auto; vertical-align: -.18em; }
:where(a, button, [role="button"], summary, input, select, textarea):focus-visible {
outline: 2px solid var(--im-accent);
outline-offset: 2px;
box-shadow: none;
}
/* ==========================================================================
App shell
========================================================================== */
.layout {
display: flex;
min-height: 100vh;
align-items: stretch;
}
.layout > * { box-sizing: border-box; }
nav.sidebar {
width: var(--im-sidebar-w);
flex: 0 0 var(--im-sidebar-w);
background: var(--im-surface);
border-right: 1px solid var(--im-line);
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
overscroll-behavior: contain;
}
nav.sidebar ul,
nav.sidebar li {
margin: 0;
padding: 0;
list-style: none;
display: block;
width: 100%;
}
/* Brand: wordmark + a printed-sheet mark built from the accent. */
.brand {
display: flex;
align-items: center;
gap: .6rem;
padding: 1.05rem 1rem .95rem;
text-decoration: none;
color: var(--im-text);
border-bottom: 1px solid var(--im-line);
}
.brand:hover { background: var(--im-surface-2); }
.brand-mark {
width: 26px;
height: 26px;
flex: 0 0 auto;
object-fit: contain;
}
.brand-name {
font-weight: 650;
font-size: 1.02rem;
letter-spacing: -.02em;
line-height: 1.1;
}
.brand-sub {
display: block;
font-size: .656rem;
font-weight: 500;
letter-spacing: .07em;
text-transform: uppercase;
color: var(--im-text-faint);
}
.nav-group { padding: .85rem 0 .2rem; }
.nav-group + .nav-group { border-top: 1px solid var(--im-line); }
.nav-group-label {
padding: 0 1rem .45rem;
font-size: .656rem;
font-weight: 600;
letter-spacing: .09em;
text-transform: uppercase;
color: var(--im-text-faint);
}
.sidebar-nav a {
display: flex;
align-items: center;
gap: .6rem;
box-sizing: border-box;
width: 100%;
padding: .5rem 1rem;
margin: 0;
text-decoration: none;
color: var(--im-text-dim);
font-size: .906rem;
font-weight: 500;
line-height: 1.4;
border-left: 2px solid transparent;
transition: background-color .12s ease, color .12s ease;
}
.sidebar-nav a .ico { color: var(--im-text-faint); transition: color .12s ease; }
.sidebar-nav a:hover {
background: var(--im-surface-2);
color: var(--im-text);
}
.sidebar-nav a:hover .ico { color: var(--im-text-dim); }
.sidebar-nav a.active {
color: var(--im-text);
font-weight: 600;
background: var(--im-accent-soft);
border-left-color: var(--im-accent);
}
.sidebar-nav a.active .ico { color: var(--im-accent); }
.sidebar-foot {
margin-top: auto;
border-top: 1px solid var(--im-line);
padding: .7rem .75rem .85rem;
}
.sidebar-foot details { margin: 0; }
.sidebar-foot summary {
display: flex;
align-items: center;
gap: .5rem;
padding: .4rem .25rem;
font-size: .844rem;
font-weight: 500;
color: var(--im-text-dim);
cursor: pointer;
list-style: none;
}
.sidebar-foot summary::after { display: none; }
.sidebar-foot summary:hover { color: var(--im-text); }
.sidebar-foot summary .ico { color: var(--im-text-faint); }
.sidebar-foot .foot-links {
padding: .1rem 0 .2rem;
display: grid;
gap: .1rem;
}
.sidebar-foot .foot-links a {
display: block;
padding: .3rem .3rem .3rem 1.85rem;
font-size: .812rem;
color: var(--im-text-dim);
text-decoration: none;
border-radius: var(--im-r-sm);
}
.sidebar-foot .foot-links a:hover { background: var(--im-surface-2); color: var(--im-accent); }
.sidebar-foot .foot-note {
padding: .35rem .3rem 0 1.85rem;
font-size: .75rem;
color: var(--im-text-faint);
line-height: 1.4;
}
/* --- Main column --------------------------------------------------------- */
.main-wrapper {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
}
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
gap: 1rem;
min-height: 58px;
padding: .6rem 1.75rem;
background: color-mix(in srgb, var(--im-bg) 88%, transparent);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--im-line);
}
.topbar-title {
display: flex;
flex-direction: column;
gap: .1rem;
min-width: 0;
}
.topbar-title h1 {
margin: 0;
font-size: 1.1rem;
font-weight: 620;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.topbar-title .crumb {
display: flex;
align-items: center;
gap: .3rem;
font-size: .75rem;
color: var(--im-text-faint);
}
.topbar-title .crumb a { color: var(--im-text-faint); text-decoration: none; }
.topbar-title .crumb a:hover { color: var(--im-accent); }
.topbar-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: .5rem;
}
.topbar-sep {
width: 1px;
height: 22px;
background: var(--im-line);
margin: 0 .15rem;
}
.icon-btn {
--pico-background-color: transparent;
display: inline-grid;
place-items: center;
width: 2rem;
height: 2rem;
padding: 0;
margin: 0;
border: 1px solid transparent;
border-radius: var(--im-r-sm);
background: transparent;
color: var(--im-text-dim);
font-size: .75rem;
font-weight: 600;
line-height: 1;
cursor: pointer;
}
.icon-btn:hover {
background: var(--im-surface-3);
border-color: var(--im-line);
color: var(--im-text);
}
.sidebar-toggle { display: none; }
.main-content {
flex: 1 1 auto;
min-width: 0;
width: 100%;
max-width: 1180px;
padding: 1.6rem 1.75rem 4rem;
}
/* ==========================================================================
Buttons
========================================================================== */
.btn,
a[role="button"].btn,
button.btn {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .48rem .8rem;
border-radius: var(--im-r-sm);
border: 1px solid var(--im-accent);
background: var(--im-accent);
color: var(--pico-primary-inverse);
font-size: .875rem;
font-weight: 570;
line-height: 1.3;
text-decoration: none;
cursor: pointer;
width: auto;
transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.btn:hover { background: var(--im-accent-hover); border-color: var(--im-accent-hover); }
.btn.ghost {
background: var(--im-surface);
border-color: var(--im-line-strong);
color: var(--im-text);
}
.btn.ghost:hover { background: var(--im-surface-2); border-color: var(--im-text-faint); }
.btn.quiet {
background: transparent;
border-color: transparent;
color: var(--im-text-dim);
padding: .35rem .5rem;
}
.btn.quiet:hover { background: var(--im-surface-3); color: var(--im-text); }
.btn.danger { background: transparent; border-color: transparent; color: var(--im-danger); }
.btn.danger:hover { background: var(--im-danger-soft); border-color: var(--im-danger-soft); }
.btn.sm { padding: .3rem .55rem; font-size: .812rem; }
.btn[aria-disabled="true"],
.btn:disabled {
opacity: .45;
cursor: not-allowed;
pointer-events: none;
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: .5rem;
align-items: center;
}
/* ==========================================================================
Cards, sections, page furniture
========================================================================== */
.card {
background: var(--im-surface);
border: 1px solid var(--im-line);
border-radius: var(--im-r);
margin: 0 0 1.1rem;
overflow: clip;
}
.card-head {
display: flex;
align-items: center;
gap: .75rem;
padding: .8rem 1rem;
border-bottom: 1px solid var(--im-line);
background: var(--im-surface-2);
}
.card-head h2,
.card-head h3 {
margin: 0;
font-size: .938rem;
font-weight: 620;
line-height: 1.3;
}
.card-head .sub {
margin: .1rem 0 0;
font-size: .781rem;
color: var(--im-text-dim);
line-height: 1.35;
}
.card-head .head-actions { margin-left: auto; display: flex; gap: .4rem; align-items: center; }
.card-body { padding: 1rem; }
.card-body > :last-child { margin-bottom: 0; }
.card-body p:first-child { margin-top: 0; }
.card-flush > .card-body { padding: 0; }
.card-grid {
display: grid;
gap: 1.1rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
margin-bottom: 1.1rem;
}
.card-grid > .card { margin-bottom: 0; }
.split {
display: grid;
gap: 1.1rem;
grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
align-items: start;
}
@media (max-width: 900px) { .split { grid-template-columns: minmax(0, 1fr); } }
.page-intro {
margin: -.35rem 0 1.15rem;
max-width: 62ch;
color: var(--im-text-dim);
font-size: .906rem;
}
/* ==========================================================================
Signature: the pipeline rail (driver -> printer -> package)
Order carries real information — each stage unlocks the next.
========================================================================== */
.rail {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
border: 1px solid var(--im-line);
border-radius: var(--im-r);
background: var(--im-surface);
overflow: clip;
margin-bottom: 1.1rem;
}
.rail-step {
position: relative;
display: block;
padding: 1rem 1.1rem 1.05rem;
text-decoration: none;
color: inherit;
border-left: 1px solid var(--im-line);
transition: background-color .12s ease;
}
.rail-step:first-child { border-left: 0; }
.rail-step:hover { background: var(--im-surface-2); }
/* Progress hairline across the top of each stage. */
.rail-step::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 2px;
background: var(--im-line-strong);
}
.rail-step.done::before { background: var(--im-ok); }
.rail-step.next::before { background: var(--im-accent); }
.rail-top {
display: flex;
align-items: center;
gap: .5rem;
margin-bottom: .55rem;
}
.rail-num {
width: 1.35rem;
height: 1.35rem;
flex: 0 0 auto;
display: grid;
place-items: center;
border-radius: 50%;
border: 1px solid var(--im-line-strong);
color: var(--im-text-faint);
font-family: var(--im-mono);
font-size: .688rem;
font-weight: 600;
}
.rail-step.done .rail-num {
background: var(--im-ok-soft);
border-color: var(--im-ok);
color: var(--im-ok);
}
.rail-step.next .rail-num {
background: var(--im-accent);
border-color: var(--im-accent);
color: var(--pico-primary-inverse);
}
.rail-title { font-weight: 600; font-size: .938rem; }
.rail-count {
margin-left: auto;
font-family: var(--im-mono);
font-size: 1.05rem;
font-weight: 600;
color: var(--im-text);
}
.rail-desc {
margin: 0;
font-size: .812rem;
line-height: 1.45;
color: var(--im-text-dim);
}
.rail-cta {
display: inline-flex;
align-items: center;
gap: .3rem;
margin-top: .55rem;
font-size: .812rem;
font-weight: 570;
color: var(--im-accent);
}
.rail-step.done .rail-cta { color: var(--im-text-faint); }
@media (max-width: 720px) {
.rail { grid-template-columns: minmax(0, 1fr); }
.rail-step { border-left: 0; border-top: 1px solid var(--im-line); }
.rail-step:first-child { border-top: 0; }
}
/* ==========================================================================
Badges, pills, status
========================================================================== */
.badge {
display: inline-flex;
align-items: center;
gap: .28rem;
padding: .12rem .45rem;
border-radius: 999px;
border: 1px solid var(--im-line-strong);
background: var(--im-surface-3);
color: var(--im-text-dim);
font-size: .719rem;
font-weight: 600;
letter-spacing: .01em;
line-height: 1.5;
white-space: nowrap;
}
.badge .ico { width: 12px; height: 12px; }
.badge.ok { background: var(--im-ok-soft); border-color: transparent; color: var(--im-ok); }
.badge.warn { background: var(--im-warn-soft); border-color: transparent; color: var(--im-warn); }
.badge.info { background: var(--im-accent-soft); border-color: transparent; color: var(--im-accent); }
.pill {
display: inline-block;
padding: .1rem .4rem;
border-radius: var(--im-r-sm);
background: var(--im-surface-3);
color: var(--im-text-dim);
font-family: var(--im-mono);
font-size: .719rem;
line-height: 1.6;
white-space: nowrap;
}
.pill-row { display: flex; flex-wrap: wrap; gap: .25rem; align-items: center; }
/* A multi-model INF can name a dozen printers: show three, fold the rest. */
details.more-pills { display: inline-block; margin: 0; }
details.more-pills > summary {
cursor: pointer;
list-style: none;
color: var(--im-accent);
background: var(--im-accent-soft);
}
details.more-pills > summary::marker { content: ""; }
details.more-pills > summary::-webkit-details-marker { display: none; }
details.more-pills > summary::after { display: none; }
details.more-pills[open] > .pill-row { margin-top: .25rem; }
/* ==========================================================================
Tables
========================================================================== */
.table-scroll {
width: 100%;
overflow-x: auto;
}
.table-scroll table,
.data-table {
margin: 0;
width: 100%;
border-collapse: collapse;
font-size: .875rem;
}
.data-table thead th {
padding: .5rem .75rem;
background: var(--im-surface-2);
border-bottom: 1px solid var(--im-line);
font-size: .688rem;
font-weight: 600;
letter-spacing: .075em;
text-transform: uppercase;
color: var(--im-text-faint);
text-align: left;
white-space: nowrap;
}
.data-table tbody td {
padding: .6rem .75rem;
border-bottom: 1px solid var(--im-line);
vertical-align: middle;
background: transparent;
}
.data-table tbody tr:last-child td { border-bottom: 0; }
.data-table tbody tr:hover td { background: var(--im-surface-2); }
.data-table .cell-name {
font-weight: 570;
color: var(--im-text);
text-decoration: none;
}
.data-table .cell-name:hover { color: var(--im-accent); text-decoration: underline; }
.data-table .cell-sub {
display: block;
margin-top: .1rem;
font-family: var(--im-mono);
font-size: .719rem;
color: var(--im-text-faint);
}
.data-table td.num { font-family: var(--im-mono); font-size: .812rem; }
.data-table td.actions { width: 1%; white-space: nowrap; text-align: right; }
.data-table td.actions .btn-row {
justify-content: flex-end;
flex-wrap: nowrap; /* keep the action cluster on one line */
gap: .25rem;
}
/* Group heading above each client's table */
.group-head {
display: flex;
align-items: center;
gap: .5rem;
padding: .7rem 1rem;
border-bottom: 1px solid var(--im-line);
background: var(--im-surface-2);
}
.group-head h3 {
margin: 0;
font-size: .875rem;
font-weight: 620;
}
.group-head h3 a { color: inherit; text-decoration: none; }
.group-head h3 a:hover { color: var(--im-accent); text-decoration: underline; }
.group-head .count {
font-family: var(--im-mono);
font-size: .75rem;
color: var(--im-text-faint);
}
.group-head .head-actions { margin-left: auto; }
/* ==========================================================================
Toolbar (search + filters + primary action)
========================================================================== */
.toolbar {
display: flex;
align-items: center;
gap: .6rem;
flex-wrap: wrap;
margin-bottom: 1.1rem;
}
.search {
position: relative;
flex: 1 1 240px;
max-width: 340px;
}
.search .ico {
position: absolute;
left: .6rem;
top: 50%;
transform: translateY(-50%);
color: var(--im-text-faint);
pointer-events: none;
}
.search input[type="search"],
.search input[type="text"] {
margin: 0;
padding-left: 2rem;
height: 2.25rem;
font-size: .875rem;
background-image: none;
}
.filter-count {
font-size: .812rem;
color: var(--im-text-faint);
font-family: var(--im-mono);
}
.toolbar .spacer { margin-left: auto; }
/* ==========================================================================
Empty states — an invitation to act, not a mood
========================================================================== */
.empty,
p.empty-state {
color: var(--im-text-dim);
font-style: normal;
}
.empty {
display: grid;
justify-items: start;
gap: .3rem;
padding: 1.75rem 1rem 1.9rem;
text-align: left;
}
.empty .empty-ico {
display: grid;
place-items: center;
width: 2.25rem;
height: 2.25rem;
border-radius: var(--im-r-sm);
background: var(--im-surface-3);
color: var(--im-text-faint);
margin-bottom: .3rem;
}
.empty strong { font-size: .938rem; font-weight: 620; color: var(--im-text); }
.empty p { margin: 0; font-size: .875rem; max-width: 52ch; }
.empty .btn { margin-top: .6rem; }
/* ==========================================================================
Key/value description grid
========================================================================== */
.kv {
display: grid;
grid-template-columns: minmax(9rem, auto) minmax(0, 1fr);
margin: 0;
}
.kv dt,
.kv dd {
margin: 0;
padding: .5rem .1rem;
border-top: 1px solid var(--im-line);
font-size: .875rem;
}
.kv dt:first-of-type,
.kv dt:first-of-type + dd { border-top: 0; }
.kv dt {
color: var(--im-text-faint);
font-size: .781rem;
font-weight: 500;
padding-right: 1rem;
}
.kv dd { color: var(--im-text); }
.kv dd.mono-val { font-family: var(--im-mono); font-size: .812rem; }
/* ==========================================================================
Command rows + copy
========================================================================== */
.cmd {
display: flex;
align-items: stretch;
gap: 0;
border: 1px solid var(--im-line);
border-radius: var(--im-r-sm);
background: var(--im-surface-2);
overflow: clip;
}
.cmd code {
flex: 1 1 auto;
min-width: 0;
padding: .5rem .65rem;
background: transparent;
color: var(--im-text);
font-family: var(--im-mono);
font-size: .781rem;
line-height: 1.5;
overflow-x: auto;
white-space: pre;
}
.cmd button {
flex: 0 0 auto;
width: auto;
margin: 0;
padding: 0 .65rem;
border: 0;
border-left: 1px solid var(--im-line);
border-radius: 0;
background: var(--im-surface-3);
color: var(--im-text-dim);
font-size: .75rem;
font-weight: 600;
cursor: pointer;
}
.cmd button:hover { background: var(--im-accent-soft); color: var(--im-accent); }
.field-stack { display: grid; gap: .85rem; }
.field-stack > label { margin: 0; }
/* ==========================================================================
Forms — grouped fieldsets so a long form reads as a few short ones
========================================================================== */
.form-section {
border: 0;
border-top: 1px solid var(--im-line);
margin: 0;
padding: 1.35rem 0 .2rem;
display: grid;
gap: 1rem;
}
.form-section:first-of-type { border-top: 0; padding-top: 0; }
/* A <legend> renders inside the fieldset's top border, so the section rule
would strike through it. Lean into that: the label knocks a gap in the
hairline and the rule continues to its right. */
.form-section legend {
padding: 0 .55rem 0 0;
margin: 0 0 -.35rem;
background: var(--im-surface);
font-size: .688rem;
font-weight: 600;
letter-spacing: .085em;
text-transform: uppercase;
color: var(--im-text-faint);
}
.form-row {
display: grid;
gap: .85rem;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.form-row > label { margin: 0; }
.label-text {
display: block;
margin-bottom: .25rem;
font-size: .812rem;
font-weight: 550;
color: var(--im-text);
}
.hint {
display: block;
margin-top: .25rem;
font-size: .75rem;
color: var(--im-text-faint);
line-height: 1.4;
}
.check-row {
display: flex;
flex-wrap: wrap;
gap: 1.1rem;
align-items: center;
}
.check-row label {
display: flex;
align-items: center;
gap: .45rem;
margin: 0;
font-size: .875rem;
}
.check-row input[type="checkbox"] { margin: 0; }
.form-foot {
display: flex;
align-items: center;
gap: .6rem;
flex-wrap: wrap;
border-top: 1px solid var(--im-line);
margin-top: 1.1rem;
padding-top: 1rem;
}
.form-foot .btn { margin: 0; }
input[type="file"] { font-size: .844rem; }
/* File upload panel */
.uploader {
display: grid;
gap: .7rem;
padding: .9rem;
border: 1px dashed var(--im-line-strong);
border-radius: var(--im-r);
background: var(--im-surface-2);
}
.uploader p { margin: 0; font-size: .812rem; color: var(--im-text-dim); }
.uploader input[type="file"] { margin: 0; }
.uploader .btn-row { margin: 0; }
/* ==========================================================================
Alerts / inline feedback
========================================================================== */
.notice,
.error,
div.error > p {
font-size: .875rem;
}
.notice,
.error {
display: block;
padding: .65rem .8rem;
border-radius: var(--im-r-sm);
border: 1px solid transparent;
margin: 0 0 .9rem;
}
.notice {
background: var(--im-warn-soft);
border-color: color-mix(in srgb, var(--im-warn) 30%, transparent);
color: var(--im-warn);
}
.error {
background: var(--im-danger-soft);
border-color: color-mix(in srgb, var(--im-danger) 30%, transparent);
color: var(--im-danger);
}
.error p, .notice p { margin: 0; }
/* Session-mode banners — memory-only (COOKIE_SECURE=false) and single-user
(COOKIE_SECURE=single_user): icon, message, and the escape hatch on one
line, stacking on narrow screens. */
.notice.session-ephemeral,
.notice.session-single-user {
display: flex;
align-items: center;
gap: .6rem;
}
.notice.session-ephemeral p,
.notice.session-single-user p { flex: 1; min-width: 12rem; }
.notice.session-ephemeral .ico,
.notice.session-single-user .ico { flex: none; }
.notice.session-ephemeral .btn {
flex: none;
border-color: color-mix(in srgb, var(--im-warn) 40%, transparent);
color: var(--im-warn);
}
.notice.session-ephemeral .btn:hover {
background: color-mix(in srgb, var(--im-warn) 14%, transparent);
border-color: var(--im-warn);
}
@media (max-width: 640px) {
.notice.session-ephemeral,
.notice.session-single-user { flex-wrap: wrap; }
}
.notice details { margin: .4rem 0 0; }
.notice summary { font-size: .812rem; cursor: pointer; }
.notice ul { margin: .4rem 0 0; font-size: .812rem; font-family: var(--im-mono); }
.ok-note {
display: inline-flex;
align-items: center;
gap: .35rem;
font-size: .844rem;
color: var(--im-ok);
}
/* ==========================================================================
Icon preview
========================================================================== */
.icon-preview {
display: flex;
align-items: center;
gap: .75rem;
padding: .6rem;
border: 1px solid var(--im-line);
border-radius: var(--im-r-sm);
background: var(--im-surface-2);
margin-bottom: .8rem;
}
.icon-preview img {
width: 56px;
height: 56px;
border-radius: var(--im-r-sm);
background: var(--im-surface-3);
image-rendering: auto;
}
.icon-preview .meta { font-size: .812rem; color: var(--im-text-dim); }
.error-note {
margin: 0 0 .8rem;
font-size: .875rem;
color: var(--im-danger);
}
/* Driver-library thumbnail: fixed box whether or not an icon exists, so the
name column does not shift when one is added out of band. */
.cell-with-thumb {
display: flex;
align-items: flex-start;
gap: .6rem;
}
.driver-thumb {
flex: none;
width: 24px;
height: 24px;
margin-top: .1rem;
border-radius: var(--im-r-sm);
background: var(--im-surface-3);
overflow: hidden;
}
.driver-thumb img { width: 24px; height: 24px; display: block; }
/* ==========================================================================
Web lookups — image picker and driver-page results
========================================================================== */
.web-picker {
margin-top: .9rem;
padding-top: .9rem;
border-top: 1px dashed var(--im-line);
}
.web-picker label { margin: 0 0 .5rem; }
.web-picker .btn-row { margin: 0; }
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
gap: .5rem;
margin: .6rem 0;
}
/* Each result is a button, not an <img> with a click handler — keyboard
selection and focus rings come for free. */
.image-option {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1;
padding: 0;
border: 1px solid var(--im-line);
border-radius: var(--im-r-sm);
background: var(--im-surface-3);
overflow: hidden;
cursor: pointer;
}
.image-option:hover,
.image-option:focus-visible { border-color: var(--im-accent); }
.image-option img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.image-option .image-meta {
position: absolute;
inset: auto 0 0 0;
padding: .1rem .25rem;
font-size: .625rem;
line-height: 1.4;
color: #fff;
background: rgba(0, 0, 0, .55);
}
.link-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: .7rem;
}
.link-list li {
padding-bottom: .7rem;
border-bottom: 1px solid var(--im-line);
}
.link-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.link-list a { font-size: .906rem; font-weight: 560; }
.link-list p { margin: .2rem 0 0; font-size: .812rem; }
.link-list .cell-sub { display: block; }
/* ==========================================================================
Dialogs
========================================================================== */
/* The edit dialog is rendered inside its row's Actions cell, so it inherits
that cell's right-aligned, no-wrap text. Reset at the dialog boundary. */
dialog {
text-align: start;
white-space: normal;
font-size: 1rem;
}
dialog > article {
border: 1px solid var(--im-line);
border-radius: var(--im-r);
box-shadow: var(--im-shadow);
padding: 0;
max-width: 34rem;
overflow: hidden;
}
/* Pico bleeds article header/footer past the article padding with negative
horizontal margins. These panels are padded themselves, so drop that. */
dialog article > header,
dialog article > footer,
dialog .dialog-head,
dialog .dialog-foot { margin: 0; }
/* Class-based, not element-based: the edit dialog wraps head/body/foot in a
<form>, so `dialog > article > header` would never match. */
dialog .dialog-head,
dialog .dialog-foot {
display: flex;
align-items: center;
gap: .55rem;
padding: .85rem 1.1rem;
margin: 0;
background: var(--im-surface-2);
}
dialog .dialog-head { border-bottom: 1px solid var(--im-line); }
dialog .dialog-head h3 {
margin: 0;
font-size: 1rem;
font-weight: 620;
}
/* Pico draws its own X into any [aria-label=Close] button — we supply the glyph. */
dialog .dialog-head .close {
margin-left: auto;
background-image: none;
font-size: 1.15rem;
line-height: 1;
}
dialog .dialog-foot {
border-top: 1px solid var(--im-line);
justify-content: flex-end;
}
/* Head and foot stay put; only the fields scroll on a short viewport. */
dialog .dialog-body {
padding: 1.1rem;
max-height: min(68vh, 36rem);
overflow-y: auto;
}
dialog .dialog-body > :last-child { margin-bottom: 0; }
dialog .dialog-body .form-section:first-of-type { padding-top: 0; }
[x-cloak] { display: none !important; }
/* ==========================================================================
HTMX feedback
========================================================================== */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline-flex; align-items: center; gap: .4rem; font-size: .812rem; color: var(--im-text-dim); }
form.htmx-request button[type="submit"] { opacity: .6; pointer-events: none; }
/* ==========================================================================
Responsive: sidebar becomes an off-canvas drawer
========================================================================== */
@media (max-width: 860px) {
.sidebar-toggle { display: inline-grid; }
nav.sidebar {
position: fixed;
z-index: 40;
inset: 0 auto 0 0;
transform: translateX(-100%);
transition: transform .18s ease;
box-shadow: var(--im-shadow);
}
body.nav-open nav.sidebar { transform: translateX(0); }
body.nav-open .nav-scrim {
position: fixed;
inset: 0;
z-index: 30;
background: var(--pico-modal-overlay-background-color);
}
.topbar { padding: .6rem 1rem; }
.main-content { padding: 1.1rem 1rem 3rem; }
}
@media (max-width: 620px) {
/* Page actions shrink to their icons; the h1 keeps the context. */
.topbar-actions .btn > span { display: none; }
.topbar-actions .btn { padding: .45rem .55rem; }
.topbar-sep { display: none; }
/* Print defaults are the least urgent column — the detail page has them. */
.data-table .col-defaults { display: none; }
}
/* ==========================================================================
Small corrections to Pico defaults that the components rely on
========================================================================== */
.kv dd a,
.hint a,
.data-table a:not(.btn):not(.cell-name) { color: var(--im-accent); }
.data-table img { display: inline-block; max-width: 100%; }
/* Headings borrowed as section eyebrows keep the eyebrow scale, not h2's. */
h2.eyebrow, h3.eyebrow { font-size: .688rem; line-height: 1.4; }
/* Pico marks any <details><summary> as an accordion; the session menu and the
folded pill list are not accordions. */
.sidebar-foot summary::marker,
details.more-pills > summary::marker { content: ""; }
/* The upload confirmation reuses the full driver table; in the add-printer
sidebar there is only room for the file and its parsed names. */
.form-aside .data-table .col-arch,
.form-aside .data-table .col-used,
.form-aside .data-table .col-added,
.form-aside .data-table .col-actions { display: none; }
.uploader label { margin: 0; }
.uploader .hint { margin-top: 0; }
figure { margin: 0; }
@media (prefers-reduced-motion: reduce) {
* { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}