/* ===========================================================================
   GILDED SLATE - the Canaan Online panel language.
   One shared stylesheet for every page under new-spec/ui/.
   Panel pages must not restyle this; new needs get a name here.
   Everything is file:// safe - no network, no imports beyond the local fonts.
   =========================================================================== */

@import url("/vendor/fonts/fonts.css");

/* No text selection anywhere - drag is a game gesture, not a highlight. Inputs keep it. */
* { user-select: none; -webkit-user-select: none; }
input, textarea, [contenteditable] { user-select: text; -webkit-user-select: text; }

/* ---------------------------------------------------------------- TOKENS */
/* STONEBEVEL. Every value below comes from the design system in
   labs/tibia-design-system; the names are Gilded Slate's, kept so the ~290 var() call sites
   across panels.css, auth.css, game.css and src/client/ui.ts retheme without being touched.
   Read tokens/colors.css and implementation-spec.md there before changing a value here.

   Non-negotiables this block enforces: zero radius, a 1px hard border outside a 2px bevel,
   no gradients, no blur, hard-offset zero-blur drop shadows, instant hover and press,
   monospace-only type on a fixed px ladder. */

/* The one knob that sizes the whole always-on HUD (see .gs-icon and src/client/ui.ts).
   This value is what every player gets: nothing in the game writes a different one any more.
   setHudScale still runs once at mount, but only to replay whatever localStorage already holds,
   so this declaration is the effective scale unless an old stored value survives.
   It lives on :root and NOT on .gs with the rest: setHudScale writes it inline on <html> and
   reads it back off <html>, and a value declared on .gs (which is on <body> and on the HUD root)
   beats one inherited from above it - so declaring it there made the write inert. */
:root { --gs-hud-scale: 1.15; }

/* --------------------------------------------------------- BASE RAMPS
   Ported verbatim from labs/tibia-design-system/tokens/colors.css. Nothing outside this
   block may use a raw ramp value as a text colour - the surfaces flip between themes and
   the ramp stops do not. Author against the semantic --gs-* names below. */
:root {
  /* Stone - the client chrome. Neutral cool slate, near-zero chroma. */
  --stone-950: #0b0b0c;
  --stone-900: #121213;
  --stone-850: #19191b;
  --stone-800: #202022;
  --stone-700: #2c2c2f;
  --stone-600: #3a3a3d;
  --stone-500: #474749;
  --stone-450: #555557;
  --stone-400: #696a6c;
  --stone-300: #848587;
  --stone-200: #a7a8aa;
  --stone-150: #babbbd;
  --stone-100: #cdced0;
  --stone-50:  #e8e9ea;

  /* Parchment - the document surface (scrolls, quest text, letters). */
  --parch-50:  #fbf6e8;
  --parch-100: #f2e8cf;
  --parch-200: #e5d7b3;
  --parch-300: #d3c096;
  --parch-400: #bda877;
  --parch-500: #9c8455;
  --parch-600: #75603c;
  --parch-700: #52422a;
  --parch-800: #33291b;
  --parch-900: #1e180f;

  /* Brass - the single brand accent. Titles, selection, currency, focus. */
  --brass-800: #543a0f;
  --brass-700: #6b4c16;
  --brass-600: #8a6420;
  --brass-500: #b8862f;
  --brass-400: #d9a441;
  --brass-300: #ecc472;

  /* Five status hues, meaning fixed and never reassigned. 600/500 fill bars and buttons,
     400 is the 1px highlight along a fill and the text stop inside a dark well,
     300 is the text stop for anything sitting on a panel face. */
  --ember-600: #8f2a1e;    /* health, destruction */
  --ember-500: #b8392a;
  --ember-400: #e0553d;
  --ember-300: #fa9b83;
  --moss-700:  #204010;
  --moss-600:  #35651f;    /* success, gain */
  --moss-500:  #4e9130;
  --moss-400:  #6ec344;
  --moss-300:  #8fd66b;
  --azure-600: #22447c;    /* mana, info */
  --azure-500: #2f5fae;
  --azure-400: #4d86dd;
  --azure-300: #8fb6f0;
  --amethyst-500: #7b52b8; /* rare, premium */
  --amethyst-400: #a37ede;
  --amethyst-300: #c4a9f0;
  --rust-500:  #a06a22;    /* warning */
  --rust-400:  #d59a3a;
  --rust-300:  #f0bd7a;

  /* The mottled slate face: two coarse fractal-noise layers (one darkening, one lightening)
     plus a fine grain pass, as a translucent OVERLAY so it rides on any surface colour.
     Peak deviation stays inside +-6% luminance, so it reads as material, not as pattern.
     Raised faces fold it in as their FIRST background layer, which is why every raised
     surface must set `background` (the shorthand) and never `background-color`: a colour
     property cannot carry an image layer and drops the texture silently.
     Sunken faces - wells, fields, slots, bar tracks - stay flat. Texture in a recess reads
     as dirt. */
  --texture-stone: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='d' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.22' numOctaves='3' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0.4 0.2 0 -0.74'/%3E%3C/filter%3E%3Cfilter id='l' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.3' numOctaves='3' seed='19' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.6 0.4 0.2 0 -0.80'/%3E%3C/filter%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='1' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0.4 0.2 0 -0.70'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23d)'/%3E%3Crect width='160' height='160' filter='url(%23l)'/%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
}

.gs {
  /* ----------------------------------------------------------- SURFACES
     At most two background values appear in one view: the app field and the panel face.
     Everything else is a well or a bar. Raised faces carry the texture; sunken ones do not. */
  --gs-app: var(--stone-700);                                /* behind everything */
  --gs-surface-panel: var(--texture-stone), var(--stone-500); /* window and panel face */
  --gs-surface-alt: var(--texture-stone), var(--stone-600);   /* nested face, title bar */
  --gs-surface-raised: var(--texture-stone), var(--stone-450);/* hover face, toolbar */
  --gs-surface-well: var(--stone-950);                        /* logs, art wells, recesses */
  --gs-surface-field: var(--stone-900);                       /* input, textarea */
  --gs-surface-slot: var(--stone-800);                        /* inventory slot */
  --gs-surface-slot-hover: var(--stone-700);
  --gs-surface-selected: var(--brass-700);                    /* selected row, menu hover */
  --gs-scrim: #0b0b0ccc;                                      /* modal backdrop */
  --gs-row-alt: #ffffff0a;                                    /* zebra */
  --gs-row-hover: #ffffff12;                                  /* hover wash */

  /* neutrals - cool slate, never warm, never pure black */
  --gs-panel-base: var(--stone-500);   /* window fill */
  --gs-panel-deep: var(--stone-600);   /* nested fill, title bar */
  --gs-void: var(--stone-950);         /* keybind caps, slot recesses */

  /* Text. Every stop clears 4.5:1 against --gs-panel-base, the lightest surface text ever
     sits on. Never use a raw ramp value as a text colour. */
  --gs-text: var(--stone-50);          /* 7.7:1 - body copy */
  --gs-text-dim: var(--stone-100);     /* 5.8:1 - secondary copy, inactive tabs */
  --gs-text-mute: var(--stone-150);    /* 4.7:1 - small print, labels, hints */
  --gs-text-prose: var(--stone-100);   /* running prose: quest text, dialog speech */
  --gs-text-disabled: var(--stone-300);/* disabled CONTROLS only; muted content uses -mute */
  --gs-text-on-brass: var(--stone-950);/* dark text on a brass 400/500 fill */
  --gs-text-on-selected: var(--stone-50); /* text on --gs-surface-selected */
  /* Stamped text - a slot count or a bar readout drawn over item art - carries its own
     outline rather than trusting the surface behind it. */
  --gs-stamp: var(--stone-50);
  --gs-stamp-outline: #000000;

  /* the log's five levels. The dev console is the one surface where colour carries meaning the
     metal cannot. They live here and not in the dev bundle so nothing paints itself with a
     raw hex. The log is a well, so these take the 400 stops, not the 300s. */
  --gs-log-trace: var(--stone-300);
  --gs-log-debug: var(--stone-200);
  --gs-log-info: var(--azure-400);
  --gs-log-warn: var(--rust-400);
  --gs-log-error: var(--ember-400);

  /* brass - the only accent. Titles, selection, focus, money. */
  --gs-gold-bright: var(--brass-300);  /* highlight edge, button label */
  --gs-gold: var(--brass-500);         /* frames, active state, primary button face */
  --gs-gold-shade: var(--brass-700);   /* lower edge of every frame, unlit pip */
  --gs-gold-hair: var(--brass-600);    /* rules and hairlines */
  --gs-gold-head: var(--brass-300);    /* panel titles, item names - 5.8:1 */
  --gs-gold-label: var(--brass-300);   /* section headers */
  --gs-gold-mono: var(--brass-300);    /* mono captions inside panels */
  /* a lit brass pip: the mail seal, the pet mood dot. Unlit it is --gs-gold-shade alone.
     Flat, because the system has no gradients - the pip reads from its bevel. */
  --gs-gold-pip: var(--brass-400);

  /* the floor of the window stack. src/client/panel.ts hands every mounted panel
     z-index = this + its place in the stack, so no panel declares one of its own. */
  --gs-z-window: 20;

  /* a modal that must beat every window: the NPC dialog, the key confirmation, the party ask.
     Above the whole stack (20 + ~30 mounted panels) and below the tooltip layer at 100. */
  --gs-z-dialog: 80;

  /* the open dropdown, which is the one thing that outranks even a modal: a select inside a
     dialog has to be able to drop its list over the dialog that owns it. See .gs-menu. */
  --gs-z-menu: 120;

  /* Hairlines. A surface's edge is a 1px HARD border outside a 2px bevel, so these are the
     soft internal rules only: a divider between rows, a section split. */
  --gs-hair-hud: var(--stone-700);
  --gs-hair-window: var(--stone-700);
  --gs-hair-soft: #ffffff14;
  --gs-hair-faint: #ffffff0a;
  --gs-rail: var(--gs-border-hard);

  /* ------------------------------------------------------------- BEVELS
     Every surface is either raised (light top-left, dark bottom-right) or sunken (the
     inverse), and a 1px hard border always sits outside the bevel. Thin variants are for
     controls under 24px. There are no soft shadows and no blur anywhere. */
  --gs-bevel-light: #ffffff2e;
  --gs-bevel-light-strong: #ffffff4d;
  --gs-bevel-dark: #00000073;
  --gs-bevel-dark-strong: #000000a6;
  --gs-border-hard: #08080a;
  --gs-border-soft: var(--stone-700);
  --gs-focus-ring: var(--brass-400);

  --gs-raised:
    inset 2px 2px 0 0 var(--gs-bevel-light),
    inset -2px -2px 0 0 var(--gs-bevel-dark);
  --gs-raised-thin:
    inset 1px 1px 0 0 var(--gs-bevel-light),
    inset -1px -1px 0 0 var(--gs-bevel-dark);
  --gs-raised-strong:
    inset 2px 2px 0 0 var(--gs-bevel-light-strong),
    inset -2px -2px 0 0 var(--gs-bevel-dark-strong);
  --gs-sunken:
    inset 2px 2px 0 0 var(--gs-bevel-dark),
    inset -2px -2px 0 0 var(--gs-bevel-light);
  --gs-sunken-thin:
    inset 1px 1px 0 0 var(--gs-bevel-dark),
    inset -1px -1px 0 0 var(--gs-bevel-light);

  /* Blur is gone: the world no longer reads through the chrome. A panel is opaque stone.
     These three names survive only so a stale call site fails flat instead of invalid. */
  --gs-glass-blur: 0px;
  --gs-glass-blur-hud: 0px;
  --gs-glass-sat: 1;
  --gs-glass: none;
  --gs-glass-hud: none;

  /* Surface stop pairs. Gilded Slate fed these to linear-gradient(top, bot); Stonebevel has
     no gradients, so each pair now holds ONE flat value twice and any surviving gradient
     call site renders flat. New code uses --gs-surface-* above instead. */
  --gs-tint-hud-top: var(--stone-600);
  --gs-tint-hud-bot: var(--stone-600);
  --gs-tint-win-top: var(--stone-500);
  --gs-tint-win-bot: var(--stone-500);
  --gs-tint-ctl-top: var(--stone-900);
  --gs-tint-ctl-bot: var(--stone-900);
  --gs-solid-ctl-top: var(--stone-450);
  --gs-solid-ctl-bot: var(--stone-450);
  --gs-solid-hud-top: var(--stone-600);
  --gs-solid-hud-bot: var(--stone-600);
  --gs-flat-hud-top: var(--stone-600);
  --gs-flat-hud-bot: var(--stone-600);
  --gs-flat-win-top: var(--stone-500);
  --gs-flat-win-bot: var(--stone-500);
  --gs-flat-ctl-top: var(--stone-900);
  --gs-flat-ctl-bot: var(--stone-900);

  /* grain - kept as an alias so old call sites keep painting. New surfaces fold
     --texture-stone straight into their `background` instead. */
  --gs-grain: var(--texture-stone);
  --gs-grain-alpha: 1;

  /* parchment - the document surface. The one light theme in the client. */
  --gs-parch: var(--parch-50);
  --gs-parch-mid: var(--parch-100);
  --gs-parch-edge: var(--parch-200);
  --gs-ink: var(--parch-900);
  --gs-ink-dim: var(--parch-700);
  --gs-ink-rule: #52422a24;
  --gs-navy: var(--azure-600);    /* the one header band allowed on parchment */
  --gs-navy-deep: var(--stone-900);

  /* semantic - fixed meanings, never decoration */
  --gs-hp: var(--ember-500);
  --gs-mp: var(--azure-500);
  --gs-xp: var(--moss-500);
  --gs-bar-track: var(--stone-900);
  --gs-cooldown: #0b0b0cb3;       /* the sweep over a slot on cooldown */
  --gs-cd: var(--azure-400);
  --gs-alert: var(--ember-400);
  --gs-gain: var(--moss-500);
  --gs-gain-ink: var(--moss-300);
  --gs-loss: var(--ember-400);
  /* The close mark sits on a title bar, which is a panel face: it takes the 300 text stop
     (4.6:1), never the 400 fill stop, which measures 3.0:1 there. */
  --gs-close: var(--ember-300);

  /* quality colours - this game stamps six on every archetype, and Stonebevel ships five
     status hues. Gold takes brass, which is already the system's rarity and money accent;
     that is the one deliberate reuse, and it is why gold and yellow read as one family. */
  --gs-q-white: var(--stone-300);
  --gs-q-green: var(--moss-500);
  --gs-q-blue: var(--azure-500);
  --gs-q-yellow: var(--rust-500);
  --gs-q-gold: var(--brass-500);
  --gs-q-purple: var(--amethyst-500);
  --gs-q-empty: #ffffff14;
  /* Dark rims frame item art inside a slot. */
  --gs-q-rim-white: var(--stone-600);
  --gs-q-rim-blue: var(--azure-600);
  --gs-q-rim-green: var(--moss-600);
  --gs-q-rim-yellow: var(--rust-500);
  --gs-q-rim-gold: var(--brass-600);
  --gs-q-rim-purple: var(--amethyst-500);
  /* Brighter partners carry item names on a panel face - these are text stops and every
     one of them clears 4.5:1 there. */
  --gs-q-text-white: var(--stone-50);
  --gs-q-text-blue: var(--azure-300);
  --gs-q-text-green: var(--moss-300);
  --gs-q-text-yellow: var(--rust-300);
  --gs-q-text-gold: var(--brass-300);
  --gs-q-text-purple: var(--amethyst-300);

  /* A stat number whose total includes an option line - the half a recast redraws. Its own token
     rather than --gs-q-text-gold, which names an item QUALITY: the two would drift apart the day
     a max roll gets a colour of its own. */
  --gs-stat-opt: #FF9A3D;
  /* ...and the NAME of a percentage stat: absorb and critical damage. A different fact from the
     one above, on a different element, because one row can carry both. */
  --gs-stat-rare: #F4DE69;
  /* The level box follows the same neutral slate used by the rest of the menu chrome. */
  --gs-plate-level: var(--stone-600);

  /* --------------------------------------------------------------- TYPE
     One sans face, and it is the window system's own: Rajdhani, the --face-ui of
     docs/hud-ui-design.html. Space Mono and IBM Plex Mono used to split this three ways
     (chrome, body, data) and the two typewriters spelled the same game in a different voice
     from every window opened beside them. The three token NAMES survive because a hundred
     rules spell them, but they are now one family: display is the chrome voice, ui the label
     voice, mono the body and data voice, and all three are the sans.

     The cursive face is Grenze and it has two jobs: a nameplate over a body in the world
     (.gs-plate__name), and the HUD rail, which reads this token through --hud-face-cursive in
     public/hud.css to draw your level and your name. That second job is why the face is Grenze
     at all: Marcellus went soft at 16px over a bright map, and a condensed serif with a
     blackletter edge keeps its shape there. Nothing else takes it. */
  --gs-face-display: "Rajdhani", "Nunito", "Segoe UI", system-ui, sans-serif;
  --gs-face-ui: "Rajdhani", "Nunito", "Segoe UI", system-ui, sans-serif;
  --gs-face-mono: "Rajdhani", "Nunito", "Segoe UI", system-ui, sans-serif;
  --gs-face-cursive: "Grenze", "Rajdhani", serif;

  /* A fixed px ladder: 10 / 11 / 12 / 13 / 16 / 21 / 28 / 40. Never rem, never fractional,
     so chrome keeps landing on the 2px grid. The whole ladder stepped down one rung once the
     HUD was drawn over a live world and the chrome was eating the game: 10px is the floor and
     only for slot counts and bar values; body text never sits below 12px. */
  --gs-size-panel-title: 12px;   /* caps - Bag, Character, Forge */
  --gs-size-item-title: 12px;    /* caps - item names */
  --gs-size-body: 13px;          /* stats, chat, copy */
  --gs-size-dense: 12px;         /* table cells, dense rows */
  --gs-size-label: 12px;         /* caps - fields, tabs, buttons */
  --gs-size-caption: 11px;       /* caps + 0.08em - section headers */
  --gs-size-mono: 11px;          /* keybinds, coords */
  --gs-size-bar: 10px;           /* values inside bars */
  --gs-size-cast: 18px;          /* the broadcast board, read across the room */
  --gs-size-cast-big: 23px;      /* ...and the top rung of an event's own ladder, retail's 20/16 */
  --gs-size-combat: 28px;        /* floating damage */

  --gs-track-label: 0.08em;      /* caps labels need air */
  --gs-track-wide: 0.16em;       /* brand mark, screen titles */
  --gs-lead-ui: 1.2;
  --gs-lead-body: 1.55;
  /* Chrome type is stamped: one hard black offset, no blur. */
  --gs-text-stamp: 1px 1px 0 #000;

  /* ------------------------------------------------------------- RHYTHM
     2px base grid. Chrome sits on even multiples, content padding on 4s. */
  --gs-gap-xs: 2px;
  --gs-gap-sm: 4px;
  --gs-gap-md: 8px;
  --gs-gap-lg: 12px;
  --gs-edge: 8px;                /* every anchored cluster, from the screen */

  /* Fixed chrome metrics - copy these, do not re-derive them. */
  --gs-size-titlebar: 20px;
  --gs-size-control: 28px;       /* button and field height */
  --gs-size-control-lg: 36px;
  --gs-size-icon-button: 24px;
  --gs-size-slot: 34px;          /* inventory / hotbar cell */
  --gs-size-icon: 16px;
  --gs-size-icon-lg: 20px;
  --gs-size-scrollbar: 12px;
  --gs-size-sidebar: 232px;
  --gs-size-statbar: 14px;       /* health / mana / xp bar height */
  --gs-pad-panel: 8px;
  --gs-pad-cell: 4px 8px;
  --gs-border-width: 1px;
  --gs-bevel-width: 2px;

  /* ------------------------------------------------------------- SHAPE
     There is no radius in this system. These two names were Gilded Slate's outer and inner
     corner radii; they are kept at 0 so every call site flattens without being edited. */
  --gs-bevel-out: 0px;
  --gs-bevel-in: 0px;
  --gs-radius: 0px;

  /* Floating chrome - windows, menus, tooltips, toasts - gets ONE hard-offset drop shadow
     with zero blur. A stamped shadow, not a soft one. Nothing else casts. */
  --gs-shadow-window: 3px 3px 0 0 #00000073;
  --gs-shadow-dialog: 5px 5px 0 0 #00000080;

  /* ------------------------------------------------------------ MOTION
     Stepped, minimal, mostly absent. Hover, press and focus are instantaneous. Bar fills
     are the only continuous animation in the system. */
  --gs-dur-instant: 0ms;
  --gs-dur-fast: 80ms;
  --gs-dur-bar: 200ms;
  --gs-ease-step: steps(4, end);

  /* Text printed straight onto the world. The world is not our chrome and a bevel cannot
     help it, so this stays a tight black halo: legibility beats the system. */
  --gs-world-shadow: 0 0 2px rgba(0, 0, 0, 0.95), 0 0 5px rgba(0, 0, 0, 0.75), 0 0 10px rgba(0, 0, 0, 0.55), 0 1px 2px rgba(0, 0, 0, 0.9);
  /* the same halo for a masked glyph, which text-shadow cannot reach */
  --gs-world-glow: drop-shadow(0 0 2px rgba(0, 0, 0, 0.95)) drop-shadow(0 0 5px rgba(0, 0, 0, 0.7)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.9));

  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-body);
  line-height: var(--gs-lead-body);
  color: var(--gs-text);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.gs *,
.gs *::before,
.gs *::after { box-sizing: border-box; }

/* ---------------------------------------------------------------- SHAPES */
/* There is no radius in Stonebevel. A surface is squared off and says which way it faces with
   a bevel: raised is light top-left and dark bottom-right, sunken is the inverse, and a 1px
   hard border always sits outside the bevel. These two classes are kept because markup across
   src/client asks for them; they now apply the bevel the name always implied. */
.gs-bevel     { border-radius: 0; box-shadow: var(--gs-raised); }
.gs-bevel-in  { border-radius: 0; box-shadow: var(--gs-sunken); }
.gs-diamond   { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
/* Nothing in this system is round. The class stays inert so old markup keeps working. */
.gs-circle    { border-radius: 0; }

/* -------------------------------------------------------------- SURFACES */
/* Exactly three. Adding a fourth is how a UI starts to look assembled.
   At most two backgrounds appear in one view - the app field and the panel face. Everything
   else is a well or a bar. Raised faces carry the stone texture and MUST set `background`,
   the shorthand: a background-color cannot hold the image layer and drops it silently. */

/* 1. Stone HUD - always-on chrome. A raised face, one bevel, one hard border, no shadow:
      it is docked to the screen edge, so it is not floating and does not cast. */
.gs-hud {
  position: relative;
  background: var(--gs-surface-alt);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised);
}

/* 2. Stone window - opened panels the player reads and acts in. Raised face, hard border,
      and the one stamped drop shadow floating chrome is allowed. The world does NOT read
      through it: there is no glass in this system. */
.gs-window {
  position: relative;
  overflow: clip;
  background: var(--gs-surface-panel);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised), var(--gs-shadow-window);
  display: flex;
  flex-direction: column;
  color: var(--gs-text);
}
.gs-window--dialog { box-shadow: var(--gs-raised), var(--gs-shadow-dialog); }

/* The texture is folded into the surface tokens as their first background layer, so no
   pseudo element paints it any more, and the diagonal hatch that used to mark an empty row
   is gone with it: nothing in this system paints a pattern over a surface. */

/* 3. Parchment - opaque. Documents: quest text, lore, letters. Reserve it; parchment
      everywhere kills the contrast. Flat, bevelled, no inner glow - a sheet, not a stage. */
.gs-parchment {
  background: var(--gs-parch);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised), var(--gs-shadow-dialog);
  color: var(--gs-ink);
  display: flex;
  flex-direction: column;
}

/* ------------------------------------------------------------- TITLE BARS */
/* 20px, fixed. Caps, letter-spaced, stamped with one hard black offset. */
.gs-titlebar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--gs-gap-md);
  min-height: var(--gs-size-titlebar);
  padding: 3px 6px;
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised-thin);
  border-bottom: var(--gs-border-width) solid var(--gs-border-hard);
}
.gs-titlebar__title {
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-panel-title);
  font-weight: 700;
  line-height: var(--gs-lead-ui);
  letter-spacing: var(--gs-track-label);
  text-transform: uppercase;
  color: var(--gs-gold-head);
  text-shadow: var(--gs-text-stamp);
}
.gs-titlebar__spacer { flex: 1; }
.gs-titlebar__meta {
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-mono);
  color: var(--gs-text-mute);
}

/* parchment variant: the single place stone and parchment meet */
.gs-titlebar--navy {
  padding: 3px 8px;
  background: var(--gs-navy);
  box-shadow: var(--gs-raised-thin);
  border-bottom: var(--gs-border-width) solid var(--gs-border-hard);
}
.gs-titlebar--navy .gs-titlebar__title { color: var(--gs-text); }
.gs-titlebar--navy .gs-titlebar__meta { color: var(--azure-300); }

.gs-close {
  appearance: none;
  border: 0;
  background: none;
  padding: 0 2px;
  cursor: var(--cur-hand, pointer);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-label);
  font-weight: 700;
  line-height: 1;
  color: var(--gs-close);
  transition: none;
}
.gs-close:hover { color: var(--ember-300); }

/* ------------------------------------------------------- CORNER ORNAMENTS */
/* Removed. The four gold elbows read as stray marks floating inside the panel edge rather than
   as a frame, so .gs-ornate and .gs-ornate__corners now draw nothing; the classes stay inert so
   existing markup keeps working. */
.gs-ornate { position: relative; }

/* --------------------------------------------------- PANELS & SUB-PANELS */
.gs-body { padding: var(--gs-pad-panel); display: flex; flex-direction: column; gap: var(--gs-gap-md); }
.gs-body--tight { padding: var(--gs-gap-sm); }

/* a nested face inside a window. Raised, so it keeps the texture. */
.gs-sub {
  padding: var(--gs-gap-md);
  background: var(--gs-surface-alt);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised-thin);
  display: flex;
  flex-direction: column;
  gap: var(--gs-gap-sm);
}
/* the darkest well: skill canvas, render stage, log. Sunken, and flat - texture in a recess
   reads as dirt. */
.gs-well {
  position: relative;
  background: var(--gs-surface-well);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-sunken);
}
/* The diagonal hatch is gone. It was meant to say "nothing here yet", but a striped fill is
   texture, and texture in this system means a raised stone face - so an empty row read as a
   surface that had something on it rather than as a hole. Emptiness is now said the way the
   rest of the system says it: a sunken well, flat and darker than its neighbours, with one flat
   sentence in it. The class stays and does nothing, so the markup that asks for it keeps
   working - see src/client/pets.ts and panels/pet-sheet.ts. */
.gs-hatch::before { content: none; }

.gs-footer {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--gs-gap-sm);
  padding: var(--gs-gap-sm) var(--gs-pad-panel);
  border-top: var(--gs-border-width) solid var(--gs-border-hard);
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised-thin);
}

/* ------------------------------------------------------- SECTION HEADERS */
.gs-section {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gs-gap-md);
  padding-bottom: var(--gs-gap-sm);
  border-bottom: var(--gs-border-width) solid var(--gs-border-soft);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-caption);
  font-weight: 700;
  line-height: var(--gs-lead-ui);
  letter-spacing: var(--gs-track-label);
  color: var(--gs-gold-label);
  text-transform: uppercase;
  text-shadow: var(--gs-text-stamp);
}
.gs-divider {
  height: 1px;
  border: 0;
  margin: var(--gs-gap-sm) 0;
  background: var(--gs-border-hard);
  box-shadow: 0 1px 0 0 var(--gs-bevel-light);
}

/* ------------------------------------------------------------ ITEM SLOTS */
/* The most repeated object in the game, and the one place item art meets chrome.
   A slot is a SUNKEN well: flat fill, no texture, hard border, 34px. Quality is carried by
   the border colour alone - tinting the cell by quality makes a full bag look like confetti. */
.gs-slot {
  position: relative;
  width: var(--gs-size-slot);
  height: var(--gs-size-slot);
  box-sizing: border-box;
  border: var(--gs-border-width) solid var(--gs-border-hard);
  background: var(--gs-surface-slot);
  box-shadow: var(--gs-sunken-thin);
  border-radius: 0;
}
.gs-slot__inner {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gs-surface-slot);
  border-radius: 0;
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-mono);
  color: var(--gs-text-mute);
}
/* An empty cell is a darker well and nothing else - see .gs-slot--empty below. The hatch this
   pseudo element used to paint is gone: stripes read as a surface, not as an absence, and a
   bag full of them looked like a fabric swatch. The element stays inert because the states
   below still reference it. */
.gs-slot__inner::before { content: none; }
.gs-slot img,
.gs-slot__art { position: relative; width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; }

/* quality - the rim, and only the rim */
/* The quality ink on its own, for text with no tile beside it to take the colour from: the pet
   sheet names its pet in its talent band's colour and that row carries no slot. Same six inks the
   rims use, so a name and a frame can never disagree about what a thing is. */
/* Qualified with .gs on purpose. These have to out-rank the type classes they are put ON -
   .gs-title carries its own gold and is declared over a thousand lines further down, so a bare
   .gs-q--green lost the cascade to it and every pet name came out gold whatever its band. */
.gs .gs-q--white  { color: var(--gs-q-text-white); }
.gs .gs-q--green  { color: var(--gs-q-text-green); }
.gs .gs-q--blue   { color: var(--gs-q-text-blue); }
.gs .gs-q--yellow { color: var(--gs-q-text-yellow); }
.gs .gs-q--gold   { color: var(--gs-q-text-gold); }
.gs .gs-q--purple { color: var(--gs-q-text-purple); }
.gs-slot--white  { border-color: var(--gs-q-rim-white); }
.gs-slot--green  { border-color: var(--gs-q-rim-green); }
.gs-slot--blue   { border-color: var(--gs-q-rim-blue); }
.gs-slot--yellow { border-color: var(--gs-q-rim-yellow); }
.gs-slot--gold   { border-color: var(--gs-q-rim-gold); }
.gs-slot--purple { border-color: var(--gs-q-rim-purple); }

/* Item presentation is global: inventory, shops, mail, trade, forge and reward panels all use
   the same rim and the same 32px atlas art. Skill keys opt out. */
.gs-slot:not(.gs-slot--skill):hover,
.gs-slot--hover:not(.gs-slot--skill) { background: var(--gs-surface-slot-hover); }
.gs-slot:not(.gs-slot--skill) .bag-ico { transform: none; }
.gs-slot:not(.gs-slot--skill) .gs-slot__count {
  color: var(--gs-stamp);
  text-shadow:
    1px 1px 0 var(--gs-stamp-outline), -1px 1px 0 var(--gs-stamp-outline),
    1px -1px 0 var(--gs-stamp-outline), -1px -1px 0 var(--gs-stamp-outline);
}

/* A row or tooltip inherits its item-name colour from the quality slot it contains. The order is
   white, blue, green, yellow, gold, purple; selectors stay semantic rather than positional.
   These are the TEXT stops, which clear 4.5:1 on a panel face - never the rim stops. */
.gs-row:has(.gs-slot--white) .gs-row__name,
.gs-tooltip__head:has(.gs-slot--white) .gs-tooltip__name { color: var(--gs-q-text-white); }
.gs-row:has(.gs-slot--blue) .gs-row__name,
.gs-tooltip__head:has(.gs-slot--blue) .gs-tooltip__name { color: var(--gs-q-text-blue); }
.gs-row:has(.gs-slot--green) .gs-row__name,
.gs-tooltip__head:has(.gs-slot--green) .gs-tooltip__name { color: var(--gs-q-text-green); }
.gs-row:has(.gs-slot--yellow) .gs-row__name,
.gs-tooltip__head:has(.gs-slot--yellow) .gs-tooltip__name { color: var(--gs-q-text-yellow); }
.gs-row:has(.gs-slot--gold) .gs-row__name,
.gs-tooltip__head:has(.gs-slot--gold) .gs-tooltip__name { color: var(--gs-q-text-gold); }
.gs-row:has(.gs-slot--purple) .gs-row__name,
.gs-tooltip__head:has(.gs-slot--purple) .gs-tooltip__name { color: var(--gs-q-text-purple); }

/* Stamped marks sit ON TOP of item art, so they carry their own outline rather than trusting
   the surface behind them. 11px is the ladder's floor and this is what it is for. */
/* Item level badge inside a generic equipment target. Character and Profile override this in
   panels.css because their worn frames carry the level as an external caption. */
.gs-slot__lv {
  position: absolute;
  left: 2px;
  top: 0;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  line-height: 1.2;
  color: var(--gs-gold-head);
  text-shadow: var(--gs-text-stamp);
}
/* stack count, bottom-right. Stackables only; never show a bare 1. */
.gs-slot__count {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 0 2px;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  font-weight: 700;
  line-height: 1.2;
  color: var(--gs-stamp);
  background: none;
  border: 0;
  border-radius: 0;
  text-shadow:
    1px 1px 0 var(--gs-stamp-outline), -1px 1px 0 var(--gs-stamp-outline),
    1px -1px 0 var(--gs-stamp-outline), -1px -1px 0 var(--gs-stamp-outline);
}
/* Keybind cap, TOP-LEFT and brass. Two different marks share a slot corner each and must never
   collide: the key is what you press (top-left, brass) and the count is what you have
   (bottom-right, white). Reading a bar is then one glance per corner rather than one per cell. */
/* THE DEFAULT MARK. Which slot the next fight opens on, said with one small brass sword in the
   bottom-right corner rather than only by washing the whole cell - a wash is a background, and a
   background is exactly what the eye stops seeing on a row of twelve. A badge is a THING sitting
   on one cell, and it survives the icon under it being bright.

   `::after` because the mark is derived entirely from the class: nothing paints it, nothing has to
   remember to clear it, and a cell that loses the class loses the sword in the same frame.

   Masked like every other vector here, so it takes the brass from `color` and needs no second
   file for a hover or a dark cell. */
.gs-slot--opener::after {
  content: "";
  position: absolute;
  right: 1px;
  bottom: 1px;
  z-index: 3;
  width: 13px;
  height: 13px;
  /* DRAWN, not masked. The file carries its own brass and its own dark seam - the same choice
     the painted icon family makes - so there is no second colour to keep in step with a hover.
     It sits on its own dark plate: a skill icon can be any colour underneath, and a badge that
     only reads on half of them is not a marker. */
  background: url(/data/art/ui/sword.b0de7905.svg) center / 11px 11px no-repeat, rgba(0, 0, 0, 0.62);
  box-shadow: inset 0 0 0 1px var(--gs-gold-head);
  pointer-events: none;
}
/* A stack count lives in that same corner. On a slot that is BOTH - a potion or a trap made the
   default, which is exactly what the auto-battle bar is for - the count steps left rather than
   sitting under the blade. */
.gs-slot--opener .gs-slot__count { right: 14px; }
.gs-slot__key {
  position: absolute;
  left: 2px;
  top: 0;
  z-index: 2;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  line-height: 1.2;
  color: var(--gs-gold-head);
  text-shadow: var(--gs-text-stamp);
}

/* a skill key on the shortcut bar. It is HUD, not a bag cell, but there is no glass in this
   system: it is the same sunken well with a brass hairline saying it is a bound action.
   Declared before the states so locked and empty still win. */
.gs-slot--skill { background: var(--gs-surface-slot); border-color: var(--gs-gold-shade); }
.gs-slot--skill .gs-slot__inner {
  background: var(--gs-surface-slot);
  box-shadow: var(--gs-sunken-thin);
  color: var(--gs-text-mute);
}

/* states - painted on top; the art underneath is never dimmed or tinted */
/* Empty is the deepest well on the panel and nothing else. Depth is the whole signal: a cell
   with something in it is --surface-slot, a cell without is --surface-well, and the difference
   reads down a whole bag at a glance without a single mark being drawn. */
.gs-slot--empty { border-color: var(--gs-border-hard); }
.gs-slot--empty .gs-slot__inner { background: var(--gs-surface-well); color: transparent; }

/* Hover lightens the face one stop. No movement, no glow. */
.gs-slot:hover,
.gs-slot--hover { background: var(--gs-surface-slot-hover); filter: none; }

.gs-slot--locked { border-color: var(--stone-600); background: var(--stone-850); }
.gs-slot--locked .gs-slot__inner { background: var(--stone-850); color: var(--gs-text-disabled); }
/* A locked slot greys what it HOLDS, not just its frame: the icon is the thing being withheld,
   and a full-colour icon inside a grey frame still reads as available. */
.gs-slot--locked .gs-ico { filter: grayscale(1) brightness(0.72); }

/* The slot you can act on RIGHT NOW. Moss, because the system already spends moss on every
   gain; distinct from --selected, which is brass and means "this is the one you picked". */
.gs-slot--ready { border-color: var(--moss-400); }
.gs-slot--ready .gs-slot__inner { box-shadow: inset 0 0 0 1px var(--moss-400); }

/* Selection is a flat brass fill with no bevel - the system's rule for a picked thing. */
.gs-slot--selected { border-color: var(--gs-gold-bright); }
.gs-slot--selected .gs-slot__inner { box-shadow: inset 0 0 0 2px var(--gs-gold-bright); }

/* THE DEFAULT SLOT - what this character throws every turn until the player points at another
   one. ONE cue and nothing else: the whole cell washed in brass. The old answer stacked three
   marks on a 40px square (a fat double frame, a 4px lift out of the row, and a pip hung off the
   corner) and the badge ended up sitting over the cooldown digits while the lift broke the row's
   baseline. A soft wash of the entire cell is read at the same glance, survives a screenshot and
   a colour-blind player just as well, and costs the icon nothing. Declared after every other
   state so the default still reads on a slot that is also hovered. --opener-soft is the same
   answer arrived at weakly - merely what was cast last - so it is the same wash, fainter. */
.gs-slot--opener {
  border-color: var(--gs-gold);
  background: var(--gs-surface-selected);
  transform: none;
  box-shadow: var(--gs-sunken-thin);
  animation: none;
}
.gs-slot--opener .gs-slot__inner {
  background: var(--gs-surface-selected);
  box-shadow: var(--gs-sunken-thin);
}

.gs-slot--opener-soft {
  border-color: var(--gs-gold-hair);
  background: var(--gs-surface-slot-hover);
}
.gs-slot--opener-soft .gs-slot__inner {
  background: var(--gs-surface-slot-hover);
  box-shadow: var(--gs-sunken-thin);
}

/* The pet strip says the same thing the same way. Its default cell used to hang a .gs-dot in the
   corner, which is the badge this system just retired on the player's own bar; a second grammar
   for one idea is how a HUD stops being readable. The dot is off here and the cell takes the
   wash above. (.gs-dot itself stays: on the round menu buttons it means "something is waiting".) */
.hud-petbar .gs-slot .gs-dot { display: none; }

/* The cooldown cover: a dark wedge that sweeps away CLOCKWISE as the skill recovers.
   --gs-cd is how far the sweep line has travelled from twelve o'clock (0deg = just cast and fully
   covered, 360deg = ready), and the mask keeps dark everything ahead of that line. A conic mask
   rather than a rotated element: one property to move per frame, no layout. Only .is-cooling
   shows it, so a ready slot draws nothing at all. */
.gs-slot__cooldown {
  position: absolute;
  inset: 0;
  display: none;
  background: var(--gs-cooldown);
  /* Unitless 0 is not a valid <angle>, so the closing stop carries its unit. */
  -webkit-mask-image: conic-gradient(from var(--gs-cd, 0deg), #000 0deg calc(360deg - var(--gs-cd, 0deg)), transparent 0deg);
  mask-image: conic-gradient(from var(--gs-cd, 0deg), #000 0deg calc(360deg - var(--gs-cd, 0deg)), transparent 0deg);
}
.gs-slot.is-cooling .gs-slot__cooldown { display: block; }
.gs-slot__cd-digits {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  text-align: center;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-label);
  font-weight: 700;
  color: var(--gs-stamp);
  text-shadow:
    1px 1px 0 var(--gs-stamp-outline), -1px 1px 0 var(--gs-stamp-outline),
    1px -1px 0 var(--gs-stamp-outline), -1px -1px 0 var(--gs-stamp-outline);
}

/* a shortcut slot states go on the inner edge, matching the bag cell */
.gs-slot--skill:hover,
.gs-slot--skill.gs-slot--selected { filter: none; }
.gs-slot--skill:hover .gs-slot__inner { box-shadow: inset 0 0 0 1px var(--gs-gold-bright); }
.gs-slot--skill.gs-slot--selected .gs-slot__inner { box-shadow: inset 0 0 0 2px var(--gs-gold-bright); }

.gs-slot-grid {
  display: grid;
  grid-template-columns: repeat(var(--gs-cols, 6), 1fr);
  gap: var(--gs-gap-xs);
  align-content: start;
}
.gs-slot-grid .gs-slot { width: auto; height: auto; aspect-ratio: 1; }

/* --------------------------------------------------------- RESOURCE BARS */
/* A sunken track, flat, with a flat fill and a 1px highlight along the top of that fill -
   the 400 stop of whatever hue the fill uses. Height still encodes importance, but on the
   2px grid: HP 14 (the system's status-bar metric), MP 12, target 10, EXP 6.
   The fill is the ONE continuous animation in the system: 200ms linear. */
.gs-bar {
  position: relative;
  height: var(--gs-size-statbar);
  border-radius: 2px;
  overflow: hidden;
  background: var(--gs-bar-track);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-sunken-thin);
}
/* One bar shape in the game (design.css .bar__fill, hud.css .hud-vital__fill and this): squared
   at a small radius, and the fill washed dark at the left to light at the right over its own
   colour. The wash is alpha over whatever hue the fill carries, so every variant below keeps its
   one-line colour and gets the gradient for free. 2px, not the 3 the bigger bars take: a plate's
   bar is 6px tall and 3 would round it into a pill. */
.gs-bar__fill {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--gs-fill, 0%);
  border-radius: 2px;
  background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(255, 255, 255, .26)), var(--gs-hp);
  box-shadow: inset 0 1px 0 0 var(--ember-400);
  transition: width var(--gs-dur-bar) linear;
}
.gs-bar__value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  line-height: 1;
  color: var(--gs-stamp);
  /* the readout is stamped over a coloured fill, so it carries an outline, not a soft drop */
  text-shadow:
    1px 1px 0 var(--gs-stamp-outline), -1px 1px 0 var(--gs-stamp-outline),
    1px -1px 0 var(--gs-stamp-outline), -1px -1px 0 var(--gs-stamp-outline);
}
.gs-bar--mp { height: 12px; }
.gs-bar--mp .gs-bar__fill { background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(255, 255, 255, .26)), var(--gs-mp); box-shadow: inset 0 1px 0 0 var(--azure-400); }
.gs-bar--exp { height: 6px; }
.gs-bar--exp .gs-bar__fill { background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(255, 255, 255, .26)), var(--gs-xp); box-shadow: inset 0 1px 0 0 var(--moss-400); }
.gs-bar--target { height: 10px; }
.gs-bar--target .gs-bar__fill { background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(255, 255, 255, .26)), var(--gs-hp); box-shadow: inset 0 1px 0 0 var(--ember-400); }
/* thin progress inside a list row: quest steps, durability, cultivation */
.gs-bar--thin { height: 4px; }
.gs-bar--thin .gs-bar__fill { background: linear-gradient(90deg, rgba(0, 0, 0, .42), rgba(255, 255, 255, .26)), var(--gs-gold); box-shadow: inset 0 1px 0 0 var(--brass-400); }

/* ------------------------------------------------------------- STAT ROWS */
.gs-stat {
  display: flex;
  align-items: center;
  gap: var(--gs-gap-md);
  padding: var(--gs-pad-cell);
  font-size: var(--gs-size-dense);
}
.gs-stat:nth-child(even) { background: var(--gs-row-alt); }
.gs-stat__k { flex: 1; color: var(--gs-text-mute); }
/* Right-aligned, and tabular below: the sans that replaced the old monospace has
   proportional figures, so a column of numbers needs the feature asked for by name. */
.gs-stat__v { font-family: var(--gs-face-display); color: var(--gs-text); }
.gs-stat__delta { width: 44px; text-align: right; font-family: var(--gs-face-display); color: var(--gs-gain-ink); }
.gs-stat__delta--down { color: var(--gs-loss); }

/* ------------------------------------------------------------- TAB STRIP */
/* Inactive tabs are raised faces; the active one is SUNKEN with a brass label - it is the
   surface you are standing on, so it goes in. No underline, no colour-only cue. */
.gs-tabs { display: flex; gap: var(--gs-gap-xs); }
.gs-tab {
  appearance: none;
  cursor: var(--cur-hand, pointer);
  height: var(--gs-size-control);
  padding: 0 var(--gs-gap-lg);
  background: var(--gs-surface-alt);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised-thin);
  border-radius: 0;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-label);
  line-height: var(--gs-lead-ui);
  letter-spacing: var(--gs-track-label);
  text-transform: uppercase;
  color: var(--gs-text-dim);
  text-shadow: var(--gs-text-stamp);
  transition: none;
}
.gs-tab:hover { background: var(--gs-surface-raised); color: var(--gs-text); }
.gs-tab[aria-selected="true"],
.gs-tab.is-active {
  background: var(--gs-surface-panel);
  box-shadow: var(--gs-sunken-thin);
  color: var(--gs-gold-head);
}
/* Disabled is a flat face with muted text: no bevel change, no opacity trick. */
.gs-tab:disabled {
  background: var(--stone-600);
  box-shadow: none;
  color: var(--gs-text-disabled);
  cursor: var(--cur-arrow, default);
}
/* A page tab under a dragged item. The bag and the warehouse accept a drop on a tab as "put this
   on that page", and the tab has to say so while the item is still in the air - a gesture with no
   target feedback is a gesture a player finds by accident. Reads as the active tab does, plus a
   glow, because that is where the item is about to be. */
.gs-tab.is-drop {
  background: linear-gradient(180deg, rgba(90, 68, 34, 0.95), rgba(40, 32, 19, 0.95));
  border-bottom-color: var(--gs-gold-bright);
  color: var(--gs-gold-head);
  box-shadow: inset 0 0 0 1px var(--gs-gold-bright), 0 0 8px rgba(224, 178, 92, 0.45);
}
/* Auto Sell's quality ladder. The band is the label, so the label wears the band's own ink -
   reading five colour NAMES in one brass is a list, reading them in their colours is a ladder.
   The active tab keeps the sunken face: which one is chosen must stay legible when the colours
   themselves are the content. Disabled inherits the rule above and greys out. */
.gs-tab--q-white:enabled { color: var(--gs-q-text-white); }
.gs-tab--q-green:enabled { color: var(--gs-q-text-green); }
.gs-tab--q-blue:enabled { color: var(--gs-q-text-blue); }
.gs-tab--q-yellow:enabled { color: var(--gs-q-text-yellow); }
.gs-tab--q-gold:enabled { color: var(--gs-q-text-gold); }
/* even-width variant for a docked panel's category row */
.gs-tabs--fill .gs-tab { flex: 1; padding-left: 0; padding-right: 0; text-align: center; font-size: var(--gs-size-caption); }

/* -------------------------------------------------------------- BUTTONS */
/* One primary action per view, and it is the only brass-filled thing.

   A button is a RAISED OBJECT and it says so with one 2px bevel: light top-left, dark
   bottom-right, a 1px hard border outside it. There are exactly three state changes and none
   of them is a new skin:
     hover  - the face lightens ONE stop. No movement, no shadow change, no bevel change.
     press  - the bevel FLIPS to sunken and the element shifts 1px, 1px. The control goes in.
     focus  - a 1px brass ring on the INNER edge, set once globally at the end of this file.
   All three are instant. Nothing here transitions.

   Height is --gs-size-control (28px) because .gs-input and .gs-select are 28px. A button and a
   field share a row all over the game and must sit on the same two lines.

   The label is caps, letter-spaced and stamped. There is no corner: --gs-radius is
   0 and stays 0. Do not add gs-bevel to new buttons; this rule already carries it. */
.gs-btn {
  appearance: none;
  cursor: var(--cur-hand, pointer);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gs-gap-sm);
  height: var(--gs-size-control);
  padding: 0 var(--gs-gap-lg);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  border-radius: 0;
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-label);
  font-weight: 700;
  line-height: var(--gs-lead-ui);
  letter-spacing: var(--gs-track-label);
  color: var(--gs-text);
  text-transform: uppercase;
  text-shadow: var(--gs-text-stamp);
  transition: none;
}
.gs-btn:hover { background: var(--gs-surface-raised); }
.gs-btn:active:not(:disabled):not([aria-disabled="true"]) {
  box-shadow: var(--gs-sunken);
  transform: translate(1px, 1px);
}
.gs-btn[aria-pressed="true"] {
  background: var(--gs-surface-panel);
  box-shadow: var(--gs-sunken);
  color: var(--gs-gold-head);
}
.gs-lock-mark {
  position: relative;
  display: inline-block;
  width: 12px;
  height: 8px;
  margin-top: 4px;
  box-sizing: border-box;
  border: 1px solid currentColor;
  border-radius: 0;
}
.gs-lock-mark::before {
  content: "";
  position: absolute;
  left: 3px;
  top: -6px;
  width: 6px;
  height: 6px;
  box-sizing: border-box;
  border: 1px solid currentColor;
  border-bottom: 0;
  border-radius: 0;
}

/* the lit toggle: a latch that stays down until it is pressed again - auto-battle, a pinned
   skill, a filter chip that is currently filtering. A latched control genuinely IS pushed in,
   so it takes the sunken bevel, and it says ON with the one thing hover never spends: a brass
   label. Pressed-and-grey is a finger passing over; pressed-and-brass is a state that will
   still be there when the pointer leaves.

   `is-active` is the system's word for a latched control, the same one .gs-tab already uses.
   Declared before the disabled block deliberately: the two rules have equal specificity, so a
   disabled toggle reads disabled rather than lit. It also outranks --primary (two classes beats
   one), which is right - what a control IS beats how loud it was asked to be. */
.gs-btn.is-active {
  background: var(--gs-surface-panel);
  box-shadow: var(--gs-sunken);
  color: var(--gs-gold-head);
}
/* a lit control still answers the pointer, or it feels dead under the hand */
.gs-btn.is-active:hover { background: var(--gs-surface-raised); }

/* The primary. A brass-500 face takes DARK text: brass is too light to sit behind --gs-text,
   and it hovers LIGHTER to brass-400 (8.8:1), which also preserves "hover lightens one stop". */
.gs-btn--primary {
  background: var(--brass-500);
  box-shadow: var(--gs-raised);
  color: var(--gs-text-on-brass);
  text-shadow: none;
}
.gs-btn--primary:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--brass-400); }

/* the ceremonial commit: craft, dismantle, confirm. The primary at the large control height.
   No hexagon: a clip-path cuts the box AND the bevel that says which way it faces, so the
   shape has to stay square for the object to read as raised at all. */
.gs-btn--commit {
  height: var(--gs-size-control-lg);
  padding: 0 32px;
  background: var(--brass-500);
  box-shadow: var(--gs-raised);
  clip-path: none;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-label);
  font-weight: 700;
  letter-spacing: var(--gs-track-label);
  color: var(--gs-text-on-brass);
  text-shadow: none;
}
.gs-btn--commit:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--brass-400); }

/* Disabled is a flat stone-600 face with muted text: no bevel change, no opacity trick. */
.gs-btn:disabled,
.gs-btn[aria-disabled="true"] {
  cursor: var(--cur-arrow, default);
  background: var(--stone-600);
  box-shadow: none;
  color: var(--gs-text-disabled);
  text-shadow: none;
  filter: none;
}
.gs-btn--commit:disabled { background: var(--stone-600); color: var(--gs-text-disabled); clip-path: none; }

/* icon button - the gear, the system marks. 24px, square, same bevel as any other button. */
.gs-btn-round {
  appearance: none;
  cursor: var(--cur-hand, pointer);
  position: relative;
  width: var(--gs-size-icon-button);
  height: var(--gs-size-icon-button);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  border-radius: 0;
  padding: 0;
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised-thin);
}
.gs-btn-round > span {
  display: flex;
  width: 100%;
  height: 100%;
  align-items: center;
  justify-content: center;
  border-radius: 0;
  background: none;
  box-shadow: none;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  color: var(--gs-gold-head);
}
.gs-btn-round:hover { background: var(--gs-surface-raised); }
.gs-btn-round:active { box-shadow: var(--gs-sunken-thin); transform: translate(1px, 1px); }

/* the system launcher - clicking it opens a system. Squared off like everything else; the
   diamond clip cut the bevel, so it read as a floating mark rather than a control. */
.gs-btn-diamond {
  appearance: none;
  cursor: var(--cur-hand, pointer);
  position: relative;
  width: var(--gs-size-control-lg);
  height: var(--gs-size-control-lg);
  padding: 0;
  border: var(--gs-border-width) solid var(--gs-border-hard);
  border-radius: 0;
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised);
  clip-path: none;
  filter: none;
}
.gs-btn-diamond:hover { background: var(--gs-surface-raised); }
.gs-btn-diamond:active { box-shadow: var(--gs-sunken); transform: translate(1px, 1px); }
.gs-btn-diamond > span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  clip-path: none;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  color: var(--gs-gold-head);
}
/* the only ember outside HP: something is waiting for the player */
.gs-dot {
  position: absolute;
  right: -2px;
  top: -2px;
  width: 6px;
  height: 6px;
  border-radius: 0;
  background: var(--gs-alert);
  box-shadow: 0 0 0 1px var(--gs-border-hard);
}

/* keybind cap - a sunken well on the control it belongs to, always mono, always near-black */
.gs-key {
  display: inline-flex;
  align-items: center;
  height: var(--gs-size-statbar);
  padding: 0 var(--gs-gap-sm);
  background: var(--gs-void);
  box-shadow: var(--gs-sunken-thin);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  line-height: 1;
  color: var(--gs-gold-head);
}

/* boon badge - a running reward multiplier (VIP, a double-experience card). A sunken well with
   the multiplier in brass and the time left muted: the meaning is on the edge, as everywhere. */
.gs-boon {
  display: inline-flex;
  align-items: center;
  gap: var(--gs-gap-sm);
  height: 18px;
  padding: 0 var(--gs-gap-sm);
  background: var(--gs-surface-well);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-sunken-thin);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-bar);
  line-height: 1;
  color: var(--gs-text);
  white-space: nowrap;
}
.gs-boon__mult { color: var(--gs-gold-head); }
.gs-boon__left { color: var(--gs-text-mute); }

/* --------------------------------------------------- INPUTS & DROPDOWNS */
/* A field is a SUNKEN well: flat --gs-surface-field, no texture, hard border, 28px so it sits
   on the same two lines as a button. Focus is the global 1px inset brass ring; no field
   re-implements it. */
.gs-input {
  height: var(--gs-size-control);
  padding: 0 var(--gs-gap-md);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  border-radius: 0;
  background: var(--gs-surface-field);
  box-shadow: var(--gs-sunken);
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-dense);
  color: var(--gs-text);
  text-shadow: none;
}
.gs-input::placeholder { color: var(--gs-text-mute); }
.gs-input:disabled { background: var(--stone-600); box-shadow: none; color: var(--gs-text-disabled); }

/* The count field's own arrows. The browser draws a spinner on a number input, but it fades it in
   on hover and paints it in the OS palette - two things a gold-on-dark panel cannot use. So the
   native one is dropped inside this wrapper only (other number fields in the game keep theirs)
   and two buttons take its place, always drawn, in the same gold the marketplace rows and the
   panel edges are cut from.

   Triangles are borders, not glyphs: a character would ride the UI face's metrics and sit off
   centre at 8px. The pair share the field's 26px, so the control keeps one height with the
   slider and the All button beside it. */
.gs-stepper { display: inline-flex; align-items: stretch; height: 26px; }
.gs-stepper .gs-input { padding-right: 4px; }
.gs-stepper__arrows { display: flex; flex-direction: column; width: 15px; }
.gs-stepper__arrow {
  flex: 1;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  cursor: var(--cur-hand, pointer);
  background: linear-gradient(180deg, var(--gs-solid-ctl-top), var(--gs-solid-ctl-bot));
  box-shadow: inset 0 0 0 1px rgba(201, 163, 95, 0.24);
}
.gs-stepper__arrow:hover { background: rgba(201, 163, 95, 0.14); }
.gs-stepper__arrow:active { background: rgba(201, 163, 95, 0.22); }
.gs-stepper__arrow::before {
  content: "";
  border: 4px solid transparent;
}
.gs-stepper__arrow--up::before { border-bottom-color: var(--gs-gold-head); margin-bottom: 3px; }
.gs-stepper__arrow--down::before { border-top-color: var(--gs-gold-head); margin-top: 3px; }
/* The browser's own spinner, gone - only here, and in both dialects. */
.gs-stepper .gs-input::-webkit-inner-spin-button,
.gs-stepper .gs-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.gs-stepper .gs-input { -moz-appearance: textfield; appearance: textfield; }

/* One slider, for a count a player would rather drag than type. Same sunken track as a resource
   bar, filled in gold up to the value, with a bevelled gold thumb - a range input is the one
   control the browser draws in three pieces, so each pseudo-element carries its own half of the
   language. The fill is a gradient stop sized by --gs-fill, set from script: a WebKit track has no
   progress element and cannot be filled any other way. Firefox has one, so it gets both and
   ignores the gradient. */
.gs-range { appearance: none; height: 22px; background: transparent; cursor: var(--cur-hand, pointer); }
.gs-range:focus { outline: 0; }
.gs-range::-webkit-slider-runnable-track {
  height: 9px;
  border-radius: 5px;
  background:
    linear-gradient(180deg, var(--gs-gold-bright), #CF9F45) 0 / var(--gs-fill, 0%) 100% no-repeat,
    linear-gradient(180deg, #14100A, #241D13);
  box-shadow: inset 0 0 0 1px rgba(201, 163, 95, 0.34), inset 0 2px 4px rgba(0, 0, 0, 0.85);
}
.gs-range::-moz-range-track {
  height: 9px;
  border-radius: 5px;
  background: linear-gradient(180deg, #14100A, #241D13);
  box-shadow: inset 0 0 0 1px rgba(201, 163, 95, 0.34), inset 0 2px 4px rgba(0, 0, 0, 0.85);
}
.gs-range::-moz-range-progress {
  height: 9px;
  border-radius: 5px;
  background: linear-gradient(180deg, var(--gs-gold-bright), #CF9F45);
}
.gs-range::-webkit-slider-thumb {
  appearance: none;
  width: 13px;
  height: 17px;
  margin-top: -4px;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--gs-gold-bright), #8A6A2C);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45), 0 1px 2px rgba(0, 0, 0, 0.7);
}
.gs-range::-moz-range-thumb {
  width: 13px;
  height: 17px;
  border: 0;
  border-radius: 3px;
  background: linear-gradient(180deg, var(--gs-gold-bright), #8A6A2C);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45), 0 1px 2px rgba(0, 0, 0, 0.7);
}
.gs-range:disabled { cursor: var(--cur-arrow, default); opacity: 0.5; }

.gs-select {
  height: var(--gs-size-control);
  padding: 0 var(--gs-gap-lg) 0 var(--gs-gap-md);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  border-radius: 0;
  appearance: none;
  cursor: var(--cur-hand, pointer);
  /* The caret rides on the control itself rather than on a wrapper element: every select in the
     game is dropped straight into a row, and a wrapper nobody remembers to add is a select with
     no arrow. A data URI cannot read a custom property, so the fill is --brass-300 spelled out. */
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 5'%3E%3Cpath d='M0 0h8L4 5z' fill='%23ecc472'/%3E%3C/svg%3E") right 6px center / 7px 5px no-repeat;
  background-color: var(--gs-surface-field);
  box-shadow: var(--gs-sunken);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-label);
  letter-spacing: var(--gs-track-label);
  color: var(--gs-text);
}
/* The list the browser draws itself. gs-select.js replaces it with .gs-menu below, so these rules
   are the floor, not the design: a page that loads gs.css without the script - or a browser that
   insists on the OS popup - still gets a dark list with readable rows instead of the system
   default. The row highlight is the OS's and cannot be reached from here; that is the whole
   reason .gs-menu exists. */
.gs-select option {
  background-color: var(--stone-900);
  color: var(--stone-50);
}
.gs-select option:disabled { color: var(--stone-300); }
.gs-checkbox { accent-color: var(--gs-gold); width: 13px; height: 13px; border-radius: 0; }

/* --------------------------------------------------------------- THE MENU */
/* The dropdown a .gs-select opens, drawn by us. The native popup is an OS window: it takes the
   system font, the system highlight and the system metrics, so on Windows our control opened
   into a blue-on-black system list. public/gs-select.js suppresses that popup and mounts this
   instead, reading the live <select> every time it opens - the <select> stays the element, the
   value and the change event, and this is only its face.

   Fixed, not absolute: the script positions it against the viewport so it can escape a panel's
   overflow and flip above the control when the screen runs out below.

   It is floating chrome, so it takes one hard-offset drop shadow and no blur. */
.gs-menu {
  position: fixed;
  z-index: var(--gs-z-menu);
  min-width: 120px;
  /* Options can be whole sentences (a recipe line). Cap the box and let the row ellipsise
     rather than let one line decide how wide the dropdown is. */
  max-width: min(420px, calc(100vw - 16px));
  max-height: 260px;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--gs-gap-xs);
  background: var(--gs-surface-panel);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised-thin), var(--gs-shadow-dialog);
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-dense);
}
.gs-menu__opt {
  padding: var(--gs-pad-cell);
  color: var(--gs-text);
  cursor: var(--cur-hand, pointer);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Two states that mean different things and must not look alike: is-selected is the row this
   select is ON, and stays lit while the pointer wanders; is-active is the row the pointer or the
   arrow keys are over, and is the one Enter would take. A selection surface is a FLAT brass fill
   with no bevel, and anything on it takes --gs-text-on-selected rather than a literal. */
.gs-menu__opt.is-selected {
  color: var(--gs-text-on-selected);
  background: var(--gs-surface-selected);
  box-shadow: none;
}
.gs-menu__opt.is-active {
  color: var(--gs-text);
  background: var(--gs-row-hover);
}
.gs-menu__opt.is-active.is-selected { color: var(--gs-text-on-selected); background: var(--gs-surface-selected); box-shadow: inset 2px 0 0 var(--gs-gold-bright); }
.gs-menu__opt[aria-disabled="true"] { color: var(--gs-text-disabled); cursor: var(--cur-arrow, default); background: none; box-shadow: none; }
/* An <optgroup> label: a heading, not a row, so it borrows the caption voice and never highlights. */
.gs-menu__group {
  padding: var(--gs-gap-sm) var(--gs-gap-md) var(--gs-gap-xs);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-caption);
  letter-spacing: var(--gs-track-label);
  text-transform: uppercase;
  color: var(--gs-gold-label);
  cursor: var(--cur-arrow, default);
}

/* The system slider - a count a player would rather drag than type. A native range input, so
   the keyboard still works; each pseudo-element carries its own half of the language. Sunken
   flat track, squared brass thumb with a raised bevel. The WebKit track has no progress element
   and cannot be filled any other way than a hard two-stop gradient sized by --gs-fill, set from
   script; Firefox has ::-moz-range-progress and ignores it. Those are the only two gradients
   left in the system and both are hard stops, not blends. */
.gs-range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: var(--gs-size-control);
  background: transparent;
  accent-color: var(--gs-gold);
  cursor: var(--cur-hand, pointer);
}
.gs-range::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 0;
  background:
    linear-gradient(90deg, var(--gs-gold) 0 var(--gs-fill, 0%), var(--gs-bar-track) var(--gs-fill, 0%) 100%);
  box-shadow: var(--gs-sunken-thin);
  border: var(--gs-border-width) solid var(--gs-border-hard);
}
.gs-range::-moz-range-track {
  height: 8px;
  border-radius: 0;
  background: var(--gs-bar-track);
  box-shadow: var(--gs-sunken-thin);
  border: var(--gs-border-width) solid var(--gs-border-hard);
}
.gs-range::-moz-range-progress { height: 8px; border-radius: 0; background: var(--gs-gold); }
.gs-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 18px;
  margin-top: -6px;
  border-radius: 0;
  border: var(--gs-border-width) solid var(--gs-border-hard);
  background: var(--brass-500);
  box-shadow: var(--gs-raised-thin);
}
.gs-range::-moz-range-thumb {
  width: 12px;
  height: 18px;
  border-radius: 0;
  border: var(--gs-border-width) solid var(--gs-border-hard);
  background: var(--brass-500);
  box-shadow: var(--gs-raised-thin);
}
/* A bus that is switched off still shows where its volume is set, greyed rather than gone. */
.gs-range:disabled { cursor: var(--cur-arrow, default); opacity: 0.4; }

/* ----------------------------------------------------------------- SCRIM */
/* The cover a modal dialog sits on. One class, so no panel reinvents inset:0. */
.gs-scrim {
  position: fixed;
  inset: 0;
  /* above the window stack, whose ceiling is --gs-z-window + the number of mounted panels */
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gs-scrim);
}

/* ---------------------------------------------------------- SCROLL AREAS */
/* 12px, the system metric. A sunken track and a raised thumb, both bevelled, no radius. */
.gs-scroll { overflow-y: auto; min-height: 0; scrollbar-color: var(--stone-500) var(--gs-surface-well); scrollbar-width: thin; }
.gs-scroll::-webkit-scrollbar { width: var(--gs-size-scrollbar); height: var(--gs-size-scrollbar); }
.gs-scroll::-webkit-scrollbar-track { background: var(--gs-surface-well); box-shadow: var(--gs-sunken-thin); }
.gs-scroll::-webkit-scrollbar-thumb { background: var(--gs-surface-panel); box-shadow: var(--gs-raised-thin); border: var(--gs-border-width) solid var(--gs-border-hard); }
.gs-scroll::-webkit-scrollbar-thumb:hover { background: var(--gs-surface-raised); }

/* --------------------------------------------------------------- TOOLTIP */
/* Floating chrome: a stone panel, one hard border, one stamped drop shadow, no blur.
   The design system's own tooltip is the parchment card, and this one is not: an item card
   here carries six quality colours, a blue socket ink and a moss gain, and every one of those
   is tuned to clear 4.5:1 on a DARK surface. Flipping the card to parchment would silently
   drop all of them below the floor, so the card stays stone and parchment stays for documents
   (.gs-parchment). That is a deliberate departure, recorded here so it is not read as a miss. */
.gs-tooltip {
  width: min(286px, calc(100vw - 16px));
  box-sizing: border-box;
  background: var(--gs-surface-panel);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised), var(--gs-shadow-dialog);
  color: var(--gs-text);
}
/* HUD icon tooltips contain only their label. Let that label size its own background while item,
   equipment and skill cards retain the shared readable column above. */
.gs-tooltip:has(> .gs-tooltip-name-only) {
  width: max-content;
  max-width: calc(100vw - 16px);
}
.gs-tooltip__head {
  display: flex;
  gap: var(--gs-gap-md);
  padding: var(--gs-gap-md);
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised-thin);
  border-bottom: var(--gs-border-width) solid var(--gs-border-hard);
}
.gs-tooltip__head > :last-child { min-width: 0; }
/* THE WORN STAMP, and THE CARD BESIDE THE CARD.
   A carried piece of equipment is hovered against the one already in its slot (client/panel.ts
   tooltip, client/items.ts wornCard): two cards, side by side, tops aligned. The second one says
   which of the two is on the character - a word and not a colour, because both cards may be the
   same quality band and "which am I wearing" is the first question a comparison asks. */
.gs-tooltip__worn {
  align-self: flex-start;
  margin-left: auto;
  flex: none;
  padding: 1px 5px;
  background: var(--gs-surface-slot);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  color: var(--gs-gold-head);
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-bar);
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* The compared card is the quieter of the two: the piece being read is the one under the pointer,
   and the worn one is the reference it is held against. */
.gs-tooltip--compare { opacity: .96; }
.gs-tooltip--compare .gs-tooltip__head { background: var(--gs-surface-slot); }
/* Item art goes in a sunken well at a fixed pixel size, rendered pixelated. */
.gs-tooltip__icon {
  width: var(--gs-size-slot);
  height: var(--gs-size-slot);
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gs-surface-slot);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-sunken-thin);
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-mono);
  color: var(--gs-text-mute);
}
.gs-tooltip__name {
  display: block;
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-item-title);
  line-height: var(--gs-lead-ui);
  font-weight: 700;
  letter-spacing: var(--gs-track-label);
  text-transform: uppercase;
  color: var(--gs-text);
  text-shadow: var(--gs-text-stamp);
  overflow-wrap: anywhere;
}
.gs-tooltip__sub { margin-top: 2px; font-family: var(--gs-face-mono); font-size: var(--gs-size-dense); line-height: var(--gs-lead-ui); color: var(--gs-text-mute); }
.gs-tooltip__body {
  padding: var(--gs-pad-panel);
  display: flex;
  flex-direction: column;
  gap: var(--gs-gap-md);
  background: var(--gs-surface-panel);
  color: var(--gs-text);
}
.gs-tooltip__section {
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-caption);
  font-weight: 700;
  letter-spacing: var(--gs-track-label);
  color: var(--gs-gold-label);
  border-bottom: var(--gs-border-width) solid var(--gs-border-soft);
  padding-bottom: var(--gs-gap-xs);
  text-transform: uppercase;
}
.gs-tooltip__body--gear { gap: var(--gs-gap-xs); padding: var(--gs-pad-panel); }
.gs-tooltip__body--gear .gs-tooltip__section { margin-bottom: var(--gs-gap-xs); }

/* The star rank. Ordinary and luxury ranks stay restrained; the server-authored 10-15 band opts
   into the radiant treatment below so endgame equipment has a visibly exclusive finish. */
.gs-tooltip__stars {
  /* This block used to read its brass off `.gs`, which play.html carried. The skin change took
     that class off <body>, so every --gs-* below resolved to nothing: the flanking rules went
     transparent, the stamp went off and the particles turned invisible while the markup still
     said they were there. The five names are re-declared here from the ramps on :root - the same
     values `.gs` hands them, so a page that still has the class sees no change - and the rating
     now paints the same whatever sheet is above it. */
  --gs-gold-head: var(--brass-300);
  --gs-gold-bright: var(--brass-300);
  --gs-gold: var(--brass-500);
  --gs-gold-shade: var(--brass-700);
  --gs-text-stamp: 1px 1px 0 #000;

  position: relative;
  min-height: 26px;
  display: grid;
  grid-template-columns: minmax(12px, 1fr) auto minmax(12px, 1fr);
  align-items: center;
  gap: var(--gs-gap-md);
  color: var(--gs-gold-head);
}
.gs-tooltip__stars::before, .gs-tooltip__stars::after {
  content: "";
  height: 1px;
  background: var(--gs-gold-shade);
}
/* The stars are drawn files, not characters: the HUD carries no icon font and a text star is
   whatever the platform decides it is. They ride the same font-size ladder so the three tiers
   still differ by SIZE, which is what a screenshot and a colour-blind reader keep. */
.gs-tooltip__stars i {
  font-style: normal;
  font-size: 13px;
  line-height: 1;
  letter-spacing: 2px;
  text-shadow: var(--gs-text-stamp);
}
.gs-star { width: 1em; height: 1em; vertical-align: -0.12em; }
.gs-tooltip__stars i .gs-star { margin-right: 2px; }
.gs-tooltip__stars.is-luxury { min-height: 30px; }
.gs-tooltip__stars.is-luxury::before, .gs-tooltip__stars.is-luxury::after { height: 2px; background: var(--gs-gold); }
.gs-tooltip__stars.is-luxury i { font-size: 16px; letter-spacing: 3px; color: var(--gs-gold-bright); }
.gs-tooltip__stars.is-radiant {
  min-height: 34px;
  grid-template-columns: minmax(0, 1fr);
  gap: 0;
}
.gs-tooltip__stars.is-radiant::before, .gs-tooltip__stars.is-radiant::after {
  display: none;
}
.gs-tooltip__stars.is-radiant i {
  position: relative;
  z-index: 1;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 5px 6px;
  font-size: 18px;
  letter-spacing: 0;
  color: var(--gs-gold-bright);
  background: none;
  box-shadow: none;
}
/* The aura is its own layer. Keeping the animation off `i` means neither the stars nor their
   layout box scale, brighten or shift while the gold light breathes behind them. */
.gs-tooltip__stars.is-radiant i::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 5px 0 6px;
  border-radius: 50%;
  background: rgba(236, 196, 114, .62);
  filter: blur(5px);
  opacity: .34;
  pointer-events: none;
  animation: gs-star-aura 1.35s ease-in-out infinite;
}
/* A bright upper edge, a dark lower extrusion and a tight halo give each endgame star its own
   embossed 3D read instead of turning the entire rating into one flat yellow strip. */
.gs-tooltip__stars.is-luxury .gs-star {
  margin-right: 0;
  transform: translateY(-1px);
  transform-origin: center;
  filter:
    drop-shadow(0 -1px 0 rgba(255, 246, 194, .95))
    drop-shadow(0 1px 0 var(--brass-700))
    drop-shadow(0 2px 1px rgba(11, 11, 12, .9))
    drop-shadow(0 0 4px rgba(236, 196, 114, .9));
}
.gs-tooltip__stars.is-radiant .gs-star {
  animation: gs-star-turn 30s linear infinite;
}
/* Thirteen to fifteen icons need the full row. Tighten the icons instead of clipping the
   achievement at the tooltip edge. */
.gs-tooltip__stars.is-radiant:is([data-stars="13"], [data-stars="14"], [data-stars="15"]) i {
  padding-inline: 2px;
  font-size: 16px;
}

/* MORE THAN TEN STARS ARE TWO ROWS: ten across, the rest under them, both from the left edge.
   items.ts marks the count with data-star-rows.

   The card is 17em wide whatever is in it, and fifteen radiant icons at 16px do not fit one row
   of that - so past ten the rating either spilled its own card or squeezed to nothing. A phone
   has had this since v0.13.11 (place.css); it was never a phone problem, it was a card-width
   problem, and the card is the same seventeen em on a laptop. Lifted here so both get it, and the
   phone block keeps only what is actually about a phone: its smaller type.

   The row is left-aligned, so the flanking rules gs.css draws either side of a centred rating
   have nothing to flank and go. */
/* `width: max-content` and not a bare block: the particle layer is positioned against this box
   (inset 2px 12%), so a block running the card's full width scattered the radiant sparks across
   the empty space to the right of a short second row. Shrink-wrapped, they hug the stars. */
.gs-tooltip__stars[data-star-rows] { display: block; width: max-content; max-width: 100%; }
.gs-tooltip__stars[data-star-rows]::before,
.gs-tooltip__stars[data-star-rows]::after { display: none; }
.gs-tooltip__stars[data-stars][data-star-rows] i {
  display: grid;
  grid-template-columns: repeat(10, auto);
  justify-content: start;
  row-gap: 2px;
}

/* THE RADIANT AURA, ON TWO ROWS. The single-row aura is one ellipse behind the strip
   (`.is-radiant i::before`, inset 5px 0 6px, border-radius 50%), and that shape cannot describe
   two rows: stretched over both it is widest through the GAP between them, so the light pools in
   the empty band where no star is, and its right half hangs past the end of a second row holding
   two icons out of ten.
   
   So the light follows the STARS rather than the box. A drop-shadow is cast from the alpha
   actually painted, which means it hugs ten icons on one line and two on the next without being
   told the shape - and it stays off the icons themselves: a shadow behind them brightens and
   softens while nothing about the stars scales, shifts or moves, which is the promise the
   ::before layer was written to keep. */
.gs-tooltip__stars[data-star-rows].is-radiant i::before { display: none; }
.gs-tooltip__stars[data-star-rows].is-radiant i {
  animation: gs-star-aura-cast 1.35s ease-in-out infinite;
}
/* The sparks come off the FULL row. The nine of them are spread over the whole particle layer by
   per cent (4%, 16%, 29% ... of its width and height), and over two rows that layer is half empty:
   a second row holding two stars out of ten left four sparks drifting in blank paper to the right
   of nothing. Held to the top row, which is always the full ten, they read as thrown off the
   rating again. */
.gs-tooltip__stars[data-star-rows] .gs-tooltip__star-particles {
  bottom: auto;
  height: 52%;
}
.gs-tooltip__star-particles {
  position: absolute;
  inset: 2px 12%;
  display: block;
  pointer-events: none;
}
.gs-tooltip__star-particles b {
  position: absolute;
  width: 2px;
  height: 2px;
  background: var(--gs-gold-bright);
  box-shadow: 0 0 4px var(--gs-gold);
  opacity: .72;
}
.gs-tooltip__star-particles b:nth-child(1) { left: 4%; top: 18%; }
.gs-tooltip__star-particles b:nth-child(2) { left: 16%; bottom: 6%; animation-delay: -.8s; }
.gs-tooltip__star-particles b:nth-child(3) { left: 29%; top: 2%; animation-delay: -1.3s; }
.gs-tooltip__star-particles b:nth-child(4) { left: 41%; bottom: 0; animation-delay: -.4s; }
.gs-tooltip__star-particles b:nth-child(5) { left: 50%; top: 0; animation-delay: -1s; }
.gs-tooltip__star-particles b:nth-child(6) { right: 39%; bottom: 4%; animation-delay: -1.5s; }
.gs-tooltip__star-particles b:nth-child(7) { right: 27%; top: 3%; animation-delay: -.6s; }
.gs-tooltip__star-particles b:nth-child(8) { right: 14%; bottom: 8%; animation-delay: -1.1s; }
.gs-tooltip__star-particles b:nth-child(9) { right: 3%; top: 20%; animation-delay: -.2s; }
@keyframes gs-star-aura {
  0%, 100% {
    filter: blur(5px);
    opacity: .3;
  }
  50% {
    filter: blur(8px);
    opacity: .9;
  }
}
/* The breathing light of a two-row rating, cast from the icons themselves. Two shadows and not
   one: the tight one keeps an edge on every star so the rating does not dissolve into a haze at
   the top of the breath, and the wide one is the glow. */
@keyframes gs-star-aura-cast {
  0%, 100% {
    filter: drop-shadow(0 0 2px rgba(236, 196, 114, .55)) drop-shadow(0 0 6px rgba(236, 196, 114, .3));
  }
  50% {
    filter: drop-shadow(0 0 3px rgba(236, 196, 114, .85)) drop-shadow(0 0 10px rgba(236, 196, 114, .7));
  }
}
@keyframes gs-star-turn {
  from { transform: translateY(-1px) rotate(0deg); }
  to { transform: translateY(-1px) rotate(360deg); }
}

.gs-tooltip__stat {
  min-height: 22px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 46px;
  align-items: center;
  column-gap: var(--gs-gap-sm);
  padding: var(--gs-pad-cell);
  font-size: var(--gs-size-dense);
  border-bottom: var(--gs-border-width) solid var(--gs-hair-faint);
}
.gs-tooltip__stat:nth-child(odd) { background: var(--gs-row-alt); }
.gs-tooltip__stat-main { min-width: 0; display: flex; align-items: baseline; gap: var(--gs-gap-sm); line-height: var(--gs-lead-ui); }
.gs-tooltip__stat-main span { color: var(--gs-text); }
/* Absorb and critical damage: a proportion, not an amount. See PERCENT_STATS in shared/stats.ts. */
.gs-tooltip__stat-main span.is-rare { color: var(--gs-stat-rare); }
.gs-tooltip__stat-main b { color: var(--gs-text-mute); font-weight: 400; }
.gs-tooltip__stat-main i { color: var(--gs-q-text-blue); font-family: var(--gs-face-display); font-style: normal; }
/* Not all guaranteed: at least one option line is inside this total. See statBlock in items.ts. */
.gs-tooltip__stat-main i.is-opt { color: var(--gs-stat-opt); }
.gs-tooltip__stat-bonus { text-align: right; color: var(--gs-gain-ink); font-family: var(--gs-face-display); }
.gs-tooltip__section--sockets { margin-top: var(--gs-gap-xs); }
.gs-tooltip__socket {
  min-height: 20px;
  display: grid;
  grid-template-columns: 10px minmax(0, 1fr);
  align-items: center;
  column-gap: var(--gs-gap-sm);
  padding: 2px var(--gs-gap-xs);
  border-bottom: var(--gs-border-width) solid var(--gs-hair-faint);
}
.gs-tooltip__socket:nth-child(odd) { background: var(--gs-row-alt); }
.gs-tooltip__socket-icon {
  width: 8px;
  height: 8px;
  display: block;
  box-sizing: border-box;
  border: 1px solid var(--gs-gold-bright);
  border-radius: 50%;
  background: var(--gs-gold);
  box-shadow: inset 0 1px 0 rgba(255, 246, 194, .72), 0 0 4px rgba(217, 164, 65, .48);
}
/* Locked holes retain the same round language but recede until the smith opens them. */
.gs-tooltip__socket-icon.is-locked {
  border-color: var(--gs-gold-shade);
  background: var(--stone-800);
  box-shadow: none;
}
.gs-tooltip__socket-info { min-width: 0; display: block; }
.gs-tooltip__socket-info i { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--gs-text); font-style: normal; font-size: var(--gs-size-dense); }
.gs-tooltip__socket-info b { color: var(--gs-text-mute); font-family: var(--gs-face-display); font-size: var(--gs-size-dense);
  line-height: var(--gs-lead-ui); }
.gs-tooltip__socket.has-bonus .gs-tooltip__socket-info b { color: var(--moss-300); }
/* A locked socket is muted CONTENT, not a disabled control, so it takes --gs-text-mute and
   still clears the contrast floor - never an opacity trick. */
.gs-tooltip__socket.is-locked .gs-tooltip__socket-info i,
.gs-tooltip__socket.is-locked .gs-tooltip__socket-info b { color: var(--gs-text-mute); }
.gs-tooltip__line { display: flex; align-items: baseline; gap: var(--gs-gap-sm); font-size: var(--gs-size-dense); }
.gs-tooltip__line::before { content: "\25C6"; font-size: var(--gs-size-bar); color: var(--gs-gold-hair); }
.gs-tooltip__line span:first-of-type { flex: 1; color: var(--gs-text); }
.gs-tooltip__gain { color: var(--gs-gain-ink); }
/* Where a stat came from: quiet, small, and never bold - the total is what the eye lands on. */
.gs-tooltip__src { font-size: var(--gs-size-dense); color: var(--gs-text-mute); }
/* The item's own words. Sits in a __body block under the head, one <p> per authored line so a
   container's bullet list keeps its shape. Narrow enough that a long description wraps into a
   readable column rather than stretching the card across the screen. */
.gs-tooltip__desc { margin: 0; max-width: 34ch; font-size: var(--gs-size-dense); line-height: var(--gs-lead-body); color: var(--gs-text-prose); }
.gs-tooltip__src i { font-style: normal; }
.gs-tooltip__flavour { margin: 0; font-size: var(--gs-size-mono); line-height: var(--gs-lead-body); font-style: italic; color: var(--gs-text-mute); }
.gs-tooltip__foot {
  display: flex;
  justify-content: space-between;
  padding-top: var(--gs-gap-sm);
  border-top: var(--gs-border-width) solid var(--gs-border-soft);
  font-size: var(--gs-size-dense);
  color: var(--gs-text-mute);
}

/* Mobile interaction contract: the world consumes drags, controls consume taps, and scrollable
   sheets retain vertical panning. Floating cards cannot exceed the viewport they explain. */
@media (max-width: 760px), (max-device-width: 1000px), (hover: none) and (pointer: coarse) {
  body { overscroll-behavior: none; touch-action: none; }
  button, input, select, textarea, .gs-btn, .gs-tab, .gs-slot { touch-action: manipulation; }
  .gs-window, .gs-scroll, .gs-menu, .gs-tooltip { touch-action: pan-y; }
  .gs-tooltip {
    width: min(340px, calc(100vw - 12px));
    max-width: calc(100vw - 12px);
    max-height: calc(100dvh - 12px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .gs-menu { max-height: min(52dvh, 320px); }
  .gs-menu__opt { min-height: 34px; display: flex; align-items: center; }
}

/* ------------------------------------------------------------- LIST ROWS */
/* Zebra, a hover wash, and selection as a FLAT brass fill with no bevel. Text on a selection
   surface takes --gs-text-on-selected, never a literal: a selection surface flips lightness
   between themes and a hardcoded light text goes light-on-light in the other one. */
.gs-list { display: flex; flex-direction: column; }
.gs-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--gs-gap-md);
  padding: var(--gs-pad-cell);
  background: var(--gs-row-alt);
  cursor: var(--cur-hand, pointer);
}
.gs-row:nth-child(even) { background: transparent; }
.gs-row:hover { background: var(--gs-row-hover); }
.gs-row.is-selected { background: var(--gs-surface-selected); box-shadow: none; }
.gs-row.is-selected .gs-row__name,
.gs-row.is-selected .gs-row__meta { color: var(--gs-text-on-selected); }
.gs-row__name { font-size: var(--gs-size-dense); color: var(--gs-text); }
.gs-row__meta { font-size: var(--gs-size-mono); color: var(--gs-text-mute); }
.gs-row__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.gs-row__text .gs-row__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gs-row__end { margin-left: auto; text-align: right; font-family: var(--gs-face-display); }

/* ------------------------------------------------------ CURRENCY DISPLAY */
/* Crowns - the one denomination. Money is brass, unrounded and comma-grouped, in the numeral
   face. A sunken well, squared: there are no pills in this system. */
.gs-coin {
  display: inline-flex;
  align-items: center;
  gap: var(--gs-gap-sm);
  height: 20px;
  padding: 0 var(--gs-gap-sm);
  border-radius: 0;
  background: var(--gs-surface-well);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-sunken-thin);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-dense);
  color: var(--gs-gold-head);
}
/* Crowns wear a coin, not an abstract pip. The bare 10px brass square read as a blank slot
   wherever it stood next to a number with no item beside it - the quest reward block above all.
   Same treatment as the Lumi coin below, so the two denominations differ by their art alone. */
.gs-coin::before {
  content: "";
  width: 12px;
  height: 12px;
  border-radius: 0;
  background: url(/data/art/ui/crown-coin.abcbfaf6.svg) center / contain no-repeat;
  box-shadow: none;
}

/* The same coin without the well. The chip is right for a standalone amount - a purse, an
   estimated total, a headline stat - and wrong wherever prices are stacked in a dense column:
   in the order book the well fights the row's own fill bar, and down a list of orders every
   line would grow its own border and double the grid. Glyph only, so a price is still marked
   as Crowns without a box drawn around each one. Used WITH .gs-coin, which owns the ::before. */
.gs-coin--bare {
  height: auto;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  color: inherit;
  font-family: inherit;
}

/* Lumi - the premium wallet. The one deliberate exception to "one metal": a real-money
   currency has to read apart from Crowns at a glance, so it keeps its own art instead of the
   brass pip. Add this class to the amount span at a premium front. */
.gs-coin--lumi::before {
  width: 12px;
  height: 12px;
  background: url(/data/art/ui/lumi.e188ac0d.webp) center / contain no-repeat;
  box-shadow: none;
}

/* ------------------------------------------------------ AVATAR / PAPERDOLL */
/* A portrait is art in a sunken well at a fixed pixel size. Square, because nothing here is
   round, and the frame is the hard border plus the bevel rather than a metal ring. */
.gs-portrait {
  position: relative;
  width: 60px;
  height: 60px;
  flex: none;
  border-radius: 0;
  padding: 0;
  border: var(--gs-border-width) solid var(--gs-border-hard);
  background: var(--gs-surface-well);
  box-shadow: var(--gs-sunken);
}
.gs-portrait__face {
  position: relative; /* headHtml() places one clipped layer per paperdoll part inside it */
  width: 100%;
  height: 100%;
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--gs-surface-well);
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-mono);
  color: var(--gs-text-mute);
}
.gs-portrait__level {
  position: absolute;
  left: 0;
  bottom: 0;
  transform: none;
  height: 14px;
  padding: 0 var(--gs-gap-sm);
  display: flex;
  align-items: center;
  border-radius: 0;
  background: var(--gs-surface-alt);
  box-shadow: var(--gs-raised-thin);
  font-family: var(--gs-face-display);
  font-weight: 700;
  font-size: var(--gs-size-bar);
  line-height: 1;
  color: var(--gs-gold-head);
}
.gs-portrait--target { width: 40px; height: 40px; }

/* the standing character render behind the equipment slots: the largest well in the client */
.gs-paperdoll {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--gs-surface-well);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-sunken);
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-caption);
  letter-spacing: var(--gs-track-label);
  color: var(--gs-text-mute);
}
/* No cast shadow under the figure. The system has no soft shadows, and the flat unlit sprites
   (D15) never had one to begin with. */

/* --------------------------------------------------------------- TEXT UTIL */
.gs-title { font-family: var(--gs-face-display); font-weight: 700; letter-spacing: var(--gs-track-label); text-transform: uppercase; color: var(--gs-gold-head); text-shadow: var(--gs-text-stamp); }
.gs-mono { font-family: var(--gs-face-mono); font-size: var(--gs-size-mono); color: var(--gs-text-mute); }
.gs-caption { font-family: var(--gs-face-display); font-size: var(--gs-size-caption); letter-spacing: var(--gs-track-label); color: var(--gs-gold-label); text-transform: uppercase; }
.gs-dim { color: var(--gs-text-mute); }
/* Tabular figures, asked for by name: the sans has proportional ones, so a column of numbers
   only lines up because of this property. It used to be a courtesy, under a monospace. */
.gs-num { font-family: var(--gs-face-display); font-variant-numeric: tabular-nums; }
.gs-gain { color: var(--gs-gain-ink); }
.gs-loss { color: var(--gs-loss); }
/* Any text sitting directly on the game world carries this. Text inside a
   panel does not. Single most reliable readability fix in the system. */
.gs-on-world { text-shadow: var(--gs-world-shadow); }
.gs-icon.gs-on-world { filter: var(--gs-world-glow); }
/* --gs-text-mute is tuned for the inside of a stone panel and disappears over sunlit sand, so
   a mono caption printed on the world takes the brass. */
.gs-mono.gs-on-world { color: var(--gs-gold-head); }

/* ====================================================== WORLD OVERLAY (HUD)
   Objects that are printed straight onto the game world rather than into a
   surface: floating name plates, the player-side magic circle, damage
   numbers, chat and system lines, the centre banner, the menu-tile lamp and
   the pet mood face. None of them is enclosed in a frame, which is why none of them uses .gs-hud.
   They all carry the world text shadow; player and NPC names strengthen it because those are the
   two labels read constantly while moving through a crowded town.

   These are the one place the design system's surface rules do not reach: there is no face
   behind them to bevel and no contrast floor to measure against, because the background is
   whatever the world happens to be painting. Colour comes from the system's five status hues,
   but the halo, the circle and the damage rise stay - legibility and game feedback beat the
   chrome language out here. */

/* The second answer to on-world legibility, for text the halo alone cannot carry: a flat dark
   plate under the glyphs. Any on-world text may take it - a nameplate, a tracker line, a
   caption. Its `text-shadow: none` has to beat the halo a nameplate's own row sets, and the
   nameplate is dressed in hud.css now, which loads after this sheet: a plate that wants the blob
   asks for it there. */
.gs-on-world--blob {
  display: inline-block;
  padding: 1px 6px 2px;
  border-radius: 0;
  background: var(--gs-scrim);
  text-shadow: none;
}

/* the cyan circle under every player-side body - side marker, not a target. World VFX, not
   chrome: it is drawn on the ground plane and keeps its glow and its turn. */
.gs-magic-circle {
  width: 120px;
  height: 44px;
  border-radius: 50%;
  background:
    radial-gradient(ellipse at 50% 50%, rgba(120, 224, 255, 0.22), rgba(0, 0, 0, 0) 68%);
  box-shadow:
    inset 0 0 0 1px rgba(126, 226, 255, 0.55),
    inset 0 0 0 9px rgba(126, 226, 255, 0.10),
    inset 0 0 0 10px rgba(126, 226, 255, 0.40),
    0 0 14px rgba(96, 208, 255, 0.35);
}

/* floating damage - peels off a struck body, rises and fades.
   The one text in the game that is NOT set in the display serif. Cinzel is a Roman inscriptional
   face: beautiful over a panel title, and at speed over a body it reads as thin and decorative
   where a hit wants to read as a hit. This is the arcade convention instead - the condensed
   grotesque the UI already ships, at its heaviest weight, cut out of the scene with a hard
   outline on all eight sides rather than a soft glow. No blur: a blur is what made the old one
   look painted onto the backdrop instead of standing off it. */
.gs-damage {
  font-family: var(--gs-face-ui);
  font-size: var(--gs-size-combat);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--ember-400);
  text-shadow: var(--gs-world-shadow);
}
.gs-damage--crit { color: var(--brass-300); }
.gs-damage--heal { color: var(--moss-400); }
.gs-damage--zero { color: var(--stone-200); font-size: 21px; }
/* The word over a number - "Critical" over a critical. Small, in the number's own colour, and
   centred over it: the number is still the thing read, this only names what kind it was. */
.gs-damage__label {
  display: block;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  margin-bottom: 2px;
}

/* the circle turns - a slow sweep of light around the ring, so a body standing still on its
   slot still reads as deployed. Motion only; the ring itself is unchanged. */
@keyframes gs-circle-spin { to { transform: rotate(360deg); } }
.gs-magic-circle--spin {
  background-image:
    conic-gradient(from 0deg, rgba(126, 226, 255, 0) 0deg, rgba(126, 226, 255, 0.35) 60deg, rgba(126, 226, 255, 0) 140deg, rgba(126, 226, 255, 0) 360deg),
    radial-gradient(ellipse at 50% 50%, rgba(120, 224, 255, 0.22), rgba(0, 0, 0, 0) 68%);
  animation: gs-circle-spin 6s linear infinite;
}

/* a struck number rises off the body and fades. The only fade in the game: everything else the
   player reads stays put. */
@keyframes gs-damage-rise {
  from { transform: translate(-50%, 0); opacity: 1; }
  to   { transform: translate(-50%, -52px); opacity: 0; }
}
.gs-damage--float { animation: gs-damage-rise 1.1s ease-out forwards; }

/* ---------------------------------------------------------- CRITICAL BURST */
/* The star of light a critical fires out of the body it lands on. Flat by design - it is a
   backdrop for a number, and a number is drawn in the DOM, so this is too (see critBurst in
   src/client/combat.ts for why that decision is not an accident).

   It is built from the crit's own ink and nothing new: the core goes white -> #FFE9B0 -> amber,
   which is the gold `.gs-damage--crit` already uses, so the burst and the number read as one
   event rather than two effects that happened to land together.

   Two things carry the geometry and both come off the element as custom properties, because the
   client sizes a burst by the BODY it belongs to: `--r` is the radius in screen pixels, `--spin`
   the whole star's rotation. Each spike then supplies its own `--a` angle and `--l` length so a
   burst is one rule, twelve elements, and no per-spike CSS.

   Additive by hand: `screen` blending is what makes overlapping spikes brighten where they cross
   instead of stacking flat, which is the whole look of the arcade original. */
.gs-crit-burst {
  --r: 44px;
  --spin: 0deg;
  width: 0;
  height: 0;
  mix-blend-mode: screen;
}
/* The hot middle. Longest-lived piece of the burst, and the one whose animationend disposes the
   whole element - so its duration is the burst's lifetime. */
.gs-crit-burst__core {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 0.62);
  height: calc(var(--r) * 0.62);
  border-radius: 50%;
  /* Tight falloff on purpose. The first version spent 1.55 radii on the glow and the burst read as
     a lamp switching on: the spikes were still there, drowned. The core is the HOT POINT the
     spikes come out of, so it stops well inside their length. */
  background: radial-gradient(circle,
    #FFFFFF 0 9%,
    #FFEFC4 20%,
    rgba(255, 196, 92, 0.55) 34%,
    rgba(255, 138, 48, 0.22) 52%,
    rgba(255, 120, 40, 0) 70%);
  animation: gs-crit-core 340ms cubic-bezier(0.1, 0.72, 0.3, 1) forwards;
}
/* One spike. A dart rather than a triangle: widest a little under halfway out, a sharp tip, and
   nearly a point at the centre - a plain isoceles triangle reads as a cone of light, and twelve
   cones read as a sunburst logo. */
.gs-crit-burst__spike,
.gs-crit-burst i {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * var(--l, 1));
  height: calc(var(--r) * 0.065);
  margin-top: calc(var(--r) * -0.0325);
  transform-origin: 0 50%;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.55) 0, #FFFFFF 22%, #FFEEC8 58%, rgba(255, 208, 120, 0) 100%);
  clip-path: polygon(0 46%, 34% 0, 100% 50%, 34% 100%);
  animation: gs-crit-spike 300ms cubic-bezier(0.08, 0.78, 0.28, 1) forwards;
}
@keyframes gs-crit-core {
  from { transform: translate(-50%, -50%) scale(0.3); opacity: 1; }
  40%  { opacity: 0.85; }
  to   { transform: translate(-50%, -50%) scale(1.15); opacity: 0; }
}
/* The rotation is restated in every frame because a keyframe replaces the whole transform: drop it
   from one of them and the spike snaps back to angle 0 halfway out. */
@keyframes gs-crit-spike {
  from { transform: rotate(calc(var(--spin) + var(--a))) scaleX(0.12); opacity: 0.85; }
  18%  { opacity: 1; }
  to   { transform: rotate(calc(var(--spin) + var(--a))) scaleX(1); opacity: 0; }
}
/* THE SOUL ORB. What a caught creature leaves behind: it is held, not killed, so the field must
   not show a corpse - the body is taken off and this stands in its place for a beat.

   Violet on purpose. Every other effect in the fight is warm - gold crits, orange numbers, red
   damage - so a cold colour is the one thing on the battlefield that cannot be mistaken for a
   blow landing. Same 0x0-point construction as the crit burst: the element centres nothing, its
   two children do, and `--r` is the orb's radius, set from the creature's own silhouette so a
   crab and a wyvern each leave an orb their own size.

   `screen`, again by hand, so the orb brightens the ground under it rather than sitting on it as
   a flat sticker. */
.gs-soul-orb {
  --r: 26px;
  width: 0;
  height: 0;
  mix-blend-mode: screen;
}
/* The orb itself: rises a little as it fades, which is what makes it read as "taken up" rather
   than "dropped". Longest-lived piece, so its end disposes the whole element. */
.gs-soul-orb__core {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  background: radial-gradient(circle,
    #FFFFFF 0 12%,
    #E6CCFF 26%,
    rgba(168, 92, 255, 0.72) 46%,
    rgba(120, 48, 220, 0.30) 68%,
    rgba(96, 32, 190, 0) 82%);
  animation: gs-soul-orb-core 620ms cubic-bezier(0.16, 0.72, 0.3, 1) forwards;
}
/* One shockwave outward, so the moment has an edge. Thin, and gone well before the orb is. */
.gs-soul-orb__ring {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  border: 2px solid rgba(198, 150, 255, 0.85);
  animation: gs-soul-orb-ring 420ms cubic-bezier(0.1, 0.7, 0.3, 1) forwards;
}
@keyframes gs-soul-orb-core {
  from { transform: translate(-50%, -50%) scale(0.25); opacity: 0; }
  22%  { transform: translate(-50%, -50%) scale(1.05); opacity: 1; }
  55%  { transform: translate(-50%, -62%) scale(0.92); opacity: 0.95; }
  to   { transform: translate(-50%, -120%) scale(0.55); opacity: 0; }
}
@keyframes gs-soul-orb-ring {
  from { transform: translate(-50%, -50%) scale(0.2); opacity: 0.9; }
  to   { transform: translate(-50%, -50%) scale(1.9); opacity: 0; }
}
/* Reduced motion keeps the FACT and drops the flourish: the creature still leaves the field, and
   the orb is a still point rather than a rise. */
@media (prefers-reduced-motion: reduce) {
  .gs-soul-orb__ring { display: none; }
  .gs-soul-orb__core { animation: gs-soul-orb-still 500ms linear forwards; }
  @keyframes gs-soul-orb-still {
    from { transform: translate(-50%, -50%) scale(1); opacity: 1; }
    to   { transform: translate(-50%, -50%) scale(1); opacity: 0; }
  }
}

/* Stillness was asked for, and a critical still says so twice without this: the number turns gold
   and the body flinches. Decoration is what goes, not information. */
@media (prefers-reduced-motion: reduce) {
  .gs-crit-burst { display: none; }
}

/* chat and system lines - printed on the world, coloured by channel. Channel colour carries
   meaning the text never restates, so these six are a fixed map like the status hues. They sit
   over the world or in a near-black well, which is why they take the saturated 400 stops
   rather than the 300s tuned for a panel face. */
.gs-chat-line {
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-dense);
  color: var(--stone-50);
  text-shadow: var(--gs-world-shadow);
}
/* An item, a worn piece or a pet somebody linked into a line. The colour is the thing's own
   quality band - the same ink the tooltip's name and the slot's rim use - because the band IS what
   is being shown off: a purple weapon reads as purple in the sentence, not as chat-coloured text
   that happens to open a purple card.

   Underlined on hover only. A run of always-underlined links turns a busy world channel into a
   page of hyperlinks, and the colour already says these words are not prose. */
.gs-chat-link {
  cursor: var(--cur-hand, pointer);
  font-weight: 700;
  text-shadow: var(--gs-world-shadow);
}
.gs-chat-link:hover { text-decoration: underline; }
/* A pet link carries its head icon: a 32px atlas cell shown at half size in a 16px window.
   SCALED, not resized - the cell's `background-position` is written in atlas pixels by
   panel.sprite, and shrinking the box instead would move the window off the glyph. The wrapper is
   what the line box measures, so a link never makes its row taller. */
.gs-chat-link .cl-ico {
  display: inline-block;
  width: 16px;
  height: 16px;
  margin-right: 3px;
  overflow: hidden;
  vertical-align: -3px;
  text-decoration: none;
}
.gs-chat-link .cl-ico .gs-ico { transform: scale(0.5); transform-origin: 0 0; }
/* The growth rows in a linked pet's card: the sheet's own two columns, at tooltip width. */
.cl-pet .gs-stat { gap: 6px; }
.cl-pet .ps-stars { font-size: 8px; letter-spacing: -1px; }
.cl-pet .ps-gnum { min-width: 34px; text-align: right; }
.gs-chat-link--white  { color: var(--gs-q-text-white); }
.gs-chat-link--blue   { color: var(--gs-q-text-blue); }
.gs-chat-link--green  { color: var(--gs-q-text-green); }
.gs-chat-link--yellow { color: var(--gs-q-text-yellow); }
.gs-chat-link--gold   { color: var(--gs-q-text-gold); }
.gs-chat-link--purple { color: var(--gs-q-text-purple); }

.gs-chat-line--world   { color: var(--amethyst-300); }
.gs-chat-line--team    { color: var(--azure-400); }
.gs-chat-line--guild   { color: var(--moss-400); }
.gs-chat-line--nearby  { color: var(--stone-50); }
.gs-chat-line--private { color: var(--stone-200); }
.gs-chat-line--system  { color: var(--brass-300); }
.gs-chat-line--loot    { color: var(--moss-400); }
.gs-chat-line--alert   { color: var(--ember-400); }
.gs-chat-line--fade    { opacity: 0.45; }
.gs-chat-line em { font-style: normal; color: var(--brass-300); }

/* centre banner - one high-priority announcement at a time */
.gs-banner {
  font-family: var(--gs-face-display);
  font-size: var(--gs-size-combat);
  font-weight: 700;
  letter-spacing: var(--gs-track-label);
  text-transform: uppercase;
  color: var(--gs-gold-bright);
  text-shadow: var(--gs-world-shadow);
}
/* the fight result: the banner parked over the middle of the field, gone on its own. */
.gs-banner--result {
  position: absolute;
  left: 50%;
  top: 34%;
  transform: translate(-50%, -50%);
  text-align: center;
  pointer-events: none;
}
.gs-banner--win { color: var(--gs-gold-bright); }
.gs-banner--loss { color: var(--ember-300); }
.gs-banner--flee { color: var(--gs-text-mute); }
.gs-banner__gain {
  font-family: var(--gs-face-mono);
  font-size: var(--gs-size-dense);
  letter-spacing: 0;
  text-transform: none;
  margin-top: var(--gs-gap-sm);
  color: var(--gs-text);
}

/* A DROP line on the result banner: the item's own tile, then its name in its own quality, then
   the count. Exp and Crowns stay plain text on the same grid - they are numbers, not things - so
   only a row that names an item grows an icon, and the row keeps the gain line's mono face and
   spacing so the block still reads as one list under Victory.

   The cell is SCALED, not resized: background-position is written in atlas pixels by
   panel.sprite, so shrinking the 32px box would move the window off the glyph. Same trick a chat
   link uses (.gs-chat-link .cl-ico), one size up - a drop is being read at rest here, not inside
   a line of chat. The wrapper is what the line box measures, so the row never grows taller than
   the icon. */
.gs-banner__gain--drop {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.cb-drop__ico {
  display: inline-block;
  flex: none;
  width: 24px;
  height: 24px;
  overflow: hidden;
}
.cb-drop__ico .gs-ico { transform: scale(0.75); transform-origin: 0 0; }

/* ---------------------------------------------------------- POTION POPUP */
/* A potion poured in a fight: the item's icon at 24px and what it gave, one line per effect -
   health in the heal green, mana in the mana blue - standing over the body's health bar. It fades
   in and out where it is rather than rising like a blow's number (combat.ts potionPop). The
   anchor is the plate's foot, so the popup is lifted past the plate by its own height and 34px. */
@keyframes cb-potion-fade {
  0%   { opacity: 0; }
  18%  { opacity: 1; }
  72%  { opacity: 1; }
  100% { opacity: 0; }
}
.cb-potion {
  display: grid;
  gap: 2px;
  justify-items: center;
  transform: translate(-50%, calc(-100% - 34px));
  animation: cb-potion-fade 1.6s ease-in-out forwards;
}
.cb-potion__line {
  display: flex;
  align-items: center;
  gap: 4px;
  font-family: var(--gs-face-ui);
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  text-shadow: var(--gs-world-shadow);
}
.cb-potion__line--hp { color: var(--moss-400); }
.cb-potion__line--mp { color: var(--gs-q-text-blue); }
.cb-potion__ico {
  display: inline-block;
  flex: none;
  width: 24px;
  height: 24px;
  overflow: hidden;
}
.cb-potion__ico .gs-ico { transform: scale(0.75); transform-origin: 0 0; }
/* The six TEXT stops, the same ones a row, a hover card and a broadcast use. */
.cb-drop__name--white  { color: var(--gs-q-text-white); }
.cb-drop__name--green  { color: var(--gs-q-text-green); }
.cb-drop__name--blue   { color: var(--gs-q-text-blue); }
.cb-drop__name--yellow { color: var(--gs-q-text-yellow); }
.cb-drop__name--gold   { color: var(--gs-q-text-gold); }
.cb-drop__name--purple { color: var(--gs-q-text-purple); }

/* play-time lamp on the menu strip: green, amber or red by hours online. Square, flat,
   one hard ring - a pixel status mark, not a bulb. */
.gs-lamp {
  width: 8px;
  height: 8px;
  border-radius: 0;
  background: var(--gs-gain);
  box-shadow: 0 0 0 1px var(--gs-border-hard);
}
.gs-lamp--amber { background: var(--rust-400); box-shadow: 0 0 0 1px var(--gs-border-hard); }
.gs-lamp--red   { background: var(--gs-alert); box-shadow: 0 0 0 1px var(--gs-border-hard); }

/* One glyph, painted not embedded: the svg is a mask so the shape takes the brass of whatever
   it sits in (one accent). In the game --gs-icon points at a file under /data/art/ui/. Sizes are
   16 (buttons, rows) and 20 (slots); 12 is for a title bar. */
.gs-icon {
  display: inline-block;
  flex: none;
  width: var(--gs-icon-size, 16px);
  height: var(--gs-icon-size, 16px);
  background: currentColor;
  -webkit-mask: var(--gs-icon) center / contain no-repeat;
  mask: var(--gs-icon) center / contain no-repeat;
}
/* The house icon family, painted rather than masked: these files carry their own brass line and
   one accent fill, so a mask would throw the colour away and hand back a flat gold blob. Only the
   drawn family opts in - anything that must stay quiet keeps the mask and --mono below. */
.gs-icon--colour {
  background: var(--gs-icon) center / contain no-repeat;
  -webkit-mask: none;
  mask: none;
}
.gs-icon--sm    { --gs-icon-size: 12px; }        /* sits on the line in a title bar */
.gs-icon--mono  { color: var(--gs-text-mute); }  /* reads as a caption, not as an emphasis */
/* Muted CONTENT, not a disabled control: it still has to clear the contrast floor, so it is a
   token and never an opacity trick. */
.gs-icon--ghost { color: var(--gs-text-mute); }

/* pet mood face - closes the right end of the pet frame. A square brass chip. */
.gs-mood {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--brass-500);
  border: var(--gs-border-width) solid var(--gs-border-hard);
  box-shadow: var(--gs-raised-thin);
  font-size: var(--gs-size-caption);
  line-height: 1;
  color: var(--gs-text-on-brass);
}

/* two named button sizes. Before these, panels reached for an inline padding
   override whenever a button had to sit inside a row - five different sizes
   across the set. A button is one of three sizes and nothing else.
   --sm lines up with a list row the way the default 28px lines up with an input. */
.gs-btn--sm { height: 20px; padding: 0 var(--gs-gap-md); font-size: var(--gs-size-caption); }
.gs-btn--step {
  padding: 0;
  width: 18px;
  height: 16px;
  gap: 0;
  font-size: var(--gs-size-bar);
  line-height: 1;
  letter-spacing: 0;
}

/* ===========================================================================
   GLOBAL BASE - what gs.css cannot reach from .gs
   The token block lives on .gs, which <html> is above, so a handful of values are spelled
   out here. This also carries the rules the browser draws for itself: the focus ring, text
   selection and the scrollbar furniture.

   The .p-* specimen-sheet chrome that used to sit here was removed with the restyle: those
   classes framed the exhibit pages under the old ui/ tree and no shipped page has referenced
   them since (grep across src/client and public/*.html returns nothing). Dead chrome is not
   worth reskinning.
   =========================================================================== */

/* --stone-700, the app field, spelled out. */
html, body { margin: 0; padding: 0; background: #2c2c2f; }
/* Everything the browser draws for itself and gs.css cannot reach - scrollbars, the focus ring,
   the form controls we have not replaced. Without it a Windows machine set to the light system
   theme paints white furniture into a black game. This is the whole game, so it goes on html. */
html { color-scheme: dark; }

/* Icons and item art stay pixel-crisp even though the type no longer is: monospacing and caps
   carry the retro reading, so text is antialiased and stays sharp at any zoom. */
.gs img, .gs svg { image-rendering: pixelated; }

/* Focus. The browser default outline paints OUTSIDE the element, which reads as a highlight
   spilling past the bevel. A negative offset draws the ring on the element's INNER edge
   instead - the system's focus treatment - and it composes with the inline box-shadow bevels
   instead of fighting them. Set once, here. No component re-implements it, and it is never an
   outer glow. */
.gs :focus { outline: none; }
.gs :focus-visible {
  outline: var(--gs-border-width) solid var(--gs-focus-ring);
  outline-offset: calc(-1 * var(--gs-border-width));
}

.gs ::selection { background: var(--brass-600); color: var(--stone-50); }

/* Scrollbar chrome for anything that is not a .gs-scroll: same 12px metric, same bevels. */
.gs ::-webkit-scrollbar { width: var(--gs-size-scrollbar); height: var(--gs-size-scrollbar); }
.gs ::-webkit-scrollbar-track { background: var(--gs-surface-well); box-shadow: var(--gs-sunken-thin); }
.gs ::-webkit-scrollbar-thumb { background: var(--gs-surface-panel); box-shadow: var(--gs-raised-thin); border: var(--gs-border-width) solid var(--gs-border-hard); }
.gs ::-webkit-scrollbar-thumb:hover { background: var(--gs-surface-raised); }

/* Links. Brass, dotted underline, solid on hover - no colour-only affordance. */
.gs a { color: var(--gs-gold-head); text-decoration: none; border-bottom: 1px dotted currentColor; }
.gs a:hover { color: var(--gs-text); border-bottom-style: solid; }

/* --------------------------------------------- ICONS
   Every icon in the game is one cell of the atlas `bun run arts-pipeline` compiles from
   arts/icons/. The sheet URL is content-hashed, so panel.ts sets --gs-icons at boot; the
   cell size is a pipeline constant and lives here, never in a per-frame style. */
.gs-ico {
  display: block;
  width: 32px;
  height: 32px;
  background-image: var(--gs-icons);
  background-size: var(--gs-icons-size); /* the sheet is 2x; drawn at its 1x size so the 32px cell holds */
}
