/*
  Grand Slam Bridge — the shared theme (2026-09-18).

  The colours, type, spacing and component styles every page adopts. home.html is the first page on
  it (HANDOVER.md 52.33); find-table, history, the Hand Editor and the Hand Viewer follow, and
  table.html's chrome last, with the playing surface left alone.

  The look comes from the home-page mockup, which was the reference, not a served file (it is in git history).

  Written as the shell other pages will adopt, so nothing here is about the home page in
  particular: home.html's own layout lives in home.html. Add to this file only what a second page
  would want — if a rule would be wrong anywhere else, it belongs on the page.

  Loaded as <link rel="stylesheet" href="/gsb-theme.css?v=…">, versioned like the modules and
  checked by scripts/check-module-versions.js.

  The two typefaces are served from this repository (public/assets/fonts), not from Google: one
  fewer third party on every page load, nothing to fail at someone else's end, and no request
  telling another company who is reading. Both are variable fonts, so one file covers every weight.
*/

@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/fraunces-variable.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Manrope";
  src: url("/assets/fonts/manrope-variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* ==========================================================
   FIXED — NOT PART OF ANY THEME (2026-09-28)

   These two are outside the themeable block on purpose, so that no theme, present or future, can
   reach them.

     the card face   a playing card is white with black and red pips in every theme, and the
                     2/4-colour switch is the user's, not the brand's
     the suit font   CLAUDE.md section 4 forces TEXT-presentation suit glyphs. Fold this into
                     --gsb-ui and a brand font brings the colourful emoji glyphs back.
   ========================================================== */
:root {
  --gsb-card-face: #FDFDFB;
  --gsb-suit-font: "Arial Unicode MS", "DejaVu Sans", Arial, "Helvetica Neue", sans-serif;
}

/* ==========================================================
   THE DEFAULT THEME

   A second theme is one `:root[data-theme="x"]` block overriding these names and nothing else.
   Everything in this file below the tokens resolves through them — which it did NOT before
   2026-09-28: 104 colour literals were written out in the components themselves, so a theme that
   overrode :root would have restyled every page and left this file's own buttons, dialogs,
   shadows and seats on the old palette.

   WHY SOME TOKENS ARE AN RGB TRIPLE. Twenty of those literals were `rgba(201, 162, 75, .08)` and
   friends — the gold at ten different alphas. A solid --gsb-gold cannot reach them, so the
   channels are a token too and the tints are `rgb(var(--gsb-gold-rgb) / .08)`. Change the gold
   once and every tint follows.
   ========================================================== */
:root, :root[data-theme="default"] {
  /* Ink and panels, darkest first. */
  --gsb-ink: #0E0A07;
  --gsb-panel: #171008;
  --gsb-panel-2: #1E160D;
  --gsb-raise: #251B10;
  --gsb-well: #120C06;

  /* Lines: the quiet one for most edges, the strong one to lift something forward. */
  --gsb-line: rgba(201, 162, 75, .16);
  --gsb-line-strong: rgba(201, 162, 75, .34);

  /*
    TEXT THAT SITS ON SOMETHING ALWAYS DARK (2026-09-29) - a photograph, a dark overlay box, the
    lesson stage. It is NOT --gsb-cream, and the difference only became visible with a light
    theme: --gsb-cream is the READING colour, which on Clubhouse is the navy #14264B, and the
    learn hero put that navy on a night-time garden photograph. Unreadable, and no amount of
    text-shadow rescues it.

    The surface these sit on is dark by its nature rather than by the theme's choice, so the text
    on it has to be light whatever the page is. It is still a token rather than a fixed value,
    because each theme should pick its own light - and here the defaults are var() of the two
    roles these rules used before, so the three dark themes are unchanged to the byte and only a
    theme that makes --gsb-cream dark has to say anything.
  */
  --gsb-on-image: var(--gsb-cream);
  --gsb-on-image-muted: var(--gsb-muted);

  /* Gold carries emphasis; cream is the reading colour; muted is everything secondary. */
  --gsb-gold: #C9A24B;
  --gsb-gold-bright: #E6CE92;
  --gsb-cream: #EFE5D2;
  --gsb-muted: #9C8A6E;
  --gsb-green: #5FA463;
  --gsb-alert: #C2452F;

  /*
    THE WORDMARK'S TWO WORDS (2026-09-29).

    The shell draws the name in two pieces — `.gsb-brand h1` is the lead and the `<span>` inside
    it the trail — and until now both were simply gold, so no token was needed. The Bridge Aces
    themes colour them differently from each other ("Bridge" in the text colour, "Aces" in the
    theme's second colour), which a shared --gsb-gold cannot express.

    The defaults are var() of the two golds the rules used before, so the default theme's
    wordmark is unchanged to the byte and a theme that only sets --gsb-gold still carries its
    wordmark along.
  */
  --gsb-wordmark-lead: var(--gsb-gold-bright);
  --gsb-wordmark-trail: var(--gsb-gold);

  /*
    THE LIVE TABLE'S OWN CHROME (part B, 2026-09-29).

    These six were table.html's private --text, --ivory, --muted, --gold, --line and --accent.
    They are NOT the shared cream, muted, gold and line under other names: the table's greys are
    cooler and its gold warmer, because that chrome sits on green felt rather than on the page's
    ink. --muted is 117 away from --gsb-muted; --gold is 40 away.

    SO THEY ARE THEIR OWN ROLES RATHER THAN OVERRIDES. Pointing table.html's --gsb-muted at
    #b6c4d1 would have recoloured every shared component on that page - 23 uses in the page and
    46 more inside this file's own buttons, dialogs and rails - which is the opposite of what a
    tokenising branch may do. Recorded in the handover as a real divergence for the site-wide
    review to settle, not forced here.

    A theme restyles the table's chrome by overriding these six, separately from the site's.
  */
  --gsb-table-text: #f7fbff;      /* the body's white, cooler than cream */
  --gsb-table-heading: #f3ead8;   /* headings, values, the board number */
  --gsb-table-muted: #b6c4d1;     /* secondary text on the felt */
  --gsb-table-gold: #d5b06f;
  --gsb-table-line: #f4e0b51f;
  --gsb-table-accent: #7bb8ff;    /* your own line in the chat */

  /*
    AND THE SAME CHROME WHERE IT SITS ON A PANEL RATHER THAN ON THE FELT (2026-09-29).

    The six roles above are for text on GREEN FELT, so they are light. The side panels - the
    auction, history, friends, chat, the score box - are var(--gsb-panel), which follows the
    theme and is WHITE under Clubhouse. Text in them was still taking the felt's light colours,
    either inherited from `body{color:var(--gsb-table-text)}` or from literals, so the auction's
    W/N/E/S headings and every call in it were pale gold and near-white on white.

    The felt is dark in every theme and always will be - it is a bridge table - so the felt roles
    stay light and these are separate. THE DEFAULTS ARE EXACTLY WHAT EACH REPLACED, two as var()
    of the felt role and two as the literal the rule carried, so the three dark themes do not
    move.
  */
  --gsb-table-panel-text: var(--gsb-table-text);
  --gsb-table-panel-muted: var(--gsb-table-muted);
  --gsb-table-panel-heading: #f1e5c7;   /* the !important heading inside the right sidebar */
  --gsb-table-panel-gold: #e6cf91;      /* the auction's column headings */
  /* The scorecard's two IMP columns. Defaults are the values the old match-break table used. */
  --gsb-scorecard-ours: #65dd8c;
  --gsb-scorecard-theirs: #ff858a;
  /*
    THE PLATE BEHIND A MENU ITEM ON table.html. That page paints every bare button -
    `input,button{background:linear-gradient(#13243a,#0d1c2f)}` - and the burger's items are bare
    buttons, so each one sits on a navy plate inside a themed menu. It has always looked wrong
    and has always READ, because the text was cream. The default here is that same gradient, so
    nothing moves on the dark themes.
  */
  --gsb-table-menu-item: linear-gradient(180deg, #13243a, #0d1c2f);

  /*
    THE SCORECARD ON A DARK PLATE (2026-09-29).

    The half-time and end-of-match panel is .match-break: a near-black plate laid over the felt,
    dark in every theme because that is what it is made of. The card inside it takes the
    --gsb-table-panel-* roles, which are for a panel that FOLLOWS the theme and is white under
    Clubhouse - so on Clubhouse the card's own text went navy on near-black.

    A separate set, because the two hosts genuinely differ: Find a Table and History show the
    same card in a light dialog and must keep the panel roles. The defaults here are var() of
    those very roles, so the three dark themes are unchanged to the byte and only Clubhouse says
    anything.
  */
  --gsb-plate-text: var(--gsb-table-panel-text);
  --gsb-plate-heading: var(--gsb-table-panel-heading);
  --gsb-plate-muted: var(--gsb-table-panel-muted);
  --gsb-plate-gold: var(--gsb-table-panel-gold);
  --gsb-plate-ours: var(--gsb-scorecard-ours);
  --gsb-plate-theirs: var(--gsb-scorecard-theirs);
  /* A seat pill in one of the card's diagrams, on that same dark plate. */
  --gsb-plate-seat: var(--gsb-panel-2);

  /*
    THE STRIP ABOVE THE TABLE - Leave Table, the room's name, "Created by". It was a hard-coded
    near-black gradient while its text followed the theme, so under Clubhouse the navy text sat
    on black. It is page chrome rather than table, so it follows the theme now; the default is
    the gradient it always had.
  */
  --gsb-table-strip-bg: linear-gradient(180deg, #160F08, #100A05);
  --gsb-table-strip-title: #d7bd83;
  --gsb-table-strip-muted: #91a4b0;

  --gsb-display: "Fraunces", Georgia, "Times New Roman", serif;
  --gsb-ui: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

  /* Behind initials, everywhere (see .gsb-avatar). */
  --gsb-avatar-bg: linear-gradient(140deg, #3A2C18, #6A5330);

  /*
    THE CHANNELS BEHIND THE TINTS. Gold and alert are used at many alphas; these let one change
    carry all of them. Kept next to the solid values they belong to.
  */
  /*
    SPACE-SEPARATED, AND THAT IS THE WHOLE POINT OF THESE FOUR LINES.

    They shipped comma-separated on 2026-09-28 and every tint built from them died silently.
    `rgb(var(--gsb-gold-rgb) / .08)` expanded to `rgb(201, 162, 75 / .08)` - legacy commas with a
    modern slash, which is not valid CSS. var() makes a declaration valid at PARSE time, so
    nothing was dropped and nothing was logged; it failed later, as invalid at computed-value
    time, and `background` fell back to its initial value. Twenty-five declarations across the
    shared theme quietly became transparent: every hover tint in the top nav, the sidebar, the
    user menu, the rails and the buttons.

    It is invisible in the source and obvious in the browser, which is why CLAUDE.md now requires
    these branches to be checked by computed value rather than by reading the diff.
  */
  /*
    VULNERABLE RED (2026-09-30), one value for the whole site.

    Bridge's own convention rather than a brand colour, which is why it is the same in all four
    themes: it already existed as a literal #c92d35 on the board widget's vulnerable seats, and
    the auction headers and the card glow now say it the same way instead of a third and fourth
    copy of the number.
  */
  --gsb-vul-red: #c92d35;
  --gsb-vul-red-rgb: 201 45 53;
  --gsb-gold-rgb: 201 162 75;
  --gsb-gold-edge-rgb: 224 190 110;
  --gsb-alert-rgb: 194 69 47;
  --gsb-green-rgb: 95 164 99;

  /* The gold button: its gradient, its hover, its edge, and the ink that goes ON it. */
  --gsb-gold-grad: linear-gradient(180deg, #D8B45C, #B98F35);
  --gsb-gold-grad-hover: linear-gradient(180deg, #E3C271, #C79A3B);
  --gsb-gold-edge: #E0BE6E;
  --gsb-gold-edge-hover: #E9CC86;
  --gsb-on-gold: #241804;

  /* Messages and states. */
  --gsb-danger-text: #F1B9B9;
  --gsb-msg-error: #E8B4A8;
  --gsb-msg-ok: #8FD3AB;
  --gsb-blocked-bg: #3A2020;
  --gsb-blocked-edge: #7D4A4A;
  --gsb-blocked-text: #F0D9D9;

  /* Quiet neutrals that were literals. */
  --gsb-dot-off: #4A4034;
  --gsb-placeholder: #7A6B55;
  --gsb-avatar-plate: #2A1F12;
  --gsb-avatar-plate-quiet: #241E17;
  --gsb-topbar-bg: linear-gradient(180deg, #1A130B, #120C06);
  --gsb-bubble-me: linear-gradient(180deg, #3A2C13, #2A1F0E);
  --gsb-fill-faint: rgba(255, 255, 255, .03);
  --gsb-fill-faint-2: rgba(255, 255, 255, .035);
  --gsb-fill-hairline: rgba(255, 255, 255, .14);
  --gsb-fill-warm: rgba(233, 213, 178, .045);
  --gsb-fill-sunken: rgba(0, 0, 0, .28);
  --gsb-fill-sunken-soft: rgba(0, 0, 0, .18);

  /* Scrim and shadows: a theme on a light ground wants its own, so they are roles, not one black. */
  --gsb-scrim: rgba(0, 0, 0, .62);
  --gsb-scrim-soft: rgba(18, 12, 6, .58);
  --gsb-shadow-dialog: 0 24px 70px rgba(0, 0, 0, .55);
  --gsb-shadow-menu: 0 18px 40px rgba(0, 0, 0, .55);
  --gsb-shadow-pop: 0 16px 40px rgba(0, 0, 0, .5);
  --gsb-shadow-inset: inset 0 1px 3px rgba(0, 0, 0, .34);
  --gsb-shadow-seg: 0 3px 10px rgba(0, 0, 0, .24);

  /*
    THE TABLE'S OWN SURFACES, themeable with today's values (2026-09-28). The felt and the wood
    are the bridge table itself rather than palette, but a light theme still has to be able to
    say what shade of table it wants — so they are tokens, and the defaults are exactly what
    every page draws today. The CARD FACES are not here; they are fixed, above.
  */
  --gsb-felt: #0b6d4f;
  /*
    The darker end of a felt gradient. Declared here rather than only in the page that uses it
    (the Hand Editor's table) for the reason --gsb-hero-signin is: a token that exists solely in
    one page's default-theme block is UNDEFINED under any other theme, and an undefined var() is
    invalid at computed-value time - the gradient would lose a stop rather than change colour.
    The default is the editor's own value, since it is the only consumer today; a new theme sets
    its own.
  */
  --gsb-felt-2: #0b412f;
  /*
    The middle stop of the three-stop felt. The live table and the Hand Editor draw the same
    gradient - 17684b, 10543d, 0b412f - so this is the product's felt rather than one page's.
    Declared here for the reason --gsb-felt-2 is: a token living only in a page's default-theme
    block is undefined under every other theme, and the gradient would lose a stop.
  */
  --gsb-felt-mid: #10543d;
  --gsb-felt-edge: #0c2c24;
  --gsb-on-felt: #e7efe9;
  --gsb-wood-1: #6c4630;
  --gsb-wood-2: #3e261a;
  /*
    The hairline around the table's mahogany surround, and the colour that shows if the texture
    fails to load. Both were literals on the live table; they are tokens so a theme can say what
    its table is made of.
  */
  --gsb-wood-edge: rgba(63, 38, 25, .30);
  --gsb-wood-under: #4d2f20;

  /* A seat on the felt: taken by a person, or held by a robot. */
  --gsb-seat-taken: #5A2222;
  --gsb-seat-taken-edge: rgba(224, 115, 115, .5);
  --gsb-seat-robot: #20303a;
  --gsb-seat-robot-edge: rgba(140, 190, 215, .42);
  --gsb-seat-robot-text: #cfe6f2;
  /* A seat with a person in it: the robot's plate in the felt's own green rather than its blue. */
  --gsb-seat-human: #1d3a2b;
  --gsb-seat-human-edge: rgba(140, 215, 170, .42);
  --gsb-seat-human-text: #d8f0e2;

  /*
    SUIT SYMBOLS DRAWN ON SITE SURFACES — the auction, history, scorecards, popups — so a light
    theme can keep their contrast. THE CARD FACES DO NOT USE THESE: a card is a card, and the
    player's own 2/4-colour switch is what governs it.

    The defaults are table.html's, which is the surface most of them are read on. Three other
    pages carry slightly different values and keep them as local overrides for now; unifying them
    would change how those pages look, which this work is not allowed to do.
  */
  --gsb-suit-spade: #4b8dff;
  --gsb-suit-heart: #ff5d69;
  --gsb-suit-diamond: #d7ac3a;
  --gsb-suit-club: #34b36b;

  /*
    THE SIGN-IN HERO. index.html's hero is a painted panel rather than a photograph, so the
    gradient is the theme's to supply. It lives here rather than only in the page because a
    token that exists solely in one page's default-theme block would be UNDEFINED under any
    other theme - and an undefined var() is invalid at computed-value time, which would leave
    the hero with no background at all rather than a different one.
  */
  --gsb-hero-signin: linear-gradient(150deg, #12694d, #0c513d 54%, #083a2d 100%);

  /* PBN, hand strings and code. A third font role, beside display and ui. */
  --gsb-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --gsb-radius: 14px;
  --gsb-radius-lg: 16px;
  --gsb-radius-pill: 999px;

  --gsb-gap: 14px;
  --gsb-pad: 18px;
  --gsb-topbar-h: 64px;
  --gsb-side-w: 236px;
  --gsb-rail-w: 314px;

  /*
    THE FONT SIZE SETTING'S MULTIPLIER (2026-10-02, shifted up a step the same day). The three
    choices are 1.15 / 1.30 / 1.45 now, so this default is 1.15 - it is the fallback for a page
    that never loads gsb-font-size.js, and it has to agree with what that module calls "Normal"
    or such a page would be a step smaller than the setting's own default.

    IT MULTIPLIES NAMED TEXT AND NOTHING ELSE - see the block at the end of this file and the
    one in table.html. It is NOT a page zoom; the first version of this setting was, and the
    page zoom and everything it dragged behind it have been taken back out.
  */
  --gsb-text-scale: 1.15;
}

/* ==========================================================
   THE BRIDGE ACES THEMES (2026-09-29)

   Two player-selectable themes for the name we are trying. Each is one block of token
   overrides and nothing else — no component rule below this point knows they exist, which is
   the property the tokenising work was for.

   HOW A THEME BLOCK RESOLVES, and why every colour is written out. The default block's selector
   is `:root, :root[data-theme="default"]`, and its FIRST half matches whatever theme is on. So a
   token a theme does not declare keeps the default's warm brown rather than falling back to
   nothing. That is a safe failure but an ugly one — one forgotten token is a brown line across a
   navy panel — so these blocks declare every colour role, including the ones that happen to
   agree with the default.

   WHAT THEY DELIBERATELY DO NOT DECLARE: the fonts, the radii and the spacing, which are the
   site's shape rather than its colour; the pure-black shadows, which are right on any dark
   ground; and --gsb-card-face and --gsb-suit-font, which no theme can reach at all (they are in
   the fixed :root block at the top of this file).

   THE CHANNEL TRIPLES ARE SPACE-SEPARATED. See the note in the default block: commas here make
   every tint built from them transparent, silently, and the page still parses.
   ========================================================== */

/* ---------- Bridge Aces · Midnight — dark navy ---------- */
:root[data-theme="bridge-aces-midnight"] {
  /* Ink and panels, darkest first. */
  --gsb-ink: #0B1630;
  --gsb-panel: #13224A;
  --gsb-panel-2: #182B58;
  --gsb-raise: #1F3568;
  --gsb-well: #081029;

  /* Lines are the text colour at low alpha, as the brief specifies, not the gold. */
  --gsb-line: rgba(242, 239, 230, .12);
  --gsb-line-strong: rgba(242, 239, 230, .26);

  --gsb-gold: #D6A93A;
  --gsb-gold-bright: #EFCB74;
  --gsb-cream: #F2EFE6;
  --gsb-muted: #9AA6C0;
  --gsb-green: #4FBF72;
  --gsb-alert: #E0574A;

  /* "Bridge" in the text colour, "Aces" in the theme's second colour. */
  --gsb-wordmark-lead: #F2EFE6;
  --gsb-wordmark-trail: #4FBF72;

  /* The live table's chrome, which sits on felt rather than on the page's ink. */
  --gsb-table-text: #F4F8FF;
  --gsb-table-heading: #F2EFE6;
  --gsb-table-muted: #AEC2D6;
  --gsb-table-gold: #E0B75A;
  --gsb-table-line: rgba(242, 239, 230, .13);
  --gsb-table-accent: #9FB8E8;

  --gsb-avatar-bg: linear-gradient(140deg, #1B2E5E, #3C5796);

  --gsb-gold-rgb: 214 169 58;
  --gsb-gold-edge-rgb: 239 203 116;
  --gsb-alert-rgb: 224 87 74;
  --gsb-green-rgb: 79 191 114;

  --gsb-gold-grad: linear-gradient(180deg, #E2B74B, #C29629);
  --gsb-gold-grad-hover: linear-gradient(180deg, #EFC868, #D2A636);
  --gsb-gold-edge: #EFCB74;
  --gsb-gold-edge-hover: #F6DA96;
  --gsb-on-gold: #0B1630;

  --gsb-danger-text: #F3B6B0;
  --gsb-msg-error: #F0B3A8;
  --gsb-msg-ok: #8FE0AC;
  --gsb-blocked-bg: #3A2028;
  --gsb-blocked-edge: #7D4A56;
  --gsb-blocked-text: #F0D9DC;

  --gsb-dot-off: #3C4866;
  --gsb-placeholder: #6F7C99;
  --gsb-avatar-plate: #16254E;
  --gsb-avatar-plate-quiet: #142043;
  --gsb-topbar-bg: linear-gradient(180deg, #14234C, #0A1329);
  --gsb-bubble-me: linear-gradient(180deg, #1E3468, #16264C);
  --gsb-fill-faint: rgba(255, 255, 255, .03);
  --gsb-fill-faint-2: rgba(255, 255, 255, .035);
  --gsb-fill-hairline: rgba(255, 255, 255, .14);
  /* The default's is a cream tint; on navy it reads as haze, so it takes the gold instead. */
  --gsb-fill-warm: rgba(214, 169, 58, .06);
  --gsb-fill-sunken: rgba(0, 0, 0, .28);
  --gsb-fill-sunken-soft: rgba(0, 0, 0, .18);

  --gsb-scrim: rgba(0, 0, 0, .62);
  --gsb-scrim-soft: rgba(6, 12, 28, .58);

  /* The table: its own green whatever the page is, and a brass rim rather than mahogany. */
  --gsb-felt: #17683F;
  --gsb-felt-mid: #115433;
  --gsb-felt-2: #0B4027;
  --gsb-felt-edge: #0A2C1C;
  --gsb-on-felt: #E7EFE9;
  /*
    THE RIM IS BRASS RATHER THAN MAHOGANY, BUT DEEPER THAN THE BRIEF'S #D6A93A, and the reason is
    measured: the empty-seat name labels and the connection notice are drawn in white ON the
    surround. At #9A7A33 white scored 4.03 against WCAG AA's 4.5 and the notice 3.51; at #7A5E28
    they are 6.1 and 5.3. The gold the brief names is the rim's HIGHLIGHT, which is what
    --gsb-wood-edge and --gsb-gold carry.
  */
  --gsb-wood-1: #7A5E28;
  --gsb-wood-2: #4E3B16;
  --gsb-wood-edge: rgba(214, 169, 58, .32);
  --gsb-wood-under: #62491E;

  --gsb-seat-taken: #5A2230;
  --gsb-seat-taken-edge: rgba(224, 115, 125, .5);
  --gsb-seat-robot: #1D3350;
  --gsb-seat-robot-edge: rgba(159, 184, 232, .45);
  --gsb-seat-robot-text: #CFE0F6;
  --gsb-seat-human: #1B3A2C;
  --gsb-seat-human-edge: rgba(159, 232, 190, .45);
  --gsb-seat-human-text: #CFF6DF;

  /* Suits as drawn on SITE surfaces. The card faces are fixed and do not use these. */
  --gsb-suit-spade: #9FB8E8;
  --gsb-suit-heart: #FF6B75;
  --gsb-suit-diamond: #E0B44A;
  --gsb-suit-club: #4FBF72;

  --gsb-hero-signin: linear-gradient(150deg, #16295A, #101E42 54%, #0A142C 100%);
}

/* ---------- Bridge Aces · Baize — dark green ---------- */
:root[data-theme="bridge-aces-baize"] {
  --gsb-ink: #0C2418;
  --gsb-panel: #12321F;
  --gsb-panel-2: #163C26;
  --gsb-raise: #1B4A2E;
  --gsb-well: #081B11;

  --gsb-line: rgba(243, 236, 216, .12);
  --gsb-line-strong: rgba(243, 236, 216, .26);

  --gsb-gold: #D6A93A;
  --gsb-gold-bright: #EFCB74;
  --gsb-cream: #F3ECD8;
  --gsb-muted: #A7B89F;
  /*
    THE BRIEF'S "secondary" FOR THIS THEME IS THE GOLD, and that is the wordmark's second word
    below. --gsb-green is a different job: it is the status green — the online dot, the "ok"
    message, the connected state — and painting those gold would say nothing. So it stays green,
    tuned to this palette (it is the club suit's value), and the brand's second colour is carried
    by --gsb-wordmark-trail.
  */
  --gsb-green: #6FD08E;
  --gsb-alert: #DE5A45;

  --gsb-wordmark-lead: #F3ECD8;
  --gsb-wordmark-trail: #D6A93A;

  --gsb-table-text: #F7FBF3;
  --gsb-table-heading: #F3ECD8;
  --gsb-table-muted: #B6C9AE;
  --gsb-table-gold: #E0B75A;
  --gsb-table-line: rgba(243, 236, 216, .13);
  --gsb-table-accent: #C9D6F2;

  --gsb-avatar-bg: linear-gradient(140deg, #1C4A2E, #3E7A50);

  --gsb-gold-rgb: 214 169 58;
  --gsb-gold-edge-rgb: 239 203 116;
  --gsb-alert-rgb: 222 90 69;
  --gsb-green-rgb: 111 208 142;

  --gsb-gold-grad: linear-gradient(180deg, #E2B74B, #C29629);
  --gsb-gold-grad-hover: linear-gradient(180deg, #EFC868, #D2A636);
  --gsb-gold-edge: #EFCB74;
  --gsb-gold-edge-hover: #F6DA96;
  --gsb-on-gold: #0C2418;

  --gsb-danger-text: #F3BDB2;
  --gsb-msg-error: #F0BCAB;
  --gsb-msg-ok: #9FE0B4;
  --gsb-blocked-bg: #3A2320;
  --gsb-blocked-edge: #7D5049;
  --gsb-blocked-text: #F0DCD8;

  --gsb-dot-off: #47573F;
  --gsb-placeholder: #7C8C74;
  --gsb-avatar-plate: #173A24;
  --gsb-avatar-plate-quiet: #14301F;
  --gsb-topbar-bg: linear-gradient(180deg, #133422, #0A2014);
  --gsb-bubble-me: linear-gradient(180deg, #1F4E31, #163A24);
  --gsb-fill-faint: rgba(255, 255, 255, .03);
  --gsb-fill-faint-2: rgba(255, 255, 255, .035);
  --gsb-fill-hairline: rgba(255, 255, 255, .14);
  --gsb-fill-warm: rgba(214, 169, 58, .06);
  --gsb-fill-sunken: rgba(0, 0, 0, .28);
  --gsb-fill-sunken-soft: rgba(0, 0, 0, .18);

  --gsb-scrim: rgba(0, 0, 0, .62);
  --gsb-scrim-soft: rgba(8, 24, 15, .58);

  /*
    The felt is LIGHTER than the page here, which is the opposite of the other two themes and is
    the point of this one: the site is the room and the table is lit.
  */
  --gsb-felt: #1B6B3F;
  --gsb-felt-mid: #145433;
  --gsb-felt-2: #0D4027;
  --gsb-felt-edge: #0A2C1C;
  --gsb-on-felt: #E7EFE9;
  /*
    THE RIM IS BRASS RATHER THAN MAHOGANY, BUT DEEPER THAN THE BRIEF'S #D6A93A, and the reason is
    measured: the empty-seat name labels and the connection notice are drawn in white ON the
    surround. At #9A7A33 white scored 4.03 against WCAG AA's 4.5 and the notice 3.51; at #7A5E28
    they are 6.1 and 5.3. The gold the brief names is the rim's HIGHLIGHT, which is what
    --gsb-wood-edge and --gsb-gold carry.
  */
  --gsb-wood-1: #7A5E28;
  --gsb-wood-2: #4E3B16;
  --gsb-wood-edge: rgba(214, 169, 58, .32);
  --gsb-wood-under: #62491E;

  --gsb-seat-taken: #5A2A22;
  --gsb-seat-taken-edge: rgba(224, 130, 115, .5);
  --gsb-seat-robot: #1E3A44;
  --gsb-seat-robot-edge: rgba(201, 214, 242, .42);
  --gsb-seat-robot-text: #D6E6F2;
  --gsb-seat-human: #1D4432;
  --gsb-seat-human-edge: rgba(201, 242, 214, .42);
  --gsb-seat-human-text: #D6F2E2;

  --gsb-suit-spade: #C9D6F2;
  --gsb-suit-heart: #FF7A80;
  --gsb-suit-diamond: #E6BE55;
  --gsb-suit-club: #6FD08E;

  --gsb-hero-signin: linear-gradient(150deg, #17603C, #11492F 54%, #0B3322 100%);
}

/* ---------- Bridge Aces · Clubhouse — light ----------

   THE FIRST LIGHT THEME, and several roles INVERT here rather than merely changing hue. Each one
   is marked below, because the name no longer describes the job:

     --gsb-cream       is the TEXT colour, and on a light page that is the navy
     --gsb-ink         is the PAGE, and on a light page that is the near-white
     --gsb-gold-bright is emphasis TEXT (nav hover, the current page, values in the score box),
                       so here it must be DARKER than --gsb-gold, not brighter
     --gsb-gold-rgb    is the hover TINT, and gold at .08 over white is invisible — it carries
                       the navy instead, which is the only channel that shows at those alphas
     --gsb-scrim-soft  is the quiet button's SURFACE (.gsb-btn), not a backdrop
     the shadows       are navy at low alpha, not black: black shadows on a light ground read as
                       dirt rather than as depth

   The table keeps a dark felt, so the six --gsb-table-* roles stay LIGHT while the page's text
   goes dark. That split is the reason those roles exist.
   ---------------------------------------------------------- */
:root[data-theme="bridge-aces-clubhouse"] {
  /* Page and panels, LIGHTEST first — the reverse of the other three. */
  --gsb-ink: #F7F5EF;        /* the page */
  --gsb-panel: #FFFFFF;
  --gsb-panel-2: #F2F0E9;
  --gsb-raise: #E9E6DC;      /* the raised/hover surface */
  --gsb-well: #EFECE3;       /* the sunken one */

  --gsb-line: rgba(20, 38, 75, .14);
  --gsb-line-strong: rgba(20, 38, 75, .28);

  --gsb-gold: #C9962A;       /* the rim gold: borders, the brand pip, the focus ring */
  /*
    INVERTED: emphasis TEXT, so it is the DARKER of the two golds. #8A6516 read 4.42 against
    AA's 4.5 on --gsb-avatar-plate, eight times across eight pages; #7E5C13 reads 5.0 there and
    5.9 on white.
  */
  --gsb-gold-bright: #7E5C13;
  --gsb-cream: #14264B;      /* INVERTED: the reading colour */
  --gsb-muted: #5B6478;
  --gsb-green: #1E7A3E;
  --gsb-alert: #C8202F;

  --gsb-wordmark-lead: #14264B;
  --gsb-wordmark-trail: #1E7A3E;

  /*
    THE ONLY THEME THAT HAS TO SAY THIS. Everywhere else --gsb-on-image inherits var(--gsb-cream)
    and is already light; here the reading colour is navy, so the text on photographs and dark
    overlays takes the page's own near-white instead. Against the learn hero's #0C0805 that is
    18.1 where the navy was 1.34.
  */
  --gsb-on-image: #F7F5EF;
  --gsb-on-image-muted: #C9C4B6;

  /* The felt stays dark green, so every one of these stays light. */
  --gsb-table-text: #F6FBF6;
  --gsb-table-heading: #FFF6E2;
  --gsb-table-muted: #C7D8C6;
  --gsb-table-gold: #E8C877;
  --gsb-table-line: rgba(255, 255, 255, .16);
  --gsb-table-accent: #BBD6FF;

  /* On a panel, not on the felt: the only theme where the two have to disagree. */
  --gsb-table-panel-text: #14264B;
  --gsb-table-panel-muted: #5B6478;
  --gsb-table-panel-heading: #14264B;
  --gsb-table-panel-gold: #7E5C13;
  /* On white, the dark themes' pale green and pink are unreadable; these are 5.4 and 5.1. */
  --gsb-scorecard-ours: #16663A;
  --gsb-scorecard-theirs: #A32432;
  /*
    NO PLATE. The menu's text is the navy #14264B here, and on that navy gradient it read 1.0 -
    navy on navy, invisible. With the plate gone the items sit on the menu's own panel and read
    13.1, which is what every other theme's items have always done.
  */
  --gsb-table-menu-item: none;

  /*
    THE ONLY THEME THAT HAS TO SAY THIS. Everywhere else the plate roles inherit the panel roles
    and are already light. Here the panel roles are the page's dark navy, and the plate they
    would be painted on is #081813 - so the card takes the light values instead, which is the
    look the other three themes have always had.
  */
  --gsb-plate-text: #F7F5EF;
  --gsb-plate-heading: #FFF6E2;
  --gsb-plate-muted: #C9C4B6;
  --gsb-plate-gold: #E8C877;
  --gsb-plate-ours: #65dd8c;
  --gsb-plate-theirs: #ff858a;
  /*
    Dark, like the other three. --gsb-panel-2 is #F2F0E9 here, and a near-white pill on a
    near-black plate is a different design rather than the same one in a light theme.
  */
  --gsb-plate-seat: #17291F;

  --gsb-table-strip-bg: linear-gradient(180deg, #FFFFFF, #F2F0E9);
  --gsb-table-strip-title: #7E5C13;
  --gsb-table-strip-muted: #5B6478;

  --gsb-avatar-bg: linear-gradient(140deg, #D7DEEC, #9FB0CE);

  /*
    INVERTED: the tint channel is the navy. Every hover in the nav, the sidebar, the user menu
    and the rails is `rgb(var(--gsb-gold-rgb) / .08)` — gold at .08 over #FFFFFF is
    rgb(254,252,248), which nobody can see. Navy at .08 is rgb(236,238,241), which reads as a
    hover. The alphas are in the component rules and are not a theme's to change, so the channel
    is the only lever.
  */
  --gsb-gold-rgb: 20 38 75;
  --gsb-gold-edge-rgb: 20 38 75;
  --gsb-alert-rgb: 200 32 47;
  --gsb-green-rgb: 30 122 62;

  /* The primary button is the navy, with white on it. */
  --gsb-gold-grad: linear-gradient(180deg, #1B2F5A, #14264B);
  --gsb-gold-grad-hover: linear-gradient(180deg, #24406F, #1A2F5C);
  --gsb-gold-edge: #14264B;
  --gsb-gold-edge-hover: #24406F;
  --gsb-on-gold: #FFFFFF;

  /* Messages: on white these have to be dark enough to read, not pale tints. */
  --gsb-danger-text: #9E1B27;
  --gsb-msg-error: #A32432;
  --gsb-msg-ok: #16663A;
  --gsb-blocked-bg: #FBE9E9;
  --gsb-blocked-edge: #D9A8A8;
  --gsb-blocked-text: #8E1D28;

  --gsb-dot-off: #C2C7D2;
  --gsb-placeholder: #8B93A3;
  --gsb-avatar-plate: #EDEAE1;
  --gsb-avatar-plate-quiet: #F2F0E9;
  --gsb-topbar-bg: linear-gradient(180deg, #FFFFFF, #F4F1EA);
  --gsb-bubble-me: linear-gradient(180deg, #E4EAF6, #D7E0F0);

  /* INVERTED: white-on-white shows nothing, so the faint fills carry the navy. */
  --gsb-fill-faint: rgba(20, 38, 75, .035);
  --gsb-fill-faint-2: rgba(20, 38, 75, .045);
  --gsb-fill-hairline: rgba(20, 38, 75, .14);
  --gsb-fill-warm: rgba(201, 150, 42, .12);
  --gsb-fill-sunken: rgba(20, 38, 75, .06);
  --gsb-fill-sunken-soft: rgba(20, 38, 75, .04);

  --gsb-scrim: rgba(20, 38, 75, .38);   /* the dialog backdrop */
  --gsb-scrim-soft: rgba(20, 38, 75, .04); /* INVERTED: the quiet button's own surface */

  /*
    ITS OWN SHADOWS. The defaults are black at .24–.55, tuned to sit on near-black; over
    #F7F5EF they read as smudges. These are the navy at a tenth of that, which is what depth
    looks like on paper.
  */
  --gsb-shadow-dialog: 0 24px 60px rgba(20, 38, 75, .22);
  --gsb-shadow-menu: 0 18px 38px rgba(20, 38, 75, .18);
  --gsb-shadow-pop: 0 16px 36px rgba(20, 38, 75, .16);
  --gsb-shadow-inset: inset 0 1px 3px rgba(20, 38, 75, .10);
  --gsb-shadow-seg: 0 3px 10px rgba(20, 38, 75, .10);

  /* The table is the one dark thing on this theme, and deliberately so. */
  --gsb-felt: #1E7A3E;
  --gsb-felt-mid: #186232;
  --gsb-felt-2: #114C26;
  --gsb-felt-edge: #0D381D;
  --gsb-on-felt: #F1F7F1;
  /*
    The rim is brass, and DARK brass, for the same measured reason as the other two themes: the
    seat names are white on it. #C9962A gives them 2.67 against AA's 4.5; #8A6516 gives 5.2. On
    this theme it also keeps the table reading as the one dark object in a light room.
  */
  --gsb-wood-1: #8A6516;
  --gsb-wood-2: #5E4410;
  --gsb-wood-edge: rgba(94, 68, 16, .45);
  --gsb-wood-under: #6E5112;

  --gsb-seat-taken: #7A2A2A;
  --gsb-seat-taken-edge: rgba(122, 42, 42, .55);
  --gsb-seat-robot: #274A63;
  --gsb-seat-robot-edge: rgba(39, 74, 99, .5);
  --gsb-seat-robot-text: #E6F0F7;
  --gsb-seat-human: #1F5A3C;
  --gsb-seat-human-edge: rgba(31, 90, 60, .5);
  --gsb-seat-human-text: #E9F7EE;

  /*
    Suits on SITE surfaces, which on this theme are white rather than felt — so these are the
    dark, printed-card values rather than the bright ones the dark themes need.
  */
  --gsb-suit-spade: #14264B;
  --gsb-suit-heart: #C8202F;
  --gsb-suit-diamond: #C9962A;
  --gsb-suit-club: #1E7A3E;

  --gsb-hero-signin: linear-gradient(150deg, #1E7A3E, #176332 54%, #114C26 100%);
}

/* ---------- page ---------- */

.gsb-theme {
  margin: 0;
  padding: 0;
  background: var(--gsb-ink);
  color: var(--gsb-cream);
  font-family: var(--gsb-ui);
  font-size: 16.5px;
  font-weight: 500;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}
.gsb-theme *, .gsb-theme *::before, .gsb-theme *::after { box-sizing: border-box; }
.gsb-theme a { color: inherit; text-decoration: none; }
/*
  The button reset normalises the browser's default chrome, and nothing more. It is wrapped in
  :where() so it carries NO specificity: without that it reads (0,1,1) and outranks every page's own
  button rule at (0,1,0) — including this file's own .gsb-btn — so any page that took the theme
  silently lost its buttons (HANDOVER.md 52.51). Keep the :where(): removing it re-breaks every page
  at once, and quietly.
*/
:where(.gsb-theme) button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
.gsb-theme :focus-visible { outline: 2px solid var(--gsb-gold); outline-offset: 2px; }
.gsb-theme h1, .gsb-theme h2, .gsb-theme h3 { font-family: var(--gsb-display); }

/* ---------- buttons ---------- */

.gsb-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 11px 15px;
  border-radius: var(--gsb-radius-pill);
  border: 1px solid var(--gsb-line-strong);
  background: var(--gsb-scrim-soft);
  color: var(--gsb-cream);
  font-size: 15px;
  font-weight: 800;
  white-space: nowrap;
  transition: background .15s ease, border-color .15s ease;
}
.gsb-btn:hover { background: var(--gsb-raise); border-color: var(--gsb-gold); }
.gsb-btn[disabled] { opacity: .55; cursor: default; }

.gsb-btn-primary {
  background: var(--gsb-gold-grad);
  border-color: var(--gsb-gold-edge);
  color: var(--gsb-on-gold);
}
.gsb-btn-primary:hover { background: var(--gsb-gold-grad-hover); }

.gsb-btn-quiet { background: transparent; border-color: var(--gsb-line); font-weight: 700; }
.gsb-btn-quiet:hover { background: rgb(var(--gsb-gold-rgb) / .08); }

/*
  The gold button (2026-09-22). The profile card's own actions and the seat admin menu are gold
  throughout; Cancel is the one thing that stays quiet, because the way out of a menu should not look
  like a thing to do.

  AFTER .gsb-btn-quiet deliberately. Save note carries both classes — quiet by default, gold added
  while the note has unsaved edits — and the two set the same properties at the same specificity, so
  whichever is written second wins. Moving this above quiet would make the dirty state invisible.
*/
.gsb-btn-gold {
  background: var(--gsb-gold-grad);
  border-color: var(--gsb-gold-edge);
  color: var(--gsb-on-gold);
  font-weight: 800;
}
.gsb-btn-gold:hover { background: var(--gsb-gold-grad-hover); border-color: var(--gsb-gold-edge-hover); }

/*
  THE BACK / LEAVE BUTTON, arrow-shaped (2026-09-22). Pointed on the left, rounded on the right, a gold
  outline and whatever colour the text already had. The shape says "back", so the pages drop the "←"
  from their wording and keep the wording itself.

  Drawn as an SVG background rather than clip-path. clip-path would cut the border off with everything
  else outside the shape, and the outline cannot be faked with a second layer underneath because these
  buttons sit on transparent backgrounds — green felt on the table, panel on the others.
  vector-effect="non-scaling-stroke" is what keeps the 1px line even when the SVG is stretched to a
  button several times wider than its viewBox.

  AFTER .gsb-btn-quiet and .gsb-btn-gold, which the pages also put on these buttons: `background` is a
  shorthand and resets background-image, so written above them this rule drew nothing at all and the
  button kept its rectangle. Equal specificity, source order decides.

  The height is untouched: only the padding grows, to clear the point on the left and the curve on the
  right, and .gsb-btn's own vertical padding still decides how tall it is.
*/
/*
  Written as .gsb-btn.gsb-btn-back, not .gsb-btn-back: each page has its own rule for this button
  (.leave-table-btn, .leave-editor-btn, .viewer-leave) setting padding, and a page <style> comes after
  the linked theme, so at one class those would keep the tight padding and crop the point off the arrow.
*/

.gsb-btn.gsb-btn-back {
  position: relative;
  border-color: transparent;
  background: transparent;
  /*
    ONE HEIGHT FOR ALL THREE (2026-09-22). table.html's is the reference at 38px; the Hand Editor's was
    30.6 and the viewer's 40, because each page sets its own min-height and vertical padding. Pinned
    here, at two classes, so those page rules cannot move it — the padding goes to zero and .gsb-btn's
    inline-flex centring does the rest.
  */
  height: 38px;
  min-height: 38px;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 30px;
  padding-right: 22px;
  border-radius: 0;
  /*
    THE OUTLINE HAS TO FIT INSIDE ITS OWN BOX. The first version arced from x=100 with r=20.5 in a
    120-wide viewBox, so the round end reached x=120.5 and was cut off flat, and at y=1.5 the top and
    bottom edges sat on the boundary with half the stroke outside it. Everything now keeps 3 units
    clear: the point at x=3, the edges at y=3 and y=41, and the arc from x=98 with r=19 reaching
    x=117.
  */
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 44" preserveAspectRatio="none"><path d="M26 3 H98 a19 19 0 0 1 0 38 H26 L3 22 Z" fill="none" stroke="%23E0BE6E" stroke-width="1.5" vector-effect="non-scaling-stroke"/></svg>');
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100% 100%;
}
/*
  THE HIGHLIGHT BELONGS INSIDE THE ARROW (2026-09-22). background-color paints the element's whole
  rectangle, so hovering lit up the corners the arrow does not occupy and the shape stopped meaning
  anything. The brightening is the SVG's own `fill` instead — the same path, so it can only ever cover
  what the outline encloses. Focus and the pressed state work the same way, each a slightly stronger
  fill, and the browser's own focus ring is turned off because it too is a rectangle.
*/
.gsb-btn.gsb-btn-back:hover {
  background-color: transparent;
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 44" preserveAspectRatio="none"><path d="M26 3 H98 a19 19 0 0 1 0 38 H26 L3 22 Z" fill="rgba(224,190,110,0.12)" stroke="%23E0BE6E" stroke-width="1.5" vector-effect="non-scaling-stroke"/></svg>');
  border-color: transparent;
}

/*
  After the base rule, not before it: all three set background-image at the same specificity, so
  written above it the base rule simply painted over them and neither focus nor the press showed.
*/
.gsb-btn.gsb-btn-back:focus-visible {
  outline: none;
  background-color: transparent;
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 44" preserveAspectRatio="none"><path d="M26 3 H98 a19 19 0 0 1 0 38 H26 L3 22 Z" fill="rgba(224,190,110,0.14)" stroke="%23F0D9A0" stroke-width="2" vector-effect="non-scaling-stroke"/></svg>');
}
.gsb-btn.gsb-btn-back:active {
  background-color: transparent;
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 44" preserveAspectRatio="none"><path d="M26 3 H98 a19 19 0 0 1 0 38 H26 L3 22 Z" fill="rgba(224,190,110,0.22)" stroke="%23E0BE6E" stroke-width="1.5" vector-effect="non-scaling-stroke"/></svg>');
}


.gsb-btn-danger { color: var(--gsb-danger-text); border-color: rgb(var(--gsb-alert-rgb) / .5); }
.gsb-btn-danger:hover { background: rgb(var(--gsb-alert-rgb) / .16); border-color: var(--gsb-alert); }

.gsb-iconbtn {
  width: 38px;
  height: 38px;
  display: grid;
  place-items: center;
  border-radius: var(--gsb-radius-pill);
  color: var(--gsb-muted);
  position: relative;
}
.gsb-iconbtn:hover { background: rgb(var(--gsb-gold-rgb) / .09); color: var(--gsb-gold-bright); }

/* ---------- pills ---------- */

.gsb-pill {
  font-size: 10.5px;
  letter-spacing: .12em;
  font-weight: 700;
  color: var(--gsb-on-gold);
  background: var(--gsb-gold);
  border-radius: var(--gsb-radius-pill);
  padding: 2px 8px;
}
/* (2026-10-17) No tinted box: a quiet pill is only ever a menu value, and it sits directly on the menu (the box was cream on cream on light menus). */
.gsb-pill-quiet { color: var(--gsb-gold-bright); font-weight: 700; }


/* ---------- panels and cards ---------- */

.gsb-panel {
  background: var(--gsb-panel);
  border: 1px solid var(--gsb-line);
  border-radius: var(--gsb-radius);
  overflow: hidden;
}
.gsb-panel > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--gsb-line);
}
.gsb-panel > header h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--gsb-gold-bright);
}
.gsb-panel > header .gsb-meta { font-size: 13px; color: var(--gsb-muted); font-family: var(--gsb-ui); }


/* ---------- inputs ---------- */

.gsb-input {
  width: 100%;
  background: var(--gsb-well);
  border: 1px solid var(--gsb-line);
  color: var(--gsb-cream);
  font: inherit;
  font-size: 14.5px;
  padding: 9px 13px;
  border-radius: var(--gsb-radius-pill);
}
.gsb-input::placeholder { color: var(--gsb-placeholder); }
.gsb-input:focus-visible { border-color: var(--gsb-line-strong); }

.gsb-textarea {
  width: 100%;
  min-height: 72px;
  resize: vertical;
  background: var(--gsb-well);
  border: 1px solid var(--gsb-line);
  color: var(--gsb-cream);
  font: inherit;
  font-size: 14.5px;
  padding: 10px 12px;
  border-radius: 10px;
}


/* ---------- avatars ---------- */

/*
  A picture when the person has one, their initials when they do not — the initials are the text
  inside, so a page sets textContent and drops an <img> in when there is a picture.

  --gsb-avatar-bg is the one colour behind initials everywhere. Four different ones grew up across
  the pages (#2A1F12 here, #17334a on table.html, #173329 on find-table.html and a gold gradient in
  the Hand Editor); the other three pages inherit this when their chrome moves onto the theme
  (HANDOVER.md 52.37). It is only ever seen behind initials: the avatar images are opaque, so no
  colour shows through one — changing those is a separate decision about the artwork.
*/
.gsb-avatar {
  width: 36px;
  height: 36px;
  flex: 0 0 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--gsb-avatar-bg);
  border: 1px solid var(--gsb-line-strong);
  color: var(--gsb-gold-bright);
  font-family: var(--gsb-display);
  font-size: 16px;
  line-height: 1;
}
.gsb-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gsb-avatar-sm { width: 30px; height: 30px; flex-basis: 30px; font-size: 15px; border-color: var(--gsb-line); }
/*
  Twice what it was (2026-09-22). Only gsb-profile-popup.js asks for -lg, so this is the profile
  card's portrait and nothing else; the initials scale with it or a two-letter fallback sits in the
  middle of a large empty disc.
*/
.gsb-avatar-lg { width: 128px; height: 128px; flex-basis: 128px; font-size: 48px; }

/* ---------- the shell: top bar and sidebar ---------- */

/*
  The chrome public/gsb-shell.js draws. It came out of home.html, which had the only copy
  (HANDOVER.md 52.46). A page supplies the two containers and its own <main>; nothing here knows
  about any one page.

  Everything is prefixed gsb-: a page's own furniture keeps its own names, and the two never collide
  even where a page calls something .brand or .hero of its own (52.47).
*/
.gsb-top {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--gsb-line);
  background: var(--gsb-topbar-bg);
  position: sticky;
  top: 0;
  z-index: 20;
}
.gsb-brand {
  width: var(--gsb-side-w);
  flex: 0 0 var(--gsb-side-w);
  padding: 14px 18px 12px;
  border-right: 1px solid var(--gsb-line);
  display: flex;
  gap: 12px;
  align-items: center;
}
.gsb-brand-pip { width: 34px; height: 34px; flex: 0 0 34px; display: grid; place-items: center; color: var(--gsb-gold); font-size: 24px; line-height: 1; }
.gsb-brand h1 {
  margin: 0;
  font-family: var(--gsb-display);
  font-weight: 700;
  font-size: 21px;
  letter-spacing: .13em;
  color: var(--gsb-wordmark-lead);
  line-height: 1.15;
  text-transform: none;            /* the name is shown as it is written - "Bridge Club", a capital each - neither all capitals (as it was until 2026-11-23) nor all lower case */
  letter-spacing: .04em;
}
.gsb-brand h1 span { display: block; font-size: 14px; letter-spacing: .34em; color: var(--gsb-wordmark-trail); }
/* The tagline is stored in sentence case; the capitals are this rule's, not the string's. */
.gsb-brand-tagline { display: block; margin-top: 2px; font-style: normal; font-size: 10px; letter-spacing: .2em; text-transform: uppercase; color: var(--gsb-muted); }

.gsb-topnav { flex: 1; display: flex; align-items: center; gap: 4px; padding: 0 14px; min-width: 0; overflow: hidden; }
.gsb-topnav a { padding: 8px 12px; border-radius: 9px; font-size: 15.5px; font-weight: 600; white-space: nowrap; }
.gsb-topnav a:hover { background: rgb(var(--gsb-gold-rgb) / .08); color: var(--gsb-gold-bright); }
.gsb-topnav a.gsb-soon { color: var(--gsb-muted); }
.gsb-topnav a.gsb-current { color: var(--gsb-gold-bright); }
.gsb-topright { display: flex; align-items: center; gap: 8px; padding: 0 16px 0 8px; }

.gsb-shell { display: flex; align-items: flex-start; min-height: calc(100vh - var(--gsb-topbar-h)); }
.gsb-side {
  width: var(--gsb-side-w);
  flex: 0 0 var(--gsb-side-w);
  border-right: 1px solid var(--gsb-line);
  background: var(--gsb-panel);
  padding: 16px 12px 28px;
  position: sticky;
  top: var(--gsb-topbar-h);
  align-self: stretch;
}
.gsb-side a {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border-radius: 10px;
  margin-bottom: 2px;
  color: var(--gsb-cream);
  font-size: 16.5px;
  font-weight: 600;
}
.gsb-side a svg { width: 18px; height: 18px; flex: 0 0 18px; stroke: var(--gsb-gold); stroke-width: 1.5; fill: none; opacity: .85; }
.gsb-side a:hover { background: rgb(var(--gsb-gold-rgb) / .08); }
/* The page you are on, marked from the key the page passes — never from the address (52.46). */
.gsb-side a.gsb-current {
  background: linear-gradient(90deg, rgb(var(--gsb-gold-rgb) / .17), rgb(var(--gsb-gold-rgb) / .04));
  color: var(--gsb-gold-bright);
  box-shadow: inset 3px 0 0 var(--gsb-gold);
}
.gsb-side a.gsb-soon { color: var(--gsb-muted); }
.gsb-side a.gsb-soon svg { opacity: .45; }
.gsb-side-rule { height: 1px; background: var(--gsb-line); margin: 18px 14px; }
.gsb-side-motto {
  font-family: var(--gsb-display);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.5;
  color: var(--gsb-muted);
  text-align: center;
  padding: 0 18px;
}
.gsb-side-motto::before { content: "♠"; display: block; color: var(--gsb-gold); margin-bottom: 8px; font-size: 13px; }

.gsb-main { flex: 1; min-width: 0; padding: 20px 22px 34px; }

@media (max-width: 820px) {
  .gsb-side { display: none; }
  .gsb-brand { width: auto; flex: 0 0 auto; border-right: none; }
  .gsb-topnav { display: none; }
  .gsb-main { padding: 16px; }
}

/* ---------- the avatar menu ---------- */

/*
  The control in the top bar: who you are, and what you can do about it. Built here rather than on
  home.html so every page picks it up when the shared shell rolls out (HANDOVER.md 52.37).

  The page owns what the items do. Everything but Sign out is disabled with a SOON pill for now.
*/
.gsb-usermenu-wrap { position: relative; }

.gsb-usermenu-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 8px 5px 5px;
  border-radius: var(--gsb-radius-pill);
  cursor: pointer;
}
.gsb-usermenu-btn:hover { background: rgb(var(--gsb-gold-rgb) / .08); }
.gsb-usermenu-btn b { display: block; font-weight: 600; font-size: 15.5px; line-height: 1.2; text-align: left; }
.gsb-usermenu-btn small { display: block; color: var(--gsb-muted); font-size: 13px; text-align: left; }
.gsb-usermenu-chev { color: var(--gsb-muted); font-size: 11px; margin-left: 2px; }

.gsb-usermenu {
  position: absolute;
  right: 8px;
  top: calc(100% + 6px);
  min-width: 232px;
  background: var(--gsb-panel-2);
  border: 1px solid var(--gsb-line-strong);
  border-radius: var(--gsb-radius);
  padding: 7px;
  box-shadow: var(--gsb-shadow-menu);
  z-index: 40;
  display: none;
}
.gsb-usermenu.open { display: block; }
.gsb-usermenu .gsb-usermenu-who { padding: 8px 12px 10px; border-bottom: 1px solid var(--gsb-line); margin-bottom: 6px; }
.gsb-usermenu .gsb-usermenu-who b { display: block; font-size: 15.5px; }
.gsb-usermenu .gsb-usermenu-who small { color: var(--gsb-muted); font-size: 13px; }
.gsb-usermenu button {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 12px;
  border-radius: 9px;
  font-size: 15.5px;
  font-weight: 600;
  text-align: left;
  color: var(--gsb-cream);
}
.gsb-usermenu button:hover:not(:disabled) { background: rgb(var(--gsb-gold-rgb) / .1); color: var(--gsb-gold-bright); }
.gsb-usermenu button:focus-visible { background: rgb(var(--gsb-gold-rgb) / .1); }
.gsb-usermenu button:disabled { color: var(--gsb-muted); cursor: default; }
.gsb-usermenu button:disabled .gsb-pill { font-size: 9.5px; }
.gsb-usermenu .gsb-usermenu-sep { height: 1px; background: var(--gsb-line); margin: 6px 4px; }
.gsb-usermenu .gsb-usermenu-out { color: var(--gsb-gold-bright); }

/*
  THE THEME PICKER, inside the user menu (2026-09-29).

  An inline group rather than a flyout, so the menu's existing arrow-key walker picks the rows up
  for nothing and there is no placement to get right at four window widths.

  Every colour here is a token. A literal would be invisible on exactly one of the four themes —
  most likely Clubhouse, the light one, where a dark literal is a hole rather than a wrong shade —
  and the picker is the last place that should fail to follow the theme it is setting.
*/
.gsb-usermenu .gsb-usermenu-themerow[aria-expanded="true"] { color: var(--gsb-gold-bright); }
/* The active theme's name, on the collapsed row. Long labels shorten rather than widen the menu. */
.gsb-usermenu .gsb-usermenu-themerow .gsb-pill-quiet {
  /* (2026-10-17) its --gsb-cream is gone: pale cream on Silver & Charcoal's light menu. It reads in the value colour, like every other value. */
  max-width: 124px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gsb-usermenu .gsb-usermenu-themes { display: none; margin: 2px 0 4px; }
.gsb-usermenu .gsb-usermenu-themes.open { display: block; }
.gsb-usermenu .gsb-usermenu-theme {
  justify-content: flex-start;   /* the menu's own buttons are space-between; these are tick-then-label */
  gap: 8px;
  padding: 7px 12px 7px 10px;
  font-size: 14.5px;
  font-weight: 600;
  color: var(--gsb-muted);
}
.gsb-usermenu .gsb-usermenu-theme.on { color: var(--gsb-cream); }
/*
  The tick keeps its space when it is not shown, so choosing a theme does not shift the four rows
  sideways — which, on a menu you are meant to click three times in a row, is the whole difference
  between comparing themes and chasing them.
*/
/*
  The tick takes --gsb-gold-bright, not --gsb-gold, for the same reason and with more at stake: it
  is the only mark saying which theme is on. --gsb-gold reads 2.34 on Clubhouse's menu, under even
  the 3:1 that non-text needs; --gsb-gold-bright reads 5.3. It is also what .gsb-usermenu-out
  already uses, so the menu keeps one accent rather than two.
*/
.gsb-usermenu .gsb-usermenu-tick { width: 14px; flex: 0 0 14px; text-align: center; visibility: hidden; color: var(--gsb-gold-bright); }
.gsb-usermenu .gsb-usermenu-theme.on .gsb-usermenu-tick { visibility: visible; }

/* ==========================================================
   THE TEAM-MATCH SCORECARD (2026-09-29)

   Drawn by gsb-score-panel.js, which three pages use, so the rules are here rather than on any
   one of them. On table.html it sits on the felt, so the text takes the --gsb-table-panel-*
   roles from the Clubhouse work: the card's own background follows the theme and is WHITE on a
   light one, and the felt's light chrome colours would be invisible on it.
   ========================================================== */
/*
  BLOCK FLOW, NOT GRID, and the reason is the table.

  As a grid item the scorecard table was given a 207px track and laid itself out at 317 - its
  rowspan header cells, which hold the seating diagrams, are resolved after track sizing, so the
  track was sized from something smaller than the table turned out to be. The footer then sat on
  top of the last two rows. Margins between block children cannot do that.
*/
.gsb-scorecard { display: block; }
.gsb-scorecard > * + * { margin-top: 10px; }
.gsb-scorecard-title {
  margin: 0;
  color: var(--gsb-table-panel-heading);
  font: 800 17px/1.2 var(--gsb-ui);
  text-align: center;
}
.gsb-scorecard-headline {
  margin: 0;
  text-align: center;
  color: var(--gsb-table-panel-text);
  font-size: 13.5px;
  font-weight: 700;
}
.gsb-scorecard-tabs { width: fit-content; margin-inline: auto; }

.gsb-scorecard-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  color: var(--gsb-table-panel-text);
}
.gsb-scorecard-table th,
.gsb-scorecard-table td {
  padding: 4px 8px;
  border-bottom: 1px solid var(--gsb-table-line);
  font-size: 12.5px;
}
.gsb-scorecard-table thead th { vertical-align: bottom; }
.gsb-scorecard-tablehead { padding: 0 6px 8px; vertical-align: top; }
.gsb-scorecard-imphead {
  color: var(--gsb-table-panel-muted);
  font-size: 11.5px;
  font-weight: 700;
  text-align: center;
  white-space: nowrap;
}
.gsb-scorecard-num { width: 1%; text-align: right; color: var(--gsb-table-panel-muted); }
.gsb-scorecard-contract { text-align: left; white-space: nowrap; }
.gsb-scorecard-score { text-align: right; white-space: nowrap; }
.gsb-scorecard-imp { text-align: center; width: 1%; white-space: nowrap; font-weight: 800; }
/*
  The two IMP colours were literals on the old panel - #65dd8c and #ff858a. They are roles now,
  with those values as the default, so the three dark themes are unchanged and Clubhouse gets a
  pair that can be read on white.
*/
.gsb-scorecard-ours   { color: var(--gsb-scorecard-ours); }
.gsb-scorecard-theirs { color: var(--gsb-scorecard-theirs); }
.gsb-scorecard-totallabel {
  text-align: right;
  color: var(--gsb-table-panel-muted);
  font-weight: 700;
}
.gsb-scorecard-table tfoot td { border-bottom: 0; border-top: 1px solid var(--gsb-line-strong); }

/*
  A contract is a button because it does something; it is styled as the link it reads as. The
  score beside it, the IMPs and the board number are plain text on purpose - one board, two
  tables, and the contract is the word that says which one you mean.
*/
/*
  The reset is explicit because table.html paints every bare `button` on the page -
  `input,button{background:linear-gradient(#13243a,#0d1c2f);border:1px solid …;border-radius:10px}`
  - so a contract came out as a navy pill with dark text on it. Naming only the colour left the
  rest of that rule in place.
*/
.gsb-scorecard-open {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  font: inherit;
  color: var(--gsb-table-panel-gold);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.gsb-scorecard-open:hover { color: var(--gsb-table-panel-heading); }

/* The seat diagrams, at about a third of the size Show lineup draws them. */
.gsb-scorecard-seats { display: grid; justify-items: center; gap: 4px; }
.gsb-scorecard-seats-title {
  color: var(--gsb-table-panel-heading);
  font-size: 13px;
  font-weight: 800;
  white-space: nowrap;
}
.gsb-scorecard-seats .seat-picker { padding: 27px 0; }
/*
  BIGGER, AND THE SAME SIZE IN ALL THREE HOSTS (2026-09-29). The half-time panel scrolls rather
  than shrinking its diagrams: the boards and the totals below them matter more than fitting one
  screen, and a 16-board match scrolls whatever this is set to.
*/
.gsb-scorecard-seats .seat-table { width: 200px; }
/*
  The same, at the scorecard's size.

  A PIXEL REMAINS, AND IT IS NOT min-height. Measured on the live table a person's pill is 21.36
  and a robot's 20.36, both with a computed min-height of 21px - and raising it to 22 moved
  neither, so something else governs these boxes and I stopped rather than keep guessing. The
  fractions are the workspace scaler, which scales everything on the table by about 0.97.

  One device pixel at this size is invisible; the complaint was 85 against 42 and a pale card
  beside a dark pill, and both of those are gone. Recorded so the next person knows it was seen
  and judged rather than missed.
*/
/*
  THE PILL HAS TO FIT ITS OWN FELT (2026-09-29), and the WIDTH is the one that was never
  overridden.

  .seat-btn is 112px, sized for the full seat picker. The lineup replaces that
  (width:max-content, min-width:74px); the scorecard set a font size and a height and left the
  width alone - so 112px pills sat on a 104px felt, East spanning 36..148 and West -44..68, and
  they met in the middle. That is the overlap, and it is why the same widget measured clean on
  table.html and collided inside a dialog: different surrounding width, same 112px pills.

  Sized from the felt instead, and the text centred - the base .seat-btn never sets text-align,
  which the lineup supplies for itself and the scorecard never did.
*/
/*
  138px, not 126: an ordinary sixteen-character username is 115px at the 14px these names are
  drawn at, and a 126px pill has 114px of inner width - so the longest names the site can issue
  came out 5px wider than their pill. Measured in scripts/seat-diagram-rig.html, which prints
  that overhang for a real sixteen-character name and for a pathological one.
*/
.gsb-scorecard-seats .seat-btn { width: 138px; text-align: center; padding: 3px 6px; font-size: 17px; min-height: 40px; }
.gsb-scorecard-seats .seat-reserve-name { white-space: nowrap; font-size: 14px; }
/*
  THE AVATAR SITS ON THE SEAT LETTER'S OWN LINE, and small (2026-09-29).

  At 17px on a line of its own it made a person's pill 32px tall against a robot's 19 - and these
  pills straddle the felt by 55% OF THEIR OWN HEIGHT, so the taller ones ate the middle of a
  104px square. Inline and 12px, a person's pill is within a pixel or two of a robot's and the
  felt is a felt again.
*/
/* An <img> avatar crops to its circle instead of stretching to it. */
.seat-avatar-img{object-fit:cover;padding:0}
.gsb-scorecard-seats .seat-avatar {
  display: inline-block;
  vertical-align: middle;
  width: 23px; height: 23px; min-width: 23px;
  margin: 0 0 0 5px;
  font-size: 11px;
  line-height: 23px;
}
/*
  AND THE CELL HAS TO RESERVE THE OVERHANG. A pill is 80px wide on a 104px felt and is pushed
  55% of its own width outside, so the diagram is really ~190px across while its <th> was sized
  to the felt alone - which is why the left table's East pill was sitting in the score columns.
*/
/*
  ROOM FOR THE OVERHANG, ON ALL FOUR SIDES. A pill is pushed 55% of its own size outside the
  felt, so the diagram is larger than its green square in every direction - and the block-start
  half of that was landing on the heading above it. The inline padding was already here for the
  East and West pills; the block padding is what stops North touching "Our table".
*/
.gsb-scorecard-seats { padding: 30px 72px 14px; }

.gsb-scorecard-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.gsb-scorecard-count {
  color: var(--gsb-table-panel-muted);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* ---------- dialogs ---------- */

/*
  The vocabulary every modal on the site converges on (HANDOVER.md 52.57). There are five or six of
  them — find-table's create and reserve, home's profile popup, the Hand Editor's import and saved
  boards, history's analysis — and each was built from scratch, which is why no two look alike.

  Written for all of them and applied so far to three; the Hand Editor's and history's come with the
  branches that convert those pages.

    .gsb-dialog-layer   the overlay. Hidden until .open
      .gsb-dialog       the panel. Width from --gsb-dialog-w, set per dialog
        .gsb-dialog-head   title, optional .gsb-meta, optional .gsb-dialog-close
        .gsb-dialog-lede   the line under the title that says what this is for
        .gsb-dialog-body   the part that scrolls
        .gsb-dialog-foot   .gsb-dialog-msg on the left, buttons on the right

  **Buttons are not part of this.** A dialog's buttons are .gsb-btn and .gsb-btn-primary like every
  other button on the site. Each modal having invented its own .cancel and .confirm is most of why
  they diverged.

  The width is a custom property rather than three size classes: the dialogs that exist want 430,
  440, 500 and 760px, and -sm/-md/-lg would have fitted none of them honestly.
*/
.gsb-dialog-layer {
  position: fixed;
  inset: 0;
  z-index: 160;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: var(--gsb-scrim);
}
.gsb-dialog-layer.open { display: flex; }

/*
  A DIALOG THAT OPENS FROM INSIDE ANOTHER ONE (2026-09-29). Every layer is z-index 160, so which
  of two open dialogs is on top is decided by document order - and the board popup and the
  scorecard both append their layer at the end, which puts them over a profile card declared in
  the page's own markup. The profile card is always the innermost of these (a name inside a
  scorecard, a lineup or a board popup opens it, never the other way round), so it is the one
  that carries this.
*/
/*
  AND 200 STOPPED BEING THE TOP (2026-09-30). .gsb-lineup-layer is z-index 12000 - it has been
  since the lineup was built, for its own reasons - so a profile card opened from a name IN the
  lineup rendered at 200, underneath it, and the click looked like it had done nothing. That is
  the whole of the reported bug: the card opened every time and nobody could see it.

  It was never right and never noticed, because the profile card and the lineup had no other
  reason to be on screen together until the lineup's names became clickable. Measured with
  elementFromPoint over the middle of the card: `gsb-lineup-layer`.

  The name says what this class is for, so the number follows the name rather than the other way
  round: above everything else the theme puts up, including the lineup.
*/
.gsb-dialog-layer.gsb-dialog-top { z-index: 12100; }

/*
  A NAME THAT OPENS A PLAYER. Used on the seat pills in the scorecard's and the lineup's diagrams;
  the board popup's frame has its own copy of this rule, because it cannot see this stylesheet.
  It sits on a <button>, so the button's own chrome has to come off first.
*/
.gsb-who-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
  /*
    currentColor, faded - NOT rgb(var(--gsb-cream-rgb) / .45), which is what this said for one
    draft. There is no --gsb-cream-rgb: the only channel tokens are gold, gold-edge, alert and
    green. An undefined token makes the declaration invalid at computed-value time, so it would
    have fallen back to the initial value silently, exactly as the 23 declarations did on
    2026-09-28. The fallback below is for anything without color-mix.
  */
  text-decoration-color: currentColor;
  text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
}
.gsb-who-link:hover { text-decoration-style: solid; }

/*
  THE WHOLE PILL OPENS THE PLAYER, not just the twelve pixels of text (2026-09-29). A seat pill
  is a picture and a name inside one box, and "click the name exactly" is a target nobody aims
  for - the first cut put the handler on the name alone and it read as not working at all. The
  underline stays on the name, because that is where the affordance belongs.
*/
.gsb-scorecard-seats .seat-btn.gsb-who-open,
.gsb-lineup-team .seat-btn.gsb-who-open { cursor: pointer; }

.gsb-dialog {
  display: flex;
  flex-direction: column;
  width: min(var(--gsb-dialog-w, 440px), 100%);
  max-height: 92vh;
  background: var(--gsb-panel);
  border: 1px solid var(--gsb-line-strong);
  border-radius: var(--gsb-radius-lg);
  box-shadow: var(--gsb-shadow-dialog);
}

.gsb-dialog-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 18px 18px 0;
  flex: 0 0 auto;
}
.gsb-dialog-head h2, .gsb-dialog-head h3 {
  margin: 0;
  font-family: var(--gsb-display);
  font-size: 23px;
  font-weight: 500;
  color: var(--gsb-cream);
}
.gsb-dialog-close {
  margin-left: auto;
  font-size: 22px;
  line-height: 1;
  color: var(--gsb-muted);
  flex: 0 0 auto;
}
.gsb-dialog-close:hover { color: var(--gsb-cream); }
.gsb-dialog-lede { margin: 7px 18px 0; color: var(--gsb-muted); font-size: 13px; line-height: 1.45; }

/*
  The BODY scrolls, not the whole dialog: a long friends list or a long profile used to carry the
  title off the top of the screen with it.
*/
.gsb-dialog-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 16px 18px; }

.gsb-dialog-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 18px 18px;
  flex: 0 0 auto;
}
/* The message takes the space, so the buttons sit right whether or not there is anything to say. */
.gsb-dialog-msg { flex: 1; min-height: 20px; margin: 0; font-size: 12.5px; color: var(--gsb-msg-error); }

/* ---------- segmented control ---------- */

/*
  One choice among two or three, all of them visible — Public / Private here. The theme had none, so
  every page that wanted one drew its own (52.51).

  There is deliberately NO switch to go with it. The only switch on the site is find-table's "Allow
  Kibitzers", and kibitzing is not implemented (CLAUDE.md invariant 19) — a shared control designed
  from one instance, of a feature that does not exist, would be designed in the dark. It stays that
  page's own furniture until a second real one turns up (52.57).
*/
.gsb-seg { display: inline-flex; gap: 4px; padding: 4px; border-radius: var(--gsb-radius-pill); background: var(--gsb-well); border: 1px solid var(--gsb-line); }
.gsb-seg-btn {
  min-height: 38px;
  padding: 0 18px;
  border-radius: var(--gsb-radius-pill);
  border: 1px solid transparent;
  background: transparent;
  color: var(--gsb-muted);
  font-weight: 800;
  font-size: 14px;
}
.gsb-seg-btn:hover:not(:disabled):not(.on) { color: var(--gsb-cream); background: rgb(var(--gsb-gold-rgb) / .07); }
.gsb-seg-btn.on {
  background: var(--gsb-gold-grad);
  border-color: var(--gsb-gold-edge);
  color: var(--gsb-on-gold);
}
/*
  A disabled option must not read as a stripped one. It keeps its border, so it still looks like a
  control that is simply not available — which is the distinction a presence check cannot make.
*/
.gsb-seg-btn:disabled { opacity: .5; cursor: not-allowed; border-color: var(--gsb-line); }

/* ---------- the profile popup ---------- */

/*
  What public/gsb-profile-popup.js and public/gsb-profile-actions.js draw inside a dialog: the head,
  the fields, the private notes and the relationship menu. Moved here verbatim from home.html, which
  had the only copy, when find-table.html became the second page to mount the popup (52.57).

  table.html mounts the same two modules but does not take this stylesheet — it keeps its own copy,
  and is not affected by anything here.
*/
.gsb-profile-head { display: flex; align-items: center; gap: 12px; }
.gsb-profile-names strong { display: block; font-family: var(--gsb-display); font-size: 19px; color: var(--gsb-cream); }
.gsb-profile-names span { display: block; color: var(--gsb-muted); font-size: 13.5px; }
.gsb-profile-fields { margin-top: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.gsb-profile-field { padding: 9px 10px; border-radius: 10px; background: var(--gsb-fill-faint-2); border: 1px solid var(--gsb-line); }
.gsb-profile-field b { display: block; color: var(--gsb-gold); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
.gsb-profile-field span { display: block; margin-top: 3px; font-size: 13px; }
.gsb-profile-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; position: relative; }
.gsb-profile-actions-host { position: relative; }
/*
  THE RELATIONSHIP CONTROL, and what its fill means (2026-09-22).

  It was solid gold in every state but blocked, so the fill said nothing: Player, Follows you,
  Following and Friend were one colour and only the word differed. Now that every other button on
  this card is gold too, that would have left the word doing all the work on the one control whose
  state matters most.

  So the fill carries it: FILLED means I follow this player — "Following", or "Friend" when they
  follow back. OUTLINED means I do not — "Player", or "Follows you" when they follow me and I have
  not. Same gold either way, so it still reads as the card's primary control; filled or not is the
  state, and the word still names it exactly.
*/
/*
  The same box as .gsb-btn (2026-09-22). This control sat at 38px with a 16.5px inherited font beside
  47.3px buttons at 15px, so the card's row of actions stepped down in the middle. It is not given
  .gsb-btn — that would hand it a background and border this rule then has to undo in four states —
  so the metrics that decide the height are mirrored instead: padding, font-size and the inline-flex
  centring. NO line-height here: .gsb-btn does not set one either, and pinning this to `normal` left it
  2.3px short of the others. Anything that changes the box has to change in both places.
*/
.profile-actions-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 11px 15px;
  font-size: 15px;
  border-radius: var(--gsb-radius-pill);
  font-weight: 800;
  white-space: nowrap;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--gsb-gold-edge);
  color: var(--gsb-gold-bright);
}
.profile-actions-btn:hover { background: rgb(var(--gsb-gold-edge-rgb) / .12); }

/* The two states that mean "I follow them". */
.profile-actions-btn[data-state="following"],
.profile-actions-btn[data-state="friend"] {
  background: var(--gsb-gold-grad);
  border-color: var(--gsb-gold-edge);
  color: var(--gsb-on-gold);
}
.profile-actions-btn[data-state="following"]:hover,
.profile-actions-btn[data-state="friend"]:hover {
  background: var(--gsb-gold-grad-hover);
}

.profile-actions-btn[data-state="blocked"] { background: var(--gsb-blocked-bg); border-color: var(--gsb-blocked-edge); color: var(--gsb-blocked-text); }
.profile-actions-menu {
  position: absolute;
  z-index: 40;
  top: calc(100% + 5px);
  left: 0;
  min-width: 180px;
  display: none;
  flex-direction: column;
  padding: 5px;
  border-radius: 11px;
  background: var(--gsb-panel-2);
  border: 1px solid var(--gsb-line-strong);
  box-shadow: var(--gsb-shadow-pop);
}
.profile-actions-menu.open { display: flex; }
.profile-actions-item { min-height: 34px; padding: 0 10px; border: 0; border-radius: 8px; background: transparent; color: var(--gsb-cream); font: inherit; font-weight: 700; text-align: left; cursor: pointer; }
.profile-actions-item:hover { background: rgb(var(--gsb-gold-rgb) / .09); }
.profile-actions-item-danger { color: var(--gsb-danger-text); }
.gsb-profile-notes-label { display: block; margin: 14px 0 6px; font-size: 12.5px; font-weight: 700; color: var(--gsb-muted); }
.gsb-profile-save-note { margin-top: 8px; }
.gsb-profile-msg { min-height: 17px; margin-top: 8px; color: var(--gsb-msg-ok); font-size: 12px; }

/* ---------- the social panel ---------- */

/*
  Friends and Messages in one panel, pinned to the window beside the page (public/gsb-social-panel.js).
  It came out of home.html, which had the only copy, when the panel became the shell's (52.53).

  The names inside it are NOT gsb-prefixed, against the rule in 52.47, and deliberately:
  .friend-row and its parts are what gsb-friends-panel.js generates for table.html and the Hand
  Editor, and .convs, .conv-row, .thread, .bubble, .friend-state and .friend-unread are pinned by the
  three home tests. Every one of them is scoped under .gsb-rail here, so none can reach a page's own
  furniture — which is what the prefix was for.

  A page does not opt into these paddings: the shell adds .gsb-has-rail when it mounts a panel.
*/
.gsb-top.gsb-has-rail { margin-right: 346px; }
.gsb-shell.gsb-has-rail { padding-right: 346px; }

.gsb-rail {
  position: fixed;
  top: 12px;
  right: 16px;
  bottom: 12px;
  width: var(--gsb-rail-w);
  display: flex;
  flex-direction: column;
  z-index: 25;
}
.gsb-rail .social { display: flex; flex-direction: column; flex: 1; min-height: 0; }

/*
  THE SWITCHER, ONE LOOK IN ONE PLACE (2026-09-22). Friends / Messages is the table's
  "The Auction / History" switcher: a well-coloured bar, an inactive tab that is transparent and muted
  in the display face, and a selected tab filled with the same gold gradient. The declarations are
  written once, for both, rather than the second being a copy of the first that drifts.

  table.html's own #tableWorkspace rules still carry that switcher, at !important, so its appearance is
  untouched by this; retiring them in favour of these is a separate step, noted in the entry.
*/
.gsb-tabbar,
/*
  A THIRD TAB NEEDS A THIRD COLUMN (2026-09-30).

  Two columns, and Director is the third button - so the moment it was unhidden it wrapped onto
  a second row under Friends and Messages. Exactly the fault table.html's sidebar had on the
  same day, with a different number of columns; the two are separate components and each had to
  be told.

  Keyed on html.gsb-is-director, the shell's own flag, set from the same answer that unhides the
  tab - so the column appears exactly when the tab does, and everybody else keeps two.
*/
html.gsb-is-director .gsb-rail .tabs { grid-template-columns: 1fr 1fr 1fr; }

.gsb-rail .tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  padding: 5px;
  background: var(--gsb-well);
  border-bottom: 1px solid var(--gsb-line);
  flex: 0 0 auto;
}
/*
  `hidden` HAS TO BE TOLD, AGAIN (2026-09-30). THIRD TIME IN THIS CODEBASE.

  `[hidden]{display:none}` is a USER-AGENT rule and any author `display` beats it, whatever the
  specificity. The rule below sets `display:flex` on every tab button, so `tab.hidden = true`
  did nothing and EVERY account saw the Director tab - on a second row, because
  html.gsb-is-director was (correctly) not set for somebody directing nothing, so the
  three-column rule did not apply. Both halves of that report are this one line.

  The unread badge has it too: `.gsb-rail .tabs .n` is `display:inline-flex`, so a count of zero
  was shown as a gold "0" beside Messages although the element was hidden.

  The same trap took Leave Table (2026-09-30) and #redealBtn and the burger's items before it.
  Anything given an author `display` needs this guard, or `hidden` is decoration.
*/
.gsb-tabbar-btn[hidden],
.gsb-rail .tabs button[hidden],
.gsb-rail .tabs .n[hidden] { display: none !important; }

.gsb-tabbar-btn,
.gsb-rail .tabs button {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-width: 0;
  min-height: 42px;
  /*
    THE LABEL HAS TO FIT INSIDE ITS OWN BACKGROUND (2026-09-30).

    The selected tab is a coloured pill the width of its grid column, and the label was drawn
    over the top of it: with three tabs and a real unread count, "Messages 12" measured 103px of
    content in a 101px button and hung a pixel past both edges. It was centred - it was just
    bigger than the thing it was centred in.

    So the padding and the gap give way first, and overflow:hidden is the backstop: whatever a
    future label says, it is clipped to the pill rather than printed across its neighbours.
  */
  padding: 0 6px;
  overflow: hidden;
  border: 0;
  border-radius: 12px;
  background: transparent;
  color: var(--gsb-muted);
  font-family: var(--gsb-display);
  font-size: 16px;
  font-weight: 750;
  cursor: pointer;
}
.gsb-tabbar-btn:not(.on):hover,
.gsb-rail .tabs button:not(.on):hover {
  color: var(--gsb-cream);
  background: rgb(var(--gsb-gold-rgb) / .07);
}
.gsb-tabbar-btn.on,
.gsb-rail .tabs button.on {
  background: var(--gsb-gold-grad);
  color: var(--gsb-on-gold);
  box-shadow:
    inset 0 0 0 1px var(--gsb-fill-hairline),
    var(--gsb-shadow-seg);
}
/*
  One tab rather than two: the title of a panel that has no second view. The tab sizes to its own text
  and whatever else the header carries — Show offline, the mic and camera — sits to the right of it.
*/
.gsb-tabbar-solo {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.gsb-tabbar-solo .gsb-tabbar-btn {
  flex: 0 0 auto;
  padding: 0 16px;
}

.gsb-tabbar-btn:focus-visible,
.gsb-rail .tabs button:focus-visible {
  outline: 2px solid var(--gsb-gold);
  outline-offset: -2px;
}
/*
  THE UNREAD BADGE SWAPS WITH ITS TAB (2026-09-22). On the inactive tab it is gold with a dark number,
  which is what makes it carry across a muted strip. On the SELECTED tab that is gold on gold, so it
  turns over: dark brown with the number in gold. The count stays legible whichever tab you are on,
  and there is only one thing to remember — the badge is always the opposite of the tab beneath it.
*/
/*
  THE BADGE IS A CIRCLE UNTIL IT CANNOT BE (2026-09-22). One digit is a true circle — the height is
  fixed and min-width matches it, so 1 to 9 come out exactly round with the number centred. Two digits
  widen on the padding alone and keep the same height, and above 99 it says "99+" (unreadLabel in
  gsb-social-panel.js), which is as wide as it is allowed to get.

  box-sizing: border-box is what makes min-width mean the whole badge rather than its text box; without
  it a single digit came out 18 wide by 16 high, which is a slightly squashed oval nobody could name.
  It is a flex box so the number is centred both ways rather than sitting on the text baseline.

  The same shape for both places a badge appears — the Messages tab and a conversation row — with only
  their colours kept separate below.
*/
.gsb-rail .tabs .n,
.gsb-rail .conv-row .n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  height: 20px;
  min-width: 20px;
  padding: 0 6px;
  border-radius: var(--gsb-radius-pill);
  font-weight: 800;
  font-size: 11.5px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.gsb-rail .tabs .n {
  background: var(--gsb-gold);
  color: var(--gsb-on-gold);
}
.gsb-rail .tabs button.on .n {
  background: var(--gsb-on-gold);
  color: var(--gsb-gold-bright);
}
.gsb-rail .panelmeta { padding: 8px 14px 0; margin: 0; font-size: 13px; color: var(--gsb-muted); font-weight: 600; }
.gsb-rail .panelmeta b { color: var(--gsb-green); font-weight: 700; }

/* Which tab is showing decides what the panel holds. */
.gsb-rail[data-tab="friends"] .convs,
.gsb-rail[data-tab="friends"] .thread,
.gsb-rail[data-tab="friends"] .splitter { display: none; }
.gsb-rail[data-tab="messages"] .friends-list,
.gsb-rail[data-tab="messages"] .friends-search,
.gsb-rail[data-tab="messages"] .friends-offline-toggle,
.gsb-rail[data-tab="messages"] .panelmeta { display: none; }
.gsb-rail[data-tab="messages"] .convs { flex: 0 1 auto; max-height: 42%; }
.gsb-rail .splitter { height: 1px; background: var(--gsb-line-strong); margin: 0; flex: 0 0 auto; }

/* the conversation list */
.gsb-rail .convs { list-style: none; margin: 0; padding: 6px 6px 8px; flex: 1; min-height: 0; overflow: auto; }
.gsb-rail .conv-row {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  width: 100%;
  /* 5px rather than 8: the conversation list is read as a list, and the rows were further apart
     than the two lines inside each of them. */
  padding: 5px 9px;
  border-radius: 12px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.gsb-rail .conv-row:hover { background: rgb(var(--gsb-gold-rgb) / .08); }
.gsb-rail .conv-row.sel { background: rgb(var(--gsb-gold-rgb) / .12); }
.gsb-rail .conv-row .tx { flex: 1; min-width: 0; }
.gsb-rail .conv-row .tx b { display: block; font-size: 15px; font-weight: 700; }
.gsb-rail .conv-row .tx small { display: block; color: var(--gsb-muted); font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; }
.gsb-rail .conv-row .n { background: var(--gsb-gold); color: var(--gsb-on-gold); align-self: center; }
.gsb-rail .convs-empty { padding: 18px 14px; color: var(--gsb-muted); font-size: 14px; text-align: center; }
.gsb-rail .friends-search { padding: 10px 14px; border-bottom: 1px solid var(--gsb-line); }
/*
  THE FRIENDS LIST IS A LIST (2026-09-22), in its own inset panel rather than a column of separate
  pills. Recessed, because it is the thing you read before choosing; the rows sit a shade LIGHTER than
  the well they are in, which is what makes them read as entries in a list rather than as buttons, and
  a hairline separates one from the next. The search box and Show offline stay where they are, outside
  the panel — they act on the list, they are not part of it.
*/
.gsb-rail .friends-list {
  list-style: none;
  margin: 8px 10px 10px;
  padding: 4px;
  flex: 1;
  min-height: 0;
  overflow: auto;
  border-radius: 12px;
  background: var(--gsb-well);
  border: 1px solid var(--gsb-line);
  box-shadow: var(--gsb-shadow-inset);
}
/*
  FOLLOWING on the ones who do not follow back, FRIEND on the mutual ones (52.43).

  Deliberately NOT scoped under .gsb-rail, unlike everything else here: find-table's reserve list
  shows the same people and now carries the same tag, and the two lists sitting on one page saying
  different things about the same person was the bug (52.57). One rule both use beats a second class
  on every row of the panel for one appearance. Nothing else on the site uses the name.
*/
.friend-state { font-size: 10.5px; font-weight: 800; letter-spacing: .1em; flex: 0 0 auto; }
.friend-state.is-friend { color: var(--gsb-gold); opacity: .9; }
.friend-state.is-following { color: var(--gsb-muted); font-weight: 700; }
.gsb-rail .friend-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 9px;
  border-radius: 8px;
  /* A shade lighter than the well behind it: rows, not pills. */
  background: var(--gsb-fill-warm);
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.gsb-rail .friend-row + .friend-row { margin-top: 2px; }
.gsb-rail .friend-row:hover { background: rgb(var(--gsb-gold-rgb) / .12); }
.gsb-rail .friend-row.offline .friend-username { color: var(--gsb-muted); }
.gsb-rail .friend-avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  flex: 0 0 30px;
  display: grid;
  place-items: center;
  /*
    VISIBLE, not hidden (52.88). The online dot sits in the bottom-right corner and hangs a pixel
    outside the circle; clipping the avatar clipped the dot. The photograph still comes out round
    because gsb-friends-panel.js gives the <img> its own border-radius:50% inline — which is why
    table.html can do the same thing, and has all along.
  */
  overflow: visible;
  background: var(--gsb-avatar-plate);
  border: 1px solid var(--gsb-line);
  color: var(--gsb-gold-bright);
  font-family: var(--gsb-display);
  font-size: 15px;
  position: relative;
}
.gsb-rail .friend-avatar img { width: 100%; height: 100%; object-fit: cover; }
.gsb-rail .friend-username { flex: 1; min-width: 0; font-size: 15.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/*
  ABSOLUTE, so it is not a grid item (52.88). It used to carry `flex: 0 0 7px`, which does nothing to a
  grid item: .friend-avatar is a grid, so the dot became a second ROW under the content and was clipped.
  It only ever showed for the initials fallback, because those are a text node rather than an element,
  leaving the dot as the grid's only item. Anyone with a profile photograph appeared permanently offline.

  Positioned rather than laid out, so it does not care whether the avatar holds an <img> or a text node
  — which is the whole bug. The ring separates it from a photograph, as table.html's does.
*/
.gsb-rail .friend-online-dot {
  position: absolute;
  right: -1px;
  bottom: -1px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--gsb-green);
  border: 2px solid var(--gsb-panel);
}
/*
  Someone I only follow is muted; a mutual friend keeps full colour (52.87). The class is put on the
  row by gsb-social-panel.js's decorateRow, beside the FRIEND / FOLLOWING tag, so the two cannot drift
  apart — and so neither reaches table.html or the Hand Editor, which show no tag.

  THE ONLINE DOT IS NOT TOUCHED, and that is the reason this is written as three separate properties
  rather than one filter on .friend-avatar. The dot is a CHILD of the avatar, a filter on an element
  applies to its whole subtree, and a child cannot opt out — so a filter there would grey the dot too.
  A grey "online" dot would be saying something false about a different thing.

  The photograph is desaturated; the initials fallback cannot be (same reason), so it is muted with
  its own background and colour instead.
*/
.gsb-rail .friend-row.is-following .friend-avatar img { filter: grayscale(1); opacity: .55; }
.gsb-rail .friend-row.is-following .friend-avatar { background: var(--gsb-avatar-plate-quiet); color: var(--gsb-muted); }
.gsb-rail .friend-row.is-following .friend-username { color: var(--gsb-muted); }

.gsb-rail .friends-empty { padding: 14px; color: var(--gsb-muted); font-size: 14px; text-align: center; }
.gsb-rail .friends-section-label { padding: 8px 12px 4px; color: var(--gsb-muted); font-size: 11.5px; letter-spacing: .12em; text-transform: uppercase; }
.gsb-rail .friends-offline-toggle { display: flex; align-items: center; gap: 8px; padding: 9px 14px; border-top: 1px solid var(--gsb-line); color: var(--gsb-muted); font-size: 14.5px; }

/*
  The conversation. Who said what is the side and the tint, not a name on every line, so the strip
  above it is the only place the name appears — and it is one tight line.
*/
.gsb-rail .thread { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.gsb-rail .thread > header { display: flex; align-items: center; gap: 10px; padding: 7px 14px; border-bottom: 1px solid var(--gsb-line); flex: 0 0 auto; }
.gsb-rail .thread > header h2 { margin: 0; font-size: 16px; letter-spacing: .01em; }
.gsb-rail .thread > header .gsb-meta { font-size: 12px; flex: 0 0 auto; }
.gsb-rail .thread-who { font: inherit; color: inherit; padding: 0; cursor: pointer; text-align: left; }
.gsb-rail .thread-who:hover { color: var(--gsb-cream); text-decoration: underline; }
.gsb-rail .thread-who:disabled { cursor: default; text-decoration: none; }
/* A page with no profile popup has nowhere for the name to go, so it does not offer (52.54). */
.gsb-rail .thread-who-inert, .gsb-rail .thread-who-inert:hover { cursor: default; text-decoration: none; color: inherit; }
/*
  THE CONVERSATION IS COMPACT AND CONTINUOUS (2026-10-02). 4px between bubbles instead of 8, and
  the bubbles themselves 5px/10px instead of 8px/12px with 1.3 of leading instead of 1.45 - so a
  thread reads as one exchange rather than as a column of separate cards. There was never a
  divider here to remove: the bubbles ARE the separation, and the space between them was doing
  the job twice.
*/
.gsb-rail .thread .lines { padding: 8px 14px; display: flex; flex-direction: column; gap: 4px; flex: 1; min-height: 0; overflow: auto; justify-content: flex-end; }
.gsb-rail .thread .thread-empty { color: var(--gsb-muted); font-size: 14px; margin: auto; text-align: center; padding: 10px 6px; }
.gsb-rail .bubble { max-width: 82%; padding: 5px 10px; border-radius: 14px; font-size: 14.5px; line-height: 1.3; }
.gsb-rail .bubble.them { background: rgb(var(--gsb-gold-rgb) / .10); border: 1px solid var(--gsb-line); align-self: flex-start; border-bottom-left-radius: 5px; }
.gsb-rail .bubble.me { background: var(--gsb-bubble-me); border: 1px solid var(--gsb-line-strong); align-self: flex-end; border-bottom-right-radius: 5px; }
.gsb-rail .bubble .when { display: block; color: var(--gsb-muted); font-size: 11px; margin-top: 1px; font-weight: 600; }
.gsb-rail .thread .compose { display: flex; gap: 8px; padding: 9px 12px; border-top: 1px solid var(--gsb-line); flex: 0 0 auto; }
.gsb-rail .thread .compose input { flex: 1; min-width: 0; }
.gsb-rail .thread .compose button { padding: 0 18px; }
.gsb-rail .thread-error { padding: 0 14px 12px; min-height: 15px; color: var(--gsb-msg-error); }
.gsb-rail .friend-unread {
  font-size: 12px;
  background: var(--gsb-gold);
  color: var(--gsb-on-gold);
  font-weight: 700;
  border-radius: var(--gsb-radius-pill);
  min-width: 18px;
  text-align: center;
  padding: 0 5px;
}

/*
  Below this the panel is hidden, and the paddings that kept the page clear of it go with it. On
  home these three rules were written against .top and .shell, which the shell had renamed to
  .gsb-top and .gsb-shell — so the panel vanished and a 346 px gutter stayed behind (52.55).
*/
@media (max-width: 1180px) {
  .gsb-rail { display: none; }
  .gsb-top.gsb-has-rail { margin-right: 0; }
  .gsb-shell.gsb-has-rail { padding-right: 0; }
}

/* ---------- small print ---------- */

.gsb-note { color: var(--gsb-muted); font-size: 12.5px; }
.gsb-scroll { overflow: auto; scrollbar-width: thin; scrollbar-color: var(--gsb-line-strong) transparent; }
.gsb-scroll::-webkit-scrollbar { width: 9px; }
.gsb-scroll::-webkit-scrollbar-thumb { background: var(--gsb-line-strong); border-radius: var(--gsb-radius-pill); }


/* ==============================================================================================
   THE PROFILE DIALOG (2026-09-25)
   ==============================================================================================
   gsb-profile-edit.js builds it; these are the few pieces the theme's .gsb-dialog does not already
   cover — the two-column rows, the avatar grid and the small grey notes.
   ============================================================================================== */
.gsb-pe-dialog { --gsb-dialog-width: 620px; }
.gsb-pe-body { display: flex; flex-direction: column; gap: 16px; }

.gsb-pe-section { border: 1px solid var(--gsb-line); border-radius: 12px; padding: 12px 14px 14px; }
.gsb-pe-section-title {
  margin: 0 0 10px; font-size: 12px; font-weight: 800; letter-spacing: .12em;
  text-transform: uppercase; color: var(--gsb-gold);
}

.gsb-pe-row { display: flex; gap: 12px; flex-wrap: wrap; }
.gsb-pe-row > .gsb-pe-field { flex: 1 1 180px; }
.gsb-pe-field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }
.gsb-pe-label { font-size: 12px; color: var(--gsb-muted); }
.gsb-pe-about { resize: vertical; min-height: 60px; }

.gsb-pe-fixed { display: flex; align-items: baseline; gap: 8px; margin: 2px 0 12px; }
.gsb-pe-note { font-size: 12px; color: var(--gsb-muted); line-height: 1.5; margin-top: 6px; }

.gsb-pe-avatars { display: grid; grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); gap: 8px; }
.gsb-pe-avatar {
  width: 100%; aspect-ratio: 1; padding: 0; overflow: hidden; cursor: pointer;
  border: 2px solid var(--gsb-line); border-radius: 12px; background: var(--gsb-fill-sunken);
  color: var(--gsb-muted); font: inherit; font-size: 12px; font-weight: 700;
}
.gsb-pe-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gsb-pe-avatar:hover { border-color: var(--gsb-line-strong); }
.gsb-pe-avatar.chosen { border-color: var(--gsb-gold); box-shadow: 0 0 0 2px rgb(var(--gsb-gold-rgb) / .28); }

/*
  A picture of your own (2026-09-25), below the twelve stock avatars. One tile the same size and
  shape as those, so the choice reads as thirteen options rather than a grid plus a form; it is
  outside .gsb-pe-avatars only because it is not one of the twelve and has a note of its own.
*/
.gsb-pe-upload { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.gsb-pe-upload .gsb-pe-avatar-upload { width: 62px; flex: 0 0 62px; display: grid; place-items: center; }
.gsb-pe-upload .gsb-pe-note { margin-top: 0; }
.gsb-pe-upload-plus { font-size: 22px; font-weight: 400; line-height: 1; color: var(--gsb-muted); }
.gsb-pe-avatar-upload:disabled { opacity: .55; cursor: progress; }

.gsb-pe-msg { font-size: 12.5px; color: var(--gsb-muted); }
.gsb-pe-msg.bad { color: var(--gsb-danger-text); }

/*
  What a player wrote about themselves, on their popup (2026-09-25). Its own panel, matching the four
  field boxes beside it rather than the quoted-aside it started as: it sits between them and the
  buttons, so it should look like it belongs to the card rather than to the person's voice.
*/
.gsb-profile-about {
  margin: 0 0 14px;
  padding: 11px 13px;
  border: 1px solid var(--gsb-line);
  border-radius: 12px;
  background: var(--gsb-fill-faint);
  color: var(--gsb-cream);
  font-size: 13.5px;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ==========================================================
   TEAM MATCH INVITATION (2026-09-27)

   The dialog lives in gsb-shell.js so it reaches somebody who is already sitting at a table, which
   means its few styles belong here rather than in any one page. Everything structural - the layer,
   the panel, the head and foot - is the shared dialog vocabulary above; this is only the list of
   facts inside it.
   ========================================================== */
.gsb-match-invite-facts{
  display:grid;grid-template-columns:auto 1fr;gap:7px 16px;margin:0;
}
.gsb-match-invite-facts dt{
  color:var(--gsb-muted);font-size:12px;font-weight:800;
  letter-spacing:.05em;text-transform:uppercase;align-self:center;
}
.gsb-match-invite-facts dd{
  margin:0;color:var(--gsb-cream);font-size:14px;font-weight:650;
}
/*
  No close button and no click-outside: eight people are waiting on this answer. Accept and
  Decline are the only ways out, so the backdrop must not look like a third.
*/
.gsb-match-invite .gsb-dialog{cursor:default}

/*
  The director's pane in the shared rail. Everything else in the panel is switched by the panel's
  own data-tab, so this follows the same rule rather than inventing one.
*/
/*
  THE DIRECTOR PANE, ON THE ELEMENT THAT ACTUALLY CARRIES data-tab (2026-09-30).

  These eight rules were written against `.gsb-panel.social[data-tab="director"]`. That section
  exists - it is the rail's inner panel - but `data-tab` is set on its PARENT, the
  `<aside class="gsb-rail" id="socialPanel">`, by showTab(). So the attribute and the selector
  were one element apart and none of this ever matched: the pane stayed display:none and the
  friends list stayed up, whatever tab was chosen.

  Written the way the friends and messages states beside them already are, which is
  `.gsb-rail[data-tab="..."]`.
*/
.gsb-rail .director-pane{display:none;padding:14px 2px;color:var(--gsb-muted);font-size:12.5px;line-height:1.5}
.gsb-rail[data-tab="director"] .director-pane{display:block}
.gsb-rail[data-tab="director"] .convs,
.gsb-rail[data-tab="director"] .thread,
.gsb-rail[data-tab="director"] .splitter,
.gsb-rail[data-tab="director"] .friends-search,
.gsb-rail[data-tab="director"] .friends-list,
.gsb-rail[data-tab="director"] .friends-offline-toggle,
.gsb-rail[data-tab="director"] .panelmeta{display:none}
.gsb-panel.social .director-note{margin:0 0 10px;color:var(--gsb-muted);font-size:12.5px;line-height:1.5}
.gsb-panel.social .director-actions{display:flex;flex-direction:column;gap:8px}
.gsb-panel.social .director-actions .gsb-btn{width:100%;min-height:38px;justify-content:flex-start;padding:0 14px}

/*
  The waiting state, inside the same invitation dialog. Three dots that breathe rather than a
  spinner: nothing here is loading, somebody is being waited for, and a spinner would suggest the
  page was busy when it is simply patient.
*/
.gsb-match-wait{margin-top:16px;padding-top:14px;border-top:1px solid var(--gsb-line)}
.gsb-match-wait-bar{display:flex;gap:7px;justify-content:center;margin-bottom:11px}
.gsb-match-wait-bar i{
  width:9px;height:9px;border-radius:50%;background:var(--gsb-gold);opacity:.3;
  animation:gsbMatchWaitPulse 1.25s ease-in-out infinite;
}
.gsb-match-wait-bar i:nth-child(2){animation-delay:.18s}
.gsb-match-wait-bar i:nth-child(3){animation-delay:.36s}
@keyframes gsbMatchWaitPulse{0%,100%{opacity:.25;transform:scale(.86)}45%{opacity:1;transform:scale(1)}}
@media (prefers-reduced-motion: reduce){
  .gsb-match-wait-bar i{animation:none;opacity:.6}
}
.gsb-match-wait-line{margin:0 0 5px;text-align:center;color:var(--gsb-cream);font-size:13.5px;line-height:1.45}
.gsb-match-wait-note{margin:0;text-align:center;color:var(--gsb-muted);font-size:11.5px;font-variant-numeric:tabular-nums}
/* Nothing to press while waiting, so the foot must not keep their space. */
.gsb-match-invite .gsb-dialog-foot button[hidden]{display:none!important}

/* ==========================================================
   THE SEAT PICKER — a felt with four seats round it
   Moved here from find-table.html on 2026-09-28.

   Three things draw it now: Create a Table (one felt), Create a Team Match (two side by side, and
   it adds its own sizing under .tm-team), and the team match invitation's Show lineup, which is
   drawn by gsb-shell.js on every page and so could not reach a rule living in find-table.html.

   The four seats are positioned against .seat-table, a square held by aspect-ratio, each nudged
   outward by a 55% translate so it straddles the edge.
   ========================================================== */
.seat-picker{display:flex;justify-content:center;padding:22px 0 18px}
/*
  The felt stays green, and is the one green left on these pages. It is not palette — it is the
  bridge table itself, and green felt is part of the visual identity (CLAUDE.md section 4).
*/
.seat-table{position:relative;width:min(360px,72vw);aspect-ratio:1;background:var(--gsb-felt);border:2px solid var(--gsb-felt-edge);border-radius:var(--gsb-radius)}
.seat-center-copy{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;font-size:20px;line-height:1.25;color:var(--gsb-on-felt)}
.seat-btn{
  position:absolute;width:112px;min-height:58px;z-index:2;
  border-radius:var(--gsb-radius);border:1px solid var(--gsb-line-strong);
  background:var(--gsb-raise);color:var(--gsb-cream);font-weight:900;font-size:20px
}
.seat-btn:hover:not(.reserved){border-color:var(--gsb-gold)}
.seat-btn.selected{background:var(--gsb-gold-grad);border-color:var(--gsb-gold-edge);color:var(--gsb-on-gold);box-shadow:0 0 0 4px rgb(var(--gsb-gold-rgb) / .16)}
.seat-n{left:50%;top:0;transform:translate(-50%,-55%)}
.seat-s{left:50%;bottom:0;transform:translate(-50%,55%)}
.seat-w{left:0;top:50%;transform:translate(-55%,-50%)}
.seat-e{right:0;top:50%;transform:translate(55%,-50%)}
.seat-btn.reserved{background:var(--gsb-seat-taken);color:var(--gsb-cream);border-color:var(--gsb-seat-taken-edge)}
/*
  WIDTH:100% IS THE CENTRING (2026-09-29).

  `display:block` centres a robot's name and did NOT centre a person's, and the two differ only
  in their element: the robot's is a <span> and a person's became a <button> when the names were
  made pressable. A button is a form control, so it sizes to fit-content even at display:block -
  a block-level box the width of its own text, sitting at the start of the row, while the pill's
  text-align:center has nothing left to centre. Measured across both widgets: the span came out
  124px wide and dead centre, the buttons 25-115px and 16 to 49px left of it, worst on the
  shortest name.

  Filling the row hands the alignment back to text-align, which the pill already sets.
*/
.seat-reserve-name{display:block;width:100%;font-size:8px;line-height:1.05;margin-top:2px}
/* A robot reads as a robot, not as a person with an odd name. */
.seat-btn.robot{background:var(--gsb-seat-robot);border-color:var(--gsb-seat-robot-edge);color:var(--gsb-seat-robot-text)}
@media(max-width:700px){
  .seat-btn{width:88px;min-height:52px}
  .seat-center-copy{font-size:17px}
}

/* The invitation's own button, under the facts and above the waiting block. */
.gsb-match-lineup-open{display:flex;justify-content:center;margin-top:14px}
.gsb-match-lineup-open button[hidden]{display:none!important}

/* ==========================================================
   SHOW LINEUP — the invitation's read-only view of both teams (2026-09-28)

   It reuses the seat picker above rather than drawing its own table, so an invited player sees
   the same felt the match was built on. Two differences, and both are the point:

     - NOTHING IS CLICKABLE. The seats are <div>s, not buttons, and every hover and pointer state
       is turned off below. This is a thing to read, not a thing to use, and a seat that lit up
       under the cursor would be promising something that does not happen.
     - THE INVITED PLAYER'S OWN SEAT carries the site's gold outline, so they can find themselves
       in eight names without reading all eight.
   ========================================================== */
/*
  THE THREE NUMBERS THIS POPUP IS MADE OF sit on the LAYER, not on the grid inside it, so the
  dialog's own width can be computed from them too (2026-09-29). They were on .gsb-lineup-teams,
  which is a descendant of the dialog - visible to the felt and the pills and not to the box
  that has to be wide enough for two of them. gsb-shell.js carried the answer as a typed 870px
  instead, and when the felt and the pills were stepped up it silently stopped being true: the
  grid stacked the teams and the dialog stayed 870px wide around one 586px panel.
*/
.gsb-lineup-layer{
  z-index:12000;
  --gsb-lineup-felt:400px;                                              /* the green square */
  --gsb-lineup-pill-max:156px;                                          /* the widest a pill may get */
  --gsb-lineup-pill-out:calc(var(--gsb-lineup-pill-max) * .55 + 4px);   /* what a side pill hangs out by */
  --gsb-lineup-panel:calc(var(--gsb-lineup-felt) + 2 * var(--gsb-lineup-pill-out) + 2px);
  /*
    Two panels, the grid's 18px gutter, the dialog body's 18px each side, and 4px of slack -
    without the slack this comes out to the exact width the grid needs, and an exact fit is one
    rounded pixel or one scrollbar away from stacking.
  */
  --gsb-dialog-w:calc(2 * var(--gsb-lineup-panel) + 18px + 36px + 4px);
}

/*
  THE SEATS SIT OUTSIDE THE FELT, AND THE PANEL HAS TO PAY FOR IT (2026-09-28).

  Every pill straddles the felt's edge - translate(-55%) and friends - so a side pill hangs
  OUT of the table by 55% of its own width and a top or bottom one by 55% of its height. The
  first cut gave the panel 12px of padding against a 41px overhang, so Home's West escaped the
  popup on the left, Away's West landed on Home's East in the gutter, Away's East was cut off at
  the right edge, and both South pills sat on the panel's bottom border.

  ONE VARIABLE DECIDES IT. --gsb-lineup-pill-max is the widest a pill may get, and the panel's
  side padding is COMPUTED from it rather than typed next to it, so the two cannot drift apart
  when either is retuned. Change the cap and the panel follows.

  WHY 156px (132px until 2026-09-29, when the name went from 11px to 13px), AND WHAT IT REALLY
  CLEARS. A username is 6-16 characters (validUsername: /^[A-Za-z0-9_.-]{6,16}$/, and the column
  is VARCHAR(16)). Measured at the size the name is actually drawn - 13px Manrope 900, in
  scripts/seat-diagram-rig.html - an ordinary sixteen-character name comes to 115px against the
  156px pill's 138px of inner width, so every real name fits with room to spare.

  A PATHOLOGICAL name does not, and it is not cut: sixteen W come to 206px and sixteen m to
  191px, and the pill's overflow is visible, so such a name is drawn in full and hangs about 26px
  out of each side of its pill. Making the cap clear it would take a 222px pill, which through
  the arithmetic below means a 654px panel and a 1366px dialog - too wide for the screens this
  runs on, to protect a name nobody has.

  The note here used to claim that 132px cleared sixteen W "at the 8px the name is drawn at".
  Neither number was right: the name was 11px, and sixteen W at 11px is about 174px, so the old
  cap did not clear the case it said it did either. The difference is that this one is measured.

  THE CAP IS THE REAL WIDTH. .gsb-lineup-team .seat-btn sets a width AND a max-width of this, so
  raising the width alone does nothing - which is what the first draft of the 2026-09-29 step up
  did, and the pills came out exactly the size they already were.
*/
.gsb-lineup-teams{
  /*
    The numbers are on .gsb-lineup-layer above, so the dialog can be sized from them as well.
    A host that draws this grid outside that layer (scripts/seat-diagram-rig.html) has to supply
    them itself, and gets a 47px felt if it forgets - which is the rig being wrong, not the page.
  */
  display:grid;
  /*
    auto-fit against the panel's OWN width, so the moment two will not fit, Home stacks above
    Away by itself. A hand-set breakpoint here would be a fourth number to keep in step with the
    three above, and it is the one that would go stale.
  */
  grid-template-columns:repeat(auto-fit,minmax(min(100%,var(--gsb-lineup-panel)),1fr));
  gap:18px;align-items:start;
}
.gsb-lineup-team{
  padding:12px var(--gsb-lineup-pill-out) 4px;
  border:1px solid var(--gsb-line);border-radius:var(--gsb-radius);
  background:var(--gsb-fill-sunken-soft);
}
.gsb-lineup-name{
  margin:0 0 2px;text-align:center;
  color:var(--gsb-gold-bright);
  font:800 16px/1.3 var(--gsb-ui);letter-spacing:.02em;
}
/*
  The two felts sit side by side, so each is smaller than a single one. The picker's vertical
  padding carries the North and South overhang - 55% of the pill's 42px height, so 24px is the
  floor and 26 leaves a little air. 60vw was wrong the moment there were two of them: on a narrow
  screen it measured the WINDOW while the felt lives in half a dialog.
*/
/*
  The same, for the lineup: 26px was enough for a 42px robot pill and not for a person's, so
  North sat on the team's name. Sized from the taller of the two now that they are one size.
*/
.gsb-lineup-team .seat-picker{padding:52px 0}
.gsb-lineup-team .seat-table{width:min(var(--gsb-lineup-felt),100%)}
/*
  A DIV, NOT A BUTTON, so it has to be told to centre its own text - a button did that for free
  and is the one thing lost by making these read-only.
*/
/*
  THE PILL GROWS TO ITS NAME rather than the name shrinking to the pill. 74px is the floor, so a
  robot or a short name looks exactly as it did; a long one widens the pill and nothing else.
*/
/*
  NOT A FLEX COLUMN ANY MORE (2026-09-29), and that one word is the whole reason a person's seat
  was twice a robot's height here while the identical change worked in the scorecard.

  A FLEX CONTAINER BLOCKIFIES ITS CHILDREN. `display:inline-block` on the avatar was therefore
  ignored in this widget and honoured in the other, where the pill is an ordinary block - so the
  avatar took a line of its own here, the pill grew to 85px against a robot's 42, and the branch
  that "fixed the avatar" looked as though it had never run. Worth remembering: the same
  declaration can work in one place and do nothing in another, silently.

  Block flow, centred text, and the avatar sits beside the seat letter as it was always meant to.
*/
.gsb-lineup-team .seat-btn{
  /*
    ONE WIDTH, not max-content. Sizing to content made a person's pill wider than a robot's -
    "not-so-lazy" plus an avatar against the word "Robot" - so the four seats round a felt were
    two different shapes again. Fixed, and wide enough for the 16 characters a username may be.
  */
  width:154px;max-width:var(--gsb-lineup-pill-max);
  /*
    ONE HEIGHT FOR BOTH. A person's pill is a line taller than a robot's, because its first line
    carries an avatar beside the letter - so the minimum is the taller of the two and the robot
    grows to meet it rather than the person shrinking out of shape.
  */
  min-height:74px;font-size:23px;padding:4px 12px;
  display:block;
  text-align:center;
}
/* The name is one line, whole, whatever it is. */
.gsb-lineup-team .seat-reserve-name{white-space:nowrap;font-size:13px}
/*
  READ-ONLY, said in CSS as well as in the markup. The seats are already divs, so none of this is
  load-bearing on its own — it is here so that a later change back to <button> cannot quietly
  reintroduce the hover states.

  IT DID ITS JOB ON THE ONE THING THAT WAS MEANT TO BE CLICKABLE (2026-09-29). The names became
  buttons that open a player's profile, and `pointer-events:none` here swallowed every click on
  them: measured, the hit test at the middle of the name landed on `.seat-table` behind it. So a
  pill that has somebody to open opts back in, below, and NOTHING ELSE CHANGES - the hover states
  stay off, and a robot's pill and an empty one stay exactly as inert as they were.
*/
.gsb-lineup-team .seat-btn,
.gsb-lineup-team .seat-btn:focus{
  cursor:default;pointer-events:none;
}
.gsb-lineup-team .seat-btn.gsb-who-open{ pointer-events:auto; cursor:pointer; }
/*
  ONLY THE HOVER IS UNDONE, not the border. The first cut set border-color on the resting rule
  too, which beat .seat-btn.reserved and .seat-btn.robot on equal specificity by being later in
  the file - so in this popup, and only here, a player's pill lost its red edge and a robot's
  lost its blue one. Undoing the base hover and nothing else leaves every state its own colour.
*/
.gsb-lineup-team .seat-btn:hover:not(.reserved){border-color:var(--gsb-line-strong)}

/*
  A FILLED SEAT IS A PERSON, NOT A BLOCKED SEAT (2026-09-29).

  Human seats used to take .seat-btn.reserved, which is the seat picker's dark red - correct on
  find-table, where the question is which seats are free, and wrong in a lineup, where every seat
  is filled and the question is who is in it. So .filled is the panel's own surface with the
  theme's gold edge, and the seat letter, the avatar and the name sit in it.

  The robot keeps .robot: that is a fact about the seat, not about whether you may have it.
*/
/*
  A MATCHED PAIR WITH .robot, WHICH IT WAS NOT (2026-09-29).

  .filled pointed at --gsb-panel-2, which FOLLOWS THE THEME, while .robot pointed at
  --gsb-seat-robot, which is a fixed dark blue. On a dark theme both came out darkish and nobody
  noticed; on a light one the person was #F2F0E9 and the robot #274A63 - a tall pale card beside
  a short dark pill, two designs rather than one.

  --gsb-seat-human is its own role now, declared beside --gsb-seat-robot and moving with it, so
  the two seats are two shades of one thing on every theme.
*/
.seat-btn.filled{
  background:var(--gsb-seat-human);
  color:var(--gsb-seat-human-text);
  border-color:var(--gsb-seat-human-edge);
}
.gsb-lineup-team .seat-btn.filled{ padding:6px 8px; }
/*
  Inline with the seat letter, for the reason the scorecard's is: on its own line it grew the
  pill and these pills straddle the felt by a share of their own height, so a person's seat
  covered the corner of the green where a robot's did not.
*/
.gsb-lineup-team .seat-avatar{
  display:inline-block;
  vertical-align:middle;
  width:30px;height:30px;min-width:30px;
  margin:0 0 0 7px;
  font-size:12px;
  line-height:30px;
}
.gsb-lineup-team .seat-btn:hover:not(.reserved):not(.filled):not(.robot){border-color:var(--gsb-line-strong)}
/* Your own seat, and the only gold on either felt. */
.gsb-lineup-team .seat-btn.yours{
  border-color:var(--gsb-gold);
  box-shadow:0 0 0 3px rgb(var(--gsb-gold-rgb) / .20);
}
/* No breakpoint here on purpose - see the auto-fit note on .gsb-lineup-teams above. */

/* ==================================================================================================
   VULNERABILITY, SAID ON THE AUCTION PANEL AND ON THE CARDS (2026-09-30)
   ================================================================================================== */

/*
  THE VULNERABLE SIDE'S SEAT LETTERS: a red box with the letter in black, on every auction panel
  on the site. It stays for the whole hand, not only while the auction is running - which is the
  point of it, since after the auction is exactly when you need to remember who is vulnerable.

  !important, deliberately and narrowly. This is a state marker that has to win in eight panels
  across six files, several of which colour their own header cells and one of which
  (#rightAuctionView #auctionPanel .auction th) already does so with !important of its own at a
  specificity no shared class can reach. The alternative is eight per-panel overrides that each
  have to be found again the next time a panel is added. Only the two declarations that carry the
  meaning are forced; everything else about the cell is left to the panel.
*/
.auction-seat-vul {
  background: var(--gsb-vul-red) !important;
  color: #000 !important;
}

/*
  THE HEADER IS ONE BAR, NOT FOUR TILES (2026-10-18).

  `border-radius: 4px` was part of the rule above, so every VULNERABLE cell was rounded on all
  four corners - including the two inner ones, which no cell has an outside at. Beside a square
  neighbour that read as a gap, and beside another red one it put a notch in the middle of what
  should be a single red bar.

  The radius belongs to the ROW, not to the cell: only the two ends of the header have an
  outside, so only they are rounded, and every edge between two cells is square. Two cells of
  the same colour then join into one shape, and where red meets white the join is a straight
  line.

  ON [data-auction-seat], which is the hook paintAuctionVulnerability already keys on, so this
  reaches every auction panel on the site - the table, the Bidding Room, the lesson pages, the
  board analysis and the replay - on the phone and on a computer alike, without naming one of
  them. --gsb-auc-head-radius lets a panel whose own corners are rounder say so (the shared
  panel's are 6 and, on a board, 9: gsb-auction-panel.css); 4 is what the cells had.
*/
thead tr th[data-auction-seat]{border-radius:0}
thead tr th[data-auction-seat]:first-child{border-top-left-radius:var(--gsb-auc-head-radius,4px)}
thead tr th[data-auction-seat]:last-child{border-top-right-radius:var(--gsb-auc-head-radius,4px)}

/*
  THE CARDS OF A VULNERABLE HAND, DURING THE AUCTION ONLY.

  A TINT ON THE CARD, not a glow around it (2026-09-30). The first cut was an outward
  drop-shadow, and on the hands that matter most it was invisible: `.hidden-card` is
  `overflow:hidden`, so a shadow painted outside the image's box was clipped at the card's edge.
  The opponents' backs - the whole point of showing vulnerability during the auction - never
  glowed at all, while the viewer's own hand did, because `.card` does not clip. A tint is drawn
  INSIDE the card, so no ancestor can clip it away.

  WHY AN OVERLAY AND NOT A BLEND. Multiplying the image onto a pale red background tints white
  paper beautifully and leaves the ink alone - but a card BACK is saturated dark blue, and
  multiplying that by a pale colour barely moves it: the backs came out looking untinted beside
  an obviously pink face-up hand. A flat wash over the card reaches both, and at 20% the black
  pips, the ranks and the red suits are all still themselves.

  26% AND NOT 20%, WHICH IS ABOUT THE BACKS. At 20 the face-up hand was unmistakable and the
  backs were not: a saturated dark blue barely moves under a fifth of a wash, and beside an
  untinted fan it read as "slightly duller", which is not a signal. Compared side by side at 20,
  26 and 32 per cent against an untinted fan, 26 is where the backs are plainly a different
  colour and the faces have not gone soft.

  POSITION:RELATIVE IS SAFE HERE, and it had to be checked rather than assumed. `.hidden-card`
  is normally static, and it carries an inline z-index of 20+i that is therefore inert - so
  positioning it switches twenty z-indexes on at once. They are assigned in document order and
  the fan already paints in document order, so the stacking is identical; the overlapping fan
  looks the same before and after.
*/
.gsb-vul-tint .card,
.gsb-vul-tint .hidden-card {
  position: relative;
}
/*
  THE VULNERABLE SIDE'S SEAT PLATES (2026-09-30).

  The same rule as the auction header and for the same span - the whole hand, not just the
  auction - so a player can see who is vulnerable while the cards are being played. Every seat
  plate on the site is classed by renderSeatLayout, which the live table calls and the analysis
  surface calls for the Hand Viewer, the board popups, replays and the Hand Editor, so there is
  one place to be right and no page keeps its own copy.

  WHY THE GREEN IS REPLACED RATHER THAN TINTED. The plate is a translucent dark-green gradient
  over the felt; washing red over it gives a muddy brown, so this is its own gradient in the
  vulnerable red, built the same way - two stops, the same direction, the same alpha - so the
  plate keeps its depth and only its hue changes.

  THE NAME STAYS WHITE, which is what it already was, and against this red it measures 5.4:1 -
  above the 4.5:1 normal-text bar, and measured rather than guessed: the first draft of this
  note claimed 6.1 from memory and the probe answered 5.4. The letter, the state line and the profile
  picture are untouched, and .seatbox is still the button it was: nothing here sets
  pointer-events, so a plate that opened a profile still opens it.

  (0,3,0) beats the .seatbox rules in table.html, which are all (0,1,0) or (0,2,0), and the
  turn glow and the `mine` outline are box-shadow and outline, so both still read on top.
*/
.seat.seat-vulnerable .seatbox {
  /*
    !important ON THE BACKGROUND ONLY, AND ONLY BECAUSE OF THE TURN (2026-09-30).

    `.seatbox.turn` sets an olive-green background with !important, so on a vulnerable player's
    own turn the red simply vanished and came back when the turn passed - the one moment the
    plate is being looked at hardest. An !important declaration beats a plain one however
    specific the plain one is, so this has to answer in kind; at (0,3,0) against the turn rule's
    (0,2,0) it then wins, and the red stays.

    NOTHING ELSE IS FORCED, deliberately. The turn's border-color and its inset glow are left to
    it, so a vulnerable player to play gets the red plate WITH the usual yellow ring and inner
    glow on top of it, and the own-seat blue outline sits outside both. All three read at once,
    which is the point: they are three different facts.
  */
  background: linear-gradient(180deg, rgb(var(--gsb-vul-red-rgb) / .90), rgb(var(--gsb-vul-red-rgb) / .78)) !important;
  border-color: rgb(var(--gsb-vul-red-rgb) / .95);
}

.gsb-vul-tint .card::after,
.gsb-vul-tint .hidden-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(var(--gsb-vul-red-rgb) / .26);
  border-radius: inherit;
  pointer-events: none;
}


/*
  A SEATED PLAYER HAS NO WAY OFF THE TABLE IN THE TOP BAR (2026-09-30).

  The nav's links go, and only those. The brand stays, and so does the whole right-hand cluster -
  the search, the bell, the gear and the avatar menu - because Sign out lives in there and must
  remain reachable: the rule is "do not wander off mid-hand", not "you are locked in".

  ON TABLE.HTML ONLY, and set by that page. It starts on whenever the page is opened with a room
  in the address, so a seated player never sees the links they are about to lose; the page takes
  it off again the moment a snapshot says this viewer holds no seat, which is every kibitzer,
  every director and anybody watching. See the note beside seatedTopBar() in table.html.
*/
body.seated-no-nav .gsb-topnav { display: none !important; }

/* ==========================================================================
   THE DIRECTOR'S TABLES LIST, AND THE WATCHER'S POPUP (2026-09-30)

   Two layers, drawn by gsb-director-pane.js over whichever page the rail is on. The list is one
   LINE per table - name, four seats, three buttons - and the live table opens over it.

   THE LINE IS A GRID THAT BECOMES TWO AND THEN THREE ROWS, rather than three sets of markup.
   Measured widths for the widest realistic line: a table name wants ~200px, four usernames want
   ~92px each, and Go to / View round score / View all scores come to ~330px with their gaps -
   about 920px, which fits a 1180px dialog at 1280px of screen and does not fit an iPad. So the
   breakpoints below are where those measurements stop being true, not round numbers.
   ========================================================================== */

.gsbd-list-layer { z-index: 170; }
.gsbd-list-dialog { --gsb-dialog-w: 1180px; position: relative; }
.gsbd-list-body { display: flex; flex-direction: column; gap: 8px; }

.gsbd-match-name {
  margin: 10px 0 2px;
  font-family: var(--gsb-display);
  font-size: 15px;
  font-weight: 500;
  color: var(--gsb-gold);
}
.gsbd-match-name:first-child { margin-top: 0; }

.gsbd-row {
  display: grid;
  /*
    THE THREE COLUMNS ARE SET FROM MEASUREMENT, not from a feel for the proportions. At 1280px
    the dialog is 1180, the line has 1086px of content across after its padding and gaps, and
    the buttons take 340 of it whatever else happens. A 16-character username needs 110px at
    13px, so four seats need 4x(110+16) + 30 of gaps = 534 - which leaves the table's NAME about
    150 and no more. Hence the cap rather than a share: the name is the one thing here that
    ellipsises harmlessly ("Open Room" needs 70), and the usernames are what a director is
    reading. The first version split 1fr/1.8fr and clipped "abdulrahman_k" at 85px.
  */
  grid-template-columns: minmax(112px, 150px) minmax(0, 1fr) auto;
  gap: 8px 16px;
  align-items: center;
  padding: 10px 12px;
  border: 1px solid var(--gsb-line);
  border-radius: var(--gsb-radius);
  background: var(--gsb-well);
}
.gsbd-row-name { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.gsbd-row-title {
  font-weight: 600;
  font-size: 14px;
  color: var(--gsb-cream);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gsbd-row-board { font-size: 11.5px; color: var(--gsb-muted); }

.gsbd-row-seats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px 10px; min-width: 0; }
.gsbd-cell { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.gsbd-cell-pos {
  flex: 0 0 auto;
  width: 11px;
  font-size: 11px;
  font-weight: 700;
  color: var(--gsb-muted);
  letter-spacing: .04em;
}
.gsbd-cell-name {
  font-size: 13px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gsbd-cell.is-empty .gsbd-cell-name, .gsbd-cell.is-robot .gsbd-cell-name { color: var(--gsb-muted); }

/* A name that opens a menu. It is a <button>, so its own chrome comes off first. */
.gsbd-who {
  padding: 0;
  font: inherit;
  font-size: 13px;
  text-align: left;
  color: inherit;
  background: none;
  border: 0;
  border-bottom: 1px dotted rgb(var(--gsb-gold-rgb) / .5);
  cursor: pointer;
}
.gsbd-who:hover, .gsbd-who[aria-expanded="true"] { color: var(--gsb-gold-bright); border-bottom-color: var(--gsb-gold); }

.gsbd-row-acts { display: flex; gap: 8px; justify-content: flex-end; flex: 0 0 auto; }
/* Tighter than a standalone .gsb-btn: the 340px these three take is 340px the names do not. */
.gsbd-row-acts .gsb-btn { min-height: 32px; padding: 0 11px; white-space: nowrap; }
.gsbd-go { min-width: 84px; justify-content: center; }

/*
  The reason a "Go to" is refused, on its own row across the whole line. Never hidden and never
  only a tooltip: a disabled button with no explanation on screen is the bug this replaced.
*/
.gsbd-why { grid-column: 1 / -1; margin: 2px 0 0; font-size: 12px; line-height: 1.45; color: var(--gsb-muted); }
.gsbd-list-body > .gsbd-why { margin: 4px 2px; }

/*
  BELOW ~1120px the three columns no longer hold their measured minimums, so the buttons take a
  row of their own and the seats stay four across - a laptop at 1280 keeps one line, an iPad in
  landscape gets two.
*/
@media (max-width: 1120px) {
  .gsbd-row { grid-template-columns: 1fr; gap: 9px; }
  .gsbd-row-acts { justify-content: flex-start; flex-wrap: wrap; }
}

/*
  AND BELOW ~820px four usernames across cannot hold an ellipsis-free name either, so the seats
  go two by two. That is an iPad in portrait, and a phone.
*/
@media (max-width: 820px) {
  .gsbd-row-seats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .gsbd-row-acts .gsb-btn { flex: 1 1 auto; }
}

/* ---------- the live table over the list ---------- */

.gsbd-layer { z-index: 185; }          /* over the list it was opened from, under a profile card */
.gsbd-dialog { --gsb-dialog-w: 1560px; width: min(var(--gsb-dialog-w), 100%); }
.gsbd-body { padding: 14px 16px; }

/*
  THE STAGE IS THE SCALED FRAME'S BOX. The iframe is laid out at the table's own 1500x980 and
  transformed down; a transform does not change layout, so the stage's height is set in script to
  the scaled height and the frame is pinned to its top-left corner. Without both, the dialog
  reserves the full 980px and the table floats in the middle of it.
*/
.gsbd-stage { position: relative; overflow: hidden; width: 100%; }
.gsbd-frame {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  border: 0;
  border-radius: var(--gsb-radius);
  background: var(--gsb-ink);
}

/* ---------- the menu a username opens ---------- */

/*
  ON <body> AND FIXED TO THE WINDOW, not inside the dialog: a two-table list is a dialog 249px
  tall and a 144px menu fits inside it from neither direction. 190 puts it over the list (170)
  and over the table popup (185), and the table popup closes it on the way up anyway.
*/
.gsbd-menu {
  position: fixed;
  z-index: 190;
  min-width: 248px;
  padding: 7px;
  background: var(--gsb-panel);
  border: 1px solid var(--gsb-line-strong);
  border-radius: var(--gsb-radius);
  box-shadow: var(--gsb-shadow-dialog);
}
.gsbd-menu-who {
  margin: 2px 6px 6px;
  font-size: 11.5px;
  color: var(--gsb-muted);
  white-space: nowrap;
}
.gsbd-menu-item {
  display: block;
  width: 100%;
  padding: 7px 9px;
  font: inherit;
  font-size: 13px;
  text-align: left;
  color: inherit;
  background: none;
  border: 0;
  border-radius: var(--gsb-radius);
  cursor: pointer;
}
.gsbd-menu-item:hover:not(:disabled) { background: rgb(var(--gsb-gold-rgb) / .08); }
.gsbd-menu-item:disabled { color: var(--gsb-muted); cursor: default; }

/* ---------- the director's scorecard, and the picker's rows ---------- */

/*
  THE DIRECTOR'S QUEUED CALLS in the shell's rail (2026-10-01). table.html carries its own copy
  of these rules for its own sidebar, which is the arrangement the rest of the director pane
  already uses.
*/
.gsbd-calls{margin-top:12px;display:grid;gap:8px}
.gsbd-calls-head{
  margin:0;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--gsb-gold)
}
.gsbd-call{
  display:grid;gap:4px;padding:8px 9px;border-radius:10px;
  border:1px solid var(--gsb-line);background:rgb(var(--gsb-gold-rgb) / .05)
}
.gsbd-call-what{margin:0;font-size:12.5px;font-weight:700;color:var(--gsb-cream);line-height:1.35}
.gsbd-call-who{margin:0;font-size:11px;color:var(--gsb-muted);line-height:1.35}
.gsbd-call-why{margin:0;font-size:11px;color:var(--gsb-danger-text);line-height:1.3}
.gsbd-call-acts{display:flex;gap:6px;flex-wrap:wrap;margin-top:2px}
.gsbd-call-acts .gsb-btn{font-size:11px;padding:3px 9px}

.gsbd-card-table{width:100%;border-collapse:collapse;font-size:13px}
.gsbd-card-table th{
  text-align:left;
  padding:0 8px 7px;
  font-size:11px;
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--gsb-muted);
  border-bottom:1px solid var(--gsb-line);
  white-space:nowrap;
}
.gsbd-card-row td{padding:7px 8px;border-bottom:1px solid var(--gsb-line)}
.gsbd-card-row:last-child td{border-bottom:0}
.gsbd-card-row{cursor:pointer}
.gsbd-card-row:hover td,.gsbd-card-row:focus-visible td{background:rgb(var(--gsb-gold-rgb) / .08)}
/*
  Headings included (2026-09-30). `.gsbd-card-table th` sets text-align:left for every column, and
  it is more specific than a bare class - so a numeric heading needs the same specificity to sit
  over its own figures rather than over the far side of the column.
*/
.gsbd-card-score{text-align:right;font-variant-numeric:tabular-nums}
.gsbd-card-table th.gsbd-card-score{text-align:right}
/*
  --gsb-green, not an invented --gsb-good with a literal fallback. A var() with a fallback is
  valid CSS and would have shipped the hard-coded colour on every theme, silently, which is the
  shape of the tokenising bug this project already wears a scar from.
*/
.gsbd-card-score.won{color:var(--gsb-green)}
.gsbd-card-score.lost{color:var(--gsb-msg-error)}

/*
  The picker's Following rows. .ft-friend-row is find-table's class and this is a second place
  drawing it, which is the cost of not extracting that dialog - see the note beside pickPerson.
  Only what a bare <button> needs is set here; everything else comes from the page.
*/
.gsbd-pick-layer .ft-friend-row{
  display:flex;
  width:100%;
  align-items:center;
  gap:8px;
  padding:7px 9px;
  font:inherit;
  font-size:13px;
  text-align:left;
  color:inherit;
  background:none;
  border:0;
  border-radius:var(--gsb-radius);
  cursor:pointer;
}
.gsbd-pick-layer .ft-friend-row:hover{background:rgb(var(--gsb-gold-rgb) / .08)}
.gsbd-pick-layer .ft-friends-list{max-height:240px;overflow:auto;margin-top:6px}
.gsbd-pick-layer .gsb-input{width:100%}

/*
  Every director layer that opens from inside another one sits above it. They all carry
  .gsb-dialog-top as well, which is 12100 and beats this on specificity - so this line is the
  floor rather than the answer, and is kept only so a layer that loses the class is not left at
  the ordinary 160 underneath the thing that opened it.
*/
.gsbd-ask-layer,.gsbd-pick-layer,.gsbd-card-layer,.gsbd-say-layer,.gsbd-adj-layer{z-index:200}

/* ---------- the scorecard's Adjust score button and its form ---------- */

/*
  The scorecard gained three columns, so the table is wider than the 720px dialog it was drawn
  for. It scrolls sideways rather than squeezing the numbers: a scorecard whose figures have
  been shortened to fit is a scorecard you cannot read.
*/
/*
  WIDE ENOUGH THAT IT DOES NOT SCROLL AT 1280 (2026-09-30). The scorecard is nine columns now,
  and the dialog was 720px - so the table scrolled sideways inside it on every screen, which is
  the one thing a scorecard must not do. Measured at 1280: the nine columns want about 900px,
  and min(1040, 100%) leaves the dialog inside a 1280 window with its own margins and the
  columns inside the dialog. overflow-x stays as the backstop for a genuinely narrow screen.
*/
.gsbd-card-layer .gsb-dialog{ --gsb-dialog-w: 1040px; }
.gsbd-card-body{overflow-x:auto}
.gsbd-card-table{min-width:0}
.gsbd-card-act{text-align:right;white-space:nowrap}
.gsbd-adjust{min-height:26px;padding:0 9px;font-size:12px}

.gsbd-adj-body{display:flex;flex-direction:column;gap:11px}
.gsbd-adj-field{display:flex;flex-direction:column;gap:4px}
.gsbd-adj-label{
  font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--gsb-muted)
}
.gsbd-adj-field .gsb-input{width:100%}


/* ============ THE FONT SIZE SETTING: FIVE KINDS OF TEXT, AND NOTHING ELSE (2026-10-02) ======= */
/*
  Every declaration here is an existing font-size restated as itself times --gsb-text-scale, so
  at Normal it is the same number and nothing in the file below has changed. The boxes are not
  touched: the lists these live in already scroll, and the text wraps inside them.

  THE SIZES ARE REPEATED RATHER THAN INHERITED, and that is forced by the site rather than
  chosen: there is not one rem on it, so there is no root size for a child's px to follow. The
  cost is that a base size changed above has to be changed here too; the alternative was a page
  zoom, which is what this replaces.

  WHY THESE FIVE AND NOT "all text". Because the ask is these five, and because a multiplier
  loose on a page whose boxes are fixed is how the first version ended up moving the tiles, the
  nav, the top bar and the felt.
*/

/* ---- messages: the panel and its conversations ---- */
.gsb-rail .conv-row .tx b          { font-size: calc(15px   * var(--gsb-text-scale)); }
.gsb-rail .conv-row .tx small      { font-size: calc(13px   * var(--gsb-text-scale)); }
.gsb-rail .convs-empty             { font-size: calc(14px   * var(--gsb-text-scale)); }
.gsb-rail .thread > header h2      { font-size: calc(16px   * var(--gsb-text-scale)); }
.gsb-rail .thread > header .gsb-meta { font-size: calc(12px * var(--gsb-text-scale)); }
.gsb-rail .bubble                  { font-size: calc(14.5px * var(--gsb-text-scale)); }
.gsb-rail .bubble .when            { font-size: calc(11px   * var(--gsb-text-scale)); }

/*
  ---- announcements, and the director-call alert's explanation ----

  SCOPED TO .gsb-director-call, not to .gsb-dialog-lede at large. That class is the shared lede
  of every dialog on the site - the Message tournament form, the board popup, the confirmations -
  and scaling it everywhere would be the setting reaching past the five things it is for. This
  one dialog is the announcement AND the call alert: the lede is the caller's explanation or the
  director's message, and the facts list is its details.
*/
/*
  THE SCOPE CAME OFF THESE THREE (2026-10-02, third pass). They were `.gsb-director-call …`
  because the setting reached five things and a dialog's lede at large would have been reaching
  past them. The setting now covers ordinary and small text everywhere, and a dialog's lede IS a
  description - so the scope is gone and every dialog's gets it.
*/
.gsb-dialog-lede                 { font-size: calc(13px   * var(--gsb-text-scale)); }
.gsb-match-invite-facts dt       { font-size: calc(12px   * var(--gsb-text-scale)); }
.gsb-match-invite-facts dd       { font-size: calc(14px   * var(--gsb-text-scale)); }

/* ---- the profile card ---- */
.gsb-profile-names strong          { font-size: calc(19px   * var(--gsb-text-scale)); }
.gsb-profile-names span            { font-size: calc(13.5px * var(--gsb-text-scale)); }
.gsb-profile-field b               { font-size: calc(10px   * var(--gsb-text-scale)); }
.gsb-profile-field span            { font-size: calc(13px   * var(--gsb-text-scale)); }


/* ====== THE FONT SIZE SETTING REACHES ORDINARY AND SMALL TEXT TOO (2026-10-02) ============== */
/*
  The five kinds above were the first pass. This is the rest of the ask: descriptions, labels,
  captions, list items, meta lines, form hints and empty states - the text somebody actually has
  to read, as opposed to the furniture around it.

  WHAT IS DELIBERATELY NOT HERE, each for a stated reason:

    titles and subtitles   a page, section, card or dialog heading is already large, and growing
                           headings as well as body text keeps the page the same shape rather
                           than making the reading easier
    menus, buttons, tabs   controls, not reading matter, and the ones whose boxes cannot grow
    the felt               seat plates, cards, the bidding box, the table's labels - a geometry
                           with four generations of tuning on it
    the History panel      #scoreHistoryList at a table; its columns are already full at 12px
                           and were measured so on 2026-10-02
    the base body size     .gsb-theme is 16.5px and most unstyled copy inherits it, so scaling it
                           would be the one rule that cascades into everything excluded above.
                           The cost is that copy with no rule of its own does not grow; the
                           alternative was a rule nobody could bound.
    inputs and textareas   what you type is ordinary text, but the field's height is a layout and
                           this grows every form on the site. .gsb-check is a LABEL and is here.
    the scorecard's body   .gsb-scorecard-table td is a results grid, which is the same kind of
                           thing as the History panel and excluded for the same reason
    list-item titles       .table-name strong and friends are the card's title; the meta line
                           under them is what scales
    the tagline, the motto, .gsb-pill   captions and labels by nature, chrome by placement

  CONTAINERS MAY GROW IN HEIGHT and two of them may not: the rail's conversation list is capped
  at 42% and the left column at a table is capped at the table's height. Both already scroll,
  which is why text may grow inside them at all.
*/

/* ---- descriptions and captions ---- */
.gsb-panel > header .gsb-meta      { font-size: calc(13px   * var(--gsb-text-scale)); }
.gsb-dialog-msg                    { font-size: calc(12.5px * var(--gsb-text-scale)); }

/* ---- labels, meta lines and counts ---- */
.gsb-scorecard-headline            { font-size: calc(13.5px * var(--gsb-text-scale)); }
.gsb-scorecard-imphead             { font-size: calc(11.5px * var(--gsb-text-scale)); }
.gsb-scorecard-count               { font-size: calc(11.5px * var(--gsb-text-scale)); }
.gsb-scorecard-seats-title         { font-size: calc(13px   * var(--gsb-text-scale)); }
.gsb-profile-notes-label           { font-size: calc(12.5px * var(--gsb-text-scale)); }
.gsb-profile-msg                   { font-size: calc(12px   * var(--gsb-text-scale)); }

/* ---- the Friends list, bigger on every page that shows it (2026-10-03) ---- */
/*
  ITEM 1 (2026-10-03): the rail's "Friends" tab is back to the size of the tabs beside it. It was
  doubled a branch ago and read as a heading rather than one tab of several; the rule is simply
  gone, so it inherits what Messages has.
*/

/* ---- the top bar matches table.html on every page (2026-10-03) ---- */
/*
  MEASURED ON table.html AND COPIED, rather than chosen:

      avatar      72 x 72, 30px initials      (other pages had 36 x 36, 16px)
      username    29.7px                      (15.5px)
      rank        22px                        (13px)
      the icons   36 x 36                     (18 x 18)

  BY ID, because the bar's container differs from page to page - table.html wraps it in
  #shellTop and the others do not - while #avatar, #userName and #userLevel are the shell's own
  ids and exist once per page, in the bar. The icons go through .gsb-topright, which is the
  cluster that holds search, bell and settings.

  TABLE.HTML IS UNAFFECTED. Its own rules under `body .table-chrome` are more specific and
  already carry these exact numbers, so it keeps what it has either way - which is what "stays
  as it is" needs.

  NOTHING IS PINNED TO A HEIGHT HERE. The bar on those pages sizes to its contents, so it grows
  to fit rather than clipping, and the menus hang off the buttons and move down with them.
*/
#avatar,
.gsb-topnav .gsb-avatar{
  width: 72px;
  height: 72px;
  min-width: 72px;
  font-size: 30px;
}
#userName{
  font-size: 29.7px;
  line-height: 1.12;
}
#userLevel{
  font-size: 22px;
  line-height: 1.12;
}
.gsb-topright svg{
  width: 36px;
  height: 36px;
}

/* ---- the rail's list items, labels and empty states ---- */
.gsb-rail .panelmeta               { font-size: calc(13px   * var(--gsb-text-scale)); }
.gsb-rail .friend-username         { font-size: calc(16.5px * var(--gsb-text-scale)); }   /* 19.4 less 15%, item 2 */
.friend-state                      { font-size: calc(10.5px * var(--gsb-text-scale)); }
.gsb-rail .friends-section-label   { font-size: calc(11.5px * var(--gsb-text-scale)); }
.gsb-rail .friends-offline-toggle  { font-size: calc(14.5px * var(--gsb-text-scale)); }
.gsb-rail .friends-empty           { font-size: calc(14px   * var(--gsb-text-scale)); }
.gsb-rail .thread .thread-empty    { font-size: calc(14px   * var(--gsb-text-scale)); }

/* ============ EVERY POPUP'S TEXT SCALES (2026-10-02) ======================================= */
/*
  The five areas and then ordinary text were the first two passes; this is the third: a popup's
  text, wherever the popup is. Results panels and their TABLES are in - the half-time and
  end-of-match panels, the scorecard, Look up board, View all scores and a board popup's text -
  which REVERSES the earlier decision to exclude the scorecard's table body. A results popup is
  a thing somebody reads.

  HEADINGS ARE STILL OUT, as everywhere: .gsb-dialog-head h2/h3, a board popup's big contract,
  and the close buttons.

  .dd-table IS OUT by decision: a dense five-by-four numeric grid in a narrow panel, and the
  likeliest thing in a popup to clip.

  THE BOARD POPUP IS A srcdoc IFRAME and does not inherit this page's --gsb-text-scale: the token
  is written onto the parent's html element by gsb-font-size.js, and the frame is a separate
  document. gsb-board-popup.js now carries the number into the frame and sets it there, which is
  what makes these rules mean anything inside it.
*/
.gsb-scorecard-table td            { font-size: calc(12.5px * var(--gsb-text-scale)); }
.gsb-scorecard-seats .seat-reserve-name { font-size: calc(14px * var(--gsb-text-scale)); }
.gsbd-why                          { font-size: calc(12px   * var(--gsb-text-scale)); }
.gsbd-row-board                    { font-size: calc(11.5px * var(--gsb-text-scale)); }
.gsbd-card-table                   { font-size: calc(13px   * var(--gsb-text-scale)); }
