rebuild the web theme as a skeuomorphic homelab desk (rack hero, drive drawer, LED dashboard, patch panel; CC0 textures + /credits)

This commit is contained in:
Martin Dvořák 2026-09-30 08:50:37 +00:00
commit dd6aeb5486
18 changed files with 1477 additions and 768 deletions

634
src/app/web.css Normal file
View file

@ -0,0 +1,634 @@
/* ============================================================
WEB THEME — a skeuomorphic homelab desk.
------------------------------------------------------------
Modern dark hardware: anodised aluminium, OLED glass, carbon
fibre, black rubber and matte plastic, with paper objects
resting on it. Textures are CC0 (see /credits). Everything is
scoped under `.w-root` so the KDE desktop is never affected.
============================================================ */
html[data-theme="web"] {
color-scheme: dark;
/* room + chassis */
--room: #0f1113;
--room-2: #15181a;
--chassis: #24282b;
--chassis-2: #1a1e20;
--chassis-edge: #0a0c0d;
--screen: #0b0e0f;
/* ink */
--ink: #fbf1c7;
--body: #d5c4a1;
--muted: #a89984;
--faint: #928374;
--paper-ink: #2a2521;
--paper-ink-soft: #5c544b;
/* signals */
--accent: #fe8019;
--accent-2: #fabd2f;
--led-green: #b8bb26;
--led-amber: #fabd2f;
--led-red: #fb4934;
--led-blue: #83a598;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
}
html[data-theme="web"] body {
background:
radial-gradient(120% 70% at 50% -15%, #191d20 0%, transparent 55%),
linear-gradient(var(--room-2), var(--room));
background-attachment: fixed;
color: var(--body);
font-family: var(--font-plex), ui-sans-serif, system-ui, sans-serif;
font-size: 1rem;
line-height: 1.65;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* ---------------------------------------------------------- materials */
.w-metal {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(0, 0, 0, 0.16) 40%, rgba(0, 0, 0, 0.34)),
linear-gradient(rgba(32, 36, 39, 0.8), rgba(18, 21, 23, 0.86)),
url("/textures/metal.jpg") center / 420px;
border: 1px solid var(--chassis-edge);
border-radius: 10px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
inset 0 -1px 0 rgba(0, 0, 0, 0.55),
0 18px 40px rgba(0, 0, 0, 0.45);
}
.w-metal-flat {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(0, 0, 0, 0.22)),
linear-gradient(rgba(30, 34, 37, 0.86), rgba(20, 23, 25, 0.9)),
url("/textures/metal.jpg") center / 380px;
border: 1px solid #0b0d0e;
border-radius: 8px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
inset 0 -1px 0 rgba(0, 0, 0, 0.5);
}
.w-carbon {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(0, 0, 0, 0.4)),
linear-gradient(rgba(26, 29, 31, 0.66), rgba(14, 16, 18, 0.78)),
url("/textures/carbon.jpg") center / 150px, #14171a;
border: 1px solid #0a0c0d;
border-radius: 7px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 6px 14px rgba(0, 0, 0, 0.4);
}
.w-rubber {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0.35)),
linear-gradient(rgba(22, 25, 27, 0.66), rgba(12, 14, 15, 0.76)),
url("/textures/rubber.jpg") center / 280px;
border: 1px solid #090b0c;
border-radius: 12px;
}
.w-plastic {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(0, 0, 0, 0.3)),
linear-gradient(rgba(34, 38, 41, 0.88), rgba(20, 23, 25, 0.92)),
url("/textures/plastic.jpg") center / 240px;
border: 1px solid #0a0c0d;
border-radius: 9px;
}
.w-paper {
background:
linear-gradient(rgba(253, 250, 243, 0.88), rgba(243, 236, 223, 0.9)),
url("/textures/paper.jpg") center / 340px;
color: var(--paper-ink);
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: 4px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.6) inset,
0 10px 22px rgba(0, 0, 0, 0.42);
}
.w-panel {
position: relative;
background:
radial-gradient(130% 90% at 50% -10%, rgba(254, 128, 25, 0.07), transparent 62%),
linear-gradient(var(--screen), #070a0b);
border: 1px solid #000;
border-radius: 8px;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.045),
inset 0 14px 34px rgba(0, 0, 0, 0.75),
0 1px 0 rgba(255, 255, 255, 0.05);
overflow: hidden;
}
/* faint scanlines so the glass reads as a panel, not a flat fill */
.w-panel::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
180deg,
rgba(255, 255, 255, 0.022) 0 1px,
transparent 1px 3px
);
opacity: 0.7;
}
/* ------------------------------------------------------------- details */
/* NOTE: no class in here may collide with a Tailwind utility name. `.w-screen`
was a real bug: Tailwind's `w-screen` sets width:100vw and beat this file, so
every "screen" panel was forced to full viewport width. Keep the `w-` names
away from Tailwind's width namespace (`w-full`, `w-screen`, `w-min`, ...). */
.w-screw {
position: relative;
width: 11px;
height: 11px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #767c7a, #2b2f30 68%);
box-shadow:
inset 0 0 0 1px rgba(0, 0, 0, 0.65),
0 1px 1px rgba(0, 0, 0, 0.5);
flex: none;
}
.w-screw::after {
content: "";
position: absolute;
inset: 3px;
background: linear-gradient(90deg, transparent 38%, #101314 38%, #101314 62%, transparent 62%);
transform: rotate(38deg);
}
.w-vent {
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.85) 1.1px, transparent 1.3px);
background-size: 8px 8px;
background-position: 0 0;
opacity: 0.75;
}
.w-led {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
box-shadow:
0 0 7px currentColor,
inset 0 0 2px rgba(255, 255, 255, 0.7);
flex: none;
}
.w-led--green { color: var(--led-green); }
.w-led--amber { color: var(--led-amber); }
.w-led--red { color: var(--led-red); }
.w-led--blue { color: var(--led-blue); }
.w-led--off {
color: #3a3f41;
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.8);
}
.w-led--pulse {
animation: w-led-pulse 1.6s ease-in-out infinite;
}
@keyframes w-led-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* etched / engraved label plate */
.w-plate {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.28rem 0.6rem;
border-radius: 5px;
background: linear-gradient(180deg, #2a2e31, #1b1f21);
border: 1px solid #0b0d0e;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.07),
inset 0 -1px 0 rgba(0, 0, 0, 0.5);
color: var(--faint);
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.7);
}
/* push button */
.w-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 0.95rem;
border-radius: 8px;
background: linear-gradient(180deg, #3c4145, #24282b);
border: 1px solid #0b0d0e;
color: var(--ink);
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.78rem;
letter-spacing: 0.02em;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
0 3px 0 #0a0c0d,
0 9px 18px rgba(0, 0, 0, 0.45);
transition: filter 140ms ease-out, transform 90ms ease-out, box-shadow 90ms ease-out;
}
.w-btn:hover { filter: brightness(1.12); }
.w-btn:active {
transform: translateY(3px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 0 0 #0a0c0d,
0 4px 10px rgba(0, 0, 0, 0.45);
}
.w-btn--accent {
background: linear-gradient(180deg, #ff9a3d, #d65d0e);
border-color: #6d2f06;
color: #22160a;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
}
.w-btn--accent:hover { filter: brightness(1.06); }
/* rocker power switch */
.w-switch {
position: relative;
width: 56px;
height: 30px;
border-radius: 7px;
background: linear-gradient(180deg, #0d0f10, #191d1f);
border: 1px solid #000;
box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.85);
flex: none;
}
.w-switch__knob {
position: absolute;
top: 3px;
left: 3px;
width: 24px;
height: 22px;
border-radius: 5px;
background: linear-gradient(180deg, #565d61, #2b3033);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
0 2px 4px rgba(0, 0, 0, 0.6);
transition: transform 180ms var(--ease);
}
.w-switch__knob::after {
content: "";
position: absolute;
inset: 6px 5px;
border-radius: 2px;
background: repeating-linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0 1px, transparent 1px 4px);
}
[data-desktop-mode="on"] .w-switch__knob { transform: translateX(26px); }
.w-switch__led {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
}
[data-desktop-mode="on"] .w-switch__led { color: var(--led-green); }
/* ------------------------------------------------------------ objects */
/* top chassis rail */
.w-rail {
position: fixed;
inset: 0 0 auto;
z-index: 50;
border-bottom: 1px solid #050607;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.55);
}
/* the hero rack */
.w-rack {
padding: 14px;
display: flex;
flex-direction: column;
gap: 9px;
}
.w-rack__head {
display: flex;
align-items: center;
gap: 10px;
padding: 0 4px 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.055);
}
.w-rack__title {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.66rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--faint);
}
.w-rack__ears {
display: flex;
align-items: center;
justify-content: space-between;
}
/* a 1U unit */
.w-unit {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
min-height: 58px;
}
.w-unit__leds {
display: flex;
align-items: center;
gap: 5px;
flex: none;
}
.w-unit__name {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.9rem;
color: var(--ink);
letter-spacing: 0.01em;
}
.w-unit__meta {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.68rem;
color: var(--faint);
}
.w-unit--link {
transition: filter 150ms ease-out, transform 150ms ease-out;
}
.w-unit--link:hover {
filter: brightness(1.14);
transform: translateX(2px);
}
/* engraved brass-style nameplate */
.w-nameplate {
padding: 1.1rem 1.3rem;
border-radius: 8px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(0, 0, 0, 0.22)),
linear-gradient(rgba(38, 42, 45, 0.9), rgba(23, 26, 28, 0.94)),
url("/textures/metal.jpg") center / 460px;
border: 1px solid #0a0c0d;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.09),
inset 0 -1px 0 rgba(0, 0, 0, 0.6);
}
.w-nameplate h1 {
font-family: var(--font-news), Georgia, serif;
font-weight: 500;
letter-spacing: -0.015em;
line-height: 1.06;
color: var(--ink);
text-shadow:
0 1px 0 rgba(0, 0, 0, 0.85),
0 -1px 0 rgba(255, 255, 255, 0.12);
}
/* paper objects */
.w-note {
padding: 1.1rem 1.2rem;
transform: rotate(-0.6deg);
}
.w-note--tilt { transform: rotate(0.7deg); }
.w-card {
padding: 1rem 1.1rem;
}
.w-grommet {
width: 14px;
height: 14px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #6d7371, #26292a 70%);
box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.45), 0 1px 1px rgba(0, 0, 0, 0.4);
}
/* SSD caddy */
.w-caddy {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 6px;
transition: transform 150ms var(--ease), filter 150ms ease-out;
}
.w-caddy:hover { transform: translateY(-2px); filter: brightness(1.15); }
.w-caddy__label {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.82rem;
color: var(--ink);
}
.w-caddy__status {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.62rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--faint);
}
/* drawer */
.w-drawer {
padding: 12px;
}
.w-drawer__handle {
height: 16px;
border-radius: 8px;
background: linear-gradient(180deg, #3a3f43, #22262a);
border: 1px solid #0a0c0d;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 2px 5px rgba(0, 0, 0, 0.5);
}
/* pegboard + hanging tag */
.w-pegboard {
padding: 14px;
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.7) 1.4px, transparent 1.6px);
background-size: 22px 22px;
}
.w-tag {
padding: 0.9rem 1rem 0.9rem 2.6rem;
position: relative;
}
.w-tag__ring {
position: absolute;
left: 12px;
top: 14px;
width: 14px;
height: 14px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, #6d7371, #26292a 70%);
box-shadow:
inset 0 0 0 2px rgba(0, 0, 0, 0.45),
0 1px 1px rgba(0, 0, 0, 0.4);
}
/* LED dashboard */
.w-dashboard {
padding: 14px;
}
.w-ledgrid {
display: flex;
gap: 3px;
overflow-x: auto;
padding-bottom: 4px;
}
.w-ledgrid__col { display: flex; flex-direction: column; gap: 3px; }
.w-ledgrid__cell {
width: 10px;
height: 10px;
border-radius: 2px;
background: #23272a;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.w-ledgrid__cell[data-on="1"] { background: var(--led-green); box-shadow: 0 0 6px rgba(184, 187, 38, 0.65); }
.w-ledgrid__cell[data-on="2"] { background: #9ea22a; box-shadow: 0 0 6px rgba(158, 162, 42, 0.6); }
.w-ledgrid__cell[data-on="3"] { background: #6f7420; }
.w-ledgrid__cell[data-on="4"] { background: #4a4e16; }
/* scoreboard digits */
.w-digits {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-weight: 500;
color: var(--led-amber);
text-shadow: 0 0 12px rgba(250, 189, 47, 0.45);
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
/* patch panel */
.w-patch {
padding: 14px;
}
.w-port {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 11px;
border-radius: 6px;
background: linear-gradient(180deg, #2c3134, #1c2022);
border: 1px solid #0a0c0d;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 -2px 0 rgba(0, 0, 0, 0.45);
transition: filter 140ms ease-out, transform 140ms ease-out;
}
.w-port:hover { filter: brightness(1.16); transform: translateY(-1px); }
.w-port__jack {
width: 14px;
height: 14px;
border-radius: 3px;
background: linear-gradient(180deg, #0b0d0e, #1a1e20);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.9);
position: relative;
flex: none;
}
.w-port__jack::after {
content: "";
position: absolute;
left: 3px;
right: 3px;
top: 5px;
height: 3px;
border-radius: 1px;
background: #3f4548;
}
.w-port__label {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.72rem;
color: var(--body);
}
/* nav link — its own class because these rules are unlayered and would beat
Tailwind's hover utilities on the same element */
.w-navlink {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.68rem;
color: var(--faint);
transition: color 140ms ease-out;
}
.w-navlink:hover { color: var(--ink); }
/* monochrome social icon: the SVG is an alpha mask painted with currentColor */
.social-mask {
display: inline-block;
background-color: currentColor;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
-webkit-mask-size: contain;
mask-size: contain;
}
/* screen text */
.w-screen-text {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.78rem;
line-height: 1.7;
color: var(--body);
position: relative;
z-index: 1;
}
.w-screen-text .k { color: var(--accent); }
.w-screen-text .v { color: var(--ink); }
.w-cursor {
display: inline-block;
width: 0.55em;
height: 1em;
background: var(--accent);
vertical-align: -0.15em;
animation: w-blink 1.05s step-end infinite;
}
@keyframes w-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
/* body copy on paper / machine */
.w-serif {
font-family: var(--font-news), Georgia, serif;
font-weight: 500;
letter-spacing: -0.01em;
color: var(--ink);
}
.w-paper .w-serif { color: var(--paper-ink); }
.w-prose {
font-size: 0.98rem;
line-height: 1.7;
}
.w-paper .w-prose { color: var(--paper-ink-soft); }
.w-kicker {
font-family: var(--font-plexmono), ui-monospace, monospace;
font-size: 0.66rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--faint);
}
.w-link {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 1px;
text-decoration-color: rgba(254, 128, 25, 0.45);
}
.w-link:hover { text-decoration-color: var(--accent); }
.w-paper .w-link { color: #b1560c; text-decoration-color: rgba(177, 86, 12, 0.45); }
@media (prefers-reduced-motion: reduce) {
.w-led--pulse,
.w-cursor { animation: none !important; }
.w-btn,
.w-btn:hover,
.w-btn:active,
.w-unit--link,
.w-caddy,
.w-port,
.w-switch__knob { transition: none !important; }
}