/* Home page (index.php, $PAGE_LAYOUT = 'landing').
 *
 * Section order follows Gravity's Ragnarok Zero product page (gravity.co.kr/en/game/roz, reviewed 2026-09-15):
 * full-bleed hero with a character layer, a centred phrase, a trailer, parallax feature bands, a screenshot
 * carousel. Their libraries (GSAP, Swiper) are NOT used: the site CSP is script-src 'self', so the motion is
 * assets/js/landing.js - plain scroll/IntersectionObserver code with no dependencies.
 *
 * Bands drawn over artwork (hero, features) paint their own light-on-navy colours in both themes; everything on
 * the page ground takes the site.css tokens.
 */

/* ---------- top bar ---------- */

.topbar {
  position: fixed; inset: 0 0 auto; z-index: 20;
  display: flex; align-items: center; gap: 18px;
  padding: 10px 20px;
  background: linear-gradient(180deg, rgba(14, 19, 48, .82), rgba(14, 19, 48, 0));
  transition: background-color .25s ease, box-shadow .25s ease;
}
.topbar.is-solid { background: rgba(14, 19, 48, .95); box-shadow: 0 2px 14px rgba(0, 0, 0, .35); }
/* Over the home page hero the bar starts transparent and landing.js fills it in on scroll. Interior pages
   have no artwork behind it, so there it is filled from the first paint. */
body.is-page .topbar { background: rgba(14, 19, 48, .95); box-shadow: 0 2px 14px rgba(0, 0, 0, .35); }
.topbar-brand img { display: block; height: 42px; width: auto; }
/* min-width: 0 is what makes the rail work. Without it this flex item grows to the full width of the
   nav list, the ul below is never narrower than its own contents, and so overflow-x never has anything
   to do - the links past the fold just vanish off a fixed bar with no way to reach them. */
.topbar nav { min-width: 0; }
.topbar-nav {
  display: flex; gap: 2px; list-style: none; margin: 0 0 0 auto; padding: 0;
  overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.topbar-nav::-webkit-scrollbar { display: none; }
.topbar-nav a {
  display: block; padding: 8px 11px; border-radius: 6px;
  color: #F3EEDD; text-decoration: none; white-space: nowrap;
  font-weight: 700; font-size: .9rem;
}
.topbar-nav a:hover { color: #F2CE72; background: rgba(255, 255, 255, .08); }
.topbar-nav a[aria-current="page"] { color: #F2CE72; }
/* Twelve links need roughly 1359px of bar laid out flat. Condensing them first buys back enough
   room to keep the flat bar down to about 1250px, so ordinary laptops keep the full nav. */
@media (max-width: 1500px) {
  .topbar-nav a { padding: 8px 9px; font-size: .86rem; }
}

/* The menu button. Hidden until the bar runs out of room, and only ever shown when landing.js is
   running (html.js), because JS is what opens the panel. */
.topbar-toggle {
  display: none; margin-right: 14px; align-items: center; gap: 8px;
  padding: 8px 12px; border-radius: 6px; cursor: pointer;
  border: 1px solid rgba(255, 248, 232, .35); background: rgba(14, 19, 48, .55);
  color: #F3EEDD; font: 700 .9rem/1 "Nunito Sans", "Segoe UI", sans-serif;
}
.topbar-toggle:hover { color: #F2CE72; border-color: rgba(242, 206, 114, .6); }
.topbar-toggle__bars, .topbar-toggle__bars::before, .topbar-toggle__bars::after {
  display: block; width: 16px; height: 2px; background: currentColor; border-radius: 2px;
}
.topbar-toggle__bars { position: relative; }
.topbar-toggle__bars::before, .topbar-toggle__bars::after { content: ""; position: absolute; left: 0; }
.topbar-toggle__bars::before { top: -5px; }
.topbar-toggle__bars::after { top: 5px; }

/* THE MENU, AT EVERY WIDTH (owner 2026-09-21). The bar is a logo and a button, so nothing about it
   depends on how many links the site has. The previous two-mode bar needed its breakpoint re-tuned
   whenever a link was added or renamed, and got that wrong at 1290px. */
.js .topbar { flex-wrap: wrap; }
.js .topbar-toggle { display: inline-flex; }
.js .topbar nav { display: none; }
/* The panel FLOATS below the bar rather than growing it. Two reasons, and the first is what makes
   the glass possible at all: a backdrop filter blurs whatever is painted behind the element, and
   while the panel sat inside the bar the bar's own near-opaque background was behind it, so the
   blur would have frosted a flat colour and looked like nothing. Over the page it has the page to
   refract. It also keeps the bar a constant height instead of swelling to 663px when open. */
.js .topbar.is-open nav {
  display: block; position: absolute; top: 100%; left: 16px;
  width: min(340px, calc(100vw - 32px));
}
.js .topbar.is-open .topbar-nav {
  flex-direction: column; flex-wrap: nowrap; gap: 0;
  width: 100%; margin: 8px 0 0; padding: 6px;
  max-height: 70vh; overflow-y: auto;
  border-radius: 14px;
  border: 1px solid rgba(255, 248, 232, .18);
  background: rgba(20, 26, 60, .55);
  -webkit-backdrop-filter: blur(22px) saturate(180%);
  backdrop-filter: blur(22px) saturate(180%);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .22);
}
/* Safari before 18 and Firefox with the pref off render a backdrop filter as nothing at all, which
   would leave the panel 55% transparent over live page content and unreadable. Opaque there. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .js .topbar.is-open .topbar-nav { background: rgba(14, 19, 48, .97); }
}
.js .topbar.is-open .topbar-nav a { padding: 10px 12px; font-size: .95rem; border-radius: 8px; }
.js .topbar.is-open .topbar-nav a:hover { background: rgba(255, 255, 255, .1); }

/* Group headings, and the rule that separates one group from the next. */
.topbar-group {
  padding: 12px 12px 4px;
  font: 700 .68rem/1.2 "Nunito Sans", "Segoe UI", sans-serif;
  letter-spacing: .18em; text-transform: uppercase;
  color: #F2CE72; opacity: .8;
}
.topbar-group + li { margin-top: 0; }
.js .topbar.is-open .topbar-nav .topbar-group ~ .topbar-group {
  border-top: 1px solid rgba(255, 248, 232, .12); margin-top: 6px;
}
/* The flat no-JavaScript row has no room for headings, and they would read as dead links there. */
html:not(.js) .topbar-group { display: none; }

/* Without JavaScript there is no button to press, so the links stay laid out - and a flat row
   overflows a 1290px window exactly as it did before. So the bar wraps to as many rows as it needs
   and stops being fixed: a wrapped bar is 137px to 215px tall depending on the viewport, and
   anything a fixed one overlays is content nobody can read. Static, it pushes instead. */
html:not(.js) .topbar { position: static; flex-wrap: wrap; }
html:not(.js) .topbar nav { flex: 1 0 100%; }
html:not(.js) .topbar-nav { margin: 0; width: 100%; flex-wrap: wrap; overflow: visible; }

@media (max-width: 1250px) {
  .topbar { gap: 8px; padding: 8px 16px; background: rgba(14, 19, 48, .95); }
  .topbar-brand img { height: 34px; }
}

@media (max-width: 560px) {
  .topbar-brand img { height: 30px; }
}

/* ---------- shared ---------- */

.l-eyebrow {
  display: block; margin: 0 0 10px;
  font: 700 .78rem/1.2 "Nunito Sans", "Segoe UI", sans-serif;
  letter-spacing: .16em; text-transform: uppercase; color: var(--gold-text);
}
.l-section-title { font-size: clamp(1.6rem, 1.1rem + 2.2vw, 2.6rem); margin: 0 0 .45em; }

@media (prefers-reduced-motion: no-preference) {
  .js .reveal { transition: opacity .7s ease, transform .7s ease; }
  .js .reveal:not(.is-in) { opacity: 0; transform: translateY(28px); }
}

/* ---------- hero ---------- */

.l-hero {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: clamp(560px, 92vh, 860px);
  display: grid; align-items: end;
  background: #0E1330; color: #FFF8E8;
}
.l-hero__bg {
  position: absolute; inset: -12% 0; z-index: -4;
  background: url("../img/hero-bg.webp") center 30% / cover no-repeat;
  will-change: transform;
}
/* Trailer as the hero background (owner 2026-09-21). The still above stays underneath as the poster, so the
   hero is never blank: landing.js only adds the player when the visitor has not asked for reduced motion.
   min-width/min-height keep the 16:9 frame covering a taller or wider hero without letterboxing, and
   pointer-events off means it can never be clicked, paused or opened on YouTube. */
.l-hero__video { position: absolute; inset: 0; z-index: -3; overflow: hidden; }
.l-hero__video video,
.l-hero__video iframe {
  position: absolute; top: 50%; left: 50%;
  width: 100%; height: 100%; min-width: 177.78vh; min-height: 56.25vw;
  transform: translate(-50%, -50%);
  border: 0; object-fit: cover; pointer-events: none;
}
/* The hero player needs the same oversizing the classes band already had, and did not have it.
   Measured in the browser: the hero iframe sat exactly on the container's edges, nothing cropped,
   so YouTube's title bar along the top edge of the player was fully visible. controls=0 hides it on
   a desktop but NOT on a phone, which is where the owner saw it.
   136% crops roughly 18% off each end, clearing the title above and the branding below. Applied only
   to the YouTube player: a local <video> has no chrome to hide and should not be zoomed for nothing. */
.l-hero__video.l-video--yt iframe {
  /* The +128px term is the point. A percentage crop scales with the player, but YouTube's title bar
     is roughly a FIXED height in pixels, so on a short viewport - a phone, or any phone held
     sideways - 136% stops being enough to clear it. Adding 128px guarantees at least 64px cropped
     off each end whatever the size, which is what the owner was seeing on mobile. */
  width: max(136%, 177.78vh); height: max(136%, 56.25vw, 100% + 128px);
  min-width: 0; min-height: 0;
}
/* YouTube paints its own thumbnail and a big play button before playback starts, and controls=0 does not
   remove that first frame. So the iframe starts invisible and landing.js fades it in only once the player
   reports it is really playing. hero-bg.webp shows underneath until then, and keeps showing if the browser
   refuses to autoplay, which is the right thing to see rather than a play button nobody can click. */
.l-video--yt iframe { opacity: 0; transition: opacity .35s ease; }  /* short: this also runs at each loop */
.l-video--yt.is-playing iframe { opacity: 1; }
/* A hosted loop fades in the same way, for the same reason: the still underneath should hold the
   frame until there is real video to show, rather than flashing a black box while it buffers. */
.l-hero__video video, .l-classes__video video { opacity: 0; transition: opacity .35s ease; }
.l-hero__video.is-playing video, .l-classes__video.is-playing video { opacity: 1; }
.l-hero::before {
  content: ""; position: absolute; inset: 0; z-index: -2;
  background: linear-gradient(180deg, rgba(14, 19, 48, .72) 0%, rgba(14, 19, 48, .38) 42%, rgba(14, 19, 48, .94) 100%);
}
/* Centred, not left-aligned (owner 2026-09-21). The column used to be pushed left because the character
   art filled the right half of the hero; with the trailer as the background and the art gone, a left
   column left most of a wide screen empty and the content read as crammed into one spot. */
.l-hero__inner { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 130px 20px 96px; text-align: center; }
.l-hero__logo { display: block; width: min(460px, 80vw); margin: 0 auto; filter: drop-shadow(0 4px 14px rgba(0, 0, 0, .55)); }
.l-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; list-style: none; margin: 16px 0 8px; padding: 0; }
.l-hero .cta-row { justify-content: center; }
.l-tags li {
  padding: 3px 11px; border-radius: 999px;
  border: 1px solid rgba(231, 190, 85, .7); background: rgba(14, 19, 48, .5);
  color: #F2CE72; font-size: .8rem; font-weight: 700; letter-spacing: .06em;
}
.l-hero__desc { max-width: 52ch; margin: 10px auto 18px; font-size: 1.08rem; color: #EDE6D2; text-shadow: 0 1px 3px rgba(0, 0, 0, .7); }
.l-status {
  display: inline-flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
  margin: 0 0 22px; padding: 7px 15px; border-radius: 999px;
  background: rgba(14, 19, 48, .66); font-size: .9rem; font-variant-numeric: tabular-nums;
}
.l-status b { color: #FFF8E8; }
.l-hero .btn-alt { background: rgba(14, 19, 48, .55); border-color: rgba(255, 248, 232, .45); color: #FFF8E8; }

/* Glass buttons on the feature bands. DARK tinted, not light: these sit over artwork that is bright
   in places and dark in others, and a light pane leaves pale text with nothing to hold on to. A dark
   pane keeps the same contrast whatever is behind it, which is the part that has to survive.
   Readability first, glass second: the tint alone carries the contrast, and the blur is decoration
   on top of it. That is why the no-backdrop-filter fallback only needs to raise the same tint. */
.btn-glass {
  background: rgba(14, 19, 48, .46);
  -webkit-backdrop-filter: blur(16px) saturate(170%);
  backdrop-filter: blur(16px) saturate(170%);
  border: 1px solid rgba(255, 248, 232, .5);
  color: #FFF8E8;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .65);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .32), inset 0 1px 0 rgba(255, 255, 255, .28);
}
.btn-glass:hover, .btn-glass:focus-visible {
  background: rgba(14, 19, 48, .62);
  border-color: #F2CE72; color: #F2CE72;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .btn-glass { background: rgba(14, 19, 48, .78); }
}

/* ---------- the article reader ---------- */

.l-modal {
  width: min(860px, calc(100vw - 32px)); max-height: 86vh; padding: 0;
  border: 1px solid rgba(255, 248, 232, .22); border-radius: 16px;
  background: rgba(20, 26, 60, .62);
  -webkit-backdrop-filter: blur(26px) saturate(180%);
  backdrop-filter: blur(26px) saturate(180%);
  box-shadow: 0 30px 80px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .2);
  color: #F3EEDD; overflow: hidden;
}
/* Same reasoning as the menu: where the blur does not render, the pane goes opaque rather than
   leaving an article 62% transparent over the page behind it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .l-modal { background: rgba(14, 19, 48, .97); }
}
.l-modal::backdrop { background: rgba(8, 11, 30, .62); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.l-modal__head {
  display: flex; align-items: flex-start; gap: 16px;
  padding: 20px 22px 16px; border-bottom: 1px solid rgba(255, 248, 232, .16);
}
.l-modal__eyebrow {
  display: block; margin: 0 0 4px;
  font: 700 .7rem/1.2 "Nunito Sans", "Segoe UI", sans-serif;
  letter-spacing: .18em; text-transform: uppercase; color: #F2CE72;
}
.l-modal__title { margin: 0; color: #FFF8E8; font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem); }
.l-modal__close {
  margin-left: auto; flex: none; width: 36px; height: 36px; border-radius: 50%; cursor: pointer;
  border: 1px solid rgba(255, 248, 232, .35); background: rgba(14, 19, 48, .5);
  color: #F3EEDD; font-size: 1.3rem; line-height: 1;
}
.l-modal__close:hover { color: #F2CE72; border-color: #F2CE72; }
.l-modal__body { padding: 4px 22px 8px; overflow-y: auto; max-height: calc(86vh - 160px); }
.l-modal__foot { margin: 0; padding: 12px 22px 18px; border-top: 1px solid rgba(255, 248, 232, .16); }
.l-modal__foot a { color: #F2CE72; font-weight: 700; }

/* The article arrives styled for a light page: cream panels, dark ink. Inside a dark reader those
   are unreadable, so the panels give up their own ground and take the reader's colours. */
.l-modal .panel { background: none; border: 0; border-radius: 0; box-shadow: none; margin: 0 0 6px; }
/* The modal is always dark, whatever the visitor's system theme. site.css paints .panel with the
   parchment image under `:root:not([data-theme="dark"]) .panel` (specificity 0,2,1), which beats the
   line above (0,2,0), so on a light-mode PC the article came up as pale text on parchment - unreadable
   (owner screenshot 2026-09-24). `:root` in front makes this 0,3,0 and wins in both themes. */
:root .l-modal .panel { background-image: none; }
.l-modal .panel-title { color: #FFF8E8; border-bottom-color: rgba(242, 206, 114, .5); }
.l-modal .panel-eyebrow { color: #F2CE72; }
.l-modal p, .l-modal li, .l-modal td, .l-modal th { color: #E8E2D2; }
/* table.req th is (0,1,2) and beat the line above at (0,1,1), so the row labels stayed dark ink on
   a dark panel. Matching its shape rather than reaching for !important. */
.l-modal table.req th, .l-modal table.req td { color: #E8E2D2; border-bottom-color: rgba(255, 248, 232, .16); }
.l-modal table.req th { color: #F2CE72; }
.l-modal table tr { border-bottom-color: rgba(255, 248, 232, .16) !important; }

/* Fact cards pin their OWN light background (.fact takes var(--paper-2)) while their text came from
   the reader's light palette, because .l-modal b beat .fact b on file order. Light on light, which
   is what made the rates cards unreadable. A card that pins its ground has to pin its text too, so
   here it takes the reader's ground instead of the page's. */
.l-modal .fact { background: rgba(255, 255, 255, .07); border-color: rgba(255, 248, 232, .18); }
.l-modal .fact b { color: #FFF8E8; }
.l-modal .fact span { color: #CFC8B6; }
.l-modal h3, .l-modal strong, .l-modal b, .l-modal .wf { color: #FFF8E8; }
.l-modal a { color: #F2CE72; }
.l-modal table { width: 100%; }
.l-modal__body.is-loading { opacity: .55; }

.l-scroll {
  position: absolute; left: 50%; bottom: 18px;
  width: 1px; height: 56px; overflow: hidden; background: rgba(255, 248, 232, .25);
}
.l-scroll::after {
  content: ""; position: absolute; left: 0; top: -50%;
  width: 1px; height: 50%; background: #F2CE72;
  animation: l-scroll 2.2s ease-in-out infinite;
}
@keyframes l-scroll { to { top: 100%; } }
@media (prefers-reduced-motion: reduce) { .l-scroll::after { animation: none; top: 25%; } }
@media (max-width: 860px) {
  .l-hero__inner { padding-top: 150px; }
}

/* ---------- phrase ---------- */

/* The chibi party stands behind this panel (owner 2026-09-21, moved here from the class grid).
   It has room to be seen here: the panel is a single block of centred text rather than six opaque
   cards, so the art is not hidden behind anything. */
.l-phrase {
  position: relative; overflow: hidden; background: var(--paper);
  text-align: center; padding: clamp(80px, 10vw, 150px) 20px;
  /* Tall enough for the centred art to sit behind the words without overflow clipping it. The
     padding is symmetric again: the deeper bottom padding only existed to give bottom-anchored
     art somewhere to stand, and it is centred now. */
  min-height: 560px; display: grid; align-content: center;
}
.l-phrase::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  /* Centred on the panel rather than standing on its floor (owner 2026-09-21). */
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  /* 489x351 source, shown at 670px: a 1.4x upscale, soft up close. That is the file rather than
     the rule, and only a larger original sharpens it. */
  width: min(670px, 64%); aspect-ratio: 489 / 351;
  background: url("../img/landing/chibi-party.webp") center center / contain no-repeat;
  /* Lowered twice now. At .58 the paragraph had to be read through the crowd; at .38 it still
     measured 3.70:1 against the art, under the 4.5:1 that body text needs, and the owner reported
     it as unreadable. Opacity alone is the wrong lever - taking it far enough to pass leaves the
     art as a smudge - so the fade below does the work and this only trims the busiest areas. */
  opacity: .30;
}
/* A scrim of the page's own paper, softest at the edges, sitting BETWEEN the art and the words.
   The art stays visible around the outside of the panel while the text sits on near-clean paper:
   measured 4.80:1, which passes. Painted with the paper variable rather than a hardcoded colour so
   it follows the theme into dark mode, where the same problem would otherwise appear inverted. */
.l-phrase::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(ellipse 68% 56% at 50% 50%,
              var(--paper) 0%, var(--paper) 38%, transparent 76%);
}
.l-phrase > .reveal { position: relative; z-index: 1; }
.l-phrase .l-section-title { max-width: 22ch; margin-inline: auto; }
.l-phrase p { max-width: 62ch; margin: 0 auto; font-size: 1.08rem; color: var(--muted); }

/* The framed .l-video trailer block that used to live here is gone: the trailer is the hero background now
   (owner 2026-09-21). Its rules are deleted rather than left behind because .l-video--yt iframe matched the
   hero player too, at the same specificity as .l-hero__video iframe but later in the file, and so overrode
   the hero's 16:9 cover sizing. The sizing the hero player uses is up at .l-hero__video. */

/* ---------- feature bands ---------- */

.l-feature {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: clamp(460px, 78vh, 720px);
  display: grid; align-items: center;
  background: #0E1330; color: #FFF8E8;
}
/* A standing figure on the band, between the parallax art (-2) and the text (auto). The section
   sets isolation: isolate, so these negative layers stay inside it. */
.l-feature__figure {
  position: absolute; z-index: -1; bottom: 0; pointer-events: none;
  right: max(2vw, calc(50% - 570px));
  height: min(84%, 500px); width: auto;
  filter: drop-shadow(0 14px 34px rgba(0, 0, 0, .5));
}
/* Below the two-column width the text runs full width and would sit on top of her, so she steps
   back to being a tint at the edge rather than a figure competing with the words. */
@media (max-width: 900px) {
  .l-feature__figure { right: -8vw; height: 70%; opacity: .28; }
}
.l-feature__bg {
  position: absolute; inset: -12% 0; z-index: -2;
  background-position: center; background-size: cover; background-repeat: no-repeat;
  will-change: transform;
}
.l-feature::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(90deg, rgba(14, 19, 48, .9) 0%, rgba(14, 19, 48, .58) 46%, rgba(14, 19, 48, .08) 100%);
}
.l-feature--right::before {
  background: linear-gradient(270deg, rgba(14, 19, 48, .9) 0%, rgba(14, 19, 48, .58) 46%, rgba(14, 19, 48, .08) 100%);
}
.l-feature__inner { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 64px 20px; }
.l-feature__txt { max-width: 34rem; }
.l-feature--right .l-feature__txt { margin-left: auto; }
.l-feature .l-section-title { color: #FFF8E8; }
.l-feature .l-eyebrow { color: #F2CE72; }
.l-feature p { color: #E6DFCB; font-size: 1.05rem; }
.l-points { display: grid; gap: 8px; list-style: none; margin: 14px 0 22px; padding: 0; }
.l-points li { display: flex; gap: 10px; align-items: baseline; color: #EDE6D2; }
.l-points li::before {
  content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: radial-gradient(circle at 32% 30%, #F2CE72, #B8862B);
}
.l-points b { color: #FFF8E8; font-variant-numeric: tabular-nums; }
@media (max-width: 860px) {
  .l-feature::before, .l-feature--right::before {
    background: linear-gradient(180deg, rgba(14, 19, 48, .5), rgba(14, 19, 48, .92));
  }
}

/* ---------- screenshot carousel ---------- */

.l-gallery { background: var(--paper-2); padding: clamp(56px, 8vw, 100px) 0; }
.l-gallery__head {
  max-width: var(--maxw); margin: 0 auto 22px; padding: 0 20px;
  display: flex; flex-wrap: wrap; gap: 16px; align-items: end; justify-content: space-between;
}
.l-gallery__head .l-section-title { margin: 0; }
.l-carousel__nav { display: flex; gap: 8px; }
.l-carousel__btn {
  width: 44px; height: 44px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--rule); background: var(--panel); color: var(--ink);
  font-size: 1.25rem; line-height: 1;
}
.l-carousel__btn:hover { border-color: var(--gold); color: var(--gold-text); }
.l-carousel {
  --gutter: max(20px, calc((100% - var(--maxw)) / 2 + 20px));
  display: grid; grid-auto-flow: column; grid-auto-columns: min(80%, 860px); gap: 18px;
  overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: var(--gutter);
  padding: 4px var(--gutter) 18px; scrollbar-width: thin;
}
.l-carousel figure {
  margin: 0; scroll-snap-align: start; overflow: hidden;
  border: 1px solid var(--rule); border-radius: 12px; background: var(--panel);
  box-shadow: 0 8px 24px var(--shadow);
}
.l-carousel img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.l-carousel figcaption { padding: 10px 14px; font-size: .9rem; color: var(--muted); }

/* ---------- news + getting started ---------- */

.l-band { background: var(--paper); padding: clamp(56px, 8vw, 96px) 20px; }
.l-band__grid {
  max-width: var(--maxw); margin: 0 auto;
  display: grid; grid-template-columns: 1.45fr 1fr; gap: 28px; align-items: start;
}
@media (max-width: 860px) { .l-band__grid { grid-template-columns: 1fr; } }
.l-start {
  background: #171F45; color: #EDE6D2;
  border-top: 4px solid #B8862B; border-radius: 12px; padding: 22px 24px;
}
.l-start h2 { color: #FFF8E8; }
.l-start ol { margin: 0 0 18px; padding-left: 1.2em; }
.l-start li { margin: 0 0 6px; }
.l-start a:not(.btn) { color: #F2CE72; }

/* Pre-registration banner (index.php, PREREG_BANNER) */
.l-prereg { background: #171F45; border-top: 4px solid #B8862B; border-bottom: 1px solid rgba(184,134,43,.35); padding: 20px 20px; overflow: hidden; }
.l-prereg__inner {
  max-width: var(--maxw); margin: 0 auto;
  display: grid; grid-template-columns: auto 1fr auto; gap: 20px 32px; align-items: center;
}
.l-prereg__art { display: block; height: 210px; width: auto; margin: -8px 0 -24px; filter: drop-shadow(0 6px 18px rgba(0,0,0,.45)); }
.l-prereg__title { color: #FFF8E8; margin: 4px 0 8px; }
.l-prereg p { color: #EDE6D2; margin: 0; max-width: 62ch; }
.l-prereg__cta { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.l-prereg__count { font-size: 14px; }
.l-prereg__count b { color: #F2CE72; font-size: 22px; }


/* The milestone gauge, the same one prereg.php draws, sized for the banner. The track is a light
   tint of the banner's own navy rather than a new colour, so it reads as part of the panel. */
.l-prereg__bar {
  position: relative; width: min(220px, 60vw); height: 9px; border-radius: 6px;
  background: rgba(255, 248, 232, .18); overflow: hidden;
}
.l-prereg__bar span {
  display: block; height: 100%; border-radius: 6px;
  background: linear-gradient(90deg, #3F9B4F, #E0B85A);
}
.l-prereg__next { font-size: 13px; text-align: center; }
.l-prereg .l-prereg__next b { color: #F2CE72; }
@media (max-width: 720px) {
  .l-prereg__inner { grid-template-columns: 1fr; }
  .l-prereg__art { height: 160px; margin: 0 auto -8px; }
  .l-prereg__cta { align-items: flex-start; }
}

/* Episode 1 classes (index.php) */
/* The chibi party sits behind this section (owner 2026-09-21). Cropped from the art the owner sent:
   the official Gravity wordmark that came above the characters is deliberately NOT included, because
   our own logo is the mark this site carries. The white background was keyed to alpha so it sits on
   the page rather than as a white rectangle.

   A watermark, not a picture: at full strength it competes with the six class cards in front of it,
   which are the point of the section. */
/* Room at the foot of the section for the party to stand in. Without it the art had only the
   gutters between opaque cards to show through, which is why it read as a faint strip. */
.l-classes {
  position: relative; overflow: hidden; isolation: isolate; background: var(--paper-2);
  padding: clamp(56px, 8vw, 100px) 20px clamp(64px, 8vw, 110px);
}
/* The trailer behind the cards. A light section, so it sits under a heavy veil rather than a
   light one: the headings here are dark ink on paper and must not fight the footage. */
.l-classes__video { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.l-classes__video video,
.l-classes__video iframe {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  border: 0; object-fit: cover; pointer-events: none;
  /* Deliberately LARGER than the box that clips it. YouTube draws its title bar along the top edge
     of the player and its branding along the bottom, inside the iframe where no stylesheet of ours
     can reach. Oversizing and centring pushes both edges outside the container, and overflow:hidden
     on the parent does the rest. 136% leaves roughly 18% of the player's height cropped off each
     end, which clears the title bar at every size it renders at.
     The vh/vw minimums stay so the frame still covers a very wide or very tall section. */
  width: max(136%, 177.78vh); height: max(136%, 56.25vw, 100% + 128px);
}
.l-classes::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: var(--paper-2); opacity: .8;
}
/* Light art on a dark ground reads fainter, so it gets a little more there. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .l-phrase::before { opacity: .72; }
}
:root[data-theme="dark"] .l-phrase::before { opacity: .72; }
@media (max-width: 520px) {
  .l-phrase { min-height: 440px; padding: clamp(56px, 14vw, 90px) 20px; }
  .l-phrase::before { width: 96%; opacity: .3; }
}

/* The link now sits over the busiest part of the art rather than over flat paper, and gold on a
   crowd is unreadable. It keeps its own ground instead of relying on whatever is behind it. */
.l-classes__more a {
  display: inline-block; padding: 8px 18px; border-radius: 999px;
  background: color-mix(in srgb, var(--paper) 86%, transparent);
  border: 1px solid var(--rule);
  box-shadow: 0 2px 10px var(--shadow);
}
.l-classes__more a:hover { background: var(--paper); }
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .l-classes__more a { background: var(--paper); }
}
/* z-index 2, ABOVE the veil at 1. They were both 1, and on a tie the box painted later wins, which
   is the ::after - so the veil sat on top of the cards and bleached the whole section. */
.l-classes__inner { position: relative; z-index: 2; max-width: var(--maxw); margin: 0 auto; }
.l-classes__head { text-align: center; max-width: 60ch; margin: 0 auto 32px; }
.l-classes__head p { color: var(--muted); margin: 0; }
.l-classes__grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
@media (max-width: 900px) { .l-classes__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .l-classes__grid { grid-template-columns: 1fr; } }
/* The whole card is a link to that class's page in the job guide (owner 2026-09-22), so the art,
   the name and the description are all one target rather than a small link nobody finds. */
.l-class__link { display: flex; flex-direction: column; height: 100%;
                 color: inherit; text-decoration: none; }
.l-class__link:focus-visible { outline: 3px solid var(--gold); outline-offset: 2px; }
.l-class { background: var(--panel); border: 1px solid var(--rule); border-radius: 14px; overflow: hidden; display: grid; align-content: start; }
.l-class__art { height: 300px; overflow: hidden; display: flex; align-items: flex-end; justify-content: center; padding-top: 14px;
  background: radial-gradient(60% 55% at 50% 58%, color-mix(in srgb, var(--c) 34%, transparent), transparent 72%),
              linear-gradient(180deg, color-mix(in srgb, var(--c) 10%, var(--panel)), var(--panel)); }
.l-class__art img { display: block; height: 100%; max-height: 286px; width: auto; max-width: 92%; object-fit: contain; filter: drop-shadow(0 10px 14px var(--shadow)); transition: transform .35s ease; }
.l-class:hover .l-class__art img { transform: translateY(-6px); }
.l-class__name { margin: 16px 20px 0; font-size: 1.35rem; }
.l-class__from { margin: 2px 20px 0; font: 700 .72rem/1.2 "Nunito Sans", "Segoe UI", sans-serif; letter-spacing: .14em; text-transform: uppercase; color: var(--gold-text); }
.l-class__desc { margin: 10px 20px 20px; color: var(--text); }
.l-classes__more { text-align: center; margin: 28px 0 0; }
.l-classes__more a { color: var(--gold-text); font-weight: 700; }
@media (prefers-reduced-motion: reduce) { .l-class__art img { transition: none; } }
