/* ===========================================================================
   skins.css — the cosmetic overlay.

   Loaded AFTER styles.css on every page, and deliberately a separate file. The
   split is a one-way dependency: styles.css must never reference anything in
   here, so the app is fully styled with this file absent and a broken skin can
   only ever look wrong, never break a layout. If you find app CSS depending on
   a skin selector, that's the bug.

   Everything here hangs off data attributes that public/cosmetics.js sets on
   <html>:  data-skin, data-bg, data-fx-*.

   THE BOUNDARY: skins set chrome tokens only. Category colours (.c-* and
   .la-*) and rarity marks (.lr-*) appear nowhere in this file and must not be
   added — a skin that recolours the semantic layer makes a higher level harder
   to read.

   (That line used to join the two selector names with a slash — c-star, then
   slash, with nothing between them. An asterisk immediately followed by a
   slash CLOSED THIS COMMENT three lines early. Everything after it became live
   CSS, and the parser's error recovery ate the next rule — the one holding
   Standard Issue's three pattern inks. So on the DEFAULT skin, every background
   drew with undefined --fx-* and no test could see it: the file is valid CSS
   either way, and the rule is right there in the source when you grep for it.

   Note this comment does not spell that selector out. test-cosmetics.js finds a
   skin's block by splitting the file on the selector TEXT, so naming it here
   sends that assertion into the prose instead of the rule — which is exactly
   what happened on the first attempt at writing this note.

   Never write `*` immediately before `/` in a comment here. Same shape as the
   backtick-inside-an-HTML-comment-inside-a-template-literal that silently
   emptied the Spellbook.)
   =========================================================================== */

/* ---------------------------------------------------------------------------
   1. SKINS
   Each is a copy of the token block from styles.css and nothing else. That's
   the whole mechanism: no per-page rules, no component overrides, so a skin
   restyles every page that exists and every page added later for free.

   Three temperatures rather than three hues — neutral, cold, warm. Two cool
   skins read as one skin with a tint, and the second one then feels like
   nothing was earned.

   Each skin also supplies --fx-1/2/3, which are the inks the BACKGROUND
   patterns draw with. That's what makes the axes multiply instead of add:
   seven backgrounds times three skins is twenty-one distinct looks out of ten
   blocks of CSS, and a background always belongs to whatever skin it's worn
   with rather than fighting it.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   A SKIN CAN NOW BE WORN BY AN ELEMENT, NOT JUST BY THE PAGE.

   Each token block below answers to two selectors:

     html[data-skin="x"]                 the viewer's own page, as always
     .brand-card[data-skin-card="x"]     one card wearing SOMEBODY ELSE'S skin

   The second exists for the feed: a post is drawn in the poster's colours, so
   the feed reads as a row of people rather than a row of rows. It works at all
   — and costs nothing — precisely because a skin was built as custom properties
   and nothing else. The values are not duplicated; the same block simply has a
   second way in, and custom properties inherit down into the card and stop
   there.

   TWO REASONS IT CANNOT LEAK, both structural:
   1. Custom properties inherit DOWN. A card wearing Arcane tints its own
      surfaces and its own descendants, and can't reach the page around it.
   2. `data-skin-card` is a DIFFERENT attribute from `data-skin`, so none of the
      html-scoped rules further down this file — the ambience overlay, the
      background patterns — can be triggered by a card. A post cannot give
      somebody an aurora.

   The category-colour rule survives untouched and that matters more here than
   anywhere: `.c-*` and `.la-*` are hardcoded and skins never define them, so a
   Botanicals chip is the same green inside a stranger's Arcane card as it is on
   your own page. Branding is chrome; meaning is not for sale.
   --------------------------------------------------------------------------- */

/* Standard Issue — the original theme, named so it can be chosen deliberately
   rather than being "no skin". Tokens match :root exactly; it's here to be
   selectable, not to change anything. */
/* The pattern inks. Kept low on purpose: these are multiplied across a whole
   viewport, so a value that looks tasteful on a 200px swatch reads as a wash at
   full size. Anything above ~0.10 alpha starts producing a visible dark-to-light
   sweep across the page rather than a texture. */
html[data-skin="standard"] {
  --fx-1: rgba(120,160,220,0.075);
  --fx-2: rgba(160,150,200,0.055);
  --fx-3: rgba(140,190,170,0.065);
}

/* Standard as a CARD has to say the quiet part out loud.
   On <html> this skin needs no tokens — it IS `:root`, and there is nothing to
   override. A card is different: it sits inside whatever skin the VIEWER is
   wearing, so "no tokens" would mean a Standard poster's card silently borrowed
   the reader's Arcane. That's the opposite of branding — it would make every
   unskinned person look like whoever is reading.
   So this block restates :root, which is the one place in this file that
   duplicates values. The assertion in test-cosmetics.js checks it stays in step
   with :root, because two copies of a palette is exactly the thing that drifts. */
.brand-card[data-skin-card="standard"] {
  --surface-0: #131419;
  --surface-1: #191b21;
  --surface-2: #20222a;
  --surface-3: #2a2c36;
  --border: rgba(255,255,255,0.08);
  --border-strong: rgba(255,255,255,0.18);
  --border-accent: #5b9cf2;
  --text-primary: #eeece6;
  --text-secondary: #9c9a95;
  --text-muted: #6b6a64;
  --text-accent: #7db2f5;
  --text-success: #6fd196;
  --bg-success: rgba(111,209,150,0.14);
  --accent-fg: #0d1117;
  --accent-hover: #6ea8f4;
  --danger: #e88;
  --xp: #9dc0f0;
  --coin: #e8c877;
  --warn-text: #dbc08a;
  --warn-bg: rgba(224,189,82,0.09);
  --warn-border: rgba(224,189,82,0.32);
  --chrome-bg: var(--surface-1);
  --glow: none;
  --fx-1: rgba(120,160,220,0.075);
  --fx-2: rgba(160,150,200,0.055);
  --fx-3: rgba(140,190,170,0.065);
}

/* Arcane — near-black voids lit by violet and rune blue, from the user's own
   style guide. The one place it departs: text is warmed slightly off the
   guide's #d8cfdf, because the app sets long-form journal and field-note copy
   at 13px and a cool grey at that size on a violet-black ground sits under AA.
   Contrast beat fidelity. */
html[data-skin="arcane"],
.brand-card[data-skin-card="arcane"] {
  --surface-0: #09070f;
  --surface-1: #171321;
  --surface-2: #221b2d;
  --surface-3: #2e2540;
  --border: rgba(216,207,223,0.12);
  --border-strong: rgba(216,207,223,0.24);
  --border-accent: #a96cff;
  --text-primary: #e2dae8;
  --text-secondary: #a99cb8;
  --text-muted: #7d7189;
  --text-accent: #54d8ff;
  --text-success: #71f6b6;
  --bg-success: rgba(113,246,182,0.12);
  --accent-fg: #120a1c;
  --accent-hover: #bb85ff;
  --danger: #ff8f9c;
  --xp: #54d8ff;
  --coin: #ffae68;
  --warn-text: #e8af7e;
  --warn-bg: rgba(255,174,104,0.10);
  --warn-border: rgba(255,174,104,0.34);
  --chrome-bg: rgba(23,19,33,0.92);
  --glow: 0 0 0 1px rgba(169,108,255,0.45), 0 0 18px rgba(169,108,255,0.22);

  --fx-1: rgba(169,108,255,0.095);
  --fx-2: rgba(84,216,255,0.065);
  --fx-3: rgba(113,246,182,0.055);
}

/* Emberlight — warm candlelight over old leather. Deliberately the calmest of
   the three for reading: the warmest ground, the lowest-contrast pattern inks,
   and no cool light anywhere to fight the text. */
html[data-skin="emberlight"],
.brand-card[data-skin-card="emberlight"] {
  --surface-0: #14100d;
  --surface-1: #1c1613;
  --surface-2: #261e19;
  --surface-3: #332822;
  --border: rgba(232,213,176,0.11);
  --border-strong: rgba(232,213,176,0.22);
  --border-accent: #ffae68;
  --text-primary: #ede2cf;
  --text-secondary: #b5a68e;
  --text-muted: #857868;
  --text-accent: #ffc490;
  --text-success: #a8c98a;
  --bg-success: rgba(168,201,138,0.12);
  --accent-fg: #1a1108;
  --accent-hover: #ffc088;
  --danger: #f0908a;
  --xp: #ffc490;
  --coin: #ffd27a;
  --warn-text: #edce90;
  --warn-bg: rgba(255,210,122,0.10);
  --warn-border: rgba(255,210,122,0.34);
  --chrome-bg: rgba(28,22,19,0.94);
  --glow: 0 0 0 1px rgba(255,174,104,0.40), 0 0 16px rgba(255,174,104,0.18);

  --fx-1: rgba(255,174,104,0.075);
  --fx-2: rgba(217,164,65,0.055);
  --fx-3: rgba(232,213,176,0.04);
}

/* ---------------------------------------------------------------------------
   2. BACKGROUNDS
   Math-drawn only — gradients and geometry, never an image. That keeps the
   nothing-loads-from-a-CDN rule intact, costs zero bytes and zero requests,
   scales to any viewport and can't 404. Adapted from the user's style guide.

   Each sets --app-bg, which styles.css already applies to <body> with
   background-attachment: fixed so a long page scrolls over a still ground
   rather than dragging the pattern with it.
   --------------------------------------------------------------------------- */

/* Size and position ride as tokens alongside the image, because the attribute
   is on <html> and the background paints on <body>. Declaring `background-size`
   here instead would silently do nothing — <html>'s own background isn't the
   one being drawn — and every pattern would render at its default single tile
   stretched across the viewport. Each layer's size must be listed in the same
   order as the images or they pair up wrong. */

html[data-bg="none"] { --app-bg: none; }

/* ---- the smooth family ----
   These deliberately DON'T tile. The first version of Ether Smoke sized its
   radials at 220x180px and let them repeat, which turned a haze into a visible
   grid — the pattern read as squares because it was squares. Sizing to the
   viewport with no-repeat is what makes haze look like haze.

   The stops are also long and the peaks low. A gradient that runs from full
   colour to transparent in 35% of its radius produces a hard edge and a wide
   dark-to-light sweep; carrying it out past 70% with a mid stop keeps the whole
   thing inside a narrow value range, which is what "subtle" actually means
   here. */
html[data-bg="smoke"] {
  --app-bg:
    radial-gradient(58% 46% at 16% 10%, var(--fx-1), transparent 72%),
    radial-gradient(52% 48% at 84% 26%, var(--fx-2), transparent 74%),
    radial-gradient(66% 52% at 42% 92%, var(--fx-3), transparent 76%),
    radial-gradient(46% 38% at 6% 68%, var(--fx-2), transparent 70%);
  --app-bg-size: 100% 100%;
  --app-bg-repeat: no-repeat;
}

/* Same palette, a single soft diagonal wash. The calmest of the three. */
html[data-bg="haze"] {
  --app-bg:
    linear-gradient(147deg, var(--fx-1) 0%, transparent 42%),
    linear-gradient(21deg, var(--fx-2) 0%, transparent 38%),
    radial-gradient(80% 60% at 50% 105%, var(--fx-3), transparent 78%);
  --app-bg-size: 100% 100%;
  --app-bg-repeat: no-repeat;
}

/* Same palette, broader and more layered — overlapping fields that never quite
   resolve into a shape. */
html[data-bg="nebula"] {
  --app-bg:
    radial-gradient(70% 50% at 78% 8%, var(--fx-1), transparent 68%),
    radial-gradient(60% 70% at 12% 34%, var(--fx-2), transparent 72%),
    radial-gradient(90% 45% at 55% 72%, var(--fx-3), transparent 70%),
    linear-gradient(200deg, var(--fx-1) 0%, transparent 55%);
  --app-bg-size: 100% 100%;
  --app-bg-repeat: no-repeat;
}

html[data-bg="blueprint"] {
  --app-bg:
    linear-gradient(var(--fx-2) 1px, transparent 1px),
    linear-gradient(90deg, var(--fx-1) 1px, transparent 1px),
    radial-gradient(circle, var(--fx-3) 1px, transparent 1.5px);
  --app-bg-size: 32px 32px, 32px 32px, 16px 16px;
}

html[data-bg="runes"] {
  --app-bg:
    linear-gradient(30deg, var(--fx-1) 12%, transparent 12.5% 87%, var(--fx-1) 87.5%),
    linear-gradient(150deg, var(--fx-2) 12%, transparent 12.5% 87%, var(--fx-2) 87.5%),
    linear-gradient(30deg, var(--fx-1) 12%, transparent 12.5% 87%, var(--fx-1) 87.5%),
    linear-gradient(150deg, var(--fx-2) 12%, transparent 12.5% 87%, var(--fx-2) 87.5%);
  --app-bg-size: 48px 84px;
  --app-bg-pos: 0 0, 0 0, 24px 42px, 24px 42px;
}

html[data-bg="vellum"] {
  --app-bg:
    repeating-radial-gradient(ellipse at 20% 30%, rgba(255,255,255,0.022) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(107deg, transparent 0 8px, rgba(255,255,255,0.016) 9px, transparent 10px 17px),
    radial-gradient(circle at 20% 10%, var(--fx-1), transparent 45%);
  --app-bg-size: 120px 90px, 80px 80px, 100% 100%;
}

html[data-bg="alchemy"] {
  --app-bg:
    radial-gradient(circle at center, transparent 0 7px, var(--fx-3) 8px 9px, transparent 10px),
    radial-gradient(circle at center, transparent 0 16px, var(--fx-1) 17px 18px, transparent 19px),
    linear-gradient(45deg, transparent 48%, var(--fx-2) 49% 51%, transparent 52%),
    linear-gradient(-45deg, transparent 48%, var(--fx-2) 49% 51%, transparent 52%);
  --app-bg-size: 72px 72px;
}

html[data-bg="crystal"] {
  --app-bg:
    linear-gradient(115deg, transparent 0 38%, var(--fx-2) 39% 41%, transparent 42%),
    linear-gradient(65deg, transparent 0 42%, var(--fx-1) 43% 45%, transparent 46%),
    linear-gradient(155deg, transparent 0 47%, var(--fx-3) 48% 50%, transparent 51%);
  --app-bg-size: 76px 92px, 92px 68px, 108px 84px;
}

/* ---- Herbalist ladder backgrounds (Professions v3) ----
   These carry their OWN botanical green rather than the skin's --fx inks: the
   whole point is that they read as the green path, on any skin. Image-free, like
   every background here. */
html[data-bg="herb-canopy"] {
  --app-bg:
    radial-gradient(70px 90px at 18% 28%, rgba(74,190,45,0.10), transparent 72%),
    radial-gradient(100px 70px at 72% 55%, rgba(46,150,70,0.085), transparent 72%),
    radial-gradient(84px 104px at 48% 82%, rgba(120,200,90,0.06), transparent 72%);
  --app-bg-size: 380px 340px;
}
html[data-bg="herb-trellis"] {
  --app-bg:
    repeating-linear-gradient(45deg, rgba(74,190,45,0.06) 0 1px, transparent 1px 24px),
    repeating-linear-gradient(-45deg, rgba(74,190,45,0.055) 0 1px, transparent 1px 24px);
}
html[data-bg="herb-rings"] {
  --app-bg:
    repeating-radial-gradient(circle at 28% 22%, transparent 0 30px, rgba(74,190,45,0.05) 30px 31px, transparent 31px 62px),
    repeating-radial-gradient(circle at 80% 78%, transparent 0 34px, rgba(46,150,70,0.04) 34px 35px, transparent 35px 70px);
}

/* ---------------------------------------------------------------------------
   3. AMBIENCE — the skin's own idle motion.

   A fixed, pointer-events-none overlay behind the app. It's a separate layer
   from --app-bg on purpose: the background is a chosen pattern and should sit
   still, while ambience is the skin breathing, and animating the pattern itself
   would make body text crawl.

   Very slow on purpose. The premium version of this reads as "the page is
   alive" and the cheap version reads as a screensaver; the difference is almost
   entirely duration. Nothing here runs faster than a 20-second cycle.
   --------------------------------------------------------------------------- */

/* inset MUST stay 0. An earlier version used -20% to give the drift somewhere
   to travel, and a fixed element with a negative inset still extends the
   scrollable area — it put 254px of horizontal overflow on every page and broke
   the "zero horizontal overflow at 390px" guarantee the responsive pass
   established. The drift pans the gradient INSIDE the box instead (see
   --ambience-size below), which costs nothing and can't overflow. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: var(--ambience, none);
  /* Oversized so there's something to pan across; the box itself never moves. */
  background-size: var(--ambience-size, 180% 180%);
  background-position: 50% 50%;
}
/* The app sits above the overlay. The top bar already carries z-index 900 and
   is unaffected; this is only needed for the shell below it. */
.app-shell { position: relative; z-index: 1; }

html[data-skin="arcane"] {
  --ambience:
    radial-gradient(circle at 18% 12%, rgba(169,108,255,0.11), transparent 58%),
    radial-gradient(circle at 82% 78%, rgba(84,216,255,0.07), transparent 54%);
  animation: cm-drift 48s ease-in-out infinite alternate;
}

html[data-skin="emberlight"] {
  --ambience:
    radial-gradient(circle at 12% 88%, rgba(255,174,104,0.10), transparent 56%),
    radial-gradient(circle at 88% 8%, rgba(217,164,65,0.06), transparent 52%);
  animation: cm-flicker 21s ease-in-out infinite;
}

/* Drift pans the oversized gradient across a stationary box. Nothing moves in
   layout terms, so there is no edge to reveal and nothing to overflow. The
   range is small on purpose — this should read as light shifting, not as a
   panning camera. */
@keyframes cm-drift {
  from { background-position: 38% 42%; }
  to   { background-position: 62% 58%; }
}

/* Candlelight, not a strobe. The stops stay uneven — that unevenness is what
   makes it read as a flame guttering rather than as a pulse — but the SWING is a
   quarter of what it was, at the user's request: every stop's distance from full
   opacity is scaled by 0.25, so the shape of the animation is unchanged and only
   its depth moved. It was 0.85-1.0 and is now 0.963-1.0.

   Kept as an animation rather than made static because the shape is still there
   to turn back up; if it ever needs to stop entirely, drop the `animation` line
   on the skin rather than flattening these stops. */
@keyframes cm-flicker {
  0%   { opacity: 0.980; }
  17%  { opacity: 1; }
  31%  { opacity: 0.963; }
  48%  { opacity: 0.993; }
  62%  { opacity: 0.970; }
  79%  { opacity: 1; }
  100% { opacity: 0.980; }
}

/* ---------------------------------------------------------------------------
   4. FLOURISHES — the separate component axis.

   Each is one toggle attaching one small idle behaviour to one kind of element.
   Independent of skin, so a calm skin can still carry a lively badge.

   These default OFF even once unlocked (cosmetics.js stores only what's been
   switched on). Earning a flourish gives you the choice, not the animation —
   which is the restraint rule: these are the things most likely to make the app
   feel cheap if they all run at once.
   --------------------------------------------------------------------------- */

/* Rune corners — opposing corner sigils on record cards, from the style guide's
   rune panel. Static, and that's deliberate: it's the one flourish that lands
   on every card at once, so motion here would be motion everywhere. */
/* The full card family (the runtime's CARD_SELECTOR), not just .record-card —
   a bought flourish that skips the Loot Table and the quest board is a
   flourish that looks broken on most of the app. Kept in step with
   effects-run.js's CARD_SELECTOR: if a card class joins there, it joins here. */
html[data-fx-runecorners] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card) { position: relative; }
html[data-fx-runecorners] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card)::before,
html[data-fx-runecorners] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card)::after {
  content: '✦';
  position: absolute;
  font-size: 9px;
  line-height: 1;
  color: var(--border-accent);
  opacity: 0.45;
  pointer-events: none;
}
html[data-fx-runecorners] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card)::before { top: 5px; left: 6px; }
html[data-fx-runecorners] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card)::after { right: 6px; bottom: 5px; }

/* Card sheen — a slow light sweep. Staggered with nth-child delays so a grid
   never sweeps in unison, which is what separates this from a loading skeleton.
   Clipped to the card, and the card keeps its own overflow otherwise. */
html[data-fx-cardsheen] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card) { position: relative; overflow: hidden; }
html[data-fx-cardsheen] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card) > * { position: relative; z-index: 1; }
html[data-fx-cardsheen] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card)::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.05) 50%, transparent 60%);
  background-size: 300% 100%;
  animation: cm-sheen 9s ease-in-out infinite;
}
html[data-fx-cardsheen] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card):nth-child(3n+2)::after { animation-delay: 3s; }
html[data-fx-cardsheen] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card):nth-child(3n+3)::after { animation-delay: 6s; }

/* Both flourishes want ::after on a card. Rune corners wins the bottom-right
   sigil back when they're worn together, because losing an ornament you chose
   is worse than losing a sweep you may not notice on one card. */
html[data-fx-runecorners][data-fx-cardsheen] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card)::after {
  content: '✦';
  background: none;
  animation: none;
  inset: auto 6px 5px auto;
  font-size: 9px;
  color: var(--border-accent);
  opacity: 0.45;
}

@keyframes cm-sheen {
  0%, 100% { background-position: 200% 0; }
  50%      { background-position: -100% 0; }
}

/* Badge ring — a slow rotating ring on the account avatar. `conic-gradient` in
   a mask would be cleaner but masks are the one part of this that varies by
   engine, so it's a dashed border rotating instead: same read, no mask.
   Both avatars, because the badge is account-wide and appearing on the profile
   but not the top-bar chip would look like a bug. */
html[data-fx-badgering] .profile-avatar,
html[data-fx-badgering] .account-avatar { position: relative; }
html[data-fx-badgering] .profile-avatar::after,
html[data-fx-badgering] .account-avatar::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  border: 1px dashed var(--border-accent);
  opacity: 0.5;
  pointer-events: none;
  animation: cm-spin 24s linear infinite;
}

@keyframes cm-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* Active glow — the skin's own light on selected and focused things. Only ever
   applied to state, never to resting elements: a glow on every panel reads as a
   nightclub, a glow on the selected one reads as focus. */
html[data-fx-activeglow] :is(.record-card, .loot-card, .quest-card, .shared-card, .codex-card).selected,
html[data-fx-activeglow] .nav-item.active,
html[data-fx-activeglow] .view-switch-btn.active,
html[data-fx-activeglow] .is-editing {
  box-shadow: var(--glow);
}

/* ---------------------------------------------------------------------------
   5. REDUCED MOTION
   Every animation above is decoration, so this is a blanket stop rather than a
   per-animation judgement — there is nothing here whose meaning depends on it
   moving. The colours, patterns and sigils all stay; only the motion goes.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  body::before,
  html[data-fx-cardsheen] .record-card::after,
  html[data-fx-badgering] .profile-avatar::after,
  html[data-fx-badgering] .account-avatar::after {
    animation: none !important;
  }
}

/* Verdigris — oxidised copper. Cool but GREEN, which is the axis that keeps it
   distinct from Arcane: same rough darkness, completely different hue family,
   and lower chroma so it reads as weathered metal rather than as lit. */
html[data-skin="verdigris"],
.brand-card[data-skin-card="verdigris"] {
  --surface-0: #0c110f;
  --surface-1: #141b18;
  --surface-2: #1c2521;
  --surface-3: #26312c;
  --border: rgba(206,224,214,0.11);
  --border-strong: rgba(206,224,214,0.23);
  --border-accent: #6fbfa0;
  --text-primary: #dfe7e0;
  --text-secondary: #9db0a6;
  --text-muted: #728279;
  --text-accent: #7fd4c0;
  --text-success: #8ce0a8;
  --bg-success: rgba(140,224,168,0.13);
  --accent-fg: #08110d;
  --accent-hover: #86cfb2;
  --danger: #e8908c;
  --xp: #7fd4c0;
  --coin: #d9b878;
  --warn-text: #ccb588;
  --warn-bg: rgba(217,184,120,0.10);
  --warn-border: rgba(217,184,120,0.34);
  --chrome-bg: rgba(20,27,24,0.92);
  --glow: 0 0 0 1px rgba(111,191,160,0.4), 0 0 16px rgba(111,191,160,0.18);

  --fx-1: rgba(111,191,160,0.085);
  --fx-2: rgba(217,184,120,0.05);
  --fx-3: rgba(127,212,192,0.05);
}

/* Bone & Ink — the high-contrast one, and contrast is the WHOLE POINT of it.
   Where the others sit around 4-6:1 on body text this pushes near-white paper
   over a near-black ground, for reading in daylight or with tired eyes. Its
   accent is the only cool note; everything else is warm paper. */
html[data-skin="boneink"],
.brand-card[data-skin-card="boneink"] {
  --surface-0: #08080a;
  --surface-1: #0c0c0e;
  --surface-2: #141417;
  --surface-3: #1e1e22;
  --border: rgba(245,240,228,0.14);
  --border-strong: rgba(245,240,228,0.30);
  --border-accent: #6f9ce0;
  --text-primary: #f5f0e4;
  --text-secondary: #c3bdae;
  --text-muted: #8d887c;
  --text-accent: #8fb4ea;
  --text-success: #86d69c;
  --bg-success: rgba(134,214,156,0.12);
  --accent-fg: #07080b;
  --accent-hover: #8bb0e8;
  --danger: #eb9a95;
  --xp: #8fb4ea;
  --coin: #e0c58a;
  --warn-text: #d5c299;
  --warn-bg: rgba(224,197,138,0.10);
  --warn-border: rgba(224,197,138,0.34);
  --chrome-bg: rgba(12,12,14,0.94);
  --glow: 0 0 0 1px rgba(111,156,224,0.36);

  --fx-1: rgba(245,240,228,0.05);
  --fx-2: rgba(111,156,224,0.045);
  --fx-3: rgba(245,240,228,0.03);
}

/* Foxfire — bioluminescence in a dark wood. Distinct by SATURATION rather than
   by hue or brightness: the ground is the darkest here by some way and the
   accents are the most chromatic, so the light looks like it is coming from
   the interface rather than falling on it. Body text stays a low-chroma pale
   green — a fully saturated text colour at this contrast is unreadable. */
html[data-skin="foxfire"],
.brand-card[data-skin-card="foxfire"] {
  --surface-0: #050706;
  --surface-1: #080b09;
  --surface-2: #0e1310;
  --surface-3: #161d18;
  --border: rgba(160,232,186,0.13);
  --border-strong: rgba(160,232,186,0.26);
  --border-accent: #7ef2a8;
  --text-primary: #c9f7d8;
  --text-secondary: #8fb79c;
  --text-muted: #647a6c;
  --text-accent: #6fe8d0;
  --text-success: #7ef2a8;
  --bg-success: rgba(126,242,168,0.14);
  --accent-fg: #041008;
  --accent-hover: #97f5b9;
  --danger: #ff9d94;
  --xp: #6fe8d0;
  --coin: #e6cf7a;
  --warn-text: #d9c98d;
  --warn-bg: rgba(230,207,122,0.10);
  --warn-border: rgba(230,207,122,0.34);
  --chrome-bg: rgba(8,11,9,0.93);
  --glow: 0 0 0 1px rgba(126,242,168,0.42), 0 0 20px rgba(126,242,168,0.24);

  --fx-1: rgba(126,242,168,0.075);
  --fx-2: rgba(111,232,208,0.055);
  --fx-3: rgba(230,207,122,0.035);
}

/* Wildwood — the Herbalist ladder's signature skin. A warm LIVING forest green,
   distinct from Verdigris (oxidised, desaturated) and Foxfire (near-black glow):
   deep ground, leaf-light accent. */
html[data-skin="wildwood"],
.brand-card[data-skin-card="wildwood"] {
  --surface-0: #070c08;
  --surface-1: #0c130e;
  --surface-2: #121b14;
  --surface-3: #1b271e;
  --border: rgba(150,215,160,0.13);
  --border-strong: rgba(150,215,160,0.26);
  --border-accent: #74cf8a;
  --text-primary: #dcecdc;
  --text-secondary: #9cbaa2;
  --text-muted: #6c8472;
  --text-accent: #8fd6a0;
  --text-success: #7fdc93;
  --bg-success: rgba(116,207,138,0.14);
  --accent-fg: #06120a;
  --accent-hover: #93dea7;
  --danger: #f0968f;
  --xp: #8fd6a0;
  --coin: #e6cf7a;
  --warn-text: #d9c98d;
  --warn-bg: rgba(230,207,122,0.10);
  --warn-border: rgba(230,207,122,0.34);
  --chrome-bg: rgba(12,19,14,0.93);
  --glow: 0 0 0 1px rgba(116,207,138,0.42), 0 0 20px rgba(116,207,138,0.22);

  --fx-1: rgba(116,207,138,0.07);
  --fx-2: rgba(143,214,160,0.05);
  --fx-3: rgba(230,207,122,0.03);
}
