/* f99c3bec37 */
/*
 * The same nine self-hosted skin fonts as the portal's
 * apps/core/app/assets/css/main.css, copied into fonts/ by
 * scripts/sync-fonts.sh so a shell never talks to Google's font servers at
 * request time. Latin subset only (unicode-range copied from Google's own
 * latin block, identical across every face here) -- a shell never renders
 * latin-ext text.
 *
 * Load this before tokens.css: tokens.css's --font-display/--font-body
 * name these families, so the @font-face rules need to exist first.
 */
@font-face {
	font-family: 'Cinzel';
	font-style: normal;
	font-weight: 500;
	font-display: swap;
	src: url('/a/f/f99c3bec37-cinzel-500.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Cinzel';
	font-style: normal;
	font-weight: 700;
	font-display: swap;
	src: url('/a/f/f99c3bec37-cinzel-700.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Spectral SC';
	font-style: normal;
	font-weight: 500;
	font-display: swap;
	src: url('/a/f/f99c3bec37-spectral-sc-500.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Spectral SC';
	font-style: normal;
	font-weight: 700;
	font-display: swap;
	src: url('/a/f/f99c3bec37-spectral-sc-700.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Roboto Condensed';
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url('/a/f/f99c3bec37-roboto-condensed-400.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Roboto Condensed';
	font-style: normal;
	font-weight: 700;
	font-display: swap;
	src: url('/a/f/f99c3bec37-roboto-condensed-700.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Source Sans 3';
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url('/a/f/f99c3bec37-source-sans-3-400.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Source Sans 3';
	font-style: normal;
	font-weight: 600;
	font-display: swap;
	src: url('/a/f/f99c3bec37-source-sans-3-600.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: 'Source Sans 3';
	font-style: normal;
	font-weight: 700;
	font-display: swap;
	src: url('/a/f/f99c3bec37-source-sans-3-700.woff2') format('woff2');
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/*
 * The five custom properties every shell-kit component and every shell's own
 * CSS is written against. These are the RiFT defaults, copied from the RiFT
 * brand palette so a shell looks right even before any brand-specific
 * override runs.
 *
 * A shell overrides them per its own brand two ways: statically, by
 * redeclaring the ones it wants under `:root` in its own stylesheet loaded
 * after this file; or at runtime, by shipping a local `brand.json` with a
 * `theme` object and calling `applyTheme(json)` from js/brand.js, which sets
 * these same properties on `document.documentElement.style`.
 *
 * --font-display and --font-body are the same idea, one level simpler: a
 * shell's own brand.json carries a `fonts` key and
 * apps/brand-rift/scripts/stamp-brand.mjs writes these two values in
 * straight from it, no runtime JS involved. The defaults below are RiFT's
 * own stacks (the portal's ember skin), same reasoning as the colors above:
 * a shell with no `fonts` key in its brand.json still renders with a real
 * typeface rather than a bare system fallback.
 */

:root {
	--portal-primary: #4C3F7A;
	--portal-surface: #14122A;
	--portal-text: #ECE8F7;
	--portal-muted: #9B93B8;
	--portal-accent: #E2B15E;
	/* Text on top of --portal-primary (buttons). Kept separate from
	   --portal-text and --portal-surface, which describe the page, not the
	   button: a dark page surface on a saturated red made the label unreadable. */
	--portal-on-primary: #F6F3FF;
	--font-display: "Cinzel", Georgia, serif;
	--font-body: "Source Sans 3", Verdana, sans-serif;
}

/*
 * Base layout and components for a brand shell. Loaded after tokens.css.
 * Every color here is one of the five --portal-* variables -- nothing in
 * this file names a color directly, so a shell can restyle itself entirely
 * by overriding tokens.css.
 */

* {
	box-sizing: border-box;
}

body {
	margin: 0;
	background: var(--portal-surface);
	color: var(--portal-text);
	font-family: var(--font-body);
	line-height: 1.5;
}

a {
	color: var(--portal-accent);
}

h1,
h2,
h3 {
	font-family: var(--font-display);
}

.bf99-shell {
	max-width: 960px;
	margin: 0 auto;
	padding: 0 1.5rem;
}

/* Header nav */

/* The frame: header pinned at the top, footer at the bottom, and only the
   middle (banner plus content, or the centred auth card) scrolls. */
body {
	height: 100svh;
	display: flex;
	flex-direction: column;
	overflow: hidden;
}

header,
footer {
	flex: none;
}

.bf99-page-scroll,
.bf99-auth-page .bf99-auth-main {
	flex: 1;
	min-height: 0;
	overflow-y: auto;
}

header {
	border-bottom: 1px solid color-mix(in oklch, var(--portal-text) 12%, transparent);
}

header .bf99-shell {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	padding-block: 1rem;
}

header nav {
	display: flex;
	flex-wrap: wrap;
	gap: 1.25rem;
	width: 100%;
}

/* Links marked side: right in brand.json (Register, Sign in) sit at the far
   edge; the first of them takes the slack after the left group. */
header nav a.bf99-nav-left + a.bf99-nav-right {
	margin-left: auto;
}

header nav a {
	color: var(--portal-text);
	font-family: var(--font-display);
	text-decoration: none;
	opacity: 0.75;
}

header nav a:hover {
	opacity: 1;
}

header nav a[aria-current='page'] {
	opacity: 1;
	color: var(--portal-accent);
	font-weight: 600;
}

/* Buttons */

.bf99-btn {
	display: inline-block;
	padding: 0.6rem 1.2rem;
	border-radius: 0.4rem;
	border: 1px solid color-mix(in oklch, var(--portal-text) 25%, transparent);
	color: var(--portal-text);
	font-family: var(--font-display);
	text-decoration: none;
	font-weight: 600;
}

.bf99-btn-primary {
	background: var(--portal-primary);
	border-color: var(--portal-primary);
	color: var(--portal-on-primary);
}

/* Cards */

.bf99-card {
	background: color-mix(in oklch, var(--portal-text) 6%, var(--portal-surface));
	border: 1px solid color-mix(in oklch, var(--portal-text) 12%, transparent);
	border-radius: 0.5rem;
	padding: 1.25rem;
}

/* Hero */

.bf99-hero-image {
	min-height: 320px;
	padding: 0;
	background: var(--brand-hero, none) center / cover no-repeat;
	border-radius: 6px;
}

/* Image-only banner: no overlay at all. Written with both classes so it
   outranks .bf99-hero::before below, which is declared later. */
.bf99-hero.bf99-hero-image::before {
	content: none;
}

.bf99-hero {
	position: relative;
	overflow: hidden;
	background: var(--brand-hero, none) center / cover no-repeat;
	min-height: 320px;
	display: flex;
	flex-direction: column;
	justify-content: center;
}

.bf99-hero::before {
	content: '';
	position: absolute;
	inset: 0;
	background: linear-gradient(to bottom, rgb(0 0 0 / 0.35), rgb(0 0 0 / 0.6));
}

.bf99-hero > * {
	position: relative;
}

/* Auth forms (login/register) */

/* A card that wraps a .bf99-form caps at a comfortable reading width and centers
   in whatever wider column its page puts it in, rather than stretching to
   main.bf99-shell's full width the way a plain .bf99-card does. */
.bf99-card:has(> .bf99-form) {
	max-width: 28rem;
	margin-inline: auto;
}

/* An auth-only page (login, register): the body itself is the whole layout,
   set by templates/<template>/auth.html, no header/nav/hero/footer at all --
   just the one .bf99-card, centred. */
.bf99-auth-page .bf99-auth-main {
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 1.5rem;
}

.bf99-form {
	display: flex;
	flex-direction: column;
	gap: 1.1rem;
}

.bf99-field {
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
}

.bf99-field label {
	font-family: var(--font-display);
	font-weight: 600;
	font-size: 0.9rem;
}

.bf99-field input,
.bf99-field select {
	width: 100%;
	padding: 0.6rem 0.75rem;
	border-radius: 0.4rem;
	border: 1px solid color-mix(in oklch, var(--portal-text) 25%, transparent);
	background: var(--portal-surface);
	color: var(--portal-text);
	font-family: var(--font-body);
	font-size: 1rem;
}

.bf99-field input:focus,
.bf99-field select:focus {
	outline: 2px solid var(--portal-accent);
	outline-offset: 1px;
}

.bf99-form button[type='submit'] {
	width: 100%;
}

/* Error slots stay in the DOM empty (no `hidden`, so a screen reader's
   aria-describedby reference always resolves) until auth.js writes a
   message into their textContent; visibility here tracks :not(:empty)
   rather than an attribute auth.js would have to toggle. A fixed red, not
   one of the five --portal-* tokens: an error has to read as a problem
   regardless of a brand's own accent hue (the same reasoning as
   .bf99-hero::before's fixed black overlay above). */
.bf99-field .bf99-error,
.bf99-form .bf99-error-form {
	display: none;
	margin: 0;
	color: oklch(0.65 0.2 25);
	font-size: 0.85rem;
}

.bf99-field .bf99-error:not(:empty),
.bf99-form .bf99-error-form:not(:empty) {
	display: block;
}

.bf99-form .bf99-error-form:not(:empty) {
	font-weight: 600;
}

/* Status */

.bf99-status {
	color: var(--portal-muted);
}

.bf99-status .bf99-is-up {
	color: var(--portal-accent);
}

.bf99-status .bf99-is-down {
	color: var(--portal-muted);
	opacity: 0.7;
}

@media (max-width: 640px) {
	header .bf99-shell {
		flex-direction: column;
		align-items: flex-start;
	}

	header nav {
		gap: 0.75rem;
	}

	.bf99-hero {
		min-height: 220px;
	}
}

/*
 * onepage template: everything a visitor needs on the home page -- hero,
 * rates, registration, sign-in and Discord -- so nobody has to navigate to
 * make an account. The other pages fall back to a short titled banner and
 * their own markdown, exactly like the hero template.
 *
 * Colours come from the --portal-* tokens, so this reads correctly for any
 * brand without editing. The only fixed colours are the scrim over the hero
 * image and the white on top of it, which must survive any photograph, and
 * Discord's own blurple, which is a third-party brand colour rather than
 * ours.
 *
 * Motion is deliberately small and all of it is disabled under
 * prefers-reduced-motion: a games site that lurches is worse than a still one.
 *
 * WHERE TO PUT RO SPRITES OR GIFS: drop them in site/img/ and add a rule for
 * .op-hero-inner::after (or a new element) with a background-image. The hero
 * is a positioned grid, so a decorative sprite can be parked in any corner
 * without disturbing the layout. Nothing is referenced by default, because a
 * missing file would 404 on every page load.
 */
:root {
	--t-line: color-mix(in oklch, var(--portal-text) 14%, transparent);
	--t-glow: color-mix(in oklch, var(--portal-accent) 55%, transparent);
	--t-card: color-mix(in oklch, var(--portal-surface) 88%, white 4%);
	--t-discord: #5865f2;
}

/* ---- the frame ---------------------------------------------------------- */
/* shell.css pins the body to `height: 100svh; overflow: hidden` and expects a
   .bf99-page-scroll wrapper to be the one thing that scrolls. This template
   deliberately has no such wrapper (a one-page site wants the document itself
   to scroll), so without this override the body was a fixed-height box with
   overflow hidden and NOTHING scrolled: every page was silently truncated at
   the fold. It showed up first on /server-info, whose tables run past it.
   Height becomes auto so the body grows with its content. */
.bf99-t-onepage {
	height: auto;
	min-height: 100svh;
	overflow: visible;
}

/* ---- header ------------------------------------------------------------ */
/* Sticky, because the document scrolls now rather than an inner pane. */
.bf99-t-onepage header { position: sticky; top: 0; z-index: 10; }
.bf99-t-onepage header {
	background: color-mix(in oklch, var(--portal-surface) 82%, black);
	border-bottom: 1px solid var(--t-line);
	backdrop-filter: blur(6px);
}
/* One centred group instead of shell.css's split bar. shell.css lays the nav
   out as left links + right links pushed to the far edge by a margin-left:auto
   on the first .bf99-nav-right; both halves of that have to be undone here, or the
   auto margin keeps eating the slack and the "centred" row sits off to one
   side. brand.json still says which links are which, so the ordering survives
   and only the placement changes. */
.bf99-t-onepage header nav {
	justify-content: center;
	gap: 1.75rem;
}
.bf99-t-onepage header nav a.bf99-nav-left + a.bf99-nav-right { margin-left: 0; }

.bf99-t-onepage header nav a {
	color: var(--portal-text);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	font-size: 0.82rem;
}
.bf99-t-onepage header nav a[aria-current='page'] { color: var(--portal-accent); }

/* ---- hero: copy on the left, auth panel on the right ------------------ */
.bf99-op-hero {
	position: relative;
	/* Not 100vh: where a page has content below, a sliver of the next section
	   showing is what tells a visitor to scroll. The home page is the
	   exception and overrides this below, because nothing follows it there. */
	min-height: 88vh;
	padding: 5rem 1rem 3rem;
	isolation: isolate;
	display: grid;
	align-items: center;
	background: var(--brand-hero) center / cover no-repeat;
}
/* Scrim. Darkest on the right so the panel has something to sit on, lighter
   on the left so the artwork still reads behind the brand name. */
.bf99-op-hero::before {
	content: '';
	position: absolute;
	inset: 0;
	z-index: -1;
	/* Left-to-right only. The generated hero already fades to dark at its own
	   foot, and a second bottom fade here stacked with it into a grey band
	   above the footer. One layer owns the fade: the image. */
	background: linear-gradient(to right, rgb(0 0 0 / 0.5) 0%, rgb(0 0 0 / 0.22) 40%, rgb(0 0 0 / 0.7) 100%);
}
.bf99-op-hero-grid {
	width: min(72rem, 100%);
	margin-inline: auto;
	display: grid;
	gap: 2.5rem;
	grid-template-columns: 1fr minmax(20rem, 24rem);
	align-items: center;
}
/* NOT justify-items:start. That sizes each row to its own content, which
   collapsed the rate chips into a vertical stack instead of a row. Items
   stretch to the column, and the flex rows inside lay out horizontally. */
.bf99-op-hero-copy { display: grid; gap: 1rem; }

.bf99-op-name {
	font-family: var(--font-display);
	font-size: clamp(2.4rem, 6vw, 4.6rem);
	line-height: 0.95;
	margin: 0;
	color: #fff;
	text-shadow: 0 2px 24px var(--t-glow), 0 1px 2px rgb(0 0 0 / 0.6);
}
.bf99-op-tag { margin: 0; font-size: clamp(1rem, 1.6vw, 1.2rem); color: rgb(255 255 255 / 0.9); max-width: 34rem; }

/* NOTE: .bf99-op-rates and .bf99-op-cta are wrapped in their own .bf99-home-only div rather
   than carrying the class themselves. `.bf99-page-home .bf99-home-only { display: revert }`
   outranks a single-class rule and would reset these back to block, which
   stacked the rate chips into a column. The hero template wraps for the same
   reason. */
/* ---- rate chips -------------------------------------------------------- */
/* The info container: rates, version and freebies as one card on the banner.
   Chunky border and a hard offset shadow rather than a soft one -- that is
   what reads as "game UI panel" instead of "web card". */
.bf99-op-info {
	display: grid;
	gap: 0.9rem;
	padding: 1rem 1.1rem;
	max-width: 30rem;
	border: 2px solid rgb(255 255 255 / 0.28);
	border-radius: 1rem;
	background: rgb(10 10 22 / 0.55);
	backdrop-filter: blur(8px);
	box-shadow: 6px 6px 0 rgb(0 0 0 / 0.35);
}
.bf99-op-rates { display: flex; flex-wrap: wrap; gap: 0.7rem; }

.bf99-op-highlights { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.bf99-op-highlights li {
	display: grid;
	grid-template-columns: 1.6rem auto 1fr;
	align-items: baseline;
	gap: 0.5rem;
	font-size: 0.92rem;
	color: rgb(255 255 255 / 0.92);
}
.bf99-op-hl-icon { font-size: 1.05rem; line-height: 1; }
.bf99-op-highlights b { font-family: var(--font-display); color: var(--portal-accent); }
.bf99-op-hl-val { color: rgb(255 255 255 / 0.72); }
.bf99-op-rate {
	display: grid;
	gap: 0.1rem;
	min-width: 6rem;
	padding: 0.65rem 1rem;
	border: 1px solid rgb(255 255 255 / 0.22);
	border-radius: 0.6rem;
	background: rgb(0 0 0 / 0.38);
	backdrop-filter: blur(4px);
}
.bf99-op-rate b { font-family: var(--font-display); font-size: 1.5rem; color: var(--portal-accent); line-height: 1; }
.bf99-op-rate span { font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; color: rgb(255 255 255 / 0.72); }

/* ---- buttons and lights ------------------------------------------------ */
.bf99-op-cta { display: flex; flex-wrap: wrap; gap: 0.6rem; }
/* On the hero the buttons sit on a photograph, so they need their own
   contrast rather than the page defaults, which assume a flat surface. */
.bf99-op-hero .bf99-btn { color: #fff; border-color: rgb(255 255 255 / 0.5); background: rgb(0 0 0 / 0.42); backdrop-filter: blur(4px); }
.bf99-op-hero .bf99-btn:hover { border-color: var(--portal-accent); }
.bf99-op-hero .bf99-btn-discord { background: var(--t-discord); border-color: var(--t-discord); }
.bf99-t-onepage .bf99-btn { transition: transform 0.12s ease, box-shadow 0.12s ease; }
.bf99-t-onepage .bf99-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgb(0 0 0 / 0.35); }
.bf99-btn-discord { background: var(--t-discord); border-color: var(--t-discord); color: #fff; }
.bf99-btn-wide { display: block; width: 100%; text-align: center; }
/* Page-scope switches. These are defined per template, not in shell.css, so
   every template must carry its own copy -- without them the home page shows
   both the brand name AND the page title. */
.bf99-home-only { display: none; }
.bf99-page-home .bf99-home-only { display: revert; }
.bf99-page-home .bf99-not-home { display: none; }

/* Status lights. Also template-scoped: shell.css only colours .bf99-status, the
   layout and the dot belong to whoever is drawing them. */
.bf99-op-lights { margin-top: 0.2rem; }
.bf99-status-lights { display: inline-flex; gap: 1.2rem; flex-wrap: wrap; font-size: 0.9rem; }
.bf99-status-lights [data-status]::before {
	content: '';
	display: inline-block;
	width: 0.5rem;
	height: 0.5rem;
	border-radius: 50%;
	background: currentColor;
	margin-right: 0.35rem;
	vertical-align: middle;
}
/* status.js sets .bf99-is-up / .bf99-is-down as CLASSES; there is no data-up attribute,
   so a selector written against one silently never matches.

   Green and red rather than shell.css's accent-vs-muted. The dot is
   background: currentColor, so colouring the label colours the dot with it.
   Accent for "up" made every light gold on this brand, which reads as a
   warning rather than as healthy, and left "down" distinguishable only by
   being slightly dimmer. These two are fixed colours, like the scrim: a
   status light means the same thing on every brand, so it must not follow a
   palette. Both are picked to clear 4.5:1 on the hero's darkened artwork. */
.bf99-op-hero .bf99-status-lights { color: rgb(255 255 255 / 0.75); }
.bf99-op-hero .bf99-status-lights .bf99-is-up { color: #5FD98B; }
.bf99-op-hero .bf99-status-lights .bf99-is-down { color: #F2726F; opacity: 1; }
.bf99-op-hero .bf99-status-lights .bf99-is-up::before { animation: op-pulse 2.4s ease-in-out infinite; }
@keyframes op-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

/* ---- the switchable panel ---------------------------------------------- */
.bf99-op-panel {
	background: color-mix(in oklch, var(--portal-surface) 82%, black 8%);
	border: 1px solid var(--t-line);
	border-radius: 0.9rem;
	padding: 1.4rem;
	box-shadow: 0 18px 50px rgb(0 0 0 / 0.45);
	backdrop-filter: blur(10px);
}
/* The radios drive everything and are never seen. Not display:none -- that
   takes them out of the tab order and the keyboard could no longer switch. */
.bf99-op-tab-input {
	position: absolute;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}
.bf99-op-tabs {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 0.3rem;
	margin-bottom: 1.2rem;
	padding: 0.25rem;
	border-radius: 0.6rem;
	background: rgb(0 0 0 / 0.3);
}
.bf99-op-tab {
	text-align: center;
	padding: 0.55rem 0.5rem;
	border-radius: 0.45rem;
	cursor: pointer;
	font-size: 0.85rem;
	letter-spacing: 0.03em;
	text-transform: uppercase;
	color: var(--portal-muted);
	transition: background 0.15s ease, color 0.15s ease;
}
.bf99-op-tab:hover { color: var(--portal-text); }

/* Panes are hidden by default and revealed by whichever radio is checked.
   Sibling selectors, so the radios must stay before the panes in the markup. */
.bf99-op-pane { display: none; }
#op-tab-register:checked ~ .bf99-op-pane-register,
#op-tab-login:checked ~ .bf99-op-pane-login { display: block; }

#op-tab-register:checked ~ .bf99-op-tabs [for='op-tab-register'],
#op-tab-login:checked ~ .bf99-op-tabs [for='op-tab-login'] {
	background: var(--portal-primary);
	color: var(--portal-on-primary);
}
/* Focus has to be drawn on the label, because the radio itself is invisible.
   Without this the keyboard user has no idea where they are. */
.bf99-op-tab-input:focus-visible ~ .bf99-op-tabs [for='op-tab-register'],
.bf99-op-tab-input:focus-visible ~ .bf99-op-tabs [for='op-tab-login'] { outline: 2px solid var(--portal-accent); outline-offset: 2px; }

.bf99-op-panel .bf99-field { margin-bottom: 0.7rem; }
.bf99-op-fine { font-size: 0.8rem; color: var(--portal-muted); margin: 0.9rem 0 0; }
/* This template shows the page surface directly under the hero, where the hero
   template never does because it fills the viewport. So a brand using it wants
   a DARK surface in its brand.json theme: with a light one the page flips from
   a dark hero to a cream band at the join. Nothing here can fix that, because
   the colour is the brand's to choose -- shell.css already paints the body from
   --portal-surface.

   There is no footer on this template: a one-page site ends on its content,
   and a copyright strip under it was the only thing below the fold on the
   short pages. shell.css's own footer rules simply never match. */

/* ---- interior pages ---------------------------------------------------- */
/* 88vh is sized for the home page, where the auth panel fills the hero. Every
   other page puts a title in it and nothing else, so at that height the first
   sentence of actual content started a full screen below the fold and the page
   read as empty. Sized to the title instead. */
body:not(.bf99-page-home) .bf99-op-hero { min-height: 34vh; padding-block: 4.5rem 2.5rem; }

/* The home page has no <main> at all (layout.html omits it when a page has no
   body), so 88vh left a band of bare surface between the foot of the hero and
   the foot of the window. The body is a flex column, so growing the hero to
   take what the sticky header leaves fills it exactly, with no header height
   guessed anywhere. */
.bf99-page-home .bf99-op-hero { flex: 1; }
body:not(.bf99-page-home) .bf99-op-hero-grid { grid-template-columns: 1fr; }

.bf99-op-main { padding-block: 3rem 4rem; }

/* The download page's own two columns: steps on the left, the client button
   and the troubleshooting note on the right. Grid rather than multicol,
   because here the split is EDITORIAL -- the markdown wraps each side in a
   .bf99-dl-col by hand -- and multicol would fragment the numbered list wherever it
   happened to run out of room instead. */
.bf99-op-main .bf99-dl-grid {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 0 3.5rem;
	align-items: start;
}
/* Both columns start flush with each other: the left one opens on a heading
   carrying a top margin, the right one on the button, and without this they
   sat at different heights. */
.bf99-op-main .bf99-dl-col > :first-child { margin-top: 0; }
.bf99-op-main .bf99-dl-col .bf99-btn-row { margin-bottom: 2rem; }

/* Two columns, and ONLY on the page built out of short independent tables.
   The markdown gives a flat run of h2 + table siblings with no wrapper per
   section, so a grid cannot group them; multicol can, provided the breaks are
   spelled out below, or a column boundary lands between a title and the table
   it names.

   Not applied site-wide, because multicol fragments any block it has to: on
   the download page it split the numbered walkthrough down the middle, putting
   steps 1-2 in one column and 3-4 in the other, which reads as two lists
   rather than one sequence. A page of prose or instructions wants one column. */
.bf99-page-server-info .bf99-op-main {
	columns: 2;
	column-gap: 3.5rem;
}
.bf99-op-main h2 {
	font-family: var(--font-display);
	color: var(--portal-accent);
	margin-top: 2.5rem;
	display: flex;
	align-items: center;
	gap: 0.75rem;
	break-inside: avoid;
	break-after: avoid;
}
/* The first heading in each column sits at the column top, so its margin would
   show as a ragged offset between the two. */
.bf99-op-main h2:first-child { margin-top: 0; }
/* The rule that trails each heading: what makes a run of sections read as
   panels in an interface rather than as a stack of prose. */
.bf99-op-main h2::after {
	content: '';
	flex: 1;
	height: 1px;
	background: linear-gradient(to right, color-mix(in oklch, var(--portal-accent) 45%, transparent), transparent);
}
.bf99-op-main p { max-width: 46rem; color: color-mix(in oklch, var(--portal-text) 88%, transparent); }
.bf99-op-main a { color: var(--portal-accent); }
.bf99-op-main ol, .bf99-op-main ul, .bf99-op-main blockquote { break-inside: avoid; }
.bf99-op-main ol, .bf99-op-main ul { max-width: 46rem; color: color-mix(in oklch, var(--portal-text) 88%, transparent); line-height: 1.7; }
.bf99-op-main ol li, .bf99-op-main ul li { margin-block: 0.5rem; }
.bf99-op-main ol li::marker { color: var(--portal-accent); font-weight: 700; }

/* ---- stat tables as game panels ---------------------------------------- */
/* Markdown tables are the whole of the server-info page. Left at browser
   defaults they were four unstyled grids on a dark ground; framed like this
   they read as the stat panels the page is describing. */
.bf99-op-main table {
	width: 100%;
	margin: 1.25rem 0 2rem;
	break-inside: avoid;
	break-before: avoid;
	border-collapse: separate;
	border-spacing: 0;
	border: 1px solid color-mix(in oklch, var(--portal-accent) 28%, transparent);
	border-radius: 0.6rem;
	overflow: hidden;
	background: color-mix(in oklch, var(--portal-surface) 84%, black);
	box-shadow: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 8px 24px rgb(0 0 0 / 0.28);
}
.bf99-op-main thead th {
	font-family: var(--font-display);
	font-size: 0.78rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	text-align: left;
	color: var(--portal-accent);
	background: color-mix(in oklch, var(--portal-accent) 12%, transparent);
	padding: 0.7rem 1rem;
	border-bottom: 1px solid color-mix(in oklch, var(--portal-accent) 26%, transparent);
}
/* The header follows its column: the value column is right-aligned below, so
   a left-aligned VALUE label floated loose in the middle of the panel. */
.bf99-op-main thead th:last-child { text-align: right; }
.bf99-op-main tbody td { padding: 0.65rem 1rem; border-top: 1px solid var(--t-line); }
.bf99-op-main tbody tr:first-child td { border-top: 0; }
.bf99-op-main tbody tr:nth-child(even) { background: rgb(255 255 255 / 0.02); }
/* The value column is the number you came to read: it gets the weight. */
.bf99-op-main tbody td:last-child { text-align: right; font-weight: 700; color: var(--portal-text); font-variant-numeric: tabular-nums; }
.bf99-op-main tbody td:first-child { color: var(--portal-muted); }

/* ---- callout ------------------------------------------------------------ */
.bf99-op-main blockquote {
	max-width: 46rem;
	margin: 1.25rem 0 0;
	padding: 1rem 1.25rem;
	border: 1px solid color-mix(in oklch, var(--portal-accent) 22%, transparent);
	border-left-width: 3px;
	border-radius: 0.5rem;
	background: color-mix(in oklch, var(--portal-surface) 86%, black);
	color: color-mix(in oklch, var(--portal-text) 86%, transparent);
}
.bf99-op-main blockquote p { margin: 0; }
.bf99-op-main code { background: rgb(255 255 255 / 0.07); padding: 0.1em 0.4em; border-radius: 0.3rem; font-size: 0.92em; }

/* ---- auth pages (/login, /register) ------------------------------------ */
/* These two are served by the brand shell, not the core, so they render
   through auth.html rather than layout.html and were the only pages with no
   artwork at all. Same image, same scrim, card centred on it. */
.bf99-t-onepage.bf99-auth-page {
	min-height: 100vh;
	display: flex;
	flex-direction: column;
	background: var(--brand-hero) center / cover no-repeat fixed;
}
.bf99-t-onepage.bf99-auth-page::before {
	content: '';
	position: fixed;
	inset: 0;
	z-index: 0;
	background: linear-gradient(to bottom, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.72));
}
.bf99-t-onepage.bf99-auth-page > * { position: relative; z-index: 1; }
.bf99-t-onepage.bf99-auth-page header { background: transparent; border-bottom-color: rgb(255 255 255 / 0.12); }
/* Written with three classes on purpose. shell.css centres this with
   `.bf99-auth-page .bf99-auth-main { display: flex }` at 0,2,0; a bare `.bf99-auth-main` here
   is 0,1,0 and LOSES, which left display:flex in force while the grid
   properties below it did nothing. The card then sat wherever the flex row put
   it rather than in the middle. Anything scoped to this element has to carry
   at least three classes to take effect. */
.bf99-t-onepage.bf99-auth-page .bf99-auth-main {
	flex: 1;
	display: flex;
	align-items: center;
	justify-content: center;
	padding-block: 3rem;
}
/* The form card itself: same panel treatment as the hero's auth panel, so the
   standalone pages and the home-page tabs are recognisably one design. */
.bf99-auth-page .bf99-card {
	width: min(26rem, 100%);
	padding: 1.75rem;
	border: 1px solid rgb(255 255 255 / 0.16);
	border-radius: 0.9rem;
	background: color-mix(in oklch, var(--portal-surface) 82%, transparent);
	backdrop-filter: blur(10px);
	box-shadow: 0 18px 50px rgb(0 0 0 / 0.45);
}
.bf99-auth-page .bf99-card p { color: var(--portal-muted); font-size: 0.92rem; }
.bf99-auth-page .bf99-card p a { color: var(--portal-accent); }

/* ---- narrow screens ---------------------------------------------------- */
@media (max-width: 60rem) {
	.bf99-op-hero { min-height: auto; padding-top: 4rem; }
	.bf99-page-server-info .bf99-op-main { columns: 1; }
	.bf99-op-main .bf99-dl-grid { grid-template-columns: 1fr; gap: 0; }
	.bf99-t-onepage.bf99-auth-page { background-attachment: scroll; }
	.bf99-op-hero-grid { grid-template-columns: 1fr; gap: 2rem; }
	.bf99-op-hero-copy { justify-items: center; text-align: center; }
	.bf99-op-rates, .bf99-op-cta { justify-content: center; }
	.bf99-op-hero::before { background: linear-gradient(to bottom, rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.7) 60%, var(--portal-surface) 100%); }
}

/* ---- motion off -------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
	.bf99-t-onepage *,
	.bf99-t-onepage *::before,
	.bf99-t-onepage *::after {
		animation: none !important;
		transition: none !important;
	}
	.bf99-t-onepage .bf99-btn:hover { transform: none; }
}
