@font-face { font-family: "Inter"; src: url("/fonts/Inter-Regular.ttf") format("truetype"); font-weight: 400; }
@font-face { font-family: "Inter"; src: url("/fonts/Inter-Medium.ttf") format("truetype"); font-weight: 500; }
@font-face { font-family: "Inter"; src: url("/fonts/Inter-SemiBold.ttf") format("truetype"); font-weight: 600; }
@font-face { font-family: "Material Symbols Rounded"; src: url("/fonts/MaterialSymbolsRounded.ttf") format("truetype"); }

:root {
	--bg: #090b14;
	--bg-elevated: #0d1020;
	--card: #10132350;
	--card-solid: #11142380;
	--border: #202540;
	--border-soft: #1a1e33;
	--accent: #6d6cf0;
	--accent-hover: #7f7ef5;
	--accent-soft: #6d6cf026;
	--text: #e7e8f0;
	--text-dim: #9296ab;
	--text-faint: #6b6f85;
	--green: #4ade80;
	--radius: 14px;
	--radius-sm: 10px;
}

* { box-sizing: border-box; }

html, body {
	margin: 0;
	padding: 0;
	background: var(--bg);
	color: var(--text);
	font-family: "Inter", -apple-system, "Segoe UI", sans-serif;
	font-size: 14px;
	/* .full-bleed sections (landing page hero) span exactly 100vw via negative
	   margins -- on a page that's otherwise tall enough to have its own
	   vertical scrollbar, 100vw measures WIDER than the visible viewport by
	   the scrollbar's own width, which overflows a few pixels horizontally
	   and shows a second, horizontal scrollbar. Harmless to hide site-wide. */
	overflow-x: hidden;
	/* Explicit, not left to default -- the CSS Overflow spec computes an
	   unspecified overflow-y as auto (not visible) the moment overflow-x is
	   anything but visible, the same trap .nav-inner's own 2026-10-19 fix
	   comment already documents at the mobile breakpoint. Here it bit at
	   desktop width too (reported live, screenshots, 2026-10-20): on a SHORT
	   page (little content, e.g. My Tickets with one ticket), the account
	   dropdown's position:absolute popup overflows below the page's natural
	   content height, and the implicit auto wasn't reliably extending the
	   page's scrollable area to cover it -- the popup's bottom (Log out)
	   read as cut off by the window edge. Explicit visible here keeps the
	   intentional horizontal clip without touching vertical overflow at all.
	*/
	overflow-y: visible;
}

a { color: inherit; text-decoration: none; }
a:hover { color: var(--accent-hover); }

.icon {
	font-family: "Material Symbols Rounded";
	font-weight: normal;
	font-style: normal;
	line-height: 1;
	letter-spacing: normal;
	text-transform: none;
	display: inline-block;
	white-space: nowrap;
	word-wrap: normal;
	direction: ltr;
	font-feature-settings: "liga";
	-webkit-font-feature-settings: "liga";
	-webkit-font-smoothing: antialiased;
	font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24;
	vertical-align: middle;
}
.icon.filled { font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 24; }

/* min-height: a SHORT page (little/no content, e.g. Items/My Tickets with
   nothing in them) used to end right where its own content stopped, with
   the account dropdown's position:absolute popup overflowing past that
   into effectively empty space below the document -- reported live,
   screenshots, 2026-10-20: the popup's bottom items (Log out, etc.) read
   as cut off / visually overlapping the site footer, and in that
   overlapping state were reportedly unclickable too (exact mechanism not
   pinned down, but not worth chasing further once avoiding the situation
   entirely is this simple). Guaranteeing the page is always at least a
   full viewport tall means the dropdown's own max realistic height always
   lands comfortably inside ordinary document flow, nowhere near the
   document's actual end, regardless of how little real content a given
   page has. 73px = .nav's own height (72px) + its 1px border-bottom. */
.page { max-width: 1240px; margin: 0 auto; padding: 0 24px 60px; animation: page-fade-in 0.3s ease; min-height: calc(100vh - 73px); }
@keyframes page-fade-in {
	from { opacity: 0; transform: translateY(8px); }
	to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
	.page { animation: none; }
}

/* ---- top nav ---------------------------------------------------------- */
.nav {
	background: var(--bg-elevated);
	border-bottom: 1px solid var(--border-soft);
	position: sticky;
	top: 0;
	z-index: 20;
}
.nav-inner {
	max-width: 1240px;
	margin: 0 auto;
	padding: 0 24px;
	height: 72px;
	display: flex;
	align-items: center;
	gap: 28px;
}
.logo { display: flex; align-items: center; gap: 12px; color: var(--text); }
.logo .logo-icon { color: var(--accent); flex-shrink: 0; }
.logo-text { display: flex; flex-direction: column; justify-content: center; }
.logo-title { font-weight: 800; font-size: 21px; letter-spacing: 0.4px; line-height: 1; }
.logo-tagline { font-size: 10.5px; font-weight: 500; letter-spacing: 0.2px; color: var(--text-faint); margin-top: 3px; white-space: nowrap; }
.nav-links { display: flex; align-items: center; gap: 24px; flex: 1; }
.nav-link { display: flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 14px; font-weight: 500; padding: 8px 2px; border-bottom: 2px solid transparent; transition: color 0.15s ease, border-color 0.15s ease; }
.nav-link .icon { font-size: 19px; }
.nav-link:hover { color: var(--text); }
.nav-link.active { color: var(--text); border-bottom-color: var(--accent); }
/* Casino nav item (2026-10-19) -- kept visible as a "coming soon" teaser
   instead of cut from the nav entirely, but it must not actually navigate:
   the route only exists in dev (Platega compliance question still pending),
   so a real href would 404 on prod. cursor:default + reduced opacity read
   as "not clickable yet" without removing the item, and the small tag makes
   that explicit instead of relying on a hover-only tooltip. */
.nav-link-soon { opacity: 0.55; cursor: default; }
.nav-link-soon:hover { color: var(--text-dim); }
.nav-link-soon-tag { font-size: 10px; font-weight: 600; color: var(--text-faint); margin-left: 2px; }
.nav-search {
	display: flex; align-items: center; gap: 8px; background: var(--bg);
	border: 1px solid var(--border); border-radius: 10px; padding: 7px 12px; width: 220px;
}
.nav-search input { background: none; border: none; outline: none; color: var(--text); font-size: 13px; width: 100%; font-family: inherit; }
.nav-search .icon { color: var(--text-faint); font-size: 18px; }
.nav-right { display: flex; align-items: center; gap: 16px; }
.nav-bell { color: var(--text-dim); font-size: 22px; position: relative; cursor: pointer; }
.nav-bell .dot { position: absolute; top: -2px; right: -2px; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
.nav-user {
	display: flex; align-items: center; gap: 8px; background: none; border: none; cursor: pointer;
	font-family: inherit; padding: 4px 6px; border-radius: 8px; color: inherit;
}
.nav-user:hover { background: var(--bg); }
.nav-user .name { font-size: 13px; font-weight: 500; }

.profile-menu-wrap { position: relative; }
.profile-menu {
	display: none; position: absolute; top: calc(100% + 10px); right: 0; width: 260px;
	background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius);
	box-shadow: 0 12px 32px rgba(0,0,0,0.45); z-index: 30;
	/* NOT overflow:hidden -- that clipped the status-pill glow (filter:
	   drop-shadow(), see badgeStyle() in view_helpers.ts) on the Custom
	   badge in .profile-menu-header below, the same bug profile.ejs's outer
	   .card had (see .profile-banner's own comment). The menu's own rounded
	   corners are clipped per-item below instead, only where something
	   actually paints a background near them. */
}
.profile-menu.open { display: block; }
.profile-menu-header { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border-soft); border-radius: var(--radius) var(--radius) 0 0; }
.profile-menu-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.profile-menu-item {
	display: flex; align-items: center; gap: 10px; padding: 11px 16px; font-size: 13px; color: var(--text);
}
.profile-menu-item:hover { background: var(--bg); }
.profile-menu > form:last-child .profile-menu-item:hover, .profile-menu > .profile-menu-item:last-child:hover { border-radius: 0 0 var(--radius) var(--radius); }
.profile-menu-item .icon { font-size: 18px; color: var(--text-dim); }
/* ---- status colors (view_helpers.ts's userBadges/userNameClass) -----------
   Member=gray, Premium=pink, HourBoost=green, Support=dark red,
   Admin=light red, Media=white, Reseller=white -- exact colors requested. */
:root {
	--status-member: #9296ab;
	--status-premium: #ec4899;
	--status-hourboost: #22c55e;
	--status-support: #7f1d2d;
	--status-designer: #8b5cf6;
	--status-admin: #f0556c;
	--status-media: #ffffff;
	--status-reseller: #ffffff;
}

/* Diagonal gradient + glossy top highlight + clipped corners -- a step up
   from a flat color pill (too plain) without going as far as the fully
   illustrated emblem badges in the reference (no per-status artwork to
   maintain) -- the "something in between" the user asked for. The clipped
   top-left/bottom-right corners are the nod to the angular "wing" reference;
   the inset highlight is what gives it the slight glossy/metallic feel.
   The gradient/clip-path/inset-shadow live on ::before now, NOT on
   .status-pill itself -- clip-path clips EVERYTHING an element paints,
   filter (the glow, below) included, not just box-shadow. Moving the
   clipped shape one level down (::before, z-index -1) means .status-pill's
   own filter: drop-shadow() is computed from an element that has no
   clip-path of its own, so it isn't clipped away. Confirmed live 2026-10-17
   -- the glow was completely invisible (not just subtle) with clip-path and
   filter on the same box, in every browser, regardless of cache state. */
.status-pill {
	position: relative; z-index: 0; display: inline-flex; align-items: center; gap: 5px;
	font-size: 12px; font-weight: 700; padding: 6px 14px; white-space: nowrap;
	text-shadow: 0 1px 2px rgba(0,0,0,0.35);
}
.status-pill::before {
	content: ""; position: absolute; inset: 0; z-index: -1;
	clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
}
.status-pill-icon { font-size: 1.15em; }

.status-pill.member {
	color: var(--status-member); border: 1px solid var(--border); font-weight: 600; text-shadow: none;
}
.status-pill.member::before { clip-path: none; }
.status-pill.premium::before   { background: linear-gradient(135deg, #ff9ed4, var(--status-premium) 55%, #c2277e); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35), 0 3px 10px -2px #ec489966; }
.status-pill.premium   { color: #fff; filter: drop-shadow(0 0 5px #ec4899f0) drop-shadow(0 0 16px #ec4899b3); }
.status-pill.hourboost::before { background: linear-gradient(135deg, #86f0ae, var(--status-hourboost) 55%, #16893f); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35), 0 3px 10px -2px #22c55e66; }
.status-pill.hourboost { color: #06240f; filter: drop-shadow(0 0 5px #22c55ef0) drop-shadow(0 0 16px #22c55eb3); }
.status-pill.support::before   { background: linear-gradient(135deg, #b84a5e, var(--status-support) 55%, #4e1420); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35), 0 3px 10px -2px #7f1d2d66; }
.status-pill.support   { color: #fff; filter: drop-shadow(0 0 5px #7f1d2df0) drop-shadow(0 0 16px #7f1d2db3); }
.status-pill.designer::before  { background: linear-gradient(135deg, #c4b5fd, var(--status-designer) 55%, #5b21b6); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35), 0 3px 10px -2px #8b5cf666; }
.status-pill.designer  { color: #fff; filter: drop-shadow(0 0 5px #8b5cf6f0) drop-shadow(0 0 16px #8b5cf6b3); }
.status-pill.admin::before     { background: linear-gradient(135deg, #ff9b7a, var(--status-admin) 55%, #c22f45); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35), 0 3px 10px -2px #f0556c66; }
.status-pill.admin     { color: #fff; filter: drop-shadow(0 0 5px #f0556cf0) drop-shadow(0 0 16px #f0556cb3); }
.status-pill.media::before     { background: linear-gradient(135deg, #ffffff, #d7dbe8 55%, #9ca3bf); box-shadow: inset 0 1px 0 rgba(255,255,255,0.6), inset 0 -8px 10px -6px rgba(0,0,0,0.2), 0 3px 10px -2px #ffffff40; }
.status-pill.media     { color: #1a1e33; filter: drop-shadow(0 0 5px #fffffff0) drop-shadow(0 0 16px #ffffffb3); }
.status-pill.reseller::before  { background: linear-gradient(135deg, #ffffff, #bcd7f2 55%, #5ec8ff); box-shadow: inset 0 1px 0 rgba(255,255,255,0.6), inset 0 -8px 10px -6px rgba(0,0,0,0.2), 0 3px 10px -2px #5ec8ff55; }
.status-pill.reseller  { color: #0d2436; filter: drop-shadow(0 0 5px #5ec8fff0) drop-shadow(0 0 16px #5ec8ffb3); }

/* Custom badge (Zyriks shop, view_helpers.ts's badgeStyle()) -- the gradient
   is per-account, so it can't be a fixed CSS rule like the ones above. Can't
   put inline style on ::before directly (pseudo-elements don't take one), so
   badgeStyle() sets these as CSS custom properties on the real element
   instead -- custom properties inherit into ::before same as into any real
   child, inline style can't set a property with a color+alpha suffix like
   `var(--x)dd` (not valid CSS token concatenation), so badgeStyle() resolves
   the three full gradient-stop colors (and the glow) itself and hands them
   over as already-complete values. */
.status-pill.custom::before { background: linear-gradient(135deg, var(--custom-a, #8b5cf6), var(--custom-b, #8b5cf6) 55%, var(--custom-c, #8b5cf6)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35); }
.status-pill.custom { color: #fff; filter: var(--custom-glow, none); }

/* ---- Title tiers (Echo/Pulse/Prism, zyriks.ts's TIER_INFO) -----------
   Echo is just the plain Custom badge above, no animation. Pulse is a
   simple opacity fade -- deliberately NOT tied to the chosen color (works
   the same regardless of which palette color is equipped). Prism replaces
   the chosen color entirely with a shifting rainbow gradient -- the whole
   point of the top tier being "full custom + the fanciest effect", per the
   2026-10-17 design: "переливание как у призмы". Same two-class split as
   the rest of this badge system: .tier-pulse/.tier-prism for the pill
   (badgeAnimationClass()), .name-tier-pulse/.name-tier-prism for the bare
   nickname (nameAnimationClass()/rowNameAnimationClass()). */
@keyframes zyriks-pulse-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.status-pill.tier-pulse, .name-tier-pulse { animation: zyriks-pulse-fade 2.2s ease-in-out infinite; }

@keyframes zyriks-prism-shift { 0% { background-position: 0% 50%; } 100% { background-position: 300% 50%; } }
/* Prism's glow -- every other badge (Admin/Support/Designer/Reseller/Media,
   plus Gold/Emerald Custom) gets a colored drop-shadow halo; Prism had been
   left with only a 1px legibility shadow (no real glow at all) since its
   fixed badgeStyle() filter was written before this was noticed live
   (2026-10-17). Cycles the glow color through the same rainbow stops as the
   fill so the halo visibly shifts in sync with the gradient, instead of
   sitting there as one flat color. */
@keyframes zyriks-prism-glow {
	0%, 100% { filter: drop-shadow(0 0 5px #ff6b6bf0) drop-shadow(0 0 16px #ff6b6bb3); }
	20% { filter: drop-shadow(0 0 5px #feca57f0) drop-shadow(0 0 16px #feca57b3); }
	40% { filter: drop-shadow(0 0 5px #1dd1a1f0) drop-shadow(0 0 16px #1dd1a1b3); }
	60% { filter: drop-shadow(0 0 5px #54a0fef0) drop-shadow(0 0 16px #54a0feb3); }
	80% { filter: drop-shadow(0 0 5px #c56cf0f0) drop-shadow(0 0 16px #c56cf0b3); }
}
.status-pill.tier-prism { animation: zyriks-prism-glow 3.5s linear infinite; }
/* Higher specificity than .status-pill.custom::before above so Prism's
   rainbow wins over the per-account --custom-* gradient entirely. */
.status-pill.custom.tier-prism::before {
	background: linear-gradient(90deg, #ff6b6b, #feca57, #1dd1a1, #54a0ff, #c56cf0, #ff6b6b);
	background-size: 300% 100%;
	animation: zyriks-prism-shift 3.5s linear infinite;
	box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), inset 0 -8px 10px -6px rgba(0,0,0,0.35);
}
/* !important on color here: a two-class context selector like
   .list-item .lm a { color: var(--text-dim); } (style.css, Latest Posts/
   Latest Profile Posts meta lines) outranks this single-class selector on
   plain specificity regardless of source order, so without it the dim-gray
   text color wins over `transparent` -- the background-clip:text gradient
   needs a truly transparent fill to show through at all, so losing that
   fight doesn't just dim the color, it hides the gradient outright and
   leaves flat dim-gray text (confirmed live 2026-10-19: reported as the
   nickname color looking "pale" in Latest Posts). Echo/Pulse don't need
   this -- their color comes from nameStyle()'s INLINE style, which already
   beats any class selector on its own. */
.name-tier-prism {
	background: linear-gradient(90deg, #ff6b6b, #feca57, #1dd1a1, #54a0ff, #c56cf0, #ff6b6b);
	background-size: 300% 100%; -webkit-background-clip: text; background-clip: text; color: transparent !important;
	animation: zyriks-prism-shift 3.5s linear infinite, zyriks-prism-glow 3.5s linear infinite;
}
/* The big banner name (.profile-identity-text .profile-name) needs its own
   override -- it normally paints a dark text-shadow outline for legibility
   over a photo, which would hide a transparent/background-clip:text name
   entirely. filter:drop-shadow still reads the text's real alpha shape
   (the rainbow fill) the same way it does for the role badges elsewhere in
   this file, so the outline survives switching from text-shadow to filter. */
/* Same color-cycling glow as zyriks-prism-glow above, with the banner's dark
   legibility outline layered into every stop (a plain `animation:` override
   here would otherwise replace the outline's static filter each frame and
   lose it, since an animated property always wins over a static one). */
@keyframes zyriks-prism-glow-banner {
	0%, 100% { filter: drop-shadow(0 1px 1px rgba(0,0,0,0.9)) drop-shadow(0 2px 4px rgba(0,0,0,0.6)) drop-shadow(0 0 16px #ff6b6bb3); }
	20% { filter: drop-shadow(0 1px 1px rgba(0,0,0,0.9)) drop-shadow(0 2px 4px rgba(0,0,0,0.6)) drop-shadow(0 0 16px #feca57b3); }
	40% { filter: drop-shadow(0 1px 1px rgba(0,0,0,0.9)) drop-shadow(0 2px 4px rgba(0,0,0,0.6)) drop-shadow(0 0 16px #1dd1a1b3); }
	60% { filter: drop-shadow(0 1px 1px rgba(0,0,0,0.9)) drop-shadow(0 2px 4px rgba(0,0,0,0.6)) drop-shadow(0 0 16px #54a0feb3); }
	80% { filter: drop-shadow(0 1px 1px rgba(0,0,0,0.9)) drop-shadow(0 2px 4px rgba(0,0,0,0.6)) drop-shadow(0 0 16px #c56cf0b3); }
}
.profile-identity-text .profile-name.name-tier-prism {
	text-shadow: none;
	animation: zyriks-prism-shift 3.5s linear infinite, zyriks-prism-glow-banner 3.5s linear infinite;
}
.profile-identity-text .profile-name.name-tier-pulse { animation: zyriks-pulse-fade 2.2s ease-in-out infinite; }

/* ---- Title tier purchase cards (zyriks.ejs's Title Tier card) ----------
   Membership-card layout per the user's Fortune/Solar/Void/Apollo
   reference -- replaced the earlier compact .plan-option row list, which
   the user called out as not matching that reference at all. */
.tier-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 640px) { .tier-grid { grid-template-columns: 1fr; } }
.tier-card {
	position: relative; display: flex; flex-direction: column; align-items: center; text-align: center;
	gap: 10px; padding: 24px 18px; border-radius: var(--radius); border: 1px solid var(--border);
	background: linear-gradient(180deg, var(--card-solid), var(--bg-elevated));
}
.tier-card.current { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 0 24px -8px var(--accent-soft); }
.tier-card-icon {
	width: 56px; height: 56px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
	font-size: 28px; background: var(--accent-soft); color: var(--accent); margin-bottom: 4px;
}
.tier-card.tier-echo .tier-card-icon { background: #ffffff1a; color: #e7e8f0; }
.tier-card.tier-pulse .tier-card-icon { background: #ff6ec71a; color: #ff6ec7; }
.tier-card.tier-prism .tier-card-icon {
	background: linear-gradient(90deg, #ff6b6b, #feca57, #1dd1a1, #54a0ff, #c56cf0, #ff6b6b);
	background-size: 300% 100%; animation: zyriks-prism-shift 3.5s linear infinite; color: #fff;
}
.tier-card-name { font-size: 18px; font-weight: 700; }
/* Fixed height (not min-height) + flex-centered text (2026-10-19) --
   descriptions got longer when the Daily Bonus perk text was added
   (Pulse/Prism wrap to 3 lines, Echo's stays 2), and a plain min-height
   wasn't reliably enough to keep the price/button row below it at the SAME
   y across all three cards -- an exact height, with the (possibly shorter)
   text centered inside it via flex, pins that box's bottom edge identically
   regardless of each card's actual text length. */
.tier-card-desc {
	font-size: 13px; color: var(--text-dim); line-height: 1.4; height: 56px;
	display: flex; align-items: center; justify-content: center;
}
.tier-card-price { font-size: 22px; font-weight: 700; color: var(--accent); margin-top: 4px; }
.tier-card-price span { font-size: 12.5px; font-weight: 500; color: var(--text-dim); }
.tier-card-btn { width: 100%; justify-content: center; margin-top: 6px; }
.tier-card-badge {
	position: absolute; top: 12px; right: 12px; font-size: 11px; font-weight: 600; color: var(--accent);
	background: var(--accent-soft); padding: 3px 9px; border-radius: 999px;
}

/* A nickname colored by its single highest-priority status, no pill --
   used in the nav, post authors, shoutbox, sidebar lists. Flat color, no
   animation -- an animated gradient sheen + sparkles were tried and
   explicitly removed again (looked "off"/crooked at small inline sizes,
   e.g. wall posts) in favor of this simpler, calmer look. */
.name-member    { color: var(--status-member); }
.name-premium   { color: var(--status-premium); }
/* Glow on the nickname text itself, same roster as the badge pills (every
   role except Premium/Member, 2026-10-17 request) -- plain text-shadow is
   enough here (no clip-path on a bare nickname the way .status-pill has, so
   none of the drop-shadow workaround above is needed). Two layers (tight+
   brighter, wide+softer) read as a smooth glow falloff -- a single hard-
   edged blur ring looked too blocky (2026-10-17 follow-up). */
/* Smaller radius than the big banner name below -- this class renders at
   many small sizes all over the forum (nav, post authors, shoutbox, list
   rows), and the banner's wider blur looked smeared/illegible at 11-13px
   (confirmed live 2026-10-17: "Latest posts" vs. "Forum Categories" showed
   the same name at two different sizes, and only the smaller one read
   badly). The big banner name has its OWN separate rule below
   (.profile-identity-text .profile-name.name-*) and keeps the wider glow. */
.name-hourboost { color: var(--status-hourboost); text-shadow: 0 0 2px #22c55ef0, 0 0 6px #22c55eb3; }
.name-support   { color: var(--status-support); text-shadow: 0 0 2px #7f1d2df0, 0 0 6px #7f1d2db3; }
.name-designer  { color: var(--status-designer); text-shadow: 0 0 2px #8b5cf6f0, 0 0 6px #8b5cf6b3; }
.name-admin     { color: var(--status-admin); text-shadow: 0 0 2px #f0556cf0, 0 0 6px #f0556cb3; }
.name-media     { color: var(--status-media); text-shadow: 0 0 2px #fffffff0, 0 0 6px #ffffffb3; }
.name-reseller  { color: var(--status-reseller); text-shadow: 0 0 2px #5ec8fff0, 0 0 6px #5ec8ffb3; }
.btn-ghost {
	background: transparent; color: var(--text); border: 1px solid var(--border);
	padding: 6px 14px; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; font-family: inherit;
	white-space: nowrap;
}

/* ---- avatars ----------------------------------------------------------- */
.avatar {
	/* inline-flex, not flex -- flex makes this a block-level box, which
	   text-align:center on an ancestor has NO effect on (that only centers
	   inline-level content). Caused the avatar to sit flush left wherever
	   it was meant to be centered via text-align (e.g. .post-author) while
	   still letting the fallback initial letter center itself internally. */
	border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
	color: #fff; font-weight: 600; flex-shrink: 0;
}
.avatar-28 { width: 28px; height: 28px; font-size: 12px; }
.avatar-32 { width: 32px; height: 32px; font-size: 13px; }
.avatar-40 { width: 40px; height: 40px; font-size: 15px; }
.avatar-72 { width: 72px; height: 72px; font-size: 24px; }

/* Avatar frame ring (2026-10-19, Zyriks shop) -- padding trick, see
   partials/avatar.ejs's own comment: the outer circle paints the frame's
   `background` (solid color or gradient, both work identically here), the
   real avatar sits centered inside with a gap equal to its padding. */
.avatar-frame-ring {
	display: inline-flex; align-items: center; justify-content: center;
	border-radius: 50%; flex-shrink: 0; box-sizing: content-box; position: relative;
}
/* Premium frames (300+ Zyriks -- Fire/Aurora/White) spin (2026-10-19,
   follow-up fix -- an earlier version shifted a LINEAR gradient's
   background-position, which didn't read as "going around the ring"; a
   rotating CONIC gradient does). The rotation is on a ::before, NOT the
   ring element itself -- .avatar-frame-ring is the containing block for
   the real avatar <img>/<span> too (position:relative above), so if the
   ring itself rotated, the avatar photo inside would spin along with it.
   --frame-bg (set inline per avatar, see partials/avatar.ejs) lets this
   pseudo-element paint the SAME gradient without repeating the value. */
.avatar-frame-ring.is-animated::before {
	content: ""; position: absolute; inset: 0; border-radius: 50%;
	background: var(--frame-bg); animation: avatar-frame-spin 2.5s linear infinite;
}
/* The ::before above is position:absolute with z-index:auto, which paints
   ABOVE a normal in-flow inline-level child in default stacking order --
   without lifting the real avatar onto its own stacking position here, the
   spinning gradient would cover it entirely instead of just ringing it. */
.avatar-frame-ring > .avatar { position: relative; z-index: 1; }
@keyframes avatar-frame-spin { to { transform: rotate(360deg); } }
.avatar-110 { width: 110px; height: 110px; font-size: 34px; }

/* ---- generic card -------------------------------------------------------*/
.card {
	background: var(--card-solid);
	border: 1px solid var(--border-soft);
	border-radius: var(--radius);
}
.card-header {
	display: flex; align-items: center; justify-content: space-between;
	padding: 16px 20px; border-bottom: 1px solid var(--border-soft);
}
.card-header .title { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; }
.card-header .title .icon { color: var(--accent); font-size: 20px; }
.view-all { font-size: 12.5px; color: var(--text-dim); display: flex; align-items: center; gap: 4px; }

/* ---- page header --------------------------------------------------------*/
.page-header { display: flex; align-items: flex-end; justify-content: space-between; margin: 28px 0 20px; }
.page-header h1 { margin: 0 0 4px; font-size: 26px; }
.page-header p { margin: 0; color: var(--text-dim); font-size: 14px; }
.btn {
	display: inline-flex; align-items: center; gap: 6px; font-family: inherit;
	font-size: 13.5px; font-weight: 600; padding: 9px 16px; border-radius: 9px;
	border: 1px solid var(--border); background: var(--bg-elevated); color: var(--text); cursor: pointer;
}
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-hover); color: #fff; }
.btn:hover { border-color: var(--accent); }
.btn:disabled, .btn.primary:disabled {
	background: var(--bg-elevated); border-color: var(--border); color: var(--text-faint);
	cursor: not-allowed;
}
.btn:disabled:hover, .btn.primary:disabled:hover { background: var(--bg-elevated); border-color: var(--border); }

/* ---- layout grid ---------------------------------------------------------*/
/* Only used on the home page. Each column is ONE grid cell (a flex-column
   stack of its cards), so the FIRST card in each column lines up at the
   same Y for free -- that's the pairing that's actually visible at a
   glance (Shoutbox next to Latest posts). An earlier version gave every
   card below that its own explicit grid-row so row 2 also lined up
   (Forum Categories / Latest profile posts) -- technically exact, but
   whichever side was shorter then had empty space forced into it to reach
   the row's height, which read as an even worse "unevenness" than the
   thing it fixed. Letting everything after the first card flow freely is
   the better trade-off: one guaranteed alignment, zero dead gaps. */
.layout { display: grid; grid-template-columns: 1fr 340px; gap: 20px; align-items: start; }
.cell-left  { grid-column: 1; grid-row: 1; }
.cell-right { grid-column: 2; grid-row: 1; }
.cell-stack { display: flex; flex-direction: column; gap: 20px; }

/* ---- shoutbox ------------------------------------------------------------*/
.shout-online { display: flex; align-items: center; gap: 6px; color: var(--text-dim); font-size: 12.5px; }
.shout-online .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); }
.shout-body {
	max-height: 260px; overflow-y: auto; padding: 14px 20px; display: flex; flex-direction: column; gap: 14px;
	scrollbar-width: thin; scrollbar-color: var(--border) transparent; /* Firefox */
}
.shout-body::-webkit-scrollbar { width: 8px; }
.shout-body::-webkit-scrollbar-track { background: transparent; }
.shout-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.shout-body::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
.shout-msg { display: flex; gap: 10px; align-items: flex-start; }
.shout-msg .shout-msg-body { flex: 1; min-width: 0; }
.shout-msg .meta { display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px; }
.shout-msg .author { font-weight: 600; font-size: 13px; }
.shout-msg .time { font-size: 11.5px; color: var(--text-faint); }
.shout-msg .text { font-size: 13.5px; color: var(--text); line-height: 1.4; }
.shout-mod-actions { display: flex; gap: 4px; flex-shrink: 0; opacity: 0; transition: opacity 0.12s; }
.shout-msg:hover .shout-mod-actions { opacity: 1; }
.shout-delete, .shout-mute { background: none; border: none; color: var(--text-faint); cursor: pointer; padding: 2px; }
.shout-delete:hover { color: #f0556c; }
.shout-mute:hover { color: var(--accent); }
.shout-form input:disabled { opacity: 0.6; cursor: not-allowed; }

#mute-dialog {
	background: var(--card-solid); border: 1px solid var(--border); border-radius: var(--radius);
	padding: 20px; color: var(--text); width: 320px; max-width: 90vw;
}
#mute-dialog::backdrop { background: rgba(0,0,0,0.5); }
#mute-dialog h3 { margin: 0 0 14px; font-size: 15px; }
#mute-form { display: flex; flex-direction: column; gap: 10px; }
#mute-form label { font-size: 12px; color: var(--text-dim); margin-bottom: -4px; }
#mute-form select, #mute-form input {
	background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
	padding: 8px 10px; color: var(--text); font-family: inherit; font-size: 13px; outline: none;
}
.mute-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
/* Matches the shoutbox card's total height (.shout-body's 260px + the
   .shout-form row below it, ~66px) so "Latest posts" and "Community
   Shoutbox" bottom out at the same y -- keeps Forum Categories and Latest
   profile posts, the next row down in each column, starting flush too. */
.latest-posts-body {
	max-height: 326px; overflow-y: auto;
	scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.latest-posts-body::-webkit-scrollbar { width: 8px; }
.latest-posts-body::-webkit-scrollbar-track { background: transparent; }
.latest-posts-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.latest-posts-body::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }

.shout-form { display: flex; gap: 10px; padding: 14px 20px; border-top: 1px solid var(--border-soft); }
.shout-form input {
	flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 10px;
	padding: 10px 14px; color: var(--text); font-family: inherit; font-size: 13.5px; outline: none;
}
.shout-form input:focus { border-color: var(--accent); }

/* ---- sticker/emoji picker (shoutbox) ---------------------------------*/
.sticker-picker-wrap { position: relative; }
.sticker-picker {
	display: none; position: absolute; bottom: calc(100% + 8px); right: 0; width: 260px;
	background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius);
	box-shadow: 0 12px 32px rgba(0,0,0,0.45); padding: 10px; z-index: 30;
	grid-template-columns: repeat(5, 1fr); gap: 4px;
}
.sticker-picker.open { display: grid; }
.sticker-option {
	display: flex; align-items: center; justify-content: center; width: 44px; height: 44px;
	border-radius: 8px; border: none; background: transparent; cursor: pointer;
}
.sticker-option:hover { background: var(--bg); }
.sticker-option img { width: 28px; height: 28px; }
.icon-btn {
	width: 38px; height: 38px; border-radius: 10px; border: 1px solid var(--border); background: var(--bg);
	color: var(--text-dim); display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.icon-btn.send { background: var(--accent); border-color: var(--accent); color: #fff; }

/* ---- category table -------------------------------------------------------*/
.cat-row {
	/* Each row is its OWN grid (not one shared grid across the whole card),
	   so a flexible last column (used to be minmax(140px, auto)) resolves
	   to a DIFFERENT width per row depending on that row's own content --
	   the row with a real last post (avatar + text) needs much more space
	   there than a bare "No posts yet" row, which visibly shifted every
	   other column (Threads/Messages/name) out of alignment between rows.
	   A fixed width keeps every row's grid identical regardless of content. */
	display: grid; grid-template-columns: auto 1fr 90px 90px 220px; gap: 16px; align-items: center;
	padding: 18px 20px; border-bottom: 1px solid var(--border-soft);
	/* Without this, a row with a real last post (avatar + two text lines)
	   is visibly taller than a "No posts yet" row (one line) right above
	   or below it -- every row now reserves that same height whether or
	   not it actually needs it, so the list reads as one even table
	   instead of a jagged one where only some rows are "full size". */
	min-height: 82px;
}
.cat-row:last-child { border-bottom: none; }
.cat-row .icon-wrap {
	width: 42px; height: 42px; background: none; color: var(--text-dim);
	display: flex; align-items: center; justify-content: center;
}
.cat-row .icon-wrap .icon { font-size: 24px; }
.cat-row .cat-name { font-weight: 600; font-size: 15px; }
/* .stat is shared with .topic-row (category.ejs's reply-count column) --
   was .cat-row-scoped only, so topic-row's own "0 Replies" rendered
   completely unstyled (left-aligned, no label styling) until this moved
   out to a base rule both rows can use. */
.stat { text-align: center; }
.stat .n { font-weight: 600; font-size: 15px; }
.stat .label { color: var(--text-faint); font-size: 12px; margin-bottom: 2px; }
.cat-row .last-post { display: flex; align-items: center; justify-content: flex-start; gap: 10px; text-align: left; }
.cat-row .last-post .lp-title { font-size: 13.5px; color: var(--text); font-weight: 600; max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cat-row .last-post .lp-title a { display: block; overflow: hidden; text-overflow: ellipsis; }
.cat-row .last-post .lp-meta { font-size: 12px; color: var(--text-faint); }

/* ---- sidebar lists ---------------------------------------------------------*/
.list-item { display: flex; align-items: center; gap: 10px; padding: 12px 20px; border-bottom: 1px solid var(--border-soft); }
.list-item:last-child { border-bottom: none; }
.list-item .lt { font-size: 13px; font-weight: 500; color: var(--text); margin-bottom: 2px; display: block; }
.list-item .lm { font-size: 11.5px; color: var(--text-faint); }
/* :not([class*="name-"]) -- a badge-colored author link (rowNameClass()
   always adds a name-admin/name-member/etc class, see view_helpers.ts)
   needs to be EXCLUDED from this dim-gray rule entirely, not merely
   outrun by it: .list-item .lm a (two classes + tag) beats any single
   .name-* class on plain specificity regardless of source order, which is
   exactly what made every role/color here (Admin, Premium, bought name
   colors, Prism's rainbow -- confirmed live 2026-10-19) render pale/dim in
   Latest Posts and Latest Profile Posts instead of its real color. The
   plain, un-badged category link on the line below (e.g. "Announcements")
   has no name-* class and still gets the dim color as intended. */
.list-item .lm a:not([class*="name-"]) { color: var(--text-dim); }

/* Profile wall / "Latest profile posts" -- unlike .lt/.lm above (title +
   dim meta line, correct for category/topic previews), here the SECOND
   line is the actual message body, not meta -- so it's the one that should
   read as normal (white, larger) text, with the date/time dim instead. */
.wall-post-meta { font-size: 13px; font-weight: 500; color: var(--text); margin-bottom: 4px; display: block; }
.wall-post-meta a { font-size: 15px; font-weight: 700; }
.wall-post-time { font-size: 12px; color: var(--text-faint); font-weight: 400; }
.wall-post-body { font-size: 14.5px; color: var(--text); line-height: 1.5; }

/* ---- stats ---------------------------------------------------------*/
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; padding: 18px 20px; }
.stats-grid .stat-n { font-size: 20px; font-weight: 700; }
.stats-grid .stat-l { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }

/* ---- members online --------------------------------------------------*/
/* Reuses .list-item (same row style as "Latest posts"/"Latest profile
   posts" above it) instead of the old flex-wrap chip list -- consistent
   with the rest of the sidebar instead of a bespoke, unfinished-looking
   layout, and each row is now a real link straight to that member's profile. */
.online-row:hover { background: var(--bg); }
.online-total { padding: 10px 20px; font-size: 11.5px; color: var(--text-faint); border-top: 1px solid var(--border-soft); }

/* ---- community member directory (homepage, under Forum Categories) -----*/
.member-directory { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; padding: 10px; }
.member-card {
	display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px;
	color: inherit; text-decoration: none;
}
.member-card:hover { background: var(--bg); }
.member-card-text { min-width: 0; }
.member-card-name { font-size: 13.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.member-card-role { display: flex; align-items: center; gap: 4px; font-size: 11.5px; color: var(--text-faint); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.member-card-role .icon { color: var(--text-faint); }
@media (max-width: 900px) {
	.member-directory { grid-template-columns: repeat(2, 1fr); }
}

/* ---- topics / posts ------------------------------------------------------*/
.topic-row { display: grid; grid-template-columns: 1fr 90px 160px auto; gap: 16px; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border-soft); }
.topic-row:last-child { border-bottom: none; }
.topic-row .t-title { font-weight: 600; font-size: 14px; }
.topic-row .t-meta { font-size: 12px; color: var(--text-faint); margin-top: 3px; }
.topic-row .t-last-reply { font-size: 12px; color: var(--text-faint); line-height: 1.5; text-align: right; }
.mod-actions { display: flex; gap: 6px; }
.mod-actions form { margin: 0; }
.mod-actions .icon-btn { width: 32px; height: 32px; }
.pin { color: var(--accent); font-size: 16px; margin-right: 4px; }

.post {
	display: grid; grid-template-columns: 140px 1fr; gap: 20px; padding: 20px; border-bottom: 1px solid var(--border-soft);
}
.post:last-child { border-bottom: none; }
.post-author { display: flex; flex-direction: column; align-items: center; text-align: center; }
.post-author .name { font-weight: 600; font-size: 13.5px; margin-top: 8px; }
.post-author-badges { display: flex; flex-direction: column; align-items: center; gap: 4px; margin-top: 6px; }
.post-body { font-size: 14px; line-height: 1.6; }
.post-body p { margin: 0 0 10px; }
.post-body p:last-child { margin-bottom: 0; }
.post-body code { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; font-size: 0.9em; font-family: "Consolas", "Monaco", monospace; }
.post-body pre { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; overflow-x: auto; margin: 10px 0; white-space: pre-wrap; }
.post-body pre code { background: none; border: none; padding: 0; white-space: pre-wrap; }
.post-body strong { font-weight: 700; }
.post-body em { font-style: italic; }
.post-body u { text-decoration: underline; }
.post-body s { text-decoration: line-through; }
.post-body h1, .post-body h2, .post-body h3 { margin: 14px 0 8px; line-height: 1.3; }
.post-body h1 { font-size: 1.5em; }
.post-body h2 { font-size: 1.3em; }
.post-body h3 { font-size: 1.1em; }
.post-body ul, .post-body ol { margin: 8px 0; padding-left: 24px; }
.post-body li { margin: 2px 0; }
.post-body blockquote {
	margin: 10px 0; padding: 4px 14px; border-left: 3px solid var(--accent);
	color: var(--text-dim); font-style: italic;
}
.post-body a { color: var(--accent); word-break: break-word; }
.post-body a:hover { text-decoration: underline; }
.post-body img { max-width: 100%; border-radius: 8px; margin: 6px 0; }
/* Quill's alignment format is class-based (ql-align-*), not inline style --
   kept as-is through sanitize-html (src/sanitize.ts allows "class" on <p>). */
.post-body .ql-align-center { text-align: center; }
.post-body .ql-align-right { text-align: right; }
.post-body .ql-align-justify { text-align: justify; }
.post-time { font-size: 11.5px; color: var(--text-faint); }
.post-time-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }

.post-attachments { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.post-attachment {
	display: inline-flex; align-items: center; gap: 6px; width: fit-content;
	background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
	padding: 6px 12px; font-size: 12.5px; color: var(--text); text-decoration: none;
}
.post-attachment:hover { border-color: var(--accent); }
.post-attachment .icon { font-size: 15px; }
.post-attachment-size { color: var(--text-faint); }
.post-attachment-image { display: block; width: fit-content; margin-top: 8px; }
.post-attachment-image img { max-width: 320px; max-height: 320px; border-radius: 10px; border: 1px solid var(--border); display: block; }
.ticket-attach-btn { cursor: pointer; font-size: 12.5px; padding: 6px 12px; }
.ticket-attach-filename { font-size: 12px; color: var(--text-faint); }
.reply-form-footer { display: flex; align-items: center; gap: 10px; }

/* ---- skin editor (visual skin picker, /skins/editor) --------------------*/
.skin-editor-toolbar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 18px 20px; border-bottom: 1px solid var(--border); }
.skin-editor-spacer { flex: 1; }
.skin-side-toggle { display: flex; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.skin-side-btn { background: var(--bg); border: none; color: var(--text-dim); padding: 8px 20px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; }
.skin-side-btn.active { background: var(--accent); color: #fff; }
.skin-sync-toggle { display: flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-dim); cursor: pointer; }
#skin-config-name {
	box-sizing: border-box; appearance: none; margin: 0; width: 220px; height: 40px; line-height: 38px;
	background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 0 14px;
	color: var(--text); font-family: inherit; font-size: 13.5px; outline: none;
}
#skin-editor-error, #skin-editor-success { margin: 16px 20px 0; }
#skin-breadcrumb { display: flex; align-items: center; gap: 12px; padding: 16px 20px 0; }
#skin-breadcrumb-title { font-weight: 600; font-size: 14.5px; }
.skin-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; padding: 20px; }
.skin-card {
	position: relative; cursor: pointer; border-radius: 10px; background: var(--bg);
	border: 1px solid var(--border); overflow: hidden; transition: border-color 0.12s;
}
.skin-card:hover { border-color: var(--accent); }
.skin-card.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.skin-card-bar { position: absolute; top: 0; left: 0; bottom: 38px; width: 3px; }
.skin-card-img { width: 100%; aspect-ratio: 2 / 1; object-fit: contain; background: #0000; display: block; padding: 6px 6px 0; box-sizing: border-box; }
.skin-card-label { padding: 6px 10px 9px; }
.skin-card-title { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.skin-card-sub { font-size: 11px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.skin-card-clear {
	position: absolute; top: 6px; right: 6px; background: rgba(0,0,0,0.55); border: none; border-radius: 50%;
	width: 22px; height: 22px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #fff;
	opacity: 0; transition: opacity 0.12s;
}
.skin-card-clear .icon { font-size: 14px; }
.skin-card:hover .skin-card-clear { opacity: 1; }
.reply-closed { display: flex; align-items: center; gap: 8px; color: var(--text-faint); font-size: 13.5px; justify-content: center; }
.reply-closed .icon { font-size: 16px; }

.post-content { display: flex; flex-direction: column; }
.post-reactions { display: flex; justify-content: flex-end; gap: 6px; margin-top: auto; padding-top: 10px; }
.reaction-btn {
	display: inline-flex; align-items: center; gap: 4px; background: var(--bg);
	border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px;
	font-size: 13px; cursor: pointer; color: var(--text); line-height: 1.4;
}
.reaction-btn:hover { border-color: var(--accent); }
.reaction-btn.active { border-color: var(--accent); background: rgba(var(--accent-rgb, 124, 58, 237), 0.15); }
.reaction-btn:disabled { opacity: 0.45; cursor: default; }
.reaction-count { font-size: 11.5px; color: var(--text-dim); }

.attachment-input {
	display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-dim); cursor: pointer;
}
.attachment-input .icon { font-size: 16px; }
.attachment-input input[type=file] { font-size: 12px; color: var(--text-dim); }
.attachment-input input[type=file]::file-selector-button {
	background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
	color: var(--text-dim); font-family: inherit; font-size: 12px; padding: 5px 10px;
	margin-right: 8px; cursor: pointer;
}
.attachment-input input[type=file]::file-selector-button:hover { border-color: var(--accent); color: var(--text); }
/* Older WebKit/Chromium alias -- ::file-selector-button is the standard
   name, but some engine versions still only recognize this prefixed one. */
.attachment-input input[type=file]::-webkit-file-upload-button {
	background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
	color: var(--text-dim); font-family: inherit; font-size: 12px; padding: 5px 10px;
	margin-right: 8px; cursor: pointer;
}
.attachment-input input[type=file]::-webkit-file-upload-button:hover { border-color: var(--accent); color: var(--text); }

.reply-form, .form-card { padding: 20px; }
.reply-form textarea, .form-card textarea, .form-card input[type=text] {
	width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 10px;
	padding: 12px 14px; color: var(--text); font-family: inherit; font-size: 13.5px; outline: none; resize: vertical;
}
.form-card input[type=text] { margin-bottom: 12px; }
.reply-form textarea:focus, .form-card textarea:focus, .form-card input:focus { border-color: var(--accent); }

/* Quill (public/editor.js) -- the "snow" theme ships light-mode colors by
   default, overridden here to match the rest of the site's dark palette
   instead of carrying a second, forum-specific theme CSS file.
   IMPORTANT: Quill does NOT nest the toolbar inside the element you pass to
   `new Quill(container, ...)` -- that element itself just gets turned into
   `.ql-container.ql-snow` in place, and the toolbar is built as a SEPARATE
   element next to it. Every rule below that scoped itself as
   ".rich-editor .ql-toolbar..." (a descendant combinator) silently matched
   nothing at all -- confirmed live 2026-10-14 via getComputedStyle() showing
   Quill's own default #ccc border completely unaffected by those rules
   despite !important. Fixed by dropping the wrong ".rich-editor " ancestor
   assumption everywhere below and targeting Quill's own classes directly. */
/* .rich-editor (the element passed to `new Quill(...)`) BECOMES
   .ql-container.ql-snow in place once Quill runs -- it never ends up
   wrapping the toolbar (that's a separate sibling, inserted right before
   it, see the big comment above). So "one dark frame around the whole
   editor" is actually two adjacently-bordered pieces with matching side
   borders and only their OUTER corners rounded, not a border on a single
   wrapping element. */
.rich-editor { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin-bottom: 12px; }
.rich-editor.ql-container {
	border: 1px solid var(--border) !important; border-top: none !important; border-radius: 0 0 10px 10px;
}
.ql-toolbar.ql-snow {
	background: var(--card-solid) !important; border: 1px solid var(--border) !important; border-bottom: 1px solid var(--border-soft) !important;
	border-radius: 10px 10px 0 0; padding: 8px 10px;
}
.ql-container.ql-snow { font-family: inherit; font-size: 13.5px; }
.ql-editor {
	color: var(--text); min-height: 140px; padding: 12px 14px; outline: none;
	scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.ql-editor::-webkit-scrollbar { width: 8px; }
.ql-editor::-webkit-scrollbar-track { background: transparent; }
.ql-editor::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.ql-editor::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
.ql-editor.ql-blank::before { color: var(--text-faint) !important; font-style: normal !important; }
.ql-snow .ql-stroke { stroke: var(--text-dim); }
.ql-snow .ql-fill, .ql-snow .ql-stroke.ql-fill { fill: var(--text-dim); }
.ql-snow .ql-picker { color: var(--text-dim); }
.ql-snow .ql-picker-options { background: var(--card-solid) !important; border: 1px solid var(--border) !important; }
.ql-toolbar.ql-snow button:hover .ql-stroke,
.ql-toolbar.ql-snow button.ql-active .ql-stroke { stroke: var(--accent); }
.ql-toolbar.ql-snow button:hover .ql-fill,
.ql-toolbar.ql-snow button.ql-active .ql-fill { fill: var(--accent); }
/* Faint box around each toolbar control (buttons + pickers) -- user asked
   for some visible separation between them, they had none before. */
.ql-toolbar.ql-snow button,
.ql-toolbar.ql-snow .ql-picker-label {
	border: 1px solid var(--border) !important; border-radius: 6px; margin: 1px;
}
.ql-toolbar.ql-snow button.ql-active,
.ql-toolbar.ql-snow .ql-expanded .ql-picker-label { border-color: var(--accent) !important; }
.ql-editor pre.ql-syntax { background: var(--bg-elevated); border-radius: 8px; }
.ql-editor a { color: var(--accent); }

/* Custom 5-step size picker (public/editor.js) -- Quill's own snow.css base
   rule (".ql-snow .ql-picker.ql-size ...::before { content:'Normal' }")
   loads after this file and would otherwise win -- !important is the
   reliable way to override a third-party stylesheet this project doesn't
   control, confirmed live 2026-10-14 (plain specificity bumps weren't
   enough once the exact competing selector was found). */
.ql-snow .ql-picker.ql-size { width: 96px; }
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before { content: "Size: 3" !important; }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="11px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="11px"]::before { content: "Size: 1" !important; }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12.5px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12.5px"]::before { content: "Size: 2" !important; }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="17px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="17px"]::before { content: "Size: 4" !important; }
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="21px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="21px"]::before { content: "Size: 5" !important; }

.reply-form { display: flex; flex-direction: column; gap: 12px; }

.auth-wrap { max-width: 400px; margin: 80px auto; }
.auth-wrap h1 { text-align: center; font-size: 22px; margin-bottom: 6px; }
.auth-wrap .sub { text-align: center; color: var(--text-dim); font-size: 13px; margin-bottom: 24px; }
.field { margin-bottom: 14px; }
.field label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
.field input { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 11px 14px; color: var(--text); font-family: inherit; font-size: 14px; outline: none; }
.field input:focus { border-color: var(--accent); }
/* Hides the native up/down spin buttons AND Edge's own little clipboard/
   "paste a number" glyph it docks inside every <input type="number"> --
   neither is part of our UI, Edge just draws them unprompted (2026-10-21,
   screenshot: a square icon sitting inside the Upgrade bet/target fields).
   Chromium/Safari use the ::-webkit-* spin-button pseudo-elements; Firefox
   needs the plain `appearance: textfield` instead, there's no pseudo-
   element to target there. */
.field input[type="number"]::-webkit-outer-spin-button,
.field input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.field input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.field textarea { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 11px 14px; color: var(--text); font-family: inherit; font-size: 14px; outline: none; resize: vertical; }
.field textarea:focus { border-color: var(--accent); }
.field select { width: 100%; height: 42px; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 0 14px; color: var(--text); font-family: inherit; font-size: 14px; outline: none; cursor: pointer; }
.field select:focus { border-color: var(--accent); }
.auth-error { background: #f0556c1a; border: 1px solid #f0556c40; color: #f0859a; font-size: 13px; padding: 10px 14px; border-radius: 9px; margin-bottom: 16px; }
.redeem-success { background: #4ade801a; border: 1px solid #4ade8040; color: var(--green); font-size: 13px; padding: 10px 14px; border-radius: 9px; margin-bottom: 16px; }
.auth-switch { text-align: center; margin-top: 18px; font-size: 13px; color: var(--text-dim); }
.auth-switch a { color: var(--accent); font-weight: 500; }

.profile-header { display: flex; gap: 20px; align-items: center; padding: 24px; }
.profile-header .info h2 { margin: 0 0 6px; font-size: 20px; }
.profile-badges { display: flex; flex-wrap: wrap; gap: 6px; }

/* ---- profile banner + overlapping avatar -------------------------------*/
.profile-banner {
	position: relative; height: 220px; background-color: var(--bg-elevated); background-size: cover; background-position: center;
	display: flex; align-items: flex-start; justify-content: flex-end; padding: 16px;
	/* Clips the banner image's own top corners to match the card's radius --
	   moved here (off the outer .card, see profile.ejs) so the card's own
	   overflow is NOT hidden, which was clipping away the status-pill glow
	   (filter: drop-shadow(), see view_helpers.ts's badgeStyle()) on any
	   badge rendered further down the card, like the profile name's Custom
	   badge -- confirmed live 2026-10-17, glow was invisible until this. */
	border-radius: var(--radius) var(--radius) 0 0; overflow: hidden;
}
/* Both the "Edit profile banner" label and the remove button sit in a flex
   row together (see profile.ejs) -- a <button> carries its own
   browser-default padding/line-height a <label> doesn't, which read as
   "crooked" (not vertically aligned) next to the text pill beside it even
   with align-items set on the row. Giving both an identical EXPLICIT
   height/box-sizing, instead of leaning on .btn-ghost's padding to produce
   the same height indirectly, is what actually makes them line up. */
.profile-banner-edit {
	display: inline-flex; align-items: center; justify-content: center; gap: 6px;
	height: 34px; box-sizing: border-box; margin: 0; padding: 0 14px;
	background: rgba(9,11,20,0.65); backdrop-filter: blur(2px); cursor: pointer; font: inherit; line-height: normal;
}
.profile-banner-remove { width: 34px; padding: 0; }
/* The nickname + badges sit ON the banner image itself (not below it) --
   absolutely positioned so .profile-banner's own flex layout (used for the
   "Edit profile banner" button top-right) doesn't also grab it. Left offset
   clears the avatar, which overlaps up from below via .profile-header-row's
   negative margin. filter:drop-shadow (not text-shadow -- that gets clipped
   away on the gradient-sheen status names, which use background-clip:text)
   keeps the name/badges legible over an arbitrary banner photo. */
.profile-identity-text { position: absolute; left: 153px; bottom: -6px; z-index: 2; }
.profile-name-mobile { display: none; margin: 0 0 8px; font-size: 19px; }
/* Crisp dark outline instead of a blurred drop-shadow -- a soft blur on
   small/thin text read as fuzzy/"not lying flat" against a busy banner
   photo. A tight 1px outline in every direction keeps the glyph edges
   sharp no matter what's behind them. */
.profile-identity-text .profile-name {
	text-shadow:
		0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9),
		1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9),
		0 2px 4px rgba(0,0,0,0.6);
}
/* All as ONE text-shadow list, not filter:drop-shadow -- drop-shadow()
   filters chain sequentially (each one shadows the PREVIOUS filter's output,
   not the original text), so stacking an offset outline shadow with a
   centered glow shadow made the glow visibly drag toward the outline's
   offset side (confirmed live 2026-10-17: glow looked bigger on the bottom,
   since the two outline layers above are both offset downward). Multiple
   text-shadow layers don't have that problem -- every layer shadows the
   original glyph independently, so they composite symmetrically regardless
   of order. (The old filter approach dated from when this rule assumed
   background-clip:text was in play -- it isn't, see .name-* above.) */
.profile-identity-text .profile-name.name-premium {
	text-shadow:
		0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9),
		1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9),
		0 2px 4px rgba(0,0,0,0.6);
}
.profile-identity-text .profile-name.name-hourboost { text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9), 1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9), 0 2px 4px rgba(0,0,0,0.6), 0 0 5px #22c55ef0, 0 0 16px #22c55eb3; }
.profile-identity-text .profile-name.name-support   { text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9), 1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9), 0 2px 4px rgba(0,0,0,0.6), 0 0 5px #7f1d2df0, 0 0 16px #7f1d2db3; }
.profile-identity-text .profile-name.name-designer  { text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9), 1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9), 0 2px 4px rgba(0,0,0,0.6), 0 0 5px #8b5cf6f0, 0 0 16px #8b5cf6b3; }
.profile-identity-text .profile-name.name-admin     { text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9), 1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9), 0 2px 4px rgba(0,0,0,0.6), 0 0 5px #f0556cf0, 0 0 16px #f0556cb3; }
.profile-identity-text .profile-name.name-media     { text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9), 1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9), 0 2px 4px rgba(0,0,0,0.6), 0 0 5px #fffffff0, 0 0 16px #ffffffb3; }
.profile-identity-text .profile-name.name-reseller  { text-shadow: 0 1px 0 rgba(0,0,0,0.9), 0 -1px 0 rgba(0,0,0,0.9), 1px 0 0 rgba(0,0,0,0.9), -1px 0 0 rgba(0,0,0,0.9), 0 2px 4px rgba(0,0,0,0.6), 0 0 5px #5ec8fff0, 0 0 16px #5ec8ffb3; }
.profile-header-row { display: flex; align-items: flex-end; gap: 20px; padding: 0 24px 20px; margin-top: -55px; position: relative; z-index: 2; }
.profile-avatar-wrap { position: relative; flex-shrink: 0; }
.profile-avatar-wrap .avatar { border: 4px solid var(--bg-elevated); }
/* z-index explicit (2026-10-19) -- the avatar frame ring's spinning
   ::before (.avatar-frame-ring.is-animated, also position:absolute,
   z-index:auto) started winning the stacking fight against this button
   without it, burying the camera icon behind the ring on an account with
   an animated frame equipped. */
.profile-avatar-edit { position: absolute; right: -2px; bottom: -2px; width: 32px; height: 32px; cursor: pointer; z-index: 2; }
/* Clicking the avatar itself removes it (confirm-gated) -- an unstyled
   button wrapping the real avatar, with an "x" overlay that only appears
   on hover so it doesn't look clickable-to-delete at a glance. */
.profile-avatar-click { all: unset; cursor: pointer; position: relative; display: inline-flex; border-radius: 50%; }
.profile-avatar-click::after {
	content: "close";
	font-family: "Material Symbols Rounded"; font-feature-settings: "liga"; -webkit-font-feature-settings: "liga";
	position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
	font-size: 30px; color: #fff; background: rgba(9,11,20,0.6); border-radius: 50%;
	opacity: 0; transition: opacity 0.15s;
}
.profile-avatar-click:hover::after { opacity: 1; }
.profile-header-row .info { padding-bottom: 6px; }
.profile-name { margin: 0 0 10px; font-size: 28px; }
.profile-badges-lg .status-pill-lg { font-size: 13px; padding: 7px 16px; }
.profile-custom-title { margin-top: 8px; font-size: 13px; color: var(--text-dim); font-style: italic; }

.profile-meta {
	display: flex; flex-wrap: wrap; gap: 4px 20px; padding: 14px 24px 20px;
	font-size: 12.5px; color: var(--text-dim); border-top: 1px solid var(--border-soft);
}
.profile-meta span { display: flex; align-items: center; gap: 6px; }
.profile-meta .icon { color: var(--text-faint); }

.invite-code { font-family: monospace; background: var(--bg); border: 1px solid var(--border); padding: 8px 12px; border-radius: 8px; display: inline-block; margin: 4px 6px 4px 0; font-size: 13px; }
.invite-used { opacity: 0.4; text-decoration: line-through; }

.empty-state { padding: 40px 20px; text-align: center; color: var(--text-faint); font-size: 13.5px; }

.site-footer { max-width: 1240px; margin: 20px auto 0; padding: 20px 24px 30px; text-align: center; font-size: 12px; color: var(--text-faint); }
.site-footer a { color: var(--text-dim); }
.site-footer-dot { margin: 0 8px; }

.format-hint { font-size: 11.5px; color: var(--text-faint); margin-top: -4px; }

/* ---- pagination ---------------------------------------------------------*/
.pagination { display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 20px; }
.pg-btn {
	min-width: 36px; height: 36px; padding: 0 10px; border-radius: 9px; border: 1px solid var(--border);
	background: var(--bg-elevated); color: var(--text-dim); display: flex; align-items: center; justify-content: center;
	font-size: 13px; font-weight: 500;
}
.pg-btn:hover { border-color: var(--accent); color: var(--text); }
.pg-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.pg-btn.disabled { opacity: 0.35; pointer-events: none; }
.pg-ellipsis { color: var(--text-faint); padding: 0 4px; }

@media (max-width: 920px) {
	.layout { grid-template-columns: 1fr; }
	.cell-left, .cell-right { grid-column: 1; grid-row: auto; }
	.nav-search { display: none; }
	.cat-row { grid-template-columns: auto 1fr; }
	.cat-row .stat, .cat-row .last-post { display: none; }
}

/* ---- phone-width nav -------------------------------------------------
   Below this, the nav's five text links + search + user name no longer
   fit in one row and were overflowing the viewport (real horizontal
   scroll on a phone) -- collapse to icon-only links and drop anything
   that's decoration rather than navigation. */
@media (max-width: 680px) {
	/* FIXED (2026-10-19, reported live on phone with screenshots): the icon
	   row (logo + 6 nav-links + up to 3 nav-right icons) still doesn't fit
	   in one row on a real phone even icon-only -- .nav-inner is a plain
	   flex row with no wrap/overflow handling, so the overflow used to
	   stretch the WHOLE PAGE wider instead of staying inside the nav bar
	   (the avatar on the right visibly cut off past the screen edge in both
	   reported screenshots).
	   FIXED AGAIN (2026-10-19, same day, reported live): the first fix put
	   overflow-x:auto on .nav-inner itself -- but .notif-wrap/.profile-menu-
	   wrap (the profile/notifications dropdown popups) live INSIDE
	   .nav-inner too (as .nav-right's children), and per the CSS overflow
	   spec, setting overflow-x to anything but visible forces the computed
	   overflow-y to auto as well when it's not also set explicitly -- so
	   .nav-inner was silently clipping both popups' own absolute-positioned
	   dropdowns vertically (reported live: "popup disappears, doesn't fully
	   appear"). The scrolling now lives ONLY on .nav-links (the 6-icon row
	   that actually doesn't fit, not the nav bar as a whole) -- .nav-right's
	   popups are a SIBLING of .nav-links, not a descendant, so they're no
	   longer inside anything with non-visible overflow at all. min-width:0
	   is required too: a flex item's default min-width is its own content's
	   width, which is why .nav-links wasn't actually shrinking/scrolling
	   before -- it was just forcing the whole row wider instead.
	   logo-title hidden too (not just logo-tagline) -- the wordmark text
	   was the single biggest non-essential chunk of width in that row. */
	.nav-inner { padding: 0 12px; gap: 8px; }
	.logo-tagline, .logo-title { display: none; }
	.logo { flex-shrink: 0; }
	.nav-right { flex-shrink: 0; }
	.nav-links { gap: 10px; min-width: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
	.nav-link-label { display: none; }
	.nav-link { padding: 8px 0; }
	.nav-link .icon { font-size: 21px; }
	/* FIXED (2026-10-19, reported live on phone, Settings page cut off on
	   the right -- Bio/Privacy cards unreachable). settings.ejs sets
	   grid-template-columns: 1fr 1fr directly as an INLINE style on
	   .layout, not through this stylesheet -- an inline style always wins
	   over an external rule no matter how it's ordered, !important is the
	   only way to override it (same reason .sub-grid > * needed !important
	   above, same root cause, different page). Two 190px-ish columns side
	   by side wouldn't actually look broken by themselves, but it stays two
	   columns instead of stacking, so it was still overflowing-feeling and
	   genuinely harder to use than a single column on a real phone. */
	.layout { grid-template-columns: 1fr !important; }
	.notif-menu { width: calc(100vw - 32px); max-width: 320px; }
	.nav-user .name { display: none; }
	.nav-right { gap: 0; }
	.page { padding: 0 14px 40px; }
	.page-header { flex-wrap: wrap; gap: 12px; }
	.sub-grid > * { grid-column: 1 !important; grid-row: auto !important; }
	/* The nickname-on-banner trick (position:absolute at a fixed left
	   offset) collided with the badges wrapping under it once the avatar +
	   badges row got this narrow -- fall back to a plain name heading
	   above the badges instead, same as any other profile field. */
	.profile-identity-text { display: none; }
	.profile-name-mobile { display: block; }
	.profile-banner { height: 160px; }
	.profile-header-row { margin-top: -40px; flex-wrap: wrap; }
	.profile-avatar-wrap .avatar { width: 84px !important; height: 84px !important; }
}

/* ---- subscription page -----------------------------------------------*/
/* Grid, not the site's usual .layout (which forces align-items:start) --
   default grid align-items is stretch, so the Subscription and Buy
   Subscription cards (both grid-row:1) are forced to the SAME height
   automatically, whichever has more content. Combined with
   .sub-card-stretch's flex column + footer margin-top:auto below, that
   keeps both cards' bottom buttons level with each other no matter how
   many rows either one has -- requested explicitly after the two buttons
   ended up visibly uneven with a plain two-column layout. */
.sub-grid { display: grid; grid-template-columns: 750px 1fr; gap: 20px; }
/* FIXED (2026-10-19, reported live on phone: page forced wider than the
   screen, every value column pushed off the right edge). This has to come
   AFTER the base .sub-grid rule above, not inside the earlier @media
   (max-width:680px) block further up the file -- same selector, same
   specificity, so on a narrow screen the LATER one in source order always
   wins regardless of which @media it's in, and the base rule's hardcoded
   750px first column was winning every time, making the whole grid (and
   so the page) try to be at least 750px wide on any screen. */
@media (max-width: 680px) {
	.sub-grid { grid-template-columns: 1fr; }
}
.sub-card-stretch { display: flex; flex-direction: column; }
.sub-card-stretch .sub-card-footer { margin-top: auto; }
.sub-card-header { display: flex; align-items: center; gap: 14px; padding: 20px; }
.sub-card-header .icon-wrap {
	width: 44px; height: 44px; border-radius: 12px; background: var(--accent-soft); color: var(--accent);
	display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.sub-card-header .icon-wrap .icon { font-size: 23px; }
.sub-icon-plain { color: var(--accent); font-size: 26px; }
.sub-card-header .st-title { font-weight: 600; font-size: 15.5px; }
.sub-card-header .st-desc { font-size: 12.5px; color: var(--text-dim); margin-top: 2px; }
.sub-row {
	display: flex; align-items: center; justify-content: space-between;
	padding: 14px 20px; border-top: 1px solid var(--border-soft); font-size: 13.5px;
}
.sub-row .k { color: var(--text-dim); }
.sub-row .v { font-weight: 600; }
.sub-row .v.accent { color: var(--accent); }
.sub-row .v.status-dot { display: flex; align-items: center; gap: 7px; color: var(--green); }
.sub-row .v.status-dot .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); }
.sub-row .v.status-dot.offline { color: #f0556c; }
.sub-row .v.status-dot.offline .dot { background: #f0556c; }
.sub-card-footer { padding: 18px 20px; border-top: 1px solid var(--border-soft); }
.sub-card-footer .btn { flex: 1; width: 100%; justify-content: center; padding: 11px; font-size: 14px; }
.sub-support-row {
	display: flex; align-items: center; gap: 14px; padding: 18px 20px;
}
.sub-support-row .icon-wrap { background: var(--accent-soft); color: var(--accent); }

/* ---- buy subscription panel ------------------------------------------*/
/* Avatar Frames (2026-10-19) -- deliberately minimal per explicit request:
   just the avatar-with-frame preview and its price underneath, no label
   text, no separate Buy/Equip button -- clicking the whole cell IS the
   buy/equip action (the <button> wraps the preview+price). Currently
   equipped one gets a highlighted ring instead of a text badge, same
   "show, don't tell" idea. */
/* justify-content:center (not the flex default flex-start) -- on a narrow
   phone (<=360px) only 2 of the 3 frames fit per row, leaving the 3rd
   stranded alone on the left on its own row instead of sitting centered
   under the first two; centering keeps any leftover row balanced instead
   of ragged, regardless of how many frames the catalog ever grows to. */
.frame-grid { display: flex; flex-wrap: wrap; gap: 14px; padding: 16px 20px; justify-content: center; }
.frame-pick-form { margin: 0; }
.frame-pick {
	display: flex; flex-direction: column; align-items: center; gap: 6px;
	background: none; border: 1px solid transparent; border-radius: 12px;
	padding: 8px 10px; cursor: pointer; font: inherit;
}
.frame-pick:hover:not(:disabled) { background: var(--bg); }
.frame-pick.is-equipped { border-color: var(--accent); cursor: default; }
.frame-price { font-size: 11px; color: var(--text-dim); }

.plan-list { display: flex; flex-direction: column; gap: 10px; padding: 16px 20px; }
.plan-option {
	display: flex; align-items: center; gap: 10px; padding: 12px 14px;
	border: 1px solid var(--border); border-radius: 10px; cursor: pointer; font-size: 13.5px;
}
.plan-option:hover { border-color: var(--accent); }
.plan-option:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.plan-option input { accent-color: var(--accent); margin: 0; }
.plan-option .plan-name { flex: 1; font-weight: 500; }
.plan-option .plan-price { font-weight: 700; color: var(--accent); }

/* ---- notifications + private messages (nav badges, dropdown, inbox/thread) ----
   Nav icons are real <a>/<button> elements now, so they reset browser chrome;
   the red-dot-style badge is a count pill, not the old decorative dot. */
.nav-icon-link {
	position: relative; color: var(--text-dim); font-size: 22px; cursor: pointer;
	background: none; border: none; padding: 4px; border-radius: 8px; display: inline-flex; align-items: center;
	transition: color 0.15s ease;
}
.nav-icon-link:hover { color: var(--text); }
.nav-badge {
	position: absolute; top: -4px; right: -6px; min-width: 17px; height: 17px; padding: 0 5px; border-radius: 9px;
	background: var(--accent); color: #fff; font-size: 10.5px; font-weight: 700; line-height: 17px; text-align: center;
	font-family: inherit;
}
.notif-wrap { position: relative; }
.notif-menu-title { font-weight: 600; font-size: 14px; }
.notif-empty { padding: 18px 16px; color: var(--text-faint); font-size: 13px; }
.notif-item {
	display: block; padding: 11px 16px; border-bottom: 1px solid var(--border-soft); color: var(--text); text-decoration: none;
}
.notif-item:hover { background: var(--bg); color: var(--text); }
.notif-item.unread { background: var(--accent-soft); }
.notif-item-text { display: block; font-size: 13px; line-height: 1.4; }
.notif-item-time { display: block; font-size: 11.5px; color: var(--text-faint); margin-top: 3px; }
.notif-all { border-top: 1px solid var(--border-soft); }

.notif-row { color: var(--text); }
.notif-row:hover { color: var(--text); background: var(--bg); }
.notif-row.unread { background: var(--accent-soft); }
.notif-row-icon { color: var(--accent); font-size: 20px; }
.notif-row-text { font-size: 13.5px; }
.notif-row-time { font-size: 11.5px; color: var(--text-faint); margin-top: 3px; }

/* ---- landing page (/home) ----------------------------------------------
   Styled after a reference screenshot the user gave (neverlose.cc): a big
   full-bleed hero with a dark image behind it and one centered CTA, a thin
   colored "product" banner strip below it, then a plain (no card/no icon)
   bold-title + text feature grid on its own gradient band. .full-bleed
   breaks a section out of .page's max-width:1240px/24px-padding container
   so it can span the whole viewport width, same trick used for all three
   bands here. */
.full-bleed { width: 100vw; position: relative; left: 50%; right: 50%; margin-left: -50vw; margin-right: -50vw; }

.hero { min-height: 560px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
/* 72px nav + ~150px for .page's bottom padding and the footer below the
   hero -- leaves just enough room that the whole page fits one viewport
   without its own scrollbar, instead of overflowing by exactly that much. */
.hero-solo { min-height: calc(100vh - 222px); }
.hero-bg {
	position: absolute; inset: 0;
	/* Placeholder background until a real in-game screenshot is dropped in --
	   swap this for background-image: url(...) once one exists, the dark
	   radial glow + vignette below still work fine layered under a photo. */
	background:
		radial-gradient(ellipse 900px 500px at 30% 20%, #6d6cf03d, transparent 60%),
		radial-gradient(ellipse 700px 500px at 80% 70%, #6d6cf024, transparent 60%),
		linear-gradient(180deg, #0b0e1a, #090b14 70%);
}
.hero-bg::after {
	content: ""; position: absolute; inset: 0;
	background: radial-gradient(ellipse 60% 50% at 50% 100%, #090b14cc, transparent);
}
.hero-inner { position: relative; text-align: center; max-width: 640px; padding: 0 24px; }
.hero-fade-in { animation: hero-fade-in 1.1s ease both; }
@keyframes hero-fade-in {
	from { opacity: 0; transform: translateY(14px); }
	to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .hero-fade-in { animation: none; } }
.hero-wordmark { font-size: 56px; font-weight: 700; letter-spacing: 2px; color: var(--text); }
.hero-wordmark .accent { color: var(--accent); }
.hero-sub { margin: 16px 0 32px; color: var(--text-dim); font-size: 15px; line-height: 1.6; }
.hero-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
.hero-btn { padding: 13px 28px; font-size: 14px; border-radius: var(--radius-sm); }
.hero-scroll-hint { position: absolute; bottom: 22px; left: 50%; transform: translateX(-50%); }
.hero-scroll-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-faint); animation: hero-scroll-bounce 1.6s ease-in-out infinite; }
@keyframes hero-scroll-bounce {
	0%, 100% { opacity: 0.4; transform: translateY(0); }
	50% { opacity: 1; transform: translateY(6px); }
}
@media (prefers-reduced-motion: reduce) { .hero-scroll-dot { animation: none; } }

.product-banner {
	background: linear-gradient(100deg, #6d6cf024, #6d6cf00a 50%, transparent 85%);
	border-bottom: 1px solid var(--border-soft);
}
.product-banner-inner {
	max-width: 1240px; margin: 0 auto; padding: 28px 24px;
	display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
}
.product-banner-title { font-size: 22px; font-weight: 700; }
.product-banner-sub { color: var(--text-dim); font-size: 13.5px; margin-top: 4px; }

.landing-section { margin: 56px 0; }
.landing-section-header { text-align: center; margin-bottom: 28px; }
.landing-section-header .eyebrow { color: var(--accent); font-size: 12px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; }
.landing-section-header h2 { margin: 6px 0 0; font-size: 24px; }

.screenshot-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.screenshot-tile {
	margin: 0; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border-soft);
	background: var(--bg-elevated); aspect-ratio: 16 / 10;
}
.screenshot-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.screenshot-tile figcaption { padding: 10px 14px; font-size: 12.5px; color: var(--text-dim); }
.screenshot-placeholder {
	display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
	color: var(--text-faint); font-size: 12.5px; border-style: dashed;
}
.screenshot-placeholder .icon { font-size: 30px; }

.feature-band { background: linear-gradient(180deg, #05060c, #0d1428); padding: 64px 0; }
.feature-band-inner {
	max-width: 1100px; margin: 0 auto; padding: 0 24px;
	display: grid; grid-template-columns: repeat(3, 1fr); gap: 36px 48px;
}
.feature-plain-title { font-weight: 700; font-size: 15px; margin-bottom: 6px; }
.feature-plain-desc { color: var(--text-dim); font-size: 13px; line-height: 1.55; }

.landing-cta { text-align: center; margin: 64px 0 40px; }
.landing-cta h2 { margin: 0 0 8px; font-size: 24px; }
.landing-cta p { margin: 0 0 24px; color: var(--text-dim); font-size: 14px; }

@media (max-width: 900px) {
	.hero-wordmark { font-size: 40px; }
	.screenshot-grid { grid-template-columns: 1fr; }
	.feature-band-inner { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
	.product-banner-inner { flex-direction: column; align-items: flex-start; }
}

.msg-row { color: var(--text); }
.msg-row:hover { color: var(--text); background: var(--bg); }
.msg-row.unread { background: var(--accent-soft); }
.msg-row.unread .msg-row-preview { color: var(--text); font-weight: 600; }
.msg-row-top { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
.msg-row-top > span:first-child { font-weight: 600; font-size: 14px; }
.msg-row-time { font-size: 11.5px; color: var(--text-faint); white-space: nowrap; }
.msg-row-preview { font-size: 13px; color: var(--text-dim); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-row-you { color: var(--text-faint); }

.pm-thread { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }
.ticket-thread {
	max-height: 480px; overflow-y: auto;
	scrollbar-width: thin; scrollbar-color: var(--border) transparent; /* Firefox */
}
.ticket-thread::-webkit-scrollbar { width: 8px; }
.ticket-thread::-webkit-scrollbar-track { background: transparent; }
.ticket-thread::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }
.ticket-thread::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
.pm-msg { display: flex; align-items: flex-end; gap: 10px; max-width: 80%; }
.pm-msg.mine { align-self: flex-end; flex-direction: row-reverse; }
.pm-bubble { background: var(--bg); border: 1px solid var(--border); border-radius: 14px; padding: 9px 13px; min-width: 0; }
.pm-msg.mine .pm-bubble { background: var(--accent-soft); border-color: var(--accent); }
.pm-body { font-size: 14px; white-space: pre-wrap; word-break: break-word; }
.pm-time { font-size: 11px; color: var(--text-faint); margin-top: 4px; }

.pm-report { background: none; border: none; color: var(--text-faint); cursor: pointer; padding: 0 2px; font-family: inherit; }
.pm-report:hover { color: var(--text-dim); }

/* ---- support tickets (routes/support.ts, views/support_tickets.ejs) ---- */
.ticket-status {
	display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px;
	font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; margin-right: 8px;
}
.ticket-status.open      { background: #f0556c1a; color: #f0859a; border: 1px solid #f0556c40; }
.ticket-status.in_review { background: #f6c3431a; color: #f8d57a; border: 1px solid #f6c34340; }
.ticket-status.resolved  { background: #4ade801a; color: var(--green); border: 1px solid #4ade8040; }
.ticket-category { font-size: 11.5px; color: var(--text-faint); }
.ticket-status-form select {
	background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
	color: var(--text); font-family: inherit; font-size: 12.5px; padding: 6px 10px; outline: none; cursor: pointer;
}

/* ---- modal overlay (support contact form, header.ejs) ---- */
.modal-overlay {
	display: none; position: fixed; inset: 0; background: #05060cb3;
	align-items: center; justify-content: center; z-index: 200; padding: 20px;
}
.modal-overlay.open { display: flex; }
.modal-card { background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius); width: 100%; max-width: 440px; max-height: 90vh; overflow-y: auto; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid var(--border-soft); }
.modal-header .title { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 15px; }
.modal-header .title .icon { color: var(--accent); font-size: 20px; }
.modal-close { background: none; border: none; color: var(--text-dim); cursor: pointer; display: flex; padding: 4px; }
.modal-close:hover { color: var(--text); }
.modal-body { padding: 20px; display: flex; flex-direction: column; gap: 14px; }
.modal-body .field label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }

/* ---- email-confirmation banner (header.ejs) ---- */
.verify-banner { display:flex; align-items:center; justify-content:center; gap:8px; flex-wrap:wrap; padding:10px 16px; background:var(--accent-soft); border-bottom:1px solid var(--border-soft); font-size:13px; color:var(--text); }
.verify-banner .icon { color:var(--accent); font-size:18px; }
.verify-banner a { color:var(--accent); font-weight:600; }

/* ---- casino (routes/casino.ts, views/casino_*.ejs, public/casino.js) --- */
.casino-subnav { display: flex; align-items: center; gap: 4px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border-soft); }
.casino-subnav-tab {
	display: inline-flex; align-items: center; gap: 6px;
	padding: 8px 16px; border-radius: 999px; font-size: 13.5px; font-weight: 600;
	color: var(--text-dim); text-decoration: none;
}
.casino-subnav-tab .icon { font-size: 17px; }
.casino-subnav-tab:hover { color: var(--text); }
.casino-subnav-tab.active { background: var(--accent-soft); color: var(--accent); }
.casino-balance { margin-left: 16px; display: flex; align-items: center; gap: 6px; font-size: 13.5px; font-weight: 600; color: var(--accent); }
.casino-balance .icon { font-size: 17px; }
/* Volume control (2026-10-21, by request -- "не просто отключать включать
   но и выбирать громкость от 0 до 100") -- sits just above the per-page
   bet card, same subnav row the balance lives in, pushed to the far right
   via its own margin-left:auto (not the balance's own anymore -- it now
   sits right after this and rides along). */
.casino-sound-control { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.casino-sound-control .icon { font-size: 18px; color: var(--text-dim); }
.casino-sound-control .slim-range { width: 90px; }

/* Phone-width Casino (2026-10-22, by request -- "оптимизируй ВСЕ игры в
   казино под телефоны"): the subnav's 6 tabs + sound slider + balance are a
   plain flex row with no wrap, which overflowed the screen width on a real
   phone the same way the main site nav once did (see that fix's own
   comment further up this file). Wraps onto 2 rows instead -- tabs shrink
   and wrap freely, the sound control claims its own full-width row via
   flex-basis so it doesn't awkwardly share a row with just one tab. */
@media (max-width: 760px) {
	.casino-subnav { flex-wrap: wrap; row-gap: 10px; }
	.casino-subnav-tab { padding: 6px 10px; font-size: 12.5px; }
	.casino-subnav-tab .icon { font-size: 15px; }
	.casino-balance { margin-left: 0; }
	.casino-sound-control { margin-left: 0; flex: 1 1 100%; justify-content: flex-end; }
}


/* The reel strip -- a fixed-width viewport (overflow:hidden) with a
   horizontally-scrolling track inside, animated by public/casino.js's
   initCaseReel() to land the ACTUAL rolled slot (data-landing-index) under
   the center pointer. See that file's own comment: the outcome is already
   decided server-side by the time this ever plays, this is purely the
   reveal animation. */
.case-reel-viewport {
	position: relative; overflow: hidden; height: 120px; border-radius: var(--radius);
	background: var(--bg); display: flex; align-items: center;
}
.case-reel-placeholder { width: 100%; display: flex; align-items: center; justify-content: center; color: var(--text-faint); }
.case-reel-pointer {
	position: absolute; left: 50%; top: 0; bottom: 0; width: 2px; background: var(--accent);
	transform: translateX(-50%); z-index: 2; box-shadow: 0 0 8px var(--accent);
}
.case-reel-track { display: flex; gap: 10px; padding: 0 10px; will-change: transform; }
.case-reel-slot {
	flex-shrink: 0; width: 110px; height: 100px; border-radius: 10px; border: 1px solid var(--border-soft);
	background: var(--bg-elevated); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
}
.case-reel-icon {
	width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
	background: var(--accent-soft); color: var(--accent); font-size: 17px;
}
.case-reel-label { font-size: 11px; color: var(--text-dim); text-align: center; padding: 0 4px; }
.case-result-banner { text-align: center; font-size: 14px; margin-bottom: 10px; color: var(--text); }

/* Upgrade dial -- the lit (win) arc runs from the top (0%, 12 o'clock)
   CLOCKWISE through --win-pct (set inline per result by casino_upgrade.ejs,
   read by conic-gradient's own default `from 0deg` = up/clockwise
   direction, no extra angle math needed to keep this in sync with the
   needle rotation public/casino.js computes). */
/* Sized and laid out after the 2026-10-21 reference screenshot (a classic
   skin-upgrader gauge): a big ring, a huge centered percentage, 0/50/50/100
   tick labels around the rim, a fixed pointer marking the 0%/100% seam at
   the top, and the win arc as a 2-stop gradient (kept in the site's own
   purple/blue brand instead of the reference's literal yellow-to-red --
   recoloring it would clash with every other purple accent on this forum,
   the gauge SHAPE is what was asked to match, not its literal palette). */
.upgrade-dial-wrap { position: relative; width: 280px; height: 280px; margin: 8px auto 0; }
/* The lit (win) arc is centered at the BOTTOM (0%, where the pointer sits)
   and grows symmetrically up BOTH sides at once, meeting at the TOP only
   at 100% (2026-10-21, by request -- "СНИЗУ должна быть цветная часть... и
   шансы СНИЗУ ВВЕРХ"). conic-gradient only sweeps in ONE direction, so a
   symmetric-about-the-start fill needs two separate stops instead of one:
   `from 180deg` anchors gradient-0% at the bottom; sweeping clockwise from
   there, the LEFT half of the ring is gradient 0%-50% and the RIGHT half
   is 50%-100% (both ending at the top, gradient-50%) -- so lighting
   [0%, win-pct/2] colors the first stretch out of the bottom going left,
   and [100%-win-pct/2, 100%] colors the LAST stretch approaching the
   bottom from the right, which together form one continuous arc centered
   on the bottom and symmetric left/right. At win-pct=100 the two pieces
   meet exactly at the top, fully lighting the ring. public/casino.js's
   playUpgradeResult() computes the needle's landing angle with the exact
   same `from 180deg`/half-split math, so a win always visually lands
   inside this lit arc and a loss always lands in the dark top arc. */
.upgrade-dial {
	--win-pct: 0%;
	width: 280px; height: 280px; border-radius: 50%; position: relative;
	background: conic-gradient(from 180deg,
		var(--accent) 0%, var(--accent) calc(var(--win-pct) / 2),
		var(--bg) calc(var(--win-pct) / 2), var(--bg) calc(100% - var(--win-pct) / 2),
		var(--accent) calc(100% - var(--win-pct) / 2), var(--accent) 100%);
	border: 8px solid var(--bg-elevated); box-shadow: 0 0 0 1px var(--border);
}
.upgrade-dial-tick {
	position: absolute; font-size: 11px; font-weight: 600; color: var(--text-faint); z-index: 1;
}
.upgrade-dial-tick-top { left: 50%; top: -42px; transform: translateX(-50%); }
.upgrade-dial-tick-left { left: -28px; top: 50%; transform: translate(-100%, -50%); }
.upgrade-dial-tick-right { right: -28px; top: 50%; transform: translate(100%, -50%); }
/* The dense ring of little clock-face dashes around the gauge (2026-10-21
   reference) -- a `repeating-conic-gradient` (solid every 9deg, 40 dashes
   around the full circle) cut down to a thin ring with a radial-gradient
   mask, instead of generating 40 individual DOM elements for what's purely
   decorative. Sits OUTSIDE .upgrade-dial-wrap's own 280px box (inset
   negative), so it doesn't compete for space with the ring/pointer/ticks. */
.upgrade-dial-ticks {
	position: absolute; inset: -16px; border-radius: 50%; pointer-events: none;
	background: repeating-conic-gradient(var(--border) 0deg 1.6deg, transparent 1.6deg 9deg);
	-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 11px), #000 calc(100% - 11px));
	mask: radial-gradient(farthest-side, transparent calc(100% - 11px), #000 calc(100% - 11px));
}
/* The donut hole -- covers the center of the conic-gradient pie so only a
   ring of the win-colored arc stays visible, and gives the chance% number
   somewhere to live that a rotating marker can never cross over. */
.upgrade-dial-inner {
	position: absolute; inset: 28px; border-radius: 50%; background: var(--bg-elevated);
	display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 4px;
}
.upgrade-dial-chance-value { font-size: 42px; font-weight: 800; color: var(--accent); line-height: 1.15; max-width: 85%; }
.upgrade-dial-chance-label { font-size: 11.5px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim); }
/* Replaces the chance% + "Chance" label combo once a bet has resolved
   (2026-10-21 by request -- "ВМЕСТО Chance ПУСТЬ БУДЕТ НАДПИСЬ Вы
   выиграли!/Вы проиграли!"), smaller than the percentage display since it's
   a whole phrase, not two characters. */
.upgrade-dial-outcome-win, .upgrade-dial-outcome-lose { font-size: 21px; }
.upgrade-dial-outcome-win { color: #4ade80; }
.upgrade-dial-outcome-lose { color: #f87171; }
/* The roll marker -- a small glowing dot riding the ring at the rolled
   angle, not a long needle from the center. It pivots with the standalone
   CSS `rotate` property (not `transform`) so casino.js can animate
   `element.style.rotate` on its own. Two bugs already happened here
   (2026-10-21), both fixed by removing `transform`/translate from this
   element ENTIRELY instead of trying to make it coexist with `rotate`:
   (1) `needle.style.transform = "rotate(Xdeg)"` from JS silently dropped
   the translateX/translateY centering that used to also live on
   `transform`. (2) Switching the centering to `rotate` + a SEPARATE
   `transform: translate(...)` still didn't work, because per spec the
   individual transform properties (translate/rotate/scale) apply BEFORE
   `transform` itself, not after -- so the rotation happened first, around
   the UN-translated (wrong) pivot, and the translate shifted the whole
   already-rotated needle afterward, landing it far outside the ring
   (confirmed live, 2026-10-21 second screenshot). Positioning the box with
   left/bottom/margin instead of left/top/transform needs no translate at
   all, so there is nothing left for `rotate`'s ordering to conflict with. */
/* Always visible, resting at the bottom (180deg) by default -- not hidden
   until a bet, not held there with a timer (2026-10-21, by request -- "она
   должна ВСЕГДА стоять", not a 5s delay). A spin just animates AWAY from
   this permanent resting angle and back to it isn't special-cased: the
   next bet's submit handler (casino_upgrade.ejs) snaps it back to 180deg
   instantly, right before starting the next spin. */
.upgrade-dial-needle {
	position: absolute; left: 50%; bottom: 50%; width: 2px; height: 44%; margin-left: -1px;
	transform-origin: bottom center; rotate: 180deg; pointer-events: none;
}
/* An arrowhead (2026-10-21, replaced a plain dot by request) pointing
   INWARD -- its apex sits at the needle's end closest to the pivot/center,
   base toward the rim (border-top, not border-bottom -- the standard CSS
   triangle recipe: coloring border-top puts the point at the box's own
   bottom edge, border-bottom puts it at the top; here that bottom edge is
   the end nearer the needle, i.e. nearer the dial's center). `filter:
   drop-shadow` (not `box-shadow`) because box-shadow follows the invisible
   0-size BOX, not the visible triangle shape drawn via borders. */
.upgrade-dial-needle::after {
	content: ""; position: absolute; top: -16px; left: 50%; width: 0; height: 0; transform: translateX(-50%);
	border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 16px solid #fff;
	filter: drop-shadow(0 0 7px rgba(255,255,255,0.85));
}
.upgrade-quick-row { display: flex; gap: 8px; }
.upgrade-quick-btn { flex: 1; padding: 6px 0; font-size: 12.5px; justify-content: center; }

/* Upgrade's 3-column layout (2026-10-21 reference) -- bet on the left,
   gauge centered, payout/target on the right, instead of a plain
   form+sidebar split. A dedicated grid rather than reusing the sitewide
   `.layout` (that one's a fixed 1fr/340px split meant for a wide content
   column + a narrow sidebar, not three roughly-even panels). */
.upgrade-layout { display: grid; grid-template-columns: 280px 1fr 280px; gap: 20px; align-items: stretch; }
/* Plain top-down stacking, no `justify-content`/`margin-top: auto` trick
   (2026-10-21 -- tried bottom-pinning the footer first, but that aligned
   the title/icon/payout while pushing the bet/chance INPUT itself to a
   different height per panel, since the two footers' own content differs
   in length below that input -- still misaligned, just a different part
   of it). Both side panels share the exact same element order up through
   the amount input (title, icon, big payout number, label row, input) --
   plain flow keeps ALL of that at identical heights on both sides with no
   extra positioning needed; only the content AFTER the input (this side's
   slider+presets vs. the other's multiplier row+presets) is free to differ
   in height, and that's fine since it's below the part asked to line up. */
.upgrade-side-panel { display: flex; flex-direction: column; align-items: center; padding: 24px 20px; gap: 14px; }
.upgrade-side-title { font-size: 13.5px; font-weight: 700; text-align: center; }
.upgrade-side-icon { width: 100%; height: 120px; display: flex; align-items: center; justify-content: center; }
.upgrade-side-icon .icon { font-size: 88px; color: var(--text-faint); opacity: 0.5; }
.upgrade-side-icon-accent .icon { color: var(--accent); opacity: 0.8; }
.upgrade-side-payout { font-size: 26px; font-weight: 800; color: var(--accent); text-align: center; }
.upgrade-side-footer { width: 100%; display: flex; flex-direction: column; gap: 10px; }
.upgrade-side-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--text-dim); }
.upgrade-side-divider { height: 1px; background: var(--border-soft); margin: 2px 0; }
/* Keeps the left panel's "Bet Amount" and right panel's "Multiplier" slots
   the same fixed height (2026-10-21) -- the bet slider that used to live
   here was removed entirely by request ("вырежи его вообще", after it
   broke when wrapped in a flex container for this exact alignment reason);
   this empty spacer takes its place in the layout so the preset-button
   grids below still land at the same height on both sides. */
.upgrade-side-slot { height: 24px; display: flex; align-items: center; }
.upgrade-preset-row { display: flex; gap: 6px; }
.upgrade-preset-btn {
	flex: 1; padding: 6px 0; font-size: 12px; font-weight: 700; border-radius: var(--radius-sm);
	background: transparent; cursor: pointer; font-family: inherit;
}
.upgrade-preset-risky { color: #f87171; border: 1px solid #f8717166; }
.upgrade-preset-medium { color: #fbbf24; border: 1px solid #fbbf2466; }
.upgrade-preset-safe { color: var(--green); border: 1px solid #4ade8066; }
.upgrade-center { display: flex; flex-direction: column; align-items: center; gap: 18px; padding-top: 40px; }
.upgrade-submit-btn { width: 100%; max-width: 280px; justify-content: center; padding: 13px 0; font-size: 15px; margin-top: 24px; }

/* Phone-width Casino (2026-10-22, by request -- "оптимизируй ВСЕ игры в
   казино под телефоны") -- this ONE grid is shared by Upgrade, Crash,
   Mines AND Plinko (all four views use the exact same upgrade-layout/
   upgrade-side-panel/upgrade-center classes), so fixing it here fixes all
   four at once. Fixed 280px side columns alone (560px) already exceed a
   phone's whole viewport width before the center column gets anything --
   stacks to one column instead, same top-to-bottom order the markup
   already has (left panel, then the dial/graph/grid/reel, then right
   panel), no reordering needed since that reading order already makes
   sense stacked. */
@media (max-width: 900px) {
	.upgrade-layout { grid-template-columns: 1fr; }
	.upgrade-center { padding-top: 16px; }
	.upgrade-side-icon { height: 90px; }
	.upgrade-side-icon .icon { font-size: 64px; }
}

/* Crash's center graph (2026-10-21, replaced a plain ring with a real
   climbing-line chart on request -- "сделай для краша примерно так же как
   на референсе" -- but in the forum's own purple/dark palette instead of
   the reference's colors). The grid is pure CSS (two repeating-linear-
   gradient layers, one per axis) -- cheaper than an SVG grid and never
   needs redrawing. The actual rising line IS an SVG <polyline> (easiest
   way to draw an arbitrary dashed path), rebuilt every frame by casino.js
   initCrashLive()'s updateGraph() from the real (time, multiplier) samples
   collected so far -- both axes rescale as the round goes on, exactly like
   a real crash game's graph, so the curve always fits the box instead of
   running off the edge on a long round. */
.crash-graph {
	position: relative; width: 100%; max-width: 420px; height: 280px; border-radius: var(--radius);
	border: 1px solid var(--border); overflow: hidden;
	background:
		repeating-linear-gradient(90deg, transparent 0 39px, var(--border-soft) 39px 40px),
		repeating-linear-gradient(0deg, transparent 0 39px, var(--border-soft) 39px 40px),
		linear-gradient(180deg, var(--card-solid), var(--bg-elevated));
}
.crash-graph-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.crash-graph-overlay {
	position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
}
.crash-ring-value { font-size: 44px; font-weight: 800; color: var(--text); line-height: 1; font-variant-numeric: tabular-nums; text-shadow: 0 2px 10px rgba(0,0,0,0.5); }

/* A single consistent slim slider look for every <input type="range"> in
   Casino (2026-10-21 polish pass) -- `accent-color` alone still leaves the
   browser's own native track/thumb SIZE and shape untouched, which reads
   as chunky/inconsistent across browsers (confirmed live, Edge screenshot:
   a thick, visibly segmented track). These pseudo-element rules replace
   the whole native look with one deliberately thin, rounded track + a
   small circular accent thumb, the same everywhere this class is used. */
.slim-range { -webkit-appearance: none; appearance: none; width: 100%; height: 4px; border-radius: 999px; background: var(--border); outline: none; cursor: pointer; margin: 0; }
.slim-range::-webkit-slider-runnable-track { height: 4px; border-radius: 999px; background: transparent; }
.slim-range::-webkit-slider-thumb {
	-webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; margin-top: -5px;
	background: var(--accent); border: 2px solid var(--bg-elevated); box-shadow: 0 0 0 1px var(--border);
}
.slim-range::-moz-range-track { height: 4px; border-radius: 999px; background: var(--border); }
.slim-range::-moz-range-progress { height: 4px; border-radius: 999px; background: var(--accent); }
.slim-range::-moz-range-thumb {
	width: 14px; height: 14px; border-radius: 50%; background: var(--accent); border: 2px solid var(--bg-elevated);
	box-shadow: 0 0 0 1px var(--border);
}

/* Recent Activity strip (2026-10-21, partials/casino_activity.ejs) -- a
   horizontally scrolling row of cards, one per resolved bet across every
   Casino mode, newest first. Lives at the bottom of every Casino page
   where there's otherwise empty space (by request -- "снизу... что была
   видна, где места много у нас"). casino.js's initCasinoActivityFeed()
   polls for NEW cards only and prepends them (its own comment has why --
   TL;DR a wholesale innerHTML swap used to reset scroll position and pop
   cards in with no transition at all, both fixed 2026-10-21). */
.casino-activity { margin-top: 32px; }
.casino-activity-title { font-size: 13px; font-weight: 700; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
.casino-activity-row {
	display: flex; gap: 12px; overflow-x: auto; padding-bottom: 10px;
	scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
/* The default OS/browser scrollbar here rendered as a stark white bar
   against the dark background (2026-10-21 screenshot) -- these rules are
   what scrollbar-width/-color above do for Firefox; Chromium/WebKit need
   the separate ::-webkit-scrollbar-* pseudo-elements instead, there's no
   single standard property that covers both yet. */
.casino-activity-row::-webkit-scrollbar { height: 6px; }
.casino-activity-row::-webkit-scrollbar-track { background: transparent; }
.casino-activity-row::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
.casino-activity-empty { color: var(--text-faint); font-size: 13px; padding: 20px 0; }
.casino-activity-card {
	flex-shrink: 0; width: 170px; display: flex; flex-direction: column; align-items: center; gap: 6px;
	padding: 16px 12px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--card-solid);
	transition: transform 0.4s ease, opacity 0.4s ease;
}
/* The "slide in" state a newly-arrived card starts in, right before
   casino.js removes this class on the next frame -- transitions animate
   FROM whatever state was already committed, so without a brief moment in
   this state first there'd be nothing to ease away from. */
.casino-activity-card-enter { opacity: 0; transform: translateX(-16px); }
.casino-activity-card.is-win { border-color: #4ade8040; }
.casino-activity-card.is-loss { border-color: #f8717140; }
.casino-activity-login { font-size: 13px; font-weight: 700; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.casino-activity-game { display: flex; align-items: center; gap: 4px; font-size: 11.5px; color: var(--text-dim); }
.casino-activity-game .icon { font-size: 14px; }
.casino-activity-bet { font-size: 11.5px; color: var(--text-faint); }
.casino-activity-result { font-size: 14px; font-weight: 800; }
.casino-activity-result.is-win { color: var(--green); }
.casino-activity-result.is-loss { color: #f87171; }

/* Shared result banner (was "roulette-result-banner" from the since-
   removed Roulette mode -- Dice still uses this generic shape, renamed
   2026-10-21 rather than leaving a Roulette-named class behind it). */
.casino-result-banner { text-align: center; font-size: 15px; }

/* Plinko (2026-10-21, replaces Roulette) -- a triangular pin field
   (plain DOM dots, one row per div, flexbox-centered so the classic
   widening-triangle shape falls out for free) with balls as separate
   absolutely-positioned circles casino.js animates row-by-row down to
   their server-decided bin, and a row of per-bin multiplier labels along
   the bottom (color-coded the same way Crash's climbing number is --
   dramatic gold at the high end, cool blue near the safe center bins). */
.plinko-board { position: relative; width: 100%; max-width: 520px; height: 400px; margin: 0 auto; }
.plinko-pins { position: absolute; inset: 0 0 60px 0; display: flex; flex-direction: column; justify-content: space-between; padding: 10px 0; }
.plinko-pins-row { display: flex; justify-content: center; gap: 26px; }
.plinko-pin { width: 7px; height: 7px; border-radius: 50%; background: var(--border); }
.plinko-balls { position: absolute; inset: 0 0 60px 0; pointer-events: none; }
.plinko-ball {
	position: absolute; top: 0; left: 50%; width: 16px; height: 16px; border-radius: 50%;
	background: var(--accent); box-shadow: 0 0 10px var(--accent); transform: translate(-50%, -50%);
	transition: left 0.18s ease, top 0.18s ease;
}
.plinko-bins { position: absolute; left: 0; right: 0; bottom: 0; height: 28px; display: flex; gap: 3px; }
.plinko-bin {
	flex: 1; display: flex; align-items: center; justify-content: center; border-radius: 6px;
	font-size: 10px; font-weight: 700; color: #fff;
}

/* Phone-width fix (2026-10-22, "оптимизируй ВСЕ игры под телефоны") -- the
   pin triangle's fixed 26px gap needs ~436px of width for the widest row
   (PLINKO_ROWS=12 -> 14 pins, 13 gaps) -- a REAL overflow on any phone, not
   just a cosmetic squeeze, since .plinko-pins-row never wraps. Shrinks the
   gap (and the board's own fixed height, which was sized for the wider
   desktop gap) below the width it actually starts clipping at. */
@media (max-width: 560px) {
	.plinko-board { height: 300px; }
	.plinko-pins-row { gap: 14px; }
}
@media (max-width: 420px) {
	.plinko-pins-row { gap: 9px; }
}

/* ---- Roulette (2026-10-21, replaces Dice) --------------------------------
   The one Casino mode that's a shared table, not a solo bet -- a countdown
   bar, a reel strip that scrolls to the winning number, and 3 betting zones
   (purple/green/black) each listing who's in them, live. Site palette
   instead of the classic red/black (by request -- "всё должно быть под
   цвета нашего форума"): var(--accent) for purple, a dark slate for black
   (pure #000 would vanish into the page background), var(--green) for the
   rare 0. */
.roulette-timer-card, .roulette-reel-card, .roulette-bet-card { padding: 18px 20px; margin-bottom: 14px; }
.roulette-timer-bar { position: relative; height: 34px; border-radius: 8px; background: var(--bg-soft, #171a2c); overflow: hidden; }
/* `transform: scaleX()`, not `width` (2026-10-21 bug -- "слева прыгают
   слоты" while the reel's own idle-scroll animation is running): animating
   `width` every ~1s is a LAYOUT property -- it forces a full style
   recalc+layout+paint pass on the main thread each tick, which can starve
   the compositor thread handling the reel's transform animation right at
   that instant and show up as the reel visibly skipping/jumping. `transform`
   is compositor-only, same category as the reel's own animation, so it
   can never block it this way. */
.roulette-timer-fill { position: absolute; inset: 0; width: 100%; transform-origin: left center; background: linear-gradient(90deg, var(--accent), #9b6cf0); transition: transform 1s linear; }
.roulette-timer-label {
	position: relative; z-index: 1; height: 100%; display: flex; align-items: center; justify-content: center;
	font-size: 14px; font-weight: 600; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.roulette-reel-viewport { position: relative; height: 72px; overflow: hidden; border-radius: 8px; background: var(--bg-soft, #171a2c); }
.roulette-reel-marker {
	position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px;
	background: #fff; z-index: 2; box-shadow: 0 0 8px #fff;
}
.roulette-reel-strip { position: absolute; top: 0; left: 50%; height: 100%; display: flex; align-items: center; will-change: transform; }
.roulette-reel-tile {
	flex: 0 0 56px; width: 56px; height: 56px; margin: 0 4px; border-radius: 8px;
	display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; color: #fff;
}
.roulette-reel-tile.is-purple { background: var(--accent); }
.roulette-reel-tile.is-black { background: #262a3e; }
.roulette-reel-tile.is-green { background: var(--green); color: #0a2e18; }
.roulette-reel-tile.is-winner { box-shadow: 0 0 0 3px #fff, 0 0 16px #fff; }
/* The idle-scroll loop (while a betting window is open) runs via the Web
   Animations API in casino.js's startIdleSpin() now, not a CSS @keyframes
   rule -- a keyframe reading its end position off a CSS custom property
   wasn't reliably compositor-only across browsers and visibly stuttered
   (2026-10-21 bug -- "во время ожидания она как будто пролагивает"). */

.roulette-colors { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 8px; }
.roulette-color-col {
	background: var(--card-solid); border: 1px solid var(--border-soft); border-radius: var(--radius);
	padding: 14px; display: flex; flex-direction: column; gap: 10px; transition: box-shadow 0.3s ease;
}
.roulette-color-col.is-winner { box-shadow: 0 0 0 2px #fff, 0 0 24px rgba(255,255,255,0.35); }
.roulette-color-btn {
	border: none; border-radius: 8px; padding: 14px 10px; font-size: 15px; font-weight: 700; color: #fff;
	cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: filter 0.15s ease, transform 0.1s ease;
}
.roulette-color-btn:hover { filter: brightness(1.1); }
.roulette-color-btn:active { transform: scale(0.98); }
.roulette-color-btn:disabled { opacity: 0.5; cursor: not-allowed; filter: none; transform: none; }
.roulette-color-btn.is-selected { box-shadow: 0 0 0 2px #fff inset; }
.roulette-color-purple { background: var(--accent); }
.roulette-color-black { background: #262a3e; }
.roulette-color-green { background: var(--green); color: #0a2e18; }
.roulette-color-mult { font-size: 12px; font-weight: 600; opacity: 0.85; }
.roulette-color-total { font-size: 13px; color: var(--text-dim); text-align: center; }
.roulette-bettors { display: flex; flex-direction: column; gap: 6px; max-height: 220px; overflow-y: auto; }
.roulette-bettor-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.roulette-bettor-login { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roulette-bettor-amount { color: var(--text-dim); font-weight: 600; }
.roulette-bettor-row.is-win .roulette-bettor-amount { color: var(--green); }
.roulette-bettor-row.is-loss .roulette-bettor-amount { color: #f87171; }
@media (max-width: 720px) {
	.roulette-colors { grid-template-columns: 1fr; }
}

/* ---- Jackpot (2026-10-21) -------------------------------------------------
   Reuses Roulette's timer-card/reel-card/bet-card chrome (same shared-table
   look) -- the reel itself shows PLAYER AVATARS, not numbered tiles (by
   request -- "прокрут аватарок игроков как в рулетке"), and there's one
   pot/participant list instead of 3 color columns, since every bet here
   goes into the same shared pot. */
/* Bigger + longer-spinning than Roulette's reel (2026-10-21, by request --
   "растяни прокрутку... подольше в 2-3 раза и более азартной") -- the
   JACKPOT_TILE_WIDTH constant in casino.js's initJackpotLive() MUST match
   this tile's total footprint (76px + 2*6px margin = 88px) or the landing
   math drifts off-center. */
.jackpot-reel-viewport { height: 112px; }
.jackpot-reel-tile {
	flex: 0 0 76px; width: 76px; height: 76px; margin: 0 6px; border-radius: 50%;
	display: flex; align-items: center; justify-content: center; overflow: hidden;
	background: var(--card-solid); border: 3px solid var(--border-soft);
	transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.jackpot-reel-tile img { width: 100%; height: 100%; object-fit: cover; }
.jackpot-reel-tile.is-winner { border-color: #fff; box-shadow: 0 0 0 4px #fff, 0 0 24px #fff; }
/* Idle placeholder -- no real bets to show yet, so the reel isn't just an
   empty void (2026-10-21, by request -- "когда ставок нет... пустой тёмный
   бар... нету сегментов"). Faint dashed slots with a slow pulse, not real
   content, just communicating "waiting for players" instead of nothing. */
.jackpot-reel-tile.is-placeholder {
	border-style: dashed; border-color: var(--border); background: transparent;
	color: var(--text-faint); font-size: 28px;
	animation: jackpot-placeholder-pulse 2.4s ease-in-out infinite;
}
.jackpot-reel-tile.is-placeholder:nth-child(3n+1) { animation-delay: 0.3s; }
.jackpot-reel-tile.is-placeholder:nth-child(3n+2) { animation-delay: 0.9s; }
@keyframes jackpot-placeholder-pulse {
	0%, 100% { opacity: 0.3; }
	50% { opacity: 0.8; }
}
/* Pulsing glow on the marker while the draw is actually spinning (2026-10-21,
   "более азартной") -- toggled via .is-spinning on .jackpot-reel-viewport
   from casino.js, purely cosmetic suspense, no effect on the real draw. */
.jackpot-reel-viewport.is-spinning .roulette-reel-marker { animation: jackpot-marker-pulse 0.6s ease-in-out infinite; }
@keyframes jackpot-marker-pulse {
	0%, 100% { box-shadow: 0 0 8px #fff; }
	50% { box-shadow: 0 0 20px 4px #fff; }
}
.jackpot-pot-card { padding: 18px 20px; }
.jackpot-pot-header { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 12px; }
.jackpot-pot-count { font-size: 13px; color: var(--text-dim); }
/* The big gold hero pot number at the top of the page (2026-10-21, by
   request -- "банк... крупным, золотистым/светящимся акцентом сверху") --
   the ONE thing every participant is actually here for, so it leads the
   page instead of being a small text line inside the participant card. */
.jackpot-pot-hero { text-align: center; margin-bottom: 14px; }
.jackpot-pot-hero-label { font-size: 13px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px; }
.jackpot-pot-hero-value {
	font-size: 40px; font-weight: 800; color: #ffd76a;
	text-shadow: 0 0 20px rgba(255, 215, 106, 0.5), 0 0 48px rgba(255, 215, 106, 0.25);
}
@media (max-width: 560px) {
	.jackpot-pot-hero-value { font-size: 30px; }
}
.jackpot-participants { display: flex; flex-direction: column; gap: 8px; }
.jackpot-participant-row {
	display: flex; align-items: center; gap: 10px; padding: 8px 10px;
	background: var(--card-solid); border: 1px solid var(--border-soft); border-radius: var(--radius-sm);
}
.jackpot-participant-login { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.jackpot-participant-bet { color: var(--text-dim); font-size: 13px; }
.jackpot-participant-pct { font-weight: 700; color: var(--accent); min-width: 48px; text-align: right; }
.jackpot-participant-row.is-winner { box-shadow: 0 0 0 2px #fff; }
.jackpot-empty { text-align: center; color: var(--text-dim); font-size: 14px; padding: 12px 0; }

/* Dice -- a horizontal win-zone bar (the colored half matches the chosen
   direction/chance) with a dot marking where the roll actually landed. */
.dice-bar { position: relative; height: 36px; border-radius: 999px; background: var(--bg); overflow: hidden; }
.dice-bar-zone { position: absolute; top: 0; bottom: 0; }
.dice-bar-zone-under { background: var(--accent-soft); }
.dice-bar-zone-over { background: var(--accent-soft); }
.dice-bar-marker {
	position: absolute; top: -4px; width: 4px; height: 44px; background: #fff; border-radius: 2px;
	box-shadow: 0 0 6px rgba(255,255,255,0.7); transform: translateX(-50%);
}

/* Mines -- 5x5 grid, each cell a plain toggle button until clicked. */
/* `width: 100%` matters here, not just max-width -- .mines-grid sits
   inside .upgrade-center, a flex column with align-items:center, which
   shrinks a child with no explicit width down to its own min-content size
   (tiny, since a 5-column 1fr grid has almost no intrinsic minimum) --
   2026-10-21 bug, the whole grid rendered at a fraction of its intended
   size. `width: 100%` makes it actually fill up to max-width instead. */
.mines-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; width: 100%; max-width: 380px; margin: 0 auto; }
.mines-cell {
	aspect-ratio: 1; border-radius: 10px; border: 1px solid var(--border-soft); background: var(--bg-elevated);
	font-size: 20px; cursor: pointer; color: var(--text);
}
.mines-cell:disabled { cursor: default; }
.mines-cell:hover:not(:disabled) { border-color: var(--accent); }
.mines-cell-revealed { background: var(--accent-soft); color: var(--accent); }
.mines-cell-bomb { background: #c0392b; color: #fff; }
/* Shown on a WIN (cash-out), not a loss -- same bombs revealed purely out
   of curiosity (2026-10-21, by request), so a neutral/muted tone instead
   of the alarming red .mines-cell-bomb uses for the one actually hit. */
.mines-cell-bomb-revealed { background: var(--bg-elevated); color: var(--text-faint); border-color: var(--border); }

