Files
gerbeur/src/themes/brutalist.css
khannurien 7b12dcbe40
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 40s
v3: the dump button keeps hovering once you point at it — a flat 1px jump was the whole of the header button's hover, so it now lifts and then rocks, tilting a couple of degrees either way over a shallow float. The three motions ride on three different properties — translate for the lift, rotate for the tilt, transform for the float — since that's what lets them compose rather than the last one declared winning outright; the tilt and the float run on periods that don't divide each other (2.6s against 3.4s) so the pair never lines up the same way twice and never settles into a metronome, and the pivot sits on the bottom edge so it reads as a head on a neck rather than as a button spinning about its middle. Arriving and leaving is what the machinery is for: the amplitude is a registered custom property the keyframes multiply through, so pointing at the button eases the rocking up from nothing while the lift is still settling, and leaving eases it back down under an animation that is merely paused — no switch from still to full throw, and no snap square when the cursor goes. Geocities and brutalist re-declare the whole transition shorthand on every button and neither list mentions translate, which outweighed the header's own rule and dropped the lift onto the frame it started on, so that rule takes a :not(:disabled) to win the weight back. nyt stays flat ink and brutalist goes on pressing into the page, both opting out by name; reduced motion keeps the lift and drops the rocking.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:47:22 +00:00

396 lines
15 KiB
CSS

/* ══════════════════════════════════════════════════════════════════════
Brutalist theme — pastel palette, hard edges, bold type
Palette: Eggshell · Burnt Peach · Twilight Indigo · Muted Teal · Apricot Cream
══════════════════════════════════════════════════════════════════════ */
/* ── Palette (light-dark: see tokens.css) ─────────────────────────── */
:root[data-style="brutalist"] {
--color-text: light-dark(#3d405b, #f4f1de);
--color-text-secondary: light-dark(#5a5e80, #c8c5b2);
--color-text-muted: light-dark(#7a7e9a, #8e8c80);
--color-on-accent: light-dark(#f4f1de, #3d405b);
--color-bg: light-dark(#f4f1de, #252838);
--color-surface: light-dark(#f8f5e8, #2e3145);
--color-border: light-dark(#8a8da8, #4a4e6a);
--color-border-subtle: light-dark(rgba(61, 64, 91, 0.25), rgba(74, 78, 106, 0.4));
--color-accent: light-dark(#e07a5f, #f2cc8f);
--color-accent-hover: light-dark(#c05040, #d4a860);
--color-link: light-dark(#b85a3a, #f2cc8f);
--color-link-hover: light-dark(#943020, #d4a860);
--color-danger: light-dark(#c04030, #e07a5f);
--color-danger-bg: light-dark(#fae4e0, #3a1e18);
--color-danger-hover: light-dark(#a02820, #c85a40);
--color-success: light-dark(#3a7860, #81b29a);
--color-success-bg: light-dark(#e0f0ea, #1a2e26);
--color-success-hover: light-dark(#286048, #5a9878);
--color-overlay: light-dark(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.6));
--color-header-user-bg: light-dark(rgba(224, 122, 95, 0.08), rgba(242, 204, 143, 0.07));
--color-header-user-bg-hover: light-dark(rgba(224, 122, 95, 0.16), rgba(242, 204, 143, 0.14));
--color-option-bg: light-dark(#ede9d4, #222436);
--color-option-border: light-dark(#e0dcc8, #252838);
}
/* ══════════════════════════════════════════════════════════════════════
Structural overrides
══════════════════════════════════════════════════════════════════════ */
/* ── Typography ──────────────────────────────────────────────────── */
[data-style="brutalist"] {
--font-body: "Space Grotesk", "Saira", sans-serif;
/* Hard edges everywhere App.css uses the radius scale. */
--radius-xs: 0;
--radius-sm: 0;
--radius-md: 0;
--radius-lg: 0;
--radius-xl: 0;
--radius-full: 0;
font-family: var(--font-body);
font-weight: 500;
}
/* Only section-level headings get the uppercase treatment */
[data-style="brutalist"] h1 {
font-weight: 900;
letter-spacing: 0.03em;
}
/* Circles App.css draws with a literal 50% (not the radius scale) — square
them off too. */
[data-style="brutalist"] .avatar-img,
[data-style="brutalist"] .avatar-initials,
[data-style="brutalist"] .avatar-change-overlay,
[data-style="brutalist"] .notif-icon,
[data-style="brutalist"] .notif-dot {
border-radius: 0;
}
/* ── Buttons — square corners, modest hard shadow ────────────────── */
/* `.dump-fab` is exempt from the generic button chrome — it keeps its own
shape, shadow and animations — but this hard-edged theme squares it off. */
[data-style="brutalist"] .dump-fab,
[data-style="brutalist"] .chat-fab {
--fab-radius: 0;
}
[data-style="brutalist"] button:not(.dump-fab, .chat-fab) {
border: 2px solid var(--color-border);
font-weight: 700;
box-shadow: 2px 2px 0 var(--color-border);
transition: box-shadow 0.08s ease, transform 0.08s ease, border-color 0.1s;
}
[data-style="brutalist"] button:not(.dump-fab, .chat-fab):hover:not(:disabled) {
border-color: var(--color-border);
box-shadow: none;
transform: translate(2px, 2px);
}
/* Ghost / icon-only buttons — keep borderless, no press effect */
[data-style="brutalist"] .modal-close-btn,
[data-style="brutalist"] .playlist-remove-btn,
[data-style="brutalist"] .playlist-card-delete-btn,
[data-style="brutalist"] .vote-btn,
[data-style="brutalist"] .emoji-picker-float [frimousse-emoji],
[data-style="brutalist"] .emoji-picker-close-btn,
[data-style="brutalist"] .new-item-toggle,
[data-style="brutalist"] .btn--ghost,
[data-style="brutalist"] .audio-player-btn,
[data-style="brutalist"] .audio-player-vol-btn,
[data-style="brutalist"] .rich-content-thumbnail-btn,
[data-style="brutalist"] .user-menu-trigger,
[data-style="brutalist"] .nav-search-btn,
[data-style="brutalist"] .auth-link-btn,
[data-style="brutalist"] .form-cancel,
[data-style="brutalist"] .form-label-action {
border: none !important;
box-shadow: none !important;
}
[data-style="brutalist"] .user-menu-trigger:hover:not(:disabled),
[data-style="brutalist"] .user-menu-trigger:active,
[data-style="brutalist"] .nav-search-btn:hover:not(:disabled),
[data-style="brutalist"] .nav-search-btn:active,
[data-style="brutalist"] .auth-link-btn:hover:not(:disabled),
[data-style="brutalist"] .auth-link-btn:active,
[data-style="brutalist"] .form-cancel:hover:not(:disabled),
[data-style="brutalist"] .form-cancel:active,
[data-style="brutalist"] .form-label-action:hover:not(:disabled),
[data-style="brutalist"] .form-label-action:active {
transform: none !important;
box-shadow: none !important;
}
[data-style="brutalist"] .modal-close-btn:hover,
[data-style="brutalist"] .playlist-remove-btn:hover,
[data-style="brutalist"] .playlist-card-delete-btn:hover,
[data-style="brutalist"] .vote-btn:hover:not(:disabled),
[data-style="brutalist"] .emoji-picker-float [frimousse-emoji]:hover,
[data-style="brutalist"] .emoji-picker-close-btn:hover,
[data-style="brutalist"] .new-item-toggle:hover:not(:disabled),
[data-style="brutalist"] .btn--ghost:hover:not(:disabled),
[data-style="brutalist"] .audio-player-btn:hover:not(:disabled),
[data-style="brutalist"] .audio-player-vol-btn:hover:not(:disabled),
[data-style="brutalist"] .rich-content-thumbnail-btn:hover:not(:disabled) {
transform: none !important;
box-shadow: none !important;
}
/* ── Segmented controls / visibility-toggle ──────────────────────── */
[data-style="brutalist"] .visibility-toggle {
border: 2px solid var(--color-border);
box-shadow: 2px 2px 0 var(--color-border);
}
[data-style="brutalist"] .visibility-toggle button {
border: none;
box-shadow: none;
}
[data-style="brutalist"] .visibility-toggle button:hover:not(:disabled) {
transform: none;
box-shadow: none;
}
/* ── Cards ───────────────────────────────────────────────────────── */
[data-style="brutalist"] .dump-card,
[data-style="brutalist"] .playlist-card {
border: 2px solid var(--color-border);
box-shadow: 3px 3px 0 var(--color-border);
transition:
box-shadow 0.08s ease,
transform 0.08s ease,
border-color 0.1s,
grid-template-rows 0.32s ease,
opacity 0.25s ease,
filter 0.3s ease;
}
[data-style="brutalist"] .dump-card:hover,
[data-style="brutalist"] .playlist-card:hover {
border-color: var(--color-accent);
box-shadow: 3px 3px 0 var(--color-accent);
transform: translate(-2px, -2px);
}
/* Don't shift cards that are animating out */
[data-style="brutalist"] .dump-card--fading,
[data-style="brutalist"] .dump-card--dismissing {
transform: none;
}
/* ── Journal sub-block breaks ────────────────────────────────────── */
/* The theme already has one divider and uses it everywhere — the app header,
the modal header, a section heading all end on 2px of --color-border. So
the journal break is that same edge, not a new mark invented for it. What
separates a break from any other rule here is the air around it, which is
the one thing a heading can't spend. */
[data-style="brutalist"] .journal-break {
--journal-break-space: 2.1rem;
--journal-break-rule: 2px solid var(--color-border);
}
/* ── Dump post detail header ─────────────────────────────────────── */
[data-style="brutalist"] .dump-post-header {
border: 2px solid var(--color-border);
box-shadow: 3px 3px 0 var(--color-border);
}
/* ── App header ──────────────────────────────────────────────────── */
[data-style="brutalist"] .app-header {
border-bottom: 2px solid var(--color-border);
}
[data-style="brutalist"] .app-header-user {
border: 2px solid var(--color-border);
box-shadow: 2px 2px 0 var(--color-border);
transition: box-shadow 0.08s ease, transform 0.08s ease, background 0.1s;
}
[data-style="brutalist"] .app-header-user:hover {
box-shadow: none;
transform: translate(2px, 2px);
}
/* ── Inputs / textareas ──────────────────────────────────────────── */
[data-style="brutalist"]
input:not([type="range"]):not([type="file"]):not([type="checkbox"]):not(
[type="radio"]
),
[data-style="brutalist"] textarea {
border-radius: 0 !important;
border-width: 2px !important;
border-color: var(--color-border) !important;
}
[data-style="brutalist"] .form input:focus,
[data-style="brutalist"] .form textarea:focus,
[data-style="brutalist"] .mention-textarea-wrap textarea:focus {
box-shadow: 2px 2px 0 var(--color-accent);
}
[data-style="brutalist"] .category-editor {
box-shadow: 2px 2px 0 var(--color-accent);
}
/* ── Modals ──────────────────────────────────────────────────────── */
[data-style="brutalist"] .modal-card,
[data-style="brutalist"] .confirm-modal {
border: 2px solid var(--color-border);
box-shadow: 4px 4px 0 var(--color-border);
}
[data-style="brutalist"] .modal-header {
border-bottom: 2px solid var(--color-border);
}
/* ── Section headings ────────────────────────────────────────────── */
[data-style="brutalist"] .profile-section > h2,
[data-style="brutalist"] .profile-section-header,
[data-style="brutalist"] .comment-section-title {
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 700;
}
[data-style="brutalist"] .profile-section > h2,
[data-style="brutalist"] .profile-section-header {
border-bottom-width: 2px;
}
/* ── Rich content cards ──────────────────────────────────────────── */
[data-style="brutalist"] .rich-content-card {
box-shadow: 3px 3px 0 var(--color-border);
}
/* Carry the provider brand colour through both the border and the hard
offset shadow, so the brand reads instead of being flattened to grey. */
[data-style="brutalist"] .rich-content-card--youtube {
border-color: var(--color-youtube);
box-shadow: 3px 3px 0 var(--color-youtube);
}
[data-style="brutalist"] .rich-content-card--bandcamp {
border-color: var(--color-bandcamp);
box-shadow: 3px 3px 0 var(--color-bandcamp);
}
[data-style="brutalist"] .rich-content-card--soundcloud {
border-color: var(--color-soundcloud);
box-shadow: 3px 3px 0 var(--color-soundcloud);
}
[data-style="brutalist"]
.rich-content-card--youtube:has(.rich-content-body:hover),
[data-style="brutalist"]
.rich-content-card--bandcamp:has(.rich-content-body:hover),
[data-style="brutalist"]
.rich-content-card--soundcloud:has(.rich-content-body:hover) {
border-color: var(--color-accent);
box-shadow: 3px 3px 0 var(--color-accent);
}
/* ── Global persistent player ────────────────────────────────────── */
[data-style="brutalist"] .global-player {
border: 2px solid var(--color-border);
box-shadow: 0 0 0 2px var(--color-border);
}
[data-style="brutalist"] .global-player .audio-player-btn {
background: transparent;
color: var(--color-text);
}
[data-style="brutalist"] .global-player .audio-player-btn:hover {
background: var(--color-border-subtle);
color: var(--color-text);
}
[data-style="brutalist"] .global-player .audio-player-vol-btn {
color: var(--color-text);
}
[data-style="brutalist"] .global-player .btn--ghost {
color: var(--color-text);
}
/* ── Notification items ──────────────────────────────────────────── */
[data-style="brutalist"] .notification-item {
border: 2px solid var(--color-border);
box-shadow: 2px 2px 0 var(--color-border);
}
[data-style="brutalist"] .notif-dot {
box-shadow: none;
}
/* ── User result items (search) ──────────────────────────────────── */
[data-style="brutalist"] .user-result-item {
border: 2px solid var(--color-border);
box-shadow: 2px 2px 0 var(--color-border);
transition: box-shadow 0.08s ease, transform 0.08s ease, border-color 0.1s;
}
[data-style="brutalist"] .user-result-item:hover {
border-color: var(--color-accent);
box-shadow: 2px 2px 0 var(--color-accent);
transform: translate(-2px, -2px);
}
/* ── btn-primary ─────────────────────────────────────────────────── */
[data-style="brutalist"] .btn-primary {
border: 2px solid var(--color-border);
box-shadow: 2px 2px 0 var(--color-border);
transition: box-shadow 0.08s ease, transform 0.08s ease;
}
[data-style="brutalist"] .btn-primary:hover:not(:disabled) {
box-shadow: none;
transform: translate(2px, 2px);
}
[data-style="brutalist"] .btn-primary:active {
box-shadow: none;
transform: translate(2px, 2px);
}
/* The header's dump button floats in the base theme; here it presses into the
page instead, so neither the lift nor the idle drift applies. */
[data-style="brutalist"] .app-header-nav .btn-primary:hover:not(:disabled) {
translate: none;
animation: none;
}
/* ── btn-border ──────────────────────────────────────────────────── */
[data-style="brutalist"] .btn-border {
box-shadow: 2px 2px 0 var(--color-border);
}
[data-style="brutalist"] .btn-border:hover {
box-shadow: none;
transform: translate(2px, 2px);
}
/* ── Category switcher dropdown ──────────────────────────────────── */
[data-style="brutalist"] .category-switcher-option {
border: none !important;
box-shadow: none !important;
}
[data-style="brutalist"] .category-switcher-option:hover:not(:disabled) {
transform: none !important;
box-shadow: none !important;
}