/* Dawn - FFXI private server
 *
 * Typography carries the identity here. Three faces, each with a job:
 *   Fraunces    - display. A warm old-style serif with real character, set
 *                 large and tight. This is the voice.
 *   IBM Plex Sans - body. More personality than the usual Inter default.
 *   IBM Plex Mono - data, labels, eyebrows. Tabular figures so numbers in
 *                 leaderboards and stat tables line up.
 */

/* Fraunces and Plex Sans are VARIABLE fonts. They were previously declared as
 * four faces pointing at two byte-identical files (fraunces-400 == fraunces-600,
 * plex-sans-400 == plex-sans-600), with single-value weight descriptors. A
 * single-value descriptor on a variable font pins it to its DEFAULT instance --
 * and Fraunces defaults to wght 900 / opsz 9. Every heading marked "600" was
 * therefore rendering as Black at the smallest optical size, which is why the
 * display face read as cramped and overweight at large sizes.
 *
 * Declaring the real axis ranges hands weight back to `font-weight`, and lets
 * `font-optical-sizing: auto` track opsz to the rendered size the way Fraunces
 * was drawn to be used: lighter contrast and wider spacing when small, high
 * contrast and tight fit when large. This is the single biggest typographic
 * change on the site and it costs one file per family instead of two.
 *
 * Plex Mono has no fvar; its 400 and 500 are genuinely separate cuts.
 * All self-hosted (SIL OFL). No CDN, no third-party request on page load.
 */

@font-face {
  font-family: "Fraunces";
  src: url("fonts/fraunces-var.woff2") format("woff2");
  font-weight: 100 900;                /* real axis: 100-900 */
  font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Sans";
  src: url("fonts/plex-sans-var.woff2") format("woff2");
  font-weight: 100 700;                /* real axis: 100-700 */
  font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("fonts/plex-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("fonts/plex-mono-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}

:root {
  /* A radius SCALE, not one value. Containers are softer than the things
     inside them; the difference is what stops every corner on the page
     reading as the same 10px stamp. --radius stays as the container value
     so existing rules keep working unchanged. */
  --r-xs:  3px;         /* pills, chips, tags - nearly square */
  --r-sm:  6px;         /* inputs, buttons, inner slots */
  --r-lg:  14px;        /* cards, table wraps, panels */
  --radius: var(--r-lg);

  /* Shadows carry the background's hue rather than neutral black. On a
     blue-black page a pure-black shadow reads as a grey smudge. */
  --shadow-lift: 0 1px 2px rgba(6, 9, 18, .5), 0 8px 24px -8px rgba(6, 9, 18, .8);

  /* One easing for everything that moves on interaction. Slight overshoot,
     so buttons feel sprung rather than linear. */
  --ease-out: cubic-bezier(.2, .8, .3, 1);
  --wrap:   1200px;
  --measure: 68ch;          /* readable line length - roughly 68 characters */

  --font-display: "Fraunces", Georgia, serif;
  --font-body:    "Plex Sans", "Segoe UI", system-ui, sans-serif;
  --font-mono:    "Plex Mono", ui-monospace, Consolas, monospace;
}

/* Theme 1 - Dawn. Sunrise over Vana'diel. The default. */
:root,
[data-theme="dawn"] {
  --night:   #0a0d16;
  --night-2: #10141f;
  --panel:   #151a28;
  --panel-2: #1b2131;
  --line:    #262e42;
  --text:    #dbe0ec;
  --muted:   #8790a6;
  --gold:    #f0b657;
  --ember:   #ff8a4c;
  --online:  #5ad48a;
  --offline: #ff6b6b;

  --hero-bg:
    radial-gradient(ellipse 80% 60% at 50% 100%, rgba(240, 182, 87, .20), transparent 70%),
    radial-gradient(ellipse 60% 50% at 50% 108%, rgba(255, 138, 76, .28), transparent 70%),
    linear-gradient(180deg, #0a0d16 0%, #121829 60%, #1c2135 100%);
}

/* Theme 2 - Dusk. Deep evening, crystal blues and violet. */
[data-theme="dusk"] {
  --night:   #0c0a16;
  --night-2: #12101f;
  --panel:   #191527;
  --panel-2: #201b31;
  --line:    #2e2745;
  --text:    #e0dcec;
  --muted:   #948aad;
  --gold:    #b89dff;
  --ember:   #ff7bc8;
  --online:  #6ae0c0;
  --offline: #ff6b8f;

  --hero-bg:
    radial-gradient(ellipse 70% 55% at 50% 100%, rgba(184, 157, 255, .22), transparent 70%),
    radial-gradient(ellipse 55% 45% at 50% 106%, rgba(255, 123, 200, .20), transparent 70%),
    linear-gradient(180deg, #0c0a16 0%, #16122a 60%, #221a3a 100%);
}

/* Theme 3 - Vana'diel. The site rendered as FFXI's own interface.
 *
 * The palette is not invented: these are the game's chat-log colours. White
 * for system text, yellow for names and NPC speech, green for item names.
 * Panels are the blue menu window every player knows on sight.
 *
 * There is no background artwork and no simulated sky. This theme is an
 * interface, not scenery. */
[data-theme="vanadiel"] {
  /* Neutrals sampled from Anima key art: storm violet base, steel blue mids,
     bone for muted text. The red is used on one thing only, which is exactly
     why it reads. */
  --night:   #14121f;   /* storm violet, darkened from #232139 */
  --night-2: #1b1930;
  --panel:   #1e2b4d;   /* steel blue window */
  --panel-2: #2a457a;
  --line:    #4f73bb;
  --text:    #eef0f6;
  --muted:   #8c8b6e;   /* bone - the unusual note that stops this reading generic */

  /* UI colours stay FFXI's own chat-log code. The world is Anima; the
     interface is still Vana'diel. */
  --gold:    #ffd45e;   /* name yellow */
  --ember:   #8ae06a;   /* item green */
  --online:  #afcea5;
  --offline: #d95564;   /* the eye. Used here and nowhere else. */

  --menu-edge:  #82c9fe;
  --menu-glow:  rgba(130, 201, 254, .22);

  /* The hero window carries the visual weight; the section behind it is
     plain. Nothing to see through, so nothing is translucent. */
  --hero-bg: none;
}

/* Window chrome: an outer dark rule, a pale inner edge, and a soft inner
   glow. That triple is what makes an FFXI menu read as an FFXI menu. */
[data-theme="vanadiel"] .card,
[data-theme="vanadiel"] .table-wrap,
[data-theme="vanadiel"] .form-card {
  border: 1px solid var(--menu-edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px rgba(4, 7, 15, .9),
              inset 0 0 22px var(--menu-glow),
              0 2px 10px rgba(0, 0, 0, .6);
  background: linear-gradient(180deg, #0f2850 0%, #0a1c3c 100%);
}

[data-theme="vanadiel"] .status-chip {
  border: 1px solid var(--menu-edge);
  border-radius: 3px;
  background: #0d2246;
}

[data-theme="vanadiel"] .site-header { background: #0d0b1a; }
[data-theme="vanadiel"] .site-footer { background: #100e1e; }

/* Menu headers are small, wide-tracked and uppercase, like the game's. */
[data-theme="vanadiel"] .card h3,
[data-theme="vanadiel"] .section-head h2,
[data-theme="vanadiel"] th {
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: .8rem;
  color: var(--menu-edge);
}
[data-theme="vanadiel"] .card h3 { font-size: .85rem; }

/* Item names take the game's green, exactly as they do in the log. */
[data-theme="vanadiel"] .prose code { color: var(--ember); }

/* ---- SIGNATURE: the selection cursor ----------------------------------
   In FFXI a small arrow sits to the left of whichever menu row is selected.
   Every interactive row here grows the same cursor on hover and focus, and
   the label shifts to the game's name-yellow. This is the one bold move;
   everything else in this theme stays quiet. */
[data-theme="vanadiel"] .nav a,
[data-theme="vanadiel"] .footer-grid a,
[data-theme="vanadiel"] tbody tr td:first-child,
[data-theme="vanadiel"] a.card {
  position: relative;
}
[data-theme="vanadiel"] .nav a::before,
[data-theme="vanadiel"] .footer-grid a::before,
[data-theme="vanadiel"] tbody tr td:first-child::before,
[data-theme="vanadiel"] a.card::before {
  content: "\25B6";
  position: absolute;
  left: -1.05em;
  color: var(--gold);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity .12s ease, transform .12s ease;
  pointer-events: none;
}
[data-theme="vanadiel"] .nav a:hover::before,
[data-theme="vanadiel"] .nav a:focus-visible::before,
[data-theme="vanadiel"] .footer-grid a:hover::before,
[data-theme="vanadiel"] .footer-grid a:focus-visible::before,
[data-theme="vanadiel"] tbody tr:hover td:first-child::before,
[data-theme="vanadiel"] a.card:hover::before,
[data-theme="vanadiel"] a.card:focus-visible::before {
  opacity: 1;
  transform: translateX(0);
}
[data-theme="vanadiel"] .nav a:hover,
[data-theme="vanadiel"] .footer-grid a:hover,
[data-theme="vanadiel"] tbody tr:hover td:first-child a,
[data-theme="vanadiel"] a.card:hover h3 { color: var(--gold); }

/* a.card sits in a grid, so its cursor needs room rather than overhang. */
[data-theme="vanadiel"] a.card::before { left: .8em; top: 1.35rem; }
[data-theme="vanadiel"] a.card:hover::before { transform: translateX(0); }
[data-theme="vanadiel"] a.card h3 { padding-left: 1.1em; }

/* FFXI menus open by expanding vertically. One page-load moment, once. */
[data-theme="vanadiel"] .hero .wrap { animation: menu-open .34s ease-out both; }
@keyframes menu-open {
  from { opacity: 0; transform: scaleY(.94); }
  to   { opacity: 1; transform: scaleY(1); }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--night);
  color: var(--text);
  font: 400 17px/1.7 var(--font-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;

  /* Let opsz track the rendered size. Fraunces is drawn with a wide optical
     range and this is the axis doing most of the work at display sizes. */
  font-optical-sizing: auto;

  /* Now that real weights load, refuse synthetic ones. A faux-bold smear is a
     bug worth seeing rather than silently papering over. */
  font-synthesis: none;
}

.wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 28px; }
.main { padding: 56px 28px 88px; }

a { color: var(--gold); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Headings are the display face in the text colour, NOT the accent colour.
   Gold on every heading reads as a forum skin; gold is reserved for links,
   rules and one accent per view. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.018em;
  margin: 0 0 .45em;
  color: var(--text);

  /* Headings are short. Balancing costs nothing here and kills the single
     orphaned word that otherwise dangles under a two-line title. */
  text-wrap: balance;
}

/* The weight ladder is real now, so it can carry hierarchy that size alone
   was doing before: the page title leans on the heavy end of the axis, and
   sub-heads step back down rather than all sitting at one weight. */
h1 { font-size: clamp(2.6rem, 5.2vw, 4rem); font-weight: 700; letter-spacing: -0.032em; }
h2 { font-size: clamp(1.7rem, 2.6vw, 2.2rem); font-weight: 600; }
h3 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.01em; }
h4 { font-size: 1rem;    font-weight: 550; letter-spacing: -0.005em; }

/* Body copy gets the cheaper of the two algorithms - `pretty` only touches
   the last few lines, so it is safe on long prose where `balance` is not. */
p, li, figcaption, .tagline { text-wrap: pretty; }

/* Mono eyebrow - the structural device that gives sections a spine. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--muted);
  display: block;
  margin-bottom: .9em;
}

.muted { color: var(--muted); }
.small { font-size: .875rem; }
.center { text-align: center; }
.mono  { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ header */

.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgba(10, 13, 22, .92);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}
.header-inner { display: flex; align-items: center; gap: 24px; height: 62px; }

.brand {
  display: flex; align-items: center; gap: 10px;
  color: var(--text); text-decoration: none;
  font-family: var(--font-display);
  font-weight: 600; font-size: 1.45rem; letter-spacing: -0.02em;
}
.brand:hover { text-decoration: none; }
.brand-mark { color: var(--gold); font-size: 1.1rem; }

.nav { margin-left: auto; display: flex; align-items: center; gap: 26px; }
.nav a {
  position: relative;
  color: var(--muted); font-size: .9rem; font-weight: 400;
  transition: color .18s var(--ease-out);
}
.nav a:hover { color: var(--text); text-decoration: none; }

/* There was no indication of which section you were in. The marker is a short
   lit rule under the label rather than a colour change alone, because the nav
   already spends colour on hover and two states sharing one signal is how you
   end up unable to tell them apart. */
.nav a[aria-current="page"] { color: var(--text); }
.nav a[aria-current="page"]::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -6px;
  height: 2px; border-radius: 2px;
  background: var(--gold);
}
/* Vana'diel marks the current row with its own selection cursor instead, which
   the theme block below already draws - it does not need a second marker. */
[data-theme="vanadiel"] .nav a[aria-current="page"]::after { display: none; }
[data-theme="vanadiel"] .nav a[aria-current="page"] { color: var(--gold); }
.nav-cta {
  background: var(--gold); color: #1a1206 !important;
  padding: 7px 16px; border-radius: var(--r-sm); font-weight: 650;
  transition: background .18s var(--ease-out);
}
.nav-cta:hover { background: #ffc978; }

.nav-toggle, .nav-burger { display: none; }

/* -------------------------------------------------------------- hero */

.hero {
  position: relative;
  padding: 92px 20px 76px;
  text-align: center;
  border-bottom: 1px solid var(--line);
  background: var(--hero-bg);
}

/* SIGNATURE SHAPE: the horizon rule.
   The hero's bottom border is a flat hairline everywhere else on the site. On
   the hero it becomes the one place the sky touches the page -- brightest at
   centre, dissolving to nothing at both edges. It is the same 1px the rest of
   the layout uses, so it costs no vertical space, but it is the only lit edge
   on the page and it points at where the light is coming from. */
.hero::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg,
      transparent,
      rgba(var(--amb-2), .55) 26%,
      var(--gold) 50%,
      rgba(var(--amb-2), .55) 74%,
      transparent);
  opacity: .85;
}
/* Vana'diel's hero is a menu window with no bottom border; a glowing horizon
   under it would be scenery leaking into an interface. */
[data-theme="vanadiel"] .hero::after { display: none; }
/* The headline was a white-to-gold gradient clipped to the glyphs. That
   device is the single most common generated-site fingerprint, and it has two
   real costs beyond taste: transparent text fill kills the selection
   highlight, and it forces a fixed light direction onto type that the sky
   behind it is already lighting from below. Solid text, and the light comes
   from the background where it belongs. */
.hero h1 {
  font-size: clamp(2.6rem, 6.4vw, 4.2rem);
  margin-bottom: .28em;
  color: var(--text);
  letter-spacing: -0.038em;
  font-weight: 700;
}
.hero p.tagline { font-size: 1.15rem; color: var(--muted); max-width: 620px; margin: 0 auto 32px; }

.btn {
  display: inline-block; padding: 12px 28px; border-radius: var(--r-sm);
  font-weight: 650; font-size: 1rem; border: 1px solid transparent; cursor: pointer;
  transition: background .18s var(--ease-out),
              border-color .18s var(--ease-out),
              color .18s var(--ease-out),
              transform .18s var(--ease-out),
              box-shadow .18s var(--ease-out);
}
.btn:hover  { transform: translateY(-1px); }
/* Pressed state travels further than the hover lift, so the control reads as
   physically depressed rather than merely re-coloured. */
.btn:active { transform: translateY(1px) scale(.985); transition-duration: .06s; }

.btn-primary { background: var(--gold); color: #1a1206; }
.btn-primary:hover { background: #ffc978; text-decoration: none; box-shadow: var(--shadow-lift); }
.btn-ghost { border-color: var(--line); color: var(--text); background: transparent; }
.btn-ghost:hover { border-color: var(--gold); color: var(--gold); text-decoration: none; }
.btn-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }

/* ------------------------------------------------------ hero: asymmetry
 *
 * The hero was a centred stack: portrait, headline, tagline, buttons, chips,
 * every one of them on the same axis. Centred stacks are the default shape of
 * a generated page, and they waste the one thing this hero has that a
 * marketing hero usually does not -- live numbers off the game database.
 *
 * So the headline holds the left and the numbers take the right. The
 * asymmetry is paid for with real content, not a decorative blob, and the
 * rail collapses under the lede on narrow screens where two columns cannot
 * both be readable.
 */
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, .65fr);
  gap: 56px;
  align-items: center;
  text-align: left;
}
.hero-lede .tagline { margin-left: 0; margin-right: 0; }
.hero-lede .btn-row,
.hero-lede .status-strip { justify-content: flex-start; }

/* Optical, not mathematical: the rail's numerals are heavy and sit high, so
   nudging the block down a touch makes it read as level with the headline. */
.hero-rail {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--line);          /* the gap IS the rule - 1px grid lines */
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  margin-top: 6px;
}
.stat-cell {
  background: rgba(21, 26, 40, .58);
  padding: 20px 18px;
  backdrop-filter: blur(3px);       /* let the sky through, just barely */
}
.stat-cell .n {
  display: block;
  font-family: var(--font-display);
  font-size: 2.1rem; font-weight: 650; line-height: 1.05;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.stat-cell .l {
  display: block; margin-top: .5em;
  font-family: var(--font-mono);
  font-size: .64rem; text-transform: uppercase; letter-spacing: .2em;
  color: var(--muted);
}

/* Vana'diel's hero is a menu window. A window does not have an editorial
   column layout, so that theme keeps the centred stack it was designed with. */
[data-theme="vanadiel"] .hero-grid { grid-template-columns: 1fr; text-align: center; }
[data-theme="vanadiel"] .hero-lede .btn-row,
[data-theme="vanadiel"] .hero-lede .status-strip { justify-content: center; }
[data-theme="vanadiel"] .hero-lede .tagline { margin-left: auto; margin-right: auto; }

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: 34px; text-align: center; }
  .hero-lede .btn-row,
  .hero-lede .status-strip { justify-content: center; }
  .hero-lede .tagline { margin-left: auto; margin-right: auto; }
}

/* --------------------------------------------------------- spec sheet */

/* Three hairline-separated rows rather than three boxes. The mono index is
   the only ornament and it earns its place by making the set countable. */
.pillars { display: grid; gap: 0; }
.pillar {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 12rem) minmax(0, 1fr);
  gap: 28px;
  align-items: start;
  padding: 30px 0;
  border-top: 1px solid var(--line);
}
.pillar:last-child { border-bottom: 1px solid var(--line); }
.pillar-index {
  font-family: var(--font-mono);
  font-size: .72rem; letter-spacing: .18em;
  color: var(--muted);
  padding-top: .5em;
  font-variant-numeric: tabular-nums;
}
.pillar h3 { margin: 0; }
.pillar ul {
  margin: 0; padding: 0; list-style: none;
  columns: 2; column-gap: 34px;
}
.pillar li {
  margin: 0 0 .55em;
  padding-left: 15px;
  position: relative;
  break-inside: avoid;
  font-size: .95rem;
}
/* A hairline dash rather than a bullet glyph - quieter, and it lines up with
   the rules that separate the rows. */
.pillar li::before {
  content: "";
  position: absolute; left: 0; top: .78em;
  width: 7px; height: 1px;
  background: var(--muted);
  opacity: .7;
}
@media (max-width: 860px) {
  .pillar { grid-template-columns: 2.5rem minmax(0, 1fr); gap: 16px; }
  .pillar h3 { grid-column: 2; }
  .pillar ul { grid-column: 2; columns: 1; }
}

/* ----------------------------------------------------------- CTA band */

.cta-band {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 32px;
  align-items: center;
  padding: 34px 34px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background:
    radial-gradient(120% 180% at 0% 120%, rgba(var(--amb-1), .13), transparent 62%),
    var(--panel);
}
.cta-band h2 { margin-bottom: .3em; }
.cta-band p  { margin: 0; max-width: 52ch; }
@media (max-width: 780px) {
  .cta-band { grid-template-columns: 1fr; padding: 26px 22px; }
}

/* ------------------------------------------------------- status strip */

.status-strip {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 10px 26px; margin-top: 34px; font-size: .9rem;
}
.status-chip {
  display: inline-flex; align-items: center; gap: 9px;
  background: rgba(21, 26, 40, .7); border: 1px solid var(--line);
  padding: 8px 16px; border-radius: 3px;
  font-family: var(--font-mono); font-size: .72rem;
  letter-spacing: .12em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
.dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.dot-on  { background: var(--online); box-shadow: 0 0 9px var(--online); }
.dot-off { background: var(--offline); }

/* ------------------------------------------------------------- cards */

.grid { display: grid; gap: 18px; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

.card {
  position: relative;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 22px;
  transition: border-color .2s var(--ease-out),
              transform .2s var(--ease-out),
              box-shadow .2s var(--ease-out);
}

/* Only cards that are themselves links react to the pointer. A static card
   that lifts on hover promises a click that never happens. */
a.card:hover {
  transform: translateY(-2px);
  border-color: rgba(240, 182, 87, .38);
  box-shadow: var(--shadow-lift);
  text-decoration: none;
}
/* Catching the light along the top edge, consistent with a sky that sits
   above and behind the page. */
a.card::after {
  content: "";
  position: absolute; inset: -1px -1px auto -1px; height: 1px;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  background: linear-gradient(90deg, transparent, var(--gold), transparent);
  opacity: 0;
  transition: opacity .2s var(--ease-out);
}
a.card:hover::after { opacity: .7; }
.card h3 { color: var(--gold); }
.card ul { margin: 0; padding-left: 18px; }
.card li { margin: .3em 0; }

.stat { text-align: center; }
.stat .n {
  font-family: var(--font-display);
  font-size: 2.9rem; font-weight: 600; line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--text); display: block;
  font-variant-numeric: tabular-nums;
}
.stat .l {
  font-family: var(--font-mono);
  color: var(--muted); font-size: .68rem;
  text-transform: uppercase; letter-spacing: .2em;
  display: block; margin-top: .6em;
}

.section { margin: 56px 0; }
.section-head { margin-bottom: 22px; }
.section-head p { color: var(--muted); margin: 0; }

/* Elements arrive as you reach them rather than all at once on load. This is
 * scroll-driven via animation-timeline, so there is no scroll listener, no JS,
 * and nothing to run on the main thread. Browsers without support simply get
 * the finished state.
 *
 * DO NOT add `.section` back to this list. `animation-range` is measured
 * against the element's own height, so a section taller than the viewport --
 * a character's Items list with an 80-row inventory table, for instance --
 * enters the screen with its progress near 0% and renders at near-zero
 * opacity until you scroll far past it. The content looks broken.
 * Only bounded-height elements are safe to animate this way.
 */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .card, .news-item {
      animation: rise-in linear both;
      animation-timeline: view();
      /* Finish well before the element is centred, so reading is never
         waiting on the animation. */
      animation-range: entry 0% cover 26%;
    }
  }
}
@keyframes rise-in {
  from { opacity: .55; transform: translate3d(0, 14px, 0); }
  to   { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------ tables */

table { width: 100%; border-collapse: collapse; }
th {
  text-align: left; font-family: var(--font-mono);
  font-size: .7rem; text-transform: uppercase; letter-spacing: .16em;
  color: var(--muted); font-weight: 400;
  padding: 12px 14px; border-bottom: 1px solid var(--line);
}
td {
  padding: 12px 14px; border-bottom: 1px solid rgba(38, 46, 66, .5);
  font-variant-numeric: tabular-nums;
}
tbody tr:hover td { background: var(--panel-2); }
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); }

.pill {
  display: inline-block; background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 3px; padding: 2px 8px;
  font-family: var(--font-mono); font-size: .68rem; letter-spacing: .08em;
  color: var(--gold); font-variant-numeric: tabular-nums;
}
.rank-num { color: var(--muted); font-variant-numeric: tabular-nums; width: 2.2rem; }
.rank-1 .rank-num { color: var(--gold); font-weight: 700; }

/* ------------------------------------------------------------- forms */

.form-card { max-width: 420px; margin: 0 auto; }
label { display: block; font-size: .85rem; color: var(--muted); margin-bottom: 6px; }
input[type=text], input[type=password], input[type=search] {
  width: 100%; padding: 11px 13px; margin-bottom: 16px;
  background: var(--night-2); border: 1px solid var(--line);
  border-radius: var(--r-sm); color: var(--text); font-size: 1rem;
  transition: border-color .18s var(--ease-out), box-shadow .18s var(--ease-out);
}
/* The old rule stripped the outline and replaced it with a border colour
   change only, which is invisible on a control that already has a border and
   leaves keyboard users with no ring at all. :focus-visible restores a real
   indicator for keyboard traversal without putting a ring on every mouse
   click. */
input:focus { outline: none; border-color: var(--gold); }
input:focus-visible { box-shadow: 0 0 0 3px rgba(240, 182, 87, .28); }

.flash { padding: 12px 16px; border-radius: 7px; margin-bottom: 20px; border: 1px solid; }
.flash-error { background: rgba(255, 107, 107, .1); border-color: rgba(255, 107, 107, .4); color: #ffb3b3; }
.flash-info  { background: rgba(240, 182, 87, .1); border-color: rgba(240, 182, 87, .4); color: var(--gold); }

.linkish { background: none; border: 0; color: var(--gold); cursor: pointer; padding: 0; font: inherit; }
.linkish:hover { text-decoration: underline; }

/* ------------------------------------------------------------- prose */

/* Constrained to a readable measure. The previous 760px at 16px ran to about
   110 characters a line; comfortable reading is 65-75. */
.prose { max-width: var(--measure); }

/* Headings sit in the text colour with a hairline rule above, rather than
   shouting in gold. The rule is what marks a new section - colour is not
   doing structural work it was never good at. */
.prose h2 {
  margin-top: 2.2em;
  padding-top: .8em;
  border-top: 1px solid var(--line);
  color: var(--text);
}
.prose h3 { margin-top: 1.6em; color: var(--text); }
.prose p  { margin: 0 0 1.15em; }
.prose > p:first-of-type { font-size: 1.08rem; color: var(--text); }
.prose code {
  background: var(--night-2); border: 1px solid var(--line);
  padding: 2px 6px; border-radius: 4px; font-size: .88em;
}
.prose pre {
  background: var(--night-2); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 16px; overflow-x: auto;
}
.prose pre code { border: 0; padding: 0; background: none; }
.prose blockquote {
  border-left: 3px solid var(--gold); margin: 1.2em 0;
  padding: .2em 0 .2em 16px; color: var(--muted);
}
.prose table { margin: 1.2em 0; }

/* ------------------------------------------------------------ footer */

.site-footer { border-top: 1px solid var(--line); background: var(--night-2); padding: 44px 0 32px; }
.footer-grid { display: grid; gap: 28px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.footer-grid h4 { color: var(--text); font-size: .9rem; text-transform: uppercase; letter-spacing: .06em; }
.footer-grid a, .footer-grid .linkish { display: block; color: var(--muted); font-size: .9rem; margin: .35em 0; }
.footer-grid a:hover { color: var(--gold); }
.footer-about { grid-column: span 2; }

/* ------------------------------------------------------ live background */

/* Ambient drifting light, built only from large soft radial gradients.
 *
 * This deliberately draws NO objects. An earlier version painted stars as
 * arcs and clouds as ellipses on a canvas, and hard-edged primitives always
 * read as programmer-art. Soft light cannot: there is no shape to judge.
 *
 * The three colours are FFXI crystal elements, chosen per theme. Two layers
 * drift on long, mismatched cycles so the motion never visibly loops.
 */

:root, [data-theme="dawn"] {
  --amb-1: 232, 86, 58;    /* fire      */
  --amb-2: 201, 162, 39;   /* earth     */
  --amb-3: 245, 230, 168;  /* light     */
}
[data-theme="dusk"] {
  --amb-1: 160, 106, 224;  /* lightning */
  --amb-2: 58, 122, 224;   /* water     */
  --amb-3: 255, 123, 200;
}
[data-theme="vanadiel"] {
  --amb-1: 126, 200, 227;  /* ice       */
  --amb-2: 58, 122, 224;   /* water     */
  --amb-3: 130, 201, 254;
}

.ambient {
  position: fixed;
  inset: -20vmax;                 /* overscan so drifting never shows an edge */
  z-index: -2;
  pointer-events: none;
  overflow: hidden;
}

.ambient::before,
.ambient::after {
  content: "";
  position: absolute;
  inset: 0;
  will-change: transform;
}

.ambient::before {
  background:
    radial-gradient(38vmax 30vmax at 22% 18%, rgba(var(--amb-1), .30), transparent 62%),
    radial-gradient(34vmax 28vmax at 78% 30%, rgba(var(--amb-2), .22), transparent 64%);
  animation: drift-a 46s ease-in-out infinite alternate;
}

.ambient::after {
  background:
    radial-gradient(42vmax 32vmax at 62% 82%, rgba(var(--amb-3), .16), transparent 60%),
    radial-gradient(30vmax 26vmax at 12% 74%, rgba(var(--amb-1), .16), transparent 62%);
  animation: drift-b 61s ease-in-out infinite alternate;
}

@keyframes drift-a {
  from { transform: translate3d(-3%, -2%, 0) scale(1);    }
  to   { transform: translate3d( 4%,  3%, 0) scale(1.12); }
}
@keyframes drift-b {
  from { transform: translate3d( 3%,  2%, 0) scale(1.08); }
  to   { transform: translate3d(-4%, -3%, 0) scale(1);    }
}

/* Fine grain over the light. This is what stops large gradients banding into
   visible stripes on 8-bit displays, and it is most of why the result reads
   as finished rather than flat. */
.ambient-grain {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: .22;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}

/* ---------------------------------------------------- the dawn cycle
 *
 * "Living background" here means the light actually goes through a sunrise,
 * not that something is drawn moving across the screen.
 *
 * The constraint above still holds and is deliberate: NO objects. The earlier
 * canvas version painted stars as arcs and clouds as ellipses, and hard-edged
 * primitives read as programmer-art. So nothing here is a thing -- there is no
 * sun disc, no horizon line, no cloud. There is only light doing what light
 * does at dawn: rising from below, warming as it climbs, and washing out the
 * cold that preceded it. Light has no silhouette to judge, which is exactly
 * why it survives scrutiny where a drawn sun would not.
 *
 * Three cycles run at mismatched, non-harmonic periods (197s / 253s / 881s),
 * so the composite never visibly repeats -- the shortest common period is
 * over a year. Everything animates transform and opacity only, so it stays
 * on the compositor and costs no layout.
 */

.horizon {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  overflow: hidden;
  contain: strict;              /* isolate from layout; this layer is decoration */
}

/* The rising warmth. Anchored below the fold and swelling upward, so the page
   is lit from under the content the way a real sunrise lights a room. */
.horizon::before {
  content: "";
  position: absolute;
  left: -25%; right: -25%; bottom: -30vh;
  height: 130vh;
  background:
    radial-gradient(60% 52% at 50% 100%, rgba(var(--amb-1), .34), transparent 68%),
    radial-gradient(46% 40% at 50% 104%, rgba(var(--amb-2), .26), transparent 66%),
    radial-gradient(30% 26% at 50% 108%, rgba(var(--amb-3), .20), transparent 62%);
  transform-origin: 50% 100%;
  will-change: transform, opacity;
  animation: dawn-rise 197s cubic-bezier(.45, 0, .55, 1) infinite;
}

/* The cold the light is displacing. Counter-cycles against the warmth, so at
   one end of the loop the page sits blue and pre-dawn and at the other it has
   been overtaken. This opposition is the whole effect; without it the warm
   layer alone just pulses. */
.horizon::after {
  content: "";
  position: absolute;
  inset: -10%;
  background:
    radial-gradient(70% 46% at 28% 6%, rgba(58, 96, 190, .26), transparent 70%),
    radial-gradient(58% 40% at 82% 0%, rgba(38, 62, 140, .22), transparent 72%);
  will-change: opacity, transform;
  animation: dawn-cool 253s cubic-bezier(.45, 0, .55, 1) infinite;
}

@keyframes dawn-rise {
  0%   { transform: translate3d(0, 16%, 0) scale(1.00); opacity: .42; }
  50%  { transform: translate3d(0,  0%, 0) scale(1.18); opacity: 1;   }
  100% { transform: translate3d(0, 16%, 0) scale(1.00); opacity: .42; }
}
@keyframes dawn-cool {
  0%   { transform: translate3d(0, -2%, 0); opacity: 1;   }
  50%  { transform: translate3d(0, -8%, 0); opacity: .18; }
  100% { transform: translate3d(0, -2%, 0); opacity: 1;   }
}

/* First-light rays. A single very slow rotation, held near the threshold of
   perception -- you should notice the page looks different after a minute
   without being able to say what moved. The radial mask is load-bearing: it
   dissolves the conic gradient's outer edge, which would otherwise read as a
   hard circle and turn this back into a drawn object. */
.horizon .rays {
  position: absolute;
  left: 50%; bottom: -55vmax;
  width: 150vmax; height: 150vmax;
  margin-left: -75vmax;
  opacity: .5;
  background: conic-gradient(from 200deg at 50% 50%,
      transparent 0deg,
      rgba(var(--amb-3), .085) 14deg,
      transparent 30deg,
      transparent 52deg,
      rgba(var(--amb-3), .06)  64deg,
      transparent 82deg,
      transparent 250deg,
      rgba(var(--amb-2), .05) 268deg,
      transparent 292deg,
      transparent 360deg);
  -webkit-mask-image: radial-gradient(closest-side, #000 20%, transparent 76%);
          mask-image: radial-gradient(closest-side, #000 20%, transparent 76%);
  will-change: transform;
  animation: dawn-rays 881s linear infinite;
}
@keyframes dawn-rays {
  from { transform: rotate(0deg);   }
  to   { transform: rotate(360deg); }
}

/* Dusk keeps the machinery but inverts the story: the cold is arriving rather
   than leaving, so the warm layer is the one that recedes. */
[data-theme="dusk"] .horizon::before { animation-direction: reverse; }
[data-theme="dusk"] .horizon::after  { animation-direction: reverse; }

/* Vana'diel is an interface, not scenery -- the standing decision above. It
   gets no sky at all, in any form. */
[data-theme="vanadiel"] .horizon { display: none; }

/* The page body sits above both layers. */
.site-header, .hero, .main, .site-footer { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .ambient::before, .ambient::after { animation: none; }

  /* Not just "stop moving" - a paused animation freezes wherever it happens to
     be, which for the cool layer can be full-strength blue. Park each layer on
     the frame the page was designed around instead. */
  .horizon::before {
    animation: none;
    transform: translate3d(0, 4%, 0) scale(1.12);
    opacity: .9;
  }
  .horizon::after  { animation: none; opacity: .3; }
  .horizon .rays   { animation: none; }
}

/* ------------------------------------------------- Vana'diel theme hero */

/* There is deliberately no simulated sky here. An earlier version drew one on
   a canvas from gradients, arcs and ellipses; it read as programmer-art,
   because that is what it was. Primitives cannot compete with real artwork,
   so this theme does not try. It commits to being an interface instead.
   Do not reintroduce a procedural background. */

/* The hero becomes a menu window - the same chrome as every other panel. On a
   site styled as the game's interface, the opening element being a window is
   the honest move rather than decoration pretending to be scenery. */
[data-theme="vanadiel"] .hero {
  padding: 56px 20px 48px;
  border-bottom: 0;
}
[data-theme="vanadiel"] .hero .wrap {
  border: 1px solid var(--menu-edge);
  border-radius: 4px;
  padding: 46px 28px 40px;
  background: linear-gradient(180deg, #223154 0%, #1a2440 100%);
  box-shadow: inset 0 0 0 1px rgba(4, 7, 15, .9),
              inset 0 0 34px var(--menu-glow),
              0 3px 16px rgba(0, 0, 0, .55);
}

/* The title is set as a menu heading rather than a display face: small, wide
   tracking, uppercase. Restraint is the point. */
[data-theme="vanadiel"] .hero h1 {
  font-size: clamp(1.5rem, 3.4vw, 2.1rem);
  letter-spacing: .16em;
  text-transform: uppercase;
  background: none;
  -webkit-text-fill-color: var(--text);
  color: var(--text);
}
[data-theme="vanadiel"] .hero p.tagline { font-size: 1rem; }

/* --------------------------------------------------------- theme switch */

.theme-switch { display: flex; gap: 4px; align-items: center; }
.theme-btn {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2px solid transparent; cursor: pointer; padding: 0;
  transition: transform .15s ease, border-color .15s ease;
}
.theme-btn:hover { transform: scale(1.15); }
.theme-btn[aria-pressed="true"] { border-color: var(--text); }
.theme-btn-dawn     { background: linear-gradient(135deg, #1c2135, #f0b657); }
.theme-btn-dusk     { background: linear-gradient(135deg, #221a3a, #b89dff); }
.theme-btn-vanadiel { background: linear-gradient(135deg, #060912, #5ad4d4 60%, #ffcb6b); }

/* -------------------------------------------------------- hero portrait */

/* The character face, framed the way FFXI frames your character in the
   status menu: a hard pale edge over the window blue. Renders only when a
   portrait is actually installed - no placeholder, because an empty
   decorative shape is exactly the templated filler this design avoids. */
.hero-portrait {
  display: inline-block;
  margin-bottom: 26px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--panel);
  line-height: 0;
}
[data-theme="vanadiel"] .hero-portrait {
  border-color: var(--menu-edge);
  background: linear-gradient(180deg, #0f2850, #0a1c3c);
  box-shadow: inset 0 0 0 1px rgba(4, 7, 15, .9), 0 3px 14px rgba(0, 0, 0, .55);
}
.hero-portrait img,
.hero-portrait .sprite {
  display: block;
  border-radius: 2px;
  image-rendering: pixelated;
}

/* A multi-frame sheet animates; a single-frame portrait sits still. */
.hero-portrait .sprite {
  background-repeat: no-repeat;
  background-size: calc(var(--frames) * 100%) 100%;
  animation: sprite-cycle steps(var(--frames)) var(--duration, 1s) infinite;
}
@keyframes sprite-cycle {
  from { background-position:    0% 0; }
  to   { background-position: -100% 0; }
}

/* Anyone who has asked their OS for less motion gets a still page. The
   Vana'diel clock text still renders - that is information, not animation. */
@media (prefers-reduced-motion: reduce) {
  .hero-portrait .sprite { animation: none; }
  [data-theme="vanadiel"] .hero .wrap { animation: none; }
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ------------------------------------------------------- profile chart */

.chart { margin: 0; }
.chart figcaption {
  font-size: .78rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); font-weight: 600; margin-bottom: 12px;
}
.job-chart { width: 100%; height: auto; display: block; overflow: visible; }

/* Text wears text tokens, never the series colour - the bar carries identity. */
.job-chart .job-label {
  font: 600 10px ui-monospace, Consolas, monospace;
  fill: var(--text);
}
.job-chart .job-value {
  font: 10px ui-monospace, Consolas, monospace;
  fill: var(--muted);
}
.job-chart .job-ml {
  font: 600 9px ui-monospace, Consolas, monospace;
  fill: var(--gold);
}
.job-chart g:hover rect { filter: brightness(1.25); }

/* ---------------------------------------------------------- equipment */

.equip-grid {
  display: grid; gap: 6px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.equip-slot {
  display: flex; flex-direction: column; gap: 1px;
  padding: 7px 10px; border-radius: 5px;
  background: var(--night-2); border: 1px solid var(--line);
  min-width: 0;
}
.equip-slot.empty { opacity: .4; }
.equip-label {
  font-size: .66rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted);
}
.equip-item {
  font-size: .84rem; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
[data-theme="vanadiel"] .equip-item { color: var(--ember); }  /* item green */

/* ------------------------------------------------------------ mobile */

@media (max-width: 780px) {
  .nav-burger {
    display: block; margin-left: auto; font-size: 1.5rem;
    color: var(--text); cursor: pointer; user-select: none;
  }
  .nav {
    display: none; position: absolute; top: 62px; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--night-2); border-bottom: 1px solid var(--line); padding: 8px 20px 16px;
  }
  .nav-toggle:checked ~ .nav { display: flex; }
  .nav a { padding: 11px 0; border-bottom: 1px solid var(--line); }
  .nav-cta { text-align: center; margin-top: 12px; border-bottom: 0; }
  .footer-about { grid-column: span 1; }
  .hero { padding: 62px 20px 54px; }
}

/* ------------------------------------------------------------ news list */

.news-list { display: grid; gap: 2px; }

.news-item {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: 20px;
  padding: 20px 22px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: inherit;
}
.news-item:hover { text-decoration: none; background: var(--panel-2); }
.news-item time {
  color: var(--muted); font-size: .72rem;
  letter-spacing: .12em; text-transform: uppercase;
  padding-top: .35em;
}
.news-item h3 { margin: 0 0 .25em; font-size: 1.15rem; }
.news-item p  { margin: 0; font-size: .93rem; }

@media (max-width: 640px) {
  .news-item { grid-template-columns: 1fr; gap: 6px; }
}

/* -------------------------------------------------------------- events */

.event-time {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin: 1em 0 0;
  padding-top: .9em; border-top: 1px solid var(--line);
}
.event-label {
  font-family: var(--font-mono); font-size: .66rem;
  letter-spacing: .18em; text-transform: uppercase; color: var(--muted);
}
.event-time .mono {
  font-size: 1.05rem; color: var(--gold);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------- item icons */

/* FFXI item icons are 32x32. Rendered at their native size with crisp
   scaling - these are pixel art and smoothing them looks wrong. Served
   straight off nginx from static/icons/<itemid>.png. */
.item-icon {
  width: 32px; height: 32px;
  image-rendering: pixelated;
  flex: none;
  border-radius: 3px;
  background: rgba(0, 0, 0, .25);
}
.item-icon-sm { width: 24px; height: 24px; }

.item-cell { display: flex; align-items: center; gap: 11px; }

.item-head { display: flex; align-items: center; gap: 16px; }
.item-head .item-icon { width: 48px; height: 48px; }

/* Worn item: icon beside the name, name allowed to truncate rather than
   breaking the slot grid. */
.equip-worn { display: flex; align-items: center; gap: 8px; min-width: 0; }
.equip-worn .equip-item {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ------------------------------------------------------- gear browser */

.filters {
  display: flex; flex-wrap: wrap; gap: 16px 20px; align-items: flex-end;
  margin-bottom: 26px;
}
.filters label {
  display: block; font-family: var(--font-mono); font-size: .66rem;
  letter-spacing: .18em; text-transform: uppercase; color: var(--muted);
  margin-bottom: 6px;
}
.filters select, .filters input {
  background: var(--night-2); border: 1px solid var(--line);
  border-radius: 6px; color: var(--text); font: inherit;
  font-size: .92rem; padding: 9px 11px; margin: 0; min-width: 11rem;
}
.filters input { min-width: 7rem; }
.filters select:focus, .filters input:focus { outline: none; border-color: var(--gold); }
.filters-actions { display: flex; gap: 10px; }
.filters-actions .btn { padding: 9px 20px; font-size: .92rem; }

.gear-grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.gear-card {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 13px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--panel); color: inherit; min-width: 0;
}
.gear-card:hover { text-decoration: none; background: var(--panel-2); border-color: var(--gold); }
.gear-meta { display: flex; flex-direction: column; min-width: 0; }
.gear-name {
  font-size: .93rem; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.gear-sub { font-family: var(--font-mono); font-size: .66rem; letter-spacing: .1em; text-transform: uppercase; }
