Files
EpicNext-Cms/src/app/globals.css
T
openhands be4dc08c31
CI / check (push) Failing after 18s
CI / tests-unit (push) Skipped
CI / tests-integration (push) Skipped
CI / tests-ui (push) Skipped
CI / tests-imaging-browsers (push) Skipped
CI / tests-quality (push) Skipped
CI / preflight (push) Skipped
CI / deploy (push) Skipped
chore: update dependencies and biome rules
2026-10-22 21:25:00 +02:00

3258 lines
80 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans:
var(--font-nunito), "Nunito", ui-sans-serif, system-ui, -apple-system,
"Segoe UI", sans-serif;
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-card-hover:
0 8px 24px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.06);
}
/* The palette lives in plain CSS (`:root` / ThemeVars / the admin remap), so
Tailwind does not know these are theme colors and would otherwise skip the
`bg-primary`, `bg-destructive`, `text-foreground`, … utilities. Re-declare
them as inline theme tokens so shadcn components get their intended colors
while value overrides from the runtime theme still apply via the variable. */
@theme inline {
--color-primary: var(--color-primary);
--color-primary-hover: var(--color-primary-hover);
--color-primary-foreground: var(--color-primary-foreground);
--color-background: var(--color-background);
--color-foreground: var(--color-foreground);
--color-card: var(--color-card);
--color-card-foreground: var(--color-card-foreground);
--color-popover: var(--color-popover);
--color-popover-foreground: var(--color-popover-foreground);
--color-secondary: var(--color-secondary);
--color-secondary-foreground: var(--color-secondary-foreground);
--color-muted: var(--color-muted);
--color-muted-foreground: var(--color-muted-foreground);
--color-accent: var(--color-accent);
--color-accent-foreground: var(--color-accent-foreground);
--color-destructive: var(--color-destructive);
--color-destructive-foreground: var(--color-destructive-foreground);
--color-border: var(--color-border);
--color-input: var(--color-input);
--color-ring: var(--color-ring);
--color-surface: var(--color-surface);
--color-text: var(--color-text);
--color-text-muted: var(--color-text-muted);
--color-success: var(--color-success);
--color-warning: var(--color-warning);
--color-error: var(--color-error);
--color-info: var(--color-info);
}
/* Default theme variables. ThemeVars may override these at runtime. */
:root {
--color-primary: #eeb425;
--color-primary-hover: #d4a01f;
--color-primary-foreground: #1a1a2e;
--color-background: #f4f6f9;
--color-foreground: #0f172a;
--color-card: #ffffff;
--color-card-foreground: #0f172a;
--color-popover: #ffffff;
--color-popover-foreground: #0f172a;
--color-secondary: #f1f5f9;
--color-secondary-foreground: #0f172a;
--color-muted: #f1f5f9;
--color-muted-foreground: #6b7280;
--color-destructive: #ef4444;
--color-destructive-foreground: #ffffff;
--color-border: #e2e8f0;
--color-input: #e2e8f0;
--color-ring: #eeb425;
--color-surface: #ffffff;
--color-dropdown: #ffffff;
--color-navbar: #ffffff;
--color-navbar-text: #1e293b;
--color-text: #0f172a;
--color-text-muted: #6b7280;
--color-text-readable: #0f172a;
--color-text-muted-readable: #6b7280;
--color-primary-readable: #000000;
--color-accent-readable: #006b49;
--color-primary-foreground-readable: #1a1a2e;
--color-accent-foreground-readable: #000000;
--color-navbar-text-readable: #1e293b;
--color-accent: #10b981;
--color-accent-foreground: #000000;
--color-success: #16a34a;
--color-warning: #eab308;
--color-error: #ef4444;
--color-info: #0ea5e9;
--color-danger: #ef4444;
--gradient-from: #f59e0b;
--gradient-to: #10b981;
--button-color: #eeb425;
--button-text-color: #1a1a2e;
--button-text-color-readable: #1a1a2e;
--button-secondary-color: #22c55e;
--button-secondary-text-color: #ffffff;
--button-secondary-text-color-readable: #000000;
--button-secondary-hover-color: var(--color-success);
--button-danger-color: #ef4444;
--button-danger-text-color: #ffffff;
--button-danger-text-color-readable: #000000;
--button-danger-hover-color: #dc2626;
--button-outline-color: #eeb425;
--button-outline-text-color: #1a1a2e;
--button-outline-text-color-readable: #1a1a2e;
--button-outline-hover-color: #d4a01f;
--border-radius: 12px;
--border-color: #eeb425;
--font-family: var(--font-nunito), "Nunito", sans-serif;
--size-heading-h1: 32px;
--size-heading-h2: 24px;
--size-heading-h3: 20px;
--avatar-border-radius: 10px;
--avatar-border-color: #eeb425;
--badge-background: #eeb425;
--badge-text-color: #000000;
--badge-border-radius: 6px;
--card-border-radius: 14px;
--link-color: #eeb425;
--link-hover-color: #d4a01f;
--link-color-readable: #000000;
--link-hover-color-readable: #000000;
--color-surface-text-readable: #0f172a;
--color-dropdown-text-readable: #0f172a;
--color-pill-text-readable: #0f172a;
--color-badge-text-readable: #000000;
--color-overlay-text-readable: #ffffff;
--input-border-color: #d1d5db;
--input-focus-color: #eeb425;
--navbar-height: 56px;
--muted: var(--color-text-muted);
--border-subtle: color-mix(in srgb, var(--color-text-muted) 18%, transparent);
/* Transition variables for smoother, consistent UI */
--transition-fast: 0.1s;
--transition-normal: 0.2s;
--transition-slow: 0.3s;
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Dark mode colors + readable contrast vars are generated by ThemeVars. */
html.dark input,
html.dark select,
html.dark textarea {
background: #11151c;
color: var(--color-text-readable, var(--color-text));
}
html.dark .admin-page table,
html.dark .admin-card {
border-color: var(--admin-border);
}
html.dark .currency,
html.dark .article-img {
background: #1f242d;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
/* ── Route transition (CSS only — no animation runtime on public pages) ── */
@keyframes page-enter-rise {
from {
opacity: 0;
transform: translate3d(0, 10px, 0);
}
to {
opacity: 1;
transform: none;
}
}
.page-enter {
animation: page-enter-rise 240ms ease-out both;
}
/* ── Radio player entrance (was a motion/react initial/animate pair) ────── */
@keyframes radio-player-enter {
from {
opacity: 0;
transform: translate3d(0, 20px, 0) scale(0.95);
}
to {
opacity: 1;
transform: none;
}
}
.radio-player-enter {
animation: radio-player-enter 400ms cubic-bezier(0.25, 0.1, 0.25, 1) both;
}
/* ── Animated counter entrance (was motion/react on a <span>) ───────────── */
@keyframes counter-rise {
from {
opacity: 0;
transform: translate3d(0, 12px, 0);
}
to {
opacity: 1;
transform: none;
}
}
.counter-rise {
animation: counter-rise 500ms ease-out both;
}
/* ── Scroll reveal (was motion/react whileInView) ───────────────────────── */
.motion-reveal {
opacity: 0;
}
@keyframes motion-reveal-rise {
from {
opacity: 0;
transform: translate3d(0, 24px, 0);
}
to {
opacity: 1;
transform: none;
}
}
.motion-reveal-in {
animation: motion-reveal-rise 550ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* The reveal hides its content until the animation runs, so a visitor who asks
for less motion must get it back immediately. The global reduced-motion block
below already flattens the animation duration; this restores the content. */
@media (prefers-reduced-motion: reduce) {
.motion-reveal {
opacity: 1;
}
}
/* ── Photo lightbox (was an AnimatePresence opacity pair) ───────────────── */
/* ── Studio furniture fade-in ─────────────────────────────────────────────
Replaces a `motion/react-m` wrapper that declared initial opacity 0 and
animate opacity 1. That minimal entry renders the element but never runs
the animation, so the style stayed at opacity: 0 and the entire furniture
list was present, loaded and measurable, yet invisible. A CSS animation
cannot strand content that way: if the animation never runs the element is
simply opaque. */
@keyframes studio-list-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.studio-list-fade-in {
animation: studio-list-fade-in 150ms ease-out both;
}
@keyframes lightbox-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes lightbox-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.lightbox-enter {
animation: lightbox-fade-in 200ms ease-out both;
}
/* Paired with the 180ms unmount delay in PhotoLightbox: 200ms in, 180ms out,
matching the 200ms the previous motion transition used. */
.lightbox-exit {
animation: lightbox-fade-out 180ms ease-in both;
}
/* ── Route progress indicator ─────────────────────────────────────────── */
.route-progress {
position: fixed;
inset: 0 0 auto 0;
z-index: 99999;
height: 3px;
overflow: hidden;
background: transparent;
pointer-events: none;
opacity: 0;
transition: opacity 200ms linear;
}
.route-progress[data-loading="true"] {
opacity: 1;
}
@keyframes route-progress-run {
from {
width: 0%;
opacity: 1;
}
70% {
width: 85%;
opacity: 1;
}
to {
width: 100%;
opacity: 0;
}
}
.route-progress__bar {
display: block;
height: 100%;
width: 0;
background: var(--color-primary);
box-shadow: 0 0 10px var(--color-primary);
}
.route-progress[data-loading="true"] .route-progress__bar {
animation: route-progress-run 400ms ease-in-out both;
}
@media (prefers-reduced-motion: reduce) {
html:focus-within {
scroll-behavior: auto;
}
*,
*::before,
*::after {
/* biome-ignore lint/complexity/noImportantStyles: must override any animation/transition for prefers-reduced-motion */
animation-duration: 0.01ms !important;
/* biome-ignore lint/complexity/noImportantStyles: must override any animation/transition for prefers-reduced-motion */
animation-iteration-count: 1 !important;
/* biome-ignore lint/complexity/noImportantStyles: must override any animation/transition for prefers-reduced-motion */
transition-duration: 0.01ms !important;
/* biome-ignore lint/complexity/noImportantStyles: must override any behavior for prefers-reduced-motion */
scroll-behavior: auto !important;
}
}
body {
font-family: var(--font-family);
color: var(--color-text-readable, var(--color-text, #0f172a));
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Mobile-friendly improvements for every screen */
button,
a,
input,
select,
textarea,
summary,
[role="button"],
[role="menuitem"] {
touch-action: manipulation;
}
/* Momentum scrolling on iOS */
* {
-webkit-overflow-scrolling: touch;
}
/* Prevent iOS zoom on input focus */
@media (max-width: 48rem) {
input,
select,
textarea {
font-size: 16px;
}
}
/* Semantic colors: safe across every complete light/dark preset. */
.theme-text-muted {
color: var(--color-text-muted-readable);
}
.theme-text-subtle {
color: var(--color-text-subtle-readable);
}
.theme-text-disabled {
color: var(--color-text-disabled-readable);
}
.theme-status-success,
.theme-status-warning,
.theme-status-error,
.theme-status-info {
border: 1px solid var(--status-border);
background: var(--status-background);
color: var(--status-text);
}
.theme-status-success {
--status-border: var(--color-success-border);
--status-background: var(--color-success-subtle);
--status-text: var(--color-success-text-readable);
}
.theme-status-warning {
--status-border: var(--color-warning-border);
--status-background: var(--color-warning-subtle);
--status-text: var(--color-warning-text-readable);
}
.theme-status-error {
--status-border: var(--color-error-border);
--status-background: var(--color-error-subtle);
--status-text: var(--color-error-text-readable);
}
.theme-status-info {
--status-border: var(--color-info-border);
--status-background: var(--color-info-subtle);
--status-text: var(--color-info-text-readable);
}
::-webkit-scrollbar-thumb {
border-radius: 100px;
border: 4px solid transparent;
background-clip: content-box;
background-color: #2c3039;
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: transparent;
}
/* Theme-aware scrollbars inside the admin panel */
[data-admin] ::-webkit-scrollbar {
width: 10px;
height: 10px;
}
[data-admin] ::-webkit-scrollbar-thumb {
background-color: color-mix(
in srgb,
var(--admin-text-muted) 32%,
transparent
);
}
[data-admin] ::-webkit-scrollbar-thumb:hover {
background-color: color-mix(
in srgb,
var(--admin-text-muted) 55%,
transparent
);
}
/* ── Atom theme components ────────────────────────────────── */
@layer components {
.nav-item {
@apply flex h-auto md:h-[54px] items-center text-[13px] font-bold px-3 md:px-4 relative;
color: var(--color-navbar-text-readable, var(--color-navbar-text, #64748b));
letter-spacing: 0.03em;
text-transform: uppercase;
white-space: nowrap;
transition:
color 0.15s ease-in-out,
background-color 0.15s ease-in-out;
border-radius: 0.5rem;
min-height: 44px;
}
.nav-item:hover {
color: var(--color-primary-readable, var(--color-primary));
background-color: color-mix(in srgb, var(--color-primary) 8%, transparent);
}
.nav-item[aria-current="page"] {
color: var(--color-primary-readable, var(--color-primary));
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.nav-item:focus-visible {
outline: 2px solid var(--color-primary-readable, var(--color-primary));
outline-offset: 2px;
}
.dropdown-item {
@apply block py-2.5 px-4 text-sm font-semibold rounded-lg mx-1;
color: var(--color-dropdown-text-readable, var(--color-text-readable));
transition:
background-color 0.1s ease-in-out,
color 0.1s ease-in-out;
min-height: 44px;
display: flex;
align-items: center;
}
.dropdown-item:hover {
background-color: color-mix(in srgb, var(--color-primary) 12%, transparent);
color: var(--color-primary-readable, var(--color-primary));
}
.dropdown-item:focus-visible {
outline: 2px solid var(--color-primary-readable, var(--color-primary));
outline-offset: -2px;
}
.dropdown-menu {
background-color: var(--color-dropdown);
border: 1px solid
color-mix(in srgb, var(--color-text-muted) 14%, transparent);
box-shadow:
0 8px 32px color-mix(in srgb, rgb(0, 0, 0) 20%, transparent),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 8%, transparent);
}
}
/* ── Site background: Habbo sky ──
The sky lives in a custom property so the Theme Manager can layer a CMS
wallpaper *on top of* it (`background-image: url(...), var(--site-bg-gradient)`)
instead of replacing it. Painting the wallpaper as the only layer meant a
missing upload silently erased the whole gradient and left the site on flat
white. */
.site-bg {
--site-bg-gradient: linear-gradient(
180deg,
color-mix(in srgb, var(--color-primary) 4%, #b3d9f7) 0%,
color-mix(in srgb, var(--color-primary) 6%, #d4e8fa) 25%,
color-mix(in srgb, var(--color-primary) 8%, #e8f0f8) 50%,
color-mix(in srgb, var(--color-primary) 10%, #f0e8d8) 75%,
color-mix(in srgb, var(--color-primary) 14%, var(--color-background)) 100%
);
background-image: var(--site-bg-gradient);
background-attachment: fixed;
background-size: cover;
background-repeat: no-repeat;
position: relative;
}
/* Habbo-style clouds */
.site-bg::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
background:
radial-gradient(
ellipse 240px 60px at 15% 8%,
rgba(255, 255, 255, 0.5) 0%,
transparent 100%
),
radial-gradient(
ellipse 320px 70px at 35% 12%,
rgba(255, 255, 255, 0.4) 0%,
transparent 100%
),
radial-gradient(
ellipse 200px 50px at 55% 6%,
rgba(255, 255, 255, 0.45) 0%,
transparent 100%
),
radial-gradient(
ellipse 280px 65px at 75% 10%,
rgba(255, 255, 255, 0.5) 0%,
transparent 100%
),
radial-gradient(
ellipse 180px 45px at 90% 5%,
rgba(255, 255, 255, 0.35) 0%,
transparent 100%
),
radial-gradient(
ellipse 260px 55px at 50% 18%,
rgba(255, 255, 255, 0.25) 0%,
transparent 100%
),
radial-gradient(
ellipse 200px 40px at 20% 22%,
rgba(255, 255, 255, 0.2) 0%,
transparent 100%
);
}
/* Floating cloud puffs drifting across the public background. */
.clouds {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
overflow: hidden;
}
.cloud {
position: absolute;
width: clamp(140px, 22vw, 260px);
height: clamp(40px, 7vw, 70px);
left: 0;
opacity: 0.7;
transform: scale(var(--cloud-scale, 1));
will-change: transform;
animation-name: cloud-drift;
animation-timing-function: linear;
animation-iteration-count: infinite;
background:
radial-gradient(
ellipse 62% 55% at 30% 50%,
rgba(255, 255, 255, 0.6) 0%,
transparent 70%
),
radial-gradient(
ellipse 70% 60% at 55% 45%,
rgba(255, 255, 255, 0.55) 0%,
transparent 72%
),
radial-gradient(
ellipse 60% 55% at 78% 58%,
rgba(255, 255, 255, 0.45) 0%,
transparent 72%
);
}
.cloud-1 {
--cloud-scale: 1.05;
top: 5%;
animation-duration: 34s;
animation-delay: -8s;
}
.cloud-2 {
--cloud-scale: 0.8;
top: 12%;
opacity: 0.85;
animation-duration: 46s;
animation-delay: -22s;
}
.cloud-3 {
--cloud-scale: 1.25;
top: 19%;
animation-duration: 28s;
animation-delay: -14s;
}
.cloud-4 {
--cloud-scale: 0.65;
top: 27%;
opacity: 0.9;
animation-duration: 52s;
animation-delay: -33s;
}
.cloud-5 {
--cloud-scale: 0.95;
top: 34%;
opacity: 0.8;
animation-duration: 38s;
animation-delay: -19s;
}
.cloud-6 {
--cloud-scale: 1.4;
top: 42%;
opacity: 0.55;
animation-duration: 60s;
animation-delay: -41s;
}
.cloud-7 {
--cloud-scale: 0.72;
top: 50%;
opacity: 0.75;
animation-duration: 43s;
animation-delay: -27s;
}
.cloud-8 {
--cloud-scale: 1.1;
top: 58%;
opacity: 0.5;
animation-duration: 66s;
animation-delay: -51s;
}
@keyframes cloud-drift {
0% {
transform: translateX(-260px) scale(var(--cloud-scale, 1));
}
100% {
transform: translateX(calc(100vw + 260px)) scale(var(--cloud-scale, 1));
}
}
html.dark .cloud {
opacity: 0.3;
}
@media (prefers-reduced-motion: reduce) {
.cloud {
animation: none;
transform: translateX(calc(var(--cloud-frozen-x, 8vw) + 10vw))
scale(var(--cloud-scale, 1));
opacity: 0.5;
}
.cloud-1 {
--cloud-frozen-x: 6vw;
}
.cloud-2 {
--cloud-frozen-x: 32vw;
}
.cloud-3 {
--cloud-frozen-x: 58vw;
}
.cloud-4 {
--cloud-frozen-x: 12vw;
}
.cloud-5 {
--cloud-frozen-x: 44vw;
}
.cloud-6 {
--cloud-frozen-x: 73vw;
}
.cloud-7 {
--cloud-frozen-x: 26vw;
}
.cloud-8 {
--cloud-frozen-x: 66vw;
}
}
/* Subtle primary glow */
.site-bg::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: -1;
background:
radial-gradient(
ellipse 600px 400px at 20% 0%,
color-mix(in srgb, var(--color-primary) 10%, transparent) 0%,
transparent 100%
),
radial-gradient(
ellipse 500px 300px at 80% 0%,
color-mix(in srgb, var(--color-primary) 6%, transparent) 0%,
transparent 100%
);
}
html.dark .site-bg {
--site-bg-gradient: linear-gradient(
180deg,
color-mix(in srgb, var(--color-primary) 6%, #0a0e1a) 0%,
color-mix(in srgb, var(--color-primary) 8%, #10152a) 30%,
color-mix(in srgb, var(--color-primary) 10%, #161c30) 60%,
color-mix(in srgb, var(--color-primary) 12%, #0f141f) 100%
);
/* Re-declaring `background-image` here would out-specify the wallpaper
layer injected by ThemeVars (body.site-bg) and silently drop it in dark
mode. The gradient is picked up through `--site-bg-gradient` instead. */
background-attachment: fixed;
}
html.dark .site-bg::before {
opacity: 0.08;
background:
radial-gradient(
ellipse 240px 60px at 15% 8%,
rgba(200, 210, 255, 0.5) 0%,
transparent 100%
),
radial-gradient(
ellipse 320px 70px at 35% 12%,
rgba(200, 210, 255, 0.4) 0%,
transparent 100%
),
radial-gradient(
ellipse 200px 50px at 55% 6%,
rgba(200, 210, 255, 0.45) 0%,
transparent 100%
),
radial-gradient(
ellipse 280px 65px at 75% 10%,
rgba(200, 210, 255, 0.5) 0%,
transparent 100%
),
radial-gradient(
ellipse 180px 45px at 90% 5%,
rgba(200, 210, 255, 0.35) 0%,
transparent 100%
),
radial-gradient(
ellipse 260px 55px at 50% 18%,
rgba(200, 210, 255, 0.25) 0%,
transparent 100%
),
radial-gradient(
ellipse 200px 40px at 20% 22%,
rgba(200, 210, 255, 0.2) 0%,
transparent 100%
);
}
html.dark .site-bg::after {
background:
radial-gradient(
ellipse 600px 400px at 20% 0%,
color-mix(in srgb, var(--color-primary) 14%, transparent) 0%,
transparent 100%
),
radial-gradient(
ellipse 500px 300px at 80% 0%,
color-mix(in srgb, var(--color-primary) 8%, transparent) 0%,
transparent 100%
);
}
/* Admin must not inherit the public wallpaper — solid HK canvas only. */
body.site-bg:has([data-admin]),
html.dark body.site-bg:has([data-admin]) {
background: var(--admin-canvas);
background-image: none;
background-blend-mode: normal;
}
/* ── Animated site backdrop (Theme Manager effects) ────────────────
A fixed, decorative layer between the body wallpaper and the content.
Theme-driven: aurora + particles are pure CSS on transform/opacity, so
they stay cheap and pause for prefers-reduced-motion automatically. */
.site-backdrop {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
contain: layout paint style;
}
.site-backdrop > span {
position: absolute;
inset: 0;
pointer-events: none;
}
body:has([data-admin]) .site-backdrop {
display: none;
}
/* Aurora blobs (large, soft radial gradients — no filters). */
.site-backdrop > .site-backdrop-aurora {
inset: -12% -10%;
overflow: hidden;
}
.site-backdrop-blob {
position: absolute;
border-radius: 999px;
opacity: 0.42;
will-change: transform;
animation: aurora-drift 20s ease-in-out infinite alternate;
}
.site-backdrop-blob-1 {
width: clamp(320px, 44vw, 620px);
height: clamp(320px, 44vw, 620px);
top: -14%;
left: -10%;
background: radial-gradient(
circle at 32% 32%,
color-mix(in srgb, var(--color-primary) 60%, transparent) 0%,
transparent 68%
);
}
.site-backdrop-blob-2 {
width: clamp(280px, 38vw, 540px);
height: clamp(280px, 38vw, 540px);
top: -10%;
right: -8%;
background: radial-gradient(
circle at 62% 40%,
color-mix(in srgb, var(--color-accent) 48%, transparent) 0%,
transparent 68%
);
animation-delay: -7s;
animation-duration: 24s;
}
.site-backdrop-blob-3 {
width: clamp(360px, 50vw, 700px);
height: clamp(360px, 50vw, 700px);
bottom: -22%;
left: 12%;
background: radial-gradient(
circle at 50% 50%,
color-mix(in srgb, var(--color-primary) 34%, transparent) 0%,
color-mix(in srgb, var(--color-accent) 18%, transparent) 42%,
transparent 72%
);
animation-delay: -13s;
animation-duration: 28s;
}
@keyframes aurora-drift {
0% {
transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
}
100% {
transform: translate3d(6vw, 5vh, 0) scale(1.16) rotate(6deg);
}
}
html.dark .site-backdrop-blob {
opacity: 0.55;
}
/* Floating particles drifting upward. */
.site-backdrop-particles {
inset: 0;
}
.site-backdrop-dot {
position: absolute;
bottom: -4vh;
width: calc(var(--dot-size, 3) * 1px + 1px);
height: calc(var(--dot-size, 3) * 1px + 1px);
border-radius: 999px;
background: color-mix(in srgb, var(--color-primary) 70%, transparent);
box-shadow: 0 0 8px color-mix(in srgb, var(--color-primary) 55%, transparent);
opacity: 0;
animation: particle-rise var(--dot-duration, 12s) linear infinite;
animation-delay: var(--dot-delay, 0s);
will-change: transform;
}
@keyframes particle-rise {
0% {
transform: translate3d(0, 0, 0);
opacity: 0;
}
10% {
opacity: var(--dot-alpha, 0.45);
}
88% {
opacity: var(--dot-alpha, 0.45);
}
100% {
transform: translate3d(var(--dot-drift, 2vw), -108vh, 0);
opacity: 0;
}
}
html.dark .site-backdrop-dot {
background: color-mix(in srgb, var(--color-primary) 82%, transparent);
}
/* Overlay tint above image/effects keeps custom backgrounds readable. */
.site-backdrop-overlay {
inset: 0;
background: var(--background-overlay, transparent);
}
/* Show only the selected effect(s). */
.site-backdrop[data-effect="none"] .site-backdrop-particles,
.site-backdrop[data-effect="none"] .site-backdrop-aurora,
.site-backdrop[data-effect="aurora"] .site-backdrop-particles,
.site-backdrop[data-effect="particles"] .site-backdrop-aurora {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.site-backdrop-blob {
animation: none;
}
.site-backdrop-dot {
animation: none;
opacity: 0;
}
}
.article-body {
line-height: 1.7;
}
.article-body img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.article-body p {
margin: 0 0 0.85rem;
}
.article-body a {
text-decoration: underline;
}
.text-muted {
color: var(--color-text-muted-readable, var(--color-text-muted));
}
.text-nav {
color: var(--color-navbar-text-readable, var(--color-navbar-text));
}
.text-shadow {
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
}
.text-shadow-sm {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}
.hover-lift {
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.hover-lift:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-card-hover);
}
/* ── App component classes (used across the converted pages) ── */
h1 {
font-size: var(--size-heading-h1, 30px);
font-weight: 800;
margin: 0.2em 0 0.5em;
}
h2 {
font-size: var(--size-heading-h2, 22px);
font-weight: 700;
}
h3 {
font-size: var(--size-heading-h3, 18px);
font-weight: 700;
}
a {
color: var(--link-color-readable, var(--link-color));
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.muted {
color: var(--color-text-muted-readable, var(--color-text-muted));
font-size: 0.875rem;
}
.hero {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--gradient-from) 14%, var(--color-navbar)),
color-mix(in srgb, var(--gradient-to) 8%, var(--color-navbar))
);
border: 1px solid color-mix(in srgb, var(--color-primary) 20%, transparent);
border-radius: var(--radius-lg);
padding: 1.5rem 1.75rem;
box-shadow: var(--shadow-card);
margin-bottom: 1.5rem;
}
.card {
background: var(--color-surface);
color: var(--color-surface-text-readable, var(--color-text-readable));
border: 1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent);
border-radius: var(--card-border-radius);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 8px rgba(0, 0, 0, 0.03),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent);
padding: 1.1rem 1.25rem;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.card:hover {
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.06),
0 8px 24px rgba(0, 0, 0, 0.04),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 8%, transparent);
}
.card.hover:hover {
transform: translateY(-3px);
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.08),
0 12px 32px rgba(0, 0, 0, 0.06),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 10%, transparent);
}
.article-img {
width: 100%;
height: 140px;
object-fit: cover;
display: block;
background: linear-gradient(135deg, #fde68a, #fbbf24);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
padding: 0.5rem 1.1rem;
border-radius: var(--border-radius);
border: 2px solid transparent;
font-family: inherit;
font-weight: 700;
font-size: 14px;
cursor: pointer;
/* Desktop density: 40px. WCAG 2.2 AA only asks for 24px, so this is already
comfortable for a mouse. The 44px touch-target size (WCAG 2.1 AAA 2.5.5 /
Apple HIG) is applied below only for coarse pointers — growing every
button on desktop pushed each admin dialog and table 4px taller for no
benefit to mouse users. */
min-height: 40px;
transition: all 0.18s ease;
background: color-mix(
in srgb,
var(--color-text-muted) 14%,
var(--color-surface)
);
color: var(--color-text-readable, var(--color-text));
text-decoration: none;
}
/* Touch devices get the comfortable target size; desktop keeps the denser
layout. `pointer: coarse` matches finger input, so a hybrid laptop still uses
the 40px desktop metrics for its trackpad. */
@media (pointer: coarse) {
.btn {
min-height: 44px;
}
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-decoration: none;
}
.btn-primary {
background: var(--button-color);
color: var(--button-text-color-readable, var(--button-text-color));
}
.btn-secondary {
background: var(--button-secondary-color);
color: var(
--button-secondary-text-color-readable,
var(--button-secondary-text-color)
);
}
.btn-danger {
background: var(--button-danger-color);
color: var(
--button-danger-text-color-readable,
var(--button-danger-text-color)
);
}
.btn-outline {
background: transparent;
border-color: var(--button-outline-color);
color: var(
--button-outline-text-color-readable,
var(--button-outline-text-color)
);
}
input,
select,
textarea {
font-family: inherit;
font-size: 14px;
padding: 0.55rem 0.8rem;
border: 2px solid color-mix(in srgb, var(--color-text-muted) 20%, transparent);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text-readable, var(--color-text));
outline: none;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-primary) 15%, transparent);
}
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
color: var(--color-text-muted-readable, var(--color-text-muted));
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.5rem 0.6rem;
font-weight: 700;
}
td {
padding: 0.6rem 0.6rem;
border-top: 1px solid
color-mix(in srgb, var(--color-text-muted) 10%, transparent);
}
tbody tr {
transition: background 0.15s ease;
}
tbody tr:hover {
background: color-mix(in srgb, var(--color-primary) 5%, transparent);
}
.avatar {
border-radius: var(--avatar-border-radius);
background: var(--color-surface);
border: 2px solid var(--avatar-border-color);
}
.currency {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-weight: 800;
font-size: 0.85rem;
padding: 0.2rem 0.6rem 0.2rem 0.2rem;
border-radius: 999px;
background: var(--color-surface);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent);
color: var(--color-pill-text-readable, var(--color-text-readable));
}
.nav-credit-icon {
background: #e9b124 url("/assets/images/icons/currency/credits.png") center
no-repeat;
background-size: contain;
outline: 1px solid #b26d18;
}
.nav-ducket-icon {
background: #c44aac url("/assets/images/icons/currency/duckets.png") center
no-repeat;
background-size: contain;
outline: 1px solid #812378;
}
.nav-diamond-icon {
background: #caf1f3 url("/assets/images/icons/currency/diamonds.png") center
no-repeat;
background-size: contain;
outline: 1px solid #6caff4;
}
/* ── Admin panel (Housekeeping) ─────────────────────────────────
Modern, consistent styling for all admin content pages. */
.admin-page {
scroll-margin-top: 1rem;
}
/* Hub chrome already shows the section title — soften page-level heroes. */
.admin-hub-panel > .space-y-6 > div:first-child > h1.text-3xl {
font-size: 1.15rem;
letter-spacing: -0.01em;
}
/* Page heading */
.admin-page main > h1:first-child,
.admin-page > h1:first-child {
margin-top: 0;
}
/* Cards used inside admin pages */
.admin-card {
background: var(--admin-surface);
color: var(--admin-text);
border: 1px solid var(--admin-border);
border-radius: 16px;
box-shadow: var(--shadow-card);
padding: 1.25rem;
transition: box-shadow 0.2s ease;
}
/* Card with filter/search bar — compact horizontal layout */
.admin-filter-bar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-bottom: 1.25rem;
}
.admin-filter-bar input,
.admin-filter-bar select {
flex: 1;
min-width: 200px;
}
/* Compact stat row for list pages (counts, summaries) */
.admin-stat-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.admin-stat-chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.75rem;
background: color-mix(in srgb, var(--admin-accent) 8%, var(--admin-surface));
border: 1px solid color-mix(in srgb, var(--admin-accent) 15%, transparent);
border-radius: 999px;
font-size: 0.78rem;
font-weight: 600;
}
/* Tables */
.admin-page table {
width: 100%;
background: var(--admin-surface);
border: 1px solid var(--admin-border);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow-card);
border-collapse: separate;
border-spacing: 0;
}
.admin-page thead th {
background: color-mix(in srgb, var(--admin-accent) 8%, var(--admin-surface));
color: var(--admin-text);
border-bottom: 1px solid var(--admin-border);
white-space: nowrap;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.75rem 0.9rem;
}
.admin-page td {
padding: 0.7rem 0.9rem;
border-top: 1px solid
color-mix(in srgb, var(--admin-text-muted) 8%, transparent);
font-size: 0.88rem;
}
.admin-page tbody tr {
transition: background 0.12s ease;
}
.admin-page tbody tr:hover {
background: color-mix(in srgb, var(--admin-accent) 4%, transparent);
}
.admin-page thead tr:first-child th:first-child {
border-radius: 16px 0 0 0;
}
.admin-page thead tr:first-child th:last-child {
border-radius: 0 16px 0 0;
}
/* Section heading inside admin pages */
.admin-section-title {
font-size: 0.9rem;
font-weight: 700;
margin: 1.5rem 0 0.75rem;
color: var(--admin-text);
}
.admin-section-title:first-child {
margin-top: 0;
}
.admin-info-item {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
/* Empty state */
.admin-empty {
text-align: center;
padding: 2.5rem 1rem;
color: var(--admin-text-muted);
font-size: 0.85rem;
}
/* ── Public design system (faithful to the atom theme) ──────────
content-card = surface card with a primary-tinted header (icon +
title + subtitle) and a padded body — the signature atom block. */
.content-card {
display: flex;
flex-direction: column;
width: 100%;
background: var(--color-surface);
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
border: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 10%,
transparent
);
border-radius: var(--radius-lg);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 8px rgba(0, 0, 0, 0.03),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 6%, transparent);
overflow: hidden;
transition:
box-shadow 0.2s ease,
transform 0.2s ease;
}
.content-card:hover {
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.06),
0 8px 24px rgba(0, 0, 0, 0.04),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 8%, transparent);
}
.content-card-head {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1.1rem;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--color-primary) 8%, var(--color-surface)),
color-mix(in srgb, var(--color-primary) 4%, var(--color-surface))
);
border-bottom: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 8%,
transparent
);
}
.content-card-icon {
flex: none;
width: 36px;
height: 36px;
border-radius: 10px;
display: grid;
place-items: center;
font-size: 1.1rem;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--color-primary) 18%, var(--color-surface)),
color-mix(in srgb, var(--color-primary) 10%, var(--color-surface))
);
box-shadow: inset 0 0 0 1px
color-mix(in srgb, var(--color-primary) 16%, transparent);
}
.content-card-head-text {
min-width: 0;
flex: 1;
}
.content-card-title {
margin: 0;
font-weight: 700;
font-size: 0.92rem;
line-height: 1.3;
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
}
.content-card-subtitle {
margin: 0.15rem 0 0;
font-size: 0.75rem;
color: var(--color-text-muted-readable, var(--color-text-muted));
}
.content-card-action {
flex: none;
}
.content-card-body {
padding: 1.1rem;
}
/* content-card used as a whole clickable element (e.g. community tiles) */
a.content-card {
text-decoration: none;
}
a.content-card:hover {
transform: translateY(-3px);
}
a.content-card:focus-visible {
outline: 2px solid var(--color-primary-readable, var(--color-primary));
outline-offset: 2px;
}
a.content-card {
transition:
box-shadow 0.2s ease,
transform 0.2s ease;
}
/* Responsive card grids (atom: 1 / sm2 / md3 / lg4). */
.card-grid {
display: grid;
gap: 0.75rem;
grid-template-columns: 1fr;
}
@media (min-width: 40rem) {
.card-grid.sm-2 {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
}
@media (min-width: 48rem) {
.card-grid.md-2 {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.card-grid.md-3 {
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
}
@media (min-width: 64rem) {
.card-grid.lg-3 {
grid-template-columns: repeat(3, 1fr);
gap: 1.125rem;
}
.card-grid.lg-4 {
grid-template-columns: repeat(4, 1fr);
gap: 1.125rem;
}
}
/* Stat tiles (value in the primary colour + muted label). */
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
gap: 0.75rem;
}
.stat-block {
background: var(--color-surface);
border: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 10%,
transparent
);
border-radius: var(--radius-sm);
padding: 0.85rem 1rem;
text-align: center;
transition:
box-shadow 0.2s ease,
transform 0.2s ease;
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.03),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 5%, transparent);
}
.stat-block:hover {
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.06),
0 0 0 0.5px color-mix(in srgb, var(--color-text-muted) 8%, transparent);
transform: translateY(-1px);
}
.stat-block-value {
font-size: 1.5rem;
font-weight: 800;
line-height: 1.1;
color: var(--color-primary-readable, var(--color-primary));
}
.stat-block-label {
margin-top: 0.2rem;
font-size: 0.75rem;
color: var(--color-text-muted-readable, var(--color-text-muted));
}
/* Empty states */
.empty-state {
text-align: center;
padding: 2rem 1rem;
}
.empty-state-icon {
font-size: 2rem;
margin-bottom: 0.4rem;
}
/* Online / offline pill */
.online-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.74rem;
font-weight: 700;
padding: 0.1rem 0.55rem 0.1rem 0.45rem;
border-radius: 999px;
background: color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 14%,
transparent
);
color: var(--color-text-muted-readable, var(--color-text-muted));
}
.online-badge.online {
background: color-mix(in srgb, var(--color-success) 16%, transparent);
color: var(--color-success);
}
.online-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: currentColor;
}
.online-badge.online .online-dot {
background: var(--color-success);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-success) 22%, transparent);
}
/* Leaderboard rank chip */
.rank-badge {
display: inline-grid;
place-items: center;
min-width: 2.1rem;
height: 2.1rem;
padding: 0 0.35rem;
border-radius: 999px;
font-weight: 800;
font-size: 0.95rem;
background: var(--color-background);
border: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 18%,
transparent
);
}
.rank-badge.rank-1 {
background: color-mix(in srgb, var(--color-warning) 20%, transparent);
border-color: var(--color-warning);
}
.rank-badge.rank-2 {
background: color-mix(in srgb, #94a3b8 22%, transparent);
border-color: #94a3b8;
}
.rank-badge.rank-3 {
background: color-mix(in srgb, #f97316 20%, transparent);
border-color: #f97316;
}
/* ── Radio sidebar ──────────────────────────────────────────── */
.radio-layout {
display: flex;
gap: 1.5rem;
align-items: flex-start;
}
.radio-sidebar {
flex: none;
width: 200px;
position: sticky;
top: 5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.radio-sidebar-link:hover {
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
text-decoration: none;
}
@media (max-width: 48rem) {
.radio-layout {
flex-direction: column;
}
.radio-sidebar {
position: static;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 14%,
transparent
);
overflow-x: auto;
}
.radio-sidebar-head {
display: none;
}
.radio-sidebar-link {
font-size: 0.82rem;
padding: 0.35rem 0.6rem;
white-space: nowrap;
}
}
/* ── Skeleton shimmer animation ─────────────────────────────── */
@keyframes shimmer {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
/* Footer CMS info popup */
.cms-info-trigger {
appearance: none;
border: 0;
background: transparent;
color: var(--color-primary-readable, var(--color-primary));
cursor: pointer;
font: inherit;
font-size: 0.78rem;
font-weight: 800;
line-height: 1;
padding: 0.35rem 0.5rem;
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 3px;
}
.cms-info-trigger:hover,
.cms-info-trigger:focus-visible {
color: var(--color-primary-readable, var(--color-primary));
}
.cms-info-trigger:focus-visible,
.cms-info-close:focus-visible {
outline: 3px solid color-mix(in srgb, var(--color-primary) 42%, transparent);
outline-offset: 3px;
}
/* Centering layer. Transparent and click-through itself, so only the backdrop
button and the dialog catch pointer events. */
.cms-info-layer {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 1rem;
pointer-events: none;
}
.cms-info-backdrop {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: 0;
cursor: default;
background: rgba(8, 10, 14, 0.68);
backdrop-filter: blur(6px);
pointer-events: auto;
}
/* Enter/exit were motion/react opacity pairs; the dialog itself also used a
scale/translate pair. Paired with the 180ms unmount delay in CmsInfoPopup. */
@keyframes cms-info-backdrop-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes cms-info-backdrop-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes cms-info-dialog-in {
from {
opacity: 0;
transform: scale(0.95) translate3d(0, 10px, 0);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes cms-info-dialog-out {
from {
opacity: 1;
transform: none;
}
to {
opacity: 0;
transform: scale(0.95) translate3d(0, 10px, 0);
}
}
.cms-info-backdrop-enter {
animation: cms-info-backdrop-in 200ms ease-out both;
}
.cms-info-backdrop-exit {
animation: cms-info-backdrop-out 180ms ease-in both;
}
.cms-info-dialog-enter {
animation: cms-info-dialog-in 200ms ease-out both;
}
.cms-info-dialog-exit {
animation: cms-info-dialog-out 180ms ease-in both;
}
/* Sits above the backdrop button inside the pointer-transparent layer. */
.cms-info-dialog-layer {
pointer-events: auto;
position: relative;
z-index: 1;
}
.cms-info-dialog {
position: relative;
width: min(440px, 100%);
border: 1px solid
color-mix(
in srgb,
var(--color-primary) 30%,
color-mix(in srgb, var(--color-text-muted) 22%, transparent)
);
border-radius: 8px;
background:
linear-gradient(
135deg,
color-mix(in srgb, var(--color-primary) 12%, transparent),
transparent 42%
),
var(--color-surface);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.34);
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
padding: 1.25rem;
}
.cms-info-close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
display: grid;
/* 44px touch target (was 2rem/32px) while keeping the circular button visually
the same weight via its border and glyph. */
width: 2.75rem;
height: 2.75rem;
place-items: center;
border: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 18%,
transparent
);
border-radius: 999px;
background: color-mix(in srgb, var(--color-surface) 84%, transparent);
color: var(--color-text-muted-readable, var(--color-text-muted));
cursor: pointer;
font-weight: 900;
line-height: 1;
}
.cms-info-close:hover {
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
border-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
}
.cms-info-kicker {
color: var(--color-primary-readable, var(--color-primary));
font-size: 0.74rem;
font-weight: 900;
letter-spacing: 0;
text-transform: uppercase;
}
.cms-info-dialog h2 {
margin: 0.2rem 2.2rem 0 0;
font-size: 1.75rem;
font-weight: 900;
line-height: 1.1;
}
.cms-info-badge {
display: inline-flex;
align-items: center;
margin-top: 0.65rem;
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
font-size: 0.75rem;
font-weight: 900;
padding: 0.3rem 0.6rem;
}
.cms-info-copy {
margin: 0.9rem 0 0;
color: var(--color-text-muted-readable, var(--color-text-muted));
font-size: 0.92rem;
line-height: 1.55;
}
.cms-info-grid {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 1rem 0 0;
}
.cms-info-grid div {
border: 1px solid
color-mix(
in srgb,
var(--color-text-muted-readable, var(--color-text-muted)) 14%,
transparent
);
border-radius: 8px;
background: color-mix(in srgb, var(--color-background) 68%, transparent);
padding: 0.7rem;
}
.cms-info-grid dt {
color: var(--color-text-muted-readable, var(--color-text-muted));
font-size: 0.68rem;
font-weight: 800;
text-transform: uppercase;
}
.cms-info-grid dd {
margin: 0.18rem 0 0;
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
font-size: 0.8rem;
font-weight: 900;
line-height: 1.25;
}
.cms-info-credit-list {
display: grid;
gap: 0.5rem;
margin-top: 1rem;
}
.cms-info-credit {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
border: 1px solid color-mix(in srgb, var(--color-primary) 18%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-primary) 8%, transparent);
padding: 0.65rem 0.75rem;
}
.cms-info-credit span {
color: var(
--color-surface-text-readable,
var(--color-text-readable, var(--color-text))
);
font-weight: 900;
}
.cms-info-credit small {
color: var(--color-text-muted-readable, var(--color-text-muted));
font-size: 0.74rem;
font-weight: 700;
text-align: right;
}
@media (max-width: 520px) {
.cms-info-dialog {
padding: 1rem;
}
.cms-info-grid {
grid-template-columns: 1fr;
}
.cms-info-credit {
align-items: flex-start;
flex-direction: column;
gap: 0.2rem;
}
.cms-info-credit small {
text-align: left;
}
}
/* ── Mobile responsive overrides ──────────────────────────── */
@media (max-width: 48rem) {
h1 {
font-size: clamp(1.25rem, 5vw, var(--size-heading-h1, 30px));
}
h2 {
font-size: clamp(1.1rem, 4vw, var(--size-heading-h2, 22px));
}
h3 {
font-size: clamp(0.95rem, 3.5vw, var(--size-heading-h3, 18px));
}
.hero {
padding: 1.25rem 1rem;
border-radius: var(--radius-md);
}
.card {
padding: 0.85rem 1rem;
border-radius: var(--radius-md);
}
.admin-card {
padding: 0.85rem;
}
.admin-filter-bar input,
.admin-filter-bar select {
min-width: 140px;
}
.admin-page td,
.admin-page th {
white-space: nowrap;
padding: 0.5rem 0.6rem;
font-size: 0.78rem;
}
.stat-grid {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.6rem;
}
.stat-block {
padding: 0.65rem 0.5rem;
border-radius: var(--radius-sm);
}
.stat-block-value {
font-size: 1.25rem;
}
.btn {
font-size: 13px;
padding: 0.4rem 0.85rem;
min-height: 44px;
}
.content-card-head {
padding: 0.65rem 0.85rem;
}
.content-card-body {
padding: 0.85rem;
}
.content-card-icon {
width: 32px;
height: 32px;
border-radius: 8px;
font-size: 0.95rem;
}
.nav-item {
white-space: normal;
text-wrap: balance;
font-size: 12px;
padding: 0.5rem 0.75rem;
height: auto;
min-height: 44px;
}
.dropdown-item {
font-size: 13px;
padding: 0.6rem 0.85rem;
min-height: 44px;
}
.content-card-title {
font-size: 0.85rem;
}
.content-card-subtitle {
font-size: 0.7rem;
}
}
/* Rich content-page grid (replaces repeated inline `display:grid;gap:1.5rem`). */
.page-grid {
display: grid;
gap: 1.5rem;
}
/* make tables scroll on small screens */
@media (max-width: 640px) {
table {
display: table;
}
.table-scroll {
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border-radius: var(--radius-sm);
-ms-overflow-style: none;
scrollbar-width: thin;
}
}
/* Cell text truncation helper for dense tables so long values (motto, URLs)
collapse gracefully instead of blowing out row width on small screens. */
.table-cell-truncate {
max-width: 14rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Safe-area insets for notched phones (header/nav/footer sit at screen edges) */
@supports (padding: max(0px)) {
.site-header,
nav,
footer {
padding-left: max(env(safe-area-inset-left), 0px);
padding-right: max(env(safe-area-inset-right), 0px);
}
}
/* ── Admin (HK) semantic token remap ──────────────────────────────────────
Shared shadcn UI primitives (Button, Badge, Input, Select, Card, Table,
Tabs, Dialog, Switch, Checkbox, Tooltip, …) are built on the site's
semantic tokens (--color-background, --color-primary, --color-popover,
--color-border, --color-ring, --color-muted-foreground, …). On admin
pages we remap those tokens onto the admin palette so every embedded
component renders with the admin theme and guaranteed contrast, without
touching any component file.
Gated by `body:has([data-admin])` so the public site is completely
unaffected. This also covers Radix portals (Dialog/Select/Dropdown/
Popover/Tooltip) which mount under <body>. */
body:has([data-admin]) {
/* Full public→admin token remap so shadcn + body + leftover
public utilities all inherit the admin palette with contrast. */
--color-background: var(--admin-canvas);
--color-foreground: var(--admin-text);
--color-card: var(--admin-surface);
--color-card-foreground: var(--admin-text);
--color-popover: var(--admin-surface-elevated);
--color-popover-foreground: var(--admin-text);
--color-primary: var(--admin-accent);
--color-primary-hover: color-mix(in srgb, var(--admin-accent) 84%, #000000);
--color-primary-foreground: var(--admin-accent-foreground);
--color-secondary: color-mix(
in srgb,
var(--admin-accent) 14%,
var(--admin-surface)
);
--color-secondary-foreground: var(--admin-text);
--color-muted: color-mix(in srgb, var(--admin-text) 9%, var(--admin-surface));
--color-muted-foreground: var(--admin-text-muted);
--color-accent: color-mix(
in srgb,
var(--admin-accent) 16%,
var(--admin-surface)
);
--color-accent-foreground: var(--admin-text);
--color-destructive: var(--admin-error);
--color-destructive-foreground: var(--admin-error-foreground);
--color-border: var(--admin-border);
--color-input: var(--admin-border);
--color-ring: var(--admin-accent);
/* Public Atom tokens still referenced by body / .muted / inputs */
--color-text: var(--admin-text);
--color-text-readable: var(--admin-text);
--color-text-muted: var(--admin-text-muted);
--color-text-muted-readable: var(--admin-text-muted);
--color-surface: var(--admin-surface);
--color-surface-text-readable: var(--admin-text);
--color-dropdown: var(--admin-surface);
--color-dropdown-text-readable: var(--admin-text);
--input-border-color: var(--admin-border);
--input-focus-color: var(--admin-accent);
--muted: var(--admin-text-muted);
--border-subtle: color-mix(in srgb, var(--admin-text-muted) 22%, transparent);
color: var(--admin-text);
background-color: var(--admin-canvas);
font-family: var(--font-family);
}
/* Admin forms / boxes follow admin tokens in both light and dark. */
body:has([data-admin])
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(
[type="file"]
):not([type="color"]),
body:has([data-admin]) select,
body:has([data-admin]) textarea {
background-color: var(--admin-surface-elevated);
color: var(--admin-text);
border-color: var(--admin-border);
caret-color: var(--admin-text);
}
body:has([data-admin]) input::placeholder,
body:has([data-admin]) textarea::placeholder {
color: var(--admin-text-muted);
opacity: 0.85;
}
body:has([data-admin]) .muted,
body:has([data-admin]) .admin-empty,
body:has([data-admin]) .text-muted-foreground {
color: var(--admin-text-muted);
}
body:has([data-admin]) .admin-card,
body:has([data-admin]) .admin-page table {
background: var(--admin-surface);
color: var(--admin-text);
border-color: var(--admin-border);
}
body:has([data-admin]) .btn-outline {
color: var(--admin-text);
border-color: var(--admin-border);
background: transparent;
}
body:has([data-admin]) .btn-outline:hover {
background: color-mix(in srgb, var(--admin-accent) 12%, transparent);
color: var(--admin-text);
}
/* Override public html.dark input hardcodes inside admin. */
html.dark
body:has([data-admin])
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not(
[type="file"]
):not([type="color"]),
html.dark body:has([data-admin]) select,
html.dark body:has([data-admin]) textarea {
background: var(--admin-surface-elevated);
color: var(--admin-text);
}
/* Opaque floating surfaces for Radix portals (Select / Dropdown / Popover / Dialog).
Without this, bg-popover can resolve transparent and lists paint over forms. */
body:has([data-admin]) [data-slot="select-content"],
body:has([data-admin]) [data-slot="dropdown-menu-content"],
body:has([data-admin]) [data-slot="dropdown-menu-sub-content"],
body:has([data-admin]) [data-slot="popover-content"],
body:has([data-admin]) [data-slot="context-menu-content"],
body:has([data-admin]) [data-slot="dialog-content"],
body:has([data-admin]) [data-slot="sheet-content"] {
background-color: var(--admin-surface-elevated);
color: var(--admin-text);
border-color: var(--admin-border);
box-shadow:
0 18px 48px rgba(0, 0, 0, 0.45),
0 4px 12px rgba(0, 0, 0, 0.25);
}
body:has([data-admin]) [data-slot="select-trigger"],
body:has([data-admin]) [data-slot="dropdown-menu-trigger"] {
background-color: var(--admin-surface-elevated);
color: var(--admin-text);
border-color: var(--admin-border);
}
body:has([data-admin]) [data-slot="select-item"][data-highlighted],
body:has([data-admin]) [data-slot="select-item"]:focus,
body:has([data-admin]) [data-slot="dropdown-menu-item"][data-highlighted],
body:has([data-admin]) [data-slot="dropdown-menu-item"]:focus,
body:has([data-admin]) [data-slot="context-menu-item"][data-highlighted],
body:has([data-admin]) [data-slot="context-menu-item"]:focus {
background-color: color-mix(
in srgb,
var(--admin-accent) 18%,
var(--admin-surface-elevated)
);
color: var(--admin-text);
}
body:has([data-admin]) [data-slot="card"] {
background-color: var(--admin-surface);
color: var(--admin-text);
border-color: var(--admin-border);
}
/* ── Details/summary open states for nav ──────────────────── */
details[open] > summary .details-open\:hidden {
display: none;
}
details[open] > summary .details-open\:block {
display: block;
}
details[open] > summary .details-open\:rotate-180 {
transform: rotate(180deg);
}
/* ── Public error / 404 screen ───────────────────────────── */
@keyframes error-screen-rise {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.error-screen {
position: relative;
isolation: isolate;
display: grid;
place-items: center;
min-height: min(70vh, 36rem);
padding: 2rem 1rem 3rem;
}
.error-screen-glow {
pointer-events: none;
position: absolute;
inset: 10% 15% auto;
height: 12rem;
border-radius: 50%;
background: radial-gradient(
ellipse at center,
color-mix(in srgb, var(--color-primary) 35%, transparent),
transparent 70%
);
filter: blur(8px);
z-index: 0;
opacity: 0.85;
}
.error-screen-panel {
position: relative;
z-index: 1;
width: min(100%, 28rem);
text-align: center;
padding: 2.25rem 1.75rem 2rem;
border-radius: 1.25rem;
border: 1px solid
color-mix(in srgb, var(--color-primary) 28%, var(--border-subtle));
background: linear-gradient(
165deg,
color-mix(in srgb, var(--color-primary) 10%, var(--color-surface)),
var(--color-surface)
);
box-shadow:
0 20px 48px color-mix(in srgb, rgb(0, 0, 0) 18%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--color-primary) 18%, transparent);
animation: error-screen-rise 0.45s ease-out both;
}
.error-screen-code {
margin: 0 0 0.65rem;
font-family: var(--font-pixel), "Courier New", monospace;
font-size: clamp(2.75rem, 9vw, 3.75rem);
font-weight: 700;
letter-spacing: 0.06em;
line-height: 1;
color: var(--color-primary-readable, var(--color-primary));
text-shadow: 0 0 36px
color-mix(in srgb, var(--color-primary) 45%, transparent);
}
.error-screen-title {
margin: 0 0 0.4rem;
font-size: 1.4rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--color-text);
}
.error-screen-subtitle {
margin: 0 0 0.85rem;
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
opacity: 0.88;
}
.error-screen-body {
margin: 0 0 1.5rem;
font-size: 0.95rem;
line-height: 1.55;
color: var(--color-text-muted);
}
.error-screen-actions {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
justify-content: center;
}
.error-screen-actions .btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
}
.error-screen-btn-icon {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
.error-screen-footer {
margin-top: 1.25rem;
}
.error-screen-digest {
margin: 0;
font-size: 0.72rem;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
word-break: break-all;
}
@media (prefers-reduced-motion: reduce) {
.error-screen-panel {
animation: none;
}
}
/* ── Premium effects ── */
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-8px);
}
}
@keyframes shimmer-glow {
0% {
opacity: 0.4;
}
50% {
opacity: 1;
}
100% {
opacity: 0.4;
}
}
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-glow {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
.animate-float {
animation: float 4s ease-in-out infinite;
will-change: transform;
}
.animate-shimmer {
animation: shimmer-glow 2.5s ease-in-out infinite;
}
.animate-fade-in-up {
animation: fade-in-up 0.6s ease-out both;
}
.animate-scale-in {
animation: scale-in 0.4s ease-out both;
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
.animate-shake {
animation: shake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes shake {
10%,
90% {
transform: translateX(-2px);
}
20%,
80% {
transform: translateX(4px);
}
30%,
50%,
70% {
transform: translateX(-6px);
}
40%,
60% {
transform: translateX(6px);
}
}
/* Glass morphism */
.glass {
background: color-mix(in srgb, var(--color-surface) 70%, transparent);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent);
}
.glass-strong {
background: color-mix(in srgb, var(--color-surface) 85%, transparent);
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 12%, transparent);
}
/* Premium card with depth */
.card-premium {
background: var(--color-surface);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent);
border-radius: var(--radius-lg);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.04),
0 12px 32px rgba(0, 0, 0, 0.04);
transition:
transform 0.25s ease,
box-shadow 0.25s ease;
}
.card-premium:hover {
transform: translateY(-3px);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 6px 16px rgba(0, 0, 0, 0.06),
0 16px 40px rgba(0, 0, 0, 0.08);
}
/* Gradient border wrapper */
.gradient-border {
position: relative;
border-radius: var(--radius-lg);
background: linear-gradient(
135deg,
var(--color-primary),
color-mix(in srgb, var(--color-primary) 50%, var(--color-accent))
);
padding: 1px;
}
.gradient-border > * {
border-radius: calc(var(--radius-lg) - 1px);
}
/* Input focus glow */
.input-glow:focus {
outline: none;
border-color: var(--color-primary);
box-shadow:
0 0 0 3px color-mix(in srgb, var(--color-primary) 15%, transparent),
0 0 20px color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.input-glow {
transition:
border-color 0.25s ease,
box-shadow 0.25s ease;
}
/* Typing cursor blink */
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.typing-cursor::after {
content: "|";
animation: blink 0.8s step-end infinite;
margin-left: 2px;
font-weight: 300;
color: var(--color-primary);
}
/* Number count-up */
@keyframes count-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-count {
animation: count-up 0.6s ease-out both;
}
/* Premium button shine */
.btn-shine {
position: relative;
overflow: hidden;
}
.btn-shine::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
transparent 0%,
transparent 35%,
color-mix(in srgb, white 8%, transparent) 50%,
transparent 65%,
transparent 100%
);
transform: translateX(-60%) rotate(25deg);
transition: transform 0.6s cubic-bezier(0.33, 1, 0.68, 1);
pointer-events: none;
will-change: transform;
}
.btn-shine:hover::after {
transform: translateX(30%) rotate(25deg);
}
/* Staggered children animation */
.stagger-children > * {
animation: fade-in-up 0.5s ease-out both;
}
.stagger-children > *:nth-child(1) {
animation-delay: 0.05s;
}
.stagger-children > *:nth-child(2) {
animation-delay: 0.1s;
}
.stagger-children > *:nth-child(3) {
animation-delay: 0.15s;
}
.stagger-children > *:nth-child(4) {
animation-delay: 0.2s;
}
.stagger-children > *:nth-child(5) {
animation-delay: 0.25s;
}
.stagger-children > *:nth-child(6) {
animation-delay: 0.3s;
}
.stagger-children > *:nth-child(7) {
animation-delay: 0.35s;
}
.stagger-children > *:nth-child(8) {
animation-delay: 0.4s;
}
/* ── Classic-landing polish (theme-aware) ────────────────────────────
Keeps the Habbo sky and theme colors; adds subtle depth + glow to the
restored landing pages. Everything is driven by theme CSS variables. */
.card-glow {
transition:
box-shadow 0.3s ease,
transform 0.3s ease,
border-color 0.3s ease;
}
.card-glow:hover {
box-shadow:
0 12px 32px -10px color-mix(in srgb, var(--color-primary) 32%, transparent),
0 0 0 1px color-mix(in srgb, var(--color-primary) 22%, transparent);
}
/* Gradient accent hairline drawn on the top edge of a relative card. */
.card-hairline {
position: relative;
}
.card-hairline::before {
content: "";
position: absolute;
inset-inline: 2%;
top: 0;
height: 2px;
border-radius: 999px;
background: linear-gradient(
90deg,
transparent,
color-mix(in srgb, var(--color-primary) 75%, transparent) 30%,
var(--color-primary) 50%,
color-mix(in srgb, var(--color-accent) 70%, transparent) 70%,
transparent
);
opacity: 0.55;
pointer-events: none;
z-index: 1;
}
/* Brand gradient text (primary → accent). */
.gradient-text {
background: linear-gradient(
115deg,
var(--color-primary) 10%,
color-mix(in srgb, var(--color-primary) 55%, var(--color-accent)) 48%,
color-mix(in srgb, var(--color-accent) 92%, var(--color-primary)) 92%
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Section eyebrow label with a small accent tick. */
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-primary-readable, var(--color-primary));
}
.eyebrow::before {
content: "";
width: 1.25rem;
height: 2px;
border-radius: 999px;
background: currentColor;
opacity: 0.75;
}
/* Section heading on the public landing pages. */
.section-title {
color: var(--color-text-readable);
font-family: var(--font-nunito);
font-weight: 900;
letter-spacing: -0.02em;
}
/* ── Landing panel system ────────────────────────────────────────────
One recipe for every card on the public pages. The old panels each mixed
their own translucent washes (surface 44–60% + backdrop blur), which read as
a milky smear over the sky gradient, gave the same four numbers two
different-looking boxes, and cost a backdrop snapshot per panel on every
scroll. These tokens are opaque, theme-derived and shared. */
:root {
--panel-surface: color-mix(
in srgb,
var(--color-surface) 93%,
var(--color-primary)
);
--panel-surface-alt: color-mix(
in srgb,
var(--color-surface) 88%,
var(--color-accent)
);
--panel-surface-sunken: color-mix(
in srgb,
var(--color-surface) 96%,
var(--color-background)
);
--panel-border: color-mix(in srgb, var(--color-text-muted) 16%, transparent);
--panel-border-strong: color-mix(
in srgb,
var(--color-primary) 32%,
transparent
);
--panel-shadow:
0 1px 2px color-mix(in srgb, #0b1220 7%, transparent),
0 18px 40px -26px color-mix(in srgb, #0b1220 55%, transparent);
--panel-header-bg: color-mix(
in srgb,
var(--color-primary) 8%,
var(--color-surface)
);
--panel-header-border: color-mix(
in srgb,
var(--color-text-muted) 12%,
transparent
);
--panel-radius: 18px;
}
html.dark {
--panel-surface: color-mix(
in srgb,
var(--color-surface) 88%,
var(--color-background)
);
--panel-surface-alt: color-mix(
in srgb,
var(--color-surface) 82%,
var(--color-background)
);
--panel-surface-sunken: color-mix(
in srgb,
var(--color-surface) 92%,
var(--color-background)
);
--panel-border: color-mix(in srgb, var(--color-text-muted) 24%, transparent);
--panel-border-strong: color-mix(
in srgb,
var(--color-primary) 46%,
transparent
);
--panel-shadow:
0 1px 2px rgb(0 0 0 / 0.4), 0 24px 48px -28px rgb(0 0 0 / 0.85);
--panel-header-bg: color-mix(
in srgb,
var(--color-primary) 13%,
var(--color-surface)
);
--panel-header-border: color-mix(
in srgb,
var(--color-text-muted) 20%,
transparent
);
}
/* The standard card. `rounded-[--panel-radius]` is spelled out in the
component, this class only owns colour, border and depth. */
.panel {
background: var(--panel-surface);
border: 1px solid var(--panel-border);
box-shadow: var(--panel-shadow);
}
/* A panel that floats over the hero, so it needs a deeper shadow. */
.panel-float {
box-shadow:
0 2px 6px color-mix(in srgb, #0b1220 10%, transparent),
0 34px 64px -30px color-mix(in srgb, #0b1220 62%, transparent);
}
html.dark .panel-float {
box-shadow:
0 2px 6px rgb(0 0 0 / 0.5),
0 36px 70px -30px rgb(0 0 0 / 0.9);
}
/* A tile that sits *inside* a panel — icon chips, stat wells, list rows. */
.panel-sunken {
background: var(--panel-surface-sunken);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent);
}
.panel-header {
background: var(--panel-header-bg);
border-bottom: 1px solid var(--panel-header-border);
}
/* Icon chip inside a panel. `--panel-tint` is set per instance. */
.panel-tile {
background: color-mix(
in srgb,
var(--panel-tint, var(--color-primary)) 15%,
transparent
);
box-shadow: inset 0 0 0 1px
color-mix(in srgb, var(--panel-tint, var(--color-primary)) 26%, transparent);
}
/* ── Smooth motion (landing) ───────────────────────────────────────── */
html {
scroll-behavior: smooth;
}
/* Soft, slow drift for hero artwork (Ken Burns). */
.ken-burns {
animation: ken-burns 20s ease-in-out infinite alternate;
will-change: transform;
}
@keyframes ken-burns {
from {
transform: scale(1.02) translateY(0);
}
to {
transform: scale(1.08) translateY(-1.5%);
}
}
/* Gentle breathing pulse for the brand glows behind Frank / the hero. */
.brand-halo {
position: absolute;
pointer-events: none;
border-radius: 999px;
background: radial-gradient(
circle,
color-mix(in srgb, var(--color-primary) 30%, transparent) 0%,
transparent 70%
);
filter: blur(20px);
animation: hero-halo-pulse 6s ease-in-out infinite;
will-change: transform;
}
@keyframes hero-halo-pulse {
0%,
100% {
opacity: 0.55;
transform: scale(1);
}
50% {
opacity: 0.9;
transform: scale(1.07);
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.ken-burns,
.brand-halo {
animation: none;
}
.hero-ring {
animation: none;
}
}
/* ── Hero pro touches ──────────────────────────────────────────────── */
/* Continuously shifting gradient hairline drawn on the hero border.
Animated with opacity only (fully composited — no per-frame repaint). */
.hero-ring {
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(
120deg,
color-mix(in srgb, var(--color-primary) 70%, transparent) 0%,
color-mix(in srgb, var(--color-accent) 60%, transparent) 35%,
var(--color-surface) 50%,
color-mix(in srgb, var(--color-accent) 60%, transparent) 65%,
color-mix(in srgb, var(--color-primary) 70%, transparent) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
animation: hero-ring-pulse 14s ease-in-out infinite;
}
@keyframes hero-ring-pulse {
0%,
100% {
opacity: 0.75;
}
50% {
opacity: 1;
}
}
/* ── Public page polish: buttons ──────────────────────────────────── */
.btn-brand {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 999px;
background: linear-gradient(
120deg,
var(--color-primary) 0%,
color-mix(in srgb, var(--color-primary) 72%, var(--color-accent)) 55%,
color-mix(in srgb, var(--color-accent) 85%, var(--color-primary)) 100%
);
color: var(--color-primary-foreground);
box-shadow:
0 10px 26px -12px color-mix(in srgb, var(--color-primary) 75%, transparent),
inset 0 1px 0 0 color-mix(in srgb, white 35%, transparent);
transition:
transform 0.22s ease,
box-shadow 0.22s ease,
filter 0.22s ease;
}
.btn-brand:hover {
transform: translateY(-2px);
filter: brightness(1.05);
box-shadow:
0 18px 36px -14px color-mix(in srgb, var(--color-primary) 85%, transparent),
inset 0 1px 0 0 color-mix(in srgb, white 45%, transparent);
}
.btn-brand:active {
transform: translateY(0) scale(0.985);
}
.btn-brand:focus-visible {
outline: none;
box-shadow:
0 0 0 3px color-mix(in srgb, var(--color-primary) 35%, transparent),
0 14px 32px -14px color-mix(in srgb, var(--color-primary) 80%, transparent);
}
/* Frosted CTA meant to sit on top of the dark hero image. */
.btn-glass-dark {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 999px;
color: #fff;
background: color-mix(in srgb, #ffffff 10%, transparent);
border: 1px solid color-mix(in srgb, #ffffff 34%, transparent);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
box-shadow: inset 0 1px 0 0 color-mix(in srgb, #ffffff 25%, transparent);
transition:
background 0.22s ease,
border-color 0.22s ease,
transform 0.22s ease;
}
.btn-glass-dark:hover {
background: color-mix(in srgb, #ffffff 20%, transparent);
border-color: color-mix(in srgb, #ffffff 60%, transparent);
transform: translateY(-2px);
}
.btn-glass-dark:active {
transform: translateY(0);
}
.btn-glass-dark:focus-visible {
outline: none;
border-color: #fff;
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-primary) 55%, transparent);
}
/* ── Public page polish: auth fields ──────────────────────────────── */
.auth-label {
display: block;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--color-text-muted);
margin-bottom: 0.4rem;
}
.auth-input {
width: 100%;
border-radius: 14px;
border: 1.5px solid
color-mix(in srgb, var(--color-text-muted) 22%, transparent);
background: color-mix(
in srgb,
var(--color-background) 88%,
var(--color-surface)
);
padding: 0.78rem 1rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-readable);
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
background-color 0.2s ease;
}
.auth-input::placeholder {
color: color-mix(in srgb, var(--color-text-muted) 75%, transparent);
font-weight: 500;
}
.auth-input:hover:not(:disabled):not(:focus) {
border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
}
.auth-input:focus {
outline: none;
border-color: var(--color-primary);
background: var(--color-surface);
box-shadow:
0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent),
0 0 24px -8px color-mix(in srgb, var(--color-primary) 60%, transparent);
}
.auth-input[aria-invalid="true"]:not(:focus) {
border-color: color-mix(in srgb, var(--color-danger) 60%, transparent);
background: color-mix(
in srgb,
var(--color-danger) 6%,
color-mix(in srgb, var(--color-background) 88%, var(--color-surface))
);
}
.auth-input[aria-invalid="true"]:focus {
border-color: var(--color-danger);
box-shadow:
0 0 0 3px color-mix(in srgb, var(--color-danger) 18%, transparent),
0 0 24px -8px color-mix(in srgb, var(--color-danger) 55%, transparent);
}
.auth-input:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.auth-input.has-icon {
padding-left: 2.75rem;
}
.auth-input.has-action {
padding-right: 2.75rem;
}
/* Alert box used for form errors. */
.auth-alert {
display: flex;
align-items: flex-start;
gap: 0.5rem;
border-radius: 14px;
padding: 0.7rem 0.85rem;
font-size: 0.85rem;
font-weight: 700;
color: color-mix(
in srgb,
var(--color-danger) 85%,
var(--color-text-readable)
);
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-danger) 32%, transparent);
}
/* Same box with the success tint — post-registration confirmation, etc. */
.auth-alert.auth-alert--success {
color: color-mix(
in srgb,
var(--color-success) 85%,
var(--color-text-readable)
);
background: color-mix(in srgb, var(--color-success) 10%, transparent);
border-color: color-mix(in srgb, var(--color-success) 32%, transparent);
}
/* ── Public page polish: explore pills ────────────────────────────── */
.explore-pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.15rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 700;
color: var(--color-text-readable);
background: var(--panel-surface);
border: 1px solid var(--panel-border);
box-shadow: 0 1px 2px color-mix(in srgb, #0b1220 6%, transparent);
transition:
background 0.25s ease,
border-color 0.25s ease,
transform 0.25s ease,
box-shadow 0.25s ease;
}
.explore-pill:hover {
background: color-mix(
in srgb,
var(--color-primary) 14%,
var(--panel-surface)
);
border-color: var(--panel-border-strong);
transform: translateY(-2px);
box-shadow: 0 14px 28px -16px
color-mix(in srgb, var(--color-primary) 85%, transparent);
}
.explore-pill .pill-arrow {
opacity: 0;
transform: translateX(-4px);
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
.explore-pill:hover .pill-arrow {
opacity: 1;
transform: translateX(0);
}
.explore-pill:focus-visible {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--color-primary) 20%, transparent);
}
/* ── Public page polish: avatar tiles ─────────────────────────────── */
.avatar-tile {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
padding: 0.55rem 0.3rem;
border-radius: 14px;
background: color-mix(in srgb, var(--color-primary) 6%, transparent);
border: 1px solid color-mix(in srgb, var(--color-primary) 14%, transparent);
transition:
transform 0.25s ease,
border-color 0.25s ease,
box-shadow 0.25s ease,
background 0.25s ease;
}
.avatar-tile:hover {
transform: translateY(-3px);
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
box-shadow: 0 12px 24px -14px
color-mix(in srgb, var(--color-primary) 85%, transparent);
}
.avatar-name {
font-size: 9.5px;
font-weight: 800;
letter-spacing: 0.04em;
color: var(--color-text-muted);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.avatar-tile:hover .avatar-name {
color: var(--color-text-readable);
}
.avatar-online {
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--color-accent);
box-shadow: 0 0 0 2px var(--color-surface);
}
/* ── Public page polish: drifting aurora blobs ────────────────────── */
.aurora-blob {
position: absolute;
border-radius: 999px;
filter: blur(70px);
pointer-events: none;
will-change: transform;
animation: aurora-drift 16s ease-in-out infinite alternate;
}
@keyframes aurora-drift {
from {
transform: translate3d(0, 0, 0) scale(1);
opacity: 0.55;
}
to {
transform: translate3d(30px, -24px, 0) scale(1.15);
opacity: 0.85;
}
}
/* Small bobbing scroll cue at the bottom of the hero. */
.hero-cue {
animation: float 2.4s ease-in-out infinite;
will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
.aurora-blob,
.hero-cue {
animation: none;
}
.btn-brand:hover,
.btn-glass-dark:hover,
.explore-pill:hover,
.avatar-tile:hover {
transform: none;
}
}
/* Hairlines between the headline counters.
The strip is two-up on narrow screens and four-up from `lg`. The old rules
only knew the four-up layout: on mobile they drew a single vertical line
between the two rows instead of between the two columns, and none between
the columns at all. Both rules are drawn from the same token so the grid
always reads as four separate figures. */
.stat-cell {
position: relative;
}
.stat-cell::before,
.stat-cell::after {
content: "";
position: absolute;
background: color-mix(in srgb, var(--color-text-muted) 20%, transparent);
}
@media (min-width: 1024px) {
/* Four across: a vertical rule in front of every cell but the first. */
.stat-cell:not(:first-child)::before {
left: -0.75rem;
top: 8%;
height: 84%;
width: 1px;
}
}
@media (max-width: 1023px) {
/* Two across: a vertical rule between the two columns of each row… */
.stat-cell:nth-child(even)::before {
left: -0.75rem;
top: 8%;
height: 84%;
width: 1px;
}
/* …and a horizontal rule between the two rows. */
.stat-cell:nth-child(n + 3)::after {
left: 6%;
right: 6%;
top: -0.75rem;
height: 1px;
}
}
/* Mouse-tracking spotlight glow for premium cards. The dot position comes
from CSS custom properties updated on pointermove (see SpotlightCard). */
.spotlight {
position: relative;
isolation: isolate;
}
.spotlight::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
opacity: 0;
transition: opacity 0.4s ease;
background: radial-gradient(
340px circle at var(--spot-x, 50%) var(--spot-y, 50%),
color-mix(in srgb, var(--color-primary) 20%, transparent),
transparent 70%
);
z-index: -1;
}
.spotlight:hover::before,
.spotlight:focus-within::before {
opacity: 1;
}
/* Section number mark (01 / 02 / …) used as a faint watermark in headers. */
.number-mark {
font-family: var(--font-nunito);
font-weight: 900;
letter-spacing: -0.02em;
line-height: 1;
background: linear-gradient(
120deg,
color-mix(in srgb, var(--color-primary) 65%, transparent),
color-mix(in srgb, var(--color-accent) 50%, transparent)
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
opacity: 0.85;
}
@media (prefers-reduced-motion: reduce) {
.spotlight::before {
transition: none;
}
.animate-fade-in-up,
.animate-scale-in,
.animate-count {
animation: none;
}
}