/*
  servermod.app - the community portal.

  TWO MODES, AND THE DEFAULT IS DARK
  `:root` carries dark, because the product is dark and most people arrive from
  Discord. layout() in pages.js always stamps `data-theme` on <html> - "dark"
  unless the visitor's cookie says "light" - so a rendered page never falls
  through to the prefers-color-scheme blocks below, and a visitor whose system
  asks for light gets dark until they press the toggle once. The media blocks
  are kept for a page that carries no attribute. There is no JavaScript on this
  site (`script-src 'none'`), so the toggle is a link the server answers with a
  cookie - see themeFromRequest in index.js.

  SIX THEMES, AND EACH ONE IS A SKIN
  `[data-portal-theme]` on <html> picks one of modern / futuristic / playful /
  cosy / fantasy / elegant, and the instance sends the key. Since 2026-09-21 a
  theme is a full skin rather than a typeface: palette and surfaces in BOTH
  modes, corners, borders, shadows, a background pattern, the avatar's shape,
  the XP bar and the chips. The anatomy never changes - the same markup, the
  same columns, the same order - so a member who can read one community's
  board can read any. Modern is the default and the fallback, and it is
  squared like the dashboard. `classic` (dropped) and `medieval` (renamed) are
  mapped to modern and fantasy in pages.js before they reach this file.

  Every skin is tokens first: the blocks below set custom properties and the
  component rules read them, so a theme almost never needs a selector of its
  own. The few that do (Futuristic's cut corners, Playful's inked controls,
  Fantasy's gems, Elegant's ornament and script hand) are grouped at the end
  of the file under "THE SKINS' OWN TOUCHES".

  THE ACCENT IS THE COMMUNITY'S COLOUR
  `--brand` (written by layout() from the guild's colour) is the primary on
  every theme: lifted towards white on a dark ground, deepened towards black on
  a light one, and the house teal #4d97a1 where a community has not set one.
  A theme never sets the accent itself.

  THE FOUR IDENTITY HUES ARE NOT DECORATION
  Four hues carry identity across the site (hub cards, guide cards, event
  categories, feedback states). Each mode has its own steps rather than one set
  dimmed: both sets were run through a colourblind validator and pass on
  lightness band, chroma floor, CVD separation, normal-vision separation and
  contrast. If you change one, re-run it - adjacent pairs are what break first.
  They are per mode, not per theme.

  CONTRAST. Every skin's body text, muted text and dim labels clear 4.5:1 on
  every surface they sit on in that skin (page, card, well, hover and each
  pastel tile); the dim step was nudged towards the ink where a mockup's
  muted colour did not.

  Copied from dashboard.css by hand rather than shared, for the reason given
  there: this origin must render without any instance being reachable.
*/
:root {
  /* MODERN, DARK: the default skin and the fallback for anything unknown. */
  --sm-bg-base: #0f1216;
  --sm-bg-raised: #171b21;
  --sm-bg-deep: #13161c;
  --sm-bg-hover: #1e232b;
  --sm-text: #e8ecf1;
  --sm-text-muted: #b5bcc6;
  --sm-text-dim: #8b95a3;
  --sm-text-faint: #535a64;
  --sm-border: #2a313b;
  --sm-border-strong: #454b54;
  --sm-card-edge: var(--sm-border);
  --sm-skeleton: #1e232b;
  --sm-track: #232a33;
  --sm-avatar-bg: #2a313b;
  --sm-accent-2: #8b95a3;

  /* the four identity hues - dark steps */
  --sm-amber: #bd8420;
  --sm-teal: #1c9dac;
  --sm-rose: #d75e78;
  --sm-violet: #9070e0;

  --sm-warning: var(--sm-amber);
  --sm-success: #6fae74;

  /* medals stay warm metal in both modes */
  --sm-gold: #d4b062;
  --sm-silver: #b9c0cc;
  --sm-bronze: #b58150;

  /* THE FOUR TIER METALS of a progression ladder (bronze to diamond), for its
     glyphs and stars. Separate from the podium medals above: a tier is a rank
     within a ladder, and diamond has no medal. Silver and diamond step darker
     on light paper; bronze and gold read on both. */
  --sm-tier-stone: #a39583;
  --sm-tier-bronze: #b87333;
  --sm-tier-silver: #b9c2cf;
  --sm-tier-gold: #d4a73a;
  --sm-tier-diamond: #6fd3e8;

  /*
    THE ROLE MARKS, which are not the four identity hues.

    A community's crew, its admins and its mods are read at a glance and are
    expected to be gold, red and blue — the colours those words already have
    everywhere else — so they are their own three tokens rather than borrowed
    from the decorative set. Crew takes the medal gold it is already sitting
    beside on a leaderboard; the other two are a step lighter than the plain
    red and blue so they clear the dark ground, and the light block below
    takes them a step darker for the same reason.
  */
  --sm-role-gold: var(--sm-gold);
  --sm-role-red: #e05252;
  --sm-role-blue: #5b8def;

  /* The accent is the community's colour, lifted for a dark ground. */
  --sm-accent-auto: color-mix(in srgb, var(--sm-brand) 85%, white);
  --sm-accent-hover-auto: color-mix(in srgb, var(--sm-brand) 62%, white);
  --sm-accent-fg: var(--sm-bg-base);
  --sm-shadow-hover: 0 0.75rem 1.5rem rgba(0, 0, 0, 0.32);
  --sm-sheet-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);

  /* THE ACCENT IS DERIVED, NEVER SET BY A THEME.
     --brand is the community's colour, written into a nonced block by layout();
     the house accent stands in where a community has none. The two -auto
     tokens are redefined per MODE; --sm-accent itself is declared here and
     nowhere else, at :root specificity, so the nonced :root block the manager
     page writes (which comes later and sets --sm-accent) still wins. */
  --sm-brand: var(--brand, #4d97a1);
  --sm-accent: var(--sm-accent-auto);
  --sm-accent-hover: var(--sm-accent-hover-auto);
  --sm-chip-bg: color-mix(in srgb, var(--sm-brand) 18%, transparent);
  --sm-chip-ink: var(--sm-accent);
  /* The figure on a stat card. Themes with pastel tiles set it in ink. */
  --sm-stat-ink: var(--sm-accent);

  --sm-font-ui: Archivo, "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --sm-font-display: var(--sm-font-ui);
  --sm-font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;
  --sm-body-size: 16px;

  /* how hard the display type is set */
  --sm-display-weight: 800;
  --sm-display-tracking: -0.035em;
  --sm-heading-weight: 700;
  --sm-display-case: none;
  /* The page title, the community's name and a member's name. The same as the
     display type unless a theme gives them a hand of their own (Elegant's
     script) or a size of their own (Futuristic's wide capitals). */
  --sm-title-font: var(--sm-font-display);
  --sm-title-weight: var(--sm-display-weight);
  --sm-title-tracking: var(--sm-display-tracking);
  --sm-title-case: var(--sm-display-case);
  --sm-title-scale: 1;

  /* THE SHAPE. Modern is squared, like the dashboard: every radius is 0. */
  --sm-radius: 0;          /* cards, rows, panels */
  --sm-radius-sm: 0;       /* fields, wells, icons, tiles inside a card */
  --sm-radius-pill: 0;     /* chips, tabs, buttons, the XP bar */
  --sm-card-bw: 1px;       /* the width of a card's border */
  --sm-clip: none;         /* a card's clip-path (Futuristic's cut corners) */
  --sm-clip-sm: none;
  --sm-shadow: 0 0 0 0 transparent;
  /* An inset rule a theme can draw inside a card: width of the mat, colour of
     the line. Zero and transparent draw nothing. */
  --sm-inset-w: 0px;
  --sm-inset-c: transparent;
  --sm-avatar-radius: 0;
  --sm-avatar-clip: none;
  --sm-avatar-border: 1px solid var(--sm-border);
  --sm-avatar-ring: 0 0 0 0 transparent;
  --sm-badge-radius: 0;
  --sm-badge-clip: none;
  --sm-pattern: none;
  --sm-pattern-size: auto;
  /* the XP bar */
  --sm-bar-h: 8px;
  --sm-bar-fill: var(--sm-accent);
  --sm-bar-edge: 1px solid var(--sm-border);
  --sm-bar-glow: none;
}

/* Light, for a page with no `data-theme` whose system prefers it. layout()
   always stamps the attribute, so rendered pages never reach this block. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --sm-bg-base: #f3f5f7;
    --sm-bg-raised: #ffffff;
    --sm-bg-deep: #eef1f4;
    --sm-bg-hover: #f5f7f8;
    --sm-text: #12161c;
    --sm-text-muted: #3a414c;
    --sm-text-dim: #5b6573;
    --sm-text-faint: #9fa6ae;
    --sm-border: #dde2e8;
    --sm-border-strong: #c1c5cb;
    --sm-card-edge: var(--sm-border);
    --sm-skeleton: #eef1f4;
    --sm-track: #e6eaef;
    --sm-avatar-bg: #e3e8ee;
    --sm-accent-2: #5b6573;

    --sm-amber: #946200;
    --sm-teal: #0090a0;
    --sm-rose: #c72a50;
    --sm-violet: #6a3fd0;

    --sm-gold: #9a7414;
    --sm-silver: #6b7280;
    --sm-bronze: #8a5a2b;
    --sm-tier-stone: #7a6e5f;
    --sm-tier-bronze: #b87333;
    --sm-tier-silver: #7b8798;
    --sm-tier-gold: #d4a73a;
    --sm-tier-diamond: #2a93ac;
    --sm-role-gold: var(--sm-gold);
    --sm-role-red: #c02626;
    --sm-role-blue: #2f5fd0;
    --sm-success: #2f7a44;

    /* ...and deepened for a light one, so one stored colour reads in both. */
    --sm-accent-auto: color-mix(in srgb, var(--sm-brand) 78%, black);
    --sm-accent-hover-auto: color-mix(in srgb, var(--sm-brand) 62%, black);
    --sm-accent-fg: #ffffff;
    --sm-shadow-hover: 0 0.75rem 1.5rem rgba(20, 28, 40, 0.14);
    --sm-sheet-shadow: 0 18px 50px rgba(20, 28, 40, 0.22);
  }
}

/* And the same again for the visitor's choice, which is how light mode is
   actually reached: the toggle's cookie, stamped on <html> by layout(). */
:root[data-theme="light"] {
  --sm-bg-base: #f3f5f7;
  --sm-bg-raised: #ffffff;
  --sm-bg-deep: #eef1f4;
  --sm-bg-hover: #f5f7f8;
  --sm-text: #12161c;
  --sm-text-muted: #3a414c;
  --sm-text-dim: #5b6573;
  --sm-text-faint: #9fa6ae;
  --sm-border: #dde2e8;
  --sm-border-strong: #c1c5cb;
  --sm-card-edge: var(--sm-border);
  --sm-skeleton: #eef1f4;
  --sm-track: #e6eaef;
  --sm-avatar-bg: #e3e8ee;
  --sm-accent-2: #5b6573;

  --sm-amber: #946200;
  --sm-teal: #0090a0;
  --sm-rose: #c72a50;
  --sm-violet: #6a3fd0;

  --sm-gold: #9a7414;
  --sm-silver: #6b7280;
  --sm-bronze: #8a5a2b;
  --sm-tier-stone: #7a6e5f;
  --sm-tier-bronze: #b87333;
  --sm-tier-silver: #7b8798;
  --sm-tier-gold: #d4a73a;
  --sm-tier-diamond: #2a93ac;
  --sm-role-gold: var(--sm-gold);
  --sm-role-red: #c02626;
  --sm-role-blue: #2f5fd0;
  --sm-success: #2f7a44;

  /* ...and deepened for a light one, so one stored colour reads in both. */
  --sm-accent-auto: color-mix(in srgb, var(--sm-brand) 78%, black);
  --sm-accent-hover-auto: color-mix(in srgb, var(--sm-brand) 62%, black);
  --sm-accent-fg: #ffffff;
  --sm-shadow-hover: 0 0.75rem 1.5rem rgba(20, 28, 40, 0.14);
  --sm-sheet-shadow: 0 18px 50px rgba(20, 28, 40, 0.22);
}

/* ============================== The skins ===============================
   Each theme is one block of shape and type plus its dark colours, then its
   light colours twice (system preference, then the visitor's choice) — the
   same layering as the modern blocks above. A theme's light block has one more
   attribute in its selector than its dark block, so it wins in light mode
   whatever the order; and every light block redefines every colour its dark
   block sets (scripts/verify-portal-themes.mjs checks this). The identity
   hues, medals, role marks and the accent formula are not redefined per
   theme: they come from the mode blocks above. */

/* Futuristic: a grid, cut corners, one neon pair. */
:root[data-portal-theme="futuristic"] {
  --sm-font-ui: "Chakra Petch", "Hanken Grotesk", system-ui, sans-serif;
  --sm-font-display: var(--sm-font-ui);
  --sm-display-weight: 700;
  --sm-display-tracking: 0.06em;
  --sm-heading-weight: 600;
  --sm-display-case: uppercase;
  /* Wide capitals: a long name shrinks rather than wrapping. */
  --sm-title-scale: 0.8;

  --sm-radius: 0;
  --sm-radius-sm: 0;
  --sm-radius-pill: 0;
  --sm-clip: polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px);
  --sm-clip-sm: polygon(6px 0, 100% 0, 100% calc(100% - 6px), calc(100% - 6px) 100%, 0 100%, 0 6px);
  --sm-avatar-radius: 0;
  --sm-avatar-clip: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0 50%);
  --sm-avatar-border: 0 solid transparent;
  --sm-badge-clip: var(--sm-clip-sm);
  --sm-pattern-size: 24px 24px;
  --sm-bar-h: 10px;
  --sm-bar-fill: repeating-linear-gradient(90deg, var(--sm-accent) 0 10px, transparent 10px 13px);
  --sm-bar-edge: 1px solid var(--sm-card-edge);

  /* dark */
  --sm-bg-base: #06080e;
  --sm-bg-raised: #0b111c;
  --sm-bg-deep: #080c15;
  --sm-bg-hover: #101a2a;
  --sm-text: #d8f7ff;
  --sm-text-muted: #9ebecc;
  --sm-text-dim: #6f8fa3;
  --sm-text-faint: #405260;
  --sm-border: rgba(60, 240, 255, 0.18);
  --sm-border-strong: rgba(60, 240, 255, 0.45);
  --sm-card-edge: rgba(60, 240, 255, 0.45);
  --sm-skeleton: #101a2a;
  --sm-track: #0f2233;
  --sm-avatar-bg: #12324a;
  --sm-accent-2: #ff4fb8;
  --sm-chip-bg: rgba(255, 79, 184, 0.14);
  --sm-chip-ink: #ff8fd0;
  --sm-pattern: linear-gradient(rgba(60, 240, 255, 0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(60, 240, 255, 0.07) 1px, transparent 1px);
  --sm-shadow-hover: 0 0 0 1px rgba(60, 240, 255, 0.45), 0 0 18px rgba(60, 240, 255, 0.18);
}
@media (prefers-color-scheme: light) {
  :root[data-portal-theme="futuristic"]:not([data-theme="dark"]) {
    --sm-bg-base: #eef3f7;
    --sm-bg-raised: #ffffff;
    --sm-bg-deep: #e6eef4;
    --sm-bg-hover: #f0f5f8;
    --sm-text: #0a1a24;
    --sm-text-muted: #304552;
    --sm-text-dim: #4f6878;
    --sm-text-faint: #97a7b1;
    --sm-border: rgba(0, 127, 156, 0.20);
    --sm-border-strong: rgba(0, 127, 156, 0.45);
    --sm-card-edge: rgba(0, 127, 156, 0.45);
    --sm-skeleton: #e6eef4;
    --sm-track: #dbe7ee;
    --sm-avatar-bg: #cfe6ef;
    --sm-accent-2: #c81d74;
    --sm-chip-bg: #fbe0ee;
    --sm-chip-ink: #a3175e;
    --sm-pattern: linear-gradient(rgba(0, 120, 160, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 120, 160, 0.08) 1px, transparent 1px);
    --sm-shadow-hover: 0 0 0 1px rgba(0, 127, 156, 0.45), 0 0 16px rgba(0, 127, 156, 0.14);
  }
}
:root[data-portal-theme="futuristic"][data-theme="light"] {
  --sm-bg-base: #eef3f7;
  --sm-bg-raised: #ffffff;
  --sm-bg-deep: #e6eef4;
  --sm-bg-hover: #f0f5f8;
  --sm-text: #0a1a24;
  --sm-text-muted: #304552;
  --sm-text-dim: #4f6878;
  --sm-text-faint: #97a7b1;
  --sm-border: rgba(0, 127, 156, 0.20);
  --sm-border-strong: rgba(0, 127, 156, 0.45);
  --sm-card-edge: rgba(0, 127, 156, 0.45);
  --sm-skeleton: #e6eef4;
  --sm-track: #dbe7ee;
  --sm-avatar-bg: #cfe6ef;
  --sm-accent-2: #c81d74;
  --sm-chip-bg: #fbe0ee;
  --sm-chip-ink: #a3175e;
  --sm-pattern: linear-gradient(rgba(0, 120, 160, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 120, 160, 0.08) 1px, transparent 1px);
  --sm-shadow-hover: 0 0 0 1px rgba(0, 127, 156, 0.45), 0 0 16px rgba(0, 127, 156, 0.14);
}

/* Playful: pastel pop, chunky and round, inked. */
:root[data-portal-theme="playful"] {
  --sm-font-ui: "Baloo 2", "Hanken Grotesk", system-ui, sans-serif;
  --sm-font-display: var(--sm-font-ui);
  --sm-display-weight: 800;
  --sm-display-tracking: -0.01em;

  --sm-radius: 22px;
  --sm-radius-sm: 16px;
  --sm-radius-pill: 999px;
  /* A thick ink outline and a hard offset shadow: a sticker, not a card. */
  --sm-card-bw: 2.5px;
  --sm-avatar-radius: 34%;
  --sm-avatar-border: 2.5px solid var(--sm-card-edge);
  --sm-badge-radius: 16px;
  --sm-stat-ink: var(--sm-text);
  --sm-bar-h: 14px;
  --sm-bar-fill: repeating-linear-gradient(-45deg, var(--sm-accent) 0 10px, color-mix(in srgb, var(--sm-brand) 55%, white) 10px 20px);
  --sm-bar-edge: 2.5px solid var(--sm-card-edge);

  /* dark */
  --sm-bg-base: #1d1730;
  --sm-bg-raised: #271f40;
  --sm-bg-deep: #221b38;
  --sm-bg-hover: #31284f;
  --sm-text: #fff4fb;
  --sm-text-muted: #d8cbe7;
  --sm-text-dim: #b9a9d6;
  --sm-text-faint: #73678b;
  --sm-border: #544b6a;
  --sm-border-strong: #837a96;
  /* One edge colour, not two: the border and its offset shadow share a
     brand-tinted ink, as light mode's do. A near-white border over a
     brand-coloured shadow read as two frames fighting (2026-09-22). */
  --sm-card-edge: color-mix(in srgb, var(--sm-accent) 45%, #2a2140);
  --sm-skeleton: #31284f;
  --sm-track: #31284f;
  --sm-avatar-bg: #4a2a45;
  --sm-accent-2: #a993ff;
  --sm-chip-bg: rgba(169, 147, 255, 0.22);
  --sm-chip-ink: #d3c6ff;
  --sm-tile-1: #4a2a45;
  --sm-tile-2: #1f4038;
  --sm-tile-3: #4a4322;
  --sm-tile-4: #342a5c;
  --sm-tile-5: #1f3a52;
  --sm-shadow: 4px 4px 0 var(--sm-card-edge);
  --sm-shadow-hover: 6px 6px 0 var(--sm-card-edge);
  --sm-sheet-shadow: 6px 6px 0 var(--sm-card-edge);
}
@media (prefers-color-scheme: light) {
  :root[data-portal-theme="playful"]:not([data-theme="dark"]) {
    --sm-bg-base: #fff6fb;
    --sm-bg-raised: #ffffff;
    --sm-bg-deep: #fff0f7;
    --sm-bg-hover: #fff6fa;
    --sm-text: #33264f;
    --sm-text-muted: #51446d;
    --sm-text-dim: #6a5d86;
    --sm-text-faint: #ada2bb;
    --sm-border: #d2cfd8;
    --sm-border-strong: #a39db0;
    --sm-card-edge: #33264f;
    --sm-skeleton: #fff0f7;
    --sm-track: #f3e8ff;
    --sm-avatar-bg: #ffd9ea;
    --sm-accent-2: #6a48f0;
    --sm-chip-bg: #e4dbff;
    --sm-chip-ink: #4a2fb8;
    --sm-tile-1: #ffd9ea;
    --sm-tile-2: #cff5e4;
    --sm-tile-3: #fff1a8;
    --sm-tile-4: #e4dbff;
    --sm-tile-5: #d3ebff;
    --sm-shadow: 4px 4px 0 var(--sm-card-edge);
    --sm-shadow-hover: 6px 6px 0 var(--sm-card-edge);
    --sm-sheet-shadow: 6px 6px 0 var(--sm-card-edge);
  }
}
:root[data-portal-theme="playful"][data-theme="light"] {
  --sm-bg-base: #fff6fb;
  --sm-bg-raised: #ffffff;
  --sm-bg-deep: #fff0f7;
  --sm-bg-hover: #fff6fa;
  --sm-text: #33264f;
  --sm-text-muted: #51446d;
  --sm-text-dim: #6a5d86;
  --sm-text-faint: #ada2bb;
  --sm-border: #d2cfd8;
  --sm-border-strong: #a39db0;
  --sm-card-edge: #33264f;
  --sm-skeleton: #fff0f7;
  --sm-track: #f3e8ff;
  --sm-avatar-bg: #ffd9ea;
  --sm-accent-2: #6a48f0;
  --sm-chip-bg: #e4dbff;
  --sm-chip-ink: #4a2fb8;
  --sm-tile-1: #ffd9ea;
  --sm-tile-2: #cff5e4;
  --sm-tile-3: #fff1a8;
  --sm-tile-4: #e4dbff;
  --sm-tile-5: #d3ebff;
  --sm-shadow: 4px 4px 0 var(--sm-card-edge);
  --sm-shadow-hover: 6px 6px 0 var(--sm-card-edge);
  --sm-sheet-shadow: 6px 6px 0 var(--sm-card-edge);
}

/* Cosy: oat, terracotta and sage by day; a candlelit navy by night. */
:root[data-portal-theme="cosy"] {
  --sm-font-ui: Alegreya, Georgia, "Iowan Old Style", serif;
  --sm-font-display: var(--sm-font-ui);
  --sm-display-weight: 800;
  --sm-display-tracking: -0.012em;

  --sm-radius: 18px;
  --sm-radius-sm: 14px;
  --sm-radius-pill: 999px;
  --sm-avatar-radius: 50%;
  --sm-avatar-border: 3px solid var(--sm-bg-raised);
  --sm-avatar-ring: 0 0 0 2px var(--sm-accent);
  --sm-badge-radius: 50%;
  --sm-stat-ink: var(--sm-text);
  --sm-bar-h: 10px;
  --sm-bar-edge: 0 solid transparent;

  /* dark */
  --sm-bg-base: #161b25;
  --sm-bg-raised: #1e2431;
  --sm-bg-deep: #1a202b;
  --sm-bg-hover: #272e3e;
  --sm-text: #f3eadf;
  --sm-text-muted: #c9c5c6;
  --sm-text-dim: #a7a6b2;
  --sm-text-faint: #666773;
  --sm-border: #2e3647;
  --sm-border-strong: #4a4f5c;
  --sm-card-edge: var(--sm-border);
  --sm-skeleton: #272e3e;
  --sm-track: #2e3647;
  --sm-avatar-bg: #3a3f55;
  --sm-accent-2: #a9bb93;
  --sm-chip-bg: rgba(169, 187, 147, 0.18);
  --sm-chip-ink: #c8d6b6;
  --sm-tile-1: #3a2c33;
  --sm-tile-2: #283a35;
  --sm-tile-3: #3a3428;
  --sm-tile-4: #332d42;
  --sm-tile-5: #24343d;
  --sm-shadow: 0 8px 22px rgba(0, 0, 0, 0.35);
  --sm-shadow-hover: 0 12px 30px rgba(0, 0, 0, 0.48);
}
@media (prefers-color-scheme: light) {
  :root[data-portal-theme="cosy"]:not([data-theme="dark"]) {
    --sm-bg-base: #f2e8da;
    --sm-bg-raised: #fbf5ec;
    --sm-bg-deep: #efe2cf;
    --sm-bg-hover: #f4eadb;
    --sm-text: #36312e;
    --sm-text-muted: #524a45;
    --sm-text-dim: #685f58;
    --sm-text-faint: #a69d92;
    --sm-border: #e2d2bc;
    --sm-border-strong: #cabba8;
    --sm-card-edge: var(--sm-border);
    --sm-skeleton: #efe2cf;
    --sm-track: #eadcc6;
    --sm-avatar-bg: #e9c9b3;
    --sm-accent-2: #6f8559;
    --sm-chip-bg: #dfe6d3;
    --sm-chip-ink: #4e6340;
    --sm-tile-1: #f3d9cb;
    --sm-tile-2: #dfe6d3;
    --sm-tile-3: #f1e2c2;
    --sm-tile-4: #ead7d2;
    --sm-tile-5: #dbe3e3;
    --sm-shadow: 0 8px 22px rgba(92, 64, 40, 0.13);
    --sm-shadow-hover: 0 12px 30px rgba(92, 64, 40, 0.2);
  }
}
:root[data-portal-theme="cosy"][data-theme="light"] {
  --sm-bg-base: #f2e8da;
  --sm-bg-raised: #fbf5ec;
  --sm-bg-deep: #efe2cf;
  --sm-bg-hover: #f4eadb;
  --sm-text: #36312e;
  --sm-text-muted: #524a45;
  --sm-text-dim: #685f58;
  --sm-text-faint: #a69d92;
  --sm-border: #e2d2bc;
  --sm-border-strong: #cabba8;
  --sm-card-edge: var(--sm-border);
  --sm-skeleton: #efe2cf;
  --sm-track: #eadcc6;
  --sm-avatar-bg: #e9c9b3;
  --sm-accent-2: #6f8559;
  --sm-chip-bg: #dfe6d3;
  --sm-chip-ink: #4e6340;
  --sm-tile-1: #f3d9cb;
  --sm-tile-2: #dfe6d3;
  --sm-tile-3: #f1e2c2;
  --sm-tile-4: #ead7d2;
  --sm-tile-5: #dbe3e3;
  --sm-shadow: 0 8px 22px rgba(92, 64, 40, 0.13);
  --sm-shadow-hover: 0 12px 30px rgba(92, 64, 40, 0.2);
}

/* Fantasy: a grimoire — plum, gilt and an arch. */
:root[data-portal-theme="fantasy"] {
  /* Fantasy, which was Medieval until the instance renamed it — a period turned
     away every server whose setting is not one. Cinzel is Roman inscriptional
     capitals — epic rather than period, and legible at a page title's size —
     over the same Cormorant Garamond body the theme always had. */
  --sm-font-ui: "Cormorant Garamond", Garamond, Georgia, serif;
  --sm-font-display: Cinzel, "Cormorant Garamond", Garamond, serif;
  --sm-display-weight: 700;
  --sm-display-tracking: 0.015em;
  --sm-heading-weight: 600;
  /* Cormorant is small on the x-height; a point up keeps a paragraph readable. */
  --sm-body-size: 17px;

  --sm-radius: 6px;
  --sm-radius-sm: 4px;
  --sm-radius-pill: 4px;
  /* A gilt ring standing off the card: the page colour, then a hairline of gold. */
  --sm-shadow: 0 0 0 3px var(--sm-bg-raised), 0 0 0 4px color-mix(in srgb, var(--sm-gilt) 35%, transparent);
  /* A rounded square, not an arch: the arch read as a half circle at avatar size. */
  --sm-avatar-radius: 18%;
  --sm-avatar-border: 2px solid var(--sm-accent);
  --sm-badge-radius: 50% 50% 6px 6px;
  --sm-bar-fill: linear-gradient(90deg, color-mix(in srgb, var(--sm-brand) 70%, black), var(--sm-accent), color-mix(in srgb, var(--sm-brand) 55%, white));
  --sm-bar-edge: 1px solid var(--sm-card-edge);

  /* dark */
  --sm-bg-base: #1e1626;
  --sm-bg-raised: #2a1f35;
  --sm-bg-deep: #241a2e;
  --sm-bg-hover: #35283f;
  --sm-text: #f1e6cc;
  --sm-text-muted: #d1c2a8;
  --sm-text-dim: #b7a58a;
  --sm-text-faint: #72655d;
  --sm-border: rgba(212, 175, 98, 0.22);
  --sm-border-strong: rgba(212, 175, 98, 0.55);
  --sm-card-edge: rgba(212, 175, 98, 0.55);
  --sm-skeleton: #35283f;
  --sm-track: #3d2e49;
  --sm-avatar-bg: #4a3658;
  --sm-accent-2: #6fae8f;
  --sm-chip-bg: rgba(111, 174, 143, 0.18);
  --sm-chip-ink: #a5d9bf;
  --sm-gilt: #d4af62;
  --sm-shadow-hover: 0 0 0 3px var(--sm-bg-raised), 0 0 0 4px color-mix(in srgb, var(--sm-gilt) 60%, transparent), 0 10px 24px rgba(0, 0, 0, 0.35);
  --sm-bar-glow: 0 0 10px color-mix(in srgb, var(--sm-accent) 45%, transparent);
}
@media (prefers-color-scheme: light) {
  :root[data-portal-theme="fantasy"]:not([data-theme="dark"]) {
    --sm-bg-base: #f3ead6;
    --sm-bg-raised: #fbf6ea;
    --sm-bg-deep: #efe3c8;
    --sm-bg-hover: #f4ebd6;
    --sm-text: #2b1d33;
    --sm-text-muted: #503f41;
    --sm-text-dim: #6e5a4c;
    --sm-text-faint: #aa9b8a;
    --sm-border: rgba(138, 102, 28, 0.25);
    --sm-border-strong: rgba(138, 102, 28, 0.55);
    --sm-card-edge: rgba(138, 102, 28, 0.55);
    --sm-skeleton: #efe3c8;
    --sm-track: #e7dac0;
    --sm-avatar-bg: #e6d6b4;
    --sm-accent-2: #2f6f51;
    --sm-chip-bg: #dcebe2;
    --sm-chip-ink: #2f6f51;
    --sm-gilt: #8a661c;
    --sm-shadow-hover: 0 0 0 3px var(--sm-bg-raised), 0 0 0 4px color-mix(in srgb, var(--sm-gilt) 60%, transparent), 0 10px 24px rgba(60, 40, 10, 0.16);
    --sm-bar-glow: none;
  }
}
:root[data-portal-theme="fantasy"][data-theme="light"] {
  --sm-bg-base: #f3ead6;
  --sm-bg-raised: #fbf6ea;
  --sm-bg-deep: #efe3c8;
  --sm-bg-hover: #f4ebd6;
  --sm-text: #2b1d33;
  --sm-text-muted: #503f41;
  --sm-text-dim: #6e5a4c;
  --sm-text-faint: #aa9b8a;
  --sm-border: rgba(138, 102, 28, 0.25);
  --sm-border-strong: rgba(138, 102, 28, 0.55);
  --sm-card-edge: rgba(138, 102, 28, 0.55);
  --sm-skeleton: #efe3c8;
  --sm-track: #e7dac0;
  --sm-avatar-bg: #e6d6b4;
  --sm-accent-2: #2f6f51;
  --sm-chip-bg: #dcebe2;
  --sm-chip-ink: #2f6f51;
  --sm-gilt: #8a661c;
  --sm-shadow-hover: 0 0 0 3px var(--sm-bg-raised), 0 0 0 4px color-mix(in srgb, var(--sm-gilt) 60%, transparent), 0 10px 24px rgba(60, 40, 10, 0.16);
  --sm-bar-glow: none;
}

/* Elegant: parchment or misty forest, lace dots, a gilt rule and a script hand. */
:root[data-portal-theme="elegant"] {
  /* Cormorant Garamond for the body and the spaced capitals, and Pinyon
     Script — a copperplate hand — for the few big words: the page title, the
     community's name and a member's name. */
  --sm-font-ui: "Cormorant Garamond", Garamond, Georgia, serif;
  --sm-font-display: "Cormorant Garamond", Garamond, Georgia, serif;
  --sm-display-weight: 600;
  --sm-display-tracking: 0.14em;
  --sm-heading-weight: 600;
  --sm-display-case: uppercase;
  --sm-title-font: "Pinyon Script", "Cormorant Garamond", cursive;
  --sm-title-weight: 400;
  --sm-title-tracking: 0;
  --sm-title-case: none;
  --sm-title-scale: 1.3;
  --sm-body-size: 17px;

  --sm-radius: 18px;
  --sm-radius-sm: 14px;
  --sm-radius-pill: 999px;
  /* The inset gilt rule: a 5px mat of the card's own colour, then one line. */
  --sm-inset-w: 5px;
  /* A true circle: the oval read as a squashed photo at avatar size. */
  --sm-avatar-radius: 50%;
  --sm-avatar-border: 2px dotted var(--sm-accent-2);
  --sm-avatar-ring: 0 0 0 4px var(--sm-bg-raised), 0 0 0 5px var(--sm-accent-2);
  --sm-badge-radius: 50% / 45%;
  --sm-stat-ink: var(--sm-text);
  --sm-pattern-size: 40px 40px;
  --sm-bar-h: 6px;
  --sm-bar-fill: linear-gradient(90deg, var(--sm-accent), var(--sm-accent-2));
  --sm-bar-edge: 0 solid transparent;

  /* dark: a neutral near-black (2026-09-22). It was a green-tinted charcoal,
     which read as green rather than black; the gilt and the warm text now do
     the colour on their own. The bot's card skin (util/cardTheme.js) matches. */
  --sm-bg-base: #111111;
  --sm-bg-raised: #1a1a1a;
  --sm-bg-deep: #151515;
  --sm-bg-hover: #232323;
  --sm-text: #efe6da;
  --sm-text-muted: #c9c1b2;
  --sm-text-dim: #aaa292;
  --sm-text-faint: #6a685d;
  --sm-border: rgba(203, 181, 146, 0.22);
  --sm-border-strong: rgba(203, 181, 146, 0.45);
  --sm-card-edge: rgba(203, 181, 146, 0.28);
  --sm-skeleton: #232323;
  --sm-track: #2b2b2b;
  --sm-avatar-bg: #2a2627;
  --sm-accent-2: #cbb592;
  --sm-chip-bg: rgba(232, 180, 188, 0.14);
  --sm-chip-ink: #f0c9cf;
  --sm-pattern: radial-gradient(circle at 10px 10px, rgba(203, 181, 146, 0.10) 1.5px, transparent 2px), radial-gradient(circle at 30px 30px, rgba(203, 181, 146, 0.06) 1px, transparent 1.5px);
  --sm-tile-1: #231e1f;
  --sm-tile-2: #1f1f20;
  --sm-tile-3: #23211d;
  --sm-tile-4: #211f23;
  --sm-tile-5: #1e2021;
  --sm-inset-c: rgba(203, 181, 146, 0.25);
  --sm-shadow: 0 14px 36px rgba(0, 0, 0, 0.40);
  --sm-shadow-hover: 0 18px 44px rgba(0, 0, 0, 0.50);
}
@media (prefers-color-scheme: light) {
  :root[data-portal-theme="elegant"]:not([data-theme="dark"]) {
    --sm-bg-base: #f4ede1;
    --sm-bg-raised: #fbf7f0;
    --sm-bg-deep: #f1e7d9;
    --sm-bg-hover: #f5ede2;
    --sm-text: #4a3b33;
    --sm-text-muted: #615147;
    --sm-text-dim: #746357;
    --sm-text-faint: #aea195;
    --sm-border: #e3d6c3;
    --sm-border-strong: #cec0af;
    --sm-card-edge: #e3d6c3;
    --sm-skeleton: #f1e7d9;
    --sm-track: #ebdfd0;
    --sm-avatar-bg: #efe0d6;
    --sm-accent-2: #b89a7a;
    --sm-chip-bg: #f3dfe0;
    --sm-chip-ink: #8a4a55;
    --sm-pattern: radial-gradient(circle at 10px 10px, rgba(184, 154, 122, 0.16) 1.5px, transparent 2px), radial-gradient(circle at 30px 30px, rgba(184, 154, 122, 0.10) 1px, transparent 1.5px);
    --sm-tile-1: #f6e1e3;
    --sm-tile-2: #e8ecdf;
    --sm-tile-3: #f3e6d3;
    --sm-tile-4: #ece3ef;
    --sm-tile-5: #e3e9ea;
    --sm-inset-c: rgba(184, 154, 122, 0.45);
    --sm-shadow: 0 10px 30px rgba(120, 90, 70, 0.10);
    --sm-shadow-hover: 0 14px 36px rgba(120, 90, 70, 0.16);
  }
}
:root[data-portal-theme="elegant"][data-theme="light"] {
  --sm-bg-base: #f4ede1;
  --sm-bg-raised: #fbf7f0;
  --sm-bg-deep: #f1e7d9;
  --sm-bg-hover: #f5ede2;
  --sm-text: #4a3b33;
  --sm-text-muted: #615147;
  --sm-text-dim: #746357;
  --sm-text-faint: #aea195;
  --sm-border: #e3d6c3;
  --sm-border-strong: #cec0af;
  --sm-card-edge: #e3d6c3;
  --sm-skeleton: #f1e7d9;
  --sm-track: #ebdfd0;
  --sm-avatar-bg: #efe0d6;
  --sm-accent-2: #b89a7a;
  --sm-chip-bg: #f3dfe0;
  --sm-chip-ink: #8a4a55;
  --sm-pattern: radial-gradient(circle at 10px 10px, rgba(184, 154, 122, 0.16) 1.5px, transparent 2px), radial-gradient(circle at 30px 30px, rgba(184, 154, 122, 0.10) 1px, transparent 1.5px);
  --sm-tile-1: #f6e1e3;
  --sm-tile-2: #e8ecdf;
  --sm-tile-3: #f3e6d3;
  --sm-tile-4: #ece3ef;
  --sm-tile-5: #e3e9ea;
  --sm-inset-c: rgba(184, 154, 122, 0.45);
  --sm-shadow: 0 10px 30px rgba(120, 90, 70, 0.10);
  --sm-shadow-hover: 0 14px 36px rgba(120, 90, 70, 0.16);
}

/*
  A RESET, NOT A RULE.

  Modern — the default — is squared like the dashboard, and it was once
  enforced here with `border-radius: 0 !important` on everything. Themes are
  skins now and bring their own corners, so this is only the floor: no
  component arrives with a browser's own rounding, and every radius on the site
  comes from a token (--sm-radius, --sm-radius-sm, --sm-radius-pill,
  --sm-avatar-radius, --sm-badge-radius) that Modern sets to 0. A new
  component that wants corners reads one of those, never a number.
*/
*, *::before, *::after { border-radius: 0; }

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--sm-font-ui);
  background: var(--sm-bg-base);
  color: var(--sm-text);
  /* The skin's pattern — Futuristic's grid, Elegant's lace dots — on the page
     itself, under every card. `none` everywhere else. */
  background-image: var(--sm-pattern);
  background-size: var(--sm-pattern-size);
  font-size: var(--sm-body-size);
  line-height: 1.6;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
/*
  ROOM UNDER THE CONTENT. The panel's own margin is the gap between sections;
  this is the gap between the page and the line at the foot of it, which
  otherwise sits directly under the last sentence on a page long enough to
  scroll.

  1.5rem rather than 2.5rem: the panel already ends on its own 1.5rem of margin,
  so the two stacked to 72px of nothing above a footer rule — height a short
  page could not spare. See the note on .page-links.
*/
/* Nothing on any page may widen the document: wide content scrolls in a box of
   its own (see .adm-scroll). */
main { flex: 1 0 auto; padding-bottom: 1.5rem; overflow-x: hidden; }

/*
  LINKS WEAR THE COMMUNITY'S COLOUR, through the accent: `--sm-accent` is
  `--brand` lifted for a dark ground or deepened for a light one (see the mode
  blocks at the top), so a pale brand colour still reads as a link on paper
  and a dark one still reads on the dark ground. `--brand` itself is written
  into a nonced block by layout().
*/
a { color: var(--sm-accent); text-decoration: none; }
/*
  AN UNDERLINE IS FOR A WORD IN A SENTENCE, AND ONLY WHERE THERE IS A POINTER.

  This applied to every anchor on the site, and roughly twenty-five components
  then had to remember to switch it back off — each of them inside
  @media (hover: hover), which a phone never matches. So on touch the opt-outs
  were invisible and the underline won: tapping a card on a community's front
  page underlined the whole block, including the cards whose own stylesheet
  plainly said not to.

  Two changes, both narrowing rather than adding. The underline is offered only
  where a pointer exists, because a tap is not a hover and no phone browser
  should be drawing a hover state at all. And a box says once, below, that it
  is a box.
*/
@media (hover: hover) {
  a:hover { color: var(--sm-accent-hover); text-decoration: underline; }
}
a:active { color: var(--sm-accent-hover); }

/*
  A LINK THAT IS A BOX NEVER UNDERLINES — hover, tap, press or hold.

  One list rather than a line per component, and OUTSIDE any media query, so it
  holds for every input a browser has. These already refused the underline on
  hover; refusing it in every state is what stops a tap, and a finger held on a
  card, from drawing a line under a paragraph of layout.
*/
.hub-card, .doc-card, .board-link, .podium-card, .res-link, .prog-shelf, .cal-chip, .cal-step, .cal-today,
.unlock-open, .badge-tile, .pager-btn, .btn, .filter-tab, .ev-cat, .section-link,
.section-info, .theme-toggle, .sm-close, .doc-nav-link, .doc-nav-sublink,
.page-bar-mark, .page-bar-name, .rule-link, .page-links .by, .adm-tile-a,
.hub-card:hover, .doc-card:hover, .board-link:hover,
.podium-card:hover, .res-link:hover, .unlock-open:hover, .badge-tile:hover,
.pager-btn:hover, .btn:hover, .filter-tab:hover, .ev-cat:hover,
.section-link:hover, .section-info:hover, .theme-toggle:hover, .sm-close:hover,
.doc-nav-link:hover, .doc-nav-sublink:hover, .page-bar-mark:hover,
.page-bar-name:hover, .rule-link:hover, .page-links .by:hover, .adm-tile-a:hover,
.hub-card:active, .doc-card:active, .board-link:active,
.podium-card:active, .res-link:active, .unlock-open:active, .badge-tile:active,
.pager-btn:active, .btn:active, .filter-tab:active, .ev-cat:active,
.section-link:active, .section-info:active, .theme-toggle:active,
.sm-close:active, .doc-nav-link:active, .doc-nav-sublink:active,
.page-bar-mark:active, .page-bar-name:active, .rule-link:active,
.page-links .by:active, .adm-tile-a:active {
  text-decoration: none;
}
a:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }

h1 {
  margin: 0;
  font-family: var(--sm-title-font);
  font-size: calc(clamp(1.9rem, 5vw, 2.75rem) * var(--sm-title-scale));
  font-weight: var(--sm-title-weight);
  letter-spacing: var(--sm-title-tracking);
  text-transform: var(--sm-title-case);
  line-height: 1.1;
}
h2, h3 { font-weight: var(--sm-heading-weight); }
h2.section-title {
  margin: 2.1rem 0 0.85rem;
  font-size: 0.82rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--sm-text-dim);
  display: flex; align-items: center; gap: 0.6rem;
}
/*
  A heading can carry the way out of its own section: pushed to the far end,
  quiet, and dropping its own letter-spacing so it reads as a sentence rather
  than as a second heading.
*/
.section-link {
  margin-left: auto;
  font-size: 0.78rem; font-weight: 500;
  letter-spacing: 0; text-transform: none;
  color: var(--sm-text-dim);
  white-space: nowrap;
}
@media (hover: hover) { .section-link:hover { color: var(--sm-accent); text-decoration: none; } }

.muted { color: var(--sm-text-muted); }
.small { font-size: 0.86rem; }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1.25rem; }

/* ================================ Hero ================================= */
.hero {
  padding: 3.5rem 1.5rem 2.5rem;
  border-bottom: 1px solid var(--sm-border);
  background:
    radial-gradient(80rem 26rem at 50% -12rem, color-mix(in srgb, var(--sm-accent) 13%, transparent), transparent 70%),
    var(--sm-bg-base);
}
.hero-inner { max-width: 62ch; margin: 0 auto; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 0.85rem; }
.hero .lede { margin: 0; color: var(--sm-text-muted); }
.guild-icon { width: 88px; height: 88px; object-fit: cover; border: 1px solid var(--sm-border-strong); }

/* =============================== Panels ================================ */
/*
  The vertical air is 1.5rem, not 2rem, top and bottom. On a leaderboard the
  difference is invisible; on a screen holding one sentence it was a sixth of
  the page spent on margin, and on a short viewport it was part of what made
  that screen scroll. Horizontal padding is untouched — that one is the gutter
  on a phone, and it was already right.
*/
.panel { max-width: 860px; margin: 1.5rem auto; padding: 0 1.25rem 1.5rem; }
.panel-narrow { max-width: 60ch; text-align: center; }
.panel-narrow h1 { margin-bottom: 0.75rem; }

.notice { padding: 0.75rem 1rem; border: 1px solid var(--sm-border-strong); background: var(--sm-bg-deep); margin: 0 0 1.25rem; }
.notice-warn { border-color: color-mix(in srgb, var(--sm-warning) 40%, transparent); color: var(--sm-warning); }

/* =============================== Board ================================= */
.board { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.board-link {
  display: grid;
  grid-template-columns: 3.25rem 40px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0.9rem;
  padding: 0.6rem 1rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  color: var(--sm-text);
}
@media (hover: hover) { a.board-link:hover { background: var(--sm-bg-hover); border-color: var(--sm-border-strong); text-decoration: none; } }
.board-link-static { cursor: default; }

.rank { font-family: var(--sm-font-mono); font-size: 1rem; color: var(--sm-text-dim); text-align: right; font-variant-numeric: tabular-nums; }
.rank-1 { color: var(--sm-gold); font-weight: 600; }
.rank-2 { color: var(--sm-silver); font-weight: 600; }
.rank-3 { color: var(--sm-bronze); font-weight: 600; }

.avatar { width: 40px; height: 40px; object-fit: cover; background: var(--sm-bg-deep); }
.avatar-fallback { display: grid; place-items: center; font-weight: 600; color: var(--sm-text-dim); border: 1px solid var(--sm-border); }

.who { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.level { color: var(--sm-text-muted); font-size: 0.86rem; white-space: nowrap; }
.xp { font-family: var(--sm-font-mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.xp-unit { color: var(--sm-text-dim); font-size: 0.8rem; }

/* The level column is the first thing to go: it is derivable from the XP beside
   it, and a name that has to be truncated to make room for a restatement is the
   wrong trade on a phone. */
@media (max-width: 560px) {
  .board-link { grid-template-columns: 2.5rem 32px minmax(0, 1fr) auto; gap: 0.65rem; padding: 0.55rem 0.75rem; }
  .avatar { width: 32px; height: 32px; }
  .level { display: none; }
}

.pager { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: 1.25rem; }
.pager-btn { padding: 0.45rem 0.9rem; border: 1px solid var(--sm-border-strong); color: var(--sm-accent); }
@media (hover: hover) { a.pager-btn:hover { background: var(--sm-bg-hover); text-decoration: none; } }
.pager-off { color: var(--sm-text-dim); border-color: var(--sm-border); }
.pager-count { color: var(--sm-text-dim); font-size: 0.86rem; font-variant-numeric: tabular-nums; }

/* =============================== Footer ================================ */
/*
  A LINE, NOT A BAND — AND THE REASON IS MEASURED, NOT TASTE.

  At 2rem of padding this was 88px of chrome at the foot of every page, and on
  the short appeals screens that was the difference between fitting and not: at
  1440x640 the "no active ban" screen came to 656px against a 640px viewport,
  so the last thing an appellant saw was a scrollbar on a page holding one
  sentence. Measured in a headless browser rather than guessed — every appeals
  screen was over by less than the footer's own height.

  So the footer carries what a footer is for and no more room than that. Its
  links are secondary on every page here: somebody on the appeals flow wants
  the sentence in the middle of the screen, not Privacy.
*/
.page-links {
  flex: 0 0 auto;
  display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 0.4rem;
  padding: 0.85rem 1.5rem;
  border-top: 1px solid var(--sm-border);
  font-size: 0.82rem;
}
/* Privacy, Terms, Appeals on one line; "By ServerMod.App" on the next. */
.page-links-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.35rem 1.25rem; }
.page-links a { color: var(--sm-text-dim); }
@media (hover: hover) { .page-links a:hover { color: var(--sm-accent-hover); } }

/* ================================ Appeals =============================== */
/*
  The appeals flow. Deliberately plainer than the leaderboard: somebody reading
  these pages has just been removed from a community and is looking for one
  fact, so the pages are narrow, the type is a little larger and there is
  nothing on them that is not part of the answer.
*/
.panel-form { max-width: 46rem; }
.panel-form h1 { margin-bottom: 0.75rem; font-size: clamp(1.6rem, 4vw, 2.1rem); }
.panel-form .stack { display: flex; flex-direction: column; gap: 0.75rem; align-items: flex-start; }
.panel-form .stack-item { display: flex; flex-direction: column; gap: 0.35rem; width: 100%; }

.form-label { font-weight: 600; font-size: 0.95rem; }
/* ---- Editing your own About ------------------------------------------
   Every rule here reads a token, so the form takes each of the six skins and
   both modes: .field already themes the inputs, --sm-badge-radius rounds the
   disclosure to whatever the skin rounds things to, and --sm-accent is the
   community's own colour. */
.info-edit {
  margin-top: 1rem;
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-badge-radius, 0);
  background: var(--sm-bg-raised);
}
.info-edit-open {
  /* A real 44px target: this is the control that turns a page into a form, and
     it is most often reached with a thumb. */
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.9rem;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--sm-text-muted);
  list-style: none;
}
.info-edit-open::-webkit-details-marker { display: none; }
.info-edit-open::after {
  content: "";
  margin-left: auto;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.6;
  transition: transform 0.12s ease;
}
.info-edit[open] > .info-edit-open { color: var(--sm-text); }
.info-edit[open] > .info-edit-open::after { transform: rotate(-135deg); }
.info-edit-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.9rem;
  padding: 0 0.9rem 0.9rem;
}
.info-field { display: flex; flex-direction: column; gap: 0.3rem; }
.info-field > span:first-child {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sm-text-dim);
}
.info-bday { display: flex; gap: 0.5rem; }
.info-bday .field { flex: 1; min-width: 0; }
.info-hint { font-size: 0.76rem; color: var(--sm-text-dim); line-height: 1.45; }
/* The Save row spans the grid so the button never ends up alone in a column
   beside an empty one. */
.info-edit-act {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 0.8rem;
  flex-wrap: wrap;
}
.info-editnote { margin: 0.9rem 0 0; }
.info-flash {
  margin: 0.9rem 0 0;
  padding: 0.65rem 0.85rem;
  border: 1px solid var(--sm-border);
  border-left-width: 3px;
  border-radius: var(--sm-badge-radius, 0);
  font-size: 0.88rem;
}
.info-flash-ok { border-left-color: var(--sm-success); }
/* Warn, not danger: there is no danger token, and "we could not read that
   country" is a thing to correct rather than a thing that broke. */
.info-flash-warn { border-left-color: var(--sm-warning); }

/* ---- Shop -------------------------------------------------------------
   Built entirely from the existing tokens, so the six skins and both modes
   come free: --sm-bg-raised for the card, --sm-card-edge for its border,
   --sm-badge-radius for its corners (0 on Modern, 16px on Playful, 50% on
   Cosy) and --sm-accent for the price and the Buy button, which is the
   community's own colour because the instance sets it per render.

   Nothing here names a hex. A hardcoded squared card would be wrong on four
   of the six themes, which is the mistake the mockups made before the real
   token set was read. */
.shop-collection { margin-bottom: 2rem; }
.shop-section { margin-top: 1.25rem; }
.shop-section-title {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.82rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--sm-text-dim);
  margin: 0 0 0.75rem;
}
/* A locked section is SHOWN, not hidden. A member-level gate is a goal, and
   "Level 20" is more use than making the things behind it invisible — which
   is a different thing from a server switch being off, where absence is
   right. */
.shop-lock {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--sm-text-dim);
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-badge-radius, 0);
  padding: 0.1rem 0.5rem;
}
.shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.85rem;
}
.shop-item {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  /* Dressed like a hub card, in every skin (2026-09-24): the same background,
     edge width and — through the skin list further down — the same radius,
     shadow and clip. It was a thin plain box inside a themed page. */
  background: var(--card-bg, var(--sm-bg-raised));
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  padding: 1.1rem 1.1rem 1rem;
}
/* Unavailable is dimmed, never removed: somebody wants to know what they are
   working towards, and a shelf that hides what you cannot afford is a shelf
   that looks empty when you are poor. */
.shop-item.is-off { opacity: 0.66; }
.shop-item-head { display: flex; align-items: flex-start; gap: 0.6rem; }
.shop-item-icon {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  object-fit: cover;
  border-radius: var(--sm-badge-radius, 0);
}
.shop-item-icon-blank { background: var(--sm-avatar-bg); display: block; }
.shop-item-name { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.shop-item-name strong { overflow-wrap: anywhere; }
.shop-kind {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sm-text-dim);
}
.shop-item-desc {
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.45;
  color: var(--sm-text-muted);
  flex-grow: 1;
}
.shop-item-foot {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  border-top: 1px solid var(--sm-border);
  padding-top: 0.55rem;
}
.shop-price { font-weight: 700; color: var(--sm-accent); flex-grow: 1; }
/* A custom currency emoji, drawn from Discord's CDN at text height. */
.cur-emoji { display: inline-block; width: 1.15em; height: 1.15em; vertical-align: -0.2em; object-fit: contain; }
.shop-stock { font-size: 0.78rem; color: var(--sm-text-dim); }
.shop-item-act { display: flex; }
.shop-buy { width: 100%; }
.shop-buy-btn { width: 100%; }
/* The command, for a signed-out reader and for anyone who would rather type
   it. Monospace because it is a thing to copy exactly. */
.shop-cmd {
  display: block;
  width: 100%;
  font-size: 0.78rem;
  color: var(--sm-text-dim);
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-badge-radius, 0);
  padding: 0.4rem 0.55rem;
  overflow-wrap: anywhere;
}
/* Why you cannot buy it, in the same slot the button would occupy, so the
   grid does not shuffle between affordable and not. */
.shop-state {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--sm-text-dim);
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-badge-radius, 0);
  padding: 0.45rem 0.6rem;
}
.shop-state-limit { color: var(--sm-success); border-color: var(--sm-success); }
/* The heading and the balance on one line: the pill sits top right. */
.shop-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.shop-head h1 { margin: 0; }
.shop-balance {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.3rem 0.85rem;
  border: var(--sm-card-bw, 1px) solid var(--sm-card-edge);
  border-radius: var(--sm-radius-pill, 999px);
  background: var(--sm-bg-raised);
  font-size: 0.95rem; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--sm-accent);
  white-space: nowrap;
}
/* The owner's own published page: "Your page is public" and Make private. */
.publish-state { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.publish-state p { margin: 0; }
.publish-state-quiet { margin: 0 0 1rem; }
/* A birthday field the member has not unlocked yet. */
.info-bday.is-locked { opacity: 0.5; }
.info-bday.is-locked .field { cursor: not-allowed; }
.shop-flash {
  border: 1px solid var(--sm-border);
  border-left-width: 3px;
  border-radius: var(--sm-badge-radius, 0);
  padding: 0.7rem 0.9rem;
  margin: 0 0 1.25rem;
}
.shop-flash-ok { border-left-color: var(--sm-success); }
/* Warn, not danger — there is no danger token, and a timeout is genuinely
   "we do not know yet" rather than "it broke". See the note in the mockup. */
.shop-flash-warn { border-left-color: var(--sm-warning); }

/* The intro panel: a member’s own words, above everything the bot counted.
   Built from the same tokens as every other panel so it takes each skin’s
   ground, edge and radius without a line of theme-specific CSS. */
.intro-panel {
  background: var(--sm-bg-raised);
  border: 1px solid var(--sm-card-edge);
  border-radius: var(--sm-badge-radius, 0);
  padding: 1rem 1.1rem;
}
.intro-quote {
  margin: 0;
  /* The accent bar is what says "quoted", and it is the community’s own
     colour because --sm-accent is set per guild at render time. */
  border-left: 3px solid var(--sm-accent);
  padding-left: 0.9rem;
  color: var(--sm-text);
  line-height: 1.6;
  /* An intro is somebody typing at length. Long enough to read, short enough
     not to push the whole page down; the rest scrolls inside the quote. */
  max-height: 22rem;
  overflow-y: auto;
  overflow-wrap: anywhere;
}
.intro-meta { margin: 0.75rem 0 0; }
.field {
  width: 100%;
  padding: 0.65rem 0.8rem;
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border-strong);
  color: var(--sm-text);
  font: inherit;
}
.field:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 1px; border-color: var(--sm-accent); }
textarea.field { resize: vertical; min-height: 6rem; }

.btn {
  display: inline-block;
  padding: 0.6rem 1.2rem;
  border: 1px solid var(--sm-border-strong);
  background: var(--sm-bg-raised);
  color: var(--sm-text);
  font: inherit;
  cursor: pointer;
}
.btn:focus-visible, .linkish:focus-visible, summary:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.btn-primary { background: var(--sm-accent); border-color: var(--sm-accent); color: var(--sm-accent-fg); font-weight: 600; }
/* Discord blurple, because it is Discord's brand on Discord's sign-in. The mark
   sits beside the words, so the control looks like what it does before anybody
   presses it. */
.btn-discord {
  display: inline-flex; align-items: center; gap: 0.55rem;
  background: #5865f2; border-color: #5865f2; color: #fff; font-weight: 600;
}
.btn-mark { flex: 0 0 auto; }
@media (hover: hover) {
  .btn:hover { background: var(--sm-bg-hover); text-decoration: none; }
  .btn-primary:hover { background: var(--sm-accent-hover); color: var(--sm-accent-fg); }
  .btn-discord:hover { background: #4752c4; color: #fff; }
}
.btn-quiet { padding: 0.35rem 0.75rem; font-size: 0.86rem; color: var(--sm-text-muted); }

.hint { color: var(--sm-text-dim); font-size: 0.86rem; margin: 0; }
.mt { margin-top: 1.25rem; }
.notice-ok { border-color: color-mix(in srgb, var(--sm-success) 45%, transparent); color: var(--sm-success); }
.notice-ok svg { vertical-align: -0.15em; margin-right: 0.5rem; }

/* A card around one step's controls: the community box, the sign-in button. */
.panel-card { padding: 1rem 1.15rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); }
.panel-card-discord { display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem; }
.panel-card-discord > p { margin: 0; }

/* .whoami is styled with .account-strip on the top strip above. */

.reason-block {
  margin: 1rem 0;
  padding: 0.85rem 1rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-left: 3px solid var(--sm-accent);
}
/*
  PRE-WRAP BELONGS ON THE REASON, NOT ON THE BOX AROUND IT.

  It was on .reason-block, which also holds the "The reason recorded at the
  time" label and, in the source, the newlines and eight spaces of indentation
  between the two — all of which pre-wrap dutifully rendered. A one-line ban
  reason came out as a blank line, an indented label, another blank line and an
  indented reason, inside a box twice the height it needed. Here it does what
  it was added for: a moderator who wrote a reason over three lines gets three
  lines.
*/
.reason-text { white-space: pre-wrap; }

.appeal-card { margin: 1.25rem 0; padding: 1rem 1.1rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); }
.appeal-card header { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.5rem; }
.appeal-card p { margin: 0.35rem 0; }

/* The state colours the LABEL only - the moderator's reason under it is theirs,
   not the decision's. See appealCard in appealsPages.js. */
.state { display: inline-flex; align-items: center; gap: 0.45rem; font-weight: 600; }
.state-dot { width: 8px; height: 8px; flex: 0 0 auto; background: currentColor; }
.state-ok { color: var(--sm-success); }
.state-part { color: var(--sm-warning); }
.state-no { color: var(--sm-rose); }
.state-wait { color: var(--sm-accent-hover); }
.state-off { color: var(--sm-text-dim); }
.withdraw-row {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 0.5rem 0.75rem;
  margin-top: 0.9rem; padding-top: 0.9rem;
  border-top: 1px solid var(--sm-border);
}

/* ============================== The top strip ===========================
   ONE BAND ABOVE THE PAGE: the light/dark toggle, then who is signed in.

   Both used to have a row of their own, which on a phone put three bands of
   chrome — toggle, account, the page's own bar — above the first thing anybody
   came to read. Right-aligned, small, and quiet: this site is public and
   read-only, and an account bar the size of a nav would suggest otherwise. */
.top-bar {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: flex-end;
  flex-wrap: nowrap; gap: 0.6rem;
  padding: 0.5rem 1rem;
  border-bottom: 1px solid var(--sm-border);
  font-size: 0.82rem;
  color: var(--sm-text-dim);
  min-width: 0;
}
.account-strip, .whoami {
  display: inline-flex; align-items: center; gap: 0.5rem;
  margin: 0;
  /* So the NAME is what shortens when there is not enough room, rather than the
     row breaking or the sign-out being pushed off the end. */
  min-width: 0;
}
.account-name, .whoami-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--sm-text-muted); font-weight: 600;
}
.account-strip .signout, .whoami .signout, .account-signin { flex: 0 0 auto; }
/*
  The avatar and name are ONE element — a link to your own profile inside a
  community, a plain span outside one — so the name's ellipsis lives on it.
*/
.account-me {
  display: inline-flex; align-items: center; gap: 0.5rem;
  min-width: 0; color: inherit; text-decoration: none;
}
a.account-me:hover, a.account-me:active { text-decoration: none; }
@media (hover: hover) { a.account-me:hover .account-name { color: var(--sm-accent); } }
a.account-me:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.account-signin span { white-space: nowrap; }
.account-avatar, .whoami img { width: 22px; height: 22px; object-fit: cover; background: var(--sm-bg-deep); }
.account-link { color: var(--sm-accent-hover); text-decoration: none; }
/* The Discord mark sits with the words rather than after them, sized to the
   strip's own type rather than the button's. */
.account-signin { display: inline-flex; align-items: center; gap: 0.4rem; }
.account-signin .btn-mark { width: 15px; height: 15px; }
@media (hover: hover) { .account-link:hover { text-decoration: underline; } }
/* Sign-out is a form, because a GET must not end a session — see signOutForm in
   pages.js. Nothing about it should look like a form. */
.signout { display: inline; margin: 0; }
.linkish {
  background: none; border: 0; padding: 0;
  font: inherit; line-height: inherit;
  cursor: pointer;
}
@media (hover: hover) { .linkish:hover { text-decoration: underline; } }
/* Sign-out is a glyph the size of the theme toggle beside it; the words are
   its aria-label and title. */
.signout-btn {
  display: inline-grid; place-items: center;
  width: 24px; height: 24px;
  padding: 0; border: 0; background: none;
  color: var(--sm-text-dim);
  font-size: 0.9rem; line-height: 0;
  cursor: pointer;
}
@media (hover: hover) { .signout-btn:hover { color: var(--sm-accent); } }
.signout-btn:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }

/* ========================= Light / dark toggle ==========================
   A link, because there is no JavaScript on this site. ONE icon, showing where
   the click goes — a moon in light mode, a sun in dark. It lives on the top
   strip above, with the account beside it. */
/*
  NO BOX. It was a bordered square, which on a strip with the account beside it
  read as the one CONTROL on the page — a button competing for attention with
  the leaderboard. It is a preference, not an action: the icon alone, at the
  end of the strip, warming to the accent when it is under the pointer.
*/
.theme-toggle {
  display: inline-grid; place-items: center;
  width: 24px; height: 24px;
  color: var(--sm-text-dim);
  line-height: 0;
}
@media (hover: hover) { .theme-toggle:hover { color: var(--sm-accent); text-decoration: none; } }
.theme-toggle:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }

/* ====================== The empty / coming-soon page =====================
   One page for a slug nobody registered, a community that has not switched
   publishing on, and a member page that does not exist — see notFoundPage.
   It reads as a placeholder rather than an error, because most people who
   land here followed a community's own link. */
.panel-empty { padding-top: 4rem; }
.panel-empty h1 { margin-bottom: 0.6rem; }
.panel-empty p { margin: 0 0 0.5rem; }
.empty-mark {
  display: grid; place-items: center;
  width: 62px; height: 62px; margin: 0 auto 1.25rem;
  border: 2px solid var(--sm-accent);
  color: var(--sm-accent);
}

/* ============================ The appeals flow ==========================
   Same tokens as the portal and deliberately calmer with them. Somebody
   reading these has just been removed from a community: narrow column, no
   banner, and colour only where it carries meaning — what a decision was, or
   which refusal this is. See the note at the top of appealsPages.js. */

/*
  THE PANEL SITS IN THE MIDDLE OF WHAT IS LEFT.

  These screens are short and a desktop window is not: "There is no active ban
  on this account" was a sentence in the top-left corner of six hundred pixels
  of nothing. The bar keeps the top — it is a header, and a header that drifts
  down the page is not one — and the panel takes the rest.

  AUTO MARGINS, NOT justify-content: center. An auto margin gives up when there
  is no free space to take, so the tall screens in this flow — the form, an
  outcome with prior appeals under it — start at the top and scroll normally,
  rather than having their headings pushed off the top edge to keep a centre.
  The panel's own top padding is what keeps it off the bar once the margins
  have collapsed to nothing.
*/
.appeals-page main { display: flex; flex-direction: column; }
.appeals-page main > .appeals-bar { flex: 0 0 auto; }
.appeals-page main > .panel { margin-top: auto; margin-bottom: auto; padding-top: 1.5rem; }

/* The four steps. Short, but nobody arriving here knows that. */
.step-rail { list-style: none; margin: 0 0 1.25rem; padding: 0; display: flex; gap: 0.4rem; }
.step { flex: 1 1 0; border-top: 3px solid var(--sm-border); padding-top: 0.5rem; }
.step-n {
  font-family: var(--sm-font-mono); font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.1em; color: var(--sm-text-faint);
}
.step-label { font-size: 0.82rem; color: var(--sm-text-dim); }
.step.is-done { border-top-color: var(--sm-accent); }
.step.is-here { border-top-color: var(--sm-accent); }
.step.is-here .step-n { color: var(--sm-accent); }
.step.is-here .step-label { color: var(--sm-text); }
@media (max-width: 520px) { .step-label { font-size: 0.74rem; } }

/* The bar every screen after the first one carries. */
.appeals-bar {
  display: flex; align-items: center; gap: 0.8rem;
  /* 0.6rem, not 0.85: this is a band of chrome above the page's own heading,
     and the 30px mark inside it already sets the bar's height. */
  padding: 0.6rem 1.5rem;
  border-bottom: 1px solid var(--sm-border);
  background: var(--sm-bg-deep);
  flex-wrap: wrap;
}
.appeals-bar .sep { color: var(--sm-text-faint); }
.appeals-bar .where { font-weight: 700; letter-spacing: -0.01em; }
/* The guild's OWN icon. The initial tile is only the fallback for a server
   that has none — see safeAvatar in pages.js. */
.guild-mark { width: 30px; height: 30px; object-fit: cover; display: block; background: var(--sm-bg-base); }
.guild-mark-fallback {
  display: grid; place-items: center;
  font-weight: 800; font-size: 0.78rem;
  color: var(--sm-text-dim); border: 1px solid var(--sm-border-strong);
}

/* What is being appealed. No case number: it is an internal reference the
   appellant has no use for. */
.fact-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; }
.fact-grid > div { background: var(--sm-bg-base); border: var(--sm-card-bw) solid var(--sm-card-edge); padding: 0.6rem 0.75rem; }
.fact-k {
  font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--sm-text-dim); font-weight: 600;
}
.fact-v { font-family: var(--sm-font-mono); font-size: 1rem; font-weight: 700; margin-top: 0.1rem; }
@media (max-width: 520px) { .fact-grid { grid-template-columns: 1fr 1fr; } }

.sanction-panel { background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); border-left: 4px solid var(--sm-rose); padding: 1.15rem 1.3rem; }
.sanction-head { font-size: 0.76rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--sm-text-dim); margin-bottom: 0.8rem; }

/* Three promises, as ticks rather than a paragraph nobody reads. */
.checklist { display: flex; flex-direction: column; gap: 0.6rem; margin: 0; padding: 0; list-style: none; }
.checklist li { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.65rem; align-items: start; }
.checklist svg { margin-top: 0.25rem; }
.checklist span { color: var(--sm-text-muted); font-size: 0.9rem; }

/* The flow ending well: nothing to appeal. */
.panel-ok { background: var(--sm-bg-raised); border: 1px solid color-mix(in srgb, var(--sm-success) 45%, transparent); border-top: 4px solid var(--sm-success); padding: 1.8rem; }
.panel-ok h1 { font-size: 1.7rem; color: var(--sm-success); margin-bottom: 0.35rem; }
.ok-head { display: flex; align-items: flex-start; gap: 1rem; }
.ok-head p { margin: 0; }
.ok-mark {
  width: 46px; height: 46px; flex: 0 0 auto;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--sm-success) 14%, transparent); border: 1px solid color-mix(in srgb, var(--sm-success) 45%, transparent);
  color: var(--sm-success);
}
.why-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.55rem; }
.why-list li { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.65rem; align-items: start; color: var(--sm-text-muted); font-size: 0.9rem; }
.why-dot { width: 5px; height: 5px; background: var(--sm-text-faint); margin-top: 0.6rem; }

/* One centred line. Privacy and Terms were competing with the only thing on
   the page that matters. */
/* .appeals-footer is gone with the second footer it styled — layout()'s own
   footer is the only one on either host now, at the foot of the page. */

/* ============================ Community banner ==========================
   The community's own image where it set one, and a wash of the accent where
   it did not — a guild that has never opened Settings › Portal still gets a
   header with a shape rather than a bare heading. */
.banner {
  position: relative;
  min-height: 240px;
  display: flex; align-items: flex-end;
  border-bottom: 1px solid var(--sm-border);
  /* The community's own embed colour where it set one, and the site accent
     where it did not. One custom property, validated as six hex digits before
     it reaches the page - see safeHex in pages.js. */
  --banner-fill: linear-gradient(115deg,
    var(--sm-brand) 0%,
    color-mix(in srgb, var(--sm-brand) 45%, var(--sm-bg-deep)) 42%,
    var(--sm-bg-base) 100%);
  background: var(--banner-fill);
  overflow: hidden;
}
.banner-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; color: transparent; font-size: 0; }
/* A banner that will not load falls back to the colour alone. There is no
   script on this site to catch onerror, but an <img> only draws ::after when
   its image failed (Chrome, Firefox), so this paints the same fill over the
   broken-image icon. A loaded image never shows it. */
.banner-img::after {
  content: "";
  position: absolute; inset: 0;
  background: var(--banner-fill);
}
/* Always over the image, so the name stays readable on whatever was uploaded —
   a bright banner must not be able to make a community's own title vanish. */
.banner-wash {
  position: absolute; inset: 0;
  background: linear-gradient(to top,
    var(--sm-bg-base) 0%,
    color-mix(in srgb, var(--sm-bg-base) 72%, transparent) 46%,
    transparent 100%);
}
.banner-inner {
  position: relative;
  width: 100%; max-width: 940px; margin: 0 auto;
  padding: 0 1.5rem 1.4rem;
  display: flex; align-items: flex-end; gap: 1.1rem;
}
/* NO FRAME. The accent border read as a selection or a state — something the
   page had done to the icon — when it is simply the server's own picture. The
   banner behind it is all the separation it needs. */
.banner .guild-icon {
  width: 96px; height: 96px; flex: 0 0 auto;
  border: 0;
  background: var(--sm-bg-deep);
  font-size: 2.2rem;
}
.banner-text { padding-bottom: 0.35rem; min-width: 0; }
/* Smaller than a page title, because it sits under a 96px icon and above a
   member count — the three together are the header, and the name shouting over
   the other two made the block read as one enormous word. */
.banner-text h1 {
  font-size: calc(clamp(1.45rem, 4.6vw, 2.15rem) * var(--sm-title-scale));
  overflow-wrap: anywhere;
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem;
}
.banner-text .lede { margin: 0.15rem 0 0; color: var(--sm-text-muted); }
@media (max-width: 560px) {
  .banner { min-height: 190px; }
  .banner .guild-icon { width: 68px; height: 68px; font-size: 1.6rem; }
}

/* The community's own words, under its banner. Centred and set at body size:
   it is an introduction, not the page's headline, and left-ragged at 1.15rem it
   was competing with the cards below it. */
.intro {
  margin: 0 auto 1.9rem;
  font-size: 1rem; line-height: 1.6;
  color: var(--sm-text-muted); max-width: 56ch;
  text-align: center;
}

/* ============================== Hub cards ===============================
   One per page the community actually publishes — sections decides, so there
   is never a card onto a 404. */
.hub-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
}
.hub-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.85rem; height: 100%;
  padding: 1.3rem 1.25rem 1.15rem;
  background: var(--card-bg, var(--sm-bg-raised));
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-top: 4px solid var(--hue, var(--sm-accent));
  color: var(--sm-text);
}
/*
  A CARD LIFTS. IT DOES NOT REPAINT ITS OWN TEXT.

  A hub card is an <a>, so `a:hover` was setting its colour to the community's
  brand — and because the title and the standfirst inherit, the whole card
  flashed to the guild colour and back on every pass of the pointer. On the
  landing page, where the cards are most of what there is, that is the page
  blinking rather than an element answering.

  The card still answers, in the way a card should: it comes forward. A shadow
  and the raised background are a change in DEPTH rather than in hue, they read
  the same on all six portal themes, and they do not ask the reader's eye to
  re-acquire words it was already reading. The brand colour stays where it is
  deliberate — the top edge, the icon and the go-line — and those do not move.
*/
@media (hover: hover) {
  .hub-card:hover {
    background: var(--sm-bg-hover);
    border-color: var(--sm-border-strong);
    border-top-color: var(--hue, var(--sm-accent));
    color: var(--sm-text);
    box-shadow: inset 0 0 0 var(--sm-inset-w) var(--sm-bg-hover), inset 0 0 0 calc(var(--sm-inset-w) + 1px) var(--sm-inset-c), var(--sm-shadow-hover);
    text-decoration: none;
  }
}
.hub-icon { color: var(--hue, var(--sm-accent)); }
.hub-title { font-family: var(--sm-font-display); text-transform: var(--sm-display-case); font-size: 1.3rem; font-weight: 700; letter-spacing: min(var(--sm-display-tracking), 0.08em); margin-top: 0.85rem; }
.hub-say { color: var(--sm-text-muted); font-size: 0.92rem; margin-top: 0.3rem; flex: 1 1 auto; }
.hub-go {
  font-family: var(--sm-font-mono); font-size: 0.84rem; font-weight: 500;
  color: var(--hue, var(--sm-accent)); margin-top: 0.9rem;
}

/* ======================= Sub-page bar (portal) ==========================
   Server icon left, page name beside it — the same shape the appeals flow
   uses, so the two halves of the site read as one product. */
.page-bar {
  display: flex; align-items: center; gap: 0.85rem;
  padding: 0.85rem 1.5rem;
  border-bottom: 1px solid var(--sm-border);
  background: var(--sm-bg-deep);
}
.page-bar-mark { display: block; line-height: 0; }
/* Unframed, like the big one in the banner: it is the server’s own picture. */
.page-bar .guild-mark { width: 38px; height: 38px; border: 0; }
.page-bar-name { color: var(--sm-text-muted); font-weight: 600; }
@media (hover: hover) { .page-bar-name:hover { color: var(--sm-accent); } }
.page-bar .sep { color: var(--sm-text-faint); }
.page-bar .where { font-weight: 700; letter-spacing: -0.01em; }

/* ============================== The podium ==============================
   The first three, on the first page only. */
.podium {
  list-style: none; margin: 0 0 1.4rem; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.75rem;
}
.podium-card {
  display: flex; align-items: center; gap: 0.9rem; height: 100%;
  padding: 1.1rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-top: 4px solid var(--medal, var(--sm-text-dim));
  color: var(--sm-text);
}
@media (hover: hover) { a.podium-card:hover { background: var(--sm-bg-hover); border-color: var(--sm-border-strong); border-top-color: var(--medal); text-decoration: none; } }
.podium-slot.rank-1 .podium-card { --medal: var(--sm-gold); }
.podium-slot.rank-2 .podium-card { --medal: var(--sm-silver); }
.podium-slot.rank-3 .podium-card { --medal: var(--sm-bronze); }
.podium-avatar {
  width: 56px; height: 56px; flex: 0 0 auto; object-fit: cover;
  background: var(--sm-bg-base); border: 1px solid var(--sm-border);
  font-size: 1.3rem;
}
.podium-body { min-width: 0; display: flex; flex-direction: column; }
.podium-place {
  font-family: var(--sm-font-mono); font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.12em; color: var(--medal);
}
.podium-name { font-weight: 700; font-size: 1.05rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.podium-xp { font-family: var(--sm-font-mono); font-size: 0.82rem; color: var(--sm-text-muted); }
@media (max-width: 640px) {
  .podium { grid-template-columns: 1fr; gap: 0.4rem; }
  .podium-avatar { width: 44px; height: 44px; }
}

/* =========================== Achievement rows ===========================
   A count and never a holder. */
.ach-list {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); gap: 0.6rem;
}
.ach-item {
  display: grid; grid-template-columns: 52px minmax(0, 1fr);
  gap: 0.9rem; align-items: start;
  padding: 0.95rem 1.05rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-top: 3px solid var(--sm-accent);
}
/*
  AND THE ACHIEVEMENTS LIST, where the rotation was by POSITION — hues[i % 4] —
  so a badge's colour was decided by how many rows happened to sit above it and
  moved the moment one was added or earned. A colour that means nothing and
  changes anyway is worse than no colour: this is the same fault the events
  page fixed by taking the operator's own token, and there is no token here to
  take. The community's colour is the honest answer.
*/
.ach-icon {
  width: 52px; height: 52px; display: grid; place-items: center;
  background: var(--sm-bg-base); border: 1px solid var(--sm-border);
  font-size: 1.5rem; line-height: 1; text-align: center;
}
/* The stand-in where a badge has neither an emoji nor artwork this site can
   reach yet. Deliberately quiet — it is a letter, not a claim to be an icon. */
.ach-initial {
  display: grid; place-items: center;
  width: 2rem; height: 2rem;
  border: 1px solid var(--hue, var(--sm-border-strong));
  color: var(--hue, var(--sm-text-dim));
  font-size: 0.9rem; font-weight: 700;
}
.ach-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.ach-desc { color: var(--sm-text-muted); font-size: 0.86rem; }
@media (max-width: 480px) { .ach-count-unit { display: none; } }
.ach-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
.ach-name { font-weight: 700; font-size: 1.02rem; }
/*
  A rung's rank, sitting with its name in the list the way it sits under the
  name on a tile. Needs the gap and the lift because here it follows running
  text rather than standing on its own line.
*/
.ach-name .tier { margin: 0 0 0 0.4rem; vertical-align: 0.06em; }
.ach-count {
  font-family: var(--sm-font-mono); font-size: 0.82rem; font-weight: 700;
  color: var(--sm-accent); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/*
  A RUNG'S RANK: one to four stars in the tier's metal. tierStars() in pages.js
  has drawn this markup since the tiers arrived, and nothing coloured it — the
  stars sat in the text colour with the tier's NAME printed after them,
  because `.sr-only` did not exist either. Both are here now.
*/
.metal-stone, .tier-stone { --metal: var(--sm-tier-stone); }
.metal-bronze, .tier-bronze { --metal: var(--sm-tier-bronze); }
.metal-silver, .tier-silver { --metal: var(--sm-tier-silver); }
.metal-gold, .tier-gold { --metal: var(--sm-tier-gold); }
.metal-diamond, .tier-diamond { --metal: var(--sm-tier-diamond); }
.metal-none { --metal: var(--sm-text-faint); }
.tier {
  display: inline-flex; align-items: center; gap: 1px;
  color: var(--metal, var(--sm-text-dim));
  font-size: 0.72rem; line-height: 1;
  vertical-align: middle; white-space: nowrap;
}
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* A badge's own artwork, proxied through this origin. */
.badge-img { width: 40px; height: 40px; object-fit: contain; display: block; }
.ach-icon .badge-img { width: 34px; height: 34px; }

/* One line, centred, and the only thing at the foot of any page. */
.page-links .by { color: var(--sm-text-dim); }
.page-links .by strong { color: var(--sm-accent); font-weight: 600; }
@media (hover: hover) { .page-links .by:hover { color: var(--sm-accent); text-decoration: none; } }

/* Partner and Verified: Discord's own badge files, the same two the dashboard
   renders. No box and no label around them — they ARE the artwork, and a border
   would frame somebody else's mark as if this site had issued it. The name they
   sit beside is the label; `alt` carries it for a screen reader. */
.guild-badges { display: inline-flex; align-items: center; gap: 0.35rem; vertical-align: middle; }
.guild-badge {
  width: 22px; height: 22px;
  object-fit: contain;
  display: inline-block;
  /* Level with the cap-height of the name rather than sitting on its baseline. */
  vertical-align: -0.15em;
}
.banner-text h1 .guild-badges { margin-left: 0.15rem; }

/* ---------------------------------------------------------------------------
   WIDTHS AS CLASSES.

   This site's CSP carries no 'unsafe-inline', and browsers apply style-src to
   style ATTRIBUTES as well as to blocks — so `style="width: 42%"` is dropped
   without a word and the bar draws at nothing. Every proportional width on
   these pages therefore comes from here, rounded to 5%: a rarity bar is a
   shape, not a measurement.
--------------------------------------------------------------------------- */
.w-5 { width: 5%; } .w-10 { width: 10%; } .w-15 { width: 15%; } .w-20 { width: 20%; }
.w-25 { width: 25%; } .w-30 { width: 30%; } .w-35 { width: 35%; } .w-40 { width: 40%; }
.w-45 { width: 45%; } .w-50 { width: 50%; } .w-55 { width: 55%; } .w-60 { width: 60%; }
.w-65 { width: 65%; } .w-70 { width: 70%; } .w-75 { width: 75%; } .w-80 { width: 80%; }
.w-85 { width: 85%; } .w-90 { width: 90%; } .w-95 { width: 95%; } .w-100 { width: 100%; }

/* ---------------------------------------------------------------------------
   THE BOARD'S FILTER — XP, messages, time in voice.

   Tabs rather than a select, because there is no JavaScript here to submit one:
   each is a link to the same board ranked another way. The current one is a
   <span>, not a link to the page you are on.
--------------------------------------------------------------------------- */
.board-filter {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem;
  margin: 0 0 1.25rem;
}
.board-filter-label {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--sm-text-dim); margin-right: 0.35rem;
}
.filter-tab {
  padding: 0.35rem 0.85rem;
  border: 1px solid var(--sm-border);
  color: var(--sm-text-muted);
  font-size: 0.86rem; font-weight: 600;
  text-decoration: none;
}
@media (hover: hover) { a.filter-tab:hover { border-color: var(--sm-border-strong); color: var(--sm-text); text-decoration: none; } }
.filter-tab.is-on {
  border-color: var(--sm-chip-ink);
  color: var(--sm-chip-ink);
  background: var(--sm-chip-bg);
}
/* "My position" is a jump, not a ranking, so it sits apart from the sort tabs
   at the end of the row and wears the accent. */
.filter-tab-me { margin-left: auto; border-color: var(--sm-accent); color: var(--sm-accent); }
.board-sorts { display: flex; flex-wrap: wrap; gap: 0.4rem; min-width: 0; }
/*
  ONE ROW OF TABS ON A PHONE (2026-09-24). Five tabs, the label and My
  position wrapped into three ragged lines. Now the label sits above, the tabs
  share one line that never wraps — each takes its share, a little tighter —
  and My position takes the line below. Five short words fit a 320px screen;
  a community with a long currency word scrolls the row sideways rather than
  breaking it.
*/
@media (max-width: 560px) {
  .board-filter-label { flex: 0 0 100%; margin: 0 0 0.1rem; }
  .board-sorts {
    flex: 1 1 100%; flex-wrap: nowrap; gap: 0.3rem;
    overflow-x: auto; scrollbar-width: none;
  }
  .board-sorts::-webkit-scrollbar { display: none; }
  .board-sorts .filter-tab {
    flex: 1 0 auto; justify-content: center; text-align: center; white-space: nowrap;
    padding: 0.35rem 0.55rem; font-size: 0.8rem;
  }
  .filter-tab-me { margin-left: 0; flex: 1 1 100%; justify-content: center; text-align: center; }
}
/* The smallest phones (320px) need a touch less, or the fifth tab is clipped. */
@media (max-width: 380px) {
  .board-sorts { gap: 0.2rem; }
  .board-sorts .filter-tab { padding: 0.3rem 0.4rem; font-size: 0.74rem; }
}
@media (hover: hover) { a.filter-tab-me:hover { border-color: var(--sm-accent-hover); color: var(--sm-accent-hover); } }

/* The reader's own row on the board, and where "My position" lands (#me).
   scroll-margin keeps it clear of the top of the viewport after the jump. */
.board-row.is-me { scroll-margin-top: 5rem; }
.board-row.is-me .board-link {
  border-color: var(--sm-accent);
  box-shadow: inset 3px 0 0 var(--sm-accent);
}
.board-row.is-me .who::after {
  content: " · you";
  color: var(--sm-accent);
  font-weight: 600;
}

/* ---------------------------------------------------------------------------
   A PROFILE THAT IS NOT OPEN.

   The shape of a profile page, drawn from nothing at all — fixed bars, no name,
   no avatar, no numbers — under a blur, with one sentence over it. See
   lockedProfilePage in pages.js for why this is a skeleton rather than a
   refusal, and why the same page answers "not published" and "no such member".
--------------------------------------------------------------------------- */
.locked { position: relative; overflow: hidden; }
.locked-skeleton {
  /*
    SOFTER THAN IT WAS. At 7px the skeleton was a smear and the page read as
    broken rather than as shut; at 3px it is plainly the shape of a profile with
    nothing readable in it, which is the whole point — there is nothing to read,
    because it is drawn from no data at all.
  */
  filter: blur(3px);
  opacity: 0.8;
  pointer-events: none;
  user-select: none;
  padding: 0.25rem 0 1rem;
  display: flex; flex-direction: column; gap: 1.25rem;
}
.sk { display: block; background: var(--sm-bg-hover); border: 1px solid var(--sm-border); }
.sk-line { height: 0.85rem; }
.sk-head { display: flex; align-items: center; gap: 1rem; }
.sk-avatar { width: 72px; height: 72px; flex: 0 0 auto; }
.sk-head-lines { display: flex; flex-direction: column; gap: 0.5rem; flex: 1 1 auto; }
.sk-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; }
.sk-fact {
  background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
  padding: 0.7rem 0.9rem; display: flex; flex-direction: column; gap: 0.5rem;
}
.sk-num { display: block; height: 1.15rem; width: 60%; background: var(--sm-bg-hover); }
.sk-badges { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; }
.sk-badge { height: 3.2rem; }
.sk-bars { display: flex; flex-direction: column; gap: 0.5rem; }
.sk-bars .sk-bar { height: 0.7rem; }
@media (max-width: 560px) {
  .sk-facts { grid-template-columns: 1fr 1fr; }
  .sk-badges { grid-template-columns: 1fr 1fr; }
}

.locked-overlay {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.35rem; text-align: center;
  padding: 1.5rem;
  /* Enough ground to read the words on, without hiding that there is a page
     underneath — the blur is the message as much as the sentence is. */
  /* Opaque where color-mix is not understood: the words still read, and the
     skeleton is simply hidden rather than half-hidden. */
  background: var(--sm-bg-base);
  background: color-mix(in srgb, var(--sm-bg-base) 55%, transparent);
}
/*
  A THEMED FAMILY FALLS BACK TO SHARP, GLYPH BY GLYPH.

  Font Awesome's Pro+ families — Chisel, Slab, Pixel, Jelly, Thumbprint — are
  drawn sets rather than complete ones: each covers a fraction of the catalogue
  Sharp covers. The kit sets `font-family: var(--_fa-family)` with ONE family in
  it, so an icon that family does not contain renders as the browser's missing
  glyph: a box with a cross through it, which is worse than no icon because it
  looks like a fault rather than an absence.

  Reported from a Fantasy portal, where the ghost on a hidden profile was a
  box. Chisel has no ghost.

  A FALLBACK LIST FIXES IT AT THE RIGHT LEVEL. Font matching is per CHARACTER,
  not per element: the browser takes each glyph from the first family in the
  list that has it. So a Chisel portal draws every icon Chisel has in Chisel,
  and quietly borrows the rest from Sharp — which is the whole catalogue and is
  already loaded. Nothing is ever missing, and the theme still shows wherever
  it can.

  THE WEIGHTS DIFFER AND THAT IS FINE. Thumbprint ships at 300 and Sharp at 400
  and 900; a face asked for a weight it does not have is matched to the nearest
  one it does, which is how font-weight has always worked.

  Sharp last in every list, because it is the only family in this kit that can
  be relied on to have the glyph.
*/
.fa-chisel { --fa-family: "Font Awesome 7 Chisel", "Font Awesome 7 Sharp"; }
.fa-slab { --fa-family: "Font Awesome 7 Slab", "Font Awesome 7 Sharp"; }
.fa-pixel { --fa-family: "Font Awesome 7 Pixel", "Font Awesome 7 Sharp"; }
.fa-jelly-fill { --fa-family: "Font Awesome 7 Jelly Fill", "Font Awesome 7 Sharp"; }
.fa-thumbprint { --fa-family: "Font Awesome 7 Thumbprint", "Font Awesome 7 Sharp"; }
.fa-etch { --fa-family: "Font Awesome 7 Etch", "Font Awesome 7 Sharp"; }

/*
  ICONS COME FROM THE OPERATOR'S FONT AWESOME KIT.

  There used to be a copy of Font Awesome Pro Sharp in this repo — one @font-face
  and a handful of hand-written ::before rules, one per glyph, added as the site
  grew. That worked and had one failing: it was a SECOND copy of a pack the
  dashboard already had, so the two surfaces could drift, and every new icon
  meant looking a codepoint up by hand and writing it in.

  The kit is the one place icons are decided now, for both. Its classes are used
  as Font Awesome intends — fa-sharp fa-solid fa-star — so nothing here has to
  know a codepoint, and adding an icon is adding a class.

  The stylesheet is linked and the loader script is not: see the note beside
  style-src in index.js. Nothing on this site executes JavaScript.
*/

.locked-mark {
  display: block;
  font-size: 2.9rem; line-height: 1;
  color: var(--sm-text-dim);
  margin-bottom: 0.6rem;
  /* No box around it: a frame would put the padlock's "shut out" back by
     another means. */
}
.locked-title { margin: 0; font-weight: 700; font-size: 1.05rem; }
.locked-say { margin: 0; color: var(--sm-text-muted); font-size: 0.9rem; max-width: 34ch; }


/* ===========================================================================
   THE GUIDE — a community's own words about itself.

   Plainer than the board on purpose: this is prose somebody wrote, and it is
   read rather than scanned. Quiet rules, wide measure, nothing competing.
=========================================================================== */
.rule-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.rule {
  position: relative;
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.9rem;
  padding: 0.85rem 1rem;
  background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-left: 3px solid var(--sm-accent);
}
.rule-n {
  font-family: var(--sm-font-mono); font-size: 0.95rem; font-weight: 700;
  color: var(--sm-accent); min-width: 1.4rem;
}
.rule-body { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.rule-title { font-weight: 700; }
/* A rule written as one piece of text: plain, with its **marked** words bold. */
.rule-plain { font-weight: 400; }
.rule-plain strong, .rule-text strong { font-weight: 700; }
.rule-text { color: var(--sm-text-muted); font-size: 0.94rem; }

.chan-group { margin-top: 1.1rem; }
.chan-group-name {
  margin: 0 0 0.5rem; font-size: 0.74rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--sm-text-dim);
}
.chan-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.chan {
  display: grid; grid-template-columns: minmax(8rem, auto) minmax(0, 1fr); gap: 0.9rem;
  padding: 0.5rem 0.9rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
}
.chan-name { font-family: var(--sm-font-mono); color: var(--sm-teal); white-space: nowrap; }
.chan-what { color: var(--sm-text-muted); font-size: 0.92rem; }

.cmd-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.cmd {
  display: grid; grid-template-columns: minmax(8rem, auto) minmax(0, 1fr); gap: 0.9rem;
  padding: 0.5rem 0.9rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
}
.cmd-name { font-family: var(--sm-font-mono); color: var(--sm-violet); white-space: nowrap; }
.cmd-what { color: var(--sm-text-muted); font-size: 0.92rem; }

.res-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); gap: 0.5rem; }
.res-link {
  display: flex; flex-direction: column; gap: 0.2rem;
  padding: 0.85rem 1rem; height: 100%;
  background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
  color: inherit;
}
@media (hover: hover) { a.res-link:hover { border-color: var(--sm-border-strong); background: var(--sm-bg-hover); text-decoration: none; } }
.res-name { font-weight: 700; }
.res-what { color: var(--sm-text-muted); font-size: 0.9rem; flex: 1 1 auto; }
.res-host { color: var(--sm-accent); font-size: 0.82rem; font-family: var(--sm-font-mono); }

@media (max-width: 560px) {
  .chan, .cmd { grid-template-columns: 1fr; gap: 0.15rem; }
}

/* ===========================================================================
   EVENTS — what is coming up, in the server's own timezone.
=========================================================================== */
.ev-legend { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 1.5rem; }
.ev-cat {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--sm-border);
  font-size: 0.74rem; font-weight: 600; color: var(--sm-text-muted);
  white-space: nowrap;
}
.ev-dot { width: 7px; height: 7px; background: var(--hue, var(--sm-accent)); flex: 0 0 auto; }
/* ============ event category colour ============
   THE COMMUNITY PICKS THIS, NOT THE PAGE.

   An event category carries a colour chosen in the dashboard, and the
   instance sends it as one of six semantic tokens. This page used to
   ignore it and assign one of four hues BY POSITION, so a category's
   colour was decided by how many categories were listed above it — and
   shifted when one was added.

   The six map onto colours this stylesheet already defines, so there is no
   new palette: `info` takes violet because there is no info blue here
   and violet is the free identity hue, `danger` takes rose because rose
   is what this file already uses for a sanction, and `neutral` takes the
   dim text colour, which is the one grey that is legible in both modes.

   One rule per token across all three components, rather than one per
   component per token: they all read `--hue` and nothing else differs.
   The hue-* rules below remain for an instance that sends no token. */
.ev-cat.sem-accent, .ev-next.sem-accent, .ev-row.sem-accent { --hue: var(--sm-accent); }
.ev-cat.sem-info, .ev-next.sem-info, .ev-row.sem-info { --hue: var(--sm-violet); }
.ev-cat.sem-success, .ev-next.sem-success, .ev-row.sem-success { --hue: var(--sm-success); }
.ev-cat.sem-warning, .ev-next.sem-warning, .ev-row.sem-warning { --hue: var(--sm-warning); }
.ev-cat.sem-danger, .ev-next.sem-danger, .ev-row.sem-danger { --hue: var(--sm-rose); }
.ev-cat.sem-neutral, .ev-next.sem-neutral, .ev-row.sem-neutral { --hue: var(--sm-text-dim); }

.ev-cat.hue-amber { --hue: var(--sm-amber); }
.ev-cat.hue-teal { --hue: var(--sm-teal); }
.ev-cat.hue-violet { --hue: var(--sm-violet); }
.ev-cat.hue-rose { --hue: var(--sm-rose); }

.ev-source, .ev-interest { font-size: 0.76rem; color: var(--sm-text-dim); white-space: nowrap; }
.ev-interest { color: var(--sm-teal); }

.ev-next {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1.1rem;
  padding: 1.1rem 1.25rem; margin: 0 0 1.5rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge); border-left: 4px solid var(--hue, var(--sm-accent));
}
.ev-next.hue-amber { --hue: var(--sm-amber); }
.ev-next.hue-teal { --hue: var(--sm-teal); }
.ev-next.hue-violet { --hue: var(--sm-violet); }
.ev-next.hue-rose { --hue: var(--sm-rose); }

.ev-date { display: flex; flex-direction: column; align-items: center; min-width: 3.2rem; }
.ev-dow { font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--sm-text-dim); }
.ev-day { font-family: var(--sm-font-mono); font-size: 1.7rem; font-weight: 700; color: var(--hue, var(--sm-accent)); line-height: 1.1; }
.ev-mon { font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--sm-text-dim); }

.ev-next-body { min-width: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.ev-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.ev-title { margin: 0; font-family: var(--sm-font-display); text-transform: var(--sm-display-case); font-size: 1.25rem; font-weight: 700; letter-spacing: min(var(--sm-display-tracking), 0.06em); }
.ev-desc { margin: 0; color: var(--sm-text-muted); font-size: 0.94rem; }
.ev-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.8rem; color: var(--sm-text-dim); font-size: 0.82rem; }
.ev-meta-bit { white-space: nowrap; }

.ev-list { list-style: none; margin: 0 0 1.5rem; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.ev-row {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.9rem; align-items: center;
  padding: 0.6rem 0.9rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge); border-left: 3px solid var(--hue, var(--sm-accent));
}
.ev-row.hue-amber { --hue: var(--sm-amber); }
.ev-row.hue-teal { --hue: var(--sm-teal); }
.ev-row.hue-violet { --hue: var(--sm-violet); }
.ev-row.hue-rose { --hue: var(--sm-rose); }
.ev-row .ev-date { min-width: 2.6rem; }
.ev-row .ev-day { font-size: 1.2rem; }
.ev-row-body { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
.ev-row-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.ev-title-sm { font-weight: 700; }
.ev-row-end { text-align: right; }
@media (max-width: 560px) {
  .ev-row { grid-template-columns: auto minmax(0, 1fr); }
  .ev-row-end { grid-column: 2; text-align: left; }
}
/* A row a calendar chip pointed at is outlined in its category's colour, and
   lands clear of the top of the screen. */
.ev-row { scroll-margin-top: 1rem; }
.ev-row:target { outline: 2px solid var(--hue, var(--sm-accent)); outline-offset: 2px; clip-path: none; }

/* ---------------------------------------------------------------------------
   THE CALENDAR VIEW — a month, weeks from Monday, in the guild's zone.

   A grid of seven columns; a day is a cell, an event a chip in its
   category's --hue (the same sem-* / hue-* classes as the rows) linking to
   its row in the list below. Days outside the month are dimmed, today is
   edged in the accent. At 560px and under it collapses to an agenda: only
   the days with something on, stacked, each naming itself in full.
--------------------------------------------------------------------------- */
.ev-view { margin-bottom: 0.9rem; }
.cal { margin: 0 0 1.5rem; }
.cal-nav { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.75rem; }
.cal-title {
  margin: 0 0.25rem; font-family: var(--sm-font-display); text-transform: var(--sm-display-case);
  font-size: 1.2rem; font-weight: 700; letter-spacing: min(var(--sm-display-tracking), 0.06em);
  min-width: 0;
}
.cal-step, .cal-today {
  display: inline-grid; place-items: center;
  min-width: 2rem; height: 2rem; padding: 0 0.6rem;
  border: 1px solid var(--sm-border); color: var(--sm-text-muted);
  font-size: 0.86rem; font-weight: 600; text-decoration: none;
}
.cal-step { font-size: 1.2rem; line-height: 1; padding: 0; }
.cal-today { margin-left: auto; }
.cal-today.is-on { border-color: var(--sm-chip-ink); color: var(--sm-chip-ink); background: var(--sm-chip-bg); }
.cal-step.is-off { color: var(--sm-text-faint); border-style: dashed; }
@media (hover: hover) { a.cal-step:hover, a.cal-today:hover { border-color: var(--sm-border-strong); color: var(--sm-text); text-decoration: none; } }
.cal-note { margin: 0 0 0.6rem; }
.cal-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px;
}
.cal-head {
  padding: 0.2rem 0.35rem;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--sm-text-dim);
}
.cal-day {
  display: flex; flex-direction: column; gap: 3px;
  min-height: 5.6rem; min-width: 0;
  padding: 0.3rem;
  background: var(--sm-bg-raised); border: 1px solid var(--sm-border);
}
.cal-day.is-out { background: transparent; border-style: dashed; opacity: 0.55; }
.cal-day.is-today { border-color: var(--sm-accent); box-shadow: inset 0 0 0 1px var(--sm-accent); }
.cal-date { display: flex; align-items: center; }
.cal-date-n {
  font-family: var(--sm-font-mono); font-size: 0.78rem; font-weight: 700;
  color: var(--sm-text-dim); font-variant-numeric: tabular-nums;
}
.cal-day.is-today .cal-date-n {
  display: inline-grid; place-items: center; min-width: 1.5rem; height: 1.5rem; padding: 0 0.2rem;
  background: var(--sm-accent); color: var(--sm-accent-fg); border-radius: var(--sm-radius-pill);
}
/* Screen readers get the long date in the grid; sighted readers get the
   number. The agenda below shows the long date to everybody. */
.cal-date-long { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.cal-chip {
  display: block; min-width: 0;
  padding: 0.1rem 0.3rem;
  font-size: 0.72rem; line-height: 1.35; color: var(--sm-text);
  background: color-mix(in srgb, var(--hue, var(--sm-accent)) 16%, transparent);
  border-left: 3px solid var(--hue, var(--sm-accent));
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-decoration: none;
}
@media (hover: hover) { .cal-chip:hover { background: color-mix(in srgb, var(--hue, var(--sm-accent)) 28%, transparent); text-decoration: none; } }
.cal-chip-time { font-family: var(--sm-font-mono); font-weight: 700; color: var(--sm-text-muted); }
.cal-more { font-size: 0.7rem; color: var(--sm-text-dim); padding: 0 0.3rem; }
.cal-chip.sem-accent { --hue: var(--sm-accent); }
.cal-chip.sem-info { --hue: var(--sm-violet); }
.cal-chip.sem-success { --hue: var(--sm-success); }
.cal-chip.sem-warning { --hue: var(--sm-warning); }
.cal-chip.sem-danger { --hue: var(--sm-rose); }
.cal-chip.sem-neutral { --hue: var(--sm-text-dim); }
.cal-chip.hue-amber { --hue: var(--sm-amber); }
.cal-chip.hue-teal { --hue: var(--sm-teal); }
.cal-chip.hue-violet { --hue: var(--sm-violet); }
.cal-chip.hue-rose { --hue: var(--sm-rose); }

/*
  ON A PHONE IT STAYS A MONTH (2026-09-22). It used to collapse to an agenda of
  only the days with something on, which read as a list rather than a
  calendar. Now the seven columns stay; a cell is too narrow for a title, so
  each event becomes a bar in its category's colour, still a link to its row
  in the list below, where the time and title are in full. The long date
  stays for screen readers only, as on a wide screen.
*/
@media (max-width: 560px) {
  .cal-grid { gap: 2px; }
  .cal-head { padding: 0.15rem 0; text-align: center; font-size: 0.6rem; letter-spacing: 0.04em; }
  .cal-day { min-height: 3.1rem; padding: 0.2rem; gap: 2px; align-items: center; }
  .cal-date { justify-content: center; }
  .cal-date-n { font-size: 0.72rem; }
  .cal-chip {
    width: 100%; height: 0.5rem; padding: 0;
    font-size: 0; line-height: 0; color: transparent;
    background: var(--hue, var(--sm-accent));
    border-left: 0; border-radius: var(--sm-radius-pill);
  }
  .cal-chip-time, .cal-chip-title { display: none; }
  .cal-more { font-size: 0.6rem; padding: 0; text-align: center; }
}

/* ===========================================================================
   A MEMBER'S PAGE.
=========================================================================== */
.member-head { display: flex; align-items: center; gap: 1.1rem; margin-bottom: 1.6rem; }
.member-avatar { width: 88px; height: 88px; object-fit: cover; background: var(--sm-bg-deep); flex: 0 0 auto; font-size: 2rem; }
.member-id { min-width: 0; flex: 1 1 auto; }
.member-name { margin: 0; font-size: calc(clamp(1.5rem, 4.5vw, 2.1rem) * var(--sm-title-scale)); overflow-wrap: anywhere; }
.member-handle { margin: 0.1rem 0 0; font-family: var(--sm-font-mono); font-size: 0.88rem; }
/* Pronouns: their own line under the handle, quieter again than it. */
.member-pronouns { margin: 0.15rem 0 0; color: var(--sm-text-dim); font-size: 0.9rem; }
/*
  The marks beside a name. Each sits in a square of its own colour at a tenth
  opacity — enough to read as a badge rather than as a stray glyph, and enough
  to tell three of them apart at a glance without any of them shouting.
*/
.name-marks {
  display: inline-flex; align-items: center; gap: 0.35rem;
  margin-left: 0.6rem; vertical-align: middle;
  /* Wraps rather than pushing a long display name off a phone: three chips and
     a name is more than a narrow screen holds on one line. */
  flex-wrap: wrap;
}
/* On a profile the marks sit on their own row under the name and handle. */
.member-id > .name-marks { display: flex; margin: 0.5rem 0 0; }
.name-mark-word {
  font-size: 0.7rem; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  line-height: 1;
}
/*
  A MARK IS A CHIP NOW, NOT A SQUARE.

  It was a fixed 1.55rem box holding one glyph, which is the right shape for a
  decoration and the wrong one for a label. It sizes to its contents instead,
  and the word sits beside the glyph at the small, tracked-out weight the rest
  of the site uses for a category.
*/
.name-mark {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.15rem 0.5rem;
  font-size: 0.8rem;
  color: var(--hue, var(--sm-text-dim));
  background: color-mix(in srgb, var(--hue, var(--sm-text-dim)) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--hue, var(--sm-text-dim)) 35%, transparent);
}
.name-mark.hue-amber { --hue: var(--sm-amber); }
.name-mark.hue-teal { --hue: var(--sm-teal); }
.name-mark.hue-violet { --hue: var(--sm-violet); }
.name-mark.hue-rose { --hue: var(--sm-rose); }
/* The three a role mark actually uses. See ROLE_MARKS in pages.js. */
.name-mark.hue-gold { --hue: var(--sm-role-gold); }
.name-mark.hue-red { --hue: var(--sm-role-red); }
.name-mark.hue-blue { --hue: var(--sm-role-blue); }

/* The glyphs these use, out of the pack already loaded for the ghost. */
.member-flag {
  padding: 0.2rem 0.6rem; border: 1px solid var(--sm-warning); color: var(--sm-warning);
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap; align-self: flex-start;
}

/* The nudge somebody gets on their own unpublished page. An invitation, so it
   takes the accent rather than the warning colour. */
.publish-prompt {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.9rem;
  padding: 1rem 1.15rem; margin: 0 0 1.6rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge); border-left: 4px solid var(--sm-accent);
}
.publish-mark { color: var(--sm-accent); font-size: 1.35rem; line-height: 1; margin-top: 0.1rem; }
.publish-form { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 0.9rem 0 0; }
.publish-alt { margin: 0.9rem 0 0; color: var(--sm-text-dim); font-size: 0.86rem; }
.publish-form .publish-alt { margin: 0; }
.publish-error { margin: 0.75rem 0 0; color: var(--sm-warning); font-size: 0.88rem; }
.publish-body { min-width: 0; }
.publish-title { margin: 0 0 0.25rem; font-weight: 700; }
.publish-say { margin: 0; color: var(--sm-text-muted); font-size: 0.92rem; }

.stat-row {
  list-style: none; margin: 0 0 0.5rem; padding: 0;
  /* auto-fit rather than a fixed four: the fourth card is the one this
     community counts, and some communities count none — a hole where a card
     would be reads as something that failed to load. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 0.5rem;
}
.stat-k { display: block; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-dim); }
.stat-v { display: block; margin-top: 0.15rem; font-family: var(--sm-font-mono); font-size: 1.35rem; font-weight: 700; }
.stat-u { color: var(--sm-text-dim); font-size: 0.78rem; font-weight: 400; }
@media (max-width: 560px) { .stat-row { grid-template-columns: 1fr 1fr; } }

.section-note { font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--sm-text-dim); white-space: nowrap; }

.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.5rem; margin: 0; }
.info-item dt { margin: 0; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-dim); }
.info-item dd { margin: 0.15rem 0 0; }
/* The zone under the clock: the answer to "which 14:05". */
.info-under { font-size: 0.78rem; color: var(--sm-text-muted); font-variant-numeric: tabular-nums; }
.info-cmd { font-size: 0.78rem; color: var(--sm-text-dim); }
.info-cmd code { color: var(--sm-teal); }

.level-panel { padding: 1.1rem 1.25rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); }
.level-head { display: flex; flex-wrap: wrap; align-items: center; gap: 1.5rem; }
.level-fig { display: flex; flex-direction: column; }
.level-k { font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-dim); }
.level-v { font-family: var(--sm-font-mono); font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.level-bar-wrap { flex: 1 1 12rem; min-width: 0; }
.level-bar {
  height: var(--sm-bar-h); background: var(--sm-track); border: var(--sm-bar-edge);
  border-radius: var(--sm-radius-pill); clip-path: var(--sm-clip-sm);
  overflow: hidden;
}
.level-bar-fill {
  display: block; height: 100%; background: var(--sm-bar-fill);
  border-radius: var(--sm-radius-pill); box-shadow: var(--sm-bar-glow);
}
.level-bar-wrap p { margin: 0.4rem 0 0; }

.unlock-list { list-style: none; margin: 1.1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
/*
  The row is the LINK, not the <li> around it — so the whole rung is the hit
  target rather than the few words in the middle of it, which on a phone is the
  difference between a list that opens and one that has to be aimed at.
*/
.unlock { opacity: 0.65; }
.unlock.is-earned { opacity: 1; }
.unlock-open {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.8rem; align-items: center;
  padding: 0.5rem 0.75rem; background: var(--sm-bg-base); border: 1px solid var(--sm-border);
  color: inherit; text-decoration: none;
}
.unlock.is-earned .unlock-open { border-color: var(--sm-border-strong); }
@media (hover: hover) { .unlock-open:hover { background: var(--sm-bg-hover); border-color: var(--sm-accent); text-decoration: none; } }
.unlock-open:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 1px; }
/*
  A RUNG WITH NOTHING BEHIND IT DOES NOT PRETEND.

  Where the levels module has no perks written against a level, the row is a
  <span> rather than a link — so it must not carry a pointer, a hover state or
  a focus ring, all of which say "press me" about a row that does nothing. It
  keeps the border and the tick, because those are facts about the level.
*/
.unlock.is-plain .unlock-open { cursor: default; }
@media (hover: hover) {
  .unlock.is-plain .unlock-open:hover { background: var(--sm-bg-raised); border-color: var(--sm-border); }
  .unlock.is-plain.is-earned .unlock-open:hover { border-color: var(--sm-border-strong); }
}
.unlock-lvl { font-family: var(--sm-font-mono); font-size: 0.74rem; color: var(--sm-text-dim); white-space: nowrap; }
.unlock-body { min-width: 0; display: flex; flex-direction: column; }
.unlock-name { font-weight: 600; font-size: 0.92rem; }
.unlock-note { color: var(--sm-text-dim); font-size: 0.82rem; }
.unlock-mark { color: var(--sm-success); font-weight: 700; }

/*
  A ROLE THAT IS NOT ON THE LADDER, in the ladder's own list.

  The instance sends these beside the rungs: roles given by hand, or carried by
  an achievement. The lane that says "Lvl 30" on a rung says how the role is
  come by on one of these, so both lists read down the same columns — which is
  the point of putting them in one dialog.

  FULL OPACITY, unlike an unreached rung. `.unlock` dims to 0.65 to mean "not
  yours yet", and these carry no per-member state at all: the instance sends no
  earned flag, because whether somebody holds a hand-picked role is a fact about
  that person rather than about the ladder. Dimming them would state the one
  thing this list deliberately does not know.
*/
.unlock.is-special { opacity: 1; }
.unlock-lvl-wide {
  /* "With Founding Member" is a phrase, not a level number: it wraps rather
     than pushing the name out of its own column. */
  white-space: normal;
  max-width: 9rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.68rem;
  line-height: 1.3;
}
/* The heading over the second list in the perks dialog, spaced away from the
   list above it so the two read as two answers rather than one long one. */
.sm-modal-subhead { margin-top: 1.4rem; }

.sub-panel { padding: 1.1rem 1.25rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); border-left: 4px solid var(--sm-violet); }
.sub-head { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-bottom: 0.5rem; }
.sub-tier { color: var(--sm-violet); }
/* How far off a rung is, where a held one shows its tick. Same column, so the
   ladder reads straight down whichever state a row is in. */
.unlock-away {
  font-family: var(--sm-font-mono); font-size: 0.74rem;
  color: var(--sm-text-faint); white-space: nowrap;
}

.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 0.5rem; }
.chart-card { margin: 0; padding: 0.9rem 1rem 0.75rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); }
.chart-card figcaption { display: flex; flex-direction: column; font-weight: 600; font-size: 0.92rem; margin-bottom: 0.7rem; }
.chart-note { font-weight: 400; font-size: 0.78rem; color: var(--sm-text-dim); }
.chart { display: block; width: 100%; height: 88px; }
.chart-fill { fill: var(--sm-accent); opacity: 0.16; }
.chart-line { fill: none; stroke: var(--sm-accent); stroke-width: 2; vector-effect: non-scaling-stroke; }
.hours { display: flex; align-items: flex-end; gap: 2px; height: 88px; }
.hour { flex: 1 1 0; height: 100%; display: flex; align-items: flex-end; }
.hour-fill { display: block; width: 100%; background: var(--sm-teal); }
.chart-axis { display: flex; justify-content: space-between; margin-top: 0.4rem; font-family: var(--sm-font-mono); font-size: 0.7rem; color: var(--sm-text-faint); }
/*
  A MONTH AXIS IS COLUMNS, NOT ENDS.

  space-between is right for two or three marks — it pins the first to the left
  edge and the last to the right, which is where the first and last data points
  are. It is wrong for twelve: every label would sit on a boundary rather than
  over the stretch of line it names, and the two on the ends would hang off it.

  Equal columns instead, each label centred in its own, so a month sits above
  its own twelfth of the series. grid-auto-columns rather than a fixed count,
  because the number of labels comes from the length of the series.
*/
.chart-axis.is-months {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  justify-content: stretch; text-align: center;
  font-size: 0.68rem;
}
/* Twelve three-letter labels at 0.68rem need about 240px and a narrow phone
   gives the card about 320: fine, but not with anything to spare, so they step
   down rather than colliding. */
@media (max-width: 24rem) { .chart-axis.is-months { font-size: 0.6rem; } }

.wrapped-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); gap: 0.5rem; }
.wrapped-card { display: block; width: 100%; height: auto; border: 1px solid var(--sm-border); background: var(--sm-bg-deep); }

/* Heights for the hour bars, as classes — the same reason the widths above are
   classes: an inline style attribute is dropped by this site's CSP. */
/* Zero, for a stacked segment with nothing in it — a day nobody signed in. */
.h-0 { height: 0; }
.h-2 { height: 2%; }
.h-5 { height: 5%; } .h-10 { height: 10%; } .h-15 { height: 15%; } .h-20 { height: 20%; }
.h-25 { height: 25%; } .h-30 { height: 30%; } .h-35 { height: 35%; } .h-40 { height: 40%; }
.h-45 { height: 45%; } .h-50 { height: 50%; } .h-55 { height: 55%; } .h-60 { height: 60%; }
.h-65 { height: 65%; } .h-70 { height: 70%; } .h-75 { height: 75%; } .h-80 { height: 80%; }
.h-85 { height: 85%; } .h-90 { height: 90%; } .h-95 { height: 95%; } .h-100 { height: 100%; }

/* ===========================================================================
   THE BADGE WALL, and a modal built out of :target.

   No script anywhere on this site, so the dialog is a link to an id and a rule
   that shows the panel the URL names. Tapping the backdrop or the × is a link
   back to "#". See badgeGrid in pages.js for what that costs and why it is
   still the right trade.
=========================================================================== */
/*
  THREE ACROSS, ON EVERY WIDTH.

  auto-fill made the wall a different shape on every screen — six small tiles on
  a laptop, three larger ones on a phone — and a wall of artwork wants to be one
  thing. Three columns reads as a deliberate block at any size, and it is the
  width at which a badge is big enough to look at rather than to identify.
*/
.badge-grid {
  list-style: none; margin: 0 0 0.5rem; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
}
.badge-tile {
  display: flex; flex-direction: column; align-items: center; gap: 0.55rem;
  padding: 0.9rem 0.6rem;
  height: 100%;
  background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
  color: inherit; text-align: center;
}
@media (hover: hover) { .badge-tile:hover { border-color: var(--sm-border-strong); background: var(--sm-bg-hover); text-decoration: none; } }
.badge-tile:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.badge-art {
  display: grid; place-items: center;
  /* Grows with the column rather than sitting fixed inside it, up to the size
     the artwork is actually drawn at. */
  width: 100%; max-width: 108px;
  aspect-ratio: 1;
  background: var(--art-bg, var(--sm-bg-deep));
}
.badge-art img, .badge-art .badge-img { width: 100%; height: 100%; object-fit: contain; display: block; }
.badge-art .badge-emoji { font-size: 2rem; line-height: 1; }
.badge-initial {
  display: grid; place-items: center; width: 100%; height: 100%;
  color: var(--sm-text-dim); font-weight: 700; font-size: 1.4rem;
}
.badge-label {
  font-size: 0.8rem; font-weight: 600; line-height: 1.3;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

/* The dialog itself: absent until the address names it. */
.sm-modal { display: none; }
/*
  CENTRED BY THE LAYOUT, NOT BY A MARGIN.

  It was `margin: 12vh auto 0` on the sheet — which centres, right up until the
  narrow-screen rule below replaced that margin with side gutters and took the
  `auto` with it. The sheet then hugged the left edge on exactly the devices
  most people are reading this on, and since the backdrop covers the viewport
  the page looked like one big link with a panel stuck in the corner.

  Grid centring cannot be undone by a later margin, and it holds on both axes.
*/
.sm-modal:target {
  display: grid;
  place-items: center;
  position: fixed; inset: 0; z-index: 100;
  padding: 1rem;
  /* A panel longer than the screen scrolls rather than escaping it. */
  overflow-y: auto;
}
.sm-backdrop {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--sm-bg-base) 82%, transparent);
  display: block;
}
.sm-sheet {
  position: relative;
  z-index: 1;
  width: min(22rem, 100%);
  margin: 0;
  padding: 1.5rem 1.4rem 1.6rem;
  background: var(--sm-bg-raised);
  border: 1px solid var(--sm-border-strong);
  box-shadow: inset 0 0 0 var(--sm-inset-w) var(--sm-bg-raised), inset 0 0 0 calc(var(--sm-inset-w) + 1px) var(--sm-inset-c), var(--sm-sheet-shadow);
  text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
}
.sm-close {
  position: absolute; top: 0.35rem; right: 0.55rem;
  font-size: 1.5rem; line-height: 1;
  color: var(--sm-text-dim); text-decoration: none;
  padding: 0.2rem 0.4rem;
}
@media (hover: hover) { .sm-close:hover { color: var(--sm-accent); text-decoration: none; } }
.badge-art-lg { width: 132px; max-width: 132px; }
/* What a level dialog says before its name: which level, and whether this
   member is past it. */
.sm-modal-kicker {
  margin: 0; font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--sm-text-dim);
}
.sm-modal-earned { color: var(--sm-success); }
/* Perks read as a list, left-aligned, because they are a list. */
.perk-list {
  list-style: none; margin: 0.9rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.5rem;
  text-align: left; width: 100%;
}
.perk-list li {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.6rem;
  color: var(--sm-text-muted); font-size: 0.92rem;
}
.perk-list li::before {
  content: ""; width: 5px; height: 5px; margin-top: 0.55rem;
  background: var(--sm-accent);
}
.sm-modal-name { margin: 0.25rem 0 0; font-family: var(--sm-font-display); text-transform: var(--sm-display-case); letter-spacing: min(var(--sm-display-tracking), 0.08em); font-size: 1.2rem; }
.sm-modal-desc { margin: 0; color: var(--sm-text-muted); font-size: 0.94rem; }
/*
  "412 members have this" IS THE COMMUNITY'S FIGURE, so it wears the community's
  colour. It was fixed teal, which is one of the four identity hues this site
  uses to tell things apart — on a portal branded anything else it was the only
  thing in the dialog that belonged to no one.
*/
.sm-modal-held { margin: 0.35rem 0 0; color: var(--sm-accent); font-size: 0.84rem; font-variant-numeric: tabular-nums; }
/* A list is read left to right, not admired from the middle. */
.sm-sheet.is-wide { width: min(30rem, 100%); text-align: left; align-items: stretch; }
.sm-modal-list { margin-top: 1rem; max-height: 60vh; overflow-y: auto; }

/*
  An icon that opens something, sitting inside a heading. Small, quiet, and a
  real link — the modal below it is :target, so this is the whole mechanism.
*/
.section-info {
  font-size: 0.9rem;
  color: var(--sm-text-faint);
  text-decoration: none;
}
@media (hover: hover) { .section-info:hover { color: var(--sm-accent); text-decoration: none; } }

@media (max-width: 480px) {
  /* Still three across — only the label and the padding give way. */
  .badge-tile { padding: 0.6rem 0.4rem; gap: 0.45rem; }
  .badge-label { font-size: 0.72rem; }
  /* No margin on .sm-sheet here. The centring above is the layout's job —
     setting one undoes it, which is the bug this comment exists to stop coming
     back. */
}

/* ===========================================================================
   PROGRESSION: ONE SHELF CARD PER LADDER, its stages in a dialog.

   See progressionGrid in pages.js. Each card is a "trophy shelf": the
   ladder's name and rank, a row of medallions (one per stage), and a muted
   line. A medallion is the theme's icon family in the stage's metal
   (`--metal`, from the .metal-* classes above) in a well tinted by that same
   metal, shaped like a badge (--sm-badge-radius / --sm-badge-clip).

   Three across on a desktop, two on a tablet (three would squeeze a five-
   stage shelf past its card), one at 560px and under. The shelf wraps rather
   than overflowing if a ladder ever has more stages than fit.
=========================================================================== */
.prog-grid {
  list-style: none; margin: 0 0 0.5rem; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
}
@media (max-width: 760px) { .prog-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 560px) { .prog-grid { grid-template-columns: minmax(0, 1fr); } }
.prog-shelf {
  display: flex; flex-direction: column; gap: 0.75rem;
  padding: 1rem 1.1rem;
  height: 100%;
  background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
  color: inherit; text-align: left;
}
@media (hover: hover) { .prog-shelf:hover { border-color: var(--sm-border-strong); background: var(--sm-bg-hover); text-decoration: none; } }
.prog-shelf:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.prog-shelf-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.prog-shelf-head .prog-label { font-size: 0.95rem; font-weight: 800; min-width: 0; }
.prog-shelf-head .tier { font-size: 0.8rem; flex: 0 0 auto; }
.prog-shelf-head .prog-count { flex: 0 0 auto; white-space: nowrap; }
/* Stone has no stars, so the corner names it instead, in its metal. */
.prog-tier-name { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }

/* The shelf. Medallions share a bottom line, so the enlarged current one rises
   out of the row rather than pushing the others about. */
.prog-medals {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.4rem;
  min-height: 2.9rem;
}
.prog-medal {
  display: grid; place-items: center; flex: 0 0 auto;
  box-sizing: border-box;
  width: 1.9rem; height: 1.9rem;
  font-size: 0.85rem; line-height: 1;
  color: var(--metal, var(--sm-text-faint));
  background: color-mix(in srgb, var(--metal, var(--sm-text-faint)) 16%, var(--sm-bg-deep));
  border: 2px solid var(--metal, var(--sm-text-faint));
}
/* On a member's page: the stage they stand on is larger... */
.prog-medal.is-current { width: 2.9rem; height: 2.9rem; font-size: 1.35rem; }
/* ...and the stages still ahead are outlines, the metal withheld. */
.prog-medal.is-ahead {
  color: var(--sm-text-faint);
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border);
}
/* The catalogue: every medallion in its own metal at one size, the top one a
   touch larger because it is the one to aim for. */
.prog-medals.is-catalogue .prog-medal { width: 2.3rem; height: 2.3rem; font-size: 1.05rem; }
.prog-medals.is-catalogue .prog-medal.is-top { width: 2.7rem; height: 2.7rem; font-size: 1.25rem; }
.prog-shelf .prog-rung { font-size: 0.84rem; color: var(--sm-text-muted); line-height: 1.3; }

.prog-glyph {
  display: grid; place-items: center; flex: 0 0 auto;
  width: 3.4rem; height: 3.4rem;
  margin-bottom: 0.2rem;
  font-size: 1.55rem; line-height: 1;
  color: var(--metal, var(--sm-text-faint));
  background: color-mix(in srgb, var(--metal, var(--sm-text-faint)) 14%, var(--sm-bg-deep));
  border: 1px solid color-mix(in srgb, var(--metal, var(--sm-text-faint)) 45%, transparent);
}
.prog-glyph-lg { width: 5rem; height: 5rem; font-size: 2.3rem; }
.prog-glyph-sm { width: 2.3rem; height: 2.3rem; font-size: 1rem; margin: 0; }
.prog-label { font-size: 0.86rem; font-weight: 700; line-height: 1.25; }
.prog-rung { font-size: 0.8rem; color: var(--sm-text-muted); line-height: 1.25; }
.prog-count { font-size: 0.74rem; color: var(--sm-text-dim); font-variant-numeric: tabular-nums; }

/* The dialog: every stage, climbing. */
.prog-stages {
  list-style: none; margin: 0.9rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 0.4rem;
  width: 100%; text-align: left;
  max-height: 60vh; overflow-y: auto;
}
.prog-stage {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.75rem; align-items: center;
  padding: 0.55rem 0.7rem;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border);
}
.prog-stage-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.prog-stage-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.prog-stage-name { font-weight: 700; }
.prog-stage-at { color: var(--sm-text-muted); font-size: 0.84rem; }
.prog-stage-end { display: flex; flex-direction: column; align-items: flex-end; gap: 0.15rem; }
.prog-stage-held {
  font-family: var(--sm-font-mono); font-size: 0.78rem; font-weight: 700;
  color: var(--sm-accent); white-space: nowrap; font-variant-numeric: tabular-nums;
}
.prog-stage-reached { font-size: 0.72rem; font-weight: 700; color: var(--sm-success); white-space: nowrap; }
/* On a member's page: the stages they have reached are edged in success, and
   the ones still ahead step back. */
.prog-stage.is-reached { border-color: color-mix(in srgb, var(--sm-success) 50%, var(--sm-border)); }
.prog-stage.is-ahead .prog-glyph { opacity: 0.55; }
/* The line under a section heading that says what the section holds. */
.section-sub { margin: -0.45rem 0 0.85rem; }

@media (max-width: 480px) {
  .prog-shelf { padding: 0.85rem 0.9rem; gap: 0.6rem; }
  .prog-stage-unit { display: none; }
}

/* The subscription bar takes the tier's colour rather than the level accent, so
   the two ladders on one page are not mistaken for each other. */
.sub-bar-fill { background: var(--sm-violet); }

/* ===========================================================================
   THE THREE THINGS A MEMBER SAID ABOUT THEMSELVES, and the four they earned.

   Both rows were grey boxes of label-and-value, which read as a form somebody
   had filled in. A glyph and a colour apiece: the quick stats take a tinted
   top edge and a coloured figure, the info tiles a mark on the left.

   THE COLOUR IS THE COMMUNITY'S. It was one of the four identity hues per
   tile, which is what the hub cards and the event categories use to tell
   things APART — and nothing on this page is being told apart. See below.
=========================================================================== */
/*
  THE COMMUNITY'S COLOUR, NOT A ROTATION THROUGH FOUR.

  These carried one of the four identity hues apiece — amber, teal, violet,
  rose — which is the right tool where colour TELLS THINGS APART: the hub cards
  use it so Leaderboard and Achievements are not two identical tiles, and the
  events page uses it so a category keeps its own colour.

  A member's page is not that. The four stat cards are four measures of one
  person and the About tiles are four facts about them; nothing is being
  distinguished, so the colour was decoration — and four decorative colours on
  a page that belongs to a community with a brand of its own is four colours
  that are not it.

  So the hue classes stay in the markup and stop being honoured here. They take
  the accent, which is the guild's own colour (`--brand`, written into a nonced
  block by layout()) lifted or deepened for the mode, or the house teal where a
  community has not set one. On a skin with pastel tiles the figure is set in
  ink instead (--sm-stat-ink): an accent figure on a pastel tile is two colours
  fighting.
*/
.stat {
  padding: 0.75rem 0.9rem; background: var(--card-bg, var(--sm-bg-raised)); border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-top: 3px solid var(--sm-accent);
}
.stat .stat-v { color: var(--sm-stat-ink); }
.stat .stat-u { color: var(--sm-text-dim); }

.info-item {
  padding: 0.7rem 0.9rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge);
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.75rem;
  align-items: center;
  border-left: 3px solid var(--sm-accent);
}
/* The About tiles, for the reason above: four facts about one person. */
.info-mark {
  font-size: 1.5rem; line-height: 1;
  /* A flag emoji is two code points wide and a name tag is one; a fixed box
     keeps the three labels underneath in a straight line whatever renders. */
  display: grid; place-items: center; width: 2rem;
}
.info-text { min-width: 0; }
.info-value { font-weight: 600; }
/* An empty field is quieter than a filled one, and its mark quieter still —
   but it is still there, because the section is also how somebody learns these
   exist at all. */
.info-unset { color: var(--sm-text-dim); font-weight: 400; font-style: italic; }
.info-item.is-unset { border-left-color: var(--sm-border); }
.info-item.is-unset .info-mark { opacity: 0.45; }

/* ===========================================================================
   THE GUIDE, AS A DOCUMENTATION HUB.

   A guide is the longest thing a community publishes and the only page people
   arrive at with a question already in mind, so it is built to be entered in
   the middle: cards for the shape of it, a contents column that stays put, and
   an id on every section, rule and channel group so a moderator can link at
   one thing.

   All anchors and CSS — there is no script on this site, so no search box and
   no collapsing. A table of contents that always shows everything is the
   honest version of both.
=========================================================================== */
.panel-doc { max-width: 1040px; }

.doc-cards {
  list-style: none; margin: 1.75rem 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 0.5rem;
}
.doc-card {
  display: flex; flex-direction: column; gap: 0.15rem;
  height: 100%; padding: 0.9rem 1rem;
  background: var(--card-bg, var(--sm-bg-raised));
  border: var(--sm-card-bw) solid var(--sm-card-edge); border-top: 3px solid var(--hue, var(--sm-accent));
  color: inherit;
}
@media (hover: hover) { .doc-card:hover { background: var(--sm-bg-hover); border-color: var(--sm-border-strong); border-top-color: var(--hue, var(--sm-accent)); text-decoration: none; } }
.doc-card-icon { color: var(--hue, var(--sm-accent)); margin-bottom: 0.4rem; }
.doc-card-title { font-family: var(--sm-font-display); text-transform: var(--sm-display-case); letter-spacing: min(var(--sm-display-tracking), 0.08em); font-weight: 700; }
.doc-card-say { color: var(--sm-text-dim); font-size: 0.82rem; font-variant-numeric: tabular-nums; }

/* Contents beside the content, on anything wide enough to hold both. */
.doc {
  display: grid; grid-template-columns: 13rem minmax(0, 1fr);
  gap: 2.5rem;
  margin-top: 2.5rem;
  align-items: start;
}
.doc-nav {
  position: sticky; top: 1.5rem;
  border-left: 1px solid var(--sm-border);
  padding-left: 1rem;
}
.doc-nav-title {
  margin: 0 0 0.6rem;
  font-size: 0.7rem; font-weight: 700;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--sm-text-dim);
}
.doc-nav-list, .doc-nav-sub { list-style: none; margin: 0; padding: 0; }
.doc-nav-list > li { margin-bottom: 0.5rem; }
.doc-nav-link { display: block; color: var(--sm-text-muted); font-size: 0.9rem; font-weight: 600; }
@media (hover: hover) { .doc-nav-link:hover { color: var(--sm-accent); text-decoration: none; } }
.doc-nav-sub { margin: 0.25rem 0 0 0.1rem; }
.doc-nav-sub li { margin-top: 0.2rem; }
.doc-nav-sublink { color: var(--sm-text-dim); font-size: 0.82rem; }
@media (hover: hover) { .doc-nav-sublink:hover { color: var(--sm-accent); text-decoration: none; } }

.doc-body { min-width: 0; }
.doc-section { margin-bottom: 3rem; }
.doc-section:last-child { margin-bottom: 0; }
/*
  scroll-margin, so a section jumped to from the contents does not land with its
  heading under the top of the window.
*/
.doc-section, .chan-group, .rule { scroll-margin-top: 1.5rem; }
.doc-h {
  margin: 0 0 1rem;
  font-family: var(--sm-font-display);
  font-size: 1.5rem; font-weight: var(--sm-display-weight);
  letter-spacing: var(--sm-display-tracking);
  text-transform: var(--sm-display-case); color: var(--sm-text);
}
.doc-lede { margin: -0.5rem 0 1rem; color: var(--sm-text-muted); font-size: 0.92rem; }

@media (max-width: 860px) {
  /*
    A sticky sidebar on a phone is a screenful of navigation, so the contents
    become a row of chips above the text — the same links, scrolled sideways.
  */
  .doc { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
  .doc-nav {
    position: static; border-left: 0; padding-left: 0;
    border-bottom: 1px solid var(--sm-border); padding-bottom: 0.9rem;
  }
  .doc-nav-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
  .doc-nav-list > li { margin: 0; }
  .doc-nav-link {
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--sm-border);
    font-size: 0.82rem;
  }
  /* The channel groups stay in the page rather than doubling the chip row. */
  .doc-nav-sub { display: none; }
}

/* ===========================================================================
   NOTHING LIGHTS UP WHEN IT IS TAPPED.

   Two separate things did. A browser paints its own translucent block over
   whatever was pressed, which on a 108px badge tile reads as the tile glowing
   rather than as a tap landing. And :hover latches after a tap on a screen
   with no pointer to move away, so the last thing touched stayed lit until
   something else was. The dialog that opens is the feedback; the tile does
   not need to announce itself as well.

   :focus-visible is deliberately untouched — it is how somebody navigating by
   keyboard knows where they are, and browsers already keep it away from taps.
=========================================================================== */
a, button, .badge-tile, .prog-shelf, .unlock-open, .hub-card, .doc-card, .board-link, .res-link {
  -webkit-tap-highlight-color: transparent;
}

/* ===========================================================================
   THE EVENTS FILTER, and the legend it used to be.

   Every chip was already a question somebody might ask — "just the group
   flights" — sitting there in the right order with the right words on it. They
   are links now: one address per category, server-side, no script.
=========================================================================== */
a.ev-cat { color: var(--sm-text-muted); }
@media (hover: hover) {
  a.ev-cat:hover { border-color: var(--hue, var(--sm-border-strong)); color: var(--sm-text); text-decoration: none; }
}
a.ev-cat.is-on {
  border-color: var(--hue, var(--sm-accent));
  color: var(--sm-text);
  background: var(--sm-bg-raised);
}
/* The "Everything" chip has no category and therefore no colour of its own. */
a.ev-cat:not([class*="hue-"]) .ev-dot { background: var(--sm-text-faint); }

/* How far off the next one is, said in words beside its category. */
.ev-soon {
  padding: 0.15rem 0.5rem;
  background: var(--sm-accent); color: var(--sm-accent-fg);
  font-size: 0.72rem; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase;
  white-space: nowrap;
}

/* ===========================================================================
   A LINK TO ONE RULE.

   Quiet until wanted: shown on hover of the row, and whenever the link itself
   has keyboard focus — which is what keeps it reachable without a pointer.
=========================================================================== */
.rule-link {
  position: absolute; top: 0.5rem; right: 0.6rem;
  font-family: var(--sm-font-mono); font-size: 0.9rem;
  color: var(--sm-text-faint);
  opacity: 0;
}
.rule:hover .rule-link, .rule-link:focus-visible { opacity: 1; }
@media (hover: hover) { .rule-link:hover { color: var(--sm-accent); text-decoration: none; } }
/* No pointer, no hover: on a touch screen it simply stays visible and quiet. */
@media (hover: none) { .rule-link { opacity: 0.5; } }

/* ===========================================================================
   THE MANAGER PAGE.

   Not a portal page: one person reads this, and it is the only page on the site
   that shows every community at once. It borrows the panel, the tables and the
   four identity hues and adds the pieces a dashboard needs — a picker, tiles, a
   column chart and a few bar rows.

   NOTHING HERE USES A `style` ATTRIBUTE. The CSP has no `style-src
   'unsafe-inline'`, so every measured thing — a bar's height, a row's width —
   comes from the .h-* and .w-* classes further up this file, in five per cent
   steps. The colours that vary per community arrive as custom properties in the
   nonced block; see DOT_SLOTS in pages.js.
=========================================================================== */

.adm-h { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 0 0 0.3rem; }
.adm-h h1 { margin: 0; }
.adm-of { color: var(--sm-accent); }
.adm-meta { color: var(--sm-text-dim); font-size: 0.8rem; font-family: var(--sm-font-mono); }

/* ---- the community picker ---- */
.gp { position: relative; margin: 0 0 1.35rem; max-width: 24rem; }
.gp-current {
  display: flex; align-items: center; gap: 0.6rem; cursor: pointer;
  padding: 0.55rem 0.8rem; background: var(--sm-bg-raised);
  border: 1px solid var(--sm-border-strong); list-style: none;
}
/* Safari draws its own triangle on a summary; this page draws its own. */
.gp-current::-webkit-details-marker { display: none; }
.gp-current::marker { content: ""; }
.gp[open] .gp-current { border-color: var(--sm-accent); }
.gp-current:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.gp-k { font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-dim); }
.gp-v { display: flex; align-items: center; gap: 0.45rem; font-weight: 600; flex: 1; min-width: 0; }
.gp-caret { color: var(--sm-text-dim); font-size: 0.75rem; }
.gp-list {
  position: absolute; z-index: 5; left: 0; right: 0; margin: 0.25rem 0 0; padding: 0.25rem;
  list-style: none; background: var(--sm-bg-raised);
  border: 1px solid var(--sm-border-strong); box-shadow: var(--sm-sheet-shadow);
  max-height: 60vh; overflow-y: auto;
}
.gp-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.45rem 0.6rem; color: var(--sm-text); font-size: 0.9rem; }
.gp-item.is-on { background: var(--sm-bg-hover); font-weight: 600; }
@media (hover: hover) { a.gp-item:hover { background: var(--sm-bg-hover); } }
.gp-sub { margin-left: auto; padding-left: 0.6rem; font-family: var(--sm-font-mono); font-size: 0.74rem; color: var(--sm-text-dim); }

/* A community's own colour, one named slot each — see DOT_SLOTS. */
.gd { width: 9px; height: 9px; display: inline-block; flex: none; background: var(--sm-text-faint); }
.gd-a { background: var(--ga, var(--sm-text-faint)); }
.gd-b { background: var(--gb, var(--sm-text-faint)); }
.gd-c { background: var(--gc, var(--sm-text-faint)); }
.gd-d { background: var(--gd, var(--sm-text-faint)); }
.gd-e { background: var(--ge, var(--sm-text-faint)); }
.gd-f { background: var(--gf, var(--sm-text-faint)); }
.gd-g { background: var(--gg, var(--sm-text-faint)); }
.gd-h { background: var(--gh, var(--sm-text-faint)); }
.gd-i { background: var(--gi, var(--sm-text-faint)); }
.gd-j { background: var(--gj, var(--sm-text-faint)); }
.gd-k { background: var(--gk, var(--sm-text-faint)); }
.gd-l { background: var(--gl, var(--sm-text-faint)); }

/* ---- tiles ---- */
.adm-tiles { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: 0.5rem; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.adm-tile { padding: 0.85rem 1rem; background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); border-top: 2px solid var(--hue, var(--sm-accent)); }
.adm-tile-k { display: block; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-dim); }
.adm-tile-v { display: block; margin-top: 0.2rem; font-family: var(--sm-font-mono); font-size: 1.5rem; font-weight: 700; }
.adm-tile-s { display: block; margin-top: 0.1rem; font-size: 0.76rem; color: var(--sm-text-dim); }
/* A tile that opens the list behind its own number. The whole tile is the
   target rather than the figure inside it: the figure is 1.5rem of monospace
   and nothing else in the tile would be clickable, which is a small target
   inside a large obvious one. */
.adm-tile-a { display: block; margin: -0.85rem -1rem; padding: 0.85rem 1rem; color: inherit; text-decoration: none; }
.adm-tile-a:focus-visible { background: var(--sm-bg-hover); }
.adm-tile-a:focus-visible .adm-tile-k { color: var(--sm-text); }
@media (hover: hover) {
  .adm-tile-a:hover { background: var(--sm-bg-hover); }
  .adm-tile-a:hover .adm-tile-k { color: var(--sm-text); }
}
.adm-tile-more { display: block; margin-top: 0.1rem; font-size: 0.76rem; color: var(--sm-accent); }

/* ---- who has opened a page, and when ---- */
.adm-when-ago { color: var(--sm-text-dim); }
.adm-sub { margin: -0.6rem 0 1rem; font-size: 0.82rem; color: var(--sm-text-muted); }
.adm-sub a { color: var(--sm-accent); }

/* ---- the traffic chart: one column a day, stacked in two ---- */
.tk-wrap { background: var(--sm-bg-raised); border: var(--sm-card-bw) solid var(--sm-card-edge); padding: 1rem 1rem 0.6rem; }
.tk-legend { display: flex; gap: 1rem; margin: 0 0 0.9rem; font-size: 0.78rem; color: var(--sm-text-muted); }
.tk-key { display: inline-flex; align-items: center; gap: 0.4rem; }
.tk-sw { width: 10px; height: 10px; display: inline-block; }
.tk-sw-in { background: var(--sm-accent); }
.tk-sw-anon { background: var(--sm-text-faint); }
.tk-chart { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2px; align-items: end; height: 190px; }
.tk-col { display: flex; flex-direction: column; justify-content: flex-end; height: 100%; position: relative; min-width: 0; }
.tk-bar { display: flex; flex-direction: column-reverse; width: 100%; overflow: hidden; background: var(--sm-bg-deep); }
.tk-seg { display: block; width: 100%; }
.tk-anon { background: var(--sm-text-faint); }
/* Two px of the panel between the halves, so a stack reads as two things. */
.tk-in { background: var(--sm-accent); box-shadow: 0 -2px 0 0 var(--sm-bg-raised); }
.tk-num { position: absolute; top: -1.15rem; left: 50%; transform: translateX(-50%); font-family: var(--sm-font-mono); font-size: 0.68rem; color: var(--sm-text-muted); white-space: nowrap; }
.tk-day { margin-top: 0.35rem; font-size: 0.62rem; color: var(--sm-text-faint); text-align: center; white-space: nowrap; overflow: hidden; }

/* ---- the two columns ---- */
.adm-grid { display: grid; gap: 1.25rem; grid-template-columns: 1fr; }
@media (min-width: 920px) { .adm-grid { grid-template-columns: 1.45fr 1fr; } }

/* ---- tables ---- */
.adm-table { width: 100%; border-collapse: collapse; font-size: 0.87rem; }
.adm-table th { text-align: left; font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-dim); font-weight: 600; padding: 0 0.6rem 0.45rem 0; border-bottom: 1px solid var(--sm-border); }
.adm-table td { padding: 0.5rem 0.6rem 0.5rem 0; border-bottom: 1px solid var(--sm-border); vertical-align: middle; }
.adm-table tr:last-child td { border-bottom: 0; }
.adm-num { font-family: var(--sm-font-mono); text-align: right; font-variant-numeric: tabular-nums; }
.adm-slug { font-family: var(--sm-font-mono); color: var(--sm-text-dim); font-size: 0.8rem; }
/*
  WHO, IN THE THREE WAYS A PERSON IS NAMED. Stacked rather than strung along one
  line: the id is nineteen digits and would push the columns beside it off a
  narrow screen, and it is the line least often read and most often copied.
*/
.adm-who { line-height: 1.35; }

/* ---- the manager, in sections --------------------------------------------
   Communities as a row of buttons, a sticky bar to jump between four
   sections, and the member lists side by side. Square, like the rest of the
   site (the global border-radius rule), and no inline styles (the CSP). */
/* WRAPPED, NOT SCROLLED. One line with its scrollbar hidden meant a desktop
   (no swipe) could never reach a community past the panel's edge. */
.mgr-pills { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0 0 0.9rem; }
/* The same choice as a GET form, for a long list. */
.mgr-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin: 0 0 0.75rem; }
.mgr-filter label { font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--sm-text-dim); }
.mgr-filter select { flex: 1 1 14rem; min-width: 0; max-width: 28rem; width: auto; }
.mgr-filter .btn { padding: 0.45rem 1rem; }
/*
  THE MANAGER USES THE WHOLE WINDOW. Every other page is an 860px reading
  column; this one is tables and charts side by side for an operator on a wide
  screen. The gutter stays, so a phone keeps its edge, and the tables still
  scroll inside .adm-scroll rather than widening the page.
*/
.page-wide .panel { max-width: none; width: 100%; box-sizing: border-box; padding-left: clamp(1rem, 2.5vw, 2.5rem); padding-right: clamp(1rem, 2.5vw, 2.5rem); }
.mgr-pill { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.35rem 0.75rem; border: 1px solid var(--sm-border); background: var(--sm-bg-raised); color: var(--sm-text); font-size: 0.85rem; white-space: nowrap; text-decoration: none; }
.mgr-pill:hover, .mgr-pill:focus-visible { border-color: var(--sm-accent); outline: none; }
.mgr-pill.is-on { border-color: var(--sm-accent); color: var(--sm-accent); font-weight: 600; }
.mgr-sect { position: sticky; top: 0; z-index: 2; display: flex; gap: 1.2rem; overflow-x: auto; scrollbar-width: none; background: var(--sm-bg-base); border-bottom: 1px solid var(--sm-border); margin: 0 0 1rem; }
.mgr-sect::-webkit-scrollbar { display: none; }
.mgr-sect a { flex: 0 0 auto; padding: 0.55rem 0; color: var(--sm-text-dim); font-size: 0.88rem; border-bottom: 2px solid transparent; text-decoration: none; }
.mgr-sect a:hover, .mgr-sect a:focus-visible { color: var(--sm-text); border-bottom-color: var(--sm-accent); outline: none; }
.mgr-section { margin: 0 0 1.75rem; scroll-margin-top: 3rem; }
.mgr-cols { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 900px) { .mgr-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.mgr-col { border: 1px solid var(--sm-border); padding: 0.85rem; min-width: 0; }
.mgr-col .adm-table { font-size: 0.85rem; }
.mgr-col-title { margin: 0 0 0.6rem; font-size: 0.95rem; }
.mgr-facts { list-style: none; margin: 0 0 0.75rem; padding: 0; display: grid; gap: 0.6rem; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
.mgr-facts li { border: 1px solid var(--sm-border); padding: 0.6rem 0.75rem; display: grid; gap: 0.2rem; }
.mgr-fact-k { font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--sm-text-dim); }
.mgr-quicks { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; }
.mgr-quick { padding: 0.3rem 0.7rem; border: 1px solid var(--sm-border); font-size: 0.85rem; text-decoration: none; }
.mgr-quick:hover, .mgr-quick:focus-visible { border-color: var(--sm-accent); outline: none; }
.mgr-system { border: 1px solid var(--sm-border); padding: 0 0.85rem; }
.mgr-system > summary { cursor: pointer; padding: 0.75rem 0; font-weight: 600; list-style: none; }
.mgr-system > summary::-webkit-details-marker { display: none; }
.mgr-system > summary::before { content: "▸"; display: inline-block; width: 1.1em; color: var(--sm-text-dim); }
.mgr-system[open] > summary::before { content: "▾"; }
.mgr-system[open] { padding-bottom: 0.85rem; }
/* A link that leaves the manager for the site opens in a new tab and says so. */
.adm-out { font-size: 0.8em; margin-left: 0.2em; opacity: 0.7; text-decoration: none; display: inline-block; }
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.adm-who-name { display: block; }
.adm-who-at,
.adm-who-id {
  display: block;
  font-family: var(--sm-font-mono); font-size: 0.72rem;
  color: var(--sm-text-dim);
}
/* Faint, and selectable as a unit: this is the value somebody is copying into
   another tool, never one they are reading for pleasure. */
.adm-who-id { color: var(--sm-text-faint); user-select: all; }
.adm-when { font-family: var(--sm-font-mono); color: var(--sm-text-dim); font-size: 0.8rem; }
.adm-ev { color: var(--sm-text-muted); }
.adm-ev.out { color: var(--sm-text-faint); }

.adm-chip { display: inline-block; padding: 0.1rem 0.45rem; font-size: 0.7rem; border: 1px solid var(--hue, var(--sm-border-strong)); color: var(--hue, var(--sm-text-muted)); white-space: nowrap; }
.adm-chip.is-ok { --hue: var(--sm-success); }
.adm-chip.is-warm { --hue: var(--sm-amber); }
.adm-chip.is-bad { --hue: var(--sm-rose); }
.adm-sec { display: inline-block; padding: 0.05rem 0.35rem; margin: 0 0.2rem 0.2rem 0; font-size: 0.68rem; border: 1px solid var(--sm-border); color: var(--sm-text-dim); }
.adm-none { color: var(--sm-text-faint); font-style: italic; font-size: 0.8rem; }

/* ---- which pages get opened ---- */
.adm-share { display: grid; gap: 0.4rem; }
.adm-share-row { display: grid; grid-template-columns: 1fr auto; gap: 0.6rem; align-items: center; font-size: 0.85rem; }
.adm-share-bar { grid-column: 1 / -1; height: 5px; background: var(--sm-bg-deep); overflow: hidden; }
.adm-share-fill { display: block; height: 100%; background: var(--sm-accent); }

/*
  THE TABLE IS THE ONE THING HERE THAT CAN BE WIDER THAN A PHONE.

  It used to be the table itself that scrolled, set to display:block at a
  breakpoint — which stops a table being a table for layout purposes, and did
  not stop the document growing wider than the screen anyway. The heading above
  it slid off to the left and the whole page had to be scrolled back.

  A wrapper instead: the overflow lives in a box of its own, at every width
  rather than below a breakpoint, and nothing outside it moves.
*/
.adm-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.adm-scroll .adm-table { min-width: 46rem; }
/* The narrow member columns scroll only when their own content needs it. */
.adm-scroll-fit .adm-table { min-width: 0; }
.page-wide .adm-grid > * { min-width: 0; }
.adm-clock { font-family: var(--sm-font-mono); font-variant-numeric: tabular-nums; }
.adm-zone { display: block; font-size: 0.72rem; color: var(--sm-text-dim); }
.adm-theme {
  display: inline-block; padding: 0.05rem 0.4rem;
  font-size: 0.7rem; text-transform: capitalize;
  border: 1px solid var(--sm-border); color: var(--sm-text-muted);
}
@media (max-width: 620px) { .tk-chart { height: 150px; } }

/* ===========================================================================
   PRINTED.

   A guide is the one page here somebody might put on paper — pinned up, or
   read away from the screen. So the chrome goes, the ink turns black, and
   anything that was a link prints its own address after it, because a link on
   paper that does not say where it goes is a dead end.
=========================================================================== */
@media print {
  .top-bar, .page-links, .doc-nav, .doc-cards, .page-bar, .rule-link { display: none !important; }
  body { background: #fff; color: #000; }
  main { padding: 0; }
  .panel { max-width: none; padding: 0; margin: 0; }
  .doc { display: block; }
  .rule, .chan, .cmd, .res-link {
    background: transparent;
    border-color: #ccc;
    break-inside: avoid;
  }
  .doc-section { break-inside: auto; }
  .doc-h, h1 { color: #000; }
  .rule-text, .chan-what, .cmd-what, .res-what, .muted { color: #333; }
  .res-link[href]::after { content: " (" attr(href) ")"; font-size: 0.85em; color: #555; }
}

/* ===========================================================================
   WHAT THE RUNGS BELOW ALREADY GRANTED.

   Quieter than the list above it, because it is not another perk — it is the
   sentence that stops every rung repeating the eight before it.
=========================================================================== */
.perk-inherits {
  margin: 0.7rem 0 0;
  padding-top: 0.7rem;
  border-top: 1px solid var(--sm-border);
  color: var(--sm-text-muted);
  font-size: 0.85rem;
}

/* ============================== FEEDBACK ==============================
   The one page that takes something from a member. There is no script
   anywhere on this host, so anything that reacts to input reacts in CSS. */

/* The signed-out gate. Right-aligned, and that was a decision: every other
   signed-out prompt here sits left with the content it belongs to, and this
   one has no content beside it — so it aligns to the edge its button is on. */
/* Centred, icon included, at every width: a door in the middle of the page. */
.fb-gate { text-align: center; padding-top: 3rem; }
.fb-gate-mark { font-size: 2.4rem; color: var(--sm-accent); display: block; margin: 0 auto 0.75rem; }
.fb-gate-title { font-size: 1.6rem; margin: 0 0 0.5rem; text-wrap: balance; }
.fb-gate-say { color: var(--sm-text-dim); margin: 0 auto 1.25rem; max-width: 34rem; }
.fb-gate-act { margin: 0; display: flex; justify-content: center; }

.fb-title { font-size: 1.6rem; margin: 0 0 0.35rem; }
.fb-say { color: var(--sm-text-dim); margin: 0 0 1.25rem; }
.fb-sub { font-size: 1.1rem; margin: 0 0 0.9rem; }

/* The flash line, from the one query parameter a redirect carries. */
.fb-flash {
  border-left: 3px solid var(--hue, var(--sm-accent));
  padding: 0.6rem 0.85rem; margin: 0 0 1.1rem;
  background: var(--sm-bg-deep);
  color: var(--sm-text);
}
.fb-flash-good { --hue: var(--sm-teal); }
.fb-flash-warn { --hue: var(--sm-amber); }
.fb-flash-bad { --hue: var(--sm-rose); }

.fb-form { display: flex; flex-direction: column; gap: 1.1rem; }
.fb-row { display: flex; flex-direction: column; gap: 0.4rem; }
.fb-label { font-weight: 600; font-size: 0.92rem; }

/* The select wears the glyph its own selection names. */
.fb-select { position: relative; display: block; }
.fb-select-icon {
  position: absolute; left: 0.8rem; top: 50%; transform: translateY(-50%);
  color: var(--sm-text-dim); pointer-events: none;
}
.fb-kind {
  width: 100%; padding: 0.65rem 0.9rem 0.65rem 2.5rem;
  background: var(--sm-bg-deep);
  color: var(--sm-text); border: 1px solid var(--sm-border-strong);
  font: inherit;
}
/*
  THE ONE PLACE A CODEPOINT IS WRITTEN RATHER THAN A CLASS NAME.

  Everywhere else here a glyph is asked for by the kit's own class name,
  because the operator's Font Awesome kit is the source of truth and the
  per-icon ::before rules this site used to carry were deleted along with the
  hook they hung on. A glyph that CHANGES WITH A SELECTION cannot be a class,
  though — there is no script here to swap one — so these three are the
  exception, written against :has() on the wrapper.

  A browser without :has() shows the default lightbulb and the page still
  works; the icon simply stops following the control.
*/
.fb-select-icon::before { content: var(--fa, "\f0eb"); }
.fb-select:has(option[value="problem"]:checked) .fb-select-icon { --fa: "\f188"; }
.fb-select:has(option[value="other"]:checked) .fb-select-icon { --fa: "\f4ad"; }

.fb-body {
  width: 100%; padding: 0.7rem 0.9rem; min-height: 8rem; resize: vertical;
  background: var(--sm-bg-deep);
  color: var(--sm-text); border: 1px solid var(--sm-border-strong);
  font: inherit; line-height: 1.5;
}
.fb-body:focus-visible, .fb-kind:focus-visible {
  outline: 2px solid var(--sm-accent); outline-offset: 2px;
}
.fb-act { display: flex; justify-content: flex-start; }

/* Submissions are full-width bands, not cards in a grid: a reply is prose,
   and prose in a 320px card is a column two words wide. */
.fb-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.85rem; }
.fb-item {
  padding: 0.8rem 1rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-left: 3px solid var(--hue, var(--sm-border-strong));
}
.fb-item.hue-slate { --hue: var(--sm-text-dim); }
.fb-item.hue-amber { --hue: var(--sm-amber); }
.fb-item.hue-teal { --hue: var(--sm-teal); }
.fb-item.hue-violet { --hue: var(--sm-violet); }
.fb-item.hue-rose { --hue: var(--sm-rose); }

.fb-item-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.4rem; }
.fb-item-kind { color: var(--hue, var(--sm-text-dim)); }
.fb-item-kind-label { font-weight: 600; font-size: 0.92rem; }
.fb-item-ref { font-size: 0.78rem; color: var(--sm-text-dim); }
/* The state sits at the far end of its row, which is where the eye goes when
   it is scanning a list for what has changed. */
.fb-item-state {
  margin-left: auto; display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: 0.82rem; color: var(--hue, var(--sm-text-dim)); white-space: nowrap;
}
.fb-item-body { margin: 0; color: var(--sm-text); line-height: 1.55; overflow-wrap: anywhere; }
.fb-item-reply {
  display: flex; gap: 0.6rem; margin-top: 0.7rem; padding-top: 0.7rem;
  border-top: 1px solid var(--sm-border);
}
.fb-item-reply-who { color: var(--sm-accent); flex-shrink: 0; }
.fb-item-reply-text { margin: 0; color: var(--sm-text-dim); line-height: 1.55; overflow-wrap: anywhere; }

@media (max-width: 560px) {
  /* The state drops under the kind rather than being squeezed against it. */
  .fb-item-state { margin-left: 0; }
}

/* ===========================================================================
   A FINGER NEEDS 44PX.

   On a coarse pointer the small controls grow a hit area without growing the
   layout: filter chips, the pager and the guide's chips get a minimum height,
   and the icon-sized links in the top strip, headings and dialogs take padding
   paid back with a negative margin, so nothing around them moves. A mouse gets
   the page exactly as drawn.
=========================================================================== */
@media (pointer: coarse) {
  .filter-tab, a.ev-cat, .pager-btn {
    display: inline-flex; align-items: center; min-height: 44px;
  }
  .doc-nav-link { display: flex; align-items: center; min-height: 44px; }
  .theme-toggle { width: 44px; height: 44px; margin: -10px -10px -10px 0; }
  /* The sign-out glyph grows the same way, paying its extra back so the strip
     keeps its height. */
  .signout-btn { width: 44px; height: 44px; margin: -10px -6px; }
  .account-me { min-height: 44px; margin: -11px 0; }
  .cal-step, .cal-today { min-width: 44px; height: 44px; }
  .account-signin, .top-bar .linkish {
    display: inline-flex; align-items: center; min-height: 44px; margin: -11px 0;
  }
  .section-info { padding: 0.75rem; margin: -0.75rem; }
  .sm-close { display: grid; place-items: center; min-width: 44px; min-height: 44px; top: 0; right: 0; padding: 0; }
  .rule-link { padding: 0.5rem 0.65rem; top: 0; right: 0; }
}

/* ===========================================================================
   THE SKIN, APPLIED.

   The theme blocks at the top of this file only set tokens; this is where the
   components read the shape ones. Grouped here, by role, rather than written
   into forty component rules, so the question "which things are cards?" has
   one answer. Colour tokens are read in the component rules above as they
   always were.

   Four roles:
     cards and panels   --sm-radius, the skin's shadow and inset rule, --sm-clip
     rows in a list     --sm-radius-sm, --sm-clip-sm, no shadow (a board of
                        twenty-five shadowed rows is mud)
     small controls     --sm-radius-pill, --sm-clip-sm
     badges and avatars --sm-badge-radius, --sm-avatar-radius and friends
=========================================================================== */
.hub-card, .shop-item, .podium-card, .ach-item, .panel-card, .appeal-card, .reason-block,
.sanction-panel, .panel-ok, .res-link, .ev-next, .publish-prompt, .stat,
.info-item, .level-panel, .sub-panel, .chart-card, .badge-tile, .prog-shelf, .doc-card,
.fb-item, .sk-fact, .tk-wrap, .adm-tile {
  border-radius: var(--sm-radius);
  /* The inset pair is Elegant's gilt rule — a mat of the card's own colour,
     then one line — and draws nothing where --sm-inset-w is 0. */
  box-shadow:
    inset 0 0 0 var(--sm-inset-w) var(--card-bg, var(--sm-bg-raised)),
    inset 0 0 0 calc(var(--sm-inset-w) + 1px) var(--sm-inset-c),
    var(--sm-shadow);
  clip-path: var(--sm-clip);
}
.sm-sheet { border-radius: var(--sm-radius); clip-path: var(--sm-clip); }
.notice, .fb-flash, .mgr-col, .mgr-facts li, .mgr-system { border-radius: var(--sm-radius-sm); clip-path: var(--sm-clip-sm); }

.board-link, .rule, .chan, .cmd, .ev-row, .unlock-open, .fact-grid > div,
.sk, .gp-current, .gp-list, .field, .fb-kind, .fb-body, .prog-stage, .cal-day {
  border-radius: var(--sm-radius-sm);
  clip-path: var(--sm-clip-sm);
}
/* The one dropdown on the site must not lose its menu to its own clip. */
.gp-current, .gp-list { clip-path: none; }

.btn, .pager-btn, .filter-tab, .ev-cat, .name-mark, .member-flag, .ev-soon,
.adm-chip, .adm-sec, .adm-theme, .mgr-pill, .mgr-quick, .state-dot, .cal-step, .cal-today {
  border-radius: var(--sm-radius-pill);
  clip-path: var(--sm-clip-sm);
}
.state-dot { clip-path: none; }
.cal-chip { border-radius: var(--sm-radius-sm); }

.ach-icon, .badge-art, .empty-mark, .ok-mark, .ach-initial, .prog-glyph, .prog-medal {
  border-radius: var(--sm-badge-radius);
  clip-path: var(--sm-badge-clip);
}
.guild-icon, .guild-mark { border-radius: var(--sm-radius-sm); }

.avatar, .account-avatar, .whoami img, .sk-avatar {
  border-radius: var(--sm-avatar-radius);
  clip-path: var(--sm-avatar-clip);
}
.podium-avatar, .member-avatar {
  border-radius: var(--sm-avatar-radius);
  clip-path: var(--sm-avatar-clip);
  border: var(--sm-avatar-border);
  box-shadow: var(--sm-avatar-ring);
}
.avatar-fallback { background: var(--sm-avatar-bg); color: var(--sm-text-muted); }

/*
  PASTEL TILES, where a skin has them (Playful, Cosy, Elegant): the stat
  cards, the hub and guide cards and the badge wells take one of five tile
  colours in turn. As a custom property on the card rather than a background,
  so a hover state still wins, and so Elegant's inset mat can match it. A skin
  with no tiles leaves --sm-tile-* unset and every card keeps its surface.
*/
.stat-row > .stat:nth-child(5n+1) { --card-bg: var(--sm-tile-1); }
.stat-row > .stat:nth-child(5n+2) { --card-bg: var(--sm-tile-2); }
.stat-row > .stat:nth-child(5n+3) { --card-bg: var(--sm-tile-3); }
.stat-row > .stat:nth-child(5n+4) { --card-bg: var(--sm-tile-4); }
.stat-row > .stat:nth-child(5n+5) { --card-bg: var(--sm-tile-5); }
.badge-grid > li:nth-child(5n+1) .badge-art { --art-bg: var(--sm-tile-1); }
.badge-grid > li:nth-child(5n+2) .badge-art { --art-bg: var(--sm-tile-2); }
.badge-grid > li:nth-child(5n+3) .badge-art { --art-bg: var(--sm-tile-3); }
.badge-grid > li:nth-child(5n+4) .badge-art { --art-bg: var(--sm-tile-4); }
.badge-grid > li:nth-child(5n+5) .badge-art { --art-bg: var(--sm-tile-5); }

/* A clip-path clips the focus ring with everything else, so whatever has
   keyboard focus drops its cut corners while it has it. */
a:focus-visible, button:focus-visible, summary:focus-visible,
.field:focus-visible, .fb-kind:focus-visible, .fb-body:focus-visible { clip-path: none; }

/*
  THE GUIDE'S CONTENTS, "On this page", in every skin.

  Modern keeps the quiet rail it always had. The other five draw it as one of
  their own panels — the column beside a documentation page is where a skin
  shows most, and a bare rule looked unfinished next to rounded, shadowed
  sections.
*/
:root:not([data-portal-theme="modern"]) .doc-nav {
  padding: 0.95rem 1.05rem;
  background: var(--sm-bg-raised);
  border: var(--sm-card-bw) solid var(--sm-card-edge);
  border-radius: var(--sm-radius);
  box-shadow:
    inset 0 0 0 var(--sm-inset-w) var(--sm-bg-raised),
    inset 0 0 0 calc(var(--sm-inset-w) + 1px) var(--sm-inset-c),
    var(--sm-shadow);
  clip-path: var(--sm-clip);
}
@media (min-width: 861px) {
  :root:not([data-portal-theme="modern"]) .doc-nav-link {
    margin: 0 -0.55rem; padding: 0.2rem 0.55rem;
    border-radius: var(--sm-radius-sm);
  }
  @media (hover: hover) {
    :root:not([data-portal-theme="modern"]) .doc-nav-link:hover { background: var(--sm-bg-hover); }
  }
}
@media (max-width: 860px) {
  .doc-nav-link { border-radius: var(--sm-radius-pill); clip-path: var(--sm-clip-sm); }
}

/* ===========================================================================
   THE SKINS' OWN TOUCHES.

   What a token cannot say. Each is scoped to its theme and styles markup the
   pages already render; the ornaments are pseudo-elements, so there is no
   extra markup, and they are screen-only so a printed guide stays plain.
=========================================================================== */

/* The marks beside a big name are labels, not part of the name: they keep the
   body face whatever hand the title is set in (Elegant's script, Futuristic's
   and Fantasy's capitals). */
.member-id .name-marks, .banner-text h1 .guild-badges {
  font-family: var(--sm-font-ui);
  letter-spacing: 0;
  text-transform: none;
}

/* ---- Futuristic: labels spaced wider, and a name that stays on one row. */
:root[data-portal-theme="futuristic"] :is(h2.section-title, .stat-k, .level-k, .info-item dt, .fact-k,
  .board-filter-label, .doc-nav-title, .sm-modal-kicker, .chan-group-name, .sanction-head, .adm-tile-k) {
  letter-spacing: 0.14em;
}
/*
  Chakra Petch in spaced capitals is wide, and a display name that wraps onto
  a second line of capitals reads as two names. So the title scale shrinks the
  big names (--sm-title-scale) and the name itself holds one line, ending in an
  ellipsis before it can push the page sideways. The name is its own span so
  the role marks and Discord badges beside it still wrap below.
*/
:root[data-portal-theme="futuristic"] :is(.member-name, .banner-text h1) .name-text {
  display: inline-block; max-width: 100%; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  vertical-align: bottom;
}
:root[data-portal-theme="futuristic"] .banner-text h1 { flex-wrap: nowrap; }

/* ---- Playful: every control is inked like the cards. */
:root[data-portal-theme="playful"] :is(.btn, .pager-btn, .filter-tab, a.ev-cat, .field, .fb-kind, .fb-body, .ach-icon, .unlock-open) {
  border-width: 2px;
  border-color: var(--sm-card-edge);
}
:root[data-portal-theme="playful"] :is(.badge-art, .prog-glyph) { border: 2px solid var(--sm-card-edge); }
:root[data-portal-theme="playful"] .avatar { border: 2px solid var(--sm-card-edge); }
:root[data-portal-theme="playful"] :is(.btn, .pager-btn) { box-shadow: var(--sm-shadow); font-weight: 700; }

/* ---- Fantasy: Cinzel on the section heads, a gilt edge on the small avatars. */
:root[data-portal-theme="fantasy"] h2.section-title { font-family: var(--sm-font-display); letter-spacing: 0.1em; }
:root[data-portal-theme="fantasy"] h2.section-title :is(.section-link, .section-note) { font-family: var(--sm-font-ui); }
:root[data-portal-theme="fantasy"] .avatar { border: 1px solid var(--sm-accent); }

/* ---- Elegant: spaced Cormorant capitals for every label, and the script hand
   kept to titles and names (a sentence in Pinyon is hard work). */
:root[data-portal-theme="elegant"] :is(h2.section-title, .stat-k, .level-k, .info-item dt, .fact-k,
  .board-filter-label, .doc-nav-title, .sm-modal-kicker, .chan-group-name, .sanction-head, .adm-tile-k,
  .ev-dow, .ev-mon) {
  letter-spacing: 0.18em;
  font-weight: 700;
}
:root[data-portal-theme="elegant"] h1 { line-height: 1.25; }
:root[data-portal-theme="elegant"] .fb-gate-title {
  font-family: var(--sm-font-display); font-weight: 600;
  text-transform: none; letter-spacing: 0.02em;
}
:root[data-portal-theme="elegant"] .avatar { border: 1px dotted var(--sm-accent-2); }

@media screen {
  /*
    FANTASY'S GEMS: a small diamond of the accent on each corner of the big
    panels. Two pseudo-elements, one per edge, each clipped to a pair of
    diamonds joined by a line of no thickness — so four gems from two boxes.
  */
  :root[data-portal-theme="fantasy"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card) { position: relative; }
  :root[data-portal-theme="fantasy"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card, .sm-sheet, .doc-nav)::before,
  :root[data-portal-theme="fantasy"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card, .sm-sheet, .doc-nav)::after {
    content: "";
    position: absolute; left: -4px; right: -4px; height: 8px;
    background: var(--sm-accent);
    clip-path: polygon(0 4px, 4px 0, 8px 4px, calc(100% - 8px) 4px, calc(100% - 4px) 0, 100% 4px,
      calc(100% - 4px) 8px, calc(100% - 8px) 4px, 8px 4px, 4px 8px);
    pointer-events: none;
  }
  :root[data-portal-theme="fantasy"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card, .sm-sheet, .doc-nav)::before { top: -4px; }
  :root[data-portal-theme="fantasy"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card, .sm-sheet, .doc-nav)::after { bottom: -4px; }

  /*
    ELEGANT'S DIAMOND CHAIN: five small gilt diamonds, largest in the middle,
    sitting on the top edge of the big panels like the rule under a poster
    title. ::before is a pad of the panel's own colour that breaks the border
    under the chain; ::after is the chain, one clip-path of five diamonds.
  */
  :root[data-portal-theme="elegant"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card) { position: relative; }
  :root[data-portal-theme="elegant"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card, .sm-sheet, .doc-nav)::before {
    content: "";
    position: absolute; top: -7px; left: 50%; width: 83px; height: 14px; margin-left: -41.5px;
    background: inherit;
    pointer-events: none;
  }
  :root[data-portal-theme="elegant"] :is(.hub-card, .shop-item, .podium-card, .level-panel, .sub-panel, .ev-next,
    .publish-prompt, .panel-card, .chart-card, .sm-sheet, .doc-nav)::after {
    content: "";
    position: absolute; top: -7px; left: 50%; width: 67px; height: 14px; margin-left: -33.5px;
    background: var(--sm-accent-2);
    clip-path: polygon(0 7px, 3.5px 3.5px, 7px 7px, 12px 7px, 17px 2px, 22px 7px, 27px 7px, 33.5px 0.5px,
      40px 7px, 45px 7px, 50px 2px, 55px 7px, 60px 7px, 63.5px 3.5px, 67px 7px, 63.5px 10.5px, 60px 7px,
      55px 7px, 50px 12px, 45px 7px, 40px 7px, 33.5px 13.5px, 27px 7px, 22px 7px, 17px 12px, 12px 7px,
      7px 7px, 3.5px 10.5px);
    pointer-events: none;
  }
}
/* On a phone the contents column is not sticky, so it needs its own box for
   the ornaments to sit on. */
@media (max-width: 860px) {
  :root:is([data-portal-theme="fantasy"], [data-portal-theme="elegant"]) .doc-nav { position: relative; }
}

/* Paper gets none of it: no shadows, no cut corners, no pattern. */
@media print {
  *, *::before, *::after { box-shadow: none !important; clip-path: none !important; }
  body { background-image: none; }
}


/* ===========================================================================
   CARD EDGES, CLEANED UP (2026-09-22).

   Hub and guide cards used to take a different hue each — rose, amber, teal,
   violet — on a thick top stripe, the icon and the go-line, over a background
   tile that also changed card to card. On the rounded and ornate skins the
   stripe fought the skin's own edge: Playful's offset shadow, Fantasy's corner
   gems, Elegant's diamond chain. Now every card speaks in the community's
   brand colour, and only the squared skins (Modern, Futuristic) keep a top
   line, thinner, where a flat edge suits one.
=========================================================================== */
.hub-card, .doc-card { border-top-width: 2px; }
:root:is([data-portal-theme="playful"], [data-portal-theme="cosy"], [data-portal-theme="fantasy"], [data-portal-theme="elegant"]) :is(.hub-card, .doc-card, .adm-tile, .shop-item) {
  border-top: var(--sm-card-bw) solid var(--sm-card-edge);
}
@media (hover: hover) {
  :root:is([data-portal-theme="playful"], [data-portal-theme="cosy"], [data-portal-theme="fantasy"], [data-portal-theme="elegant"]) :is(.hub-card, .doc-card):hover {
    border-top-color: var(--sm-border-strong);
  }
}

/* =========================================================================
   COSMETICS — what a member bought, worn on their page.

   THIS IS THE SURFACE WHERE THEY MOVE. The profile card is a PNG and its
   overlay is painted once; here the same overlay falls, and a shimmering name
   shimmers. That difference is real, members notice it, and the shop states it
   before the sale rather than leaving this stylesheet to be blamed for it.

   NOTHING HERE DECIDES WHAT MAY BE WORN. The instance has already refused a
   colour that would be illegible and an item whose season has passed; by the
   time a value reaches these rules it is one somebody is entitled to. What
   this file owes in return is restraint: no cosmetic may sit between a reader
   and a link, cover text, or keep moving for somebody who asked for stillness.

   EVERY POSITION IS IN THIS FILE RATHER THAN IN AN ATTRIBUTE. The site's CSP
   carries no unsafe-inline and browsers apply style-src to attributes as well
   as to blocks, so a style attribute on a flake is dropped without an error
   and the snow falls in a single column. The twenty :nth-child rules below are
   what that constraint costs, and they are cheaper than the bug.
   ========================================================================= */

/*
  A MEMBER'S OWN COLOUR, SWAPPED IN AT THE PANEL AND NOWHERE ABOVE IT.

  layout() writes --wear into the nonced :root block; this is the only place it
  is read. Scoped to the panel so the top strip, the community bar and the
  footer stay in the community's colours — a member's page is theirs, the site
  around it is not.
*/
.panel.is-worn {
  --sm-brand: var(--wear, var(--brand, #4d97a1));
  --sm-accent: var(--wear, var(--sm-accent-auto));
  --sm-accent-hover: var(--wear-hover, var(--sm-accent-hover-auto));
}

/* ---- Name styles ------------------------------------------------------- */

/*
  Painted on the text itself, so the styles compose with whatever typeface the
  community's portal theme is using rather than replacing it.

  background-clip: text needs a transparent fill to show through, and a browser
  without it would render the name INVISIBLE rather than unstyled — which is
  the one failure worth guarding, so the colour is only dropped inside
  @supports.
*/
.name-text[data-namestyle="gradient"],
.name-text[data-namestyle="shimmer"],
.name-text[data-namestyle="chrome"] {
  color: var(--sm-accent);
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .name-text[data-namestyle="gradient"],
  .name-text[data-namestyle="shimmer"],
  .name-text[data-namestyle="chrome"] {
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}
/*
  A GRADIENT'S TWO ENDS ARE THE ITEM'S, when it names them. --wear-from and
  --wear-to arrive in the nonced block (or, on the shop, against the preview's
  own id) and fall back to the accent pair, which is what the instance does on
  the card for an item that names neither.
*/
.name-text[data-namestyle="gradient"] {
  background-image: linear-gradient(100deg, var(--wear-from, var(--sm-accent)), var(--wear-to, var(--sm-accent-hover)));
}
.name-text[data-namestyle="chrome"] {
  background-image: linear-gradient(180deg, #ffffff 0%, #c9ccd6 45%, #8b8f9c 50%, #e8eaf0 55%, #9aa0ad 100%);
}
/*
  THE ONE THAT ANIMATES. A sweep across the letters, three seconds. It moves a
  background position and nothing else, so it cannot cause a reflow behind
  somebody trying to read the page.
*/
.name-text[data-namestyle="shimmer"] {
  background-image: linear-gradient(100deg, var(--wear-from, var(--sm-accent)) 0%, #ffffff 50%, var(--wear-to, var(--sm-accent-hover)) 100%);
  background-size: 250% 100%;
  animation: wear-shimmer 3s linear infinite;
}
@keyframes wear-shimmer {
  from { background-position: 200% 0; }
  to { background-position: -50% 0; }
}
.name-text[data-namestyle="outline"] {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--wear-from, var(--sm-accent));
}
.name-text[data-namestyle="shadow"] {
  text-shadow: 0 2px 14px var(--wear-from, var(--sm-accent));
}
/*
  SERIF AND TECHNICAL ARE A TYPEFACE, and the page has the fonts to do it
  honestly — which the card does not, where they fall back to a weighted fill.
  The two surfaces differ the opposite way round from the overlays here, and
  that is worth knowing when either is next touched.
*/
.name-text[data-namestyle="serif"] {
  font-family: Georgia, "Times New Roman", serif;
}
.name-text[data-namestyle="technical"] {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  letter-spacing: -0.02em;
}

/* ---- Glow -------------------------------------------------------------- */

/* In the glow's own colour when the item names one (--wear-glow), and the
   wearer's accent when it does not — the instance's fallback on the card. */

.panel[data-glow="halo"] .member-avatar {
  box-shadow: 0 0 34px 6px color-mix(in srgb, var(--wear-glow, var(--sm-accent)) 55%, transparent);
}
.panel[data-glow="rim"] .member-avatar {
  border: 3px solid var(--wear-glow, var(--sm-accent));
  box-shadow: 0 0 16px color-mix(in srgb, var(--wear-glow, var(--sm-accent)) 80%, transparent);
}
.panel[data-glow="bar"] .level-bar-fill,
.panel[data-glow="bar"] .xp-bar-fill {
  box-shadow: 0 0 18px color-mix(in srgb, var(--wear-glow, var(--sm-accent)) 80%, transparent);
}
/*
  An underglow is a wash along the bottom of the panel, drawn in ::after so it
  costs no element and cannot be mistaken for content. pointer-events: none, or
  it would sit over whatever is at the foot of the page.
*/
.panel[data-glow="underglow"] { position: relative; overflow: hidden; }
.panel[data-glow="underglow"]::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 140px;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, color-mix(in srgb, var(--wear-glow, var(--sm-accent)) 32%, transparent));
}

/* ---- Frames ------------------------------------------------------------ */

/*
  A BORDER AROUND THE AVATAR, drawn as an OUTLINE so it stacks with a rim
  glow's border instead of replacing it — frame and glow are two slots, sold
  separately, and a member wearing both should see both. outline follows
  border-radius in every current browser, so a round avatar gets a round ring.

  In the frame's own colour (--wear-frame), or the wearer's accent. Gilt is
  gold whatever it names, because that is the whole of what gilt means, and
  neon adds its glow with filter rather than box-shadow so a halo glow's
  box-shadow is left alone. None of them moves.
*/
.wear-frame[data-frame] { outline-offset: 3px; }
.wear-frame[data-frame="solid"] { outline: 3px solid var(--wear-frame, var(--sm-accent)); }
.wear-frame[data-frame="double"] { outline: 6px double var(--wear-frame, var(--sm-accent)); }
.wear-frame[data-frame="dashed"] { outline: 3px dashed var(--wear-frame, var(--sm-accent)); }
.wear-frame[data-frame="dotted"] { outline: 4px dotted var(--wear-frame, var(--sm-accent)); }
.wear-frame[data-frame="gilt"] {
  outline: 4px double #d4af37;
  filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.55));
}
.wear-frame[data-frame="neon"] {
  outline: 2px solid var(--wear-frame, var(--sm-accent));
  filter: drop-shadow(0 0 6px var(--wear-frame, var(--sm-accent)));
}

/* ---- Title ------------------------------------------------------------- */

/* A line the server wrote, under the name: a small pill, in the accent. */
.member-title { margin: 0.2rem 0 0.1rem; }
.wear-title {
  display: inline-block;
  max-width: 100%;
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.4;
  padding: 0.1rem 0.6rem;
  border: 1px solid color-mix(in srgb, var(--sm-accent) 60%, transparent);
  border-radius: var(--sm-radius-pill);
  background: color-mix(in srgb, var(--sm-accent) 14%, transparent);
  color: inherit;
  overflow-wrap: anywhere;
}

/* ---- Seasonal overlays ------------------------------------------------- */

/*
  WEATHER, AND IT MUST NEVER BE IN THE WAY.

  pointer-events: none so nothing under it becomes unclickable, aria-hidden in
  the markup so nothing under it becomes unreadable, and clipped to the panel
  so it does not snow over the rest of the site.
*/
.wear-overlay {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  border-radius: inherit;
  z-index: 0;
}
.panel:has(> .wear-overlay) { position: relative; }
/*
  Everything the panel actually holds sits above the weather — only in a panel
  that HAS weather, and never a dialog. This was `.panel > :not(.wear-overlay)`
  on every panel, and it outranked `.sm-modal:target { position: fixed }` by
  coming later at the same specificity: a badge's dialog fell back into the
  page and its backdrop dimmed only a band the width of the panel behind the
  sheet, instead of the screen.
*/
.panel:has(> .wear-overlay) > :not(.wear-overlay, .sm-modal) { position: relative; z-index: 1; }

.wear-overlay i {
  position: absolute;
  top: -12%;
  display: block;
  opacity: 0.3;
  animation: wear-fall linear infinite;
}
/*
  HOW STRONG THE WEATHER IS, from the item — 0.05 to 0.45 in steps of 0.05, the
  instance's cap and this page's (see overlayOpacity in pages.js). One rule per
  step because the number cannot travel as a style attribute. 0.3 above is the
  instance's default, for an overlay that names none; it used to be 0.55, above
  the most the instance would ever send.
*/
.wear-overlay[data-opacity="5"] i { opacity: 0.05; }
.wear-overlay[data-opacity="10"] i { opacity: 0.1; }
.wear-overlay[data-opacity="15"] i { opacity: 0.15; }
.wear-overlay[data-opacity="20"] i { opacity: 0.2; }
.wear-overlay[data-opacity="25"] i { opacity: 0.25; }
.wear-overlay[data-opacity="30"] i { opacity: 0.3; }
.wear-overlay[data-opacity="35"] i { opacity: 0.35; }
.wear-overlay[data-opacity="40"] i { opacity: 0.4; }
.wear-overlay[data-opacity="45"] i { opacity: 0.45; }
@keyframes wear-fall {
  from { transform: translateY(-20%) rotate(0deg); }
  to { transform: translateY(1400%) rotate(220deg); }
}

.wear-overlay[data-overlay="snow"] i {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #ffffff;
}
.wear-overlay[data-overlay="confetti"] i {
  width: 8px; height: 4px;
  background: var(--sm-accent);
}
/* Confetti is many colours by definition; one colour repeated twenty times is
   a texture rather than a celebration. */
.wear-overlay[data-overlay="confetti"] i:nth-child(3n+1) { background: #ff5f6d; }
.wear-overlay[data-overlay="confetti"] i:nth-child(3n+2) { background: #ffc371; }
.wear-overlay[data-overlay="confetti"] i:nth-child(3n+3) { background: #7ee8fa; }
.wear-overlay[data-overlay="petals"] i {
  width: 12px; height: 7px;
  border-radius: 60% 40% 60% 40%;
  background: #ffb7d5;
}

/*
  TWENTY PIECES, EACH WITH ITS OWN COLUMN, SPEED AND DELAY.

  Written out because an attribute cannot carry them — see the note at the top
  of this section. The delays are negative so the flakes are already spread
  down the panel on the first frame; without that, a page load begins with an
  empty panel and twenty flakes entering in a line across the top.
*/
.wear-overlay i:nth-child(1)  { left:  2%; animation-duration: 11s; animation-delay:  -1s; }
.wear-overlay i:nth-child(2)  { left:  9%; animation-duration:  8s; animation-delay:  -5s; }
.wear-overlay i:nth-child(3)  { left: 15%; animation-duration: 13s; animation-delay:  -9s; }
.wear-overlay i:nth-child(4)  { left: 21%; animation-duration:  9s; animation-delay:  -3s; }
.wear-overlay i:nth-child(5)  { left: 27%; animation-duration: 12s; animation-delay: -11s; }
.wear-overlay i:nth-child(6)  { left: 33%; animation-duration: 10s; animation-delay:  -7s; }
.wear-overlay i:nth-child(7)  { left: 39%; animation-duration: 14s; animation-delay:  -2s; }
.wear-overlay i:nth-child(8)  { left: 45%; animation-duration:  9s; animation-delay:  -6s; }
.wear-overlay i:nth-child(9)  { left: 51%; animation-duration: 11s; animation-delay: -10s; }
.wear-overlay i:nth-child(10) { left: 57%; animation-duration:  8s; animation-delay:  -4s; }
.wear-overlay i:nth-child(11) { left: 63%; animation-duration: 13s; animation-delay:  -8s; }
.wear-overlay i:nth-child(12) { left: 69%; animation-duration: 10s; animation-delay: -12s; }
.wear-overlay i:nth-child(13) { left: 74%; animation-duration: 12s; animation-delay:  -1s; }
.wear-overlay i:nth-child(14) { left: 79%; animation-duration:  9s; animation-delay:  -5s; }
.wear-overlay i:nth-child(15) { left: 84%; animation-duration: 14s; animation-delay:  -9s; }
.wear-overlay i:nth-child(16) { left: 88%; animation-duration: 11s; animation-delay:  -3s; }
.wear-overlay i:nth-child(17) { left: 92%; animation-duration:  8s; animation-delay:  -7s; }
.wear-overlay i:nth-child(18) { left: 95%; animation-duration: 13s; animation-delay: -11s; }
.wear-overlay i:nth-child(19) { left: 97%; animation-duration: 10s; animation-delay:  -2s; }
.wear-overlay i:nth-child(20) { left: 99%; animation-duration: 12s; animation-delay:  -6s; }

/*
  SOMEBODY WHO ASKED FOR STILLNESS GETS IT, AND STILL GETS WHAT THEY PAID FOR.

  The cosmetics are not removed — a member who set reduced motion and bought
  Snowfall should still see snow on their page, and a shimmering name is still
  their name in their colours. What stops is the movement: the flakes settle
  where the :nth-child rules put them, and the shimmer holds one position.
*/
@media (prefers-reduced-motion: reduce) {
  .wear-overlay i,
  .name-text[data-namestyle="shimmer"] {
    animation: none;
  }
  .wear-overlay i { top: auto; }
  .wear-overlay i:nth-child(odd) { top: 22%; }
  .wear-overlay i:nth-child(even) { top: 64%; }
}

/* ---- Bought, not earned ------------------------------------------------ */

/*
  A BADGE FROM THE SHOP IS DRAWN APART. After the earned ones, under a line of
  its own, with a dashed edge and a small "Bought" mark — so a keepsake cannot
  pass for an achievement on the page whose point is what somebody did here.
*/
.badge-bought-title {
  margin: 1rem 0 0.6rem;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--sm-text-dim);
}
.badge-tile.is-bought {
  border-style: dashed;
  border-color: var(--sm-border-strong);
  background: transparent;
}
.badge-bought {
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sm-text-dim);
  border: 1px dashed var(--sm-border-strong);
  border-radius: var(--sm-radius-pill);
  padding: 0 0.45rem;
}

/* ---- Shop: what a cosmetic looks like ---------------------------------- */

/*
  THE SAME RULES AS THE MEMBER PAGE, pointed at a sample. A name style is a
  .name-text with its data-namestyle; a frame is a .wear-frame ring; a title is
  the same pill. Colours arrive against each preview's own id in the page's
  nonced block, so one card's gradient is not the next card's.
*/
.shop-preview {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-height: 2.25rem;
  padding: 0.45rem 0.6rem;
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border);
  border-radius: var(--sm-badge-radius, 0);
}
.shop-preview-name { font-size: 1.15rem; font-weight: 800; }
.shop-preview-k { font-family: var(--sm-font-mono); font-size: 0.78rem; color: var(--sm-text-dim); }
.shop-swatch {
  display: block;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--sm-avatar-radius);
  border: 1px solid var(--sm-border-strong);
  background: var(--pv-hex, var(--sm-accent));
}
.shop-ring {
  display: block;
  width: 1.75rem;
  height: 1.75rem;
  margin: 0.25rem;
  /* The avatar's own shape, so the ring previews the frame the theme will draw. */
  border-radius: var(--sm-avatar-radius);
  background: var(--sm-avatar-bg);
}
.shop-note {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--sm-text-dim);
}
.shop-motion { font-style: italic; }
.shop-until { font-size: 0.78rem; color: var(--sm-text-dim); }
