/* ThroneLogs leaderboard — glass theme, chosen after several mockup rounds
   (see mockups/leaderboard-v6/ for the full history: 20 background
   candidates, then narrowed to a Deep Sea x Starfield combination, then
   tuned — panel blur, star brightness, a header/grid gap mask, motion
   removed). This file is the production version of that mockup: same
   layout, same "Cold Expanse" background, reskinned from the mockup's
   purple accent to blue/navy and then again to S Tier's own red (see the
   --accent token below), with the parse-detail viewer changed from a
   slide-over to a centered modal.

   Self-contained on purpose — this page does not load dashboard.css or the
   site nav/footer scripts. It's a full-viewport tool, not an article page,
   and reintroducing the site chrome would break "fits the window exactly,
   always" (the header/footer bars would eat into the fixed 100vh layout).
   The ThroneLogs wordmark links back to / instead. */

:root{
  --panel:rgba(255,255,255,.04); --panel-2:rgba(255,255,255,.08);
  --border:rgba(255,255,255,.12); --hair:rgba(255,255,255,.09);
  /* Nav controls (buttons + the Patch box). Four passes now: a
     separately-tuned light grey -> literally var(--panel) (matched the
     header) -> a black overlay (too dark, went below the page background)
     -> a white overlay (fixed that, but landed back close to where var(
     --panel) already was -- "right back where we started"). Settled on
     literally matching instead of any overlay: transparent at rest, so the
     button shows exactly the surface behind it (the header panel in Glass,
     the flat grey in HUD) with zero color math to get wrong either
     direction. The border still defines the button's shape; hover is the
     only non-transparent state, just enough to prove it's clickable. */
  --nav-btn:transparent; --nav-btn-hover:rgba(255,255,255,.08);
  --text:#ece7e4; --text-dim:#a49dab;
  /* Site accent. Was a blue/navy replacing the mockup batch's purple through
     several rounds (see the button comment below for that history); now
     pinned to S Tier's own color from the tier list (`#d81f2c`, live-checked
     via GET /api/tier-list, not just the code default -- the owner can edit
     tier colors from the admin tier-list editor, so the default in
     lib/tierLists.js is not guaranteed to be current). Renamed --gold ->
     --accent since "gold" stopped describing the color a while ago; kept
     --accent-2 as the alias other rules already reference. Never used for
     row/rank colors, which are always tier-color DATA (computed per parse),
     not this fixed variable -- this is chrome only. If S Tier's color is ever
     changed in the tier list, this does NOT update automatically; it would
     need to be matched here by hand again, the same way it was matched once
     already in dashboard.css's own --accent (see that file's comment). */
  --accent:#d81f2c; --accent-2:#d81f2c;
  --sans:-apple-system,"Segoe UI",Helvetica,Arial,sans-serif;
  --mono:ui-monospace,"SF Mono",Consolas,"Roboto Mono",monospace;
  --tlb-track:rgba(255,255,255,.05); --tlb-border:rgba(255,255,255,.12); --tlb-dim:#a49dab;
  --tlb-valtext:#14101a; --tlb-mono:var(--mono); --tlb-label-w:130px;
  --r:5px;
  /* Brand color -- the ONE thing the Blue/Red toggle now controls (see the
     "colors" comment further down for the full history of what this toggle
     used to do). Red (S Tier's color) is the bare/default value, matching
     --accent above; body[data-accent="blue"] repoints it at the old navy
     from the pre-S-Tier button-matching rounds (#2f5fce, the last value
     that round settled on before the switch to red -- reused rather than
     inventing a new blue). Deliberately its own variable, NOT a re-scoped
     --accent: only the wordmark and the Upload-parse button read --brand,
     everything else that reads --accent (rank dots, HUD corner brackets,
     the settings-chip highlight, tier-adjacent chrome) stays the fixed S
     Tier red regardless of which brand color is picked. */
  --brand-blue:#2f5fce; --brand-red:#d81f2c; --brand:var(--brand-red);

  /* --dmg: the colour damage READS in -- the DPS timeline, the per-skill
     bars, the four totals. The default here is the lighter salmon every skin
     shared before hud-red got its own; hud-red overrides it to --brand below.
     A token rather than a literal because this is mid-skin-pass: hud-red is
     built first and the other skins are ported deliberately, not by
     inheriting a red that was never chosen for them. Parchment stays on the
     salmon until its own pass. */
  --dmg:#e0685f;

  /* --- surface tokens ---------------------------------------------------
     Every "subtle overlay" on the page used to be a hardcoded
     rgba(255,255,255,.0X) or rgba(0,0,0,.3X) literal, scattered across ~30
     rules (hover states, control fills, icon rings, recessed wells). That
     works fine while every skin is dark, and breaks completely the moment
     one isn't: a white-on-white hover is invisible, a white icon ring
     around a light thumbnail disappears, a black "recessed" well becomes
     the darkest thing on a paper-white page.

     These tokens name the INTENT instead of the color, so a light theme
     can flip the whole set to dark-ink overlays by redefining ~14 values
     rather than overriding ~30 rules. Values below are the dark defaults
     and are byte-identical in effect to what they replaced (a few
     near-duplicates -- .02/.025/.035 -- collapsed onto one --wash, which
     is imperceptible and was never a deliberate distinction).

       --wash        faintest surface fill (drop zone, menu row, rules card)
       --wash-2      control fill (select, text input, inline code)
       --wash-hover  hover state for rows/tabs/buttons
       --wash-on     selected/active row
       --ring        1px outline around icons and icon-like controls
       --ring-soft   inset outline on an EMPTY well
       --ring-sel    the "this one is chosen" ring on a spec point
       --well        recessed panel (gauges, the detect box)
       --well-2      empty equipment slot
       --icon-well   fill behind a circular spec-point icon
       --guide       chart crosshair line
       --tip-*       tooltip surface, since it's a floating window with its
                     own background rather than one of the page's panels */
  --wash:rgba(255,255,255,.03);
  --wash-2:rgba(255,255,255,.06);
  --wash-hover:rgba(255,255,255,.07);
  --wash-on:rgba(255,255,255,.10);
  --ring:rgba(255,255,255,.16);
  --ring-soft:rgba(255,255,255,.07);
  --ring-sel:#fff;
  --well:rgba(0,0,0,.28);
  --well-2:rgba(0,0,0,.32);
  --icon-well:#0d0c10;
  --guide:rgba(255,255,255,.35);
  --tip-bg:rgba(20,22,30,.9);
  --tip-border:rgba(255,255,255,.22);
  --tip-hair:rgba(255,255,255,.1);

  /* An UNCHOSEN spec point is dimmed so it reads as switched off. On a dark
     skin that means darkening it -- but darkening on a LIGHT skin does the
     opposite of what it's for: a near-black icon on white paper is the
     highest-contrast thing on screen, so the off state ends up shouting
     louder than the chosen one. Light skins fade toward their background
     with opacity instead. (Chosen is `filter:none` on every skin -- full
     color IS the signal.) */
  /* Opaque fill for floating menus. Must be a SOLID color, not one of the
     washes -- a native <option> list can't composite against the page. */
  --menu-bg:#14121a;

  /* STATUS COLORS. Every one of these was a bare hex written against a dark
     panel, and on parchment they measured 1.8-2.2:1 against #f2e5cd -- not
     "a bit washed out", effectively invisible. The dark-skin values here are
     the exact ones they replace, so HUD is unchanged; parchment overrides them
     with deep warm equivalents that clear 4.5:1.
     --st-err-2 is the softer/darker red (field errors, the gear sheet, a banned
     label); --st-err is the loud one. Two survive rather than one because the
     loud red on a light ground has nowhere to go that isn't the quiet one. */
  --st-ok:#6fb3a8; --st-warn:#e0a05f; --st-err:#ff8080; --st-err-2:#e5484d;
  --spec-off:grayscale(1) brightness(.38) contrast(1.05);
  --spec-hover:grayscale(.35) brightness(.72);
}
/* Brand color is a property OF THE THEME now, not a separate axis. The old
   Blue/Red toggle (body[data-accent]) is gone: HUD is red, Glass is blue,
   and there's nothing orthogonal left to pick. Glass also repoints --accent
   (corner brackets, rank dots, chip highlights -- everything that used to
   stay fixed S-Tier red regardless of brand) so the theme reads as one
   coherent blue rather than blue-brand-on-red-chrome.

   Scoped to bare-body + explicit glass, NOT `:not([data-skin="hud"])` as it
   was when HUD and Glass were the only two skins -- that older form matched
   every theme added since and silently repainted their accents blue at the
   same specificity as their own blocks, leaving which-one-wins up to source
   order. */
body:not([data-skin]),
body[data-skin="glass"]{--brand:var(--brand-blue);--accent:var(--brand-blue);--accent-2:var(--brand-blue);}
*{box-sizing:border-box;}
html,body{height:100%;margin:0;overflow:hidden;}
body{font-family:var(--sans);color:var(--text);position:relative;}
a{color:inherit;}
/* The hidden attribute loses to an explicit display -- keep this last-word
   rule so [hidden] always means hidden regardless of what else an element
   is styled as (bit ThroneLogs codebase has hit this trap before). */
[hidden]{display:none !important;}

/* --- backgrounds: one per skin, NOT one per color ------------------------
   Used to be two color variants of the same star-field construction, picked
   by the Blue/Red toggle. That toggle now controls --brand only (the
   wordmark + Upload-parse button, see the --brand comment in :root) and no
   longer touches the background at all -- so each SKIN gets its own fixed,
   on-theme background instead, chosen once and not recolored by the toggle.
   Glass keeps the original "Cold Expanse" star field (bottom-anchored
   color-wash gradient + a static seven-layer star field tiled at different
   sizes so the tiling never lines up -- that mismatch is what reads as
   depth). HUD replaces it entirely with a scanline/grid tactical-HUD
   pattern instead (see the body[data-skin="hud"] block below) -- stars
   never matched the Combat-HUD identity, they were a Glass-only motif.
   Motion was tried and explicitly rejected earlier ("doesn't work for our
   needs") -- neither background animates. */
body{background:#03050a;}
body::before,body::after{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;}
body::before{background:
  linear-gradient(180deg, rgba(3,5,10,1) 0%, rgba(6,14,28,.9) 72%, rgba(14,40,70,.75) 100%),
  radial-gradient(1100px 340px at 50% 108%, rgba(90,190,245,.34), transparent 72%);}
body::after{background-image:
  radial-gradient(2px 2px at 18% 22%, rgba(255,255,255,1), transparent 50%),
  radial-gradient(1.6px 1.6px at 68% 44%, rgba(255,255,255,.95), transparent 50%),
  radial-gradient(2.2px 2.2px at 42% 78%, rgba(255,255,255,1), transparent 50%),
  radial-gradient(1.4px 1.4px at 88% 14%, rgba(255,255,255,.88), transparent 50%),
  radial-gradient(1.7px 1.7px at 8% 62%, rgba(255,255,255,.92), transparent 50%),
  radial-gradient(1.3px 1.3px at 55% 12%, rgba(210,232,255,.8), transparent 50%),
  radial-gradient(1.5px 1.5px at 30% 55%, rgba(218,238,255,.75), transparent 50%);
  background-size:340px 340px, 250px 250px, 420px 420px, 190px 190px, 300px 300px, 160px 160px, 220px 220px;
  /* Keeps stars out of the thin gap between the header and the grid below,
     and ONLY that gap -- every other gutter still shows them. --star-gap-
     top/-bottom are px offsets from the viewport top, written by
     updateStarGap() in thronelogs-page.js from the real .tl2-topbar/.tl2-grid
     rects, so the masked band always matches the actual visible gap
     regardless of window size or content reflow. Default 0px/0px (before
     JS runs) is a zero-height hole -- no visible effect until real values
     land, so this degrades safely if JS is slow to run. Shared by both
     background variants -- the header/grid geometry doesn't change with
     theme, so there's no reason to duplicate this per variant. */
  -webkit-mask-image:linear-gradient(to bottom,
    #000 0, #000 var(--star-gap-top,0px),
    transparent var(--star-gap-top,0px), transparent var(--star-gap-bottom,0px),
    #000 var(--star-gap-bottom,0px), #000 100%);
  mask-image:linear-gradient(to bottom,
    #000 0, #000 var(--star-gap-top,0px),
    transparent var(--star-gap-top,0px), transparent var(--star-gap-bottom,0px),
    #000 var(--star-gap-bottom,0px), #000 100%);}

/* HUD's own background -- replaces the Glass body::before/::after entirely
   for body[data-skin="hud"] (attribute selector on body beats the plain
   body::before/::after above on specificity, so this fully overrides rather
   than layering). Third attempt: the first pass had a red glow + a grid
   (rejected: "no red/blue, no grid"); the second dropped those for a
   colorless vignette + scanlines, which was ALSO cut in favor of a flat,
   plain dark grey -- no gradient, no texture, no pattern at all, just solid
   color. First value tried (#09080a) came back "too dark"; lightened to
   #131217, still deliberately darker than the HUD panel fill (rgba(20,18,
   23,.94), effectively ~#14121a) so the chamfered panels still read as
   sitting on top of it instead of blending in, just not by as much.
   ::before/::after are still declared (background:none) rather than
   removed, so the shared header/grid-gap mask machinery and the base
   `body::before,body::after{...}` positioning rule stay harmlessly in
   place if a texture ever comes back. */
/* --dmg: HUD-RED ONLY, for now. Damage reads in the brand red here; every
   other skin keeps the salmon from :root until the batched skin pass reaches
   it. */
body[data-skin="hud"]{background:#131217;--dmg:var(--brand);--dialog-bg:#1b1a21;}
body[data-skin="hud"]::before,
body[data-skin="hud"]::after{background:none;}

/* =========================================================================
   ADDITIONAL SKINS — Porcelain / Vaporwave / Sakura / Parchment
   Ported from mockups/themes-v1 (see that batch's gallery.html for the full
   25-candidate set these four were picked from).

   Three of them are LIGHT, which is the whole reason the surface tokens in
   :root above exist: the page was built assuming a dark background
   everywhere, so every hover/ring/well was a white or black overlay
   literal. Each theme below redefines that token set rather than
   overriding the ~30 rules that consume it.

   The starfield (body::after) belongs to Glass alone -- it's a Glass motif,
   not a shared background layer -- so every non-Glass skin drops it. HUD
   already does this in its own block above; this covers the rest in one
   rule so a future skin gets it for free instead of inheriting stars it
   never asked for. */
body[data-skin="sakura"]::after,
body[data-skin="parchment"]::after{background-image:none;}

/* --- Sakura: soft pink daylight, light, gentle ------------------------- */
body[data-skin="sakura"]{
  --accent:#e0538c; --accent-2:#e0538c; --brand:#e0538c;
  --panel:rgba(255,255,255,.82); --panel-2:#fdeef4; --border:#f2d3e0; --hair:#f7e3ec;
  --text:#3a2430; --text-dim:#94707f;
  --nav-btn:rgba(255,255,255,.6); --nav-btn-hover:#fce7f0;
  --tlb-track:#fbe9f1; --tlb-border:#f2d3e0; --tlb-dim:#94707f; --tlb-valtext:#fff;
  --wash:rgba(58,36,48,.03); --wash-2:rgba(58,36,48,.05);
  --wash-hover:rgba(224,83,140,.10); --wash-on:rgba(224,83,140,.16);
  --ring:rgba(58,36,48,.18); --ring-soft:rgba(58,36,48,.10); --ring-sel:#3a2430;
  --well:rgba(58,36,48,.05); --well-2:rgba(58,36,48,.08); --icon-well:#fbe9f1;
  --guide:rgba(58,36,48,.35);
  --menu-bg:#fff7fa;
  --spec-off:grayscale(1) opacity(.28); --spec-hover:grayscale(.4) opacity(.6);
  --tip-bg:rgba(255,250,252,.97); --tip-border:rgba(58,36,48,.18); --tip-hair:rgba(58,36,48,.12);
  background:#fdf3f7;
}
body[data-skin="sakura"]::before{background:
  radial-gradient(900px 420px at 15% 0%, rgba(255,183,214,.55), transparent 70%),
  radial-gradient(800px 400px at 85% 8%, rgba(255,214,230,.6), transparent 70%);}
body[data-skin="sakura"] .tl2-panel,
body[data-skin="sakura"] .tl2-dm-panel,
body[data-skin="sakura"] .tl2-settings-panel{
  background:var(--panel);border:1px solid var(--border);border-radius:16px;
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  box-shadow:0 4px 18px -8px rgba(180,110,145,.3);}
body[data-skin="sakura"] .tl2-btn.primary{color:#fff;background:var(--accent);border-color:var(--accent);}

/* --- Parchment: aged paper, sepia ink, serif ---------------------------- */
body[data-skin="parchment"]{
  --accent:#8a5a2b; --accent-2:#8a5a2b; --brand:#8a5a2b; --dialog-bg:#f2e5cd;
  --panel:#f2e5cd; --panel-2:#e8d8ba; --border:#c9ab7c; --hair:#dcc59d;
  --text:#3a2c18; --text-dim:#7d6647;
  --nav-btn:#ece0c6; --nav-btn-hover:#e3d3b2;
  --tlb-track:#e6d6b6; --tlb-border:#c9ab7c; --tlb-dim:#7d6647; --tlb-valtext:#f6ecd8;
  --wash:rgba(58,44,24,.04); --wash-2:rgba(58,44,24,.06);
  --wash-hover:rgba(138,90,43,.12); --wash-on:rgba(138,90,43,.18);
  --ring:rgba(58,44,24,.22); --ring-soft:rgba(58,44,24,.12); --ring-sel:#3a2c18;
  --well:rgba(58,44,24,.07); --well-2:rgba(58,44,24,.10); --icon-well:#e6d6b6;
  --guide:rgba(58,44,24,.4);
  --menu-bg:#f2e5cd;
  --spec-off:grayscale(1) opacity(.28); --spec-hover:grayscale(.4) opacity(.6);
  --tip-bg:rgba(246,236,216,.98); --tip-border:rgba(138,90,43,.4); --tip-hair:rgba(58,44,24,.15);
  /* 5.1 / 5.0 / 5.9 / 5.6 against --panel, and warm enough to belong here. */
  --st-ok:#1c6b5c; --st-warn:#8a5510; --st-err:#a32724; --st-err-2:#a33025;
  /* The one theme that changes typeface -- a serif is most of what makes
     "aged paper" read as aged paper. --mono is deliberately left alone:
     it's used for numeric/tabular readouts where a proportional serif
     would break column alignment. */
  --sans:Georgia,"Times New Roman",serif;
  background:#e5d5b3;
}
body[data-skin="parchment"]::before{background:
  radial-gradient(1000px 700px at 30% 20%, rgba(255,250,235,.7), transparent 70%),
  radial-gradient(800px 600px at 80% 90%, rgba(160,120,70,.16), transparent 70%);}
body[data-skin="parchment"] .tl2-panel,
body[data-skin="parchment"] .tl2-dm-panel,
body[data-skin="parchment"] .tl2-settings-panel{
  background:var(--panel);border:1px solid var(--border);border-radius:3px;
  backdrop-filter:none;-webkit-backdrop-filter:none;
  box-shadow:0 2px 8px -3px rgba(90,65,30,.35);}
body[data-skin="parchment"] .tl2-btn.primary{color:#f6ecd8;background:var(--accent);border-color:var(--accent);}

/* --- full-viewport app shell --------------------------------------------
   Every pane is a min-height:0 grid/flex child -- without that, a
   scrolling child forces its track to grow and the layout spills past the
   viewport. Nothing scrolls the page itself; the class rail and the
   ranking each scroll inside their own pane.
   grid-template-columns is explicit for the same reason horizontally: left
   as the implicit `none` default, the single column sizes to the MAX-
   CONTENT width of whichever row can't shrink (the topbar, on narrow
   screens, since flex-wrap only kicks in once its container is actually
   squeezed) -- template-columns:minmax(0,1fr) means it's the container's
   own width, this whole page's mobile horizontal-overflow bug traced back
   to exactly this. Reported live: "the entire thing is too wide and
   doesn't fit small devices." */
.tl2-app{height:100vh;display:grid;grid-template-rows:auto 1fr;grid-template-columns:minmax(0,1fr);gap:12px;padding:12px 14px;}

/* panel surface (frosted glass) -------------------------------------------
   Panels let the stars read THROUGH them, not just around them -- the blur
   is what matters here, not the fill opacity. Full history, same day:
   18px (original) -> 3px -> 1.5px -> 2.3px -> 14px -> 8px -> 4px (current).
   Lower = sharper stars/less frost, higher = softer stars/more frost; most
   of that range has been tried at this point. If it moves again, that's
   the tradeoff to weigh.

   saturate(1.4) is gone. It boosted the color sampled from behind the panel
   UNIFORMLY, including the faint traces of color present even in the
   background's mostly-black upper reaches -- so panels read as much more
   evenly colored than the actual background, which is mostly black with
   color concentrated low/near the horizon glow and stars. Dropping the
   boost (now 1, i.e. no saturation change at all) lets the panel's color
   ratio track the real backdrop's black:color ratio instead of amplifying
   it everywhere equally. */
.tl2-panel{position:relative;background:var(--panel);border:1px solid var(--border);border-radius:16px;
  backdrop-filter:blur(4px) saturate(1);-webkit-backdrop-filter:blur(4px) saturate(1);
  box-shadow:0 12px 40px -22px rgba(0,0,0,.75);}

/* HUD skin -- ported from the round-4 mockup's "Combat HUD" concept
   (mockups/leaderboard-v4/skin-hud.css: chamfered panels, bracket corner
   accents, no frost) as an alternative to the shipped Glass skin. Toggled
   via data-skin="hud" on <body>, orthogonal to the Blue/Red background
   choice -- it only overrides panel chrome, never the background layers,
   so it renders correctly over either color. Solid instead of frosted
   (backdrop-filter:none) because the mockup's tactical-dashboard read came
   from crisp, opaque panels, not glass -- softening it back toward Glass
   would just make it a rounded-corner variant of the same skin. Corner
   brackets use --accent (not the mockup's separate purple --gold) so the
   accent color stays the single one used site-wide. */
body[data-skin="hud"] .tl2-panel{
  border-radius:0;background:rgba(20,18,23,.94);border-color:rgba(255,255,255,.14);
  backdrop-filter:none;-webkit-backdrop-filter:none;
  clip-path:polygon(14px 0,100% 0,100% calc(100% - 14px),calc(100% - 14px) 100%,0 100%,0 14px);
}
body[data-skin="hud"] .tl2-panel::before,
body[data-skin="hud"] .tl2-panel::after{content:"";position:absolute;width:15px;height:15px;
  border:2px solid var(--accent);opacity:.7;pointer-events:none;z-index:1;}
body[data-skin="hud"] .tl2-panel::before{top:6px;right:6px;border-left:none;border-bottom:none;}
body[data-skin="hud"] .tl2-panel::after{bottom:6px;left:6px;border-right:none;border-top:none;}
body[data-skin="hud"] .tl2-btn{border-radius:0;}
body[data-skin="hud"] .tl2-btn.primary{background:color-mix(in srgb, var(--brand) 22%, transparent);}
body[data-skin="hud"] .tl2-patchbox{border-radius:0;}
body[data-skin="hud"] .tl2-rail-list button.on{box-shadow:inset 2px 0 0 var(--dot,var(--accent));}

/* Same HUD chamfer/bracket treatment extended to the parse-detail modal
   (.tl2-dm-panel). It's a plain div, not a .tl2-panel, so the HUD pass above
   doesn't reach it and it would otherwise keep Glass's rounded/frosted look
   with HUD active everywhere else. Same construction: chamfered clip-path
   corners, the same 15px bracket accents, solid background instead of blur.

   (This used to cover the upload <dialog> too. That dialog is gone -- the
   upload flow is now the in-grid #tl-upload-view panel, which is a real
   .tl2-panel and so picks all of this up for free.)

   Bracket color reads --c: the specific parse's own percentile color, set
   inline on the panel itself by openDetail() in thronelogs-page.js. It has to
   be on the PANEL, not the content div nested inside it, since a
   pseudo-element only inherits custom properties from its own originating
   element. Falls back to the fixed accent for the Rules/Tutorial popups,
   which reuse .tl2-dm-panel but never set --c. */
body[data-skin="hud"] .tl2-dm-panel{
  border-radius:0;background:rgba(20,18,23,.96);border-color:rgba(255,255,255,.14);
  backdrop-filter:none;-webkit-backdrop-filter:none;
  clip-path:polygon(14px 0,100% 0,100% calc(100% - 14px),calc(100% - 14px) 100%,0 100%,0 14px);
}
body[data-skin="hud"] .tl2-dm-panel::before,
body[data-skin="hud"] .tl2-dm-panel::after{content:"";position:absolute;width:15px;height:15px;
  opacity:.7;pointer-events:none;z-index:1;border:2px solid var(--c, var(--accent));}
body[data-skin="hud"] .tl2-dm-panel::before{top:6px;right:6px;border-left:none;border-bottom:none;}
body[data-skin="hud"] .tl2-dm-panel::after{bottom:6px;left:6px;border-right:none;border-top:none;}
body[data-skin="hud"] .tl2-dm-scrim{backdrop-filter:none;-webkit-backdrop-filter:none;}

/* header ------------------------------------------------------------------ */
.tl2-topbar{display:flex;align-items:center;gap:14px;padding:11px 16px;flex-wrap:wrap;}
.tl2-topbar h1{font-size:15px;margin:0;letter-spacing:.16em;text-transform:uppercase;font-weight:900;white-space:nowrap;}
.tl2-topbar h1 a{text-decoration:none;color:inherit;}
.tl2-topbar h1 span{color:var(--brand);}
/* "by Aedra" credit line -- mockup candidate 05 (mockups/wordmark-v1). Stacked
   under the wordmark, name carries --brand to tie it back visually. */
.tl2-topbar h1.tl2-wm{display:flex;flex-direction:column;gap:2px;}
.tl2-topbar h1 .tl2-wm-by{font-size:8.5px;font-weight:600;color:var(--text-dim);}
.tl2-topbar h1 .tl2-wm-by i{font-style:normal;font-weight:800;color:var(--brand);}
.tl2-topbar .spacer{flex:1;}
.tl2-iabuild{font-size:10.5px;font-weight:400;letter-spacing:.02em;color:var(--text-dim);
  white-space:nowrap;opacity:.75;}

.tl2-patchbox{display:flex;align-items:center;gap:8px;padding:5px 11px;border-radius:var(--r);
  background:var(--nav-btn);border:1px solid var(--border);}
.tl2-patchbox-k{font-size:10px;letter-spacing:.1em;text-transform:uppercase;white-space:nowrap;color:var(--text-dim);}
.tl2-patchbox select{background:none;border:none;font-family:inherit;font-size:12.5px;font-weight:800;
  cursor:pointer;padding:0;color:var(--brand);}
.tl2-patchbox select:focus{outline:none;}
.tl2-patchbox select option{background:var(--menu-bg);color:var(--text);}

/* Settings chip-pair toggle -- shared by both groups in the settings
   drawer: the Theme (Glass/HUD) toggle and the Color (Blue/Red, `--brand`)
   toggle. Not mockup scaffolding. Started as a single background-only
   toggle directly in the header; moved into Settings once that panel
   existed (a background/color choice reads as a setting, not a per-visit
   action like Rules/Tutorial/Upload), then split into two groups once HUD
   became real. The "on" highlight uses `--brand` -- on the Color toggle
   that's exactly the color you just picked; on the Theme toggle it just
   means the highlight always matches whatever brand color is currently
   active, which is harmless and keeps one shared rule instead of two. */
/* Was a single flex ROW of two segmented buttons (HUD | Glass). With six
   skins that row can't fit the 320px settings drawer, so it's a 2-column
   grid instead -- the segmented-control look (one shared outer border, hair
   lines between cells) is kept by drawing the internal lines as cell
   borders rather than the old `button + button{border-left}`, which only
   knows about one dimension and would leave stray lines at row starts. */
/* One equal column per button, however many there are. A fixed `1fr 1fr`
   left an empty half behind when a skin was taken off the selector. */
.tl2-bgtoggle{display:grid;grid-auto-flow:column;grid-auto-columns:1fr;
  border-radius:var(--r);overflow:hidden;border:1px solid var(--border);}
.tl2-bgtoggle button{font-family:inherit;font-size:10px;letter-spacing:.08em;text-transform:uppercase;
  padding:7px 12px;cursor:pointer;border:none;background:var(--wash-2);color:var(--text-dim);
  text-align:center;}
.tl2-bgtoggle button:hover{background:var(--wash-on);}
.tl2-bgtoggle button.on{background:color-mix(in srgb, var(--brand) 16%, transparent);color:var(--brand);font-weight:800;}
/* A hair between neighbours, never on the outside edge -- the container's own
   border is the outside. */
.tl2-bgtoggle button + button{border-left:1px solid var(--border);}

/* --- settings slideout ---------------------------------------------------
   Right-side drawer, same slide-in-from-the-edge mechanism the round-6
   mockup's parse detail used before it became a centered modal in
   production -- reused here because a persistent settings drawer is
   exactly the case that pattern fits (pinned to an edge, not competing with
   the ranking for the middle of the screen the way parse detail does).
   Always in the DOM; opened/closed via the "is-open" class on #tl-settings,
   not built/destroyed per-open like the parse detail modal, since its
   content is static. z-index above .tl2-dm (60) so it can layer over a
   detail modal if one happens to be open too, though nothing currently
   opens both at once. */
.tl2-settings{position:fixed;inset:0;z-index:70;display:flex;justify-content:flex-end;pointer-events:none;}
.tl2-settings-scrim{position:absolute;inset:0;background:rgba(4,6,12,.55);
  backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);opacity:0;transition:opacity .2s ease;}
.tl2-settings-panel{position:relative;height:100%;width:min(320px,88vw);overflow-y:auto;
  background:rgba(20,22,30,.92);border-left:1px solid var(--border);
  backdrop-filter:blur(22px);-webkit-backdrop-filter:blur(22px);
  transform:translateX(100%);transition:transform .22s ease;}
.tl2-settings.is-open{pointer-events:auto;}
.tl2-settings.is-open .tl2-settings-scrim{opacity:1;}
.tl2-settings.is-open .tl2-settings-panel{transform:none;}
/* HUD: solid, no frost -- same treatment .tl2-panel/.tl2-dm-panel get. */
body[data-skin="hud"] .tl2-settings-panel{background:rgba(20,18,23,.97);
  backdrop-filter:none;-webkit-backdrop-filter:none;border-left-color:rgba(255,255,255,.14);}
body[data-skin="hud"] .tl2-settings-scrim{backdrop-filter:none;-webkit-backdrop-filter:none;}
.tl2-settings-head{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;
  border-bottom:1px solid var(--hair);}
.tl2-settings-head h2{font-size:14px;margin:0;letter-spacing:.1em;text-transform:uppercase;}
.tl2-settings-body{padding:18px;}
.tl2-settings-group{margin-bottom:22px;}
.tl2-settings-group:last-child{margin-bottom:0;}
.tl2-settings-group h3{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin:0 0 10px;}
.tl2-settings-account{display:flex;gap:8px;}
.tl2-settings-note{font-size:11px;color:var(--text-dim);margin:9px 0 0;line-height:1.5;}

.tl2-actions{display:flex;gap:8px;align-items:stretch;}
/* Settings gear -- square, icon-only, right of Upload parse. Grey (not
   --brand/--accent) on purpose: it's chrome, not an action competing with
   the primary button beside it. Sized from the sibling .tl2-btn's own box
   (align-items:stretch above) with aspect-ratio keeping it 1:1, so it
   matches their height without hardcoding a number that drifts if the
   button padding changes. */
.tl2-gear{display:flex;align-items:center;justify-content:center;aspect-ratio:1;padding:0;
  cursor:pointer;border-radius:var(--r);background:var(--nav-btn);border:none;
  color:var(--text-dim);flex:none;}
.tl2-gear svg{width:16px;height:16px;display:block;}
.tl2-gear:hover{background:var(--nav-btn-hover);color:var(--text);}
body[data-skin="hud"] .tl2-gear{border-radius:0;}
/* Second darkening pass -- see the --nav-btn comment in :root. Was
   literally var(--panel) (the header's own surface color); now a black
   overlay darker than that, so the buttons read as recessed relative to
   the header instead of just blending flush into it. */
.tl2-btn{font-family:inherit;font-size:11px;letter-spacing:.09em;text-transform:uppercase;padding:8px 15px;
  cursor:pointer;border-radius:var(--r);white-space:nowrap;
  background:var(--nav-btn);border:1px solid var(--border);color:var(--text);}
.tl2-btn:hover{background:var(--nav-btn-hover);}
/* The Account group's Login/Register/Manage controls are real links, not
   buttons -- they navigate to their own pages, so an <a> is the honest
   element (middle-click, open-in-new-tab, and the status bar all keep
   working). .tl2-btn was written for <button>, which supplies display and
   text-decoration for free; an <a> needs both spelled out. */
a.tl2-btn{display:inline-block;text-decoration:none;text-align:center;}
.tl2-btn:disabled{opacity:.4;cursor:not-allowed;}
.tl2-btn:disabled:hover{background:var(--nav-btn);}
/* Third attempt at matching the wordmark, and the one that actually works:
   NOT a solid fill at all. Two earlier passes both used var(--accent) as a
   literal hex match -- a separately-tuned darker --gold-fill (rejected: the
   owner wants the SAME token, not a compensated one), then --gold directly
   (rejected too: confirmed via getComputedStyle that both were the
   pixel-identical rgb(47,95,206), and the owner still saw the button read
   brighter). That means the mismatch was never about the hex value -- a
   large solid-filled block of a saturated color reads brighter than the
   same color as thin text, at the SAME underlying color, a real perceptual
   effect no hex choice fixes. So instead of filling, this renders the
   button the same way the wordmark renders: colored TEXT with only a thin
   border and a faint tint, not a block. Same rendering character as the
   wordmark -> reads as the same color, not just coded as the same color.
   Background/border read --brand, not --accent -- this button (along with
   the wordmark) is what the Blue/Red toggle actually controls now; the
   tint is a color-mix() off --brand instead of the old hardcoded
   rgba(216,31,44,...) so it switches color along with it. Text itself is
   plain white now (a later, separate request) rather than --brand -- the
   "match the wordmark" reasoning above was about the border/tint, not the
   label text specifically. */
.tl2-btn.primary{background:color-mix(in srgb, var(--brand) 14%, transparent);border-color:var(--brand);color:#fff;font-weight:800;}
.tl2-btn.primary:hover{background:color-mix(in srgb, var(--brand) 24%, transparent);}

/* body: rail + ranking + sidebar ------------------------------------------ */
.tl2-grid{display:grid;grid-template-columns:minmax(0,1fr) 244px;gap:12px;min-height:0;}
/* min-WIDTH:0 is load-bearing on the parse page, not boilerplate. Its
   rotation timeline is ~6000px wide (50px/sec x 120s) inside its own
   overflow-x:auto scroller. Grid/flex items default to min-width:auto --
   "never shrink below your content's min-content width" -- which IGNORES
   the inner scroller and sizes the column to the full 6000px, shoving the
   right-hand widgets clean off-screen. Found exactly that way in the
   mockup round (measured: a 6294px grid inside a 1500px viewport). The
   matching minmax(0,1fr) above is the other half of the same fix. */
.tl2-grid > *{min-height:0;min-width:0;}

/* .tl2-rail is a plain flex wrapper, NOT itself a .tl2-panel -- same
   convention as .tl2-side on the right (a bare flex column holding
   separately-panel'd .tl2-mod boxes). Content (what the parse is FROM) and
   Classes (who logged it) are two genuinely separate panels with a real gap
   between them, not one panel with an internal divider, so each gets its
   own border/background/HUD chamfer+brackets for free -- consistent with
   every other box on this page instead of a special case. Even 50/50 split
   via flex:1 1 50% on both, each its own flex column so its own button list
   scrolls independently. Content has only 4 static placeholder rows today
   so it'll rarely need its own scroll, but the framework should hold if
   that list grows later. */
.tl2-rail{display:flex;flex-direction:column;gap:12px;min-height:0;}
.tl2-rail-content,
.tl2-rail-classes{display:flex;flex-direction:column;flex:1 1 50%;min-height:0;}
.tl2-rail-head{font-size:9px;letter-spacing:.12em;text-transform:uppercase;padding:12px 13px 8px;flex:none;color:var(--text-dim);border-bottom:1px solid var(--hair);}
/* padding-top, not 0: the first row sat flush against .tl2-rail-head's
   bottom rule and read as clipped by it -- reported as the topmost item
   in both left selectors being cut off at the top. */
.tl2-rail-list{list-style:none;margin:0;padding:7px 8px 10px;overflow-y:auto;flex:1;min-height:0;}
.tl2-rail-list li{margin-bottom:2px;}
.tl2-rail-list button{width:100%;display:grid;grid-template-columns:7px 1fr auto;align-items:center;gap:9px;
  background:none;border:none;padding:6px 9px;cursor:pointer;font-family:inherit;font-size:11.5px;
  text-align:left;border-radius:var(--r);color:var(--text);}
.tl2-rail-list button:hover{background:var(--wash-hover);}
.tl2-rail-list button.on{background:var(--wash-on);box-shadow:inset 2px 0 0 var(--dot,var(--accent));}
.tl2-rail-list .dot{width:7px;height:7px;border-radius:50%;background:var(--dot,#555);}
.tl2-rail-list .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tl2-rail-list .n{font-size:9.5px;font-variant-numeric:tabular-nums;color:var(--text-dim);}

.tl2-main{display:flex;flex-direction:column;min-height:0;padding:14px 6px 12px 18px;position:relative;}
.tl2-main h2{font-size:11px;letter-spacing:.14em;text-transform:uppercase;margin:0 12px 14px 0;
  display:flex;align-items:center;gap:10px;flex:none;color:var(--text-dim);}
.tl2-board-h-rule{flex:1;height:1px;background:var(--border);}
#tl-board-h-section{flex:none;}
/* #tl-chart, not .tl2-chart: renderChart() in thronelogs-page.js sets
   el.className = "tlb-chart" on every redraw (mirroring the mockup's
   chart.js), which silently strips any class this container had -- the
   mockup got away with a class here because its equivalent container had
   no class at all, only an id. Styling by id survives the redraw; this bit
   production directly (243 rows rendered with no scroll container, so the
   ranking silently overflowed the viewport with no way to reach the rest). */
#tl-chart{overflow-y:auto;flex:1;min-height:0;padding:0 12px 4px 0;}

.tl2-side{display:flex;flex-direction:column;gap:12px;min-height:0;overflow-y:auto;}
.tl2-mod{padding:13px 14px;flex:none;}
.tl2-mod h3{font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;margin:0 0 10px;color:var(--text-dim);}
.tl2-empty{font-size:11.5px;font-style:italic;margin:4px 0;color:var(--text-dim);}

/* current best (no meta line -- those numbers live in the detail modal) --- */
.tl2-cb{cursor:pointer;}
.tl2-cb-v{font-size:28px;font-weight:900;line-height:1;font-variant-numeric:tabular-nums;color:var(--c);}
.tl2-cb-who{font-size:13.5px;font-weight:700;margin-top:6px;}
.tl2-cb-cls{font-size:10.5px;margin-top:1px;color:var(--text-dim);}

.tl2-tiles{display:grid;grid-template-columns:1fr 1fr;gap:9px;}
.tl2-tile{display:flex;flex-direction:column;gap:2px;}
.tl2-tile-k{font-size:8.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);}
.tl2-tile-v{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1;}

/* class record board ------------------------------------------------------ */
.tl2-record{margin:0;padding:0;list-style:none;}
.tl2-record li{display:flex;justify-content:space-between;align-items:baseline;gap:8px;font-size:11px;
  padding:6px 0;cursor:pointer;border-bottom:1px solid var(--hair);}
.tl2-record li:last-child{border-bottom:none;}
.tl2-rec-l{display:flex;flex-direction:column;min-width:0;}
.tl2-rec-l b{font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tl2-rec-l em{font-style:normal;font-size:9px;color:var(--text-dim);}
.tl2-rec-val{font-variant-numeric:tabular-nums;font-weight:800;color:var(--c);}

/* --- the ranking bars ----------------------------------------------------
   Chosen design, unchanged since round 1: horizontal bars, width ∝ the
   board's top DPS, filled with the row's own percentile/tier color, class
   shown under the player name. */
.tlb-chart{display:flex;flex-direction:column;gap:9px;}
.tlb-lane{display:grid;grid-template-columns:var(--tlb-label-w,200px) 1fr;align-items:center;gap:12px;cursor:pointer;}
/* Labels left-aligned, hugging the panel edge, instead of right-aligned --
   right-aligning left a wall of empty space in front of short names
   (nothing to anchor to on the left) while doing nothing useful for the
   bars, which don't start until the grid's second column regardless of
   label alignment. Left-aligning plus a narrower label column (--tlb-label-w
   130px, was 186px) reclaims that space for the bars instead, which is what
   this chart is actually about. */
.tlb-label{text-align:left;font-size:12.5px;min-width:0;overflow:hidden;}
.tlb-label .tlb-top{display:flex;align-items:baseline;justify-content:flex-start;gap:6px;white-space:nowrap;overflow:hidden;}
.tlb-rank{font-family:var(--tlb-mono,ui-monospace,"SF Mono",Consolas,monospace);color:var(--lane-color);font-weight:800;font-size:11px;flex:none;}
.tlb-player{font-weight:700;overflow:hidden;text-overflow:ellipsis;}
.tlb-cls{display:block;font-size:10.5px;color:var(--tlb-dim,#9c9391);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px;}
.tlb-track{position:relative;height:26px;background:var(--tlb-track,#1a171b);border:1px solid var(--tlb-border,#2e282c);border-radius:5px;overflow:hidden;}
.tlb-fill{height:100%;width:var(--pct,0%);background:linear-gradient(90deg,color-mix(in srgb, var(--lane-color) 60%, transparent), var(--lane-color));display:flex;align-items:center;justify-content:flex-end;padding-right:8px;transition:width .25s ease;}
.tlb-val{font-size:12px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--tlb-valtext,#121013);white-space:nowrap;}
.tlb-lane:hover .tlb-track{border-color:var(--lane-color);}
.tlb-empty{color:var(--tlb-dim,#9c9391);font-size:13px;padding:34px 0;text-align:center;}

/* --- parse detail: CENTERED MODAL, smaller than and over the leaderboard -
   Replaces the earlier in-page slide-over. Sized narrower than the ranking
   panel (which flexes to fill most of the window) and centered over the
   whole page via a flex scrim, rather than pinned to an edge. */
.tl2-dm{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;padding:24px;}
.tl2-dm-scrim{position:absolute;inset:0;background:rgba(4,6,12,.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);}
.tl2-dm-panel{position:relative;width:min(440px,92vw);max-height:80vh;overflow-y:auto;
  background:rgba(20,22,30,.88);border:1px solid var(--border);border-radius:16px;
  backdrop-filter:blur(22px);-webkit-backdrop-filter:blur(22px);
  box-shadow:0 30px 70px -30px rgba(0,0,0,.8);
  transform:translateY(10px) scale(.98);opacity:0;transition:transform .16s ease,opacity .16s ease;}
.tl2-dm.is-in .tl2-dm-panel{transform:none;opacity:1;}

/* Generic popup shell now -- used to also style a parse-detail mini-modal
   (rank/value/stat-grid/link-through chrome), removed along with that
   modal now that clicking a parse opens the full breakdown directly. What's
   left is just the title-bar + body pattern Rules/Tutorial/Disclaimer use. */
/* sticky, not static: .tl2-dm-panel itself is the scrolling element
   (overflow-y:auto), and a plain in-flow header would scroll away with the
   body -- fine for Rules/Tutorial/ToS's short content, but the Color
   Legend's 101 rows are tall enough that losing the close button off the
   top would strand the popup open. background:inherit copies whichever
   skin's actual panel fill (HUD or Glass have different values) so the
   pinned header stays opaque over scrolled content instead of a hardcoded
   color that'd be wrong for one of the two. */
.tl2-dt-head{display:flex;align-items:flex-start;gap:12px;padding:16px 18px;border-bottom:1px solid var(--hair);
  position:sticky;top:0;z-index:2;background:inherit;}
.tl2-dt-id{display:flex;flex-direction:column;min-width:0;flex:1;}
.tl2-dt-who{font-size:18px;font-weight:800;line-height:1.2;margin-top:2px;}
.tl2-dt-x{background:none;border:none;cursor:pointer;font-size:14px;padding:2px 5px;font-family:inherit;line-height:1;color:var(--text-dim);}
.tl2-dt-x:hover{color:var(--text);}
.tl2-dt-body{padding:16px 18px 22px;}

/* Color Legend popup body: gradient strip + percentile labels, nothing
   else -- see colorLegendHtml() in thronelogs-page.js. The gradient itself
   is inline (background set from JS, since the stop list lives there, not
   here) -- this only styles the fixed shape/border and the label rows
   overlaid on top of it.
   Widens just this one popup instance -- :has() rather than a new class on
   the shared .tl2-dm-panel/openInfoPopup API, since every other popup
   (Rules/Tutorial/ToS) is plain text that's fine at the default width. */
.tl2-dm-panel:has(.tl2-clegend){width:min(1040px,95vw);}
/* HUD's bottom-left corner bracket (.tl2-dm-panel::after) removed for this
   popup ONLY -- same :has() scoping. The top-right bracket (::before) is
   untouched. */
body[data-skin="hud"] .tl2-dm-panel:has(.tl2-clegend)::after{display:none;}
/* The strip must fill the popup with NO gap anywhere -- not above it
   (against the header's own bottom divider) and not below it (against the
   panel's own bottom edge). .tl2-dt-body's normal padding (16px 18px 22px,
   meant for Rules/Tutorial/ToS's paragraphs) would leave exactly those two
   gaps, so it's zeroed out here, scoped the same :has() way, and the strip
   fills the full resulting box edge to edge. */
.tl2-dm-panel:has(.tl2-clegend) .tl2-dt-body{padding:0;}
.tl2-clegend{display:block;}
/* Tall (16000px) so the popup genuinely needs to scroll
   (.tl2-dm-panel's own overflow-y:auto/max-height:80vh handles that, same
   sticky header as before keeps the close button reachable throughout) --
   this is the "longer, allow scrolling" part of the request. Only 7 named
   percentiles get a TEXT label (see COLOR_LEGEND_POINTS in
   thronelogs-page.js), spaced out across this real length rather than
   packed into a compact box; an earlier pass generated one label per data
   point and was corrected ("way too many numbers") -- the extra length
   here is about the strip/scroll feel, not label count. No border of its
   own: it's flush against the panel on every side, so a separate border
   would just double up against the panel's existing one. Corner rounding
   matches whichever skin's popup shape it's sitting inside: HUD panels
   are chamfered, not rounded, so the strip stays square there; Glass
   panels ARE rounded (16px), and since this is the bottom-most content in
   an edge-to-edge layout, only its bottom corners need to follow that
   curve -- the top stays square against the straight header divider
   above it. */
.tl2-clegend-strip{position:relative;width:100%;height:16000px;overflow:hidden;
  border-radius:0 0 15px 15px;}
body[data-skin="hud"] .tl2-clegend-strip{border-radius:0;}
/* top is a PIXEL value set by JS (renderColorLegend), not a percentage --
   centering each row on its target point can push a row near an edge
   partly past the strip's own bounds, clipped by overflow:hidden; JS
   clamps the computed position to stay inside instead.
   font-size reverted to 11px -- a half-size (5.5px) pass shipped briefly
   and was confirmed too small to read in an actual screenshot, not just
   small in theory. */
.tl2-clegend-row{position:absolute;left:0;right:0;height:14px;
  display:flex;align-items:center;padding:0 14px;
  font-family:var(--mono);font-size:11px;font-weight:700;color:rgba(255,255,255,.92);
  text-shadow:0 1px 2px rgba(0,0,0,.85),0 0 6px rgba(0,0,0,.5);}


@media (max-width:1000px){
  .tl2-grid{grid-template-columns:172px 1fr;}
  .tl2-side{display:none;}
}
@media (max-width:700px){
  .tl2-grid{grid-template-columns:1fr;grid-template-rows:auto 1fr;}
  /* 26vh (both panels combined, ~13vh/~105px each) fit barely 2 rows before
     scrolling -- reported live as "too short to be usable on mobile".
     42vh gives each of Classes/Content roughly 4-5 visible rows before its
     own internal scroll (.tl2-rail-list already scrolls, see padding-top
     comment above) takes over, without starving the ranking panel below
     it, which still scrolls internally too (#tl-chart, overflow-y:auto). */
  .tl2-rail{max-height:42vh;}
  .tl2-topbar{gap:9px;}
  .tl2-topbar h1{font-size:13px;}
  /* Ranking bars -> badges. A variable-length fill scaled to %-of-top DPS
     needs real horizontal room to read as a bar at all; on a phone-width
     lane it was reported as the main reason "the entire thing is too wide"
     -- the label column and the bar were fighting over the same ~300px.
     Dropping the %-width fill for a small fixed-size rank-color badge
     (color + number only, no bar) removes that fight entirely: the label
     gets the room it needs, the badge stays a constant size regardless of
     DPS. tlb-val's existing dark text color is unchanged on purpose -- it
     was already tuned to sit on top of the bar's SOLID lane-color end
     (see .tlb-fill's gradient), and a badge is that same solid color. */
  .tlb-lane{grid-template-columns:1fr auto;gap:8px;}
  .tlb-track{width:auto;height:24px;border-radius:12px;background:none;border:none;overflow:visible;}
  .tlb-fill{width:auto !important;background:var(--lane-color);border-radius:12px;
    padding:0 10px;justify-content:center;}
  /* .tl2-actions (Patch/Rules/Tutorial/Upload/gear) is one flex item inside
     .tl2-topbar -- .tl2-topbar's own flex-wrap only wraps ITS direct
     children, so without this .tl2-actions just overflowed the topbar
     sideways instead of wrapping itself. .tl2-patchbox getting flex:none
     stops ITS OWN children (the Patch label + select) from being squeezed
     illegibly thin before .tl2-actions as a whole gets to wrap. */
  .tl2-actions{flex-wrap:wrap;}
  .tl2-patchbox{flex:none;}
  /* Parse breakdown header: .tl2-phead's align-items:center vertically
     centers every item against the row's tallest item -- fine at desktop
     width where .tl2-ptitle is one line, but once its meta line wraps to
     several rows on a narrow phone, "centered" put Report/Delete/Board
     floating mid-height, visually overlapping the meta text underneath
     them. flex-wrap + forcing .tl2-ptitle onto its own full-width row
     drops the button group onto a clean second row below the title
     instead, where it wraps its own buttons via .tl2-btn's normal sizing. */
  .tl2-phead{flex-wrap:wrap;align-items:flex-start;row-gap:10px;}
  .tl2-ptitle{flex:1 1 100%;}
}

/* ===========================================================================
   FULL PARSE BREAKDOWN
   Chosen from the ten in mockups/parse-v1/ -- "08 Instrument Cluster": the
   four totals stand as a gauge column BESIDE the DPS chart rather than a row
   above it, so numbers and curve share one band and the page buys back the
   vertical space a full-width stat row spends.

   Placement, per the owner's plan: the breakdown replaces the two leftmost
   windows (Classes, DPS Ranking) with one wider window IN PLACE -- this is
   NOT a separate page. #tl-parse-view lives inside the same .tl2-grid as
   #tl-board-rail/#tl-board-main and is shown/hidden by JS
   (openParseView()/closeParseView() in thronelogs-page.js) while the URL
   gains/loses a ?parse= param via history.pushState -- no navigation, no
   document reload, so the HUD+Red lock and everything else about the page
   is simply still true. (First cut of this shipped as an actual second
   page, /other/parse.html with its own <html> -- reported back as a
   critical bug: it loaded in Glass, because the LOCK_HUD_RED script only
   ran on the document that included it, and a real navigation starts a
   fresh document. Folding it into this page fixes that at the root instead
   of duplicating the lock script into a second file.) Current best / Class
   record holders / Board stay in .tl2-side, untouched either way. */
/* SPANS ONE COLUMN, not two. These full-panel views (parse, upload, account,
   profile) replace the board, and the grid used to be `rail | main | side` --
   so spanning 2 covered rail+main and left .tl2-side in column 3. The rail is
   gone and the grid is `main | side`, so the same rule now swallows the widget
   column too and drops the widgets onto a second row at full width. Reported
   as "parse breakdowns are broken". */
.tl2-parse{display:flex;flex-direction:column;min-height:0;min-width:0;
  grid-column:1;}
/* Same min-width:0 reasoning as .tl2-grid > * -- these are the ancestors
   between that column and the wide rotation scroller, and any one of them
   left at min-width:auto reintroduces the blow-out. */
.tl2-parse-scroll{overflow-y:auto;min-height:0;min-width:0;flex:1;padding:16px 18px 22px;}
.tl2-parse-msg{color:var(--text-dim);font-size:13px;padding:40px 0;text-align:center;margin:0;}

/* Header. No class color bar -- removed on the owner's call; the class name
   is already in the meta line and the bar was the only thing on this page
   still pulling from /api/class-colors, so that fetch is gone too. */
/* .tl2-ptitle takes the slack so the action buttons sit RIGHT. The push
   used to come from .tl2-del-err's margin-left:auto, but that element is
   [hidden] unless a delete actually fails -- so in the normal case there
   was nothing to push and the buttons collapsed against the title. */
.tl2-phead{display:flex;align-items:center;gap:14px;margin-bottom:18px;}
.tl2-ptitle{flex:1;min-width:0;}
.tl2-ptitle h2{margin:0;font-size:22px;font-weight:800;line-height:1.15;}
.tl2-pmeta{font-size:11.5px;color:var(--text-dim);margin-top:4px;display:flex;flex-wrap:wrap;gap:12px;}
/* Each meta item (class, duration, upload date, player count) wraps to its
   own line as a whole unit when the row runs out of room -- without this
   they had no protection against wrapping mid-item instead, which on a
   narrow phone width turned "Aug 17, 2026, 8:22 PM" into one word per
   line. */
.tl2-pmeta span{white-space:nowrap;}
.tl2-del-err{color:#ff8080;font-size:11.5px;margin-left:auto;}
.tl2-btn.danger{border-color:rgba(224,104,95,.5);color:#e0685f;}
.tl2-btn.danger:hover{background:rgba(224,104,95,.14);}
a.tl2-btn{text-decoration:none;display:inline-flex;align-items:center;}

/* Section shell inside the parse panel. */
.tl2-sec{margin-bottom:20px;}
.tl2-sec:last-child{margin-bottom:0;}
.tl2-sec-h{display:flex;align-items:center;gap:10px;font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--text-dim);margin:0 0 10px;}
.tl2-sec-h::after{content:"";flex:1;height:1px;background:linear-gradient(90deg,var(--border),transparent);order:5;}
.tl2-sec-h .dur{flex:none;color:var(--text-dim);letter-spacing:0;text-transform:none;font-size:11px;}

/* --- the cluster: gauge column + chart -------------------------------- */
.tl2-cluster{display:grid;grid-template-columns:190px minmax(0,1fr);gap:16px;margin-bottom:20px;}
.tl2-gauges{display:grid;grid-template-columns:1fr;gap:8px;}
/* Darkened vs. the mockup's rgba(255,255,255,.02): these sit against the
   panel and were reading as raised chips competing with the chart beside
   them. A black overlay recesses them instead -- safe here (unlike the nav
   buttons, which had to stay above the page background) because the panel
   fill they sit on is itself lighter than the page behind it. */
.tl2-gauge{border:1px solid var(--border);background:var(--well);padding:10px 13px;
  display:flex;flex-direction:column;justify-content:center;}
.tl2-gauge h4{font-size:9.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--text-dim);margin:0 0 6px;font-weight:600;}
.tl2-gauge .v{font-size:19px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1;}
.tl2-gauge .sub{font-size:11px;color:var(--text-dim);margin-left:5px;font-weight:600;}
/* Per-figure colours: the original four-colour cluster, and still what every
   skin but hud-red shows. */
.v-dmg{color:var(--dmg);} .v-dps{color:#e0a53b;} .v-crit{color:#e0a53b;} .v-heavy{color:#4a9fe0;}
/* On hud-red all four read in --dmg instead -- these are THE numbers the
   breakdown exists to report, and four separate colours made the cluster read
   as another table. Scoped to the skin it was designed against; the others
   keep the four-colour version until their own pass. The COLUMN colours in
   the skills table (.c-crit, .c-heavy, .c-dps) are a separate scheme and are
   untouched on every skin. */
body[data-skin="hud"] .tl2-gauge .v{color:var(--dmg);}
.tl2-sec-chart{display:flex;flex-direction:column;margin-bottom:0;}
.tl2-sec-chart #tlp-chart{flex:1;min-height:0;}

/* DPS timeline chart. #tlp-chart, not #tl-chart -- #tl-chart is already the
   LEADERBOARD's DPS-ranking-bars host (see the comment above .tl2-chart's
   own #tl-chart rule further up this file); the parse breakdown living in
   the same document now means a shared id would silently break one of the
   two (duplicate ids, `getElementById` only ever finds the first).
   Height comes from --chart-h on the HOST, not the svg, because the
   renderer READS that value to build a 1:1 viewBox (see renderTimeline in
   thronelogs-page.js). Fixed viewBox="0 0 1000 240" + preserveAspectRatio=
   "none" (the pre-merge page's approach) only ever looked right because the
   old dashboard.css article shell was always ~1000px wide -- in this
   narrower column the non-uniform scale squashed every axis label
   horizontally. Setting an svg height directly here would desync the two
   and bring that back. */
#tlp-chart{--chart-h:250px;display:block;}
svg.tl-chart{display:block;max-width:100%;}
svg.tl-chart .grid{stroke:var(--border);stroke-width:1;}
svg.tl-chart .axis-label{fill:var(--text-dim);font-size:10px;}
/* Damage reads in --dmg (see the token's note in :root): the brand red on
   hud-red, the older salmon everywhere else until each skin gets its pass. */
svg.tl-chart .area{fill:color-mix(in srgb,var(--dmg) 14%,transparent);}
svg.tl-chart .line{fill:none;stroke:var(--dmg);stroke-width:2;stroke-linejoin:round;stroke-linecap:round;}

/* --- view picker (Skills / Targets / Players) --------------------------
   Skills is THE view; Targets and Players are verification data the owner
   wants reachable without it competing for attention, so they live behind a
   chevron next to the heading instead of as three equal tabs. */
.tl2-viewpick{position:relative;flex:none;}
.tl2-chev{display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;
  background:var(--nav-btn);border:1px solid var(--border);color:var(--text-dim);cursor:pointer;}
.tl2-chev:hover{background:var(--nav-btn-hover);color:var(--text);}
.tl2-chev svg{width:9px;height:6px;transition:transform .15s ease;}
.tl2-chev[aria-expanded="true"] svg{transform:rotate(180deg);}
.tl2-viewmenu{position:absolute;top:calc(100% + 5px);left:0;z-index:20;display:flex;flex-direction:column;
  min-width:104px;background:var(--menu-bg);border:1px solid var(--border);}
.tl2-viewmenu button{font-family:inherit;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;
  text-align:left;padding:7px 12px;background:none;border:none;color:var(--text-dim);cursor:pointer;}
.tl2-viewmenu button:hover{background:var(--wash-hover);color:var(--text);}
.tl2-viewmenu button.on{color:var(--brand);font-weight:800;}

/* --- tables ------------------------------------------------------------ */
.tl2-tblwrap{overflow-x:auto;}
table.tl2-ptable{width:100%;border-collapse:collapse;font-size:12.5px;}
table.tl2-ptable th{text-align:right;padding:7px 9px;color:var(--text-dim);font-size:9.5px;
  text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);white-space:nowrap;}
table.tl2-ptable th.l,table.tl2-ptable td.l{text-align:left;}
table.tl2-ptable td{text-align:right;padding:7px 9px;border-bottom:1px solid var(--hair);
  white-space:nowrap;font-variant-numeric:tabular-nums;}
table.tl2-ptable tr:last-child td{border-bottom:none;}

/* Damage Event Log intro line. The log itself is the rotation viewer with
   its filters off (renderEventTimeline in thronelogs-page.js /
   drawTimeline in upload-view.js), so it reuses .tl2-rot-* wholesale --
   this note is the only styling it needs of its own. */
.tl2-evt-note{font-size:11.5px;color:var(--text-dim);line-height:1.6;margin:0 0 10px;max-width:82ch;}

.tl2-skcell{position:relative;}
.tl2-skcell .bar{position:absolute;left:0;top:0;bottom:0;
  background:color-mix(in srgb,var(--dmg) 16%,transparent);z-index:0;}
.tl2-skcell .nm{position:relative;z-index:1;font-weight:700;color:var(--text);}
.tl2-skicon{position:relative;z-index:1;display:inline-block;width:17px;height:17px;background-size:cover;
  background-position:center;border:1px solid var(--border);vertical-align:-4px;margin-right:7px;}
/* Missing art. One format across the whole breakdown (see iconCell in
   thronelogs-page.js): the skill's initial in a muted square, never a
   hash-coloured tile -- a saturated block reads as an icon you don't
   recognise rather than as one that isn't there yet. */
.tl2-skicon.no-icon,.tl2-rot-tile.no-icon{display:inline-flex;align-items:center;justify-content:center;
  background:var(--wash-hover);color:var(--text-dim);font-weight:800;line-height:1;}
.tl2-skicon.no-icon{font-size:9.5px;}
.tl2-rot-tile.no-icon{display:flex;font-size:12px;}
.c-crit{color:#e0a53b;} .c-heavy{color:#4a9fe0;} .c-hcrit{color:#43c98a;} .c-dps{color:#e0a53b;}

/* --- rotation timeline -------------------------------------------------
   Every damage TICK gets its own icon tile (no grouping into
   "activations"). When two tiles would overlap in time the later one stacks
   into a new lane rather than covering the first (greedy interval packing,
   assignLanes() in thronelogs-page.js). Any skill firing more than ~1x/sec on
   average is pulled out into its own collapsible group below so it doesn't
   force the whole view into permanent stacking. */
.tl2-wip{font-size:11px;font-weight:400;color:var(--text-dim);text-transform:none;letter-spacing:0;}
.tl2-rot-debug{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--text-dim);margin:12px 0 10px;cursor:pointer;}
.tl2-rot-debug input{margin:0;accent-color:var(--brand);}
.tl2-rot-scroll{overflow-x:auto;padding-bottom:9px;}
.tl2-rot-ruler{position:relative;height:17px;border-bottom:1px solid var(--border);margin-bottom:4px;}
.tl2-rot-ruler span{position:absolute;top:0;font-size:9.5px;color:var(--text-dim);transform:translateX(-50%);}
.tl2-rot-lanes{position:relative;}
.tl2-rot-tile{position:absolute;width:26px;height:26px;background-size:cover;background-position:center;
  border:1px solid var(--border);cursor:default;}
.tl2-rot-tile:hover{outline:2px solid var(--accent);outline-offset:1px;z-index:5;}
/* Where a click on the DPS timeline landed in the event log
   (jumpEventLogTo in thronelogs-page.js). Scrolling alone drops you into a
   wall of tiles with no sign of which second you asked for, so the target
   second gets a line that flashes and then fades -- present long enough to
   find, gone before it becomes permanent chart furniture. */
.tl2-rot-marker{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;
  background:var(--accent);opacity:0;pointer-events:none;z-index:6;}
.tl2-rot-marker.is-on{animation:tl2-rot-marker-flash 2.4s ease-out forwards;}
@keyframes tl2-rot-marker-flash{
  0%{opacity:0;} 8%{opacity:1;} 55%{opacity:1;} 100%{opacity:0;}
}
@media (prefers-reduced-motion:reduce){
  /* Still shown, just without the pulse -- the marker is information, not
     decoration, so it can't simply be dropped here. */
  .tl2-rot-marker.is-on{animation:none;opacity:.9;}
}
.tl2-rot-empty{color:var(--text-dim);font-size:12px;margin:0;}

/* Dense-skill section, collapsible per the owner: it's explanatory overflow
   rather than the rotation itself, and its lanes are the tallest thing on
   the page. Collapsed by default -- <details>/<summary> so the open/closed
   state, keyboard operation and hit target all come from the browser
   instead of being rebuilt in JS. */
.tl2-rot-dense{margin-top:16px;padding-top:13px;border-top:1px dashed var(--border);}
.tl2-rot-dense > summary{list-style:none;cursor:pointer;display:flex;align-items:flex-start;gap:8px;
  color:var(--text-dim);font-size:11.5px;line-height:1.5;}
.tl2-rot-dense > summary::-webkit-details-marker{display:none;}
.tl2-rot-dense > summary::before{content:"";flex:none;width:0;height:0;margin-top:5px;
  border-left:5px solid currentColor;border-top:4px solid transparent;border-bottom:4px solid transparent;
  transition:transform .15s ease;transform-origin:2px 50%;}
.tl2-rot-dense[open] > summary::before{transform:rotate(90deg);}
.tl2-rot-dense > summary:hover{color:var(--text);}
.tl2-rot-dense-body{margin-top:12px;}
.tl2-rot-dense-group{margin-bottom:14px;}
.tl2-rot-dense-group:last-child{margin-bottom:0;}
.tl2-rot-dense-label{font-size:12px;font-weight:700;color:var(--text);margin:0 0 6px;display:flex;align-items:center;gap:8px;}
.tl2-rot-dense-label .count{font-weight:600;color:var(--text-dim);font-size:10.5px;}

@media (max-width:1000px){
  .tl2-cluster{grid-template-columns:1fr;}
  .tl2-gauges{grid-template-columns:1fr 1fr;}
}

/* Bottom-right corner links (Color Legend, Terms of Service) -- plain small
   red text, not buttons (owner's call on ToS: the bordered/filled button
   treatment was too prominent for what's just a legal-text link; Color
   Legend matches it for the same reason plus consistency between the two).
   The row itself is what's fixed to the viewport corner, not each link
   individually, so it stays put regardless of which view (board or parse)
   is showing and the two links' spacing is never hand-tuned. */
/* Three links now, not two -- font-size and gap trimmed so the row still
   clears the side panel on a narrow window instead of overlapping it. */
.tl2-corner-links{position:fixed;right:14px;bottom:12px;z-index:15;display:flex;gap:10px;}
.tl2-tos-btn{font-family:inherit;font-size:9px;font-weight:600;letter-spacing:.02em;
  padding:0;cursor:pointer;border:none;background:none;color:var(--brand);white-space:nowrap;}
.tl2-tos-btn:hover{text-decoration:underline;}

/* Rules popup content. Plain HUD-panel-family styling -- HUD+Red is the
   only theme this site ships right now, so there's nothing Glass-specific
   to maintain here in parallel. */
.tl2-rules{display:flex;flex-direction:column;gap:18px;}
.tl2-rules-group h4{margin:0 0 8px;font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--accent);}
.tl2-rules-group ul,.tl2-rules-group ol{margin:0;padding-left:18px;}
.tl2-rules-group li{font-size:12.5px;line-height:1.6;color:var(--text);}
.tl2-rules-group li::marker{color:var(--accent);}
.tl2-rules-group ol li{margin-bottom:4px;}
/* Standalone paragraph, outside any .tl2-rules-group -- the flagged-parse
   note applies to the whole list, not either section, and the Disclaimer
   popup is a single paragraph with no groups at all. */
.tl2-rules-note{margin:0;font-size:12.5px;line-height:1.6;color:var(--text);}
.tl2-rules-note code{font-family:var(--mono);font-size:11px;background:var(--wash-2);
  border:1px solid var(--border);border-radius:4px;padding:2px 6px;word-break:break-all;}

/* Rotation Timeline's Disclaimer button -- sits where the Active/Full
   toggle used to (same .tl2-sec-h slot), same .tl2-btn base as the rest of
   the header chrome, just smaller to match the toggle's footprint. */
.tl2-rot-disclaimer-btn{padding:4px 11px;font-size:10px;}


/* ===========================================================================
   UPLOAD VIEW
   Replaced a five-field <dialog>. Lives in the same .tl2-grid as the board and
   the parse breakdown and is shown/hidden by JS (see public/js/upload-view.js
   for why it must not be a separate page). Five tabs: Combat log (contact
   merged in), Breakdown, Specializations, Mastery, Terms.
   =========================================================================== */
.tl2-upload{display:flex;flex-direction:column;min-height:0;min-width:0;grid-column:1;}
.tlu-head{display:flex;align-items:center;gap:14px;padding:14px 18px 10px;flex:none;}
.tlu-head h2{margin:0;font-size:19px;font-weight:800;}
.tlu-head .tl2-btn{margin-left:auto;}
/* The account panel's back control. A bare link rather than a bordered button:
   it goes BACK, and giving it the same chrome as Save/Claim/Delete had it
   competing with the panel's real actions for the same signal. Keeps the
   button's typography and hit area, drops only the box. */
.tla-back{margin-left:auto;font-family:inherit;font-size:11px;letter-spacing:.09em;
  text-transform:uppercase;padding:8px 4px;background:none;border:0;cursor:pointer;
  color:var(--text-dim);}
.tla-back:hover{color:var(--text);}

/* Edit-build mode (openBuildEdit in thronelogs-page.js): the same shell as a
   normal upload but with no tab strip -- the Save/Cancel bar is a sibling
   AFTER .tlu-body rather than inside it (where .tlu-submit normally lives,
   on the terms tab), so it stays pinned below the scrolling builder instead
   of scrolling away with it. */
.tl2-upload > .tlu-submit{flex:none;padding:14px 20px 18px;border-top:1px solid var(--hair);margin-top:0;}

.tlu-tabs{display:flex;padding:0 12px;border-bottom:1px solid var(--hair);flex:none;}
.tlu-tab{display:flex;align-items:center;gap:8px;background:none;border:none;
  border-bottom:2px solid transparent;padding:12px 15px 10px;cursor:pointer;font-family:inherit;
  font-size:12px;color:var(--text-dim);white-space:nowrap;}
.tlu-tab:hover{background:var(--wash-hover);color:var(--text);}
.tlu-tab.on{color:var(--text);border-bottom-color:var(--accent);}
.tlu-tab.locked{opacity:.4;cursor:default;}
.tlu-tab.locked:hover{background:none;color:var(--text-dim);}
.tlu-tab svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round;flex:none;}
.tlu-body{flex:1;min-height:0;min-width:0;overflow-y:auto;padding:16px 20px 22px;}
/* Section headings inside the upload flow need real top space of their own.
   .tl2-sec-h only carries a BOTTOM margin -- on the permanent parse-breakdown
   page that's fine because each heading sits inside its own .tl2-sec, and the
   top gap comes from the PREVIOUS .tl2-sec's margin-bottom:20px. This tab body
   has no such wrapper (flat siblings, see tabLog()/tabBreakdown() in
   upload-view.js), so without an override "Region"/"Content"/"Contact" and
   "DPS Timeline"/"Skills"/"Rotation" sit flush against whatever's above them
   -- reported as everything looking too close together. Scoped to .tlu-body
   only, so the shared class's behavior on the permanent breakdown page (not
   part of this complaint) is untouched. */
.tlu-body h3.tl2-sec-h{margin-top:26px;}
/* ...but only for a heading that FOLLOWS something. The rule above exists to
   separate stacked sections; the first heading in a scroll body has nothing
   above it to be separated from, so the 26px is pure dead space at the top of
   the pane. It also outranks (0,2,1) the tidier rules further down that try to
   zero it -- which is how every Build pane ended up with a large gap above its
   title. Fixed here, at the source, rather than by escalating specificity in
   five other places. */
.tlu-body > h3.tl2-sec-h:first-child{margin-top:0;}
.tlu-err{color:#ff8080;font-size:12.5px;margin:0;padding:0 20px 14px;flex:none;}
.tlu-empty{font-size:12.5px;color:var(--text-dim);font-style:italic;margin:10px 0;}

/* --- 1. combat log ------------------------------------------------------ */
.tlu-drop{display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;
  border:1px dashed var(--border);background:var(--wash);padding:26px 22px;cursor:pointer;}
.tlu-drop b{font-size:14px;}
.tlu-drop span{font-size:11.5px;color:var(--text-dim);}
.tlu-drop.has-file{flex-direction:row;align-items:center;text-align:left;gap:14px;padding:14px 18px;
  border-style:solid;border-color:rgba(67,201,138,.5);background:rgba(67,201,138,.06);cursor:default;}
.tlu-drop-ico svg{width:30px;height:30px;stroke:#43c98a;fill:none;stroke-width:1.4;
  stroke-linecap:round;stroke-linejoin:round;}
.tlu-drop-main{flex:1;min-width:0;}
.tlu-drop-main b{display:block;font-size:13.5px;}
.tlu-drop-main span{display:block;margin-top:3px;}
.tlu-drop .tl2-btn{cursor:pointer;}
.tl2-btn.sm{padding:5px 11px;font-size:10px;}

.tlu-detect{display:flex;align-items:center;gap:12px;border:1px solid var(--border);
  background:var(--well);padding:9px 14px;margin-top:12px;}
/* Auto-detection failed (inferComboId() couldn't map enough of the log's
   skills to weapons -- happens on classes the skill-weapon seed data doesn't
   cover yet). Previously the <select> below just silently defaulted to its
   first OPTION (Crusader, alphabetically/positionally first in the combo
   list) with nothing marking it as unselected, which read as "detected:
   Crusader" -- reported live as skill selection "always defaulting to
   Crusader". This state needs to look like what it is: a required manual
   pick, not a low-confidence guess. */
.tlu-detect-fail{border-color:var(--accent);}
.tlu-detect-fail .tlu-w{color:var(--accent);}
.tlu-k{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--text-dim);flex:none;}
.tlu-v{font-size:14px;font-weight:800;}
.tlu-w{font-size:11.5px;color:var(--text-dim);margin-left:6px;}
.tlu-select{margin-left:auto;padding:6px 9px;background:var(--wash-2);
  border:1px solid var(--border);color:var(--text);font-family:inherit;font-size:12px;}

/* Region: four options, so a segmented row rather than a dropdown -- they fit
   one line and picking is a single click, not open-then-choose. */
.tlu-pick{display:flex;flex-wrap:wrap;}
.tlu-opt{font-family:inherit;font-size:12px;font-weight:600;padding:8px 18px;cursor:pointer;
  background:none;border:1px solid var(--border);color:var(--text-dim);}
.tlu-opt + .tlu-opt{border-left:none;}
.tlu-opt:hover{background:var(--wash-2);color:var(--text);}
.tlu-opt.on{background:color-mix(in srgb, var(--brand) 16%, transparent);border-color:var(--brand);color:#fff;}
.tlu-opt.on + .tlu-opt{border-left-color:var(--brand);}


.tlu-field{max-width:52ch;}
.tlu-field label{display:block;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text-dim);margin-bottom:6px;}
.tlu-field input[type=text]{display:block;width:100%;padding:9px 10px;background:var(--wash-2);
  border:1px solid var(--border);color:var(--text);font-size:13px;font-family:inherit;}
.tlu-field input[type=text]:focus{outline:none;border-color:var(--brand);}
.tlu-field input[type=text].err{border-color:var(--st-err-2);}
.tlu-field input[type=text].err ~ .tlu-note{color:var(--st-err-2);}
.tlu-note{font-size:11px;color:var(--text-dim);margin:6px 0 0;line-height:1.5;}

/* --- 2. breakdown ------------------------------------------------------- */
.tlu-gauges{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:6px;}
#tlu-chart{display:block;margin-bottom:6px;}
.tlu-guide{stroke:var(--guide);stroke-width:1;stroke-dasharray:3 3;}
.tlu-dot{fill:#fff;stroke:var(--dmg);stroke-width:2;}
.tlu-hit{cursor:crosshair;}
.tlu-rt-scroll{overflow-x:auto;padding-bottom:8px;}
.tlu-rt-inner{position:relative;}
.tlu-rt-ruler{position:relative;height:16px;border-bottom:1px solid var(--border);margin-bottom:6px;}
.tlu-rt-ruler span{position:absolute;top:0;font-size:9.5px;color:var(--text-dim);transform:translateX(-50%);}
.tlu-rt-lanes{position:relative;}
.tlu-rt{position:absolute;width:24px;height:24px;background-size:cover;background-position:center;
  border:1px solid var(--border);border-radius:4px;}
.tlu-rt:hover{outline:2px solid #fff;outline-offset:1px;z-index:5;}
.tlu-rt.no-icon{display:flex;align-items:center;justify-content:center;background:var(--wash-hover);
  font-size:11px;font-weight:800;color:var(--text-dim);}
.tlu-rt-dense{font-size:11px;color:var(--text-dim);line-height:1.9;margin:8px 0 0;}
.tlu-rt-dense span{border:1px solid var(--border);padding:2px 8px;margin:0 4px 0 0;white-space:nowrap;}
.tlu-rt-dense em{font-style:normal;font-family:var(--mono);font-size:10px;color:var(--text);margin-left:5px;}

/* --- 3. specializations -------------------------------------------------
   Laid out at a fixed natural size (1220x~580) and scaled as a whole by
   fitBuilder(), so the step never scrolls and keeps its proportions at any
   window size. The geometry is deliberately wider than any standard pane is
   proportionally tall, so WIDTH is what binds -- that's what makes the
   horizontal spacing identical on every screen instead of varying with
   aspect ratio. */
/* .tlu-fit is laid out at natural size and scaled by fitBuilder(); the
   wrapper is what carries the SCALED size. A transformed element still takes
   up its untransformed 1220px in layout, so without this the scrolling pane
   measured 1220 and grew a horizontal scrollbar under content that visually
   fit. The wrapper clips, so there is nothing to scroll to. */
.tlu-fitwrap{overflow:hidden;}
/* FLUID WIDTH, scaled only for height. This used to be a fixed 1220px laid out
   at natural size and scaled down as a whole -- the comment above still
   describes why width was made the binding constraint. The cost of that was
   permanent: a 1076px pane meant scale 0.888, so nothing ever reached the
   edges, every 54px icon was resampled down, and the locked aspect ratio left
   126px of dead space along the bottom that no amount of padding trimming
   could reach. Reported twice as "doesn't fill the window", correctly.
   Columns are now equal fractions of whatever width there is, so the builder
   meets both edges at 1:1 and fitBuilder only has to scale when the content is
   too TALL. minmax(0,1fr) rather than 1fr so the ellipsising skill names can
   actually shrink their track instead of forcing it wide. */
/* --tlk is the fit factor. It used to be a transform: scale(), which shrank
   BOTH axes -- so on a short 16:9 window, where height binds, the builder also
   got narrower and left a band of dead space down the right-hand side. Reported
   from a laptop, and the reason padding trims never touched it.
   Now it scales SIZES instead: rows get shorter and icons smaller, while the
   grid stays exactly as wide as the pane. */
.tlu-fit{width:100%;--tlk:1;}
.tlu-cols{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:20px;align-items:start;}
.tlu-colL{min-width:0;}
.tlu-colR{min-width:0;}
.tlu-hd{display:flex;align-items:baseline;gap:8px;margin-bottom:9px;}
.tlu-hp b{font-size:23px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--text);}
.tlu-hp i{font-style:normal;font-size:13px;color:var(--text-dim);margin-left:2px;}
.tlu-hp.over b{color:var(--accent);}
/* Local skill presets: a named list per class, not one slot -- see
   savePresetAs()/loadPresetById()/deletePresetById() in upload-view.js.
   Sits right under the point counter, above the pools.
   The load/delete row is a <select> + two buttons, NOT a list of one
   button per preset -- a per-preset list grows taller with every save,
   and this whole builder is laid out at natural size then scaled down to
   fit the pane (fitBuilder(), so the step never scrolls); a growing list
   grew the natural content height and broke that, reported live as the
   window scrolling after adding a preset. A <select> stays exactly one
   row tall no matter how many presets exist -- its OPEN dropdown is a
   native overlay outside document flow, so it can't affect that
   measurement either. */
.tlu-preset-bar{display:flex;align-items:center;gap:8px;margin-bottom:10px;flex-wrap:wrap;}
.tlu-preset-name{flex:1;min-width:120px;max-width:220px;padding:6px 9px;font-size:11px;
  font-family:inherit;background:var(--wash-2);border:1px solid var(--border);color:var(--text);}
.tlu-preset-name:focus{outline:none;border-color:var(--brand);}
.tlu-preset-msg{font-size:10.5px;color:var(--text-dim);}
.tlu-preset-select{flex:1;min-width:120px;max-width:220px;padding:6px 9px;font-size:11px;
  font-family:inherit;background:var(--wash-2);border:1px solid var(--border);color:var(--text);}
.tlu-preset-select:focus{outline:none;border-color:var(--brand);}
.tlu-ht{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-dim);}
h4.tlu-lbl{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--text-dim);
  margin:0 0 8px;padding-bottom:4px;border-bottom:1px solid var(--border);}
h4.tlu-lbl ~ h4.tlu-lbl{margin-top:10px;}
.tlu-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:10px;}

/* A slot is three things: icon, name, spec points. Nothing else -- that's what
   lets twelve of them fit without shrinking anything. An EMPTY slot is the
   same card with an empty icon well, not a dashed box with a number, which
   read as a different kind of object rather than one waiting to be filled. */
.tlu-slot{position:relative;display:flex;align-items:center;gap:calc(13px * var(--tlk,1));min-height:calc(78px * var(--tlk,1));
  border:1px solid var(--border);background:var(--wash);padding:6px 11px;}
.tlu-slot[draggable="true"]{cursor:grab;}
.tlu-si{width:calc(62px * var(--tlk,1));height:calc(62px * var(--tlk,1));flex:none;display:block;border-radius:5px;border:1px solid var(--ring);}
.tlu-si.well{background:var(--well-2);box-shadow:inset 0 0 0 1px var(--ring-soft);border-color:transparent;}
.tlu-sb{min-width:0;flex:1;display:flex;flex-direction:column;gap:7px;}
.tlu-snm{font-size:13px;font-weight:700;line-height:1.2;color:var(--text);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tlu-pts{display:flex;gap:7px;}
.tlu-slot.over,.tlu-pslot.over,.tlu-pool.over,.tlu-ppool.over{
  border-color:var(--accent);background:color-mix(in srgb, var(--accent) 12%, transparent);}

/* THE SPEC POINT. The source icons are square with a light background, so
   unclipped they read as white boxes -- hence the circle. Unchosen is a hard
   brightness cut so it reads as switched off rather than as a pale blob.
   Selected rings WHITE, not the brand red, which was competing with the
   site's chrome accent for the same signal. No cost badge and no label: a
   spec point is explained only by its tooltip. */
.tlu-pt{position:relative;width:calc(30px * var(--tlk,1));height:calc(30px * var(--tlk,1));flex:none;padding:0;cursor:pointer;border:none;
  border-radius:50%;overflow:hidden;background:var(--icon-well);box-shadow:0 0 0 1px var(--ring);
  transition:box-shadow .12s ease;}
.tlu-pt img{width:100%;height:100%;display:block;object-fit:cover;
  filter:var(--spec-off);transition:filter .12s ease;}
.tlu-pt:hover img{filter:var(--spec-hover);}
.tlu-pt.on img{filter:none;}
.tlu-pt.on{box-shadow:0 0 0 2px var(--ring-sel);}

/* Passives carry no spec points, so a slot is an icon and nothing else. */
.tlu-pgrid{display:grid;grid-template-columns:repeat(8,1fr);gap:calc(6px * var(--tlk,1));}
.tlu-pslot{position:relative;display:flex;align-items:center;justify-content:center;
  border-radius:6px;box-shadow:0 0 0 1px var(--ring);padding:4px;}
.tlu-pslot[draggable="true"]{cursor:grab;}
.tlu-pslot img{width:100%;height:auto;aspect-ratio:1;display:block;border-radius:5px;}
.tlu-pslot.empty{box-shadow:none;border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;}
.tlu-pwell{width:100%;height:100%;border-radius:5px;background:var(--well-2);
  box-shadow:inset 0 0 0 1px var(--ring-soft);}

/* The basins. Fractional columns, not fixed widths: at a fixed icon width the
   row's remainder all collected on the right and the box looked lopsided. */
.tlu-pool,.tlu-ppool{display:grid;grid-template-columns:repeat(var(--tlcols,8),1fr);gap:calc(7px * var(--tlk,1));
  border:1px solid var(--border);background:var(--wash);padding:10px;}
.tlu-ps{padding:0;border:none;background:none;cursor:grab;border-radius:5px;display:block;width:100%;
  font-family:inherit;box-shadow:0 0 0 1px var(--ring);}
.tlu-ps img{width:100%;height:auto;aspect-ratio:1;display:block;border-radius:5px;}
.tlu-ps:hover{box-shadow:0 0 0 2px var(--accent);}

/* Drag ghost. On <body>, outside the scaled subtree, so it's the same size at
   any zoom -- and rounds correctly, which a snapshot of the <img> does not. */
.tlu-ghost{position:fixed;top:-200px;left:-200px;width:48px;height:48px;background-size:cover;
  background-position:center;border-radius:5px;box-shadow:0 0 0 1px var(--ring);
  pointer-events:none;z-index:-1;}

/* =========================================================================
   PARSE BUILD -- read-only display of the equipped skills/specs/passives a
   parse was uploaded with, on the permanent breakdown page. Not an editor:
   no drag, no click-to-toggle, just what was chosen.

   Layout descends from mockups/parse-build-v1 candidate 03 "By weapon"
   (chosen by the owner over four other candidates in that round; the
   first-shipped version before that round was a plain auto-fill grid of
   the upload flow's own .tlu-slot cards). Own tlp-b*-prefixed classes
   rather than reusing .tlu-* -- those are sized for the upload flow's
   fixed 600px-square panel meant to be scaled to fit one screen; this page
   scrolls normally at whatever width the browser gives it, and needed its
   own sizing (see tlp-bwrow below) to boot.

   Splitting equipped actives into one column PER WEAPON (candidate 03's
   original design) turned out to be a real bug, not just a styling choice:
   a real build is rarely divided evenly across its two weapons (reported
   live: 7 actives on one, 4 on the other), so the heavier weapon's column
   just ran taller than the other with real page background showing through
   underneath the shorter one. Two fixes were tried and didn't hold before
   the owner pointed out the actual answer -- worth not re-attempting either
   blind: bumping row size to fill that gap only worked for the one test
   build that happened to split evenly across weapons; align-items:start
   (below) is correct CSS but changed nothing VISIBLE, since these columns
   have no background/border for a "stretched box" to be seen on -- there
   was never a box, just less real content on one side.

   The actual fix drops weapon grouping entirely: which weapon a skill
   belongs to isn't load-bearing information here, so every equipped active
   is ONE undifferentiated list (order as the uploader built them), split by
   raw count into two balanced side-by-side sub-columns once there are
   enough of them (see the equipped/activesHtml logic in
   renderParseBuild()). Splitting one flat list by count, rather than
   splitting two per-weapon lists independently, is what makes the height
   converge regardless of how a real build happens to divide between its
   weapons -- not a height-matching trick, just genuinely less imbalanced
   input to begin with. */
.tlp-bweapons{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px;align-items:start;}
.tlp-bwcol h4{font-size:9.5px;letter-spacing:.13em;text-transform:uppercase;color:var(--text-dim);
  margin:0 0 8px;padding-bottom:5px;border-bottom:1px solid var(--border);}

/* The actives column once it's split into two sub-columns (more than 4
   equipped): each needs about half the horizontal room a single-column row
   gets, so tlp-bwcol-split scales the row content down to fit rather than
   letting a fixed size overflow. */
/* align-items:start here too -- the two sub-columns split an arbitrary
   count in half (6 rows / 5 rows), and default grid stretch was making the
   shorter one match the taller one's height with the same kind of dead
   space as the original per-weapon bug, just smaller (measured 49px before
   this). Same fix, same reason: let each side be exactly as tall as its
   own content. */
.tlp-bwsplit{display:grid;grid-template-columns:1fr 1fr;gap:0 16px;align-items:start;}
.tlp-bwcol-split .tlp-bsi{width:32px;height:32px;}
.tlp-bwcol-split .tlp-bnm{width:auto;font-size:12.5px;}
.tlp-bwcol-split .tlp-bpt{width:26px;height:26px;}
.tlp-bwcol-split .tlp-bwrow{gap:9px;padding:8px 2px;}

/* Skill/passive icon. Rounded corner, not a circle -- circles are reserved
   for spec points so the two kinds of thing stay visually distinct. */
.tlp-bsi{display:block;width:40px;height:40px;border-radius:6px;
  border:1px solid var(--ring);flex:none;}

/* Sized up from the mockup round's first pass on the owner's request. (Not
   for height-matching -- see the align-items:start comment above for why
   that was never the right mechanism; this sizing is just a real
   improvement on its own.) */
.tlp-bwrow{display:flex;align-items:center;gap:13px;padding:11px 2px;}
.tlp-bwrow + .tlp-bwrow{border-top:1px solid var(--hair);}
.tlp-bnm{font-size:14px;font-weight:700;color:var(--text);width:170px;flex:none;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.tlp-bpts{display:flex;flex-wrap:wrap;gap:6px;}

/* THE SPEC POINT. Source icons are square with a light background --
   unclipped they read as white boxes, hence the circle. Unchosen is a hard
   brightness/grayscale cut so it reads as switched off rather than a pale
   blob; chosen removes that cut so the icon's real color reads through,
   which is the actual signal -- the ring is not doing that job on its own,
   so it matches the same quiet border every skill/passive icon (.tlp-bsi)
   already uses, rather than a bold white ring competing for attention. No
   cost badge, no label -- a spec point's meaning is explained ONLY by its
   hover tooltip. */
.tlp-bpt{position:relative;width:32px;height:32px;flex:none;padding:0;cursor:pointer;
  border:none;border-radius:50%;overflow:hidden;background:var(--icon-well);
  box-shadow:0 0 0 1px var(--ring);transition:box-shadow .12s ease;}
.tlp-bpt img{width:100%;height:100%;display:block;object-fit:cover;
  filter:var(--spec-off);transition:filter .12s ease;}
.tlp-bpt:hover img{filter:var(--spec-hover);}
.tlp-bpt.on img{filter:none;}
.tlp-bpt.on{box-shadow:0 0 0 1px var(--ring-sel);}

/* Passives: a third column beside the actives.
   ...
   Two earlier fixes (min-height, then a plain forced `height` on a FIXED
   2-column grid) both left the icons visually stretched taller than wide --
   object-fit:cover on a non-square cell doesn't crop cleanly, it zooms in
   and crops, which looks like a smeared sliver of the icon on a cell far
   taller than wide. Confirmed on three separate real machines, not a
   caching or one-system issue. A fixed column count simply cannot fill an
   arbitrary height without cells stopping being square.

   The real fix (syncPassiveColHeight/layoutPassiveIcons in
   thronelogs-page.js) picks the column count DYNAMICALLY so the icons can
   stay square while the grid's total height still reaches the actives
   column's real height -- gridTemplateColumns/gridAutoRows/width below are
   all set inline by that function; the values here are just a sane default
   for the instant before JS first runs. */
.tlp-bpcol{width:118px;display:flex;flex-direction:column;}
.tlp-bpcol-grid{display:grid;grid-template-columns:repeat(2,1fr);grid-auto-rows:54px;gap:10px;}
.tlp-bpcol-icon{width:100%;height:100%;object-fit:cover;}

@media (max-width:1000px){
  .tlp-bweapons{grid-template-columns:1fr;}
  /* !important: syncPassiveColHeight() sets height/width inline (see the
     comment above), which otherwise outranks anything here since it's an
     inline style -- stacked mobile layout has no actives column beside it
     to match, so those need to be cancelled, not just overridden. */
  .tlp-bpcol{width:auto !important;height:auto !important;}
  .tlp-bpcol-grid{grid-template-columns:repeat(4,1fr) !important;grid-auto-rows:auto !important;max-width:260px;flex:none;}
  .tlp-bpcol-icon{height:auto;aspect-ratio:1;}
}

/* --- 4/5. mastery + terms ----------------------------------------------- */
.tlu-sec-note{display:flex;align-items:center;gap:9px;margin-bottom:6px;}
.tlu-sec-note b{font-size:16px;}
.tlu-sec-note span{font-size:9px;letter-spacing:.09em;text-transform:uppercase;color:var(--text-dim);
  border:1px solid var(--border);padding:3px 8px;}
/* Terms tab enlarged on the owner's call -- the panel is the same size as
   the Specializations step (which fills it edge to edge), and five short
   bullets at the old sizing read as a tiny block of text floating in a big
   empty panel. Bigger type, more line-height, a wider column and a roomier
   accept box use more of that space rather than fighting it. */
.tlu-accept{display:flex;align-items:flex-start;gap:12px;font-size:14px;line-height:1.65;cursor:pointer;
  border:1px solid var(--border);background:var(--wash);padding:18px 20px;max-width:86ch;}
.tlu-accept input{flex:none;margin-top:2px;accent-color:var(--brand);width:19px;height:19px;}
.tlu-accept.checked{border-color:rgba(67,201,138,.5);background:rgba(67,201,138,.06);}
.tlu-submit{display:flex;align-items:center;gap:14px;margin-top:20px;}
.tlu-submit .tl2-btn{font-size:13.5px;padding:13px 26px;}

/* Tooltip. Fixed to the viewport, not nested in a scrolling pane that would
   clip it. The only place a spec point is ever explained. */
/* Glass default (frosted, rounded); HUD squares it off below, same as every
   other window on the page. */
.tlu-tip{position:fixed;z-index:200;max-width:290px;pointer-events:none;
  background:var(--tip-bg);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border:1px solid var(--tip-border);border-radius:10px;padding:9px 11px;
  box-shadow:0 16px 40px -16px rgba(0,0,0,.9);}
body[data-skin="hud"] .tlu-tip{background:#1b1922;border-radius:0;
  backdrop-filter:none;-webkit-backdrop-filter:none;}
.tlu-tip b{display:block;font-size:12.5px;font-weight:700;color:var(--text);line-height:1.3;}
.tlu-tip .tlu-tip-m{display:block;font-family:var(--mono);font-size:10px;color:var(--accent);margin-top:3px;}
.tlu-tip .tlu-tip-d{display:block;font-size:11px;line-height:1.5;color:var(--text-dim);margin-top:7px;
  padding-top:7px;border-top:1px solid var(--tip-hair);}

/* --- account view -------------------------------------------------------- */
/* Sign in / register / account / email confirmation, rendered by
   account-view.js into #tl-account-view. Reuses the upload view's chrome
   (.tlu-head, .tlu-body, .tlu-submit, .tlu-note) rather than restating it --
   these are the same kind of panel doing the same kind of job, and a second
   set of near-identical form styles would drift.
   Built against hud-red first, like every new element here; the other skins
   get it in the next batched skin pass. */
/* grid-column matches .tl2-parse/.tl2-upload: one column, the widgets keep
   so the panel has to span its column too or it renders squeezed into the
   198px rail track. */
.tl2-account{display:flex;flex-direction:column;min-height:0;min-width:0;grid-column:1;}
.tla-sub{margin:0;padding:0 18px 12px;font-size:12.5px;color:var(--text-dim);}
.tla-lede{font-size:12.5px;color:var(--text-dim);line-height:1.6;margin:0 0 18px;max-width:62ch;}

.tla-form{max-width:52ch;}
.tla-field{margin-bottom:16px;}
.tla-field label{display:block;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;
  color:var(--text-dim);margin-bottom:6px;}
/* Every input type the account forms use, not just [type=text] like
   .tlu-field -- password and email fields are the majority here. */
.tla-field input,.tla-field textarea,.tla-field select{display:block;width:100%;padding:9px 10px;
  background:var(--wash-2);border:1px solid var(--border);border-radius:var(--r);
  color:var(--text);font-size:13px;font-family:inherit;}
.tla-field textarea{resize:vertical;line-height:1.5;}
/* A bare <select> keeps the OS widget's own colours, which read as a dark
   dropdown dropped onto a light skin. Setting appearance:none hands the
   painting back to us, so it follows the theme like every other control. */
.tla-field select{appearance:none;-webkit-appearance:none;cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),
                   linear-gradient(135deg,currentColor 50%,transparent 50%);
  background-position:calc(100% - 16px) calc(50% + 1px),calc(100% - 11px) calc(50% + 1px);
  background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:32px;}
.tla-field input:focus,.tla-field textarea:focus,.tla-field select:focus{outline:none;border-color:var(--brand);}
.tla-field .tlu-note{margin-top:6px;}

.tla-err{color:var(--st-err);font-size:12.5px;margin:14px 0 0;}
.tla-ok{color:var(--st-ok);font-size:12.5px;margin:14px 0 0;}
.tla-alt{font-size:12px;color:var(--text-dim);}
.tla-alt a{color:var(--brand);text-decoration:none;}
.tla-alt a:hover{text-decoration:underline;}

.tla-ident{border:1px solid var(--border);border-radius:var(--r);background:var(--wash-2);
  padding:14px 16px;margin-bottom:8px;}
.tla-ident-name{font-size:17px;font-weight:800;}
.tla-ident-meta{font-size:11.5px;color:var(--text-dim);margin-top:3px;text-transform:uppercase;
  letter-spacing:.06em;}
.tla-unverified{color:var(--st-warn);margin-left:6px;text-transform:none;letter-spacing:0;}
.tla-verify-line{font-size:12px;color:var(--text-dim);margin-top:10px;display:flex;
  align-items:center;gap:8px;flex-wrap:wrap;}
.tla-resend-msg{font-size:11.5px;color:var(--text-dim);}
.tla-ident-actions{margin-top:12px;}

.tla-count{font-size:11px;color:var(--text-dim);font-weight:400;letter-spacing:0;
  text-transform:none;margin-left:6px;}
.tla-chars{margin-bottom:18px;}
.tla-char{padding:10px 0;border-bottom:1px solid var(--border);}
.tla-char-top{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;}
.tla-char-top strong{font-size:14px;}
.tla-char-sub{font-size:12px;color:var(--text-dim);margin-top:4px;}
.tla-char-actions{margin-top:8px;display:flex;gap:14px;}
.tla-claim{margin-top:4px;}


/* --- admin view ---------------------------------------------------------- */
/* Antitribu's ThroneLogs tools (admin-view.js). Shares .tl2-account's panel
   shape and the upload view's tabs/body chrome; only the row and art-grid
   styles below are its own. hud-red first, like everything new here. */
.tla-tabcount{display:inline-block;min-width:16px;padding:0 4px;margin-left:5px;border-radius:8px;
  background:var(--brand);color:#fff;font-size:9.5px;line-height:15px;text-align:center;font-weight:700;}
.tla-dim{color:var(--text-dim);font-size:11.5px;}
.tla-spacer{flex:1;}
.tla-current{color:var(--st-ok);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;}
.tla-banned{color:var(--st-err-2);font-size:10.5px;letter-spacing:.08em;margin-left:8px;}

.tla-row{padding:11px 0;border-bottom:1px solid var(--border);}
.tla-row-top{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;font-size:13.5px;}
.tla-row-sub{margin-top:3px;}
.tla-row-actions{margin-top:9px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:12px;}
.tla-row-actions a{color:var(--brand);text-decoration:none;}
.tla-row-actions a:hover{text-decoration:underline;}
.tla-lookup{margin-bottom:6px;}
.tla-art{display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);}
.tla-art-img{width:40px;height:40px;flex:none;border-radius:6px;background-size:cover;
  background-position:center;background-color:var(--wash-2);border:1px solid var(--border);}
.tla-art-label{flex:1;font-size:13px;line-height:1.45;}
.tla-art label.tl2-btn{cursor:pointer;}

/* Admin-only account profile stats (admin-view.js, Accounts tab). Wraps rather
   than scrolling: six short figures should reflow on a narrow panel, not hide
   behind an overflow. */
.tla-stats{display:flex;flex-wrap:wrap;gap:18px;margin:14px 0 10px;padding:12px 0;
  border-top:1px solid var(--border);border-bottom:1px solid var(--border);}
.tla-stat{display:flex;flex-direction:column;gap:2px;min-width:64px;}
.tla-stat-n{font-size:17px;font-weight:800;line-height:1.1;}
.tla-stat-l{font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--text-dim);}

/* Uploader's notes on the parse breakdown. pre-wrap so their line breaks
   survive; the text itself is set via textContent, never innerHTML. */
.tl2-pnotes{margin:8px 0 0;max-width:78ch;font-size:12.5px;line-height:1.6;color:var(--text-dim);
  white-space:pre-wrap;word-break:break-word;}
/* The notes textarea in the upload form, matching .tlu-field's inputs. */
.tlu-field textarea{display:block;width:100%;padding:9px 10px;background:var(--wash-2);
  border:1px solid var(--border);color:var(--text);font-size:13px;font-family:inherit;
  resize:vertical;line-height:1.5;}
.tlu-field textarea:focus{outline:none;border-color:var(--brand);}

/* --- public player profile (Hero Score) ---------------------------------- */
/* Only this panel's body scrolls; the page itself never does, same as every
   other in-app view. hud-red first, per the usual rule. */
.tl2-profile{display:flex;flex-direction:column;min-height:0;min-width:0;grid-column:1;}
/* The hero no longer carries the gradient -- it belongs to the best-parse
   block, which sits flush in the panel's top-right corner rather than inside
   the hero's padding. position:relative here is what that block anchors to. */
/* overflow:hidden matters: the best-parse gradient is 230px tall and would
   otherwise bleed past the hero's bottom border and sit behind the tab row,
   which read as the tabs clipping into it. Clipped here, the wash stops
   exactly where the hero does. */
/* No wash any more -- the identity block carries the header on its own, scaled
   up to fill the height the gradient used to occupy. Both sides sit on the
   same baseline grid, so nothing needs absolute positioning to stay put. */
.tlp2-hero{display:flex;gap:22px;align-items:center;
  padding:26px 24px 26px;border-bottom:1px solid var(--hair);flex:none;}
.tlp2-port{width:92px;height:92px;flex:none;display:grid;place-items:center;font-size:34px;font-weight:800;
  color:var(--brand);background:var(--wash-2);border:1px solid var(--border);}
.tlp2-name{font-size:32px;font-weight:800;letter-spacing:.005em;line-height:1.08;}
.tlp2-where{font-size:14px;color:var(--text-dim);margin-top:5px;}
.tlp2-where b{color:var(--text);font-weight:600;}
.tlp2-class{font-size:14px;color:var(--brand);font-weight:600;margin-top:4px;}
.tlp2-rank{font-size:13px;color:var(--text-dim);font-style:italic;margin-top:3px;}
/* Pushed to the right edge by the identity block growing, not by being taken
   out of flow -- so it stays vertically centred with the name beside it. */
.tlp2-best{margin-left:auto;text-align:right;}
.tlp2-best b{display:block;font-size:56px;font-weight:800;color:var(--text);line-height:.94;letter-spacing:-.02em;}
.tlp2-best b.tlp2-none{color:var(--text-dim);}
.tlp2-best span{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--text-dim);}
.tlp2-best em{display:block;font-style:normal;font-size:12px;color:var(--text-dim);margin-top:6px;}
/* Account settings, NOT site settings -- the topbar gear keeps the theme
   controls and is untouched. Owner only. */
/* Board sits where Account settings used to, at the right end of the tab row. */
.tlp2-board{margin-left:auto;align-self:center;margin-right:12px;}
/* Breathing room under the hero so the tabs aren't crowded against the wash
   that ends just above them. */
.tlp2-tabs{flex:none;}
.tlp2-card{margin-bottom:14px;}
.tlp2-card .tl2-sec-h{display:flex;align-items:center;gap:9px;}
/* Carries .tl2-link for its look (red text, no badge); all this adds is
   pushing it to the far end of the section heading it sits in. */
.tlp2-pencil{margin-left:auto;}
.tlp2-text{font-size:13px;line-height:1.65;max-width:74ch;white-space:pre-wrap;word-break:break-word;}
.tlp2-text.is-empty{color:var(--text-dim);font-style:italic;}
.tlp2-edit{display:block;width:100%;max-width:74ch;padding:9px 10px;background:var(--wash-2);
  border:1px solid var(--border);border-radius:var(--r);color:var(--text);font:inherit;
  font-size:13px;line-height:1.6;resize:vertical;}
.tlp2-edit:focus{outline:none;border-color:var(--brand);}
.tlp2-editbar{display:flex;gap:8px;margin-top:9px;}
.tlp2-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px;}
.tlp2-stat{border:1px solid var(--border);background:var(--wash);padding:11px 13px;}
.tlp2-stat b{display:block;font-size:19px;font-weight:800;}
.tlp2-stat span{font-size:9px;letter-spacing:.14em;text-transform:uppercase;color:var(--text-dim);}
.tlp2-char{display:grid;grid-template-columns:1fr 150px 130px auto;gap:12px;align-items:center;
  padding:9px 0;border-bottom:1px solid var(--hair);font-size:13px;}
.tlp2-char:last-child{border-bottom:0;}
.tlp2-dim{color:var(--text-dim);font-size:11.5px;}
.tlp2-mainflag{font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--brand);
  border:1px solid var(--brand);padding:2px 7px;}

/* Player names that resolve to a profile. Underline on hover only -- the board
   is dense and a permanent underline on every claimed name would fight the
   bars for attention. */
.tl2-plink{color:inherit;text-decoration:none;border-bottom:1px dotted color-mix(in srgb,currentColor 45%,transparent);}
.tl2-plink:hover{color:var(--brand);border-bottom-color:var(--brand);}

/* Theme groups in the settings drawer. Headers only -- no dropdowns; the four
   skins all stay visible and one click switches. */
.tl2-skin-cat{font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--text-dim);
  margin:0 0 6px;}
.tl2-skin-cat + .tl2-bgtoggle{margin-bottom:14px;}
/* The settings groups sat flush against each other -- ThroneLogs Admin was
   touching the button above it. */
.tl2-settings-group + .tl2-settings-group{margin-top:18px;}
.tl2-settings-account{flex-wrap:wrap;gap:8px;margin-bottom:8px;}

/* Profile image: the frame IS the control for the owner -- clicking it opens
   the picker, so there's no separate button competing with it. */
.tlp2-port{background-size:cover;background-position:center;overflow:hidden;position:relative;}
.tlp2-port.is-owner{cursor:pointer;}
.tlp2-portedit{position:absolute;inset:auto 0 0 0;font-style:normal;font-size:8.5px;letter-spacing:.12em;
  text-transform:uppercase;text-align:center;padding:3px 0;background:rgba(0,0,0,.72);color:var(--text);
  opacity:0;transition:opacity .12s;}
.tlp2-port.is-owner:hover .tlp2-portedit{opacity:1;}

/* Typed links. Kind and value are separate so a visitor can see where a link
   goes before following it. */
.tlp2-links{display:flex;flex-wrap:wrap;gap:8px;}
.tlp2-link{display:inline-flex;align-items:baseline;gap:7px;border:1px solid var(--border);
  background:var(--wash);padding:6px 11px;font-size:12.5px;}
.tlp2-link b{font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--brand);}
.tlp2-linkrow{display:grid;grid-template-columns:130px 1fr auto;gap:8px;margin-bottom:8px;max-width:74ch;}
.tlp2-linkrow select,.tlp2-linkrow input{padding:7px 9px;background:var(--wash-2);
  border:1px solid var(--border);color:var(--text);font:inherit;font-size:12.5px;border-radius:var(--r);}
.tlp2-linkrow select:focus,.tlp2-linkrow input:focus{outline:none;border-color:var(--brand);}
.tlp2-add{font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:6px 12px;cursor:pointer;
  font-family:inherit;background:none;border:1px dashed var(--border);color:var(--text-dim);border-radius:var(--r);}
.tlp2-add:hover{color:var(--brand);border-color:var(--brand);}

/* Account settings: side tabs. A vertical list reads as settings rather than
   a wizard, and leaves each pane the full width. Only the pane scrolls. */
.tla-settings{flex:1;min-height:0;display:grid;grid-template-columns:190px minmax(0,1fr);}
.tla-vtabs{border-right:1px solid var(--hair);padding:14px 0 12px;overflow-y:auto;min-height:0;}
.tla-vtabs .tla-ident{margin:0 12px 14px;padding:11px 12px;}
.tla-vtabs .tla-ident-name{font-size:14px;}
.tla-vtab{display:block;width:100%;text-align:left;font-family:inherit;font-size:12.5px;
  padding:10px 16px;background:none;border:0;border-left:2px solid transparent;
  color:var(--text-dim);cursor:pointer;}
.tla-vtab:hover{color:var(--text);background:var(--wash);}
.tla-vtab.on{color:var(--text);border-left-color:var(--brand);background:var(--wash);}
.tla-pane{display:flex;flex-direction:column;min-height:0;min-width:0;}

/* Account settings mounted inside the profile panel. It brings its own side
   tabs and its own scrolling pane, so this is just the slot they sit in. */
.tlp2-account{flex:1;min-height:0;display:flex;flex-direction:column;}

/* The claim form's selects, matching its text inputs. */
.tla-field select{display:block;width:100%;padding:9px 10px;background:var(--wash-2);
  border:1px solid var(--border);border-radius:var(--r);color:var(--text);
  font:inherit;font-size:13px;cursor:pointer;}
.tla-field select:focus{outline:none;border-color:var(--brand);}

/* Inline "Why?" on a rule that needs one. Plain red text -- it's an aside, not
   a control, so it gets no border or badge. The tooltip is ours rather than a
   native title=, which can't be themed and reads as an OS artifact. */
/* NOT the positioning context -- the rule's <li> is, see below. Anchored to
   this inline word instead, a 280px tooltip hanging off a "Why?" that sits
   two-thirds across a 400px dialog overhung the right edge by 16px, and an
   absolutely positioned box still counts toward scrollable overflow. That
   gave the whole Rules popup a horizontal scrollbar. */
.tl2-why{margin-left:6px;color:var(--brand);cursor:help;
  border-bottom:1px dotted color-mix(in srgb,var(--brand) 55%,transparent);}
.tl2-rules-group li{position:relative;}
.tl2-why:focus{outline:none;}
/* Spans its rule rather than its word, so it can never be wider than the
   dialog that holds it however far along the line the "Why?" falls. */
.tl2-why-tip{position:absolute;left:0;right:0;bottom:calc(100% + 8px);z-index:5;
  width:auto;max-width:min(280px,100%);padding:8px 11px;
  /* --tip-bg, not --panel-2: the panel tokens are translucent overlays meant
     to sit ON a surface, so using one here let the rules text read straight
     through the tooltip. --tip-bg is the token for exactly this, and the blur
     backs it up on the skins where it's still slightly transparent. */
  background:var(--tip-bg);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border:1px solid var(--brand);color:var(--text);
  font-size:11.5px;line-height:1.5;letter-spacing:0;text-transform:none;
  box-shadow:0 8px 22px rgba(0,0,0,.45);
  opacity:0;visibility:hidden;transition:opacity .12s;pointer-events:none;}
.tl2-why-tip::after{content:"";position:absolute;top:100%;left:14px;
  border:6px solid transparent;border-top-color:var(--brand);}
.tl2-why:hover .tl2-why-tip,.tl2-why:focus .tl2-why-tip{opacity:1;visibility:visible;}

/* The Builds pane hosts the specialization builder, which brings its own
   .tlu-body and scrolling; this is only the slot it mounts into. */
.tla-builds{flex:1;min-height:0;display:flex;flex-direction:column;}

/* Inline text controls: Edit, Rename, Transfer, Release. Same treatment as the
   inline "Why?" above -- red text, no border and no badge. These sit inside
   content (a bio block, a character row), and a bordered button there reads as
   part of the record rather than as an aside acting on it. */
.tl2-link{background:none;border:0;padding:0;font-family:inherit;font-size:11px;
  letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--brand);
  cursor:pointer;border-bottom:1px dotted color-mix(in srgb,var(--brand) 55%,transparent);}
.tl2-link:hover{border-bottom-style:solid;}
.tl2-link:focus{outline:none;border-bottom-style:solid;}

/* Email and password share one Security pane; this is the air between them. */
.tla-sec-gap{margin-top:26px;}

/* --- scrollbars ----------------------------------------------------------
   Every scrolling surface in the app: panel bodies, the settings pane, the
   rotation strip, the event log. The browser default is a grey OS widget that
   sits on a dark HUD panel looking like a seam, and it ignores the skin.
   scrollbar-color is INHERITED, so setting it once at the root covers every
   scroller without a universal selector.

   Standard properties only, deliberately. ::-webkit-scrollbar could give a
   fancier bar, but Chromium ignores those rules entirely as soon as
   scrollbar-width is set, so keeping both would mean Chromium showing thin
   overlay bars while older WebKit showed permanent 10px gutters -- the same
   page laid out two different ways. A browser that doesn't know these
   properties falls back to its own scrollbar, which is a fine thing to be.

   The thumb is mixed up from --text-dim rather than --border: at --border's
   12% alpha the bar was invisible against the panel it sits on.

   Declared on BODY, not :root -- the skins redefine their tokens on
   body[data-skin], so a rule on html would resolve --text-dim to the base
   palette and every skin would share one scrollbar colour. */
body{
  scrollbar-color:color-mix(in srgb,var(--text-dim) 65%,transparent) transparent;
  scrollbar-width:thin;
}

/* First run: what a character claim is FOR, said once, on the pane where you
   would do it. Sits in the flow above the form rather than over it -- a modal
   here would cover the very form it is describing. */
.tla-intro{position:relative;margin:0 0 16px;padding:13px 15px;
  background:var(--tip-bg);border:1px solid var(--brand);border-radius:var(--r);}
.tla-intro h4{margin:0 0 6px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--brand);}
.tla-intro p{margin:0;font-size:12.5px;line-height:1.6;color:var(--text);max-width:62ch;}
.tla-intro-x{position:absolute;top:8px;right:10px;background:none;border:0;padding:0;
  font-family:inherit;font-size:15px;line-height:1;color:var(--text-dim);cursor:pointer;}
.tla-intro-x:hover{color:var(--brand);}

/* A note that stands between two fields (the invisible-Unicode warning on the
   claim form) needs air under it, or it reads as the caption of the field
   BELOW rather than of the one it belongs to. */
.tla-form .tlu-note + .tla-field{margin-top:14px;}

/* --- weapon mastery (mastery-view.js) ------------------------------------
   Ring Rows: three ring strips and an epic strip, four nodes across each --
   one per category, which is the data's own shape once the flat stat nodes
   are dropped. Chosen over four other layouts (mockups/mastery.html) because
   it is the only one with room for what each passive DOES on every node, and
   these are passives nobody has memorised. hud-red first. */
.tlu-mastery{min-height:0}
.tlm-head{display:flex;align-items:center;gap:14px;margin-bottom:11px;flex-wrap:wrap}
.tlm-weapons{flex:0 1 340px}
.tlm-err{font-size:12px;color:var(--brand);margin:0 0 12px}

.tlm-ring{margin-bottom:16px}
.tlm-rh{font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--text-dim);
  padding-bottom:7px;border-bottom:1px solid var(--hair);margin:0 0 10px;font-weight:600}
.tlm-ring.is-epic .tlm-rh{color:var(--brand);
  border-bottom-color:color-mix(in srgb,var(--brand) 40%,transparent)}
.tlm-cnt{float:right;letter-spacing:0;text-transform:none;font-size:11px;font-weight:400}
.tlm-cnt.full{color:var(--brand)}
/* Four across, never fewer: the four columns ARE the four categories, so
   reflowing to two would put Attack above Tactic and break the one reading
   the layout exists for. Narrow panels shrink the cards instead. */
.tlm-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;align-items:stretch}

.tlm-n{position:relative;display:flex;gap:9px;align-items:flex-start;padding:8px 9px;height:100%;
  border:1px solid var(--border);background:var(--wash);cursor:pointer;text-align:left;
  font-family:inherit;color:var(--text)}
.tlm-n:hover{border-color:var(--ring);background:var(--wash-2)}
.tlm-n.on{border-color:var(--brand);background:color-mix(in srgb,var(--brand) 13%,transparent)}
/* Locked is DIMMED, never hidden -- you have to be able to read what you'd be
   unlocking before you decide the ring pick is worth it. */
.tlm-n.off{opacity:.34}
.tlm-n.off:hover{border-color:var(--border);background:var(--wash)}
.tlm-n img{width:34px;height:34px;flex:none;border:1px solid var(--ring);display:block}
.tlm-noicon{width:34px;height:34px;flex:none;display:flex;align-items:center;justify-content:center;
  border:1px solid var(--ring);background:var(--wash-hover);color:var(--text-dim);
  font-size:13px;font-weight:800;line-height:1}
.tlm-noicon.sm{width:18px;height:18px;font-size:9px}
.tlm-t{min-width:0;flex:1}
.tlm-nm{display:block;font-size:12.5px;font-weight:700;line-height:1.3}
.tlm-n.on .tlm-nm{color:var(--brand)}
.tlm-ct{display:block;font-size:8.5px;letter-spacing:.13em;text-transform:uppercase;
  color:var(--text-dim);margin-top:2px}
/* Clamped, not truncated server-side: the full text is on the node's title,
   and a clamp keeps every card in a row the same height whatever the pane
   width. Four lines is enough to tell two passives apart, which is the job. */
.tlm-fx{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;line-clamp:4;
  overflow:hidden;font-size:11px;color:var(--text-dim);line-height:1.45;margin-top:5px}
.tlm-pin{position:absolute;top:6px;right:7px;width:6px;height:6px;background:var(--brand)}

/* Read-only summary on the parse breakdown. One row per ring, names inline --
   eight picks don't need the picker's full card treatment, and the breakdown
   is already a dense page. */
.tlm-sum{margin-bottom:12px}
.tlm-sum:last-child{margin-bottom:0}
.tlm-sum-w{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--text);
  margin:0 0 7px;font-weight:700}
.tlm-sum-row{display:flex;gap:12px;align-items:center;padding:5px 0;
  border-bottom:1px solid var(--hair);font-size:12.5px}
.tlm-sum-row:last-child{border-bottom:0}
.tlm-sum-rl{flex:none;width:58px;font-size:9px;letter-spacing:.13em;text-transform:uppercase;
  color:var(--text-dim)}
.tlm-sum-ns{display:flex;flex-wrap:wrap;gap:7px 16px;min-width:0}
.tlm-sum-n{display:flex;align-items:center;gap:7px}
.tlm-sum-n img{width:18px;height:18px;border:1px solid var(--ring);display:block}

/* The Builds tab borrows Account Settings' side-tab frame; this is only the
   slot the spec builder and the mastery picker mount into. */
.tlp2-builds{flex:1;min-height:0}
.tlp2-buildmount{flex:1;min-height:0;display:flex;flex-direction:column}

/* The Build panes carry NO heading row -- it repeated the highlighted side tab
   beside it, and the height it cost came straight off the spec builder, which
   is scaled to whatever is left. .tlu-builder-top is gone with it. */
/* The generic .tlu-body padding is sized for stacked form sections; a Build
   pane opens straight onto its own heading and needs no such lead-in. This is
   also what pulls Class up towards the top of the window. */
.tlp2-buildmount > .tlu-body{padding-top:18px}

/* A Build pane that needs a class before it can show anything. Dimmed rather
   than hidden: the menu is the map of what this step contains, and hiding four
   of five entries until an unrelated click makes the step look like it grew. */
.tla-vtab.locked{opacity:.38;cursor:not-allowed}
.tla-vtab.locked:hover{background:none;color:var(--text-dim)}

/* The upload flow's Build step borrows Account Settings' side-tab frame, same
   as the profile's Builds tab -- one shape for one menu, in both places. */
.tlu-build{flex:1;min-height:0}
.tlu-build .tla-pane > .tlu-body{padding-top:14px}

/* ---- Build: Class pane -------------------------------------------------
   HUD-RED ONLY, per the standing process rule -- other skins get this in the
   batched skin pass, not now. A first pass to be replaced from mockups.

   ONE FLAT ALPHABETICAL GRID, 45 buttons. It was grouped by weapon, which put
   every class under BOTH halves of its pair -- 90 buttons for 45 classes, two
   routes to the identical outcome. Weapon grouping cannot be made complete
   without that duplication, since a class belongs to both its weapons equally,
   so the grouping went and the weapons moved onto the button.

   The tracks are sized for the longest real pair -- "Sword and Shield +
   Greatsword" -- because a class name wrapping onto two lines in some cells and
   not others makes a grid of equal things look like a grid of unequal ones. */
.tlc-intro{max-width:70ch;margin:0 0 16px}
.tlc-grid{display:grid;gap:6px;grid-template-columns:repeat(auto-fill,minmax(196px,1fr))}
.tlc-btn{display:flex;flex-direction:column;gap:2px;align-items:flex-start;text-align:left;
  font:inherit;padding:8px 10px;cursor:pointer;color:var(--text);
  background:var(--wash-2);border:1px solid var(--border);border-radius:var(--r)}
.tlc-btn:hover{background:var(--wash);border-color:var(--text-dim)}
.tlc-btn.on{border-color:var(--brand);background:color-mix(in srgb, var(--brand) 12%, transparent)}
/* The class NAME carries the accent; the weapon pair under it stays dim. In a
   grid of 45 near-identical buttons the name is the thing being scanned for,
   and giving it the brand colour makes it the thing the eye lands on rather
   than one of two equal lines. hud-red only, per the standing process rule. */
.tlc-n{font-size:12.5px;font-weight:700;letter-spacing:.01em}
body[data-skin="hud"] .tlc-n{color:var(--brand)}
.tlc-w{font-size:10px;color:var(--text-dim);letter-spacing:.04em}
.tlc-btn.on .tlc-w{color:var(--brand)}

/* ---- Build: Build Presets (preview before load) -------------------------
   HUD-RED ONLY, per the standing process rule.

   Mockup 08. List left, the selected build rendered read-only right. The pane
   this replaced could only tell you what a build held by LOADING it, which
   destroyed whatever was on screen -- so comparing what you have against what
   you saved cost you the thing you had. Selecting is free here; only Load
   changes anything. */
.tlp-md{display:grid;grid-template-columns:minmax(0,240px) minmax(0,1fr);gap:16px;align-items:start}
.tlp-list{display:flex;flex-direction:column;gap:4px;min-width:0}
.tlp-item{display:flex;flex-direction:column;gap:2px;align-items:flex-start;text-align:left;
  font:inherit;padding:8px 10px;cursor:pointer;color:var(--text);
  background:var(--wash-2);border:1px solid var(--border);border-radius:var(--r)}
.tlp-item:hover{background:var(--wash);border-color:var(--text-dim)}
.tlp-item.on{border-color:var(--brand);background:color-mix(in srgb, var(--brand) 12%, transparent)}
.tlp-item-n{font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;max-width:100%}
.tlp-item-s{font-size:10px;color:var(--text-dim)}
.tlp-none{margin:0 0 4px}
/* Direction A: no permanent name field. One row at the end of the list, which
   becomes the field when pressed. Dashed so it reads as "a build could go
   here" rather than as an existing one. */
.tlp-new-row{display:block;width:100%;text-align:center;font:inherit;font-size:11.5px;
  padding:9px;cursor:pointer;color:var(--text-dim);background:none;
  border:1px dashed var(--border);border-radius:var(--r)}
.tlp-new-row:hover:not(:disabled){color:var(--text);border-color:var(--text-dim);background:var(--wash)}
.tlp-new-row:disabled{opacity:.45;cursor:not-allowed}
.tlp-namer{display:flex;flex-direction:column;gap:6px;padding:8px;
  border:1px solid var(--brand);border-radius:var(--r);
  background:color-mix(in srgb, var(--brand) 8%, transparent)}
.tlp-namer .tlu-preset-name{max-width:none;min-width:0;flex:none;width:100%}
.tlp-namer-acts{display:flex;gap:6px}
.tlp-namer-acts .tl2-btn{flex:1}

.tlp-detail{min-width:0}
.tlp-dhead{display:flex;align-items:baseline;gap:10px;margin-bottom:12px;flex-wrap:wrap}
.tlp-dhead h4{margin:0;font-size:15px;letter-spacing:.01em}
.tlp-dhead span{font-size:10.5px;color:var(--text-dim)}
/* The preview shows the build's ACTUAL CONTENTS, read-only and in full.
   Twice too thin before: first counts, then chips carrying only skill names.
   Both lost what actually separates two builds of a class -- which spec points
   sit on which skill, and which mastery nodes. Those render now. Nothing here
   is interactive; it is the build as it stands, not a second builder. */
.tlp-block{margin-bottom:16px}
.tlp-bh{margin:0 0 7px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;
  color:var(--text-dim);font-weight:700}
.tlp-empty{margin:0;font-size:11.5px;color:var(--text-dim);font-style:italic}

/* A skill and the spec points chosen on it, lit exactly as the builder lights
   them -- the unpicked ones stay visible because "which of the six" is the
   information, and a list of only the chosen ones cannot show that. */
.tlp-sks{display:grid;gap:6px;grid-template-columns:repeat(auto-fill,minmax(232px,1fr))}
.tlp-sk{display:flex;align-items:center;gap:9px;padding:6px 9px;min-width:0;
  background:var(--wash-2);border:1px solid var(--border);border-radius:var(--r)}
.tlp-sk-i{width:38px;height:38px;flex:none;border-radius:4px;display:block}
.tlp-sk-b{min-width:0;flex:1;display:flex;flex-direction:column;gap:4px}
.tlp-sk-n{font-size:12px;font-weight:700;line-height:1.2;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.tlp-sk-pts{display:flex;gap:4px}
.tlp-pt{width:17px;height:17px;flex:none;border-radius:50%;overflow:hidden;display:block;
  box-shadow:0 0 0 1px var(--border);background:var(--wash)}
.tlp-pt img{width:100%;height:100%;display:block;object-fit:cover;filter:grayscale(1) brightness(.45)}
.tlp-pt.on img{filter:none}
.tlp-pt.on{box-shadow:0 0 0 2px var(--text)}
.tlp-sks.is-pass .tlp-sk-i{width:32px;height:32px}

/* Mastery: the nodes themselves, by weapon, ordered inner -> epic. */
.tlp-mw{margin-bottom:10px}
.tlp-mw h6{margin:0 0 5px;font-size:10.5px;font-weight:700;color:var(--text)}
.tlp-mnodes{display:grid;gap:5px;grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
.tlp-mn{display:flex;align-items:center;gap:8px;padding:5px 8px;min-width:0;
  background:var(--wash-2);border:1px solid var(--border);border-radius:var(--r)}
.tlp-mn.epic{border-color:color-mix(in srgb, var(--brand) 45%, transparent)}
.tlp-mn img{width:26px;height:26px;flex:none;border-radius:4px;display:block}
.tlp-mn-no{width:26px;height:26px;flex:none;border-radius:4px;display:flex;align-items:center;
  justify-content:center;font-style:normal;font-size:11px;background:var(--wash);color:var(--text-dim)}
.tlp-mn-t{min-width:0;display:flex;flex-direction:column}
.tlp-mn-t b{font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tlp-mn-t em{font-style:normal;font-size:9.5px;color:var(--text-dim)}
.tlp-mn.epic .tlp-mn-t em{color:var(--brand)}

/* Gear: flat, in the sheet's own order. */
.tlp-stats{display:grid;gap:4px;grid-template-columns:repeat(auto-fill,minmax(190px,1fr))}
.tlp-stat{display:flex;justify-content:space-between;gap:8px;font-size:11.5px;
  padding:4px 9px;background:var(--wash-2);border-radius:var(--r)}
.tlp-stat i{font-style:normal;color:var(--text-dim);overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.tlp-stat b{flex:none;font-variant-numeric:tabular-nums}
.tlp-dacts{display:flex;gap:6px;flex-wrap:wrap}
.tlp-hint{font-size:10.5px;color:var(--text-dim);margin:10px 0 0}
.tlp-msg{margin:12px 0 0}

/* ---- Build: Gear Stats (two-column ledger) ------------------------------
   HUD-RED ONLY, per the standing process rule.

   Mockup 01 with the grouping removed. It was ~32 equal boxes in a grid under
   invented category headings; label-left / value-right on a rule is the shape
   of the in-game sheet these are copied FROM, and right-aligned tabular figures
   let you run down the column and spot the wrong number -- which a grid of
   centred boxes prevents. The server's field order is the only structure. */
.tlg-sheet{width:100%;border-collapse:collapse;max-width:640px}
.tlg-sheet td{border-bottom:1px solid var(--hair);padding:0;vertical-align:middle}
.tlg-sheet tr:hover td{background:var(--wash)}
.tlg-sheet tr:focus-within td{background:color-mix(in srgb, var(--brand) 8%, transparent)}
.tlg-sheet td.k{padding:7px 10px;color:var(--text-dim);font-size:12px;width:58%}
.tlg-sheet td.k label{cursor:pointer;display:block}
/* A filled row reads differently from an empty one, so a half-done sheet stops
   looking identical to an untouched one. */
.tlg-sheet tr.filled td.k{color:var(--text)}
.tlg-sheet td.v{width:auto}
.tlg-sheet td.v input{width:100%;padding:7px 8px;background:none;border:0;color:var(--text);
  font:inherit;font-size:12.5px;text-align:right;outline:none;font-variant-numeric:tabular-nums}
.tlg-sheet td.v input::placeholder{color:var(--text-dim);opacity:.55}
.tlg-sheet td.u{width:30px;padding:7px 10px 7px 2px;font-size:10.5px;color:var(--text-dim)}
.tlg-sheet tr.err td{background:rgba(229,72,77,.09)}
.tlg-sheet tr.err td.k{color:var(--st-err-2)}
.tlg-sheet .tlg-err{display:block;font-size:10px;color:var(--st-err-2);margin-top:2px}
.tlg-actions{margin-top:12px}

/* ---- Skinned confirmation dialog ---------------------------------------
   HUD-RED ONLY, per the standing process rule. Replaces window.confirm across
   the build panes -- see TL.confirmDialog in thronelogs-shared.js for why the
   native one had to go (unstyleable, prints the hostname, and Chrome's
   "prevent additional dialogs" makes every later confirm silently return
   false, which on destructive actions means saves that quietly stop). */
.tl2-confirm{position:fixed;inset:0;z-index:300;display:flex;align-items:center;
  justify-content:center;padding:24px}
.tl2-confirm-back{position:absolute;inset:0;background:rgba(0,0,0,.62)}
/* OPAQUE GROUND, not a panel token. --panel-2 is rgba(255,255,255,.08) -- an
   overlay meant to sit on a surface, not to be one. Used here it let the
   dimmed backdrop and the page show straight through the dialog, which read as
   half the box being invisible. --dialog-bg is a real colour per skin. */
.tl2-confirm-box{position:relative;max-width:420px;width:100%;padding:20px 22px;
  background:var(--dialog-bg,#1b1a21);border:1px solid var(--border);border-left:3px solid var(--brand);
  border-radius:var(--r);box-shadow:0 18px 50px rgba(0,0,0,.55)}
.tl2-confirm-box h3{margin:0 0 8px;font-size:14.5px;letter-spacing:.01em;color:var(--text)}
.tl2-confirm-box p{margin:0 0 18px;font-size:12.5px;line-height:1.6;color:var(--text-dim)}
.tl2-confirm-acts{display:flex;gap:8px;justify-content:flex-end}


/* ---- The board selector: content > variant > class ----------------------
   Replaces the two rail panels. They could not survive the vocabulary
   growing -- every difficulty of every encounter was another top-level row,
   and there are 33 across three categories now. Menus do not grow: a
   thirty-fourth costs the page nothing.

   ONE BAR, not three controls. They were never independent: content decides
   which variants exist, and both narrow the class list. A single chamfered
   outline with hairline dividers says "one path, read left to right".

   hud-red first, per the standing process rule, but written against tokens so
   the skin pass has little to do. --tlq-bg is the one value that genuinely
   differs: on parchment a black recess turns a warm page into a bruise. */
/* --tlq-lip is the hairline that makes the bar read as RECESSED, and it has to
   INVERT between skins rather than just fade: on a dark skin a recess catches
   light along its top edge, on paper it falls into shadow there. A white lip on
   cream is not a weak recess, it is no recess at all.
   --shadow-menu is the lift under a floating menu. The dark skin's near-black
   drop is a grey bruise on parchment, so that skin gets a warmer, tighter one
   in the key of its own panel shadow. */
:root{--tlq-bg:rgba(0,0,0,.34);--tlq-lip:rgba(255,255,255,.04);
  --shadow-menu:0 16px 44px rgba(0,0,0,.55);}
body[data-skin="parchment"]{--tlq-bg:rgba(58,44,24,.09);--tlq-lip:rgba(58,44,24,.10);
  --shadow-menu:0 10px 24px -6px rgba(90,65,30,.4);}
/* THE ROW IS A LAYER ABOVE THE BOARD, and the z-index has to live HERE rather
   than on the menu. .tlq-step is positioned with z-index:1, making it a
   stacking context -- so .tlq-menu's z-index:40 only ranks it against its own
   siblings inside that step, and against the rest of the page the whole
   selector would sit at 1, under anything later in the DOM that also has one.
   Raising the row lifts every menu it contains with it.
   Note when testing this kind of bug: anything pointer-events:none is
   invisible to elementFromPoint, which will happily name the menu as topmost
   whether it is painting on top or not. Check the pixels, not the hit test. */
.tlq-row{display:flex;align-items:center;flex-wrap:wrap;margin-bottom:14px;flex:none;
  position:relative;z-index:50;}
.tlq{display:inline-flex;align-items:stretch;position:relative;}
/* THE CHAMFER IS ON A BACKDROP LAYER. clip-path clips DESCENDANTS, and the
   dropdowns are descendants -- put it on .tlq and every menu is cut off at the
   bar's own bottom edge, rendering nothing while still reporting itself
   visible with a correct bounding box. */
.tlq::before{content:"";position:absolute;inset:0;pointer-events:none;
  background:var(--tlq-bg);border:1px solid var(--border);
  /* A hairline of light along the top is what reads as recessed rather than
     merely dark; without it the bar is a flat hole. */
  box-shadow:inset 0 1px 0 var(--tlq-lip);
  clip-path:polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));}
.tlq-step{position:relative;display:flex;z-index:1;}
.tlq-step + .tlq-step{border-left:1px solid var(--ring-soft);}
/* Label ABOVE value: the label is fixed furniture and the value is the part
   that changes, so stacking lets the value carry weight without a taller row. */
.tlq-pick{display:flex;flex-direction:column;align-items:flex-start;gap:3px;font:inherit;
  padding:8px 34px 9px 14px;cursor:pointer;color:var(--text);background:none;border:0;
  position:relative;min-width:118px;text-align:left;}
.tlq-pick:hover{background:var(--wash-hover);}
.tlq-pick[aria-expanded="true"]{background:color-mix(in srgb, var(--brand) 15%, transparent);
  box-shadow:inset 2px 0 0 var(--brand);}
.tlq-k{font-size:8px;letter-spacing:.16em;text-transform:uppercase;color:var(--text-dim);
  font-weight:700;line-height:1;}
.tlq-v{font-weight:700;font-size:13px;line-height:1.15;white-space:nowrap;}
/* Parked in the padding and centred on the control, so a long encounter name
   and a short one put the caret in the same place. */
.tlq-car{position:absolute;right:13px;top:50%;transform:translateY(-50%);
  color:var(--text-dim);font-size:8px;transition:transform .15s ease;}
.tlq-pick[aria-expanded="true"] .tlq-car{transform:translateY(-50%) rotate(180deg);color:var(--brand);}
.tlq-menu{position:absolute;top:calc(100% + 6px);left:-1px;z-index:40;min-width:216px;
  max-height:330px;overflow-y:auto;padding:5px;border-radius:var(--r);
  /* --menu-bg, not --dialog-bg: this is a floating menu, and it is the token
     the other two menus on the page (the patch select, the view menu) already
     use. Same value on both skins today, so nothing moves -- but a menu
     surface that needs to lift off the panel later gets changed in one place. */
  background:var(--menu-bg);border:1px solid var(--border);
  box-shadow:var(--shadow-menu);}
.tlq-menu[hidden]{display:none;}
.tlq-mi{display:flex;align-items:center;gap:8px;width:100%;text-align:left;font:inherit;
  font-size:12px;padding:6px 9px;border:0;border-radius:2px;background:none;
  color:var(--text-dim);cursor:pointer;}
.tlq-mi:hover{background:var(--wash);color:var(--text);}
.tlq-mi.on{background:color-mix(in srgb, var(--brand) 14%, transparent);color:var(--text);font-weight:700;}

/* THE VERIFICATION GATE. Sits beside .tlq rather than inside it: the steps
   choose which board you are looking at, this decides what is allowed onto any
   of them, and dressing it as a fourth step would claim it filters the same
   way. A switch rather than a checkbox because it is a mode the board stays in.
   Tokens throughout, so Parchment needs nothing added for it. */
.tlq-sw{display:inline-flex;align-items:center;gap:8px;margin-left:12px;font:inherit;
  font-size:11.5px;color:var(--text-dim);background:none;border:0;padding:7px 4px;
  cursor:pointer;border-radius:2px;}
.tlq-sw:hover{color:var(--text);}
.tlq-sw .tlq-sw-t{position:relative;flex:none;width:26px;height:15px;border-radius:8px;
  background:var(--well-2);border:1px solid var(--border);transition:background .15s ease;}
.tlq-sw .tlq-sw-t::after{content:"";position:absolute;top:1px;left:1px;width:11px;height:11px;
  border-radius:50%;background:var(--text-dim);transition:transform .15s ease,background .15s ease;}
.tlq-sw.on{color:var(--text);}
.tlq-sw.on .tlq-sw-t{background:color-mix(in srgb, var(--brand) 55%, transparent);border-color:var(--brand);}
.tlq-sw.on .tlq-sw-t::after{transform:translateX(11px);background:var(--text);}
.tlq-sw:focus-visible{outline:2px solid var(--brand);outline-offset:2px;}
/* How many rows the gate is holding back, so the switch says what it is for
   before it is touched. Hidden once it is on -- then the board shows them. */
.tlq-sw-n{font-variant-numeric:tabular-nums;font-size:10px;font-weight:700;
  padding:1px 5px;border-radius:7px;background:var(--wash-2);color:var(--text-dim);}

.tlb-empty-s{display:inline-block;margin-top:7px;font-size:12px;color:var(--text-dim);}
.tlb-empty-s b{color:var(--text);font-weight:700;}
.tlq-mi .n{margin-left:auto;font-size:10px;color:var(--text-dim);font-variant-numeric:tabular-nums;}
.tlq-mi.zero{opacity:.5;}
/* The same swatch the old class rail used, and the same rule behind it: the
   colour is that class's TOP parse's rank colour, so it reads as "how this
   class is doing here" rather than as an assigned brand shade. */
.tlq-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dot,#555);}
/* Category headings. "Dragaryle" and "Target Dummy (Buffed)" are not the same
   kind of thing, and a flat list of eleven claims they are. */
.tlq-grp{padding:9px 9px 4px;font-size:8px;letter-spacing:.15em;text-transform:uppercase;
  color:var(--text-dim);font-weight:700;}
.tlq-menu hr{border:0;border-top:1px solid var(--hair);margin:5px 2px;}
.tlq-find{width:100%;padding:6px 9px;margin-bottom:4px;font:inherit;font-size:12px;
  background:var(--wash-2);border:1px solid var(--border);border-radius:2px;color:var(--text);}
.tlq-find:focus{outline:none;border-color:var(--brand);}
@media (prefers-reduced-motion:reduce){.tlq-car{transition:none;}}

/* The content picker in the upload form is the board's selector, so it needs
   only the two things that differ: it sits in a form rather than over a chart,
   and a step can be unset before anything is chosen. */
.tlu-crow{margin:0 0 4px;}
.tlq-pick.tlq-unset .tlq-v{color:var(--text-dim);font-weight:400;font-style:italic;}
