/* ===== THEMES LIVE IN public/themes/ (2026-09-14) =====
   The token blocks that used to open this file — Royal (:root, the light base whose values
   every other theme falls back to), Shadow ([data-theme="dark"]) and Gilded
   ([data-theme="dark"][data-skin="gilded"]) — are one file each under public/themes/, linked
   from index.html after this sheet, with Glass and the Frosted layout beside them, a
   README, and _template.css for adding one. Custom properties resolve at computed time, so
   the order of the sheets changes nothing. The registry the pickers read is js/themes.js.
   What stays here: every component rule, and the [data-theme="dark"] fixes written beside
   the components they fix — those are dark's behaviour, not a palette, and a skin over
   dark inherits them for free. */
/* --- Base & Reset --- */
/* Form controls do NOT inherit `color` — the browser paints them with its own default,
   which is black. On a light page nobody notices; in dark mode any button that forgot to
   set a colour renders black-on-near-black (the Community search rows were 1.19:1). Making
   inheritance the default fixes the whole class at once, and costs nothing: a component
   that sets its own colour uses a class selector, which outranks this element selector. */
button, input, select, textarea, optgroup { color: inherit; }

body {
    background-color: var(--bg-body);
    /* Two layers, the FIRST painting on top: the paper wash over the artwork, both anchored
       to the viewport rather than the document so they behave like something the page is
       lying on instead of wallpaper that scrolls with it. --bg-body stays underneath, so a
       missing image file changes nothing at all. */
    background-image: var(--paper-wash), var(--backdrop);
    background-size: cover, cover;
    background-position: center center, center center;
    background-attachment: fixed, fixed;
    background-repeat: no-repeat, no-repeat;
    color: var(--text-main);
    font-family: 'Segoe UI', 'Helvetica Neue', sans-serif;
    margin: 0;
    padding-top: var(--nav-h);
    display: flex;
    flex-direction: column;
    /* + --sf-cols-extra: see .sf-cols. On a short page the footer sits at the bottom of the
       screen, so whatever height it gains comes out of the page above it unless it is added here. */
    min-height: calc(100vh + var(--sf-cols-extra, 0px));
    line-height: 1.6;
    position: relative; /* Context for pseudo-elements */
}

h1, h2, h3 {
    font-weight: 300;
    margin-top: 0;
    color: var(--text-main);
}

h1 { letter-spacing: var(--h1-tracking); text-transform: var(--h1-transform); color: var(--h1-color); font-weight: var(--h1-weight); }
h2 { letter-spacing: var(--h2-tracking); font-size: 1.5rem; border-bottom: var(--h2-rule); padding-bottom: var(--h2-rule-gap); margin-bottom: 25px; display: inline-block; }
h3 { font-weight: 500; font-size: 1.1rem; }

/* The home hero's headline. Lifted out of an inline style so a theme can reach it at all —
   the colour is deliberately NOT restated here, because the h1 rule above already resolves it
   through --h1-color, which is the same --gold-text the inline style hard-coded in both of the
   original themes. Gilded points that token at --text-main instead and sizes it responsively. */
.home-title { font-size: 3.5rem; margin-bottom: 10px; }

/* --- Navigation --- */
/* SCOPED TO THE HEADER BY ID, not to the `nav` element. A bare `nav {}` selector styling
   the site header silently captures every other <nav> on the page: the Community sidebar was
   a <nav> and inherited position:fixed, height:70px, width:100%, a bottom border and the
   header background, which is what made it render as a stray bar. Class rules on the sidebar
   could not fix it — a class overrides the properties it declares, and the damage came from
   the ones it did not. Any new landmark nav can now style itself freely. */
#site-nav {
    position: fixed;
    top: 0;
    width: 100%;
    height: 70px;
    background: var(--bg-nav);
    border-bottom: 1px solid var(--border-gold);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 40px;
    box-sizing: border-box;
    z-index: 100;
    box-shadow: 0 2px 10px rgba(0,0,0,0.02);
}

.nav-brand {
    font-size: 1.4rem;
    font-weight: 700;
    color: var(--gold-text);
    text-transform: uppercase;
    /* Slightly tighter than before: "HERO'S INN" is shorter than "AQW HUB" was wide, and 2px
       of tracking on a short two-word name reads as spaced-out rather than deliberate. */
    letter-spacing: 1.5px;
    cursor: pointer;
    padding: 5px 0; 
}

/* flex:1 + centre reproduces what `justify-content: space-between` gave when nav had
   exactly three children, but survives the right-hand controls being hidden one by one at
   the narrower breakpoints (the clock already goes at 1100px). */
.nav-links { display: flex; gap: 20px; flex: 1; justify-content: center; }
.nav-btn {
    background: transparent;
    border: none;
    color: var(--text-muted);
    padding: 8px 0;
    cursor: pointer;
    font-size: 0.95rem;
    transition: 0.3s;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 500;
    position: relative;
}

/* Golden Underline Animation */
.nav-btn::after {
    content: ''; position: absolute; width: 0; height: 1px; 
    bottom: 0; left: 0; background-color: var(--gold); transition: width 0.3s;
}
.nav-btn:hover, .nav-btn.active { color: var(--gold-text); }
.nav-btn:hover::after, .nav-btn.active::after { width: 100%; }

/* User Section */
/* Wrappers that do not exist on desktop: `display: contents` drops the box and lets the
   children participate in nav's own flex row, so the bar is laid out exactly as it was
   before it was grouped. The grouping only becomes real at the mobile breakpoint. */
.nav-account, .nav-live { display: contents; }
#site-nav { gap: 20px; }
.nav-menu-btn { display: none; background: transparent; border: none; cursor: pointer;
    color: var(--text-muted); padding: 6px; margin-left: auto; }
.nav-menu-btn:hover { color: var(--gold-text); }
.nav-user {
    color: var(--text-main);
    font-size: 0.9rem;
    cursor: pointer;
    padding: 5px 12px;
    border-radius: 20px;
    /* Use transparent grey for better contrast on both Light/Dark modes */
    background: rgba(128,128,128, 0.1);
    transition: 0.2s;
    border: 1px solid var(--border); 
}
.nav-user:hover { background: var(--gold-light); color: var(--gold-text); border-color: var(--gold); }
/* The chip joins the tab order whenever it DOES something — a character page, "add a character",
   or the sign-in prompt a guest gets (renderHeaderIdentity sets tabindex/role together). A stop
   the eye cannot find is worse than no stop, and there is no global focus rule in this file, so
   it carries its own. :focus-visible, so a mouse click does not leave a ring behind. */
.nav-user:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Held for exactly one frame while the theme tokens are swapped (applyThemeMode in
   format.js). Without it, anything with a transition keeps painting the previous theme's
   colour, because a transition does not re-run when only a custom property changes. */
.theme-switching, .theme-switching *, .theme-switching *::before, .theme-switching *::after {
    transition: none !important;
}

/* --- Live Nav Clock --- */
.nav-clock {
    font-size: 0.85rem;
    color: var(--text-muted);
    white-space: nowrap;
    /* Keeps the width steady as the seconds tick over */
    font-variant-numeric: tabular-nums;
}
/* The clock is the first thing dropped when the bar runs short of room — and it is no
   longer a width breakpoint. `syncNavFit()` in main.js measures what the bar actually needs
   and sets this class. A fixed number can only ever be right for ONE combination of role,
   beta access, link count and username length: 1100 was set against a guest's bar and broke
   for admins, 1320 replaced it, and putting Party Finder in the header would have broken it
   again. Measuring costs one reflow on resize and cannot go stale. */
.nav-noclock .nav-clock { display: none; }

/* Belt and braces, because these numbers depend on things that change: how many nav links
   exist, and how long somebody's username is. The bar must degrade rather than overflow
   even when a breakpoint is slightly wrong. */
.nav-links { min-width: 0; }
.nav-user { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- Small screens: the nav collapses into a menu ---------------------------------
   Before this, the bar simply overflowed: at 375px its contents ran to 747px and the
   page did NOT scroll sideways, so the bell, messenger, theme toggle, username and
   LOGOUT were off-screen and unreachable — not merely cramped.
   `nav` becomes a wrapping flex container: row one is brand + the live controls +
   the toggle, and the links and account rows wrap underneath only when open. The
   closed height stays exactly --nav-h, which the friends drawer and the notification
   panel both anchor to. */
/* Turned on by a CLASS on <html>, not by a width. `syncNavFit()` in main.js measures what
   the bar needs and collapses it when the viewport is narrower — so a guest with four
   links keeps the full bar on a 1280px laptop while an admin with six gets the menu, which
   no single media query can express. The layout itself is unchanged from the width-based
   version; only its trigger is.
   Not a media query and therefore JS-dependent: this app renders every view from JS, so a
   page without it has no nav to collapse in the first place. */
.nav-compact { --nav-h: 58px; }
.nav-compact #site-nav { flex-wrap: wrap; height: auto; min-height: var(--nav-h); padding: 0 14px; gap: 10px; }
.nav-compact .nav-brand { font-size: 1.15rem; letter-spacing: 1px; }
/* Row one keeps the bar exactly --nav-h tall whether the menu is open or shut. */
/* border-box matters here: .nav-brand carries 5px of vertical padding, and without it
   the bar measures --nav-h + 10 while body's padding-top uses --nav-h — an 11px strip
   of content hiding under a FIXED header. */
.nav-compact .nav-brand, .nav-compact .nav-live, .nav-compact .nav-menu-btn {
    height: var(--nav-h); display: flex; align-items: center; box-sizing: border-box; }
.nav-compact .nav-live { gap: 4px; margin-left: auto; order: 2; }
.nav-compact .nav-brand { order: 1; }
.nav-compact .nav-menu-btn { display: flex; order: 3; margin-left: 0; }
/* Everything else wraps to its own full-width row, and only when open. */
.nav-compact .nav-links, .nav-compact .nav-account { flex-basis: 100%; display: none; }
.nav-compact .nav-links { order: 4; }
.nav-compact .nav-account { order: 5; }
.nav-compact #site-nav.nav-open .nav-links { display: flex; flex-direction: column; gap: 0; padding-bottom: 6px; }
.nav-compact #site-nav.nav-open .nav-account { display: flex; flex-direction: row; align-items: center;
    gap: 14px; padding: 8px 0; border-top: 1px solid var(--border); }
.nav-compact #site-nav.nav-open .nav-links .nav-btn { padding: 11px 2px; text-align: left; font-size: 1rem; }
/* The underline indicator is a desktop affordance; on a stacked list it reads as a rule. */
.nav-compact #site-nav.nav-open .nav-links .nav-btn::after { display: none; }
.nav-compact #site-nav.nav-open .nav-links .nav-btn.active { color: var(--gold-text); }
.nav-compact .nav-menu-btn svg { width: 26px; height: 26px; }
/* Below this the brand competes with the controls for the same row. */
@media (max-width: 380px) {
    .nav-brand { font-size: 1rem; letter-spacing: 0; }
}

/* (Removed 2026-08-29: the .theme-icon-btn rules. They dressed the theme button for the
   HEADER — a 40px circle with a 2px gold ring, a --bg-nav fill and `margin-right: 15px` —
   and followed the button into the control rail, where the margin pushed it 15px off the
   rail's centre line and the ring made it the only control with a different outline. It also
   styled an <img> the button no longer renders, now that the themed PNGs are gone. Deleted
   rather than left unused: a dead rule that would silently re-apply the moment somebody
   re-added the class is a trap, not documentation.) */

/* --- Layout --- */
.container { max-width: 1100px; width: 100%; margin: 0 auto; padding: 40px 20px; box-sizing: border-box; position: relative; z-index: 1; }
.view-section { display: none; animation: fadeIn 0.6s ease-out; }
.view-section.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

/* --- Cards --- */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 30px; }
.card {
    background: var(--panel-bg);
    border: 1px solid var(--panel-border);
    border-radius: var(--r-panel);
    padding: 30px;
    position: relative;
    box-shadow: var(--shadow-card);
}
/* Hover belongs to cards that GO somewhere. `.card` is also the plain container for the
   profile, the guild hub and the MOTD box — panels that sit flush against each other and do
   nothing when clicked. There, a 5px lift raised the hovered panel OVER its neighbour's
   border and read as the layout drifting under the cursor, and the gold highlight was
   answering a question nobody had asked. Both are gone for those.
   The database result grid is the one place it earns its keep: those cards ARE links, and
   the grid's 30px gutter absorbs the lift entirely, so nothing ever overlaps. `[onclick]`
   selects exactly the clickable ones without needing a marker class. */
.card[onclick] { transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease; }
.card[onclick]:hover {
    transform: translateY(-5px);
    border-color: var(--gold);
    box-shadow: var(--shadow-hover);
}
.card h3 { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }

/* Card header: name (with its inline tags) top-left, type tag pinned top-right.
   The h3 override stops the rule above from spreading a name and its own tags apart. */
.card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
.card-header h3 { display: block; margin: 0; flex: 1; }
.card-header > .tag { flex-shrink: 0; }

.tag {
    background: white;
    border: 1px solid var(--border);
    padding: 2px 8px;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--text-muted);
}
/* Fix for dark mode tags */
[data-theme="dark"] .tag {
    background: #2a2a2a; /* Darker background for tags in dark mode */
    border-color: var(--border);
    color: var(--text-main); /* Ensure text is readable */
}
/* --- Entry Tags: currency (AC), access (Legend), rarity --- */
.tag-seasonal {
    border-color: #e0218a;
    color: #e0218a;
    font-weight: 600;
}
.tag-legend {
    border-color: #9c27b0;
    color: #9c27b0;
    font-weight: 600;
}
.tag-ac {
    border-color: #ff9800;
    color: #ff9800;
    font-weight: 600;
}
.tag-rare {
    border-color: #1b7a3d;
    color: #1b7a3d;
    font-weight: 600;
}
.tag-pseudo {
    border-color: #5cc85c;
    color: #5cc85c;
    font-weight: 600;
}
/* Deep green loses contrast on the dark background */
[data-theme="dark"] .tag-rare {
    border-color: #3fa45f;
    color: #3fa45f;
}

/* Short form: tiny tags riding after a name in lists */
.entry-tag {
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.5px;
    margin-left: 3px;
    vertical-align: super;
    cursor: help;
}

.highlight { color: var(--gold-text); font-weight: 500; }

/* --- Action Buttons --- */
/* The opt-in, and the case .action-btn stretching was born for: a row of equal-width buttons
   (the database Own / Track / Remove set). Everywhere else a button is as wide as what it says. */
/* Wraps when the row does not fit (2026-09-23): four buttons on a guide or review card ran ~80px
   off a phone's edge, and with scrollbars hidden app-wide nothing showed the rest was there. */
.btn-group { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
.btn-group .action-btn { flex: 1; }
.action-btn {
    /* SIZE TO THE LABEL. This was flex: 1 from the day the class was written for the database
       Own/Track rows, where a row of buttons sharing the width evenly is exactly right — but
       that is a property of THAT ROW, not of a button, and it followed the class into every
       flex container in the app. The results were absurd once measured: a "Save" button 376px
       wide for four letters, a lone "Withdraw" taking 60% of its row, and a party action row
       forced to its longest label so "Keep listed · 1h left" wrapped to three lines and dragged
       three neighbours to 184px each.
       SIX separate rules existed only to undo it, which is the signal that the default was
       backwards rather than the sites unlucky. Rows that genuinely want equal widths now ASK,
       via .btn-group below — the one container the original behaviour was written for. */
    flex: 0 0 auto;
    padding: 10px;
    cursor: pointer;
    /* A <button> does not inherit font-family — the UA sheet gives it Arial — so every button
       carrying this class was set in a different typeface from the rest of the page, and from
       the inputs and selects right beside it, which have inherited since they were written.
       This rule set a SIZE but never a family, which is the whole of the bug: it was invisible
       until a <label> wearing the same class sat next to a <button> and the two plainly did not
       match. Every other control rule in this file already says `font: inherit` or
       `font-family: inherit`; .action-btn is simply the one that was missed.
       Buttons get ~3px taller with it: Segoe UI's normal line box is taller than Arial's. That
       is the site's own metric, and it also settles a second mismatch — buttons holding an
       inline SVG were already 40px while text-only ones were 37, so rows mixing the two now
       line up. Measured across the party, database and profile views at 1280 and 375: nothing
       wraps, nothing overflows, nothing leaves the screen. */
    font-family: inherit;
    font-size: 0.85rem;
    font-weight: var(--btn-weight);
    text-transform: uppercase;
    letter-spacing: 1px;
    background: transparent;
    transition: all 0.2s;
    border-radius: var(--r-btn);
    /* A default border and colour: without them a bare .action-btn falls back to
       the browser's own button styling, which is unreadable on the dark theme */
    border: 1px solid var(--border);
    color: var(--text-main);
}
.action-btn:hover { border-color: var(--gold); color: var(--gold-text); }

/* Primary (Own/Save) */
.btn-own { border: 1px solid var(--success); color: var(--success); }
.btn-own:hover { background: var(--success); color: white; }

/* Secondary (Track) */
.btn-track { border: 1px solid var(--gold); color: var(--gold-text); }
.btn-track:hover { background: var(--gold); color: var(--on-gold); }

/* Destructive */
.btn-remove { border: 1px solid #bbb; color: #999; }
.btn-remove:hover { border-color: var(--danger); color: var(--danger-text); }

/* --- Profile Specific --- */
.profile-header {
    background: var(--bg-panel); /* Ensure background follows theme */
    padding: 40px;
    border: 1px solid var(--border-gold);
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 50px;
    box-shadow: var(--shadow-card);
}
.level-control { display: flex; align-items: center; gap: 15px; }
.level-input {
    width: 70px; padding: 8px; text-align: center;
    border: 1px solid var(--border); font-size: 1.1rem; color: var(--text-main);
    background: transparent;
}
.level-input:focus { outline: none; border-color: var(--gold); }

/* --- Reputation Bars --- */
.rep-container { 
    display: grid; 
    grid-template-columns: 1fr 1fr; 
    gap: 30px; 
    margin-bottom: 50px; 
    align-items: start; 
}
@media (max-width: 768px) { .rep-container { grid-template-columns: 1fr; } }

/* NEW: Scrollable Faction List styles */
#rep-list {
    max-height: 400px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding-right: 10px; /* Space for scrollbar */
}

/* Custom Scrollbar for Rep List (Matches Dropdown) */
#rep-list::-webkit-scrollbar { width: 6px; }
#rep-list::-webkit-scrollbar-track { background: var(--bg-body); }
#rep-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
#rep-list::-webkit-scrollbar-thumb:hover { background: var(--gold); }


.rep-card {
    padding: 20px; border: 1px solid var(--border); background: var(--bg-panel); /* Use var for background */
    display: flex; justify-content: space-between; align-items: center;
    flex-shrink: 0; /* Prevent squishing in flex container */
}
.rep-bar-bg { width: 120px; height: 4px; background: #eee; margin-top: 8px; }
/* Dark mode adjust for bar bg */
[data-theme="dark"] .rep-bar-bg { background: #333; }

.rep-bar-fill { height: 100%; background: var(--gold); }

.rep-input-group { display: flex; gap: 10px; margin-top: 15px; }

input[type="text"], input[type="number"], input[type="password"], input[type="email"], select, textarea {
    width: 100%; padding: 12px; margin-bottom: 10px;
    border: 1px solid var(--border); background: #fafafa;
    border-radius: var(--r-field);
    color: var(--text-main); box-sizing: border-box;
    font-family: inherit;
}
/* Input styles for dark mode */
[data-theme="dark"] input[type="text"], 
[data-theme="dark"] input[type="number"], 
[data-theme="dark"] input[type="password"], 
[data-theme="dark"] input[type="email"],
[data-theme="dark"] select,
[data-theme="dark"] textarea {
    background: #1a1a1a;
    border-color: #333;
    color: var(--text-main);
}

input:focus { outline: none; border-color: var(--gold); background: white; }
[data-theme="dark"] input:focus { background: #222; }

/* --- Custom Dropdown --- */
.custom-select-wrapper {
    position: relative;
    width: 100%;
}

.custom-dropdown-list {
    position: absolute;
    top: 100%; /* Just below input */
    left: 0;
    right: 0;
    background: var(--bg-panel);
    border: 1px solid var(--gold);
    border-top: none;
    z-index: 1000;
    max-height: 250px;
    overflow-y: auto;
    display: none; /* Hidden by default */
    box-shadow: var(--shadow-hover);
    margin-top: -10px; /* Slight overlap or adjustment */
}

.custom-dropdown-item {
    padding: 12px 15px;
    cursor: pointer;
    color: var(--text-main);
    border-bottom: 1px solid rgba(0,0,0,0.05);
    transition: background 0.2s;
    font-size: 0.95rem;
}

.custom-dropdown-item:hover {
    background: var(--gold-light);
    color: var(--gold-text);
    font-weight: 500;
}

.custom-dropdown-item:last-child {
    border-bottom: none;
}

/* Custom Scrollbar for Dropdown */
.custom-dropdown-list::-webkit-scrollbar {
    width: 6px;
}
.custom-dropdown-list::-webkit-scrollbar-track {
    background: var(--bg-body);
}
.custom-dropdown-list::-webkit-scrollbar-thumb {
    background: var(--border);
    border-radius: 3px;
}
.custom-dropdown-list::-webkit-scrollbar-thumb:hover {
    background: var(--gold);
}


/* --- Character Chips --- */
.character-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.char-chip {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 15px;
    background: var(--gold-light);
    border: 1px solid var(--gold);
    border-radius: 20px;
    font-size: 0.9rem;
    color: var(--text-main);
}
.char-icon { font-size: 1.1rem; }
.char-remove { 
    margin-left: 5px; cursor: pointer; color: var(--text-muted); font-weight: bold; 
    transition: color 0.2s;
}
.char-remove:hover { color: var(--danger-text); }

/* --- Auth Modal --- */
.auth-overlay {
    position: fixed; top: 0; left: 0; width: 100%; height: 100%; 
    background: rgba(255,255,255,0.95);
    display: none; justify-content: center; align-items: center; z-index: 1000;
}
[data-theme="dark"] .auth-overlay {
    background: rgba(0,0,0,0.9);
}

.auth-box {
    background: var(--bg-panel); padding: 50px; width: 350px;
    border: 1px solid var(--gold);
    text-align: center;
    box-shadow: 0 10px 40px rgba(197, 160, 40, 0.2);
}
.submit-btn {
    background: var(--gold); color: var(--on-gold); border: none;
    padding: 12px 20px; width: 100%; font-weight: 500;
    text-transform: uppercase; letter-spacing: 1px;
    cursor: pointer; margin-top: 20px;
}
.submit-btn:hover { background: #b08d21; }

/* --- Social sign-in --- */
.social-divider {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 22px 0 14px;
    color: var(--text-muted);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 1px;
}
.social-divider::before,
.social-divider::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--border);
}
.social-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    padding: 11px 14px;
    margin-bottom: 10px;
    border: 1px solid var(--border);
    background: var(--bg-panel);
    color: var(--text-main);
    font-size: 0.9rem;
    font-family: inherit;
    cursor: pointer;
    transition: border-color 0.2s, box-shadow 0.2s;
}
.social-btn:hover { border-color: var(--gold); box-shadow: var(--shadow-hover); }
.social-btn svg { width: 18px; height: 18px; flex-shrink: 0; }

.linked-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
}
.linked-row:last-of-type { border-bottom: none; }
.linked-name { display: flex; align-items: center; gap: 8px; flex: 1; }
.linked-name svg { width: 18px; height: 18px; }
.linked-state { font-size: 0.85rem; white-space: nowrap; }

.auth-link {
    cursor: pointer;
    text-decoration: underline;
    color: var(--text-muted);
    font-size: 0.9rem;
    display: block;
    margin-top: 10px;
}
.auth-link:hover { color: var(--gold-text); }

/* --- Guides --- */
.guide-card { display: flex; flex-direction: column; }
.guide-cover {
    width: calc(100% + 60px);
    margin: -30px -30px 15px;   /* bleed to the card edge, cancelling its padding */
    height: 150px;
    object-fit: cover;
}
.guide-cover-large {
    width: 100%;
    max-height: 320px;
    object-fit: cover;
    margin-bottom: 20px;
}

/* Like toggle on guide cards and the detail page */
.guide-card-foot {
    margin-top: 10px;
    display: flex;
    justify-content: flex-end;
}
.guide-detail-like {
    margin: 4px 0 16px;
    /* The like pill and the Share button side by side (2026-09-20). */
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
/* Share sits beside the like pill, so it takes the pill's rounded shape rather than the square
   .action-btn one — two controls in a row should read as a pair, not as two design languages. */
.guide-share-btn { display: inline-flex; align-items: center; gap: 6px; border-radius: 999px;
    padding: 5px 12px; font-size: .85rem; }
.guide-share-btn svg { width: 14px; height: 14px; }
/* A message that carries a link on its own line (the share-link fallback) keeps the break. */
.ask-message { white-space: pre-line; }
.guide-like {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: none;
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 4px 12px;
    cursor: pointer;
    color: var(--text-muted);
    font-size: 0.9rem;
    transition: color 0.15s, border-color 0.15s;
}
.guide-like:hover { border-color: var(--danger); color: var(--danger-text); }
.guide-like.liked { color: var(--danger-text); border-color: var(--danger); }
.guide-like-heart { font-size: 1.05rem; line-height: 1; }
.guide-like-count { font-variant-numeric: tabular-nums; }
.guide-meta { font-size: 0.8rem; color: var(--text-muted); margin-top: auto; padding-top: 10px; }
.guide-summary {
    font-size: 1.05rem;
    color: var(--text-muted);
    font-style: italic;
    border-left: 3px solid var(--gold);
    padding-left: 15px;
}
.guide-badge {
    display: inline-block;
    margin-left: 8px;
    padding: 1px 7px;
    border-radius: 10px;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    vertical-align: middle;
}
.guide-draft { border: 1px solid var(--danger); color: var(--danger-text); }
.guide-pinned { border: 1px solid var(--gold); color: var(--gold-text); }

.guide-contents {
    background: var(--gold-light);
    border-left: 2px solid var(--gold);
    padding: 15px 20px;
    margin: 20px 0 30px;
    font-size: 0.9rem;
}
.guide-contents ol { margin: 8px 0 0; padding-left: 20px; }
.guide-contents li { margin: 4px 0; }
.guide-contents a { color: var(--gold-text); cursor: pointer; text-decoration: none; border-bottom: 1px dotted var(--gold); }

.guide-section { margin-bottom: 35px; scroll-margin-top: 90px; } /* clears the fixed nav when jumping */
/* Body-text colour with the accent in the rule beneath, matching how h2 is styled
   site-wide. Gold text here reads at only 2.56:1 on the dark theme, and a section
   heading is something you actually read rather than glance at. */
.guide-section h3 {
    border-bottom: 2px solid var(--gold);
    padding-bottom: 8px;
    margin-bottom: 12px;
    color: var(--text-main);
}
.guide-section p { margin: 8px 0; line-height: 1.7; }
/* A section's picture is CENTRED, always (2026-09-20, the owner). It was an inline <img> with
   `margin: 12px 0`, so anything narrower than the column sat against the left edge under a
   centred-feeling heading. `display: block` is what lets `auto` side margins centre it; the
   `onerror` hide on the tag still wins, because an inline style outranks this rule. */
/* border-box so the 1px border sits INSIDE max-width: 100% — without it a picture as wide as
   the column ran 2px past its edge (measured: right edge at -2px), a pre-existing overflow the
   centring work happened to expose. */
.guide-section-image { display: block; box-sizing: border-box; max-width: 100%; height: auto; margin: 12px auto; border: 1px solid var(--border); }

/* Reorder buttons in the section editor */
.section-move {
    background: none;
    border: 1px solid var(--border);
    color: var(--text-muted);
    cursor: pointer;
    padding: 4px 8px;
    line-height: 1;
    font-size: 0.85rem;
}
.section-move:hover:not(:disabled) { border-color: var(--gold); color: var(--gold-text); }
.section-move:disabled { opacity: 0.3; cursor: not-allowed; }

/* Legacy guide step styling, kept for anything still using it */
.guide-step {
    padding: 12px; margin: 5px 0;
    border-left: 2px solid var(--gold);
    background: var(--gold-light);
    font-size: 0.9rem; color: #555;
}
[data-theme="dark"] .guide-step {
    color: #ccc; /* Better contrast for dark mode */
}

/* Database Category Buttons */
.database-cat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; margin-bottom: 20px; }
.database-cat-btn { padding: 10px 5px; background: var(--bg-panel); border: 1px solid var(--border); cursor: pointer; color: var(--text-muted); font-size: 0.85rem; border-radius: 4px; transition: all 0.2s; }
.database-cat-btn:hover { border-color: var(--gold); color: var(--text-main); }
.database-cat-btn.active { background: var(--gold); color: var(--on-gold); border-color: var(--gold); font-weight: bold; }
/* Reorder-by-drag: text selection would otherwise fight the hold gesture */
.database-cat-btn { user-select: none; -webkit-user-select: none; }
.database-cat-btn.dragging {
    opacity: 0.65;
    border-color: var(--gold);
    box-shadow: 0 4px 14px rgba(0,0,0,0.18);
    transform: scale(1.05);
    cursor: grabbing;
    position: relative;
    z-index: 5;
}

/* --- NEW: Chip Input Styles --- */
.chip-input-wrapper {
    position: relative;
    border: 1px solid var(--border);
    background: var(--bg-body); /* To match regular inputs */
    padding: 5px;
    min-height: 48px; /* Approximate height of other inputs */
    margin-bottom: 10px;
}
[data-theme="dark"] .chip-input-wrapper {
    background: #1a1a1a;
    border-color: #333;
}

.chip-area {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
}

.admin-chip {
    background: var(--gold-light);
    border: 1px solid var(--gold);
    border-radius: 12px;
    padding: 4px 10px;
    font-size: 0.85rem;
    color: var(--text-main);
    display: flex;
    align-items: center;
    gap: 5px;
}
[data-theme="dark"] .admin-chip {
    background: #3a2e05; /* Dark gold-ish */
}

.chip-close {
    cursor: pointer;
    font-weight: bold;
    color: var(--text-muted);
    line-height: 1;
}
.chip-close:hover {
    color: var(--danger-text);
}

.chip-input-field {
    border: none !important;
    background: transparent !important;
    outline: none !important;
    padding: 5px !important;
    margin: 0 !important;
    flex: 1;
    min-width: 120px;
    color: var(--text-main) !important;
}

.chip-dropdown {
    position: absolute;
    top: 100%;
    left: -1px;
    right: -1px;
    background: var(--bg-panel);
    border: 1px solid var(--gold);
    z-index: 1000;
    max-height: 200px;
    overflow-y: auto;
    display: none;
    box-shadow: var(--shadow-hover);
}

.chip-suggestion {
    padding: 10px;
    cursor: pointer;
    border-bottom: 1px solid rgba(0,0,0,0.05);
    font-size: 0.9rem;
    color: var(--text-main);
}
.chip-suggestion:hover {
    background: var(--gold-light);
    color: var(--gold-text);
}
/* Which collection the suggestion is from. Pushed right and faded from the
   inherited colour, so it stays subordinate to the name and follows the row's
   colour on hover instead of fighting it. */
.chip-suggestion-type {
    float: right;
    margin-left: 12px;
    font-size: 0.8em;
    opacity: 0.6;
}

/* Hover explanations in markup (2026-09-20): ?[words]?=(explanation). The SAME shape as the
   class-skill buff tooltip (.buff-ref / .buff-tip) on purpose, so a dotted word means the same
   thing everywhere in the app. Opens on hover AND on focus: the trigger is tabindex=0, which is
   what lets a tap open it on a phone (no hover there) and a keyboard reach it at all. */
.mk-tipref {
    position: relative; cursor: help;
    border-bottom: 1px dotted var(--info, var(--gold));
    color: var(--info, inherit); font-weight: 500;
}
.mk-tipref:focus { outline: none; }
.mk-tipref:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* `.mk-tipbox`, never `.mk-tip`: that is the "> tip:" CALLOUT's class, and this rule's
   display:none once hid every tip callout on every guide (2026-09-20 → 21). markup.test.js now
   checks that no class the renderer puts on a visible block is also one of these. */
.mk-tipbox {
    display: none; position: absolute; left: 0; top: calc(100% + 6px); z-index: 60;
    min-width: 200px; max-width: 340px; padding: 10px 12px;
    background: var(--bg-panel); border: 1px solid var(--info, var(--border));
    box-shadow: var(--shadow-hover, 0 6px 18px rgba(0,0,0,.35));
    color: var(--text-main); font-size: 0.85rem; font-weight: normal; line-height: 1.5;
    white-space: normal; text-align: left; text-decoration: none;
    /* An explanation can be one unbroken token with no spaces in it. Normal wrapping only breaks
       at existing opportunities, so without this the box ignores its own max-width and runs off
       the page — the lesson .pt-rowdesc-tip already paid for. */
    overflow-wrap: anywhere;
}
.mk-tipref:hover .mk-tipbox,
.mk-tipref:focus .mk-tipbox,
.mk-tipref:focus-within .mk-tipbox { display: block; }
/* On a phone, a box anchored to a word near the right edge would push the page sideways (the
   same thing .buff-tip guards against). There is no reliable positioned container around
   arbitrary markup to span instead, so it becomes a sheet pinned to the bottom of the screen:
   it can never overflow, and a tap on the word is what opens it. */
@media (max-width: 600px) {
    .mk-tipbox { position: fixed; left: 12px; right: 12px; top: auto; bottom: 16px;
        min-width: 0; max-width: none; z-index: 1300; }
}

/* The slim gallery card (2026-09-20): picture, title, like count. The title is a BUTTON now
   rather than a heading, because the whole card opens the pop-up and a click target that looks
   like text is one people do not try. It is styled back to look like the heading it replaced —
   left-aligned, inheriting the font, no chrome — so nothing about the grid changes visually. */
.of-card-slim .of-body { padding: 10px 12px 12px; gap: 6px; }
.of-title-btn { background: none; border: none; padding: 0; margin: 0; font: inherit;
    font-size: 1.05rem; color: inherit; text-align: left; cursor: pointer; }
.of-title-btn:hover { color: var(--gold-text); }
/* The pop-up's picture zooms, so it has to read as pressable — it was a plain div before. */
.of-popup-fig { padding: 0; cursor: zoom-in; width: 100%; }

/* The character panel in the header (2026-09-20). Anchored under the chip like the
   notifications panel, and sharing its floating surface token so the opacity sliders reach it
   too. Ticks are a fixed-size square rather than a checkbox so a row cannot reflow as its
   content changes between a check mark and nothing. */
.chp-panel {
    position: fixed; top: calc(var(--nav-h) + 6px); right: 16px; width: 300px;
    max-width: calc(100vw - 24px); max-height: min(70vh, 560px); overflow-y: auto;
    background: var(--bg-float, var(--bg-panel)); border: 1px solid var(--border-gold, var(--border));
    border-radius: 6px; box-shadow: 0 10px 30px rgba(0,0,0,.3); padding: 12px 14px; z-index: 1200;
}
.chp-head { display: flex; justify-content: space-between; align-items: center; gap: 10px;
    padding-bottom: 8px; margin-bottom: 4px; border-bottom: 1px solid var(--border); }
.chp-lvl { font-size: 0.75rem; color: var(--text-hint); margin-left: 6px; }
.chp-empty { font-size: 0.85rem; color: var(--text-hint); margin: 10px 0; }
/* The groups FOLD (2026-09-22, the owner's call): the header row is a button — chevron, label,
   "6/8" — and the list appears only when it is opened. */
.chp-group { margin-top: 8px; }
.chp-group-head {
    display: flex; align-items: center; gap: 6px; width: 100%; margin: 0; padding: 6px 4px;
    background: none; border: 0; border-radius: 4px; cursor: pointer; font: inherit;
    font-size: 0.8rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-hint); text-align: left;
}
.chp-group-head:hover { color: var(--text-main); background: var(--gold-light, rgba(0, 0, 0, .04)); }
.chp-group-head:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
.chp-group-label { flex: 1 1 auto; }
.chp-chev { flex: none; transition: transform .15s; }
.chp-group:not(.open) .chp-chev { transform: rotate(-90deg); }
.chp-group .chp-list { padding: 2px 4px 4px 26px; }
.chp-count { color: var(--gold-text); font-weight: 600; }
.chp-count.all { color: var(--success); }
/* The account's other characters, one click each (2026-09-22). */
.chp-switch { margin-top: 6px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
.chp-switch-label { display: block; margin: 0 0 2px 4px; font-size: 0.68rem; color: var(--text-hint); }
.chp-chars { list-style: none; margin: 0; padding: 0; }
.chp-char {
    display: flex; align-items: center; gap: 6px; width: 100%; padding: 5px 4px; margin: 0;
    background: none; border: 0; border-radius: 4px; cursor: pointer; font: inherit; font-size: 0.85rem;
    color: var(--text-main); text-align: left;
}
.chp-char:hover { background: var(--gold-light, rgba(0, 0, 0, .04)); color: var(--gold-text); }
.chp-char:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
.chp-char-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.chp-char-ok { flex: none; display: inline-flex; color: var(--success); }
.chp-char-lvl { flex: none; font-size: 0.72rem; color: var(--text-hint); }
.chp-head:focus { outline: none; }
@media (prefers-reduced-motion: reduce) { .chp-chev { transition: none; } }
.chp-list { list-style: none; margin: 0; padding: 0; }
.chp-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 0.85rem; }
.chp-row.done .chp-goal { color: var(--text-hint); text-decoration: line-through; }
.chp-tick { width: 18px; height: 18px; flex: 0 0 18px; padding: 0; display: flex;
    align-items: center; justify-content: center; border: 1px solid var(--border);
    border-radius: 3px; background: none; color: var(--gold-text); cursor: pointer; }
.chp-tick:hover:not(:disabled) { border-color: var(--gold); }
.chp-tick:disabled { cursor: default; opacity: .8; }
.chp-row.done .chp-tick { border-color: var(--gold); }
.chp-goal { flex: 1 1 auto; min-width: 0; }
.chp-kind { font-size: 0.65rem; text-transform: uppercase; color: var(--text-hint);
    border: 1px solid var(--border); border-radius: 8px; padding: 0 5px; }
.chp-foot { display: flex; justify-content: space-between; gap: 10px;
    margin-top: 12px; padding-top: 8px; border-top: 1px solid var(--border); font-size: 0.8rem; }
@media (max-width: 700px) { .chp-panel { right: 8px; left: 8px; width: auto; } }

/* "Notify me" on the Party Finder board, and its dialog (2026-09-20). The button carries the
   state in its own label ("Notify me" / "Watching") as well as in colour, so the setting is
   readable without relying on the gold. The scope block dims rather than disappearing when the
   master tick is off: a control that vanishes leaves you wondering where it went. */
.pt-watch-btn.on { border-color: var(--gold); color: var(--gold-text); }
.pt-watch-btn svg { vertical-align: -2px; margin-right: 4px; }
.pt-watch-dialog { max-width: 420px; }
.pt-watch-row { display: flex; align-items: center; gap: 8px; margin: 10px 0; cursor: pointer; }
.pt-watch-row input { margin: 0; }
.pt-watch-scope { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); transition: opacity .15s; }
.pt-watch-scope.off { opacity: .45; pointer-events: none; }
.pt-watch-goals { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: 4px 12px; margin: 8px 0 0 24px; max-height: 210px; overflow-y: auto; }
.pt-watch-goals.off { opacity: .45; pointer-events: none; }
.pt-watch-goal { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; cursor: pointer; }
.pt-watch-warn { color: var(--danger-text); }

/* Your data (2026-09-20). Plain blocks rather than anything decorative: this is a page people
   arrive at with a specific worry, and the job is legibility, not presence. The status pills
   borrow the app's existing semantic colours so "Refused" reads the same as every other refusal
   in the app, and they carry their own text so colour is never the only signal. */
.yd-block { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); }
.yd-block:first-of-type { border-top: none; padding-top: 0; }
.yd-card select, .yd-card textarea { width: 100%; }
.yd-h4 { margin: 0 0 8px; font-size: 0.95rem; }
.yd-list { list-style: none; margin: 0; padding: 0; }
.yd-row { border: 1px solid var(--border); border-radius: 4px; padding: 10px 12px; margin-bottom: 8px; }
.yd-row-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.yd-row-detail { margin: 6px 0 0; font-size: 0.85rem; color: var(--text-hint); }
.yd-row-reply { margin: 8px 0 0; font-size: 0.85rem; }
.yd-row-foot { display: flex; justify-content: space-between; align-items: center; gap: 10px;
    margin-top: 8px; font-size: 0.75rem; color: var(--text-hint); }
.yd-status { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .05em;
    padding: 2px 8px; border-radius: 10px; border: 1px solid var(--border); white-space: nowrap; }
.yd-open { border-color: var(--gold); color: var(--gold-text); }
.yd-done { border-color: var(--success); color: var(--success); }
.yd-refused { border-color: var(--danger); color: var(--danger-text); }
.yd-withdrawn { opacity: .7; }

/* Dismiss, with "party" under it (2026-09-20). Two lines in the space of one: the vertical
   padding drops from the .action-btn default so the taller content does not make the button
   taller than the ones beside it, and both lines are set tight. The sub-label inherits COLOUR
   and dims with opacity, never a fixed grey — .action-btn:hover repaints the text gold, and a
   hard-coded hint colour would sit there unchanged looking broken. .pt-actions centres its
   row, so any residual difference in height cannot misalign the neighbours. */
.pt-dismiss-btn {
    display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
    padding-top: 5px; padding-bottom: 5px; line-height: 1.15;
}
.pt-dismiss-sub {
    font-size: 0.62rem; letter-spacing: .04em; opacity: .6;
    color: inherit; margin-top: 1px;
}

/* --- Deleting an account (2026-09-20) ---------------------------------------------------
   Its own card at the foot of Settings. The red is on the BORDER and the final button only:
   a whole panel of warning colour reads as an error state rather than a choice, and this
   panel is mostly explanation that people need to actually read. */
.da-card { border-top: 4px solid var(--danger); }
/* Signed-in devices (session.js, 2026-10-07): one line per device, Sign out at its end. */
.sd-card[hidden] { display: none; }
.sd-list { list-style: none; margin: 0 0 12px; padding: 0; }
.sd-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 9px 0; border-top: 1px solid var(--border); }
.sd-name { font-weight: 600; }
.sd-when { color: var(--text-muted); font-size: .85rem; flex: 1 1 auto; }
.da-open { border-color: var(--danger); color: var(--danger-text); }
.da-open:hover { background: color-mix(in srgb, var(--danger) 10%, transparent); }
#da-panel { margin-top: 16px; }
.da-lead { margin: 0 0 6px; font-weight: 600; }
.da-list { margin: 0 0 14px; padding-left: 20px; font-size: 0.85rem; line-height: 1.55; color: var(--text-hint); }
.da-list li { margin-bottom: 5px; }
.da-list strong { color: var(--text-main); }
.da-label { display: block; margin: 12px 0 5px; font-size: 0.9rem; color: var(--text-muted); }
.da-warn { color: var(--danger-text); font-size: 0.85rem; margin: 0 0 12px; }
.da-msg { min-height: 20px; margin: 10px 0 0; font-size: 0.85rem; color: var(--danger-text); }
.da-actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.da-go { border-color: var(--danger); color: var(--danger-text); font-weight: 600; }
.da-go:hover:not(:disabled) { background: var(--danger); color: #fff; border-color: var(--danger); }
.da-go:disabled { opacity: 0.45; cursor: not-allowed; }

/* --- Database Cross-Links (referenced names that exist in the database) --- */
.entity-link {
    color: var(--gold-text);
    cursor: pointer;
    text-decoration: none;
    border-bottom: 1px dotted var(--gold);
    transition: opacity 0.2s;
}
.entity-link:hover {
    opacity: 0.75;
    border-bottom-style: solid;
}

/* --- Field help ("?" beside a label on the submission forms) --- */
.field-help {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    margin-left: 6px;
    border: 1px solid var(--info);
    border-radius: 50%;
    color: var(--info);
    font-size: 0.65rem;
    font-weight: 700;
    cursor: help;
    position: relative;
    vertical-align: middle;
}
.field-help-tip {
    display: none;
    position: absolute;
    left: 0;
    top: calc(100% + 6px);
    z-index: 60;
    width: max-content;
    max-width: 320px;
    padding: 10px 12px;
    background: var(--bg-panel);
    border: 1px solid var(--info);
    box-shadow: var(--shadow-hover);
    color: var(--text-main);
    font-size: 0.82rem;
    font-weight: normal;
    line-height: 1.5;
    text-transform: none;
    letter-spacing: normal;
    white-space: normal;
    text-align: left;
    cursor: auto;
}
.field-help:hover .field-help-tip,
.field-help:focus .field-help-tip { display: block; }

/* --- Shop stock table --- */
/* Colours here derive from `inherit` / currentColor rather than naming a grey.
   This table lives inside .card, which sets its own background and text colour
   together, so deriving from the card keeps the pair legible in both themes
   without these rules having to know which background they landed on. */
/* Monster attacks: an attack with notes is expandable, one without is a plain
   line — no disclosure arrow on something with nothing behind it */
.attack-entry { border-bottom: 1px solid var(--border); }
.attack-entry:last-child { border-bottom: none; }
.attack-plain, .attack-entry > summary { padding: 10px 4px; }
.attack-entry > summary { cursor: pointer; transition: color 0.2s; }
.attack-entry > summary:hover, .attack-entry[open] > summary { color: var(--gold-text); }
.attack-plain { display: flex; justify-content: space-between; align-items: baseline; gap: 15px; }
.attack-entry > summary { display: flex; justify-content: space-between; align-items: baseline; gap: 15px; }
.attack-entry > summary::-webkit-details-marker { display: none; }
.attack-entry > summary { list-style: none; }

/* A boss phase groups its attacks under a heading. Phases are sequential, so they
   render in entry order — never sorted. */
.attack-phase { margin-top: 18px; }
.attack-phase-name {
    margin: 0 0 2px;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    opacity: 0.75;
}
.attack-message { font-style: italic; opacity: 0.85; margin: 6px 0; }

.ability-entry { padding: 10px 4px; border-bottom: 1px solid var(--border); }
.ability-entry:last-child { border-bottom: none; }
.ability-effects p { margin: 4px 0 0; line-height: 1.6; }

/* Long prose kept behind a click */
.prose-details { margin: 15px 0; }
.prose-summary {
    cursor: pointer;
    font-weight: 600;
    color: var(--gold-text);
    padding: 6px 0;
    list-style: none;
}
.prose-summary::-webkit-details-marker { display: none; }
.prose-summary::after { content: " ▾"; font-size: 0.85em; }
.prose-details[open] .prose-summary::after { content: " ▴"; }
.prose-block p { margin: 6px 0; line-height: 1.7; }

/* "Any one of: A or B" — the connective words are muted so the quest names lead */
.or-label { font-style: italic; opacity: 0.7; font-size: 0.9em; }
.or-sep { opacity: 0.6; font-style: italic; }

/* Divider heading that groups admin form fields (Requirements, Rewards…). Colour
   from --text-main with a gold rule, matching the site's h2/section headings. */
.form-section-heading {
    margin: 28px 0 10px;
    padding-bottom: 6px;
    font-size: 1.05rem;
    color: var(--text-main);
    border-bottom: 2px solid var(--gold);
}
.form-section-heading:first-child { margin-top: 8px; }

/* Quest panel footer: "Open →" left, tiny correction button bottom-right */
.qt-panel-foot {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 18px;
}
.qt-correct-btn {
    margin-left: auto;
    background: none;
    border: 1px solid var(--border);
    color: var(--text-hint);
    font-family: inherit;
    font-size: 0.75rem;
    padding: 3px 9px;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.2s;
}
.qt-correct-btn:hover { border-color: var(--gold); color: var(--gold-text); }

/* --- Quest tables --- */
.qt-filter { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 16px; }
.qt-btn {
    background: none;
    border: 1px solid var(--border);
    color: inherit;
    padding: 6px 14px;
    cursor: pointer;
    font-size: 0.9rem;
    font-family: inherit;
    transition: all 0.2s;
}
.qt-btn:hover { border-color: var(--gold); color: var(--gold-text); }
.qt-btn.active { border-color: var(--gold); background: var(--gold-light); color: var(--gold-text); font-weight: 600; }

.qt-table { margin-bottom: 26px; border: 1px solid var(--border); }
.qt-table[open] > .qt-head { border-bottom: 1px solid var(--border); }
.qt-head {
    cursor: pointer;
    display: flex;
    align-items: baseline;
    gap: 12px;
    padding: 10px 14px;
    background: var(--bg-body);
    list-style: none;
}
.qt-head::-webkit-details-marker { display: none; }
/* Chevron rotates open, matching the shop groups */
.qt-head::before {
    content: "";
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform 0.2s;
    opacity: 0.6;
    align-self: center;
}
.qt-table[open] > .qt-head::before { transform: rotate(45deg); }
.qt-title { font-weight: 600; flex: 1; }
.qt-loc { font-size: 0.85rem; opacity: 0.8; }
/* Two-class selector so it beats `.field-hint { margin: 6px 0 0 }`, which is defined
   later and would otherwise zero the horizontal margin and jam the note to the edge */
.qt-table .qt-note { margin: 10px 16px 8px; }

/* Derived Location: one bullet per map, a sub-bullet naming who puts it there
   (the NPC for a shop, the collection for a quest). Kept tight so it reads as a
   small labelled list, not a wall of indentation. */
.derived-loc-list { margin: 0 0 6px; padding-left: 20px; list-style: disc; }
.derived-loc-list > li { margin: 2px 0; }
.derived-loc-sources { margin: 1px 0 4px; padding-left: 18px; list-style: circle; }
.derived-loc-sources > li { font-size: 0.9rem; opacity: 0.85; }

/* Under a temporary turn-in item: the monsters that drop it, each with a /join
   button. Circle bullets + muted so they read as a sub-list of the item above. */
.turnin-drops { margin: 1px 0 4px; padding-left: 18px; list-style: circle; }
.turnin-drops > li { font-size: 0.9rem; opacity: 0.85; }

/* ---------------- Farming Plan (preview) ---------------- */
/* --ok ("have enough" green — semantic, not the gold accent) now lives with the rest of
   the tokens at the top of this file, in both themes. */

.pl-note { font-size: 0.8rem; color: var(--text-muted); font-style: italic; margin: 0 0 14px; }
.pl-card { padding: 0; overflow: hidden; }                 /* rides the app's .card */

.pl-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px 24px; flex-wrap: wrap; padding: 18px 20px; border-bottom: 1px solid var(--border); }
.pl-target { font-size: 1.25rem; font-weight: 600; letter-spacing: .3px; color: var(--text-main); display: flex; align-items: center; gap: 10px; }
.pl-head-src { margin-top: 7px; }
.pl-controls { display: flex; gap: 18px; flex-wrap: wrap; }
.pl-switch { display: inline-flex; align-items: center; gap: 8px; font-size: .85rem; color: var(--text-main); cursor: pointer; user-select: none; }
.pl-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.pl-track { width: 34px; height: 20px; border-radius: 11px; background: var(--border); position: relative; transition: background .18s; flex: none; }
.pl-track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--bg-body); box-shadow: 0 1px 2px rgba(0,0,0,.25); transition: transform .18s; }
.pl-switch input:checked + .pl-track { background: var(--gold); }
.pl-switch input:checked + .pl-track::after { transform: translateX(14px); }
.pl-switch input:focus-visible + .pl-track { outline: 2px solid var(--gold); outline-offset: 2px; }

.pl-progress { display: flex; align-items: center; gap: 14px; padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--gold-light); }
.pl-bar { flex: 1; height: 7px; border-radius: 4px; background: var(--border); overflow: hidden; }
.pl-bar i { display: block; height: 100%; background: var(--gold); border-radius: 4px; transition: width .3s; }
.pl-pcount { font-size: .85rem; color: var(--text-muted); white-space: nowrap; }
.pl-pcount b { color: var(--text-main); font-variant-numeric: tabular-nums; }

.pl-mats { padding: 4px 10px; }
.pl-mat { border-bottom: 1px solid var(--border); }
.pl-mat:last-child { border-bottom: 0; }
.pl-row { display: flex; align-items: flex-start; gap: 11px; padding: 13px 8px; }
.pl-exp { width: 22px; height: 22px; flex: none; border: 0; background: transparent; color: var(--text-muted); cursor: pointer; border-radius: 5px; display: grid; place-items: center; margin-top: 1px; transition: transform .18s, color .15s; }
.pl-exp svg { width: 15px; height: 15px; }
.pl-mat.collapsed .pl-exp { transform: rotate(-90deg); }
.pl-exp.ghost { visibility: hidden; }
.pl-exp:hover { color: var(--text-main); }

.pl-check { width: 22px; height: 22px; flex: none; border: 1.5px solid var(--border); border-radius: 6px; background: var(--bg-body); cursor: pointer; display: grid; place-items: center; color: transparent; margin-top: 1px; transition: .15s; }
.pl-check svg { width: 13px; height: 13px; }
.pl-check:hover { border-color: var(--gold); }
.pl-check.on { background: var(--ok); border-color: var(--ok); color: #fff; }
.pl-check.manual { background: var(--gold); border-color: var(--gold); color: var(--on-gold); }
.pl-check:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

.pl-body { flex: 1; min-width: 0; }
.pl-name { font-size: .95rem; color: var(--text-main); font-weight: 500; }
.pl-name.pl-lead { font-weight: 600; }
.pl-x { color: var(--text-muted); font-weight: 600; font-variant-numeric: tabular-nums; }
.pl-ac { font-size: .62rem; font-weight: 800; letter-spacing: .04em; color: var(--gold-text); border: 1px solid var(--gold); border-radius: 4px; padding: 0 4px; }
.pl-mat.done > .pl-row .pl-name.pl-lead, .pl-sub.done .pl-name { color: var(--text-muted); text-decoration: line-through; }

.pl-src { font-size: .8rem; color: var(--text-muted); margin-top: 5px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.pl-tag { font-size: .66rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; padding: 1px 6px; border-radius: 4px; border: 1px solid var(--border); color: var(--text-hint); }
.pl-tag.merge, .pl-tag.chance { color: var(--gold-text); }
.pl-join { font-family: ui-monospace, Consolas, monospace; font-size: .75rem; background: var(--gold-light); border: 1px solid var(--border); border-radius: 5px; padding: 0 6px; color: var(--text-main); }
.pl-dim { color: var(--text-hint); }

.pl-qty { flex: none; font-family: ui-monospace, Consolas, monospace; font-size: .84rem; font-variant-numeric: tabular-nums; color: var(--text-muted); white-space: nowrap; margin-top: 2px; }
.pl-qty.ok { color: var(--ok); font-weight: 600; }
.pl-qty .pl-slash { opacity: .5; margin: 0 1px; }

.pl-branch { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 2px 8px 12px 44px; padding: 9px 11px; background: var(--gold-light); border: 1px solid var(--border); border-radius: 7px; }
.pl-branch-lab { font-size: .7rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; }
.pl-seg { display: inline-flex; gap: 3px; background: var(--bg-body); border: 1px solid var(--border); border-radius: 7px; padding: 2px; }
.pl-seg button { font: inherit; font-size: .82rem; font-weight: 500; color: var(--text-muted); cursor: pointer; border: 0; background: transparent; padding: 4px 11px; border-radius: 5px; transition: .15s; }
.pl-seg button[aria-pressed="true"] { background: var(--gold); color: var(--on-gold); }
.pl-seg button:hover:not([aria-pressed="true"]) { color: var(--text-main); }
.pl-branch-note { font-size: .8rem; color: var(--text-muted); flex: 1 1 200px; }

.pl-steps { margin: 0 8px 8px 44px; border-left: 2px solid var(--border); }
.pl-mat.collapsed .pl-steps, .pl-mat.collapsed .pl-branch { display: none; }
.pl-sub { display: flex; align-items: flex-start; gap: 11px; padding: 9px 12px; }
.pl-sub .pl-name { font-weight: 500; font-size: .9rem; }
.pl-allset { font-size: .82rem; color: var(--text-muted); padding: 8px 12px; }

/* --- Recursive real-data tree, picker & badges --- */
.pl-back { border: 0; background: transparent; color: var(--text-muted); font: inherit; font-size: .8rem; cursor: pointer; padding: 0; margin-bottom: 8px; }
.pl-back:hover { color: var(--gold-text); }
.pl-tree { padding: 4px 10px 12px; }
.pl-node > .pl-row { display: flex; align-items: flex-start; gap: 11px; padding: 9px 6px; }
.pl-node.done > .pl-row .pl-name { color: var(--text-muted); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--text-muted) 55%, transparent); }
.pl-node.collapsed > .pl-row .pl-exp { transform: rotate(-90deg); }
.pl-node.is-req > .pl-row .pl-name { color: var(--text-muted); }
.pl-kids { margin: 0 0 2px 30px; border-left: 2px solid var(--border); padding-left: 3px; }
.pl-check.ghost { visibility: hidden; }

.pl-badge { font-size: .62rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--text-hint); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px; margin-left: 6px; }
.pl-badge.ac { color: var(--gold-text); border-color: var(--gold); }
.pl-req-pill { font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-hint); background: var(--gold-light); border-radius: 10px; padding: 1px 7px; margin-left: 7px; }

.pl-tag.buy { color: var(--text-main); }
.pl-tag.drop { color: var(--gold-text); }
.pl-tag.quest, .pl-tag.npc { color: var(--ok); }
.pl-tag.map { color: var(--gold-text); }
.pl-tag.req { color: var(--text-hint); }
.pl-covered { font-size: .72rem; color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); border-radius: 9px; padding: 1px 8px; margin-left: 7px; font-weight: 600; }
.pl-cost { font-weight: 600; color: var(--text-main); }
.pl-cost.ac { color: var(--gold-text); }
.pl-alt { font-size: .74rem; color: var(--gold-text); background: var(--gold-light); border-radius: 9px; padding: 0 7px; }
.pl-muted { color: var(--text-hint); font-style: italic; }
.pl-warn, .pl-gap { color: var(--danger-text); }
/* "I own this" / membership toggle on access-item gate nodes */
.pl-own { font-size: .68rem; font-weight: 700; letter-spacing: .02em; margin-left: 8px; padding: 1px 9px; border-radius: 10px; cursor: pointer;
    border: 1px dashed var(--border); background: var(--bg-alt); color: var(--text-muted); transition: background .12s, border-color .12s, color .12s; }
.pl-own:hover { border-color: var(--ok); color: var(--ok); }
.pl-own.on { border-style: solid; border-color: var(--ok); background: color-mix(in srgb, var(--ok) 14%, transparent); color: var(--ok); }
.pl-node.is-owned > .pl-row .pl-name { color: var(--text-muted); }
.pl-owned { color: var(--ok); font-weight: 600; }
/* Why the plan moved off the #1 route (access/override) */
.pl-note { font-size: .72rem; font-weight: 600; }
.pl-note.reroute { color: var(--gold-text); }
.pl-note.warn { color: var(--danger-text); }
.pl-note.pick { color: var(--text-hint); }
/* "WIP" badge on the Farming Plan nav tab + the beta banner atop the plan view */
.wip-badge {
    display: inline-block; margin-left: 6px; padding: 0 6px;
    font-size: 0.58rem; font-weight: 800; letter-spacing: .04em; vertical-align: middle;
    color: var(--gold-text); background: var(--gold-light);
    border: 1px solid var(--gold); border-radius: 8px;
}
.pl-wip {
    display: flex; align-items: center; gap: 8px;
    margin-bottom: 16px; padding: 10px 16px;
    font-size: 0.85rem; color: var(--text-main);
    background: var(--gold-light); border: 1px solid var(--gold);
    border-radius: 10px;
}
/* BETA Testers admin panel */
.beta-msg { margin-top: 8px; font-size: 0.85rem; min-height: 1.1em; }
.beta-msg.ok { color: var(--ok); }
.beta-msg.err { color: var(--danger-text); }
/* BETA Testers list: same searchable/paged user table as Users & Roles, toggled per row */
.beta-filter { display: flex; gap: 7px; margin-bottom: 4px; flex-wrap: wrap; }
.beta-byrole { font-size: .7rem; color: var(--text-muted); font-style: italic; margin-right: 8px; }
/* Manual route switcher (Option 1) */
.pl-routewrap { margin-top: 4px; }
.pl-routebtn { font-size: .68rem; font-weight: 700; letter-spacing: .02em; padding: 1px 9px; border-radius: 10px; cursor: pointer;
    border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-muted); transition: border-color .12s, color .12s; }
.pl-routebtn:hover, .pl-routebtn.on { border-color: var(--gold); color: var(--gold-text); }
.pl-routemenu { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; padding: 6px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-card, var(--bg-alt)); max-width: 420px; }
.pl-pick { display: flex; align-items: center; gap: 8px; text-align: left; padding: 5px 8px; border-radius: 7px; cursor: pointer; border: 1px solid transparent; background: transparent; color: var(--text-main); font-size: .8rem; }
.pl-pick:hover { background: var(--gold-light); }
.pl-pick.cur { border-color: var(--gold); background: var(--gold-light); }
.pl-pick.auto { color: var(--text-muted); font-style: italic; border-bottom: 1px dashed var(--border); border-radius: 7px 7px 0 0; }
.pl-pick-kind { flex: none; font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-hint); background: var(--bg-input, var(--bg-alt)); border: 1px solid var(--border); border-radius: 6px; padding: 0 5px; min-width: 42px; text-align: center; }
.pl-pick-now { margin-left: auto; flex: none; font-size: .62rem; font-weight: 700; color: var(--gold-text); }
.pl-gap { border-bottom: 1px dotted color-mix(in srgb, var(--danger) 50%, transparent); cursor: help; }
.pl-loading { padding: 30px 20px; color: var(--text-muted); }

.pl-picker { padding: 30px 26px 34px; }
.pl-picker-h { margin: 0 0 6px; font-size: 1.2rem; color: var(--text-main); }
.pl-picker-sub { margin: 0 0 18px; color: var(--text-muted); font-size: .9rem; max-width: 58ch; line-height: 1.55; }
.pl-search { position: relative; max-width: 470px; }
.pl-search input { width: 100%; padding: 11px 14px; font: inherit; font-size: .95rem; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-body); color: var(--text-main); }
.pl-search input:focus { outline: 2px solid var(--gold); outline-offset: 1px; border-color: var(--gold); }
.pl-suggest { margin-top: 8px; display: grid; gap: 3px; }
.pl-suggest-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; text-align: left; font: inherit; font-size: .9rem; color: var(--text-main); background: var(--bg-body); border: 1px solid var(--border); border-radius: 8px; padding: 9px 12px; cursor: pointer; transition: border-color .12s, background .12s; }
.pl-suggest-row:hover { border-color: var(--gold); background: var(--gold-light); }
.pl-suggest-type { font-size: .72rem; color: var(--text-hint); flex: none; }
.pl-suggest-hint { font-size: .82rem; color: var(--text-hint); padding: 8px 2px; }

/* --- Dashboard of tracked farms --- */
.pl-dash-head { padding: 18px 20px 4px; }
.pl-dash-sub { color: var(--text-muted); font-size: .86rem; margin-top: 4px; }
.pl-dash-empty { color: var(--text-muted); font-size: .9rem; padding: 16px 20px 6px; }
.pl-tcards { display: grid; gap: 10px; padding: 12px 16px 4px; }
.pl-tcard { display: flex; align-items: stretch; gap: 8px; border: 1px solid var(--border); border-radius: 0; background: var(--bg-body); overflow: hidden; transition: border-color .12s; }
.pl-tcard:hover { border-color: var(--gold); }
.pl-tcard.done { border-color: color-mix(in srgb, var(--ok) 45%, var(--border)); }
/* Panel surface, not the cream --gold-light: this column is chrome, not an accent.
   Using the token rather than literal white keeps it coherent in the dark theme too. */
.pl-tcard-move { display: flex; flex-direction: column; justify-content: center; gap: 2px; padding: 4px; border-right: 1px solid var(--border); background: var(--bg-panel); }
.pl-mv { border: 0; background: transparent; color: var(--text-muted); cursor: pointer; font-size: .6rem; line-height: 1; padding: 3px 4px; border-radius: 4px; }
.pl-mv:hover:not(:disabled) { color: var(--gold-text); background: var(--bg-alt); }
.pl-mv:disabled { opacity: .25; cursor: default; }
.pl-tcard-body { flex: 1; min-width: 0; text-align: left; border: 0; background: transparent; cursor: pointer; padding: 12px 14px; font: inherit; color: var(--text-main); }
.pl-tcard-top { display: flex; align-items: center; gap: 9px; margin-bottom: 9px; }
.pl-tcard-name { font-weight: 600; font-size: .98rem; }
.pl-tcard-prog { display: flex; align-items: center; gap: 10px; }
.pl-tcard-prog .pl-bar { flex: 1; }
.pl-tcard-pct { font-size: .78rem; color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pl-untrack { flex: none; border: 0; background: transparent; color: var(--text-hint); cursor: pointer; font-size: 1rem; padding: 0 14px; }
.pl-untrack:hover { color: var(--danger-text); }
.pl-newplan { border-top: 1px solid var(--border); margin-top: 10px; padding: 16px 20px 22px; }
.pl-newplan-h { font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--text-hint); font-weight: 700; margin-bottom: 10px; }

.pl-trackbtn { font: inherit; font-size: .84rem; font-weight: 600; cursor: pointer; border: 1px solid var(--border); background: var(--bg-body); color: var(--text-main); border-radius: 8px; padding: 6px 12px; }
.pl-trackbtn:hover { border-color: var(--gold); }
.pl-trackbtn.on { border-color: var(--gold); color: var(--gold-text); background: var(--gold-light); }

/* --- Home mini card --- */
.pl-homecard { display: block; text-align: left; cursor: pointer; padding: 16px 18px; border-left: 3px solid var(--gold); transition: box-shadow .15s, transform .1s; }
.pl-homecard:hover { box-shadow: 0 4px 16px rgba(0,0,0,.08); }
.pl-homecard-lab { font-size: .66rem; letter-spacing: .1em; text-transform: uppercase; color: var(--gold-text); font-weight: 700; }
.pl-homecard-name { font-size: 1.05rem; font-weight: 600; margin: 5px 0 12px; display: flex; align-items: center; gap: 9px; }
.pl-homecard-cta { margin-top: 12px; font-size: .82rem; color: var(--gold-text); font-weight: 600; }

/* Tab strip — one tab per quest, like the AQW wiki. Wraps rather than scrolling,
   since a table can hold a dozen quests and a phone should still see them all. */
.qt-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 12px 14px 0;
}
.qt-tab {
    background: var(--bg-body);
    border: 1px solid var(--border);
    border-bottom: none;
    color: inherit;
    padding: 7px 14px;
    cursor: pointer;
    font-size: 0.9rem;
    font-family: inherit;
    border-radius: 4px 4px 0 0;
    position: relative;
    top: 1px;
    transition: color 0.2s, background 0.2s;
}
.qt-tab:hover { color: var(--gold-text); }
/* Active tab reads as the front sheet of the panel below — gold top edge, its
   bottom border erased so it merges into the panel */
/* The gold top edge + bold + panel background carry "active"; the LABEL stays
   --text-main, because gold text measures only 2.56:1 (a fail) — the same reason
   guide-section headings do not use gold text either */
.qt-tab.active {
    background: var(--bg-panel);
    border-color: var(--border);
    border-top: 2px solid var(--gold);
    color: var(--text-main);
    font-weight: 600;
    padding-top: 6px;
}
/* "Mark completed" checkmark on quests (table panels + individual quest pages). Shown
   only to a signed-in character; feeds the Farming Plan's completed-quest gate logic. */
.qt-panel {
    border: 1px solid var(--border);
    border-radius: 0 4px 4px 4px;
    padding: 18px;
    margin: 0 14px 14px;
}
.qt-panel h4:first-child { margin-top: 0; }

/* Requirements card, shared by the full page and the panels */
.req-card { background: var(--gold-light); padding: 20px; border-radius: 4px; margin-bottom: 20px; }

.shop-search { display: flex; align-items: center; gap: 12px; margin: 10px 0 4px; }
.shop-search input { flex: 1; margin-bottom: 0; }
.shop-search-msg { font-size: 0.85rem; opacity: 0.7; white-space: nowrap; }

.shop-group { margin: 22px 0; }
.shop-group-head {
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 10px;
    padding-bottom: 6px;
    border-bottom: 2px solid var(--gold);
    font-weight: 600;
    font-size: 1.05rem;
    transition: color 0.2s;
}
.shop-group-head:hover { color: var(--gold-text); }
/* Replace the native triangle with a chevron that rotates when open */
.shop-group-head { list-style: none; }
.shop-group-head::-webkit-details-marker { display: none; }
.shop-group-head::before {
    content: "";
    width: 6px;
    height: 6px;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform 0.2s;
    flex: 0 0 auto;
    margin-left: 2px;
}
.shop-group[open] > .shop-group-head::before { transform: rotate(45deg); }
.shop-group-name { flex: 1; }
/* Count sits right, quiet — it is orientation, not data */
.shop-group-count {
    font-size: 0.8rem;
    font-weight: 500;
    opacity: 0.65;
    border: 1px solid currentColor;
    border-radius: 999px;
    padding: 1px 9px;
}
/* A three-column table still outgrows a phone, so it scrolls inside its own box
   rather than making the whole page scroll sideways */
.shop-stock-wrap { overflow-x: auto; }
.shop-stock { width: 100%; border-collapse: collapse; font-size: 0.95rem; margin-top: 4px; }
.shop-stock tbody tr { transition: background 0.15s; }
.shop-stock tbody tr:hover { background: var(--gold-light); }
/* Muted by fading the INHERITED colour rather than naming a grey, for the same
   reason as above — a fixed grey is only safe against a known background. */
.shop-stock th {
    text-align: left;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: inherit;
    opacity: 0.7;
    border-bottom: 1px solid var(--border);
    padding: 8px 10px;
}
.shop-stock td { padding: 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
.shop-stock tr:last-child td { border-bottom: none; }
.shop-stock-price { text-align: right; white-space: nowrap; }
.shop-stock-none { color: inherit; opacity: 0.55; }
/* "(in Artix's ArchPaladin Quests)" — subordinate to the quest it qualifies.
   Faded from the inherited colour rather than a fixed grey, as elsewhere in .card */
.quest-parent { opacity: 0.75; font-size: 0.9em; }

/* Guidance under an input (image sizes and the like). Muted and small so it
   informs without competing with the label above the box. */
.field-hint {
    margin: 6px 0 0;
    font-size: 0.8rem;
    line-height: 1.5;
    color: var(--text-hint);
}
/* Anchored to a badge near the right edge it would push the page sideways */
@media (max-width: 600px) {
    .field-help { position: static; }
    .admin-form-group { position: relative; }
    .field-help-tip { left: 0; right: 0; width: auto; max-width: none; }
}

/* Who an entry is credited to, at the foot of its page */
.entry-credits {
    margin-top: 12px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* --- Submission Review (admin) --- */
.submission-card {
    border: 1px solid var(--border);
    border-left: 3px solid var(--gold);
    background: var(--bg-panel);
    padding: 18px;
    margin-bottom: 18px;
}
.submission-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 15px;
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.submission-note {
    font-style: italic;
    color: var(--text-muted);
    border-left: 2px solid var(--border);
    padding-left: 10px;
    margin: 10px 0;
}

/* --- Admin → Import (2026-09-22): the batch list reuses the submission cards above --- */
.imp-intro { margin: 0 0 12px; color: var(--text-muted); }
.imp-label { display: block; font-weight: 600; margin-bottom: 4px; }
.imp-textarea { width: 100%; box-sizing: border-box; font-family: ui-monospace, Consolas, monospace; font-size: 0.82rem; }
.imp-actions, .imp-listbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 10px 0; }
.imp-listbar { justify-content: space-between; border-top: 1px solid var(--border); padding-top: 12px; }
.imp-go { width: auto; min-width: 180px; }
.imp-link { background: none; border: none; padding: 0; cursor: pointer; color: var(--text-main); text-decoration: underline; font: inherit; }
.imp-msg { min-height: 1.2em; margin: 6px 0; color: var(--text-muted); }
.imp-msg .imp-bad, .imp-note.bad { color: var(--danger-text); }
.imp-note { margin: 6px 0; font-size: 0.85rem; color: var(--text-muted); }
.imp-card { padding: 14px 16px; margin-bottom: 12px; }
.imp-card .btn-group { margin-top: 10px; }
.imp-card.imp-add { border-left-color: var(--success); }
.imp-card.imp-fill, .imp-card.imp-update { border-left-color: var(--info); }
.imp-card.imp-nothing { border-left-color: var(--border); }
.imp-card.imp-invalid { border-left-color: var(--danger); }
.imp-card.imp-skipped { opacity: 0.6; }
.imp-badge {
    display: inline-block; margin-left: 6px; padding: 1px 8px; border-radius: 10px;
    font-size: 0.75rem; font-weight: 600; border: 1px solid currentColor;
}
.imp-badge.imp-add { color: var(--success); }
.imp-badge.imp-fill, .imp-badge.imp-update { color: var(--info); }
.imp-badge.imp-nothing, .imp-badge.imp-skip { color: var(--text-muted); }
.imp-badge.imp-invalid { color: var(--danger-text); }
.imp-badge.imp-edited { color: var(--gold-text); }
.imp-details > summary { cursor: pointer; color: var(--gold-text); font-size: 0.85rem; margin: 6px 0; }
.diff-table { margin: 12px 0; font-size: 0.9rem; }
.diff-row {
    display: grid;
    grid-template-columns: 190px 1fr;
    gap: 12px;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    align-items: start;
}
@media (max-width: 700px) { .diff-row { grid-template-columns: 1fr; gap: 2px; } }
.diff-label { color: var(--text-muted); font-size: 0.85rem; }
.diff-same { display: block; color: var(--text-main); white-space: pre-wrap; word-break: break-word; padding: 2px 0; }
.diff-more { display: block; color: var(--text-muted); font-size: 0.8rem; font-style: italic; padding: 2px 0; }
/* Original struck through in red, the user's replacement in green */
.diff-old, .diff-new {
    display: block;
    padding: 3px 6px;
    white-space: pre-wrap;
    word-break: break-word;
    border-radius: 2px;
}
.diff-old {
    background: rgba(220, 53, 69, 0.16);
    color: var(--text-main);
    text-decoration: line-through;
    text-decoration-color: rgba(220, 53, 69, 0.7);
}
.diff-new {
    background: rgba(40, 167, 69, 0.18);
    color: var(--text-main);
    margin-top: 3px;
}
/* An EDITED line (2026-09-21): only the words that differ are struck or marked, the rest reads
   normally — in a long paragraph, the one changed word is otherwise a hunt. A line that was
   replaced outright keeps the whole-line strike above. */
.diff-old.has-marks { text-decoration: none; }
.diff-old mark.dw, .diff-new mark.dw { color: inherit; border-radius: 2px; padding: 0 1px; }
.diff-old mark.dw {
    background: rgba(220, 53, 69, 0.38);
    text-decoration: line-through;
    text-decoration-color: rgba(220, 53, 69, 0.9);
}
.diff-new mark.dw { background: rgba(40, 167, 69, 0.42); }
/* A section heading in a guide's sections row: kept in view when the text around it is folded */
.diff-same.diff-anchor { font-weight: 600; }

/* --- Class Skills (collapsible list) --- */
.skill-list { border-top: 1px solid var(--border); margin-top: 10px; }
.skill-entry { border-bottom: 1px solid var(--border); }
.skill-entry summary {
    cursor: pointer;
    padding: 12px 4px;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 15px;
    transition: color 0.2s;
}
.skill-entry summary:hover,
.skill-entry[open] summary { color: var(--gold-text); }
.skill-meta { font-size: 0.85rem; color: var(--text-muted); white-space: nowrap; }

/* Icon + name group, so space-between still pushes the meta line to the right */
.skill-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
.skill-icon {
    flex: 0 0 28px;
    width: 28px;
    height: 28px;
    display: block;
}
.skill-icon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.skill-body { padding: 0 4px 15px; font-size: 0.95rem; }
.skill-body p { margin: 6px 0; }

/* Buff / debuff mentioned inside a skill description — dotted underline invites
   the hover that reveals what it does */
.buff-ref {
    color: var(--info);
    border-bottom: 1px dotted var(--info);
    cursor: help;
    position: relative;
    font-weight: 500;
}
.buff-tip {
    display: none;
    position: absolute;
    left: 0;
    top: calc(100% + 6px);
    z-index: 60;
    min-width: 220px;
    max-width: 340px;
    padding: 10px 12px;
    background: var(--bg-panel);
    border: 1px solid var(--info);
    box-shadow: var(--shadow-hover);
    color: var(--text-main);
    font-size: 0.85rem;
    font-weight: normal;
    line-height: 1.5;
    white-space: normal;
    text-align: left;
}
/* focus as well as hover, so it is reachable by keyboard */
.buff-ref:hover .buff-tip,
.buff-ref:focus .buff-tip { display: block; }
.buff-tip span { display: block; }
.buff-tip span + span { margin-top: 4px; }
/* Anchored to the word, a tooltip near the right edge would push the page sideways
   on a phone, so there it spans the skill body instead */
@media (max-width: 600px) {
    .buff-ref { position: static; }
    .skill-body { position: relative; }
    .buff-tip { left: 0; right: 0; min-width: 0; max-width: none; }
}

/* Skill editor (admin) */
.skill-stats { display: flex; gap: 10px; flex-wrap: wrap; }
.skill-stats .skill-field { flex: 1; min-width: 110px; }
.skill-field input, .skill-field select, .skill-field textarea { margin-bottom: 0; }
/* Path box + live thumbnail. The input must flex or the attribute-selector width
   rule (see input[type=...]) stretches it past the preview. */
.skill-icon-row { display: flex; align-items: center; gap: 10px; }
.skill-icon-row input[type="text"] { flex: 1; min-width: 0; margin-bottom: 0; }
.skill-icon-preview {
    flex: 0 0 32px;
    width: 32px;
    height: 32px;
    object-fit: contain;
    border: 1px solid var(--border);
    background: var(--bg-body);
}

/* --- Acquisition Methods (Admin Forms) --- */
.acq-method {
    border: 1px solid var(--border);
    background: var(--bg-body);
    padding: 12px;
    margin-bottom: 10px;
}
.acq-method-head {
    display: flex;
    gap: 8px;
    align-items: center;
    margin-bottom: 10px;
}
.acq-method-head select {
    flex: 1;
    margin-bottom: 0;
    padding: 8px;
    font-weight: 600;
}
.acq-default-badge {
    font-size: 0.66rem; font-weight: 700; letter-spacing: .02em; white-space: nowrap;
    color: var(--gold-text); background: var(--gold-light); border: 1px solid var(--gold);
    border-radius: 10px; padding: 1px 8px; flex: none;
}
.acq-method-body label {
    display: block;
    font-size: 0.8rem;
    color: var(--text-muted);
    margin: 8px 0 4px;
}
.acq-row {
    display: flex;
    gap: 8px;
    align-items: center;
    margin-bottom: 8px;
}
.acq-row input,
.acq-row select {
    margin-bottom: 0;
}
/* The global input rule uses attribute selectors (input[type="number"]), which
   outrank a bare class — these need the extra element specificity to win, or the
   fixed-width boxes take 100% and collapse the field beside them. */
.acq-row .acq-row-name {
    position: relative;
    flex: 1 1 0%;
    min-width: 0;
}
.acq-row .acq-row-name input {
    width: 100%;
}
.acq-row input.acq-row-cost {
    flex: 1 1 0%;
    width: auto;
    min-width: 0;
}
.acq-row input.acq-row-qty {
    flex: 0 0 90px;
    width: 90px;
}
.acq-row select.acq-row-currency {
    flex: 0 0 110px;
    width: 110px;
}
/* Farming-priority box, leftmost on a source row. Empty until clicked; once ranked it
   shows its 1-based position across all methods and turns gold. */
.acq-order-box {
    flex: 0 0 28px;
    width: 28px;
    height: 28px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8rem;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    border: 1px dashed var(--border);
    border-radius: 6px;
    background: var(--bg-input, var(--bg-alt));
    color: var(--text-muted);
    transition: background .12s, border-color .12s, color .12s;
}
.acq-order-box:hover {
    border-color: var(--gold);
    color: var(--gold-text);
}
.acq-order-box.set {
    border-style: solid;
    border-color: var(--gold);
    background: var(--gold-light);
    color: var(--gold-text);
}

/* Copies "/join <map>" for anything you can travel to, beside its link */
.copy-join {
    background: none;
    border: none;
    padding: 0 4px;
    margin-left: 2px;
    cursor: pointer;
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1;
    vertical-align: baseline;
    transition: color 0.2s;
}
.copy-join:hover { color: var(--gold-text); }
.copy-join.copied { color: var(--success); }

/* --- Reputation Rows (Admin Forms) --- */
.rep-row {
    display: flex;
    gap: 8px;
    align-items: center;
    margin-bottom: 8px;
}
.rep-row-faction {
    position: relative;
    flex: 1;
}
.rep-row-faction input {
    margin-bottom: 0;
}
.rep-row select.rep-row-rank {
    width: 110px;
    flex-shrink: 0;
    margin-bottom: 0;
    padding: 10px;
}
.rep-row-remove {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-muted);
    font-size: 1.2rem;
    font-weight: bold;
    line-height: 1;
    padding: 5px;
}
.rep-row-remove:hover {
    color: var(--danger-text);
}
.rep-add-btn {
    background: none;
    border: 1px dashed var(--border);
    cursor: pointer;
    color: var(--text-muted);
    padding: 6px 14px;
    font-size: 0.85rem;
    margin-top: 4px;
}
.rep-add-btn:hover {
    border-color: var(--gold);
    color: var(--gold-text);
}

/* Image upload field: file button + status + thumbnail preview */
.image-upload-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 4px;
}
.image-upload-status {
    font-size: 0.8rem;
    color: var(--text-muted);
}
.image-upload-preview {
    display: block;
    max-width: 220px;
    max-height: 130px;
    margin-top: 8px;
    border: 1px solid var(--border);
    border-radius: 4px;
    object-fit: contain;
    background: var(--bg-body);
}
/* ---------- PROFILE SYSTEM ---------- */
/* Public profile page (#view-user) + the self editor and people search on the dashboard */
.pf-avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; flex: none; font-size: 1.15rem; line-height: 1;
    border-radius: 50%; background: var(--gold-light); border: 1px solid var(--border);
}
.pf-avatar.big { width: 72px; height: 72px; font-size: 2.1rem; }
.pf-card { border-top: 4px solid var(--gold); }
.pf-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.pf-ident { flex: 1 1 auto; min-width: 0; }
.pf-name { margin: 0; border: none; padding: 0; font-size: 1.6rem; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pf-joined { color: var(--text-muted); font-size: .85rem; margin-top: 3px; }

/* The profile's two Party Finder figures — runs finished, and the Likeness score (2026-08-30).
   They sit as a PAIR: before this, completions rendered as an unstyled div, and putting a
   scored figure beside a bare line of text would have read as one stat and one afterthought. */
.pf-stats { display: flex; align-items: baseline; gap: 20px; flex-wrap: wrap; margin-top: 8px; }
.pf-stat { display: inline-flex; align-items: baseline; gap: 6px; font-size: .85rem;
    color: var(--text-muted); }
.pf-stat-n { font-size: 1.05rem; font-weight: 700; color: var(--text-main); }
.pf-stat-sub { font-size: .76rem; color: var(--text-hint); }
/* Only the NUMBER takes the colour. Tinting the whole stat would make a disliked player's
   profile read as an error state, which is a heavier judgement than either figure is.
   Shared by Likeness (what people generally make of them) and Affinity (how they get on with
   the reader): two different questions, but the same good/bad/neutral reading. */
.pf-likeness.good .pf-stat-n, .pf-affinity.good .pf-stat-n { color: var(--ok); }
.pf-likeness.bad  .pf-stat-n, .pf-affinity.bad  .pf-stat-n { color: var(--danger-text); }
.pf-likeness.flat .pf-stat-n { color: var(--text-muted); }
.pf-role { font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
    padding: 2px 8px; border-radius: 10px; border: 1px solid var(--gold); color: var(--gold-text); background: var(--gold-light); }
.pf-role.newname { border-color: var(--info); color: var(--info); background: color-mix(in srgb, var(--info) 12%, transparent); }
.pf-role.admin { border-color: var(--danger); color: var(--danger-text); background: color-mix(in srgb, var(--danger) 12%, transparent); }
/* The bio is the player talking, so it gets a speech bubble whose tail points up
   at their avatar (72px wide, so its centre is 36px in). The tail is two stacked
   triangles: the outer one is the border, the inner one is a pixel smaller and
   nudged down so the fill punches through and the outline reads unbroken. Both
   reuse --bg-alt/--border, which are opaque in every theme, so the tail can
   never sit a shade off the body it grows out of. */
.pf-bio {
    position: relative;
    /* A bubble the width of the card would be mostly empty air for a one-line bio, so it
       shrinks to its longest line. min-width keeps a two-character bio wider than the tail
       is far along, so the tail can never poke out past the corner it grows from. */
    width: fit-content;
    max-width: 100%;
    min-width: 62px;
    margin: 26px 0 0;
    padding: 12px 16px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg-alt);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    line-height: 1.6;
}
.pf-bio::before, .pf-bio::after { content: ''; position: absolute; bottom: 100%; width: 0; height: 0; border: 9px solid transparent; }
.pf-bio::before { left: 27px; border-bottom-color: var(--border); }
.pf-bio::after { left: 28px; margin-bottom: -1px; border-width: 8px; border-bottom-color: var(--bg-alt); }
.pf-empty { color: var(--text-muted); font-style: italic; margin: 14px 0 0; }

/* Friends in common (2026-08-30). A row of faces rather than a sentence: the names are the
   payload, and a reader scanning a stranger's profile recognises a face faster than they read
   a list. Wraps rather than scrolls — this sits mid-page, and a horizontal scroller buried in
   a profile is a control nobody finds. */
.pf-mutuals { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 16px 0 0; }
.pf-mutuals-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-muted); }
.pf-mutual { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
    padding: 3px 10px 3px 3px; border: 1px solid var(--border); border-radius: 999px;
    background: var(--bg-alt); color: var(--text-main); font-size: .8rem;
    transition: border-color .12s, color .12s; }
.pf-mutual:hover { border-color: var(--gold); color: var(--gold-text); }
.pf-mutual-name { white-space: nowrap; }
.pf-mutuals-more { font-size: .78rem; color: var(--text-hint); }
.pf-error { color: var(--danger-text); margin: 0; }
.pf-sub { margin: 26px 0 10px; font-size: 1rem; }
/* A plain list: one character or guild per line, no box around it. These are FACTS about
   an account, not controls — a border invites a click that most of them do not accept, and
   boxing each line was what produced the stretched rows and then the odd little chips. The
   guild NAME is still a link because it genuinely navigates; nothing else pretends to. */
.pf-chars { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
.pf-char { display: flex; align-items: center; gap: 8px; padding: 3px 0; flex-wrap: wrap; }
/* Inside this list the rank is plain text — the bordered plate belongs to the guild lists,
   where it sits on a busy row and has to hold its own. */
.pf-char .gd-rank { border: none; padding: 0; }
.pf-char-name { font-weight: 600; }
/* --text-hint, not --text-muted: this is small print, and --text-muted measured 3.3:1 on
   the chip background in light mode. That is precisely the split the two tokens exist for. */
.pf-char-lvl { font-size: .8rem; color: var(--text-hint); }
/* The tick alone, next to the name. `margin-left: auto` is what used to shove the old
   "verified" pill to the far edge and open the gap — the mark means the same thing without
   the word, and the title attribute carries the explanation for anyone who needs it. */
.pf-verified { display: inline-flex; color: var(--ok); }
.pf-verified svg { width: 15px; height: 15px; }
.pf-editbtn, .pf-viewbtn { background: none; border: 1px solid var(--border); color: var(--text-muted);
    border-radius: 8px; padding: 7px 14px; cursor: pointer; font-size: .82rem; }
.pf-editbtn:hover, .pf-viewbtn:hover { border-color: var(--gold); color: var(--gold-text); }

/* Self editor */
.pf-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; }
.pf-editor-head h3 { margin: 0; }
.pf-label { display: block; font-size: .8rem; color: var(--text-muted); margin: 16px 0 6px; }
.pf-count { float: right; font-size: .74rem; }
.pf-picker { display: flex; flex-wrap: wrap; gap: 7px; }
.pf-pick { width: 38px; height: 38px; font-size: 1.15rem; line-height: 1; cursor: pointer;
    border: 1px solid var(--border); border-radius: 8px; background: var(--bg-alt); }
/* Same rule as .card: highlight, never move. */
.pf-pick:hover { border-color: var(--gold); }
.pf-pick.on { border-color: var(--gold); background: var(--gold-light); box-shadow: 0 0 0 2px var(--gold-light); }
.pf-bio-input { width: 100%; resize: vertical; }
.pf-editor-actions { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
.pf-save { background: var(--gold); color: var(--on-gold); border: none; border-radius: 8px; padding: 9px 22px; cursor: pointer; font-weight: 600; }
.pf-save:hover { filter: brightness(1.06); }
.pf-saved { color: var(--ok); font-size: .84rem; font-weight: 600; }

/* People search */
.pf-results { margin-top: 12px; display: flex; flex-direction: column; gap: 7px; }
.pf-hint { color: var(--text-muted); font-size: .86rem; font-style: italic; }
.pf-result { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer;
    padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-alt); }
.pf-result:hover { border-color: var(--gold); background: var(--gold-light); }
.pf-result-main { display: flex; flex-direction: column; min-width: 0; }
.pf-result-title { font-weight: 600; }
.pf-result-lvl { font-weight: 400; font-size: .78rem; color: var(--text-muted); }
.pf-result-sub { font-size: .76rem; color: var(--text-muted); }
.pf-gate { border-left: 2px solid var(--gold); }
.pf-gate h3 { margin-top: 0; }
.pf-gate p { color: var(--text-muted); margin: 0 0 14px; }

/* ---------- CHARACTER VERIFICATION ---------- */
.char-verified { color: var(--ok); font-weight: 700; margin-left: 4px; }
.vf-wrap { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); }
.vf-title { margin: 0 0 6px; font-size: 1rem; }
.vf-intro { color: var(--text-muted); font-size: .88rem; margin: 0 0 14px; }
.vf-rejected { border: 1px solid var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent);
    color: var(--danger-text); border-radius: 8px; padding: 10px 12px; font-size: .86rem; margin-bottom: 12px; }
/* Room for the dismiss control, which sits at the end of the line rather than under it —
   the notice is one sentence and stacking a button below it would double its height. */
.vf-rejected { display: flex; align-items: flex-start; gap: 10px; }
.vf-rejected > span { flex: 1; }
.vf-rejected-x {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    padding: 2px; margin: -2px -2px 0 0; cursor: pointer;
    background: none; border: none; color: var(--text-muted); border-radius: var(--r-sm);
}
.vf-rejected-x:hover { color: var(--danger-text); }
.vf-pending { border: 1px solid var(--gold); border-radius: 10px; padding: 14px; margin-bottom: 14px; background: var(--bg-alt); }
.vf-pending-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.vf-char { font-weight: 700; }
.vf-status { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    color: var(--gold-text); background: var(--gold-light); border-radius: 10px; padding: 2px 9px; }
.vf-codebox { display: flex; align-items: center; gap: 10px; margin: 12px 0; flex-wrap: wrap; }
.vf-code-label { font-size: .74rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; }
.vf-code { font-size: 1.15rem; font-weight: 700; letter-spacing: .08em; padding: 5px 12px;
    border: 1px dashed var(--gold); border-radius: 8px; background: var(--gold-light); color: var(--gold-text); }
.vf-copy { border: 1px solid var(--border); background: none; border-radius: 6px; cursor: pointer;
    width: 30px; height: 30px; color: var(--text-muted); }
.vf-copy:hover { border-color: var(--gold); color: var(--gold-text); }
.vf-how p { margin: 6px 0; font-size: .86rem; color: var(--text-muted); line-height: 1.55; }
.vf-how code { background: var(--bg-body); border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; }
/* The ready-to-paste whisper command + which server to send it on */
.vf-cmdrow { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; margin-top: 10px; }
.vf-cmd { font-weight: 600; letter-spacing: .01em; padding: 7px 12px; border-radius: 8px;
    border: 1px solid var(--gold); background: var(--bg-body); color: var(--text-main);
    overflow-wrap: anywhere; }
.vf-server { font-size: .82rem; color: var(--text-muted); white-space: nowrap; margin-left: 4px; }
.vf-server strong { color: var(--gold-text); }
.vf-cancel { margin-top: 8px; background: none; border: none; color: var(--text-muted);
    text-decoration: underline; cursor: pointer; font-size: .82rem; padding: 0; }
.vf-cancel:hover { color: var(--danger-text); }
.vf-request-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.vf-request-row select { margin-bottom: 0; flex: 1 1 200px; }

/* Moderator claim queue */
.vq-tabs { display: flex; gap: 7px; margin-bottom: 14px; flex-wrap: wrap; }
.vq-tab { text-transform: capitalize; padding: 5px 14px; border-radius: 20px; cursor: pointer;
    border: 1px solid var(--border); background: none; color: var(--text-muted); font-size: .82rem; }
.vq-tab:hover { border-color: var(--gold); color: var(--gold-text); }
.vq-tab.on { border-color: var(--gold); background: var(--gold-light); color: var(--gold-text); font-weight: 600; }
.vq-list { display: flex; flex-direction: column; gap: 10px; }
.vq-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
    border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; background: var(--bg-alt); }
.vq-main { flex: 1 1 240px; min-width: 0; }
.vq-char { font-weight: 700; font-size: 1.02rem; }
.vq-meta { font-size: .8rem; color: var(--text-muted); margin-top: 2px; }
.vq-user { background: none; border: none; padding: 0; cursor: pointer; color: var(--gold-text); text-decoration: underline; font: inherit; }
.vq-note { font-size: .84rem; margin-top: 5px; font-style: italic; }
.vq-code { font-weight: 700; letter-spacing: .06em; padding: 5px 11px; border-radius: 8px;
    border: 1px dashed var(--gold); background: var(--gold-light); color: var(--gold-text); white-space: nowrap; }
.vq-actions { display: flex; gap: 8px; }
.vq-approve, .vq-reject { border: none; border-radius: 8px; padding: 8px 16px; cursor: pointer; font-weight: 600; color: #fff; }
.vq-approve { background: var(--ok); }
.vq-reject { background: var(--danger); }
.vq-approve:hover, .vq-reject:hover { filter: brightness(1.08); }
.vq-status { text-transform: capitalize; font-size: .78rem; color: var(--text-muted); font-weight: 600; }
/* Moderator-editable in-game account/server for verification */
.vq-settings { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; margin-bottom: 18px; background: var(--bg-alt); }
.vq-settings-head { font-weight: 700; font-size: .85rem; margin-bottom: 10px; }
.vq-settings-hint { display: block; font-weight: 400; font-size: .78rem; color: var(--text-muted); margin-top: 2px; }
.vq-settings-row { display: flex; align-items: flex-end; gap: 12px; flex-wrap: wrap; }
.vq-settings-row label { display: flex; flex-direction: column; gap: 4px; font-size: .78rem; color: var(--text-muted); }
.vq-settings-row input { margin-bottom: 0; width: 190px; }
.vq-settings-msg { font-size: .8rem; font-weight: 600; }
.vq-settings-msg.good { color: var(--ok); }
.vq-settings-msg.bad { color: var(--danger-text); }

/* ---------------- Icon set (js/icons.js) ---------------- */
/* Icons inherit colour from their parent and sit on the text baseline. Size is set by the
   component, never on the element, so one icon serves several contexts. */
.icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.14em; flex: none; }
.icon-sm { width: 13px; height: 13px; }
.icon-md { width: 16px; height: 16px; }
.icon-lg { width: 20px; height: 20px; }
/* A button that is ONLY an icon: square, centred, no text metrics to fight. */
.icon-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    cursor: pointer; background: none; border: 1px solid transparent; color: inherit;
    border-radius: var(--r-sm); padding: 5px; line-height: 0; transition: color .12s, border-color .12s, background .12s; }
.icon-btn:hover { color: var(--gold-text); }

/* ---------------- Avatars (js/avatars.js) ---------------- */
.av { width: 38px; height: 38px; border-radius: 50%; display: inline-flex; align-items: center;
    justify-content: center; flex: none; border: 1px solid var(--border); background: var(--bg-alt);
    overflow: hidden; }
.av svg { width: 60%; height: 60%; }
.av-sigil { color: var(--gold-text); border-color: var(--border-gold); background: var(--gold-light); }
/* Game art. `.av` already clips to a circle with overflow:hidden, so the image only has to
   fill it; `cover` keeps a non-square source from squashing. */
.av-art { padding: 0; border-color: var(--border); background: var(--bg-alt); }
.av-art img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* A framed avatar: the ornament sits OVER the picture and is bigger than it, the way these
   frames are drawn. The wrapper is the size of the avatar so surrounding layout is unchanged
   — the frame overhangs on all four sides without pushing anything around, which is why it is
   absolutely positioned and the wrapper is not. */
.av-framed { position: relative; display: inline-flex; flex: none; }
/* `.av` carries a 1px border on the default content-box, so a `width: 100%` avatar renders 2px
   LARGER than the wrapper it is filling — and the frame, which centres on the wrapper, then
   sat 1px up and left of the avatar's real centre. Border-box makes the two boxes identical,
   which is what "concentric" requires. */
.av-framed .av { box-sizing: border-box; }
.av-frame {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: 152%; height: 152%; object-fit: contain;
    /* Decorative and NEVER clickable: the avatar underneath is usually a button, and an image
       laid over it would swallow every click aimed at it. */
    pointer-events: none;
    /* Above the avatar, below anything the app floats over the page. */
    z-index: 1;
}
/* Monogram: hue is set per-user from the username, so it stays stable across the app.
   Both themes read the same --av-h; only the lightness differs. */
.av-mono { font-weight: 700; font-size: 0.95rem; letter-spacing: .01em;
    color: hsl(var(--av-h, 40) 45% 32%); background: hsl(var(--av-h, 40) 45% 93%);
    border-color: hsl(var(--av-h, 40) 40% 82%); }
[data-theme="dark"] .av-mono { color: hsl(var(--av-h, 40) 55% 74%);
    background: hsl(var(--av-h, 40) 30% 15%); border-color: hsl(var(--av-h, 40) 28% 28%); }
.av.big { width: 72px; height: 72px; }
.av.big.av-mono { font-size: 1.9rem; }
.av.sm { width: 28px; height: 28px; }
.av.sm.av-mono { font-size: 0.78rem; }

/* Picker */
.av-picker { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 6px; }
.av-pick { width: 40px; height: 40px; padding: 0; cursor: pointer; border-radius: var(--r-md);
    border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-muted);
    display: inline-flex; align-items: center; justify-content: center; transition: all .12s; }
.av-pick svg { width: 22px; height: 22px; }
.av-pick:hover { border-color: var(--gold); color: var(--gold-text); }
.av-pick.on { border-color: var(--gold); color: var(--gold-text); background: var(--gold-light);
    box-shadow: 0 0 0 2px var(--gold) inset; }
.av-pick.av-clear { font-size: .68rem; font-weight: 700; width: auto; padding: 0 10px; }

/* ---------------- Friends (js/friends.js) ---------------- */
.fr-card { margin-bottom: 22px; }
.fr-h { display: flex; align-items: center; gap: 8px; margin: 0 0 14px; font-size: 1.02rem; }
.fr-h svg { color: var(--gold-text); }
.fr-count { font-size: .72rem; font-weight: 700; color: var(--gold-text); background: var(--gold-light);
    border: 1px solid var(--border-gold); border-radius: 10px; padding: 1px 8px; }
.fr-hint { font-size: .82rem; color: var(--text-muted); margin: -6px 0 12px; }
.fr-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--border);
    border-radius: var(--r-md); background: var(--bg-alt); margin-bottom: 8px; }
.fr-row.compact { padding: 6px 8px; margin-bottom: 6px; }
.fr-person { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; text-align: left;
    background: none; border: none; cursor: pointer; color: inherit; padding: 0; font: inherit; }
.fr-person.static { cursor: default; }
.fr-person:hover .fr-name { color: var(--gold-text); }
.fr-text { display: flex; flex-direction: column; min-width: 0; }
.fr-name { font-weight: 600; font-size: .92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fr-sub { font-size: .76rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fr-lvl { opacity: .8; }
.fr-more { color: var(--gold-text); }
.fr-nochar { font-style: italic; opacity: .75; }
.fr-actions { display: flex; gap: 6px; flex: none; }
.fr-btn { display: inline-flex; align-items: center; gap: 5px; font-size: .76rem; font-weight: 600;
    padding: 5px 10px; border-radius: var(--r-sm); cursor: pointer; border: 1px solid var(--border);
    background: var(--bg-panel); color: var(--text-main); transition: all .12s; }
.fr-btn:hover { border-color: var(--gold); color: var(--gold-text); }
.fr-btn.ok { border-color: var(--ok); color: var(--ok); }
.fr-btn.ok:hover { background: color-mix(in srgb, var(--ok) 14%, transparent); }
.fr-btn.danger:hover { border-color: var(--danger); color: var(--danger-text); }
.fr-btn.ghost { color: var(--text-muted); }

/* Drawer — pinned to the right edge, above the page, on every view */
.fr-drawer { position: fixed; top: var(--nav-h); right: 0; height: calc(100vh - var(--nav-h)); z-index: 900;
    display: flex; align-items: flex-start; pointer-events: none; }
.fr-drawer .fr-handle, .fr-drawer .fr-drawer-body { pointer-events: auto; }
.fr-handle { position: relative; top: 20px; display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 10px 7px; cursor: pointer; color: var(--text-muted);
    background: var(--bg-float); border: 1px solid var(--border); border-right: none;
    border-radius: var(--r-md) 0 0 var(--r-md); box-shadow: var(--shadow-card); transition: color .12s, border-color .12s; }
.fr-handle:hover { color: var(--gold-text); border-color: var(--border-gold); }
.fr-handle-count { font-size: .68rem; font-weight: 700; }
/* The body is the CLIPPING box; `.fr-drawer-inner` holds the content at a fixed width so it
   slides out from behind the edge instead of squashing. Without the fixed inner width the
   head, list and add-box would reflow through every intermediate width of the transition,
   which reads as text thrashing rather than a panel sliding. */
.fr-drawer-body { width: 0; overflow: hidden; height: 100%; background: var(--bg-float);
    border-left: 1px solid var(--border); box-shadow: var(--shadow-card);
    transition: width .2s cubic-bezier(.22,.61,.36,1); }
.fr-drawer.open .fr-drawer-body { width: 270px; }
.fr-drawer-inner { width: 270px; height: 100%; display: flex; flex-direction: column; }
.fr-drawer.open .fr-handle { display: none; }
.fr-drawer-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
    padding: 14px 12px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: .9rem; }
.fr-drawer-head svg { color: var(--gold-text); vertical-align: -0.18em; }
.fr-drawer-req { margin: 10px 10px 0; padding: 8px; font-size: .78rem; font-weight: 600; cursor: pointer;
    color: var(--gold-text); background: var(--gold-light); border: 1px solid var(--border-gold); border-radius: var(--r-sm); }
.fr-drawer-list { flex: 1; overflow-y: auto; padding: 10px; }
.fr-drawer-list .fr-row { background: none; border-color: transparent; }
.fr-drawer-list .fr-row:hover { border-color: var(--border); background: var(--bg-alt); }
.fr-drawer-all { margin: 0 10px 12px; padding: 8px; font-size: .78rem; cursor: pointer; color: var(--text-muted);
    background: none; border: 1px dashed var(--border); border-radius: var(--r-sm); }
.fr-drawer-all:hover { color: var(--gold-text); border-color: var(--gold); }
/* On a phone the panel takes the screen rather than squeezing the page */
@media (max-width: 700px) {
    .fr-drawer.open .fr-drawer-body { width: 100vw; }
    .fr-drawer-inner { width: 100vw; }
    .fr-handle { top: 14px; }
}
.pf-actions { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; }

/* ---------------- Notification bell (js/notifications.js) ---------------- */
.notif-bell { display: inline-flex; }
.notif-btn { position: relative; display: inline-flex; align-items: center; justify-content: center;
    width: 38px; height: 38px; padding: 0; cursor: pointer; color: var(--text-muted);
    background: none; border: 1px solid transparent; border-radius: 50%; transition: color .12s, border-color .12s; }
.notif-btn:hover { color: var(--gold-text); border-color: var(--border-gold); }
.notif-dot { position: absolute; top: 1px; right: 0; min-width: 16px; height: 16px; padding: 0 4px;
    font-size: .62rem; font-weight: 700; line-height: 16px; text-align: center; color: #fff;
    background: var(--danger); border-radius: 9px; }
/* Body-level and fixed. NOT inside <nav>: raising nav's z-index would cover the friends
   drawer's own header + close button (.fr-drawer sits at z-index 900). */
.notif-panel { position: fixed; top: calc(var(--nav-h) + 6px); right: 16px; width: 320px; max-width: calc(100vw - 24px);
    max-height: 70vh; overflow: hidden; display: flex; flex-direction: column; z-index: 950;
    background: var(--bg-float); border: 1px solid var(--border); border-radius: var(--r-lg);
    box-shadow: var(--shadow-card); }
.notif-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
    padding: 12px 14px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: .88rem; }
.notif-head svg { color: var(--gold-text); vertical-align: -0.18em; }
.notif-readall { font-size: .74rem; font-weight: 600; cursor: pointer; color: var(--text-muted);
    background: none; border: none; text-decoration: underline; }
.notif-readall:hover { color: var(--gold-text); }
.notif-list { overflow-y: auto; padding: 6px; }
.notif-row { display: block; width: 100%; text-align: left; cursor: pointer; padding: 9px 10px;
    background: none; border: 1px solid transparent; border-radius: var(--r-sm); color: inherit; font: inherit; }
.notif-row:hover { background: var(--bg-alt); border-color: var(--border); }
.notif-row.unread { background: var(--gold-light); border-color: var(--border-gold); }
.notif-text { display: block; font-size: .84rem; line-height: 1.4; }
.notif-when { display: block; font-size: .72rem; color: var(--text-muted); margin-top: 3px; }
.notif-empty { padding: 18px; text-align: center; color: var(--text-muted); font-size: .84rem; }
.notif-more { margin: 6px; padding: 8px; font-size: .78rem; cursor: pointer; color: var(--text-muted);
    background: none; border: 1px dashed var(--border); border-radius: var(--r-sm); }
.notif-more:hover { color: var(--gold-text); border-color: var(--gold); }
/* Queue depth on the admin Review buttons + the Admin nav button */
.tab-count { display: inline-block; min-width: 16px; margin-left: 6px; padding: 0 5px; font-size: .62rem;
    font-weight: 700; line-height: 16px; text-align: center; color: #fff; background: var(--danger); border-radius: 9px; }
@media (max-width: 700px) { .notif-panel { right: 8px; width: auto; left: 8px; } }
.rp-row .rp-kind { font-size: .66rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
    border: 1px solid var(--border); border-radius: 8px; padding: 0 6px; margin-left: 6px; }
.rp-repeat { font-size: .66rem; font-weight: 700; color: var(--danger-text); background: color-mix(in srgb, var(--danger) 12%, transparent);
    border-radius: 8px; padding: 1px 7px; margin-left: 6px; }
/* Honest warning when the expander hit its node/depth budget */
.pl-truncated { display: flex; align-items: flex-start; gap: 7px; margin: 0 0 16px; padding: 9px 12px;
    font-size: .82rem; line-height: 1.45; color: var(--danger-text);
    background: color-mix(in srgb, var(--danger) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent); border-radius: var(--r-sm); }
.pl-truncated svg { flex: none; margin-top: 2px; }

/* ---------------- Checkable state: one shared language ---------------- */
/* The quest "Mark completed" toggle and the BETA "Tester" badge both say the same thing —
   this thing is checked — so they share a box instead of each inventing its own.
   They follow the APP's accent, not a semantic green: every other selected/active state
   here is gold (.database-cat-btn.active, .acq-order-box.set, .av-pick.on), and --gold becomes
   blood-red in the dark theme, so the control changes with it. Corners are SQUARE to match
   the buttons they sit beside (.action-btn is border-radius: 0). */
.aq-check {
    display: inline-flex; align-items: center; justify-content: center;
    width: 17px; height: 17px; flex: none;
    border: 1.5px dashed currentColor; border-radius: 0;
    background: transparent;
    /* NO colour transition here on purpose. These colours come from var(--gold), and
       Chromium does not re-run a transition when only the underlying custom property
       changes — a transitioned background FROZE at whatever the theme was on first paint
       and never followed a light/dark switch. Only the tick's opacity animates. */
}
.aq-check svg { width: 11px; height: 11px; opacity: 0; transition: opacity .12s; }
/* Checked — the app's active pattern: solid accent fill, white mark. */
.aq-toggle.on > .aq-check, .aq-badge > .aq-check {
    border-style: solid; border-color: var(--gold);
    background: var(--gold); color: var(--on-gold);
}
.aq-toggle.on > .aq-check svg, .aq-badge > .aq-check svg { opacity: 1; }

/* Interactive: the quest completion toggle */
.aq-toggle {
    display: inline-flex; align-items: center; gap: 8px;
    margin: 0 0 12px; padding: 5px 12px 5px 6px;
    font-size: .8rem; font-weight: 600; cursor: pointer;
    border: 1px solid var(--border); border-radius: 0;
    background: var(--bg-panel); color: var(--text-muted);
    /* No colour transition, for the same reason as .aq-check above: `color: var(--gold-text)` on
       the .on state has an unchanging DECLARED value, so a theme switch only changes the
       variable — which does not re-run the transition, and the text froze at the previous
       theme's accent. Hover snaps instead of fading; correctness beats 200ms of easing. */
}
.aq-toggle:hover { border-color: var(--gold); color: var(--gold-text); }
.aq-toggle:hover .aq-check { border-style: solid; }
.aq-toggle.on { border-color: var(--gold); color: var(--gold-text); background: var(--gold-light); }

/* Static: a "this is true" badge (BETA Tester). Sits beside the USER/ADMIN .tag chips, so
   it takes their uppercase treatment and square corners rather than reading as a pill. */
.aq-badge {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 2px 9px 2px 3px; margin-right: 8px;
    font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
    color: var(--gold-text);
    border: 1px solid var(--border-gold); border-radius: 0;
    background: var(--gold-light);
}
.aq-badge .aq-check { width: 14px; height: 14px; border-width: 1px; }
.aq-badge .aq-check svg { width: 9px; height: 9px; }

/* ---------------- Notification panel: You / Queues tabs (moderators) ---------------- */
.notif-tabs { display: flex; gap: 6px; padding: 8px 10px 0; }
.notif-tab { flex: 1; padding: 6px 8px; font-size: .76rem; font-weight: 700; cursor: pointer;
    text-transform: uppercase; letter-spacing: .04em;
    color: var(--text-muted); background: none; border: 1px solid var(--border); border-radius: 0; }
.notif-tab:hover { color: var(--gold-text); border-color: var(--gold); }
.notif-tab.on { color: var(--gold-text); border-color: var(--gold); background: var(--gold-light); }
.notif-kind { display: inline-block; margin-right: 6px; font-size: .62rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: .05em; color: var(--gold-text); }


/* ---------------- Messenger: recent list + docked chat windows ---------------- */
/* Modelled on a desktop messenger: windows live at the bottom-right, above the page but
   below nothing else, and stay put while you move between views. Square corners and the
   gold/blood-red accent, like every other surface here. */
.msg-list-panel { right: 60px; }
.msg-listrow { display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; cursor: pointer;
    padding: 8px 10px; font: inherit; color: inherit; background: none;
    border: 1px solid transparent; border-radius: 0; }
.msg-listrow:hover { background: var(--bg-alt); border-color: var(--border); }
.msg-listrow.unread { background: var(--gold-light); border-color: var(--border-gold); }
.msg-listrow-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.msg-listrow-top { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.msg-listrow-name { font-weight: 600; font-size: .86rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msg-listrow-prev { font-size: .74rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msg-badge { min-width: 16px; padding: 0 5px; font-size: .62rem; font-weight: 700; line-height: 16px;
    text-align: center; color: var(--on-gold); background: var(--gold); flex: none; }

/* The dock. Sits clear of the friends rail (48px handle) on the right, and clear of the party
   chat if one is open — `--aqw-pt-dock` is that dock's width plus the gap, published by
   party.js. The two used to be independently positioned and simply overlapped; now each
   reserves the other's space, so they read as one strip along the bottom edge. */
.msg-dock { position: fixed; right: calc(56px + var(--aqw-pt-dock, 0px)); bottom: 0; z-index: 940;
    display: flex; align-items: flex-end; gap: 10px; pointer-events: none; }
.msg-dock > * { pointer-events: auto; }
.chat-win { display: flex; flex-direction: column; width: 288px; height: 380px;
    background: var(--bg-float); border: 1px solid var(--border); border-bottom: none;
    box-shadow: var(--shadow-card); }
.chat-win.min { height: auto; flex-direction: row; align-items: center; }
/* The same chat, sitting IN the party window instead of hanging off the corner of the screen
   (2026-08-30). Wider and taller because it can be: a dock has to stay out of the way of the
   page behind it, and this one IS the page. Specificity beats the 700px rule that gives the
   dock 100vw/60vh, so a phone gets the inline chat at the card's width rather than the
   viewport's. */
.pt-chat-inline { margin-top: 16px; }
.pt-chat-inline .chat-win, .chat-win.chat-win-inline {
    width: auto; height: 440px; max-height: 62vh;
    border-bottom: 1px solid var(--border); box-shadow: none; }
.pt-chat-inline .chat-bar { cursor: default; }
.chat-bar { display: flex; align-items: center; justify-content: space-between; gap: 6px;
    padding: 7px 8px; border-bottom: 1px solid var(--border); background: var(--bg-alt); }
.chat-head { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; cursor: pointer;
    background: none; border: none; padding: 4px 6px; font: inherit; color: inherit; }
.chat-head:hover .chat-name { color: var(--gold-text); }
.chat-name { font-weight: 600; font-size: .84rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-actions { display: flex; gap: 2px; flex: none; }
.chat-x { display: inline-flex; align-items: center; justify-content: center; padding: 4px; cursor: pointer;
    background: none; border: 1px solid transparent; color: var(--text-muted); border-radius: 0; }
.chat-x:hover { color: var(--gold-text); border-color: var(--border); }
.chat-win.min { border-bottom: 1px solid var(--border); background: var(--bg-alt); }
.chat-win.min .chat-head { padding: 8px 10px; }
.chat-scroll { flex: 1; overflow-y: auto; padding: 10px; }
.chat-compose { display: flex; gap: 6px; align-items: flex-end; padding: 8px;
    border-top: 1px solid var(--border); }
.chat-compose textarea { flex: 1; margin-bottom: 0; resize: none; max-height: 90px; font-size: .84rem; padding: 7px 9px; }
.chat-send { display: inline-flex; align-items: center; justify-content: center; padding: 8px 10px; cursor: pointer;
    color: var(--on-gold); background: var(--gold); border: 1px solid var(--gold); border-radius: 0; }
.chat-send:disabled { opacity: .5; cursor: default; }

/* Bubbles are shared by every chat surface */
.msg-row { display: flex; margin-bottom: 7px; }
.msg-row.mine { justify-content: flex-end; }
/* The avatar beside another person's bubble in a group chat (2026-09-10). A button when it
   leads to a profile, a span for a guest who has none. Sits at the bubble's top, beside the
   name line, and never grows or shrinks with the bubble. The same size as a row in the
   contacts drawer (.av.sm), by the owner's request — a 22px version was tried first and read
   as a speck. */
.msg-av { flex: none; align-self: flex-start; margin: 2px 6px 0 0; padding: 0; border: 0; background: none; line-height: 0; }
button.msg-av { cursor: pointer; }
button.msg-av:hover .av, button.msg-av:focus-visible .av { border-color: var(--gold); }
.msg-bubble { max-width: 82%; padding: 6px 10px; border: 1px solid var(--border); background: var(--bg-alt); }
.msg-row.mine .msg-bubble { border-color: var(--border-gold); background: var(--gold-light); }
.msg-body { font-size: .84rem; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
.msg-when { margin-top: 2px; font-size: .66rem; color: var(--text-muted); }
.msg-more { width: 100%; margin-bottom: 8px; padding: 6px; font-size: .74rem; cursor: pointer;
    color: var(--text-muted); background: none; border: 1px dashed var(--border); border-radius: 0; }
.msg-more:hover { color: var(--gold-text); border-color: var(--gold); }

/* On a phone one window takes the width; the rest stay minimised. */
@media (max-width: 700px) {
    /* On a phone the strip cannot sit side by side, so expansion is EXCLUSIVE (enforced in
       party.js/messenger.js): the expanded chat takes the full width and the OTHER dock steps
       aside completely while it is up — its windows are one minimise away, not gone. The
       previous answer shrank the messenger window by the party chat's width, which "worked"
       at 85px wide, i.e. did not. */
    .msg-dock { right: 0; left: 0; gap: 0; }
    .pt-chat-dock { right: 0; left: 0; gap: 0; }
    .chat-win { width: 100vw; height: 60vh; }
    body.pt-chat-full .msg-dock { display: none; }
    body.msg-chat-full .pt-chat-dock { display: none; }
    .chat-win.min { width: auto; }
    .msg-list-panel { right: 8px; left: 8px; }
}
/* A minimised window with unread messages takes the accent, so it reads at a glance from
   the corner of the screen rather than relying on the small badge alone. */
.chat-win.min.has-unread { background: var(--gold-light); border-color: var(--gold); }
.chat-win.min.has-unread .chat-name { color: var(--gold-text); font-weight: 700; }
/* Title bars are the drag handle for repositioning the dock along the bottom edge. */
.chat-bar, .chat-win.min .chat-head { cursor: grab; touch-action: none; }
.msg-dock.dragging { user-select: none; }
.msg-dock.dragging .chat-bar, .msg-dock.dragging .chat-win.min .chat-head { cursor: grabbing; }

/* ---------------- Friends drawer: sections + context menu ---------------- */
/* The drawer is the ONLY friends surface now, so it carries requests, friends, sent and
   blocked — the nav tab that used to duplicate this list is gone. */
.fr-sec { margin-bottom: 14px; }
.fr-sec-head { display: flex; align-items: center; gap: 6px; width: 100%; margin: 0 0 6px; padding: 4px 2px;
    font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-muted); cursor: pointer; background: none; border: none; text-align: left; }
.fr-sec-head:hover { color: var(--gold-text); }
.fr-sec-chev { transition: transform .15s; flex: none; }
.fr-sec.closed .fr-sec-chev { transform: rotate(-90deg); }
.fr-sec.req .fr-sec-head { color: var(--gold-text); }
.fr-more { padding: 4px 6px; }
/* ---- Guilds in the friends drawer ------------------------------------------------------
   A guild is a group of groups, so its header is a flex ROW: the toggle and the chat button
   are siblings rather than nested, because a button inside a button is invalid markup and the
   inner one eats the outer one's clicks. */
.fr-guild-head { display: flex; align-items: stretch; gap: 2px; }
.fr-guild-head .fr-sec-head { flex: 1; min-width: 0; }
.fr-guild-head .fr-sec-head > span:not(.fr-count) {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fr-guild-chat { display: inline-flex; align-items: center; padding: 0 8px; cursor: pointer;
    background: none; border: none; color: var(--text-muted); border-radius: var(--r-sm); }
.fr-guild-chat:hover { color: var(--gold-text); background: var(--bg-alt); }
/* Nested one step in, so a guild reads as living inside the Guilds group rather than beside
   it. The roster indents with it. */
.fr-sec.guilds > .fr-sec-body { padding-left: 8px; }
.fr-gmember .fr-sub { display: inline-flex; align-items: center; gap: 5px; }
/* Rank, and only where it means something — every row saying "member" is noise. */
.fr-grole { font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
    padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border); color: var(--text-muted); }
.fr-grole.leader { color: var(--gold-text); border-color: var(--border-gold); }
.fr-grole.officer { color: var(--text-main); }

.fr-drawer-tip { margin: 0; padding: 8px 12px 12px; font-size: .68rem; color: var(--text-muted);
    text-align: center; line-height: 1.5; border-top: 1px solid var(--border); }
.fr-drawer-tip svg { width: 11px; height: 11px; vertical-align: -0.1em; opacity: .85; }
/* A count on the collapsed handle for pending requests, so they are visible while shut. */
.fr-handle-dot { min-width: 15px; padding: 0 4px; font-size: .6rem; font-weight: 700; line-height: 15px;
    text-align: center; color: #fff; background: var(--danger); }

.fr-ctx { position: fixed; z-index: 1000; min-width: 176px; padding: 4px;
    background: var(--bg-panel); border: 1px solid var(--border); box-shadow: var(--shadow-card); }
.fr-ctx-head { padding: 7px 10px 8px; margin-bottom: 3px; font-size: .76rem; font-weight: 700;
    border-bottom: 1px solid var(--border); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fr-ctx-item { display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; cursor: pointer;
    padding: 8px 10px; font: inherit; font-size: .82rem; color: var(--text-main);
    background: none; border: 1px solid transparent; border-radius: 0; }
.fr-ctx-item:hover, .fr-ctx-item:focus { background: var(--gold-light); border-color: var(--border-gold);
    color: var(--gold-text); outline: none; }
.fr-ctx-item svg { color: var(--text-muted); flex: none; }
.fr-ctx-item:hover svg, .fr-ctx-item:focus svg { color: var(--gold-text); }
.fr-ctx-item.danger:hover, .fr-ctx-item.danger:focus { background: color-mix(in srgb, var(--danger) 10%, transparent);
    border-color: var(--danger); color: var(--danger-text); }
.fr-ctx-item.danger:hover svg, .fr-ctx-item.danger:focus svg { color: var(--danger-text); }
/* Inline "add friend" inside the drawer — sending a request should not require leaving it */
.fr-add { padding: 10px 10px 4px; border-bottom: 1px solid var(--border); }
.fr-addbtn { display: flex; align-items: center; justify-content: center; gap: 7px; width: 100%;
    padding: 7px; font-size: .78rem; font-weight: 600; cursor: pointer;
    color: var(--text-muted); background: none; border: 1px dashed var(--border); border-radius: 0; }
.fr-addbtn:hover, .fr-addbtn.on { color: var(--gold-text); border-color: var(--gold); border-style: solid; background: var(--gold-light); }
#fr-add-input { margin: 8px 0 0; font-size: .82rem; padding: 7px 9px; }
.fr-add-results { max-height: 190px; overflow-y: auto; margin-top: 6px; }
.fr-add-row { display: flex; align-items: center; gap: 8px; padding: 6px 4px; border-bottom: 1px solid var(--border); }
.fr-add-row:last-child { border-bottom: none; }
.fr-add-hint, .fr-add-msg { margin: 8px 2px; font-size: .74rem; color: var(--text-muted); }
.fr-add-msg { color: var(--ok); font-weight: 600; }
/* Presence dot: derived from the live stream, so it is accurate rather than a stale flag */
.fr-dot { width: 7px; height: 7px; flex: none; border-radius: 50%; background: var(--border);
    box-shadow: 0 0 0 1px var(--bg-panel); }
.fr-dot.on { background: var(--ok); }
/* Favourite star — hidden until the row is hovered/focused, ALWAYS shown once set, so a
   favourite reads at a glance while an unset star stays out of the way. */
.fr-star { display: inline-flex; align-items: center; justify-content: center; padding: 4px;
    cursor: pointer; background: none; border: 1px solid transparent; border-radius: 0;
    color: var(--text-muted); opacity: 0; transition: opacity .12s, color .12s; }
.fr-row:hover .fr-star, .fr-row:focus-within .fr-star { opacity: 1; }
.fr-star:hover { color: var(--gold-text); }
.fr-star.on { opacity: 1; color: var(--gold-text); }
.fr-row.fav .fr-name { font-weight: 700; }
/* Touch has no hover, so the star is always visible there rather than unreachable. */
@media (pointer: coarse) { .fr-star { opacity: 1; } }
/* The add-friend block is pinned to the bottom of the drawer, below the scrolling list. */
.fr-add { margin-top: auto; border-top: 1px solid var(--border); border-bottom: none; }
.fr-add .fr-drawer-tip { border-top: none; padding-bottom: 4px; }
.fr-drawer-list { flex: 1; }
/* Username change (account settings) */
.uname-row { display: flex; gap: 8px; align-items: stretch; }
.uname-row input { flex: 1; margin-bottom: 0; }
.uname-row .pf-save:disabled { opacity: .5; cursor: not-allowed; }
/* Small print, so it uses --text-hint rather than --text-muted: that is exactly the
   distinction the two tokens exist for (--text-muted is 3.54:1 on the light background,
   which fails AA at this size). */
.uname-msg { margin: 6px 0 0; font-size: .78rem; color: var(--text-hint); }
.uname-msg.good { color: var(--ok); }
.uname-msg.bad { color: var(--danger-text); }
/* Confirmation boxes appear beneath the field they confirm, only while it is being changed */
.confirm-field { margin-top: -6px; }
/* ...but when a confirm/current box follows the explanatory NOTE rather than an input,
   the same negative margin pulls it over the text. Pair it with the field, not the prose. */
.uname-msg + .confirm-field { margin-top: 8px; }
#settings-pw-note { margin-bottom: 10px; line-height: 1.45; }
/* Rename control on a character chip, beside the remove × */
.char-rename { display: inline-flex; align-items: center; margin-left: 6px; cursor: pointer;
    color: inherit; opacity: .55; transition: opacity .12s; }
.char-rename:hover { opacity: 1; }
.char-rename svg { width: 12px; height: 12px; }

/* "New since you last looked" on the Moderation tab. A dot, not a number: the number next
   to it is the unresolved WORKLOAD and the two would read as contradicting each other. */
.notif-new-dot {
    display: inline-block; width: 7px; height: 7px; margin-left: 6px;
    vertical-align: middle; background: var(--gold);
}

/* ---------------- Attribution footer (Artix fan-content guidelines) ---------------- */
/* Square corners and the app's own muted small print — a required notice should read as
   part of the site, not as a pasted-in legal band. `margin-top: auto` puts it at the
   bottom of the viewport on short pages, which body's column flex already sets up. */
.site-footer {
    margin-top: auto;
    padding: 26px 20px 30px;
    border-top: 1px solid var(--border);
    /* TRANSPARENT, by repainting rather than by removing. The body's wash is opaque at its
       rim, which is exactly where the footer sits, so simply dropping this fill would have
       left the footer white — the body's own edge showing through it. Instead the footer
       paints the artwork again at the same fixed alignment, so its copy lines up pixel for
       pixel with the body's and covers the rim inside the footer's box. A flat wash goes over
       it at --paper-footer, no vignette, because this is the one strip where the picture is
       meant to be LOOKED at rather than sensed — and because the attribution here is a legal
       requirement, so it has to stay readable over whatever the artwork happens to be. */
    background-color: transparent;
    background-image:
        linear-gradient(rgba(var(--paper-ink), var(--paper-footer)), rgba(var(--paper-ink), var(--paper-footer))),
        var(--backdrop);
    background-size: cover, cover;
    background-position: center center, center center;
    background-attachment: fixed, fixed;
    background-repeat: no-repeat, no-repeat;
    text-align: center;
    font-size: 0.78rem;
    line-height: 1.65;
    /* --text-main, not --text-hint, and that swap is what pays for the thin wash above: over a
       worst-case backdrop --text-hint needs a 0.90 veil to clear AA, which would have hidden
       the artwork here as thoroughly as in the body. Darker ink clears it at 0.62 instead, so
       the picture actually shows. Fitting for the one block on the page that is a legal
       requirement rather than decoration. */
    color: var(--text-main);
}
.site-footer p { margin: 0 0 6px; }
.site-footer p:last-child { margin-bottom: 0; }
/* Both selectors carry `.site-footer`, and that is NOT redundant: `.site-footer p` above
   sets the `margin` SHORTHAND, which writes all four sides, and at (0,1,1) it outranked a
   bare `.sf-legal` at (0,1,0). The auto margins were being zeroed, so these 620px blocks
   sat flush against the footer's left padding with ~600px of dead space beside them
   instead of centring. */
.site-footer .sf-legal { max-width: 620px; margin-left: auto; margin-right: auto; }
.site-footer .sf-disclaim { max-width: 620px; margin-left: auto; margin-right: auto; font-style: italic; }
.sf-links { margin-top: 12px; }
/* These links carry --text-main with a permanent underline, so link-ness never depends on
   colour alone — which is the reason to keep it now that --gold-text is readable in light
   mode too (it originally worked around that). The accent moves to the underline on hover. */
.site-footer a { color: var(--text-main); text-decoration: none; border-bottom: 1px solid var(--border); }
.site-footer a:hover, .site-footer a:focus { border-bottom-color: var(--gold); color: var(--text-main); }
.sf-sep { margin: 0 10px; color: var(--border); }
/* The site's own links, in two columns (2026-09-22) under the attribution row. The columns stretch
   to the same height and their items start at the top, so rows line up across them and the
   shorter column simply has space at its foot. The group is centred; each column reads left. */
.sf-cols { display: flex; justify-content: center; align-items: stretch; gap: 8px 56px; margin-top: 14px; }
/* The columns made the footer 84px taller than the single row they replaced (measured 200 → 284px),
   and the body gives those pixels back (min-height, above) so the page ABOVE the footer keeps the
   height it had; the extra rows are a short scroll below (the owner's call, 2026-09-22). Not on a
   phone: there the old row stacked all seven links, so the columns made the footer shorter. */
:root { --sf-cols-extra: 84px; }
@media (max-width: 480px) { :root { --sf-cols-extra: 0px; } }
.sf-col { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; text-align: left; }
.sf-col li { margin: 0; }
/* Buttons that must be indistinguishable from the anchors beside them — they open a dialog
   rather than navigating, but that is not something a reader should have to know. */
.sf-linkbtn { background: none; border: none; padding: 0; font: inherit; cursor: pointer;
    color: var(--text-main); border-bottom: 1px solid var(--border); }
.sf-linkbtn:hover, .sf-linkbtn:focus { border-bottom-color: var(--gold); color: var(--text-main); }
@media (max-width: 480px) {
    .site-footer { padding: 20px 16px 24px; }
    /* Stack the attribution links rather than letting a separator strand one of them mid-line.
       (The site's own links are already in columns.) */
    .sf-links a { display: block; }
    .sf-sep { display: none; }
    .sf-cols { gap: 8px 32px; }
}

/* ---------------- Guilds (js/guilds.js) ---------------- */
/* Square corners and the app's own tokens throughout — these rows sit next to the people
   search in Community, so they must read as the same surface. */
.gd-card h3 { display: flex; align-items: center; gap: 8px; margin-top: 0; }
.gd-hint { color: var(--text-hint); font-size: .86rem; }
.gd-list { display: flex; flex-direction: column; margin: 10px 0; }
/* .gd-row / .gd-row-main are defined with the crest + capacity styles further down. */
.gd-sub { font-size: .76rem; color: var(--text-hint); }
.gd-tag { display: inline-block; font-size: .68rem; text-transform: uppercase; letter-spacing: .04em;
    border: 1px solid var(--border-gold); color: var(--gold-text); padding: 0 5px; margin-left: 6px; }

.gd-claims { display: flex; flex-direction: column; gap: 7px; margin: 10px 0; }
.gd-claim { display: flex; align-items: center; justify-content: space-between; gap: 10px;
    padding: 9px 12px; border: 1px solid var(--border); background: var(--bg-alt); }
.gd-claim.bad { border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
.gd-claim-main { min-width: 0; }
/* Pushed to the end so Withdraw sits opposite the guild name. The auto margin is UNDONE below
   600px — see the media query — because an auto margin overrides align-items: stretch, and the
   stacked mobile row wants the full-width button that stretch gives it. */
.gd-claim .action-btn { margin-left: auto; padding: 8px 20px; }

.gd-form { display: flex; flex-direction: column; gap: 4px; margin-top: 10px;
    border-top: 1px solid var(--border); padding-top: 12px; }
.gd-form label { font-size: .8rem; color: var(--text-muted); margin-top: 6px; }
.gd-form input, .gd-form textarea { width: 100%; box-sizing: border-box; }
.gd-form-actions { display: flex; gap: 8px; margin-top: 10px; }
/* The instructions block: what to do in game, which is the part people get wrong. */
.gd-steps { background: var(--bg-alt); border: 1px solid var(--border); padding: 10px 12px; margin: 6px 0 4px; }
.gd-steps ol { margin: 6px 0 0; padding-left: 18px; font-size: .84rem; color: var(--text-muted); }
.gd-steps li { margin-bottom: 3px; }
.gd-target { font-size: .88rem; }
.gd-target-label { color: var(--text-hint); margin-right: 6px; }
.gd-sep { margin: 0 8px; color: var(--border); }

@media (max-width: 600px) {
    .gd-claim { flex-direction: column; align-items: stretch; }
    /* Stacked, so full width is right here — and the auto margin above would defeat the stretch
       that gives it, since an auto margin wins over align-items. */
    .gd-claim .action-btn { margin-left: 0; }
    .gd-target { display: flex; flex-direction: column; gap: 2px; }
    .gd-sep { display: none; }
}

/* Guild page + group chat additions */
.gd-head h2 { margin-top: 0; }
.gd-section { margin-top: 18px; }
.gd-section h4 { display: flex; align-items: center; gap: 6px; margin: 0 0 8px;
    text-transform: uppercase; letter-spacing: .05em; font-size: .78rem; color: var(--text-muted); }
/* The gap is 22px rather than the 6px other lists use because the roster mark is drawn
   LARGER than its row on purpose (see .gd-member-art) and its points break past the top and
   bottom edges. At 6px the overflow of one row touched the border of the next and read as a
   rendering fault rather than a flourish. If the mark is ever resized, this moves with it. */
.gd-member { display: flex; align-items: center; gap: 10px; padding: 8px 10px;
    border: 1px solid var(--border); background: var(--bg-alt); margin-bottom: 22px; }
.gd-member-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
/* The roster mark. `contain`, not `cover`: the shaped avatars are diamond composites with
   transparent margins and art deliberately breaking past the frame's edges, so cropping to
   fill would slice off the parts that make them worth using. */
/* Deliberately LARGER than the row wants. The negative block margins stop it dragging the
   row height up with it, so the art breaks past the row's top and bottom edges instead —
   which is the point: these composites are drawn with the character bursting out of the
   frame, and sizing them to fit politely throws that away. `position/z-index` keep the
   overflow drawing ABOVE the next row's background rather than being covered by it. */
.gd-member-art { width: 96px; height: 96px; flex: none; display: block;
    margin: -12px 4px -12px 0; position: relative; z-index: 1; pointer-events: none; }
/* The box owns the size, so BOTH kinds of mark fill it: a shaped composite is a bare <img>,
   a fallback is renderAvatar's .av span whose own 28/38px classes would otherwise win. */
.gd-member-art > img { width: 100%; height: 100%; object-fit: contain; display: block;
    /* SCALED UP because these PNGs carry their own transparent margins: a 300x300 canvas
       holds only ~193x246 of drawn art (64% x 82%), so `contain` fits the EMPTY canvas to
       the box and the visible diamond lands about a third smaller than the round avatars
       beside it, which use `cover` and fill theirs completely. 1.5 cancels that out and
       spends the rest on the intended overflow. Tune this, not the box size — changing the
       box moves the round marks too. If new shaped art ships with different margins, this
       is the number that needs revisiting. */
    transform: scale(1.5); transform-origin: center; }
.gd-member-art .av { width: 100%; height: 100%; }
.gd-member-art .av-mono { font-size: 2.6rem; }
.gd-member-actions { display: flex; gap: 6px; flex-wrap: wrap; }
/* Presence is pulled at roster-read time, never pushed — a live dot in a 255-member guild
   would be a fan-out on every tab open and close. */
.gd-dot { width: 8px; height: 8px; background: var(--text-muted); flex: none; }
.gd-dot.on { background: var(--ok); }
.gd-warn { border-color: color-mix(in srgb, var(--danger) 45%, transparent); color: var(--danger-text); }
.gd-motd p { margin: 0 0 6px; white-space: pre-wrap; }
.gd-motd-edit textarea { width: 100%; box-sizing: border-box; }
/* The embedded chat panel: the same body the dock draws, given a fixed height so the page
   does not grow without bound as the history loads.
   `min-height` repeats the default on purpose — it is the FLOOR the drag grip below must not
   go under, and having it here means the floor holds even with JavaScript out of the picture.
   guilds.js reads this value rather than carrying its own copy, so the phone default at the
   bottom of this file moves the floor with it and the two can never disagree. */
.gd-chat-host { display: flex; flex-direction: column; height: 340px; min-height: 340px;
    /* border-box so the drag maths is self-consistent: guilds.js measures the rendered box
       and writes back a `height`, and under content-box those are two different numbers —
       every drag gained the 2px border, so the panel crept away from the cursor. */
    box-sizing: border-box;
    border: 1px solid var(--border); background: var(--bg-panel); }
.gd-chat-host .chat-scroll { flex: 1; overflow-y: auto; }

/* The drag grip: a full-width strip under the panel, reading as its bottom edge. A native
   `resize: vertical` would have been less code, but its handle claims the bottom-right corner
   — which is exactly where the Send button is, the most-pressed control in the panel. */
.gd-chat-wrap { display: flex; flex-direction: column; }
.gd-chat-grip {
    display: flex; align-items: center; justify-content: center;
    height: 14px; cursor: ns-resize;
    background: var(--bg-alt); border: 1px solid var(--border); border-top: none;
    /* Without this a touch drag scrolls the page instead of resizing the panel. */
    touch-action: none;
}
.gd-chat-grip:hover, .gd-chat-grip:focus-visible, .gd-chat-grip.is-dragging { background: var(--bg-panel); }
.gd-chat-grip-bar { display: block; width: 34px; height: 2px; background: var(--border); }
.gd-chat-grip:hover .gd-chat-grip-bar,
.gd-chat-grip:focus-visible .gd-chat-grip-bar,
.gd-chat-grip.is-dragging .gd-chat-grip-bar { background: var(--gold); }
/* A group bubble names its sender; a DM never did, because you already know who it is. */
.msg-who { font-size: .7rem; font-weight: 700; color: var(--gold-text); margin-bottom: 2px; }
.msg-blocked { font-style: italic; color: var(--text-hint); }
.chat-avatar-group { display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border: 1px solid var(--border-gold); color: var(--gold-text); flex: none; }

@media (max-width: 600px) {
    .gd-member { flex-wrap: wrap; }
    .gd-member-actions { width: 100%; }
    /* Both, or the floor would stay at the desktop 340 and a phone could never sit at its
       own default. guilds.js reads min-height, so this moves the drag floor too. */
    .gd-chat-host { height: 280px; min-height: 280px; }
}
.pf-guild-link { background: none; border: none; padding: 0; cursor: pointer;
    color: var(--gold-text); font: inherit; font-weight: 600; line-height: inherit; }
.pf-guild-link:hover { text-decoration: underline; }
/* A search box spanning a 1000px pane reads as a form field for the whole page rather than
   a filter for the list beneath it. Capped to roughly the longest guild name it will hold
   (30 characters), and it still goes full-width once the pane itself is narrow. */
#gd-browse { width: 100%; max-width: 340px; box-sizing: border-box; margin-bottom: 12px; }
.gd-form select { width: 100%; box-sizing: border-box; }
/* Two rows for one account is normal here (alts hold their own place on a guild roster),
   so a member row says which character it is rather than implying one row per player. */
.gd-member-main strong { word-break: break-word; }

/* ---------------- Community: sidebar shell ---------------- */
/* Text labels rather than icons: three destinations do not need a legend, and "Guilds" and
   "Guides" are one letter apart — an icon pair for those two would be a guessing game.

   THE CONTENT COLUMN IS WHAT IS CENTRED, not the sidebar-plus-content pair. The sidebar is
   lifted out of the flow and hung in the gutter to the left, so switching sections never
   shifts the reading column sideways and Community lines up with every other page. That
   needs gutter space to hang in, so there are three arrangements, and the breakpoints below
   are the measured widths at which each one stops fitting. */
.cm-layout { position: relative; }
.cm-main { min-width: 0; }

.cm-side { display: flex; flex-direction: column; gap: 2px; }
.cm-link {
    background: none; border: none; cursor: pointer; font: inherit; text-align: left;
    padding: 9px 12px; color: var(--text-muted); white-space: nowrap;
    /* A left rule, not a pill: square corners, and it reads as an index down the side
       rather than as a row of buttons. */
    border-left: 2px solid transparent;
}
.cm-link:hover { color: var(--text-main); background: var(--bg-alt); }
.cm-link.on { color: var(--gold-text); border-left-color: var(--gold); font-weight: 600; }
.cm-pane { display: none; }
.cm-pane.on { display: block; }

/* WIDE — the sidebar hangs outside the centred column. `right: 100%` anchors it to the
   column's left edge, so it moves with the layout instead of being positioned by hand.
   Needs (viewport - container)/2 >= 170px of margin, and .container maxes at 1100. */
@media (min-width: 1440px) {
    .cm-side { position: absolute; top: 0; right: 100%; margin-right: 26px; width: 132px;
               align-items: stretch; }
}

/* MEDIUM — not enough gutter to hang in, so the sidebar rejoins the flow beside the content.
   The pair is centred here; that is the honest fallback, since the alternative is a sidebar
   hanging off the edge of the screen. */
@media (min-width: 781px) and (max-width: 1439px) {
    .cm-layout { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 26px; align-items: start; }
    .cm-side { position: sticky; top: calc(var(--nav-h) + 16px); }
}

/* NARROW — a 150px column beside a phone-width pane would leave neither enough room, so the
   sidebar becomes a row of tabs above the content. */
@media (max-width: 780px) {
    .cm-side { flex-direction: row; gap: 0; border-bottom: 1px solid var(--border);
        overflow-x: auto; margin-bottom: 14px; }
    .cm-link { border-left: none; border-bottom: 2px solid transparent; padding: 10px 14px; }
    .cm-link.on { border-left-color: transparent; border-bottom-color: var(--gold); }
}

/* The guild's public description, as read by a visitor. */
.gd-desc { margin: 10px 0; line-height: 1.6; }

/* "Full" on a browse row: muted, not alarming — it is a fact about the guild, not a warning
   about it, and it sits next to a name somebody deliberately searched for. */
.gd-tag-full { color: var(--text-hint); border-color: var(--border); }

/* ---------------- Guild rows: crest, rank, capacity ----------------
   The brief was "more game-y" while the house rules still hold — square corners, the app
   accent, no stock artwork and no emoji. So the game feel comes from STRUCTURE rather than
   decoration: a heraldic crest plate with a cut corner, a rank plate in small caps, and a
   capacity meter. Nothing here is an image, so it themes and scales with everything else. */

.gd-crest {
    flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex;
    align-items: center; justify-content: center;
    /* The plate takes the guild's derived hue at low saturation so crests differ from each
       other without any of them shouting; the SIGIL itself uses the theme accent, which is
       the contrast-checked colour. */
    background: hsl(var(--av-h, 0) 30% 92%);
    border: 1px solid var(--border);
    /* The sigil takes a SATURATED, DARK version of the same hue rather than the theme
       accent. Two reasons: the accent on a pale plate measured 2.15:1 (a graphic still owes
       3:1), and tinting the mark itself is what makes one guild's crest tell apart from
       another's at a glance. The hue is the guild's identity; the accent stays the app's. */
    color: hsl(var(--av-h, 0) 72% 28%);
    /* A clipped top-left corner — the one flourish. Square everywhere else, per the app's
       existing rule; a notch reads as a shield edge without becoming a rounded pill. */
    clip-path: polygon(9px 0, 100% 0, 100% 100%, 0 100%, 0 9px);
}
[data-theme="dark"] .gd-crest {
    background: hsl(var(--av-h, 0) 22% 14%);
    color: hsl(var(--av-h, 0) 68% 72%);   /* light mark on a dark plate — mirrored, same rule */
}
.gd-crest svg { width: 24px; height: 24px; }
.gd-crest-lg { width: 60px; height: 60px; clip-path: polygon(12px 0, 100% 0, 100% 100%, 0 100%, 0 12px); }
.gd-crest-lg svg { width: 34px; height: 34px; }

.gd-head-id { display: flex; align-items: center; gap: 14px; }
.gd-head-id h2 { margin: 0; }

/* A row is a banner: crest, then the guild's details, with a left edge that lights on hover. */
.gd-row {
    display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
    padding: 12px 14px; margin-bottom: 8px; cursor: pointer;
    background: var(--bg-alt); border: 1px solid var(--border); border-left: 3px solid transparent;
    color: var(--text-main); font: inherit;
    transition: border-left-color .12s, background .12s;
}
.gd-row:hover, .gd-row:focus-visible { border-left-color: var(--gold); background: var(--bg-panel); }
.gd-row-mine { border-left-color: var(--border-gold); }
.gd-row-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1; }
.gd-row-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.gd-row-name { font-size: 1rem; }
/* "as <character>" and friends: one short fact, one line, ellipsis if even that is too long. */
.gd-row-desc {
    font-size: .82rem; color: var(--text-hint);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The guild's own blurb, bounded by the SPACE rather than by a character count.
   It used to be `slice(0, 90)` in guilds.js on top of this one-line clamp, and the two fought:
   90 characters nearly always fit the row, so the clamp never fired and the reader got a cut
   mid-word with no ellipsis at all — which reads as corrupted text rather than "there is more".
   It also threw away room a wide row plainly had, since a character count cannot know how wide
   the screen is. Letting the browser do the cutting means the ellipsis always appears and the
   text grows with the viewport. Two lines rather than one because one was not enough to say
   what a guild IS; still bounded, so a list of these cannot become a wall of paragraphs.

   THE CLAMP IS ON AN INNER SPAN ON PURPOSE, and this is the whole trap: `.gd-row-main` is a
   flex container, so anything directly inside it is a FLEX ITEM — and a flex item's display is
   blockified, which means `display: -webkit-box` can never survive there and the clamp silently
   does nothing. Current Chromium hides that (it applies -webkit-line-clamp to block containers
   too, so it happened to work), but Safari and older Chrome would have rendered the full text
   and ballooned the row. The inner span is a child of a block, so the box type holds. */
.gd-row-blurb {
    font-size: .82rem; color: var(--text-hint); line-height: 1.35;
    /* Belt and braces: if the clamp is unsupported entirely, this still stops at two lines —
       clipped without an ellipsis, which is ugly but bounded. Nothing can grow the row. */
    display: block; max-height: 2.7em; overflow: hidden;
}
.gd-row-blurb > span {
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
    /* A single unbroken 200-character "word" is otherwise allowed to widen the row. */
    overflow: hidden; overflow-wrap: anywhere;
}

/* Rank plates. Small caps and letterspacing do the "insignia" work — no icon needed, and
   the words stay readable, which an icon-only rank would not be. */
.gd-rank {
    font-size: .62rem; font-weight: 800; letter-spacing: .09em; text-transform: uppercase;
    padding: 2px 7px; border: 1px solid var(--border); color: var(--text-hint); white-space: nowrap;
}
/* The accent lives in the BORDER, not the letters. That began as a contrast workaround and
   is kept on its own merits: a bordered plate reads as insignia, and the rank stays legible
   against any row background it lands on. */
.gd-rank-leader { color: var(--text-main); border-color: var(--gold); border-width: 2px; }
.gd-rank-officer { color: var(--text-main); border-color: var(--text-hint); }
.gd-rank-full { color: var(--text-hint); border-color: var(--border); }

/* Capacity meter. Segmented rather than a smooth bar — it reads as slots in a roster, which
   is what it is counting. */
.gd-cap { display: flex; align-items: center; gap: 8px; margin-top: 2px; }
.gd-cap-bar {
    position: relative; width: 120px; height: 6px; background: var(--border); overflow: hidden;
    /* The segment ticks are drawn OVER the fill, so the fill stays one element to animate. */
    background-image: repeating-linear-gradient(90deg, transparent 0 11px, var(--bg-alt) 11px 13px);
}
.gd-cap-fill { display: block; height: 100%; background: var(--gold); }
.gd-cap-fill.is-full { background: var(--text-hint); }
.gd-cap-text { font-size: .72rem; color: var(--text-hint); font-variant-numeric: tabular-nums; }

@media (max-width: 560px) {
    /* The description is the first thing to go: the name, rank and capacity are what a
       narrow row is actually for. The crest and padding shrink with it, because a 46px
       emblem in a 107px row was a banner where a list row was wanted. */
    .gd-row-blurb { display: none; }   /* the "as <character>" line stays; the blurb goes */
    .gd-cap-bar { width: 78px; }
    .gd-row { padding: 9px 10px; gap: 10px; }
    .gd-crest { width: 36px; height: 36px; clip-path: polygon(7px 0, 100% 0, 100% 100%, 0 100%, 0 7px); }
    .gd-crest svg { width: 20px; height: 20px; }
    .gd-row-name { font-size: .92rem; }
}

/* An uploaded crest fills its plate; the derived sigil sits inside one. Same box either way,
   so a guild with an upload and one without line up in the same list. */
.gd-crest-img { padding: 0; background: var(--bg-alt); }
.gd-crest-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gd-crest-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
/* (`.gd-upload` lived here: a <label> dressed up as a button, wrapping the hidden file input.
   It could never quite pass — a label inherits the page line-height and content-box, so it
   stood 43.75px next to a 37px button carrying the same class. The control is a real <button>
   now and needs no styling of its own, so the rule is gone rather than left to rot. See the
   comment on it in guilds.js.) */

/* The verification target — the one detail a player must get right, so the eye has to land
   on it. The accent is the UNDERLINE and the letters stay --text-main: an underline is a
   stronger "look here" than a colour shift, and it survives however the palette moves. */
.vf-target { color: var(--text-main); border-bottom: 2px solid var(--gold); padding-bottom: 1px; }
.vf-how-sub { color: var(--text-hint); font-size: .86rem; margin: 4px 0 0; }

/* ---------------- Guild page: sections ----------------
   Same shell as Community and for the same reason: Home / Members / Chat / Management are
   four different errands, and stacking them down one page made every visit scroll past
   three things it did not come for. Reuses .cm-link so the two sidebars can never drift. */
.gd-layout { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 26px; align-items: start; }
.gd-side { position: sticky; top: calc(var(--nav-h) + 16px); }
.gd-main { min-width: 0; }
.gd-secbadge {
    display: inline-block; min-width: 16px; margin-left: 7px; padding: 0 4px;
    font-size: .68rem; font-weight: 800; line-height: 16px; text-align: center;
    color: #fff; background: var(--danger);
}
.gd-manage-block { padding: 14px 0; border-bottom: 1px solid var(--border); }
.gd-manage-block:first-child { padding-top: 0; }
.gd-manage-block:last-child { border-bottom: none; padding-bottom: 0; }
.gd-manage-block h4 { margin: 0 0 8px; }
/* The destructive block reads as one before the button is even seen. */
.gd-danger { border-left: 3px solid var(--danger); padding-left: 12px; }
.gd-motd-card { margin-top: 14px; }
.gd-motd-card h4 { margin-top: 0; }
.gd-actions { margin-top: 14px; }

@media (max-width: 780px) {
    .gd-layout { grid-template-columns: 1fr; gap: 14px; }
    .gd-side { flex-direction: row; position: static; gap: 0;
        border-bottom: 1px solid var(--border); overflow-x: auto; }
    .gd-side .cm-link { border-left: none; border-bottom: 2px solid transparent; padding: 10px 14px; white-space: nowrap; }
    .gd-side .cm-link.on { border-left-color: transparent; border-bottom-color: var(--gold); }
}

/* Five rules used to sit here scoping these guild surfaces back to intrinsic width, one at a
   time, because .action-btn stretched by default. That default is now the other way round
   (see .action-btn), so they are gone — deleted rather than left as no-ops, since a rule that
   undoes something no longer happening is a trap for the next reader.
   The rank plate below is a DIFFERENT cause and stays: stretch is the default cross-axis
   alignment for a column, which has nothing to do with the button class. */
.gd-member-main { align-items: flex-start; }
.gd-row-main { align-items: flex-start; }
.gd-rank { align-self: flex-start; }

/* A removed message keeps its place in the thread so replies still make sense, but reads as
   an absence rather than as content. */
.msg-removed { font-style: italic; color: var(--text-hint); }
/* The remove control sits with the timestamp, visible on hover so it does not compete with
   the message itself. Always visible on touch, which has no hover. */
.msg-remove { background: none; border: none; cursor: pointer; padding: 0 0 0 6px;
    color: var(--text-hint); opacity: 0; transition: opacity .12s; }
.msg-bubble:hover .msg-remove, .msg-remove:focus-visible { opacity: 1; }
.msg-remove:hover { color: var(--danger-text); }
@media (pointer: coarse) { .msg-remove { opacity: 1; } }
/* Mute durations, offered as a small row of choices in place of the row's usual actions. */
.gd-mute-menu { display: inline-flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.gd-preset { font-size: .72rem; padding: 3px 8px; cursor: pointer;
    border: 1px solid var(--border); border-radius: 4px;
    background: var(--bg-alt); color: var(--text-muted); }
.gd-preset:hover { border-color: var(--gold); color: var(--text-main); }
.gd-mins { width: 68px; }

/* Guild settings rows: label on the left, control on the right, one per line. */
.gd-setting { display: flex; align-items: center; gap: 12px; margin: 8px 0; flex-wrap: wrap; }
.gd-setting > span { min-width: 140px; color: var(--text-hint); font-size: .86rem; }
.gd-setting select { flex: 1; min-width: 220px; max-width: 380px; }
.gd-setting-check { gap: 8px; }
.gd-setting-check > span { min-width: 0; color: var(--text-main); font-size: .92rem; }
#gd-invite-name { min-width: 200px; }

/* The cog sits in the composer row, so both chat surfaces get it without either needing a
   toolbar of its own — the docked window has a title bar, the guild-page panel does not. */
.chat-cog { background: none; border: none; cursor: pointer; padding: 0 6px;
    color: var(--text-hint); flex: 0 0 auto; }
.chat-cog:hover, .chat-cog:focus-visible { color: var(--gold-text); }

/* Chat settings dialog. Body-level and fixed, NOT inside the chat window: a dock window is
   small, draggable and minimisable, and a dialog living inside one would be clipped by all
   three. z-index sits above the dock (1000) for the same reason. */
.chat-settings-overlay, .app-overlay {
    position: fixed; inset: 0; z-index: 1200;
    display: flex; align-items: center; justify-content: center; padding: 20px;
    /* Heavily dimmed rather than merely tinted: the point is that nothing behind it competes
       for attention while it is open. */
    background: rgba(255, 255, 255, 0.82);
}
[data-theme="dark"] .chat-settings-overlay,
[data-theme="dark"] .app-overlay { background: rgba(0, 0, 0, 0.78); }
.chat-settings-box, .app-dialog {
    background: var(--bg-panel); border: 1px solid var(--gold);
    padding: 24px 26px; width: 100%; max-width: 400px;
    box-shadow: var(--shadow-card);
}
.chat-settings-box h3, .app-dialog h3 { margin: 0 0 16px; }

/* ---- Dialogs ARRIVE; they do not blink into existence -----------------------------------
   The backdrop fades and the panel rises slightly as it scales up. Both are short: this is
   meant to say "something opened, here it is", not to make anyone wait. The panel curve
   overshoots a touch (the 1.08) so it settles rather than stopping dead.

   Every one of these dialogs is opened by a deliberate act and repainted only by another one
   — none is on a timer — so the animation cannot end up replaying under a poll. Anything
   later that repaints one of these on an interval has to keep that true, or the dialog will
   pulse every few seconds.

   The overlays are the ELEMENTS THAT PERSIST for as long as the dialog is open; the panel
   inside is what gets rebuilt between screens, which is exactly the split we want — the
   backdrop fades once, and each new screen pops in on its own. */
@keyframes ui-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ui-pop-in {
    from { opacity: 0; transform: scale(.94) translateY(8px); }
    to   { opacity: 1; transform: none; }
}
.app-overlay, .chat-settings-overlay, .pt-overlay, .auth-overlay {
    animation: ui-fade-in .15s ease both;
}
/* THE POP RUNS ON OPENING, NOT ON EVERY REPAINT. These dialogs are rebuilt with `innerHTML`
   whenever their state changes — sending feedback, switching a screen, showing an error — and a
   fresh element replays its animation, so the panel visibly re-popped mid-interaction. That was
   reported as "some weird animation" after submitting feedback, and it was exactly this.
   
   The gate is a class the OVERLAY wears for one beat when it is created (`markOpening` in
   format.js). The overlay is created once per opening; the dialog inside it is not. */
.app-overlay.is-opening .app-dialog,
.pt-overlay.is-opening .pt-to-dialog,
.pt-overlay.is-opening .pt-again-dialog,
.chat-settings-overlay.is-opening .chat-settings-box,
/* The auth modal is display-toggled rather than rebuilt, so showing it restarts the animation
   on its own and it needs no gate. */
.auth-overlay .auth-box {
    animation: ui-pop-in .2s cubic-bezier(.2, .8, .3, 1.08) both;
}
/* Motion is a preference, and someone who has turned it off usually means it. They still get
   every dialog, drawer and banner — just instantly, the way the app behaved before. */
@media (prefers-reduced-motion: reduce) {
    .app-overlay, .chat-settings-overlay, .pt-overlay, .auth-overlay,
    .app-dialog, .chat-settings-box, .pt-to-dialog, .pt-again-dialog, .auth-box,
    .app-overlay.is-opening .app-dialog, .pt-overlay.is-opening .pt-to-dialog,
    .pt-overlay.is-opening .pt-again-dialog,
    .chat-settings-overlay.is-opening .chat-settings-box { animation: none; }
    .fr-drawer-body, .cr-banner { transition: none; }
}
/* A form dialog is taller than the chat one and must never grow past the viewport. */
.app-dialog { max-width: 460px; max-height: calc(100vh - 40px); overflow-y: auto; }
.cs-field { display: flex; flex-direction: column; gap: 6px; }
.cs-field > span { font-size: .82rem; color: var(--text-hint); }
.cs-field select { width: 100%; box-sizing: border-box; }
.cs-hint { color: var(--text-hint); font-size: .8rem; margin: 8px 0 0; line-height: 1.5; }
.cs-actions { display: flex; gap: 8px; margin-top: 18px; }
/* An unsaved change makes Save the obvious next move rather than one of two equal buttons. */
.cs-primary { border-color: var(--gold); color: var(--gold-text); font-weight: 600; }
.cs-status { min-height: 1.2em; margin: 10px 0 0; font-size: .8rem; color: var(--text-hint); }
.cs-status.ok { color: var(--ok); }
.cs-status.bad { color: var(--danger-text); }
/* The dialog's last row of buttons sits under everything, so Close is always in the same
   place no matter which sections the room actually has. */
.cs-actions-end { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 16px; }

/* Moderation. Muting is a CHAT action, so it belongs where the chat is; the roster on the
   guild page keeps its copy for the times you arrive from the other direction. */
.cs-section { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 16px; }
.cs-section h4 { margin: 0; font-size: .78rem; text-transform: uppercase;
    letter-spacing: .06em; color: var(--text-muted); }
#cs-mod-q { width: 100%; box-sizing: border-box; margin-top: 10px; }
#cs-mod-results { margin-top: 6px; max-height: 210px; overflow-y: auto; }
/* Name above, controls below: five presets, a number box and a button do not fit beside a
   character name at this width, and a row that wraps mid-control reads as broken. */
.cs-mod-row { padding: 8px 0; border-bottom: 1px solid var(--border); }
.cs-mod-row:last-child { border-bottom: none; }
.cs-mod-name { display: block; font-size: .86rem; }
.cs-mod-role { color: var(--text-hint); font-size: .72rem; text-transform: uppercase;
    letter-spacing: .04em; margin-left: 6px; }
.cs-mod-muted { color: var(--danger-text); font-size: .72rem; margin-left: 6px; }
.cs-mod-act { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-top: 6px; }
.cs-preset { font-size: .72rem; padding: 3px 8px; cursor: pointer;
    border: 1px solid var(--border); border-radius: 4px;
    background: var(--bg-alt); color: var(--text-muted); }
.cs-preset:hover { border-color: var(--gold); color: var(--text-main); }
.cs-mins { width: 68px; }

/* ---------------- Feedback & Contact dialogs (js/feedback.js) ---------------- */
.fb-field { margin-top: 14px; }
.fb-field textarea, .fb-field input, .fb-field select { width: 100%; box-sizing: border-box; }
.fb-field textarea { resize: vertical; min-height: 110px; font-family: inherit; }
.fb-field em { font-style: normal; color: var(--text-hint); }
.fb-count { margin-top: 4px; text-align: right; }
/* Only near the ceiling does the count become worth noticing. */
.fb-count.low { color: var(--danger-text); }

.fb-contacts { margin-top: 14px; }
.fb-contact-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 8px 0; border-bottom: 1px solid var(--border); }
.fb-contact-row:last-child { border-bottom: none; }
.fb-contact-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .05em;
    color: var(--text-hint); min-width: 52px; }
.fb-contact-row a { color: var(--text-main); border-bottom: 1px solid var(--border); text-decoration: none; }
.fb-contact-row a:hover { border-bottom-color: var(--gold); }
/* A button that has to read as part of a sentence. */
.fb-inline-link { background: none; border: none; padding: 0; font: inherit; cursor: pointer;
    color: var(--text-main); border-bottom: 1px solid var(--border); }
.fb-inline-link:hover { border-bottom-color: var(--gold); }
/* Someone's own words, kept as they typed them — line breaks included. */
.fb-msg { white-space: pre-wrap; }

/* ---------------- Party Finder (js/party.js) ----------------
   A server browser: a filter strip, a header row, and dense rows the eye can scan down a
   column at a time. Deliberately NOT cards — a card grid answers "show me one thing well",
   and this answers "compare twenty at a glance". */
.pt-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.pt-head h3 { margin: 0 0 4px; }
/* One global default still has to be undone here. (The other was .action-btn { flex: 1 },
   which grew this button to 684px to hold twelve characters; that default was inverted on
   2026-08-30 and no longer needs undoing anywhere.) The global input/select/textarea rule
   (width: 100% + a 10px bottom margin)
   put the character picker on a line of its own at 360px — the same trap that had already
   stacked the filter row, so this is the second sighting in one feature: ANY select placed
   in a flex or inline context in this app needs `width: auto` before it will behave. */
.pt-head .action-btn { padding: 10px 24px; }
.pt-head .pt-hint { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.pt-head select { width: auto; margin-bottom: 0; padding: 5px 8px; font-size: .82rem; }
.pt-hint { color: var(--text-hint); font-size: .8rem; margin: 0; line-height: 1.5; }

.pt-filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 16px 0 12px; }
/* `width: auto` and `margin-bottom: 0` are doing real work here. The GLOBAL rule at the top
   of this file gives every input and select `width: 100%` and a 10px bottom margin — which
   made each control claim a whole line no matter what flex basis it was given, so a row of
   four short filters ate four rows of vertical space. Flex cannot override a width it was
   never allowed to set. */
.pt-filters > * { margin-bottom: 0; }
.pt-filters #pt-q { flex: 2 1 200px; width: auto; min-width: 150px; }
.pt-filters select { flex: 1 1 130px; width: auto; min-width: 120px; }
/* ONE height for every control on the row, set explicitly rather than left to each element's
   own padding. The search box was inheriting the global 12px padding while the selects had
   10px, so they came out 43px and 41px — centred by the flex row, but with their tops and
   bottoms 6px apart, which is exactly the kind of misalignment the eye catches even when it
   cannot name it. Horizontal padding only, so nothing can reintroduce the difference. */
/* `margin: 0` belongs HERE, not on `.pt-filters > *`: the global reset selector is
   `input[type="text"]`, whose specificity is (0,1,1) — an attribute selector counts at class
   level — so it outranked `.pt-filters > *` at (0,1,0) and the search box kept its 10px
   bottom margin. That made its OUTER box 50px against everyone else's 40, and a centred
   50px box sits 5px higher than a centred 40px one. Same height, still misaligned. */
.pt-filters #pt-q,
.pt-filters select,
.pt-filters .pt-refresh { height: 40px; box-sizing: border-box; margin: 0; }
.pt-filters #pt-q { padding: 0 12px; }
.pt-filters select { padding: 0 8px; }
.pt-filters .pt-refresh { padding: 0 16px; }
.pt-check { display: inline-flex; align-items: center; gap: 6px; font-size: .8rem; color: var(--text-muted); }
/* The same control in the create form, where it is a decision rather than a filter — so it
   reads at body size in the body colour, not as muted fine print. */
/* The marker on a board row's occupancy: this party may start short of its seat count.
   NOT the accent token. `--gold-text` is gold in light and Gilded but BLOOD RED in dark — it
   is the brand accent, not a colour name — so using it here painted a red warning onto what
   is only a property of the listing, and only in the default theme. Muted reads the same in
   all three; the weight is what makes it noticeable, and the title says what it means. */
.pt-count.undersized sup { color: var(--text-muted); font-weight: 700; margin-left: 1px; }
/* Size is shown, not typed. Sized and aligned to sit level with the real controls beside it
   so the row still reads as one line of settings rather than a field that failed to render. */
.pt-capfixed { display: flex; align-items: center; min-height: 38px; padding: 0 2px;
    font-size: .9rem; font-weight: 600; color: var(--text-main); }
.pt-undersized { font-size: .88rem; color: var(--text-main); margin: 2px 0 0; cursor: pointer; }
/* Unavailable at two seats. Dimmed AND not clickable, so it cannot be toggled by clicking the
   label text — a disabled input still has an enabled <label> pointing at it. */
.pt-undersized.is-off { color: var(--text-muted); opacity: .55; cursor: not-allowed; pointer-events: none; }
.pt-refresh { display: inline-flex; align-items: center; gap: 6px; }

/* One grid template shared by the header and every row, so the columns cannot drift apart. */
.pt-listhead, .pt-row {
    display: grid;
    /* GOAL IS A FIXED 260px, not a fraction. Measured, not guessed: the longest goal in
       settings.js is "Kolr, Usurper of Flames" at 168px in the app's own 16px/600 face, and
       the widest seat badge ("asked to join") is another 91px beside it. Anything narrower
       ellipsises the one string the column exists to hold. Host dropped 130 -> 110 to pay
       for part of it; the description takes whatever is left, which is what the hover is
       there to rescue.
       These MUST stay fixed px: each row is its own grid, so max-content would let the
       columns drift row to row instead of lining up. */
    grid-template-columns: 260px minmax(0, 1fr) 110px 90px 74px 110px 130px;
    gap: 10px; align-items: center;
}
.pt-listhead {
    font-size: .66rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-hint); padding: 0 10px 6px; border-bottom: 1px solid var(--border);
}
.pt-list { display: block; }
.pt-row {
    width: 100%; text-align: left; background: none; border: none; cursor: pointer;
    padding: 9px 10px; border-bottom: 1px solid var(--border); font: inherit;
    color: var(--text-main); border-left: 3px solid transparent;
}
.pt-row:hover, .pt-row:focus-visible { background: var(--bg-alt); border-left-color: var(--gold); }
.pt-row.on { background: var(--gold-light); border-left-color: var(--gold); }
.pt-goal { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pt-server, .pt-policy, .pt-host, .pt-age { color: var(--text-muted); font-size: .8rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* NAMED .pt-rowdesc, not .pt-desc: that name is already taken by the detail panel
   below, whose `white-space: pre-wrap` rendered the newlines in the row markup as real
   line breaks. This is only the positioning context and must NOT clip, or it would clip its own
   tooltip. The inner span does the clipping. */
.pt-rowdesc { position: relative; min-width: 0; }
.pt-rowdesc-text {
    display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    color: var(--text-muted); font-size: .8rem;
}
/* Same shape as .buff-tip on class skills, so a hover means the same thing everywhere. */
.pt-rowdesc-tip {
    display: none; position: absolute; left: 0; top: calc(100% + 6px); z-index: 60;
    min-width: 220px; max-width: 340px; padding: 10px 12px;
    background: var(--bg-alt); border: 1px solid var(--border);
    color: var(--text-main); font-size: .8rem; font-weight: 400; line-height: 1.45;
    white-space: normal; text-align: left; box-shadow: 0 6px 18px rgba(0,0,0,.35);
    /* A description can be one unbroken 500-character token with no spaces in it. Normal
       wrapping only breaks at existing opportunities, so without this the tooltip ignores
       its own max-width and runs off the page. */
    overflow-wrap: anywhere;
}
/* Only when the text is genuinely cut off — markTruncated() in party.js sets this. */
.pt-rowdesc.is-truncated .pt-rowdesc-text { cursor: help; }
.pt-rowdesc.is-truncated:hover .pt-rowdesc-tip,
.pt-rowdesc.is-truncated:focus-within .pt-rowdesc-tip { display: block; }
.pt-count { font-variant-numeric: tabular-nums; font-weight: 700; font-size: .84rem; }
.pt-count.full { color: var(--danger-text); }
.pt-row.full .pt-goal { opacity: .75; }
.pt-you { font-size: .62rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
    color: var(--ok); margin-left: 6px; }
.pt-you.pending { color: var(--text-hint); }
.pt-stamp { color: var(--text-hint); font-size: .74rem; margin: 8px 0 0; text-align: right; }

/* The expanded listing. Sits under the board rather than over it: a server browser shows
   you the detail without hiding the list you are working through. */
.pt-detail, .pt-create { margin-top: 18px; border: 1px solid var(--gold); background: var(--bg-panel); padding: 18px 20px; }
.pt-detail h3, .pt-create h3 { margin: 0 0 6px; }
.pt-detail-meta { color: var(--text-muted); font-size: .78rem; margin-bottom: 12px; }
/* pre-wrap keeps the host's own line breaks; overflow-wrap makes a spaceless string break
   anyway, instead of pushing out past the panel edge. */
.pt-desc { white-space: pre-wrap; margin: 0 0 8px; overflow-wrap: anywhere; }
/* Also host-supplied free text (140 chars), so it can be one spaceless token like the
   description. Same treatment, for the same reason. */
.pt-req { margin: 0 0 8px; font-size: .86rem; color: var(--text-muted); overflow-wrap: anywhere; }
/* The dismissal screen (2026-08-30). Deliberately plain and deliberately short: it exists to
   answer one question and offer one way out, and a card dressed up with anything more would
   imply there was something here to do. */
.pt-dismissed { text-align: center; padding: 32px 24px; }
.pt-dismissed h4 { margin: 0 0 8px; font-size: 1.05rem; }
.pt-dismissed .pt-hint { max-width: 46ch; margin: 0 auto 18px; }

.pt-block { margin-top: 14px; }
.pt-block h4 { margin: 0 0 6px; font-size: .72rem; text-transform: uppercase;
    letter-spacing: .06em; color: var(--text-muted); }
.pt-seat { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    padding: 6px 0; border-bottom: 1px solid var(--border); }
.pt-seat:last-child { border-bottom: none; }
.pt-seat-name { font-size: .88rem; display: inline-flex; align-items: center; gap: 5px; }
.pt-seat-sub { color: var(--text-hint); font-size: .76rem; }
/* BESIDE the name, not shoved to the far edge (2026-08-30). These rows previously carried
   `margin-left: auto`, which reads fine in a narrow column and badly in a wide card: on a
   1100px party card the buttons ended up several hundred pixels from the person they act on,
   and picking the right row meant tracking a line across empty space. The controls now follow
   the name with one clear gap, so the thing you click sits next to the thing you clicked it
   for. Applies to the roster's Remove/Time out, the pending list's Accept/Decline, and the
   post-party verdicts alike — they are all the same gesture. */
.pt-seat-acts { margin-left: 14px; display: inline-flex; gap: 5px; }
.pt-verified { color: var(--ok); display: inline-flex; }
.pt-tag { font-size: .6rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-hint); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px; }
.pt-tag.guest { color: var(--text-muted); }

/* The post-party screen (2026-08-30). Panelled and gold-edged rather than sitting flat in the
   detail view, because it is a different moment: the party is over, and this is the one screen
   asking the reader to DO something about the run they just finished. */
.pt-wrapup { margin-top: 14px; padding: 12px 14px; border-radius: 10px;
    border: 1px solid var(--border-gold, var(--border)); background: var(--bg-alt); }
.pt-wrapup-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.pt-wrapup-head h4 { margin: 0; font-size: .9rem; }
.pt-wrapup-clock { margin-left: auto; font-size: .72rem; color: var(--text-hint); white-space: nowrap; }
.pt-wrapup .pt-hint { margin: 6px 0 10px; }
/* Quiet by default and only louder on approach: the note behind it matters, but it is a thing
   you consult once, not a paragraph the screen has to carry every time. Focusable so it is not
   mouse-only — a title nobody can reach with a keyboard is not an affordance. */
.pt-wrapup-why { display: inline-flex; align-items: center; gap: 5px; cursor: help;
    margin: 4px 0 12px; font-size: .74rem; color: var(--text-hint); }
.pt-wrapup-why svg { width: 13px; height: 13px; }
.pt-wrapup-why:hover, .pt-wrapup-why:focus-visible { color: var(--text-muted); }
.pt-wrapup .pt-seat:last-child { border-bottom: none; }

/* The WRAPPER carries the auto margin, never the buttons — the .pt-mini note above records what
   happens otherwise: two buttons each claiming the free space in turn end up split across the
   row, visibly attached to nothing. */
.pt-verdicts { margin-left: 14px; display: inline-flex; gap: 5px; }
/* Square-ish now that they carry a thumb rather than a word. The SVG is sized HERE and never
   on the element, per the icon set's own rule — one icon, several sizes, no duplicate markup. */
.pt-verdict { padding: 6px; cursor: pointer; background: transparent; line-height: 0;
    border: 1px solid var(--border); border-radius: 4px; color: var(--text-muted);
    transition: border-color .12s, color .12s, background .12s; }
.pt-verdict svg { width: 17px; height: 17px; display: block; }
.pt-verdict:hover { color: var(--text-main); }
/* Colour arrives on hover and stays when pressed, so the row reads at a glance as "I said
   something about this person" without either verdict shouting at the other. */
.pt-verdict.up:hover, .pt-verdict.up.on { border-color: var(--ok); color: var(--ok); }
.pt-verdict.down:hover, .pt-verdict.down.on { border-color: var(--danger); color: var(--danger-text); }
.pt-verdict.on { font-weight: 700; background: color-mix(in srgb, currentColor 12%, transparent); }
/* Never rely on colour alone (2026-08-30). The pressed verdict is told apart from the unpressed
   one by hue and a faint tint, which is exactly the distinction red-green colour blindness
   erases — so this reader gets a solid ring and a heavier border as well. Only under the
   setting: it is louder than the default, and the default should stay quiet for everyone who
   does not need it. */
:root[data-colorblind] .pt-verdict.on {
    border-width: 2px; padding: 2px 9px;
    box-shadow: inset 0 0 0 1px currentColor; }
:root[data-colorblind] .pt-verdict:not(.on) { opacity: .75; }

/* Running it back. Sits at the FOOT of the wrap-up panel, separated by a rule: the verdicts
   above are about the run that just ended, this is about the next one, and the two should not
   read as one row of controls. */
.pt-again { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
/* The way out. Right-aligned and unemphasised: it is the thing you do when you have finished
   with this screen, not the thing the screen is for. */
.pt-wrapup-out { display: flex; justify-content: flex-end; margin-top: 12px; }
.pt-wrapup-out .action-btn { padding: 8px 22px; }
.pt-again-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-muted); }
/* The select still needs taming here: the global input/select rule (width: 100% + a 10px
   bottom margin) padded it to 45px tall and pushed this row to 68px. ANY select in a flex
   context in this app needs width: auto before it will behave. (The button half of this note
   is gone — .action-btn no longer stretches by default.) */
.pt-again .action-btn { padding: 10px 24px; }
.pt-again select { width: auto; margin-bottom: 0; padding: 5px 8px; font-size: .82rem; }
.pt-again-goal { flex: 0 1 auto; min-width: 0; max-width: 260px; }
.pt-again-note { font-size: .85rem; color: var(--text-muted); }
.pt-again-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
/* Sits between the explanation and the buttons, so it reads as part of the answer being given
   rather than as a setting hidden in a dialog. */
.pt-close-remember { margin-top: 14px; font-size: .82rem; color: var(--text-muted); }

/* The invitation's countdown. Bled past the dialog's 18px/20px padding with negative margins so
   it spans the full width and reads as the edge of the card rather than a control inside it.
   Thin on purpose: it is a deadline you glance at, not a thing to watch.
   Its duration is set INLINE from REGROUP_ASK_MS in party.js — the bar finishing and the timer
   firing are the same moment, and two copies of a number is two numbers.
   Kept under prefers-reduced-motion, unlike the pop-in animations: this one is not decoration.
   Removing it would leave the dialog dismissing itself with no warning that time was passing,
   which is worse for exactly the readers that setting is meant to help. */
.pt-again-timer { height: 2px; margin: 16px -20px -18px; background: var(--border); }
.pt-again-timer span { display: block; height: 100%; width: 0; background: var(--gold);
    animation-name: pt-again-countdown; animation-timing-function: linear;
    animation-fill-mode: forwards; }
@keyframes pt-again-countdown { from { width: 0; } to { width: 100%; } }
/* NO `margin-left: auto` here, and that is a fix rather than an omission. It used to be on
   this rule, so EVERY mini button shoved itself to the right of whatever row it was in — and
   where a row held two of them they each claimed the free space in turn and ended up split
   across the width, visibly attached to nothing. It broke the roster (Remove / Time out) and
   the join-code row (New code) the same way. Every container that uses .pt-mini wants its
   buttons ADJACENT: the seat row, the code row, and the timeout dialog's presets. The one
   place that genuinely wants right-alignment is `.pt-seat-acts`, which carries the auto margin
   on the WRAPPER, where it belongs — one element claiming the space, not each button racing
   for it. */
.pt-mini { font-size: .72rem; padding: 3px 9px; cursor: pointer;
    border: 1px solid var(--border); border-radius: 4px; background: var(--bg-alt); color: var(--text-muted); }
.pt-mini:hover { border-color: var(--gold); color: var(--text-main); }
.pt-mini.ok { color: var(--ok); }
.pt-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 16px;
    border-top: 1px solid var(--border); padding-top: 14px; }
.pt-note { color: var(--text-muted); font-size: .84rem; }
.pt-guest-gate { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pt-guest-gate label { font-size: .8rem; color: var(--text-hint); }
.pt-guest-gate input { width: 150px; }
.pt-guest-gate .pt-hint { flex-basis: 100%; }
.pt-create-row { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 14px; }
.pt-create-row .cs-field { flex: 1 1 130px; }

/* Narrow screens drop the columns that are nice-to-have and keep goal + occupancy, which
   are the two you actually scan for. */
@media (max-width: 720px) {
    .pt-listhead, .pt-row { grid-template-columns: minmax(0, 1fr) 74px; }
    /* Indices shifted by one when Description was inserted at position 2: Players moved
       from nth-child(4) to nth-child(5). Description hides here too — a column that needs a
       hover to be read whole is the wrong thing to keep on a phone. */
    .pt-listhead span:nth-child(2), .pt-listhead span:nth-child(3),
    .pt-listhead span:nth-child(4), .pt-listhead span:nth-child(6),
    .pt-listhead span:nth-child(7) { display: none; }
    .pt-listhead span:nth-child(5) { grid-column: 2; }
    .pt-rowdesc, .pt-server, .pt-policy, .pt-host, .pt-age { display: none; }
}

/* The full-size guild crest (js/guilds.js). Reuses .app-overlay / .app-dialog for the
   backdrop and the box; only the stage is special. 520px as asked, but capped against the
   viewport so a phone gets the largest square that fits instead of a dialog it must scroll. */
.gd-crest-btn { background: none; border: none; padding: 0; cursor: zoom-in; line-height: 0;
    border-radius: 4px; }
.gd-crest-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.gd-zoom-box { max-width: min(560px, calc(100vw - 32px)); }
.gd-zoom-stage {
    width: min(520px, calc(100vw - 76px), calc(100vh - 200px));
    height: min(520px, calc(100vw - 76px), calc(100vh - 200px));
    margin: 0 auto; display: grid; place-items: center;
    border: 1px solid var(--border); background: var(--bg-alt); overflow: hidden;
}
/* `contain`, never `cover`: this view exists so nothing is cropped away. */
.gd-zoom-img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* The generated sigil is an SVG, so it simply fills the stage at any size. */
.gd-crest-zoom-sigil { width: 74%; height: 74%; display: grid; place-items: center; }
.gd-crest-zoom-sigil svg { width: 100%; height: 100%; }

/* --- Party Finder: goal picker, post-run feedback -------------------------------------- */
/* A combobox rather than a <select>: the list is long enough that typing three letters beats
   scrolling, and it grows every time AQW ships another ultra. */
.pt-goalpick { position: relative; }
.pt-goalpick input { width: 100%; box-sizing: border-box; }
.pt-goal-chosen { display: inline-block; margin-top: 6px; font-size: .78rem; font-weight: 600;
    color: var(--gold-text); }
.pt-goal-list { position: absolute; z-index: 30; left: 0; right: 0; top: 100%; margin-top: 2px;
    max-height: 230px; overflow-y: auto; background: var(--bg-panel);
    border: 1px solid var(--gold); box-shadow: var(--shadow-card); }
.pt-goal-opt { display: block; width: 100%; text-align: left; padding: 7px 10px; cursor: pointer;
    background: none; border: none; font: inherit; color: var(--text-main); }
.pt-goal-opt:hover, .pt-goal-opt:focus-visible { background: var(--bg-alt); }
.pt-goal-opt.on { background: var(--gold-light); font-weight: 600; }
.pt-goal-empty { padding: 8px 10px; margin: 0; }

/* The rating form. One row per teammate, criteria in-line after the name — the shape the
   whole thing is meant to be scanned in, not a card per person. */
.pt-fb-box { max-width: min(760px, calc(100vw - 32px)); }
.pt-rate-list { margin-top: 14px; max-height: 46vh; overflow-y: auto; }
.pt-rate-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
    padding: 9px 0; border-bottom: 1px solid var(--border); }
.pt-rate-row:last-child { border-bottom: none; }
.pt-rate-row.guest { opacity: .72; }
.pt-rate-name { font-weight: 600; font-size: .9rem; display: inline-flex; align-items: center; gap: 6px;
    flex: 0 0 auto; min-width: 150px; }
.pt-crits { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.pt-crit { display: inline-flex; align-items: center; gap: 5px; font-size: .8rem;
    color: var(--text-muted); cursor: pointer; }
.pt-crit input { cursor: pointer; }
/* The "?" carries the definition. Without it these are four words everyone reads
   differently, which is not a reputation — it is noise. */
.pt-help { width: 16px; height: 16px; flex: none; border-radius: 50%; cursor: help;
    border: 1px solid var(--border); background: var(--bg-alt); color: var(--text-hint);
    font-size: .66rem; font-weight: 700; line-height: 1; padding: 0; }
.pt-help:hover { border-color: var(--gold); color: var(--gold-text); }

.pt-vote-list { margin-top: 14px; }
.pt-vote { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: start;
    padding: 10px 0; border-bottom: 1px solid var(--border); cursor: pointer; }
.pt-vote:last-child { border-bottom: none; }
.pt-vote input { margin-top: 3px; }
.pt-vote-label { font-weight: 600; font-size: .88rem; }
.pt-vote .pt-hint { grid-column: 2; }

/* Skipping feedback is undoable for four seconds. Bottom-centre, out of the way of the nav
   and the dock, with the remaining time drawn rather than counted down in words. */
.pt-undo { position: fixed; z-index: 1250; left: 50%; transform: translateX(-50%); bottom: 24px;
    display: flex; align-items: center; gap: 12px; padding: 10px 16px;
    background: var(--bg-panel); border: 1px solid var(--gold); box-shadow: var(--shadow-card);
    font-size: .84rem; overflow: hidden; }
.pt-undo-btn { background: none; border: none; padding: 0; font: inherit; font-weight: 700;
    color: var(--gold-text); cursor: pointer; text-decoration: underline; }
.pt-undo-bar { position: absolute; left: 0; bottom: 0; height: 2px; background: var(--gold);
    width: 100%; transform-origin: left; animation: ptUndoDrain linear forwards; }
@keyframes ptUndoDrain { from { transform: scaleX(1); } to { transform: scaleX(0); } }
@media (max-width: 640px) {
    .pt-rate-name { min-width: 0; flex-basis: 100%; }
}

/* Per-goal room size, shown against every option — it is the first thing a host needs and
   the reason the size box is not free. */
.pt-goal-opt { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.pt-goal-cap { font-size: .72rem; color: var(--text-hint); flex: none; }
/* The moderator's dismissal report. */
.pt-mod-runs { margin-top: 8px; border-top: 1px solid var(--border); padding-top: 8px; }
.pt-mod-run { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
    font-size: .78rem; color: var(--text-muted); padding: 4px 0; }
.pt-mod-votes { color: var(--text-hint); }

/* "You still owe someone feedback" — offered, not sprung. The dialog no longer reappears on
   its own once dismissed, so this is the way back to it. */
.pt-pending { display: flex; align-items: center; justify-content: space-between; gap: 14px;
    flex-wrap: wrap; margin: 14px 0; padding: 10px 14px;
    border: 1px solid var(--gold); background: var(--gold-light); font-size: .86rem; }

/* The party chat window. Wears the messenger's own classes so it is the same object to the
   eye and the hand; only the dock it sits in is its own, so the two never fight for the
   same corner. */
/* The party chat is the RIGHT-HAND anchor of the strip: it cannot be closed while you are in
   a party, so it is the stable one, and the messenger's windows extend leftward from it. Same
   56px rail clearance as the messenger dock, which is what makes the two line up. */
.pt-chat-dock { position: fixed; right: 56px; bottom: 0; z-index: 1000;
    display: flex; align-items: flex-end; gap: 10px; }
.pt-chat-dock .msg-who .pt-tag { margin-left: 4px; }

/* The room talking, not a person. Centred and quiet: no bubble, no side, because giving it
   either would make "X joined" look like something X said. */
.pt-sys { text-align: center; font-size: .76rem; color: var(--text-hint);
    margin: 6px 0; line-height: 1.5; }
/* A name that will be a link to a character page. Styled as one already so the affordance is
   there the day those pages exist; it is a <button> because today it opens nothing. */
.pt-charlink { background: none; border: none; padding: 0; font: inherit; cursor: pointer;
    color: var(--gold-text); border-bottom: 1px solid transparent; }
.pt-charlink:hover, .pt-charlink:focus-visible { border-bottom-color: var(--gold); }
.pt-sys .pt-charlink { font-weight: 700; }
.msg-who .pt-charlink { color: inherit; font: inherit; }
.pt-seat-name .pt-charlink { font-weight: 600; }
/* The dock is draggable, so it must not select text as you slide it. */
.pt-chat-dock .chat-bar, .pt-chat-dock .chat-win.min .chat-head {
    user-select: none; -webkit-user-select: none; cursor: grab; }

/* Party Finder reputation on a profile. Three tiers, and the tier is carried by weight and
   border rather than by colour alone — a badge that only differs by hue tells a
   colour-blind reader nothing, and these are meant to be scanned at a glance. */
/* The moderator's server-list strip reuses the verification-settings look. */
.vq-settings #pt-servers { width: 100%; box-sizing: border-box; margin-bottom: 8px; }

/* Leaderboards. Two columns on a wide screen, stacked when there is not room for both. */
.lb-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 26px; }
/* three boards (2026-09-14): players, guilds, outfits */
.lb-three { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); align-items: start; }
.lb-subtabs { margin: 0 0 10px; }
.lb-outfit { display: inline-flex; align-items: center; gap: 8px; background: none; border: 0; padding: 0; color: var(--text-main); font: inherit; cursor: pointer; text-align: left; }
.lb-outfit img { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; background: #0b0b0d; flex: none; }
.lb-outfit > span { display: flex; flex-direction: column; min-width: 0; }
.lb-outfit-title { font-weight: 600; }
.lb-outfit:hover .lb-outfit-title { color: var(--gold-text); }
.lb-crown { display: inline-flex; align-items: center; gap: 4px; margin-left: 8px; font-size: .72rem; color: var(--gold-text); border: 1px solid var(--gold); border-radius: 999px; padding: 1px 8px; white-space: nowrap; vertical-align: middle; }
.lb-crown svg { width: 12px; height: 12px; }
.lb-rec { font-variant-numeric: tabular-nums; }
.lb-first td { background: var(--gold-light); }
.lb-table { width: 100%; border-collapse: collapse; font-size: .86rem; }
.lb-table th { text-align: left; font-size: .66rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-hint); font-weight: 700; padding: 0 8px 6px; border-bottom: 1px solid var(--border); }
.lb-table td { padding: 7px 8px; border-bottom: 1px solid var(--border); }
.lb-table tr:last-child td { border-bottom: none; }
.lb-rank { width: 34px; color: var(--text-hint); font-variant-numeric: tabular-nums; }
.lb-pts { font-weight: 700; font-variant-numeric: tabular-nums; width: 70px; }
.lb-sub { color: var(--text-muted); font-size: .78rem; }

/* A character's page. Dense on purpose: a party host is scanning it in the ten seconds
   before they accept somebody, not reading it. */
.ch-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.ch-name { margin: 0; display: flex; align-items: center; gap: 8px; }
.ch-sub { color: var(--text-muted); font-size: .86rem; margin-top: 2px; }
/* The Share button beside the name, and the one line it may need under the header when the
   link had to be shown rather than copied. `.copied` is what flashCopied() sets for a moment. */
.ch-actions { display: flex; align-items: center; gap: 8px; }
.ch-share.copied { color: var(--success); border-color: var(--success); }
.ch-notice { margin: 10px 0 0; font-size: .85rem; color: var(--text-muted); }
.ch-link { color: var(--text-main); word-break: break-all; user-select: all; }
.ch-group { margin-top: 20px; }
.ch-group h4 { margin: 0 0 8px; font-size: .74rem; text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-muted); display: flex; align-items: baseline; gap: 8px; }
.ch-count { font-size: .8rem; font-weight: 700; color: var(--text-main); letter-spacing: 0; }
.ch-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
/* Held and not-held differ by the MARK and by nothing else — one tick, one cross, and an
   otherwise identical chip. The earlier version also swapped border, weight, colour and
   background, which turned a list of eleven enhancements into eleven small highlights and
   made a page of them exhausting to read. A count sits in the heading for the summary, so the
   chips only have to answer "this one, yes or no", and the glyph already does that.

   The MARK still carries colour — green for yes, red for no — because that is one small glyph
   per row rather than a whole chip, so it reads at a glance without competing with the label.
   It is never the only signal: the tick and cross differ in shape, so nothing is lost to a
   reader who cannot tell the two hues apart. */
.ch-tag { display: inline-flex; align-items: center; gap: 5px;
    font-size: .74rem; padding: 2px 9px 2px 6px; border-radius: 10px;
    border: 1px dashed var(--border); color: var(--text-hint); }
/* `.on` is kept in the markup — it still says what the row MEANS, and the tooltip and tests
   read it — it simply no longer changes how the chip looks. */
.ch-mark { display: inline-flex; color: var(--danger-text); }
.ch-tag.on .ch-mark { color: var(--ok); }
.ch-mark svg { width: 13px; height: 13px; }
/* A group shown as a bare count has no list under it, so it does not need the gap for one. */
.ch-group-count h4 { margin-bottom: 0; }
.ch-empty, .ch-note { color: var(--text-muted); font-size: .84rem; line-height: 1.6; margin-top: 16px; }
.ch-note { border-top: 1px solid var(--border); padding-top: 12px; font-size: .78rem; }
/* From AQW (2026-10-16): what the character's page on AQW says, under the tracker. */
.ch-aqw-facts { margin: 0 0 8px; }
/* The character's picture (AQ Closet's render, kept here) beside its gear; under it on a phone. */
.ch-aqw-look { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 28px; }
.ch-aqw-pic { margin: 0; width: 260px; max-width: 100%; }
.ch-aqw-pic img { display: block; width: 100%; height: auto; }
.ch-aqw-pic figcaption { text-align: center; font-size: .72rem; margin-top: 2px; }
.ch-aqw-pic figcaption a { color: var(--text-muted); }
.ch-aqw-worn ul { list-style: none; margin: 0 0 6px; padding: 0; display: grid; gap: 4px; }
.ch-aqw-worn li { font-size: .88rem; }
.ch-aqw-slot { display: inline-block; min-width: 4.5rem; color: var(--text-muted); font-size: .74rem; text-transform: uppercase; letter-spacing: .05em; }
.ch-aqw-badges { margin-top: 12px; }
.ch-badges { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
/* AQW's badge pictures carry the corners of AQW's own page (#FEF0C1) and a light line along the
   top: clipped just inside them, rounded. */
.ch-badge { width: 45px; height: 41px; object-fit: cover; clip-path: inset(1px round 5px); }

/* A catalogue entry the database has never heard of: invisible to every character, so it has
   to be called out or it reads as "nobody owns this". */
.ch-unresolved { font-size: .82rem; color: var(--danger-text); margin: 0 0 8px; line-height: 1.5; }
.vq-settings textarea[data-cat-text] { width: 100%; box-sizing: border-box; font-family: ui-monospace, monospace;
    font-size: .8rem; margin-bottom: 8px; }
/* (Removed 2026-08-28: an orphaned `color: var(--text-muted); font-style: italic; }` sat here
   with no selector and no opening brace — the body of a rule whose head was deleted at some
   point. Browsers recover from a stray `}` by discarding until they resynchronise, so it never
   showed; the new brace check in dev/check-frontend.js is what found it. Two dangling
   comments describing rules that no longer exist went with it.) */

/* The Party Finder's own overlay (the timeout dialog). Same backdrop weight as the other
   body-level dialogs. */
.pt-overlay { position: fixed; inset: 0; z-index: 1200; background: rgba(0,0,0,.45);
    display: flex; align-items: center; justify-content: center; padding: 16px; }
.pt-to-dialog, .pt-again-dialog { background: var(--bg-panel); border: 1px solid var(--border);
    border-radius: 10px; padding: 18px 20px; max-width: 420px; box-shadow: var(--shadow-card); }
.pt-to-dialog h3, .pt-again-dialog h3 { margin: 0 0 8px; }
.pt-to-row { display: flex; align-items: center; gap: 8px; margin: 12px 0 4px; }
.pt-to-row label { font-size: .84rem; color: var(--text-muted); }
/* The global input rule is (0,1,1); the attribute keeps this above it. */
.pt-to-row input[type="number"] { width: 90px; margin-bottom: 0; }
.pt-to-msg { font-size: .78rem; color: var(--danger-text); min-height: 1em; display: block; }

/* An ARMED destructive button: the same button, now stating what the next click does. The
   colour shift is reinforcement — the changed LABEL is the accessible signal. */
.action-btn.pt-armed { border-color: var(--danger); color: var(--danger-text); font-weight: 700; }

/* The join code. Big, monospaced and letter-spaced, because its job is being read aloud or
   copied off a second screen — the ambiguous glyphs are already excluded server-side, and
   this makes the rest unmistakable. A button, not a span: clicking it copies. */
.pt-codeblock .pt-hint { margin-top: 6px; }
.pt-coderow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pt-code { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    font-size: 1.5rem; font-weight: 700; letter-spacing: .22em; text-indent: .22em;
    padding: 6px 14px; cursor: pointer;
    color: var(--gold-text); background: var(--gold-light);
    border: 1px dashed var(--border-gold); border-radius: 8px; }
.pt-code:hover { border-style: solid; }
/* Joining by code is not a filter, so it sits on its own line under them. */
.pt-codejoin { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: -6px 0 14px; }
.pt-codejoin label { font-size: .82rem; color: var(--text-muted); }
/* The global input rule is (0,1,1) and beats a plain class, so the attribute is needed here. */
.pt-codejoin input[type="text"] { width: 9em; margin-bottom: 0; text-transform: uppercase;
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; letter-spacing: .12em; }

/* ============================================================================
   THE CONTROL RAIL (2026-08-29)
   A fixed pill of account controls down the right edge: avatar, settings, theme,
   session. Adapted from a portable spec; three of its decisions did NOT survive
   contact with this app, and the reasons are worth keeping:

   1. THE SPEC'S OWN PALETTE IS GONE. It shipped ~15 hard-coded colour tokens
      (--rail-fill, --rail-icon, --power-ink …). This app already has a measured
      three-theme palette, so the rail reads THAT instead: a second set would have
      needed its own contrast pass per theme and would drift from the first the
      moment either moved.
   2. z-index IS NOT 46. The spec assumed page chrome at 30-45 and modals at 50.
      Here the friends drawer is 900, the message dock 940, the chat dock 1000 and
      the overlays 1200. The rail sits at 890 — above the page and the header, and
      deliberately BELOW all four of those, so the friends drawer covers it when it
      opens rather than the rail floating over a panel the reader just opened.
   3. It IS at the spec's own right: 0.9rem, hard against the edge. That puts it in
      the same strip as .fr-drawer (pinned right:0, opening to 270px) — accepted on
      purpose, and (2) is what makes it read correctly: the drawer wins.
   ============================================================================ */
.control-rail {
    position: fixed;
    right: 0.9rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 890;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 10px 8px;
    /* ONE size for every control, so they cannot drift apart. They are centred by
       align-items, but equal boxes are what makes the column read as a single stack of
       discs rather than four different shapes that happen to share a midline. */
    --rail-size: 36px;
    border-radius: 999px;
    background: var(--bg-panel);
    /* BORDERLESS — the shadow is the edge (2026-08-29, owner's request).
       
       That makes the shadow load-bearing rather than decorative, because the rail sits on a
       ground of its OWN colour: measured, `--bg-panel` is #ffffff on a #ffffff page in light
       and #141414 on #141414 in dark, so the 1px border was the only thing separating it from
       the page. Dropping the border and keeping `--shadow-card` would have erased the rail in
       light, where that token is 3% black.
       
       Two layers, which is what makes a small floating object read as lifted rather than
       outlined: a tight contact shadow holding the near edge, and a wide soft one for the
       ambient falloff. */
    box-shadow: 0 1px 3px rgba(0, 0, 0, .07), 0 8px 24px rgba(0, 0, 0, .12);
}
/* DARK CANNOT USE A SHADOW FOR THIS. Black on near-black is invisible however it is tuned, so
   the separation has to come from LUMINANCE: the rail lifts to --bg-alt (#1c1c1c) above the
   #141414 page, and the shadow goes back to being depth rather than definition. Both values
   are already in the palette; nothing new was invented for the rail. */
[data-theme="dark"] .control-rail {
    background: var(--bg-alt);
    box-shadow: 0 2px 6px rgba(0, 0, 0, .55), 0 12px 32px rgba(0, 0, 0, .45);
}
/* Glass only where the theme is already glassy — Gilded is the theme built around
   translucency, and it is the one place a blurred rail reads as intended rather
   than as a smudge over parchment. It separates on its own (a translucent white over a
   near-black ground measured 18.75:1), so it needs no lift, only depth. */
[data-theme="dark"][data-skin="gilded"] .control-rail {
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(14px) saturate(140%);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    box-shadow: 0 8px 32px rgba(0, 0, 0, .5);
}

.rail-icon-btn {
    box-sizing: border-box;
    display: flex; align-items: center; justify-content: center;
    width: var(--rail-size); height: var(--rail-size); padding: 0;
    border: 1px solid transparent;   /* matches the bordered controls' box exactly */
    background: none; border-radius: 999px;
    color: var(--text-muted); cursor: pointer;
    transition: background-color .2s ease, color .2s ease;
}
.rail-icon-btn:hover, .rail-icon-btn:focus-visible {
    color: var(--gold-text); background: var(--gold-light);
}
.rail-icon-btn svg { width: 20px; height: 20px; }

/* The cog turns a quarter clockwise on hover and unwinds on the way out. The
   transition sits on the RESTING state on purpose: put it under :hover and only
   the way in animates, and the return snaps. */
#rail-settings svg { transition: transform .3s cubic-bezier(.4, 0, .2, 1); }
#rail-settings:hover svg, #rail-settings:focus-visible svg { transform: rotate(90deg); }

.rail-avatar-wrap {
    box-sizing: border-box;
    width: var(--rail-size); height: var(--rail-size); flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    border-radius: 999px; cursor: pointer;
    border: 1px solid var(--border-gold);
    overflow: hidden; transition: border-color .2s ease, transform .2s ease;
}
/* `hidden` is only `display: none` from the UA sheet, which ANY class rule setting display
   outranks — so the guest state left an empty bordered circle sitting in the rail. Restated
   here because the rule above sets display at all. */
.rail-avatar-wrap[hidden] { display: none; }
.rail-avatar-wrap:hover, .rail-avatar-wrap:focus-visible {
    border-color: var(--gold); transform: scale(1.05);
}
.rail-avatar-wrap .av { width: 100%; height: 100%; font-size: .95rem; }
/* A FRAMED avatar adds a wrapper between the two, and the rule above then sized `.av` against
   a box that was itself sizing to `.av` — both collapsed to ~2px. The frame wrapper has to
   take the rail's fixed size so the avatar has something real to be 100% of. */
.rail-avatar-wrap .av-framed { width: 100%; height: 100%; }
/* The frame is drawn to overhang, and this wrapper clips. `:has` keeps the clipping for a
   plain avatar — where it is what keeps the circle clean — and lifts it only when there is an
   ornament that is MEANT to spill past the edge. The gold ring goes with it: two concentric
   rings, one of them ours, reads as a mistake. */
.rail-avatar-wrap:has(.av-frame) { overflow: visible; border-color: transparent; }

/* Session. Red power when signed in, green arrow when signed out — the app's own
   --danger / --ok rather than the spec's rose/emerald, so it stays in palette. */
.rail-session-btn {
    display: flex; align-items: center; justify-content: center;
    width: var(--rail-size); height: var(--rail-size); flex: 0 0 auto; padding: 0;
    border-radius: 999px; cursor: pointer;
    transition: background-color .2s ease, color .2s ease, transform .2s ease;
}
.rail-session-btn { border: 1px solid transparent; box-sizing: border-box; }
.rail-session-btn svg { width: 17px; height: 17px; }
.rail-session-btn:hover { transform: scale(1.06); }
.rail-session-btn.is-signed-in {
    color: var(--danger-text);
    background: color-mix(in srgb, var(--danger) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
}
.rail-session-btn.is-signed-in:hover { background: var(--danger); color: #fff; }
.rail-session-btn.is-signed-out {
    color: var(--ok);
    background: color-mix(in srgb, var(--ok) 12%, transparent);
    border: 1px solid color-mix(in srgb, var(--ok) 35%, transparent);
}
.rail-session-btn.is-signed-out:hover { background: var(--ok); color: #fff; }

@media (prefers-reduced-motion: reduce) {
    .rail-icon-btn, #rail-settings svg, .rail-session-btn, .rail-avatar-wrap { transition: none; }
}

/* On a narrow window the rail lies down along the bottom edge. It sits ABOVE the
   chat docks' own strip rather than beside it — those are pinned to bottom:0 and
   the rail would land on top of them. */
@media (max-width: 700px) {
    .control-rail {
        top: auto; bottom: 12px;
        right: 50%; transform: translateX(50%);
        flex-direction: row;
    }
    /* While a chat is expanded on a phone the dock owns the bottom of the screen
       (see .msg-chat-full), so the rail steps out of the way entirely. */
    body.msg-chat-full .control-rail { display: none; }
}

/* --- the settings dialog --- */
/* ---- The settings dialog's sections -----------------------------------------------------
   The tabs are `.qt-tabs`/`.qt-tab`, borrowed wholesale from the quest-collection tables at
   the owner's request. Reused rather than restyled, so a row of dividers means the same thing
   wherever it turns up — and so the active tab keeps reading as the front sheet of the panel
   below it, which is the whole trick of that pattern. */
.rail-settings-dialog { max-width: 640px; }
.rs-tabs { padding: 4px 0 0; margin-bottom: -1px; }
/* The pane is the sheet the active tab merges into, so it carries the border the tab's
   bottom edge is erased against. */
.rs-pane {
    border: 1px solid var(--border);
    background: var(--bg-panel);
    padding: 16px 16px 4px;
    /* Tall content (139 thumbnails) scrolls INSIDE the pane rather than growing the dialog
       past the viewport. The dialog's own max-height still applies as a backstop. */
    max-height: min(60vh, 520px);
    overflow-y: auto;
}
/* Borrowed sections arrive wearing their PROFILE-PAGE chrome: a card border, a 2px gold left
   edge, a panel background and 30px of padding. On that page it was right — each was a card
   among cards. Inside a pane that already has its own border it reads as a box in a box in a
   box, which is what the owner reported.
   
   Stripped HERE rather than at the source, so the markup stays correct for wherever else it is
   ever shown. `!important` is not decoration: the gold edge and the 40px margin are INLINE
   styles on the element, which no class can outrank. */
.rs-mount > *,
.rs-mount .card {
    border: none !important;
    background: none !important;
    box-shadow: none !important;
    padding: 0 !important;
    max-width: none !important;
    margin: 0 !important;
}
/* Spacing BETWEEN adopted sections, now that their own margins are gone. */
.rs-mount > * + * { margin-top: 22px !important; }
/* One heading style for every section in this dialog, borrowed or built — the pane's own h4.
   The adopted markup uses h2 and h3, and left alone they arrived louder than the sections
   beside them and carried the global heading underline into a dialog that has no use for it. */
.rs-mount h2, .rs-mount h3 {
    font-size: .95rem; font-weight: 700; line-height: 1.3;
    margin: 0 0 8px; padding: 0; border: none; color: var(--text-main);
}

/* The art pickers. A dense grid of thumbnails: the art IS the label, so the names are titles
   and alt text rather than captions that would treble the height of the grid. */
.rs-grid { display: grid; gap: 8px; }
.rs-grid-background { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
.rs-grid-avatar { grid-template-columns: repeat(auto-fill, minmax(58px, 1fr)); }
/* FRAMES GET DOUBLE THE CELL (owner's call, 2026-08-29), fewer per row and worth it. An avatar
   swatch is one picture; a frame swatch is a COMPOSITE — your face inside an ornament — and at
   58px the ornament was a thin ring you had to lean in to read. The avatar inside scales with
   it, or the composite would just float in a bigger empty box. */
.rs-grid-frame { grid-template-columns: repeat(auto-fill, minmax(116px, 1fr)); }
.rs-pick {
    padding: 0; cursor: pointer; overflow: hidden; position: relative;
    background: var(--bg-alt); border: 2px solid transparent; border-radius: var(--r-sm);
    transition: border-color .15s ease, transform .15s ease;
}
.rs-pick img { display: block; width: 100%; height: 100%; object-fit: cover; }
.rs-grid-background .rs-pick { aspect-ratio: 16 / 9; }
.rs-grid-avatar .rs-pick, .rs-grid-frame .rs-pick { aspect-ratio: 1; }
/* `.rs-pick img` above sizes an art thumbnail to fill its cell — and it also matched the frame
   OVERLAY inside a composite swatch, pinning a 152% ornament to 100% of the avatar. Every frame
   preview therefore rendered at exactly the size of the face it is supposed to surround, which
   is the one thing a frame must never do. Two classes beat one class plus a type, so this wins
   without !important. `contain` because a frame is mostly empty in the middle and cropping its
   corners is how you lose the part that identifies it. */
.rs-pick .av-frame { width: 152%; height: 152%; object-fit: contain; }
/* The avatar inside a frame swatch keeps `cover`: it is a portrait filling a circle, and the
   rule above would otherwise have let the generic `contain` reach it too. */
.rs-grid-frame .rs-pick .av img { object-fit: cover; }
.rs-pick:hover { border-color: var(--border-gold); transform: translateY(-1px); }
.rs-pick:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* Gold border AND a check, never colour alone — the selected state has to survive a reader
   who cannot separate gold from grey. */
.rs-pick.on { border-color: var(--gold); }
.rs-pick.on::after {
    content: '✓'; position: absolute; right: 3px; bottom: 2px;
    font-size: .8rem; font-weight: 700; line-height: 1;
    color: var(--on-gold); background: var(--gold);
    border-radius: 3px; padding: 1px 3px;
}
/* A swatch and its magnifier. The cell positions the zoom button over the corner of the pick
   without nesting one button inside another. */
.rs-cell { position: relative; display: block; }
.rs-cell .rs-pick { width: 100%; }
.rs-zoom {
    position: absolute; top: 2px; right: 2px; z-index: 2;
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0; cursor: pointer;
    color: var(--text-main); background: var(--bg-panel);
    border: 1px solid var(--border); border-radius: var(--r-sm);
    /* Revealed on hover or focus so a dense grid is art rather than a field of buttons —
       but ALWAYS present for a keyboard, which cannot hover. */
    opacity: 0; transition: opacity .15s ease;
}
.rs-cell:hover .rs-zoom, .rs-zoom:focus-visible { opacity: 1; }
.rs-zoom:hover { color: var(--gold-text); border-color: var(--border-gold); }
/* Touch has no hover state at all, so the control simply stays visible there. */
@media (pointer: coarse) { .rs-zoom { opacity: 1; } }

/* The sigils and the "Initial" swatch size themselves; art fills its cell. */
.rs-grid-avatar .rs-pick .av, .rs-grid-frame .rs-pick .av-framed { margin: 0 auto; }
/* 64px avatar → a 97px frame in a 116px cell, which fills it the way the 28px avatar filled
   the old 58px one. Sized here rather than by passing a class, so the two numbers that have to
   stay in proportion sit on adjacent lines. */
.rs-grid-frame .av { width: 64px; height: 64px; font-size: 1.7rem; }
.rs-none-label { font-size: .68rem; color: var(--text-muted); }

/* "How you look now" — the current avatar and frame, with its own full-size button. */
.rs-current { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.rs-current h4 { width: 100%; margin: 0; }
.rs-current-art { display: inline-flex; padding: 6px 10px; }
.rs-current-zoom { flex: 0 0 auto; }

/* The full-size viewer. The ONE overlay in the app that deliberately covers another, so it
   sits above the settings dialog's 1200 rather than tying with it. */
.rs-zoom-overlay { z-index: 1300; }
.rs-zoom-box { max-width: min(760px, calc(100vw - 32px)); }
.rs-zoom-stage {
    display: grid; place-items: center; margin: 10px 0 4px;
    background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--r-sm);
    padding: 14px; min-height: 220px;
}
/* Capped to the VIEWPORT, not to a fixed pixel size: "maximum resolution" means as large as
   the file and the screen allow, and a small source is not upscaled past its natural size. */
.rs-zoom-img { max-width: min(100%, 90vw); max-height: 62vh; width: auto; height: auto; display: block; }
.rs-zoom-frame { display: inline-flex; }
/* The framed composite at size. The frame overhangs at 152%, so the stage has room for it. */
.av.rs-zoom-av { width: 200px; height: 200px; font-size: 5rem; }

/* ---- The app's own input / choice dialog (js/ask.js) ------------------------------------
   Replaces window.prompt, so it has to be at least as usable as the thing it replaces: the
   field is focused on open, Enter confirms, Escape cancels. */
.ask-overlay { z-index: 1400; }   /* above the settings dialog AND its full-size viewer */
.ask-dialog { max-width: 440px; }
.ask-dialog h3 { margin: 0 0 8px; }
.ask-message { margin: 0 0 12px; font-size: .88rem; color: var(--text-muted); line-height: 1.5; }
.ask-dialog #ask-input { width: 100%; box-sizing: border-box; margin-bottom: 4px; }
.ask-choices { display: flex; flex-direction: column; gap: 6px; margin-bottom: 6px; }
.ask-choice {
    display: flex; flex-direction: column; gap: 2px; text-align: left;
    padding: 9px 12px; cursor: pointer; font-family: inherit;
    background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--r-sm);
    transition: border-color .15s ease, background .15s ease;
}
.ask-choice:hover { border-color: var(--border-gold); background: var(--gold-light); }
.ask-choice:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }
.ask-choice-label { font-size: .9rem; font-weight: 600; color: var(--text-main); }
.ask-choice-hint { font-size: .76rem; color: var(--text-muted); }
/* A destructive confirmation reads as one. Colour is NOT the only signal — the label says
   what will happen ("Remove", "Disband"), so this survives a reader who cannot see it. */
.ask-danger { border-color: var(--danger); color: var(--danger-text); }
.ask-danger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); }

/* ---- Artwork: the CSS half of the save-friction (js/protect-art.js) ---------------------
   `user-drag` is what actually stops a picture being dragged to the desktop in WebKit/Blink;
   the JS listener covers the browsers that ignore it. `touch-callout` suppresses iOS's
   long-press save sheet. `user-select` keeps a drag from selecting the image as content.
   
   Scoped to artwork. Applying any of this to the page at large would take selection away from
   readers who have every right to it — this is friction against casual copying, not a lockdown,
   and the difference is who else pays for it. */
.av img, .av-frame, .gd-crest img, .rs-pick img, .aa-preview img, .rs-zoom-img {
    -webkit-user-drag: none;
    -webkit-touch-callout: none;
    user-select: none;
    -webkit-user-select: none;
    pointer-events: none;      /* the button around it owns the click; the image never does */
}
/* The full-size viewer's image is not inside a button, so it keeps its own pointer events —
   without this the overlay's click-away could not tell it apart from the backdrop. */
.rs-zoom-img { pointer-events: auto; }

/* ---- Admin: the app's default art (js/admin-appearance.js) ------------------------------ */
.aa-panel { max-width: 620px; }
.aa-intro { margin: 0 0 20px; font-size: .88rem; color: var(--text-muted); line-height: 1.55; }
.aa-field { margin-bottom: 20px; }
.aa-label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: 6px; }
.aa-row { display: flex; align-items: center; gap: 12px; }
.aa-row select { flex: 1; margin-bottom: 0; }
/* The preview is a fixed box whether or not it holds a picture, so choosing does not shunt
   the select sideways as the image swaps. */
.aa-preview {
    flex: 0 0 auto; width: 84px; height: 52px; overflow: hidden;
    display: grid; place-items: center;
    background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--r-sm);
}
.aa-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.aa-preview.empty { border-style: dashed; }
.aa-none { font-size: .7rem; color: var(--text-muted); }
.aa-hint { margin: 5px 0 0; font-size: .78rem; color: var(--text-muted); }

/* A guild's fallback mark: its initial on a hue derived from the name, used when no default
   crest is configured. Replaced the twelve generated sigils, deleted 2026-08-29. */
.gd-crest-mono { font-weight: 700; color: hsl(var(--av-h, 40) 45% 32%);
    background: hsl(var(--av-h, 40) 45% 93%); border-color: hsl(var(--av-h, 40) 40% 82%); }
[data-theme="dark"] .gd-crest-mono { color: hsl(var(--av-h, 40) 55% 74%);
    background: hsl(var(--av-h, 40) 30% 18%); border-color: hsl(var(--av-h, 40) 25% 30%); }
.gd-crest-art { padding: 0; overflow: hidden; }
.gd-crest-art img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The confirmation that REPLACES a submitted form: a circled check and one line, centred,
   sized so the dialog does not lurch to a different width as it swaps in. */
.fb-done { text-align: center; padding: 34px 26px; max-width: 340px; }
.fb-done-mark {
    display: inline-flex; align-items: center; justify-content: center;
    width: 64px; height: 64px; border-radius: 50%;
    color: var(--ok); border: 2px solid var(--ok);
    background: color-mix(in srgb, var(--ok) 10%, transparent);
}
.fb-done-mark svg { width: 34px; height: 34px; }
.fb-done-text { margin: 14px 0 0; font-size: 1rem; font-weight: 600; color: var(--text-main); }

.rs-pick-none {
    display: flex; align-items: center; justify-content: center;
    font-size: .72rem; color: var(--text-muted); background: var(--bg-alt);
}
.rail-settings-dialog h4 { margin: 0 0 8px; font-size: .95rem; }
.rs-block { margin-bottom: 22px; }
.rs-block:last-of-type { margin-bottom: 14px; }
.rs-hint { font-size: .82rem; color: var(--text-hint); margin: 0 0 10px; line-height: 1.5; }
/* The one checkbox in settings. Laid out like a row rather than a form field because it is a
   preference you flip, not a value you fill in. */
.rs-toggle { display: flex; align-items: center; gap: 9px; cursor: pointer; font-size: .88rem;
    margin-bottom: 8px; }
.rs-toggle input { margin: 0; flex: none; }
.rs-themes { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.rs-theme {
    display: flex; flex-direction: column; gap: 2px; text-align: left;
    padding: 10px 12px; cursor: pointer; font: inherit;
    background: var(--bg-alt); color: var(--text-main);
    border: 1px solid var(--border); border-radius: var(--r-sm);
    transition: border-color .15s, background .15s;
}
.rs-theme:hover { border-color: var(--border-gold); }
.rs-theme.on { border-color: var(--gold); background: var(--gold-light); }
.rs-theme-name { font-weight: 600; font-size: .88rem; }
.rs-theme-blurb { font-size: .74rem; color: var(--text-hint); }
/* Overlay opacity sliders (2026-09-14) */
.rs-sub { font-weight: 400; color: var(--text-hint); font-size: .8rem; margin-left: 6px; }
.rs-range { display: grid; grid-template-columns: 150px 1fr 48px; align-items: center; gap: 12px; margin: 8px 0; font-size: .85rem; cursor: pointer; }
.rs-range > span { color: var(--text-main); line-height: 1.25; }
.rs-range small { display: block; font-size: .7rem; color: var(--text-hint); }
.rs-range input[type=range] { width: 100%; margin: 0; accent-color: var(--gold); }
.rs-range output { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-hint); font-size: .8rem; }
.rs-op-foot { display: flex; align-items: center; gap: 12px; margin-top: 8px; flex-wrap: wrap; }
.rs-op-foot .fb-inline-link:disabled { opacity: .5; cursor: default; }
@media (max-width: 560px) { .rs-range { grid-template-columns: 1fr 48px; } .rs-range > span { grid-column: 1 / -1; } }

/* Themes past light/dark show a NUMERAL instead of a glyph — see applyThemeMode.
   Sized and weighted to sit at the same optical weight as the 20px icons beside it,
   and tabular so a future "10" cannot shuffle the button's width. */
.theme-num {
    font-size: .95rem; font-weight: 700; line-height: 1;
    font-variant-numeric: tabular-nums;
}

/* The header's character name scrolls here. Without the margin the fixed header lands on top
   of the heading it just jumped to — `scroll-margin-top` is the property that exists for
   exactly this, and it belongs on the TARGET rather than as arithmetic at the call site. */
#my-characters { scroll-margin-top: calc(var(--nav-h) + 14px); }

/* ============================================================================
   SCROLLBARS ARE HIDDEN APP-WIDE (2026-08-29, standing rule from the owner)
   Hidden, NOT disabled. Every one of these surfaces still scrolls by wheel,
   trackpad, touch, keyboard and programmatic scrollTop — only the painted bar is
   gone. That distinction is the whole implementation: `overflow: hidden` would
   have trapped content, so nothing here touches `overflow`.

   Three properties, because no single one covers every engine:
     scrollbar-width      Firefox and the modern standard
     -ms-overflow-style   legacy Edge
     ::-webkit-scrollbar  Chrome, Edge, Safari
   Width AND height are zeroed — a horizontal bar is a scrollbar too, and setting
   only `width` leaves it painted on an overflowing row.

   OPTING BACK IN: add `.wants-scrollbar`, or name the element in the list below.
   The ones already there are the surfaces that had hand-styled scrollbars before
   this rule existed, which is exactly what "asked for explicitly" means (the Admin
   sidebar was a third until the 2026-09-23 redesign replaced it with wrapping pills). They win
   on specificity alone (a class or id beats `*`), so their existing rules needed
   no edit — but `scrollbar-width` has to be restored per element, since the
   universal selector reaches them too.
   ============================================================================ */
* {
    scrollbar-width: none;
    -ms-overflow-style: none;
}
*::-webkit-scrollbar { width: 0; height: 0; }

/* The explicit opt-ins. */
#rep-list,
.custom-dropdown-list,
.wants-scrollbar {
    scrollbar-width: thin;
    -ms-overflow-style: auto;
}
/* A reusable opt-in for anything added later, styled to match the two above so a
   new scrollbar does not arrive looking like a fourth design. */
.wants-scrollbar::-webkit-scrollbar { width: 6px; height: 6px; }
.wants-scrollbar::-webkit-scrollbar-track { background: var(--bg-body); }
.wants-scrollbar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.wants-scrollbar::-webkit-scrollbar-thumb:hover { background: var(--gold); }

/* --- account recovery (js/credentials.js) --- */
.cr-forgot { display: inline-block; margin-top: 4px; font-size: .82rem; }
.cr-dialog h3 { margin: 0 0 10px; }
.cr-choices { display: grid; gap: 8px; margin: 4px 0 2px; }
.cr-label { display: block; margin: 12px 0 5px; font-size: .85rem; color: var(--text-muted); }
/* A reset code is read off one screen and typed into another, so it is set wide and
   monospaced — the same treatment the party join code gets, for the same reason. */
.cr-code-input {
    font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
    letter-spacing: .18em; text-transform: uppercase; font-size: 1.05rem;
}

/* The confirmation notice. Fixed, above the page but below every dialog, and dismissible —
   it reports something that already happened, so it must never block anything. */
.cr-banner {
    position: fixed; top: calc(var(--nav-h) + 12px); left: 50%; transform: translateX(-50%);
    z-index: 1100; max-width: min(560px, calc(100vw - 32px));
    display: flex; align-items: center; gap: 10px;
    padding: 10px 12px; font-size: .86rem;
    background: var(--bg-panel); border: 1px solid var(--border);
    border-left: 3px solid var(--ok); border-radius: var(--r-sm); box-shadow: var(--shadow-card);
}
.cr-banner.bad { border-left-color: var(--danger); }
/* The self-clearing fade. Paired with a removal in credentials.js — a banner left at zero
   opacity would still cover the page and eat the clicks meant for what is under it. */
.cr-banner { transition: opacity .3s ease, transform .3s ease; }
.cr-banner.is-leaving { opacity: 0; transform: translateX(-50%) translateY(-6px); pointer-events: none; }
.cr-banner-x {
    background: none; border: none; padding: 0; cursor: pointer; color: var(--text-muted);
    display: inline-flex; align-items: center;
}
.cr-banner-x:hover { color: var(--text-main); }

/* ============================================================================
   NATIVE WIDGETS MUST BE TOLD WHICH THEME THEY ARE IN (2026-08-29)
   A <select>'s dropdown LIST is painted by the browser, not by this stylesheet —
   no rule here can reach it. Left unspecified, the browser assumes a light page
   and paints a white popup, so in the dark themes the server list rendered as
   pale grey text on white and was genuinely unreadable.
   `color-scheme` is the one property that tells it otherwise. It also fixes the
   scrollbars inside those popups, the caret colour in text fields, and the
   default styling of date/number spinners — all the parts of a form control that
   live outside CSS's reach.
   Declared per theme rather than globally, because the whole point is that it
   must track the theme in force.
   ============================================================================ */
:root { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }

/* The option ROWS can be styled in Chromium even though the popup chrome cannot,
   so they are pinned to the theme's own surface as well. Belt and braces: on an
   engine that ignores these, color-scheme above still produces a readable popup. */
[data-theme="dark"] select option,
[data-theme="dark"] datalist option {
    background-color: var(--bg-panel);
    color: var(--text-main);
}
[data-theme="dark"][data-skin="gilded"] select option,
[data-theme="dark"][data-skin="gilded"] datalist option {
    /* Gilded's --bg-panel is nearly transparent glass, which a native popup cannot
       composite — it needs a solid colour or the text lands on white. */
    background-color: #14161b;
    color: var(--text-main);
}

/* Icon-only roster controls (Remove / Time out / New code). Square rather than
   text-width so the three line up in a column of seats, and sized to the same
   optical weight as the seat text beside them. */
.pt-mini.pt-icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0; flex: 0 0 auto;
}
.pt-mini.pt-icon svg { width: 15px; height: 15px; }

/* The one-line party notice ("Party no longer exists."). Reuses the app's overlay so
   it dims and dismisses like every other dialog here. */
.pt-notice-dialog { max-width: 380px; text-align: center; }
.pt-notice-dialog p { margin: 0 0 18px; color: var(--text-main); }

/* The standing "confirm your email" bar. Pinned to the BOTTOM rather than the top: the top is
   where the one-off confirmation notice appears, and two bars stacked there read as one broken
   thing. Below the chat docks' z-index so it never covers a conversation. */
.cr-verify-banner {
    position: fixed; left: 50%; bottom: 12px; transform: translateX(-50%);
    z-index: 930; max-width: min(680px, calc(100vw - 32px));
    display: flex; align-items: center; gap: 12px;
    padding: 10px 14px; font-size: .86rem;
    background: var(--bg-panel); border: 1px solid var(--border);
    border-left: 3px solid var(--gold); border-radius: var(--r-sm); box-shadow: var(--shadow-card);
}
.cr-verify-banner .action-btn { padding: 6px 14px; font-size: .74rem; }
@media (max-width: 700px) {
    /* On a phone the chat dock owns the bottom strip; the bar steps above it. */
    .cr-verify-banner { bottom: 72px; flex-wrap: wrap; }
}


/* This week's bosses, on a character's page (2026-09-08). A grid of pills rather than a list:
   thirteen names are scanned, not read, and the question is "which ones are missing". The
   tick/cross carries the state; the tint is confirmation, never the only signal. */
.ch-weekly h4 { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.ch-week-reset { margin-left: auto; font-size: .72rem; font-weight: 400; text-transform: none;
    letter-spacing: 0; color: var(--text-hint); }
.ch-week-hint { margin: 0 0 8px; }
/* The tracker's rows ARE the enhancement chips (2026-09-09) — same class, same dashed edge,
   same 13px mark in green-or-red — so a page that shows both reads as one list of the same
   thing rather than two designs stacked. The earlier version was a grid of filled boxes with a
   green wash, which the owner rightly wanted gone: it made the tracker look like a different
   app from the enhancements right under it.
   The ONE difference, deliberately kept: a cleared entry takes the THEME accent — gold in the
   light theme, blood-red in the dark one, whatever --gold is — rather than the green the mark
   already carries. It says "done this period" louder than a held enhancement needs to, because
   that is the question the whole section is there to answer at a glance. */
.ch-week-list { list-style: none; margin: 0 0 6px; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.ch-week { display: inline-flex; }
/* A chip that is a BUTTON (the owner's) still looks like the chip that is a span (everyone
   else's): the UA's button chrome is stripped and the chip's own rule does the rest. */
/* font-family and font-weight only — NOT the font shorthand, which also resets font-size and
   silently undid the chip's .74rem, leaving the tracker at 16px beside 11.84px enhancements. */
.ch-week-btn { cursor: pointer; background: none; font-family: inherit; font-weight: inherit; line-height: inherit; text-align: left;
    transition: border-color .12s, color .12s, background .12s; }
.ch-week-btn:hover:not(:disabled) { border-style: solid; border-color: var(--gold); color: var(--text-main); }
.ch-week-btn:disabled { cursor: default; }
/* One line, never shrunk: as a flex item the name could be squeezed below its own width and
   wrap ("Ultra / Dage"), which made the chip a line taller than the enhancement chip whose
   name is bare text and cannot. */
.ch-week-name { white-space: nowrap; flex: none; }
.ch-weekly .ch-tag.on { border-style: solid; border-color: var(--gold); background: var(--gold-light); color: var(--gold-text); }
.ch-week-src { flex: none; font-size: .58rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-hint); border: 1px solid var(--border); border-radius: 4px; padding: 0 4px; margin-left: 2px; }
/* Never rely on colour alone: with the setting on, a cleared chip also gets a heavier edge. */
:root[data-colorblind] .ch-weekly .ch-tag.on { border-width: 2px; padding: 1px 8px 1px 5px; }

/* The moderator's weekly-boss checklist: a wrapping grid of ticks, since thirteen names in one
   column is a scroll and one row is a squint. */
.pt-weekly-picks { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 4px 14px; margin: 0 0 10px; }

/* The daily half of the tracker (2026-09-08). A quest wears a badge the way a witnessed run
   does, so a reader can tell a boss from a chore without reading the name. */
.ch-week-src.quest { color: var(--text-muted); }
/* The moderator's period radios: one row per goal, three columns of radios after the name. */
.pt-period-picks { display: grid; gap: 2px 0; margin: 0 0 10px; max-width: 560px; }
.pt-period-row { display: grid; grid-template-columns: minmax(0, 1fr) 70px 70px 70px; align-items: center;
    padding: 3px 0; border-bottom: 1px solid var(--border); font-size: .84rem; }
.pt-period-row:last-child { border-bottom: none; }
.pt-period-row input { justify-self: center; margin: 0; }
.pt-period-head { font-size: .66rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-hint); }
.pt-period-head span:not(:first-child) { text-align: center; }
#pt-daily-quests { width: 100%; box-sizing: border-box; margin-bottom: 8px; }

/* The Users list: a name is a button that opens the moderator summary. Dressed as the bold text
   it replaced, with the accent on hover, so the table does not sprout a column of buttons. */
.admin-user-open { background: none; border: none; padding: 0; margin: 0; cursor: pointer;
    font: inherit; color: inherit; text-align: left; }
.admin-user-open:hover strong, .admin-user-open:focus-visible strong { color: var(--gold-text); text-decoration: underline; }
/* The summary card (2026-09-09). A definition list, since it is a set of labelled facts and not
   a form; the label column is fixed so the values line up down the card. */
.mod-summary { max-width: 520px; }
.mod-summary h3 { display: flex; align-items: center; gap: 8px; }
.mod-summary-role { font-size: .7rem; }
.mod-summary-grid { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 8px 14px; margin: 12px 0 0; font-size: .88rem; }
.mod-summary-grid dt { color: var(--text-muted); }
.mod-summary-grid dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.mod-summary-list { margin: 4px 0 0; padding-left: 16px; }
.mod-summary-list li { margin: 2px 0; }
.mod-summary-list small { color: var(--text-hint); margin-left: 4px; }
.mod-summary-warn { color: var(--danger-text); }

/* ---------------- Outfits: the gallery and the battler (2026-09-12) ---------------- */
.of-tabs { display: flex; gap: 6px; margin-bottom: 18px; border-bottom: 1px solid var(--border); }
.of-tab { background: transparent; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; color: var(--text-muted); padding: 10px 14px; cursor: pointer; font: inherit; text-transform: uppercase; letter-spacing: 1px; font-size: .85rem; }
.of-tab.on, .of-tab:hover { color: var(--gold-text); border-bottom-color: var(--gold); }
.of-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.of-sorts { display: flex; gap: 4px; }
.of-sort { background: transparent; border: 1px solid var(--border); color: var(--text-muted); padding: 6px 12px; border-radius: 999px; cursor: pointer; font: inherit; font-size: .85rem; }
.of-sort.on, .of-sort:hover { color: var(--gold-text); border-color: var(--gold); }
.of-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
.of-card, .of-fighter { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; display: flex; flex-direction: column; }
/* The picture keeps its own shape inside a fixed box: a screenshot is never cropped here. */
/* One fixed frame per surface (2026-09-13): a character screenshot is about square, so the
   frame is square; `contain` + full height means a small upload scales UP to fill it and a
   large one scales down — every outfit shows at the same size, never at the size it was sent.
   The gallery frame is the card's width (280–400px); the battle frame is capped at 520px. */
.of-fig { display: block; width: 100%; aspect-ratio: 1 / 1; background: #0b0b0d; border: 0; padding: 0; cursor: zoom-in; }
.of-fig img { width: 100%; height: 100%; object-fit: contain; display: block; }
.of-fighter-fig { aspect-ratio: 1 / 1; max-width: 520px; margin: 0 auto; }
.of-body { padding: 12px 14px 14px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.of-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.of-title { margin: 0; font-size: 1.05rem; }
.of-score { white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
.of-like { display: inline-flex; align-items: center; gap: 4px; font: inherit; font-size: .8rem; color: var(--text-muted); background: none; border: 1px solid var(--border); border-radius: 999px; padding: 2px 9px; }
button.of-like { cursor: pointer; transition: color .15s, border-color .15s; }
button.of-like:hover { color: var(--text-main); border-color: var(--text-muted); }
.of-like.on { color: var(--danger-text); border-color: currentColor; }
.of-like svg { width: 13px; height: 13px; }
.of-rec { color: var(--text-muted); font-size: .8rem; }
.of-w { color: var(--success); font-weight: 700; }
.of-l { color: var(--danger-text); font-weight: 700; }
.of-by { display: inline-flex; align-items: center; gap: 8px; font-size: .88rem; }
.of-as { color: var(--text-muted); }
.of-notes { margin: 0; color: var(--text-muted); font-size: .9rem; }
.of-pieces { display: flex; flex-wrap: wrap; gap: 6px; }
.of-piece { background: transparent; border: 1px solid var(--border); border-radius: 6px; color: var(--text-main); padding: 3px 8px; font: inherit; font-size: .8rem; cursor: pointer; }
.of-piece:hover { border-color: var(--gold); color: var(--gold-text); }
.of-slot { color: var(--text-muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .04em; margin-right: 5px; }
.of-actions { display: flex; align-items: center; gap: 12px; margin-top: auto; font-size: .82rem; }
.of-when { color: var(--text-muted); margin-right: auto; }
.of-remove { color: var(--danger-text); }
.of-empty { color: var(--text-muted); text-align: center; padding: 30px 0; }
/* the share form */
.of-form { margin-bottom: 20px; }
.of-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.of-form label { display: block; font-size: .8rem; color: var(--text-muted); margin-bottom: 4px; }
.of-form input[type=text], .of-form input[type=url], .of-form textarea, .of-form select { width: 100%; padding: 9px 10px; background: transparent; border: 1px solid var(--border); color: var(--text-main); font: inherit; border-radius: 6px; }
.of-form input[type=url] { margin-bottom: 8px; }
.of-form .of-check { display: flex; align-items: flex-start; gap: 8px; font-size: .85rem; color: var(--text-main); cursor: pointer; line-height: 1.4; }
.of-form .of-check input { margin: 3px 0 0; flex: none; }
.of-out { font-size: .72rem; color: var(--text-muted); border: 1px solid var(--border); border-radius: 999px; padding: 1px 7px; margin-left: 6px; white-space: nowrap; }
.of-queued { color: var(--gold-text); border-color: var(--gold); }
.of-preview { aspect-ratio: 16 / 10; background: #0b0b0d; border: 1px dashed var(--border); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: .85rem; text-align: center; padding: 10px; overflow: hidden; }
.of-preview img { width: 100%; height: 100%; object-fit: contain; }
.of-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; margin-top: 14px; }
.of-slotrow { position: relative; }
.of-chip { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--gold); color: var(--gold-text); border-radius: 6px; padding: 5px 8px; font-size: .85rem; }
.of-chip button { background: none; border: 0; color: inherit; cursor: pointer; font: inherit; display: inline-flex; padding: 0; }
.of-chip button svg { width: 12px; height: 12px; }
.of-hits { position: absolute; left: 0; right: 0; top: 100%; z-index: 5; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; max-height: 220px; overflow-y: auto; }
.of-hit { display: block; width: 100%; text-align: left; background: none; border: 0; color: var(--text-main); padding: 8px 10px; cursor: pointer; font: inherit; font-size: .85rem; }
button.of-hit:hover { background: var(--gold-light); color: var(--gold-text); }
.of-formfoot { display: flex; align-items: center; gap: 12px; margin-top: 14px; flex-wrap: wrap; }
/* the battle */
.of-battle { display: grid; grid-template-columns: 1fr auto 1fr; gap: 16px; align-items: stretch; }
.of-fighter { transition: border-color .2s, opacity .2s; }
.of-fighter.won { border-color: var(--success); box-shadow: 0 0 0 1px var(--success); }
.of-fighter.lost { opacity: .55; }
.of-vs { align-self: center; color: var(--text-muted); font-weight: 700; letter-spacing: .1em; }
.of-pick { width: 100%; margin-top: auto; }
.of-verdict { text-align: center; margin: 14px 0 0; color: var(--text-muted); min-height: 1.4em; }
/* the welcome the Battle tab opens on */
.of-welcome { max-width: 640px; margin: 0 auto; }
.of-welcome h4 { margin-top: 0; }
.of-welcome p, .of-welcome li { line-height: 1.5; }
.of-welcome ul { padding-left: 20px; margin: 10px 0 18px; }
.of-welcome li { margin-bottom: 8px; }
.of-delta { font-weight: 700; color: var(--success); font-size: .85rem; }
.of-delta.down { color: var(--danger-text); }
/* the zoom (body-level) and the profile strip */
.of-zoom { position: fixed; inset: 0; z-index: 2000; background: rgba(0, 0, 0, .88); display: flex; align-items: center; justify-content: center; padding: 20px; cursor: zoom-out; }
.of-zoom img { max-width: 100%; max-height: 100%; object-fit: contain; }
.of-strip { display: flex; gap: 10px; flex-wrap: wrap; }
.of-thumb { width: 150px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 8px; padding: 0; overflow: hidden; cursor: pointer; text-align: left; }
.of-thumb img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; }
.of-thumb-cap { display: block; padding: 6px 8px; font-size: .78rem; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The profile strip on one line, slid with arrows (2026-09-14). The row scrolls sideways with
   its scrollbar hidden, as everywhere in the app; the arrows are the way through it. */
.of-strip-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
.of-strip-row { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; padding: 2px; scrollbar-width: none; -ms-overflow-style: none; }
.of-strip-row::-webkit-scrollbar { display: none; }
.of-strip-row .of-thumb { flex: none; }
.of-strip-arrow { flex: none; width: 32px; height: 32px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-muted); cursor: pointer; padding: 0; }
.of-strip-arrow:hover:not(:disabled) { color: var(--gold-text); border-color: var(--gold); }
.of-strip-arrow:disabled { opacity: .35; cursor: default; }
.of-strip-wrap.no-overflow .of-strip-arrow { display: none; }
/* The outfit pop-up: picture beside the details, closed by its X, a click outside, or Escape. */
.of-popup { z-index: 1400; }
.of-popup-dialog { position: relative; max-width: 940px; width: min(940px, calc(100vw - 32px)); padding: 20px 20px 16px; }
.of-popup-x { position: absolute; top: 10px; right: 10px; z-index: 2; width: 30px; height: 30px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; background: var(--bg-panel); border: 1px solid var(--border); color: var(--text-muted); cursor: pointer; padding: 0; }
.of-popup-x:hover { color: var(--text-main); border-color: var(--text-muted); }
.of-popup-note { margin: 10px 30px 4px 0; }
.of-popup-body { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 20px; align-items: start; }
.of-popup-fig { aspect-ratio: 1 / 1; background: #0b0b0d; border-radius: 10px; overflow: hidden; border: 1px solid var(--border); }
.of-popup-fig img { width: 100%; height: 100%; object-fit: contain; display: block; }
.of-popup-info { display: flex; flex-direction: column; gap: 10px; padding-right: 28px; min-width: 0; }
.of-popup-title { margin: 0; font-size: 1.25rem; }
.of-popup-info .of-actions { margin-top: 4px; }
@media (max-width: 760px) { .of-popup-body { grid-template-columns: 1fr; } .of-popup-info { padding-right: 0; } }
@media (max-width: 760px) { .of-battle { grid-template-columns: 1fr; } .of-vs { display: none; } .of-fields { grid-template-columns: 1fr; } }

/* The Guides section's "Write a guide" row (2026-09-13). Empty for everyone outside the beta. */
.guides-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
.guides-toolbar:empty { display: none; }

/* The dashboard's one line about mail (2026-09-13): only while the last send failed. */

/* The bar atop a guide previewed from the moderator queue (2026-09-13). */
.guide-preview-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; padding: 10px 14px; border: 1px solid var(--gold); background: var(--gold-light); border-radius: 8px; font-size: .9rem; color: var(--text-main); }
.guide-preview-bar svg { width: 16px; height: 16px; color: var(--gold-text); flex: 0 0 auto; }

/* --- What a pending guide edit changes (2026-09-21) ---
   New text is green; hovering, focusing or tapping it shows the old text in the red box below.
   The tints are translucent on purpose, so they read on every theme's surface. */
.gd-legend { flex: 1 1 100%; display: flex; align-items: center; gap: 6px 14px; flex-wrap: wrap; }
.gd-swatch { background: rgba(40, 167, 69, 0.22); box-shadow: 0 0 0 1px rgba(40, 167, 69, 0.4); border-radius: 3px; padding: 0 3px; }
.gd-toggle { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; white-space: nowrap; }
.gd-ins {
    background: rgba(40, 167, 69, 0.22);
    box-shadow: 0 0 0 1px rgba(40, 167, 69, 0.4);
    border-radius: 3px;
    cursor: help;
}
.gd-del {
    display: inline-block; margin: 0 3px; padding: 0 6px; vertical-align: 0.1em;
    font-size: 0.7em; line-height: 1.6; font-weight: 600; letter-spacing: 0.02em;
    color: var(--danger-text); background: rgba(220, 53, 69, 0.14);
    border: 1px solid rgba(220, 53, 69, 0.55); border-radius: 8px; cursor: help;
}
.gd-del::before { content: "removed"; }
.gd-block { display: block; border-radius: 6px; cursor: help; }
.gd-block-chg { background: rgba(40, 167, 69, 0.1); box-shadow: inset 3px 0 0 var(--success); padding: 2px 10px; margin: 6px 0; }
.gd-block-del {
    margin: 6px 0; padding: 4px 10px; font-size: 0.85rem; color: var(--danger-text);
    background: rgba(220, 53, 69, 0.08); border: 1px dashed rgba(220, 53, 69, 0.6);
}
.gd-ins:focus, .gd-del:focus, .gd-block:focus { outline: none; }
.gd-ins:focus-visible, .gd-del:focus-visible, .gd-block:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.gd-sec-new { box-shadow: inset 3px 0 0 var(--success); background: rgba(40, 167, 69, 0.06); padding-left: 12px; }
.gd-sec-removed h3 s { color: var(--danger-text); text-decoration-color: rgba(220, 53, 69, 0.8); }
.gd-badge {
    display: inline-block; vertical-align: middle; padding: 1px 8px; border-radius: 10px;
    font-size: 0.7rem; font-weight: 600; letter-spacing: 0.02em; border: 1px solid currentColor;
}
.gd-badge-new { color: var(--success); }
.gd-badge-del { color: var(--danger-text); }
.gd-badge-moved { color: var(--info, var(--gold-text)); }
/* A change hidden INSIDE a hover explanation's box would be invisible until someone happened to
   hover that word — so the word itself says there is something to look at. */
.mk-tipref:has(.gd-ins, .gd-del) { outline: 1px dashed var(--success); outline-offset: 2px; }
/* The "before" box: one floating element, placed by guides.js. */
.gd-was-float {
    position: fixed; z-index: 1400; max-width: min(440px, calc(100vw - 24px)); padding: 10px 12px;
    background-color: var(--bg-panel);
    background-image: linear-gradient(rgba(220, 53, 69, 0.12), rgba(220, 53, 69, 0.12));
    border: 1px solid rgba(220, 53, 69, 0.75); border-radius: 6px;
    box-shadow: var(--shadow-hover, 0 6px 18px rgba(0, 0, 0, 0.35));
    color: var(--text-main); font-size: 0.88rem; line-height: 1.5; overflow-wrap: anywhere;
    max-height: 60vh; overflow: auto;
}
.gd-was-float::before {
    content: "Before"; display: block; margin-bottom: 4px;
    font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--danger-text);
}
.gd-was-float.gd-was-float-none { background-image: none; border-color: rgba(40, 167, 69, 0.7); color: var(--success); font-weight: 600; }
.gd-was-float.gd-was-float-none::before { content: none; }
.gd-was-float img { max-width: 100%; height: auto; max-height: 240px; display: block; margin: 4px auto; }
.gd-was-float > :last-child { margin-bottom: 0; }
.gd-was-float.gd-was-sheet { left: 12px; right: 12px; bottom: 16px; top: auto; max-width: none; }
/* "Highlight changes" off: the page as readers will see it once approved. */
.gd-plain .gd-ins, .gd-plain .gd-block-chg, .gd-plain .gd-sec-new { background: none; box-shadow: none; cursor: auto; }
.gd-plain .gd-block-chg, .gd-plain .gd-sec-new { padding: 0; margin: 0; }
.gd-plain .gd-del, .gd-plain .gd-block-del, .gd-plain .gd-sec-removed, .gd-plain .gd-badge { display: none; }
.gd-plain .mk-tipref:has(.gd-ins, .gd-del) { outline: none; }

/* Drafts (2026-09-13): the author's controls on a draft's page, and the submit view's action row. */
.guide-draft-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 14px; font-size: .9rem; }
.guide-draft-actions .fb-inline-link svg { width: 14px; height: 14px; vertical-align: -2px; }
.guide-danger { color: var(--danger-text); }
.guide-flash { margin: 0 0 12px; }
.submit-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* The header chip when the account has no character yet (2026-09-13): a call to add one. */
.nav-user.nav-user-add { border-color: var(--gold); color: var(--gold-text); }

/* A guest's name in a party roster: the link's look without the link (2026-09-13). */
.pt-charlink.pt-plain { cursor: default; color: var(--text-muted); }
.pt-charlink.pt-plain:hover { border-bottom-color: transparent; }
.pt-seat-sub .pt-charlink { font-size: inherit; color: inherit; }

/* The Guides head (2026-09-13): category chips on the left, the author's buttons on the right. */
.guides-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; }
.guides-head .database-cat-grid { flex: 1 1 320px; margin-bottom: 0; }
.guides-head .guides-toolbar { margin-bottom: 0; margin-left: auto; flex-wrap: nowrap; justify-content: flex-end; }
/* "My guides" is a FILTER on the cards (2026-09-25): pressed, it wears a chosen category's gold.
   Scoped this tightly because a skin restyles every .action-btn (Gilded's glass pill and its
   hover are more specific than a plain class), and the pressed state must survive every skin. */
.guides-toolbar .action-btn.guides-mine-btn[aria-pressed="true"],
.guides-toolbar .action-btn.guides-mine-btn[aria-pressed="true"]:hover:not(:disabled) { background: var(--gold); color: var(--on-gold); border-color: var(--gold); }
.guide-badge.guide-pending { border: 1px solid var(--gold); color: var(--gold-text); }
/* A guide's own page: "Edit this guide" and, for its author, "Delete", side by side. */
.guide-edit { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }

/* The identity block SHRINKS and the button keeps the right-hand spot (2026-09-13). With the
   default flex-basis (max-content), a long hint — "No verified character — you appear under
   your account name…" — made the block claim the whole row, and "Open a party" wrapped
   underneath it: the button sat in two different places depending on whether the account had
   a verified character. Basis 0 lets the text wrap instead, so the row never breaks. */
.pt-head > div:first-child { flex: 1 1 0; min-width: 0; }
.pt-head > .action-btn, .pt-head > .pt-hint { flex: 0 0 auto; }
/* On a phone the identity block takes the row and the button sits under it, at the right —
   the same place for everyone, rather than a four-line hint squeezed into a 165px column. */
@media (max-width: 480px) {
    .pt-head > div:first-child { flex-basis: 100%; }
    .pt-head > .action-btn { margin-left: auto; }
}

/* The Home card while the reader is in a party (2026-09-13): "Back to My Party", marked gold. */
.home-card-live { border-color: var(--gold); }
.home-card-live h3 { color: var(--gold-text); }

/* ---- Markup (public/js/markup.js, 2026-09-13): what a guide's formatted text looks like ---- */
.mk-h { margin: 18px 0 6px; line-height: 1.3; }
.mk-h1 { font-size: 1.25rem; }
.mk-h2 { font-size: 1.1rem; }
.mk-h3 { font-size: 1rem; color: var(--text-muted); }
.mk-list { margin: 6px 0 10px 22px; padding: 0; }
.mk-list li { margin: 3px 0; }
.mk-quote { margin: 10px 0; padding: 10px 14px; border-left: 3px solid var(--border); background: var(--bg-panel); color: var(--text-main); border-radius: 0 6px 6px 0; }
.mk-quote.mk-tip { border-left-color: var(--gold); }
.mk-quote.mk-warning { border-left-color: var(--danger-text); }
.mk-quote.mk-note { border-left-color: var(--info); }
.mk-callout-label { display: block; margin-bottom: 4px; font-size: .8rem; letter-spacing: .04em; text-transform: uppercase; }
.mk-tip .mk-callout-label { color: var(--gold-text); }
.mk-warning .mk-callout-label { color: var(--danger-text); }
.mk-note .mk-callout-label { color: var(--info); }
.mk-table-wrap { overflow-x: auto; margin: 10px 0; }
.mk-table { border-collapse: collapse; width: 100%; font-size: .92rem; }
.mk-table th, .mk-table td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; vertical-align: top; }
.mk-table th { background: var(--bg-panel); font-weight: bold; }
.mk-link { color: var(--gold-text); text-decoration: underline; text-underline-offset: 2px; }
.mk-link.mk-yt::before { content: '▶ '; font-size: .8em; }
.mk-img { display: block; max-width: 100%; height: auto; margin: 10px 0; border-radius: 6px; }
/* A centred line, `->…<-` (2026-09-20). `text-align` centres text and anything inline, but a
   markup picture is display:block, which text-align does not move — so it needs auto side
   margins of its own, or "centre this picture" would quietly do nothing. A list keeps its
   bullets beside its items by centring the LIST as a block rather than each line of it. */
.mk-center { text-align: center; }
.mk-center .mk-img { margin-left: auto; margin-right: auto; }
.mk-center .mk-list { display: inline-block; text-align: left; }
.mk-unresolved { border-bottom: 1px dotted var(--danger-text); cursor: help; }
/* The editor's toolbar, autocomplete and preview (form-components.js). */
.mk-toolbar { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 6px; }
.mk-toolbar button { padding: 4px 8px; font-size: .8rem; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; color: var(--text-main); cursor: pointer; }
.mk-toolbar button:hover { border-color: var(--gold); }
.mk-toolbar .mk-tb-gap { flex: 1; }
.mk-suggest { position: absolute; z-index: 60; max-height: 220px; overflow-y: auto; min-width: 240px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 6px 20px rgba(0,0,0,.25); }
.mk-suggest button { display: block; width: 100%; text-align: left; padding: 6px 10px; background: none; border: none; color: var(--text-main); cursor: pointer; font-size: .85rem; }
.mk-suggest button.on, .mk-suggest button:hover { background: var(--gold-light); }
.mk-suggest .mk-sg-type { color: var(--text-muted); font-size: .75rem; margin-left: 6px; }
.mk-preview { margin-top: 8px; padding: 10px 14px; border: 1px dashed var(--border); border-radius: 6px; }
.mk-preview:empty { display: none; }
.mk-help { margin: 6px 0 0; font-size: .82rem; color: var(--text-muted); }
.mk-help summary { cursor: pointer; color: var(--gold-text); }
.mk-help code { background: var(--bg-panel); padding: 1px 4px; border-radius: 3px; }
.mk-textarea-wrap { position: relative; }

/* The Formatting explainer (markup-help.js, 2026-09-13). */
.mk-help-dialog { max-width: 760px; }
.mk-help-dialog h4 { margin: 18px 0 6px; }
.mk-help-dialog code { background: var(--bg-panel); padding: 1px 5px; border-radius: 3px; font-size: .88em; }
.mk-help-src { margin: 0; white-space: pre-wrap; font-size: .82rem; font-family: ui-monospace, Consolas, monospace; color: var(--text-main); }
.mk-help-table td { vertical-align: top; }
.mk-help-table td:first-child { width: 45%; }
.mk-help-table .mk-h { margin: 0 0 2px; }
.mk-help-table .mk-list { margin: 0 0 0 18px; }
.mk-help-table .mk-quote { margin: 0; }
.mk-help-table .mk-table-wrap { margin: 0; }
.mk-help-keys { columns: 2; column-gap: 24px; }
/* Clear of the viewport's edges: the generic dialog cap of 100vh − 40px is CONTENT height, and
   this dialog's own padding pushed it past both edges. Border-box, and 40px of air each side. */
.mk-help-dialog { box-sizing: border-box; max-height: calc(100vh - 80px); border-radius: 10px; }
/* A published guide's number (2026-09-13): what "[[guide: #12]]" names. */
.guide-badge.guide-number { border: 1px solid var(--gold); color: var(--gold-text); font-variant-numeric: tabular-nums; }
/* A class on a character page that the database knows: the tag is a link (2026-09-13). */
.ch-tag-link { cursor: pointer; border-style: solid; color: var(--text-main); text-decoration: none; }
.ch-tag-link:hover, .ch-tag-link:focus-visible { border-color: var(--gold); color: var(--gold-text); }

/* ---- The global chat (globalchat.js, 2026-09-13) ---- */
.gc { display: flex; flex-direction: column; height: min(72vh, 680px); }
.gc-head h3 { margin: 0 0 4px; }
.gc-head .pt-hint { margin-bottom: 10px; }
.gc-list { flex: 1; min-height: 0; overflow-y: auto; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-panel); }
.gc-row .msg-bubble { max-width: 100%; }
.gc-row .msg-body p { margin: 0; }
.gc-time { color: var(--text-hint); font-size: .72rem; margin-left: 8px; }
.gc-x { background: none; border: none; padding: 0; margin-left: 10px; font: inherit; font-size: .72rem; color: var(--text-hint); cursor: pointer; }
.gc-x:hover, .gc-x:focus-visible { color: var(--danger-text); }
.gc-removed { color: var(--text-hint); }
.gc-more { text-align: center; margin: 0 0 8px; }
.gc-gate { margin-top: 10px; }
.gc-compose { display: flex; gap: 10px; align-items: flex-end; margin-top: 10px; }
.gc-compose textarea { flex: 1; margin-bottom: 0; resize: none; }
.gc-send { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
.gc-count { font-size: .72rem; color: var(--text-hint); }
.gc-count.low { color: var(--danger-text); }

/* Links to hosts outside the trusted list (markup.js + leaving.js, 2026-09-13). */
.mk-host { color: var(--text-hint); font-size: .85em; }
.mk-link.mk-ext { text-decoration-style: dotted; }
.mk-leaving { max-width: 480px; }
.mk-leaving-url { margin: 6px 0 10px; padding: 8px 10px; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; font-family: ui-monospace, Consolas, monospace; font-size: .8rem; word-break: break-all; }
.mk-leaving-warn { color: var(--danger-text); }

/* ==========================================================================================
   ADMIN (redesigned 2026-09-23). Painted by admin-hub.js: a front page of category cards, like
   Home, and a workspace per category. Every colour, radius and shadow is a theme token, so each
   theme wears it in its own shape: squared in Royal and Shadow, rounded in Gilded and Glass.

   The front page is a small bento that answers to the SECTION's width (a container query, so an
   open rail or a narrow window lays it out the same way): three columns where there is room,
   two on a tablet, one on a phone. When something is waiting in Review, that card takes a
   2 x 2 block and lists every queue. A moderator sees five cards, so there Review always spans
   two columns: no layout ever leaves a hole in a row.
   ========================================================================================== */
.adm { container: adm / inline-size; max-width: 1180px; margin: 0 auto; padding: 4px 0 56px; }
/* Every rule below sets a display, and an author display beats the browser's own [hidden]. */
.adm [hidden] { display: none !important; }

/* --- the header: title, who you are, and the site's two health chips --- */
.adm-head {
    position: relative; display: flex; justify-content: space-between; align-items: flex-end;
    flex-wrap: wrap; gap: 14px 28px; padding: 10px 2px 20px; margin-bottom: 28px;
}
.adm-head::after {             /* a hairline of the accent that fades at both ends */
    content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; opacity: .6;
    background: linear-gradient(90deg, transparent, var(--gold) 15%, var(--gold) 85%, transparent);
}
.adm .adm-title {              /* an h2 dressed as the theme's h1: the page's own name */
    display: block; margin: 0; padding: 0; border: 0; font-size: 2.4rem; line-height: 1.05;
    letter-spacing: var(--h1-tracking); text-transform: var(--h1-transform);
    font-weight: var(--h1-weight); color: var(--h1-color);
}
.adm-sub { margin: 8px 0 0; color: var(--text-muted); font-size: .9rem; }
.adm-sub:empty { display: none; }

.adm-health { position: relative; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
.adm-chip {
    display: inline-flex; align-items: center; gap: 8px; font-size: .8rem; color: var(--text-muted);
    background: var(--panel-bg); border: 1px solid var(--border); border-radius: var(--r-btn);
}
span.adm-chip, .adm-chip > summary { padding: 6px 13px 6px 11px; }
.adm-chip > summary {
    display: inline-flex; align-items: center; gap: 8px; list-style: none; cursor: pointer; border-radius: inherit;
}
.adm-chip > summary::-webkit-details-marker { display: none; }
.adm-chip > summary:hover { color: var(--text-main); }
.adm-chip > summary:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.adm-chip[open] { border-color: var(--gold); }
/* The detail opens under the chips, anchored to the row rather than to one chip, so it can
   never hang off the side of a phone. */
.adm-chip > p {
    position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 340px; max-width: calc(100vw - 32px);
    margin: 0; padding: 12px 14px; font-size: .85rem; line-height: 1.5; color: var(--text-main); text-align: left;
    background: var(--bg-float); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
    border: 1px solid var(--panel-border); border-radius: var(--r-md); box-shadow: var(--shadow-hover);
    animation: adm-drop .18s ease backwards;
}
.adm-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; background: var(--text-hint); }
.adm-chip-ok .adm-dot { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
.adm-chip-bad { color: var(--danger-text); border-color: var(--danger-text); }
.adm-chip-bad .adm-dot { background: var(--danger-text); box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger-text) 25%, transparent); }
.adm-chip-idle { border-style: dashed; }

/* --- the front page --- */
.adm-hub { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
.adm-card {
    position: relative; isolation: isolate; overflow: hidden;
    display: flex; flex-direction: column; min-height: 216px; padding: 24px 24px 20px; cursor: pointer;
    background: var(--panel-bg); border: 1px solid var(--panel-border); border-radius: var(--r-panel);
    box-shadow: var(--shadow-card);
    transition: transform .22s ease, border-color .22s ease, box-shadow .22s ease;
    /* Arriving on the front page the cards rise in one after another. `backwards`, not `both`:
       a held final frame would outrank the hover lift below for as long as the page is open. */
    animation: adm-rise .5s cubic-bezier(.2, .7, .2, 1) backwards;
    animation-delay: calc(var(--i, 0) * 60ms);
}
.adm-card.no-enter { animation: none; }
.adm-card::before {            /* a soft pool of the accent in the corner, brighter on hover */
    content: ''; position: absolute; z-index: -1; top: -90px; right: -90px; width: 240px; height: 240px;
    border-radius: 50%; pointer-events: none; opacity: .75;
    background: radial-gradient(closest-side, color-mix(in srgb, var(--gold) 18%, transparent), transparent);
    transition: opacity .3s ease, transform .45s ease;
}
.adm-hub .adm-card:hover, .adm-hub .adm-card:has(.adm-open:focus-visible) {
    transform: translateY(-5px); border-color: var(--gold); box-shadow: var(--shadow-hover);
}
.adm-card:hover::before, .adm-card:has(.adm-open:focus-visible)::before { opacity: 1; transform: scale(1.15); }
.adm-card:has(.adm-open:focus-visible) { outline: 2px solid var(--gold); outline-offset: 3px; }
.adm-card.is-hot { border-color: var(--border-gold); }
.adm-card.is-hot::after {      /* waiting work: a bar of the accent along the top edge */
    content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--gold);
}

.adm-card-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.adm-glyph {
    display: grid; place-items: center; flex-shrink: 0; width: 48px; height: 48px;
    background: var(--gold-light); color: var(--gold-text); border: 1px solid var(--border-gold);
    border-radius: var(--r-btn);   /* square in Royal and Shadow, a circle in Gilded */
    transition: transform .3s ease;
}
.adm-glyph svg { width: 22px; height: 22px; }
.adm-card:hover .adm-glyph { transform: translateY(-1px) scale(1.06); }
.adm-count {
    display: inline-grid; place-items: center; min-width: 32px; height: 32px; padding: 0 10px; border-radius: 16px;
    background: var(--gold); color: var(--on-gold); font-weight: 700; font-size: .95rem; font-variant-numeric: tabular-nums;
    animation: adm-ping 2s ease-out 3;
}
.adm-card-title { margin: 16px 0 4px; font-size: 1.25rem; line-height: 1.25; }
.adm-open { all: unset; cursor: pointer; }
.adm-open:focus-visible { outline: none; }   /* the whole card takes the ring (:has above) */
.adm-card-blurb { margin: 0 0 18px; color: var(--text-muted); font-size: .9rem; line-height: 1.5; max-width: 38ch; }

.adm-stat {
    display: flex; align-items: center; gap: 7px; margin: auto 0 0; padding: 14px 34px 0 0;
    border-top: 1px dashed var(--border); font-size: .82rem; color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.adm-stat svg { width: 15px; height: 15px; flex-shrink: 0; }
.adm-stat-hot { color: var(--gold-text); font-weight: 600; }
.adm-stat-ok { color: var(--ok); }
.adm-stat-wait span { animation: adm-breathe 1.2s ease-in-out infinite; }
.adm-go {
    position: absolute; right: 22px; bottom: 21px; display: grid; color: var(--text-hint);
    transition: transform .22s ease, color .22s ease;
}
.adm-go svg { width: 18px; height: 18px; }
.adm-card:hover .adm-go, .adm-card:has(.adm-open:focus-visible) .adm-go { transform: translateX(4px); color: var(--gold-text); }

/* Review's queues: every one with its depth, a click from each. The empty ones only show where
   the card is big enough to hold all six. */
.adm-qs { display: grid; gap: 2px; margin: -6px 0 18px; }
.adm-card-review:not(.is-hot) .adm-qs { display: none; }
.adm-q {
    display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%;
    margin: 0; padding: 7px 10px; border: 0; border-radius: var(--r-sm); cursor: pointer;
    background: transparent; color: var(--text-main); font: inherit; font-size: .88rem; text-align: left;
    transition: background .15s ease;
}
.adm-q:hover { background: var(--gold-light); }
.adm-q:focus-visible { outline: none; background: var(--gold-light); box-shadow: inset 0 0 0 2px var(--gold); }
.adm-q-n {
    min-width: 28px; padding: 1px 9px; border-radius: 999px; text-align: center; font-size: .78rem; font-weight: 700;
    background: var(--gold); color: var(--on-gold); font-variant-numeric: tabular-nums;
}
.adm-q.is-zero { display: none; color: var(--text-hint); }
.adm-q.is-zero .adm-q-n { background: transparent; color: var(--text-hint); font-weight: 400; }

/* Database: what the catalogue is made of, in the accent's four strengths. */
.adm-mix { margin: -6px 0 18px; }
.adm-mix-bar { display: flex; gap: 3px; height: 8px; overflow: hidden; border-radius: calc(var(--r-sm) / 2); }
.adm-mix-bar i { flex: 0 1 0; min-width: 4px; background: var(--mix); }   /* flex-grow: the script's count */
.adm-mix-0 { --mix: var(--gold); }
.adm-mix-1 { --mix: color-mix(in srgb, var(--gold) 66%, transparent); }
.adm-mix-2 { --mix: color-mix(in srgb, var(--gold) 42%, transparent); }
.adm-mix-3 { --mix: color-mix(in srgb, var(--gold) 24%, transparent); }
.adm-mix-key { display: grid; grid-template-columns: repeat(2, max-content); gap: 4px 20px; margin-top: 10px; font-size: .78rem; color: var(--text-muted); }
.adm-mix-key span { display: inline-flex; align-items: center; gap: 6px; }
.adm-mix-key b { width: 8px; height: 8px; border-radius: 2px; background: var(--mix); }
.adm-mix-key em { font-style: normal; font-weight: 600; color: var(--text-main); font-variant-numeric: tabular-nums; }

/* Two columns from a tablet up; three where there is room. */
@container adm (min-width: 560px) {
    .adm-hub { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
    .adm-hub[data-n="5"] .adm-card-review { grid-column: span 2; }
    .adm-hub[data-n="5"] .adm-qs { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 16px; }
}
@container adm (min-width: 860px) {
    .adm-hub { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; }
    /* The 2 x 2 Review: all six queues, set larger. */
    .adm-hub[data-n="6"] .adm-card-review.is-hot { grid-column: span 2; grid-row: span 2; padding: 30px 32px 24px; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot::before { top: -170px; right: -170px; width: 440px; height: 440px; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-glyph { width: 56px; height: 56px; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-glyph svg { width: 26px; height: 26px; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-count { min-width: 42px; height: 42px; border-radius: 21px; font-size: 1.15rem; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-card-title { font-size: 1.6rem; margin-top: 20px; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-q { padding: 10px 12px; font-size: .95rem; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-q.is-zero { display: flex; }
    .adm-hub[data-n="6"] .adm-card-review.is-hot .adm-go { right: 30px; bottom: 25px; }
}

/* --- a category's workspace --- */
.adm-work { animation: adm-fade .28s ease backwards; }
.adm-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 18px; margin-bottom: 18px; }
.adm-back {
    display: inline-flex; align-items: center; gap: 7px; padding: 7px 15px 7px 11px; cursor: pointer;
    font: inherit; font-size: .88rem; color: var(--text-muted); background: var(--panel-bg);
    border: 1px solid var(--border); border-radius: var(--r-btn);
    transition: color .18s ease, border-color .18s ease;
}
.adm-back svg { width: 16px; height: 16px; transition: transform .2s ease; }
.adm-back:hover, .adm-back:focus-visible { color: var(--gold-text); border-color: var(--gold); outline: none; }
.adm-back:focus-visible { box-shadow: 0 0 0 2px var(--gold); }
.adm-back:hover svg, .adm-back:focus-visible svg { transform: translateX(-3px); }
.adm-cat { display: flex; align-items: center; gap: 12px; margin: 0; font-size: 1.6rem; font-weight: 400; outline: none; }
.adm-cat .adm-glyph { width: 40px; height: 40px; }
.adm-cat .adm-glyph svg { width: 19px; height: 19px; }

/* The category's sections. They wrap rather than scroll: this app shows no scrollbars. */
.adm-tabs {
    display: grid; gap: 12px; margin-bottom: 18px; padding: 14px 16px;
    background: var(--panel-bg); border: 1px solid var(--panel-border); border-radius: var(--r-panel);
}
.adm-group { display: grid; grid-template-columns: 92px minmax(0, 1fr); align-items: start; gap: 12px; }
.adm-group + .adm-group { padding-top: 12px; border-top: 1px dashed var(--border); }
.adm-group-label { padding-top: 7px; font-size: .7rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--text-hint); }
.adm-pills { display: flex; flex-wrap: wrap; gap: 7px; }
.adm-pill {
    display: inline-flex; align-items: center; gap: 8px; padding: 6px 13px; cursor: pointer;
    font: inherit; font-size: .86rem; color: var(--text-muted); background: transparent;
    border: 1px solid var(--border); border-radius: var(--r-btn);
    transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.adm-pill:hover { color: var(--gold-text); border-color: var(--gold); }
.adm-pill:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.adm-pill.active { color: var(--on-gold); background: var(--gold); border-color: var(--gold); font-weight: 600; }
.adm-pill-count {
    display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px;
    background: var(--gold); color: var(--on-gold); font-size: .7rem; font-weight: 700;
}
.adm-pill.active .adm-pill-count { background: var(--on-gold); color: var(--gold); }
.adm-pill-n { font-size: .74rem; color: var(--text-hint); font-variant-numeric: tabular-nums; }
.adm-pill.active .adm-pill-n { color: inherit; opacity: .8; }

/* The panel every section renders into. Unchanged underneath. */
.adm-panel {
    padding: 28px; background: var(--panel-bg); border: 1px solid var(--panel-border);
    border-radius: var(--r-panel); box-shadow: var(--shadow-card);
}
.adm-panel > .adm-section-title { margin: 0 0 18px; font-size: 1.3rem; color: var(--gold-text); }
#admin-form-container { margin-bottom: 30px; padding: 20px; background: var(--gold-light); border-left: 2px solid var(--gold); }
.adm-table-wrap { overflow-x: auto; }

/* Phones. */
@container adm (max-width: 559.98px) {
    .adm-head { padding-top: 4px; margin-bottom: 22px; }
    .adm .adm-title { font-size: 1.9rem; }
    .adm-health { flex-basis: 100%; justify-content: flex-start; }
    .adm-chip > p { left: 0; right: 0; width: auto; max-width: none; }
    .adm-card { min-height: 0; padding: 20px 20px 18px; }
    .adm-go { right: 18px; bottom: 19px; }
    .adm-cat { font-size: 1.35rem; }
    .adm-tabs { padding: 12px; }
    .adm-group { grid-template-columns: minmax(0, 1fr); gap: 8px; }
    .adm-group-label { padding-top: 0; }
    .adm-pills { gap: 6px; }
    .adm-pill { padding: 5px 11px; font-size: .82rem; }
    .adm-panel { padding: 16px; }
    #admin-form-container { padding: 14px; }
}

@keyframes adm-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes adm-fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes adm-drop { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@keyframes adm-ping {
    0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--gold) 50%, transparent); }
    80%, 100% { box-shadow: 0 0 0 12px transparent; }
}
@keyframes adm-breathe { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) {
    .adm-card, .adm-card::before, .adm-glyph, .adm-go, .adm-back svg, .adm-q, .adm-pill { transition: none; }
    .adm-card, .adm-work, .adm-count, .adm-chip > p, .adm-stat-wait span { animation: none; }
    .adm-hub .adm-card:hover, .adm-hub .adm-card:has(.adm-open:focus-visible), .adm-card:hover .adm-glyph { transform: none; }
}

/* Admin -> Guides: the list's search and pages (guides.js, 2026-09-23). The pager matches the
   Users one: bordered header-style buttons around "Page x of y". */
#admin-guides-list.is-loading { opacity: .55; transition: opacity .15s ease; }
.agl-pages { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 20px; color: var(--text-muted); font-size: .9rem; }
.agl-pages .nav-btn { border: 1px solid var(--border); padding: 5px 15px; }
.agl-pages .nav-btn:disabled { opacity: .45; cursor: default; color: var(--text-muted); }
.agl-pages .nav-btn:disabled::after { width: 0; }

/* Browser notifications, in Settings (browser-notify.js, 2026-09-13). */
.bn-settings { margin: 18px 0 6px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 8px; }
.bn-settings h4 { margin: 0 0 6px; }
.bn-master { display: flex; align-items: center; gap: 8px; font-weight: bold; margin: 6px 0; }
.bn-state { font-size: .82rem; color: var(--text-hint); margin: 2px 0 8px; }
.bn-state.bad { color: var(--danger-text); }
.bn-kinds { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 6px 14px; margin: 6px 0 10px; }
.bn-kinds label { display: flex; align-items: center; gap: 8px; font-size: .9rem; }
.bn-kinds label.off { opacity: .5; }
.bn-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.bn-msg { font-size: .85rem; margin: 6px 0 0; }

/* ---- Legal Dialog (Terms, Privacy, Cookies) ---- */
.legal-dialog {
    box-sizing: border-box;
    width: 100%;
    max-width: 820px;
    max-height: calc(100vh - 80px);
    display: flex;
    flex-direction: column;
    border-radius: 8px;
    padding: 22px 26px 18px;
    background: var(--bg-panel);
    border: 1px solid var(--gold);
}
.legal-header {
    display: flex;
    flex-direction: column;
    gap: 12px;
    border-bottom: 1px solid var(--border);
    padding-bottom: 14px;
    margin-bottom: 4px;
    flex-shrink: 0;
}
.legal-header-top {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.legal-main-title {
    margin: 0;
    font-size: 1.3rem;
    font-weight: 600;
    color: var(--gold-text);
}
.legal-close-x {
    background: none;
    border: none;
    font-size: 1.5rem;
    line-height: 1;
    color: var(--text-hint);
    cursor: pointer;
    padding: 2px 6px;
    border-radius: 4px;
    transition: color 0.15s, background 0.15s;
}
.legal-close-x:hover, .legal-close-x:focus-visible {
    color: var(--danger-text);
    background: var(--bg-alt, rgba(0,0,0,0.05));
}
.legal-tabs {
    display: inline-flex;
    align-self: flex-start;
    gap: 4px;
    background: var(--bg-body);
    border: 1px solid var(--border);
    border-radius: 7px;
    padding: 3px;
}
.legal-tab {
    font-family: inherit;
    font-size: 0.84rem;
    font-weight: 500;
    color: var(--text-muted);
    cursor: pointer;
    border: 0;
    background: transparent;
    padding: 6px 14px;
    border-radius: 5px;
    transition: all 0.15s ease;
    white-space: nowrap;
    line-height: 1.35;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.legal-tab:hover:not(.active) {
    color: var(--text-main);
}
.legal-tab.active {
    background: var(--gold);
    color: var(--on-gold);
    font-weight: 600;
}
.legal-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 14px 10px 10px 0;
    line-height: 1.6;
    font-size: 0.88rem;
    color: var(--text-main);
}
.legal-body::-webkit-scrollbar {
    width: 6px;
}
.legal-body::-webkit-scrollbar-thumb {
    background: var(--gold);
    border-radius: 3px;
}
.legal-doc-head {
    margin-bottom: 14px;
    padding-bottom: 10px;
    border-bottom: 1px dashed var(--border);
}
.legal-doc-title {
    margin: 0 0 4px;
    font-size: 1.15rem;
    color: var(--gold-text);
    font-weight: 600;
}
.legal-subtitle {
    margin: 0;
    font-size: 0.8rem;
    color: var(--text-hint);
}
.legal-body h4 {
    color: var(--gold-text);
    margin: 18px 0 6px;
    font-size: 1rem;
    font-weight: 600;
}
.legal-body h4:first-child {
    margin-top: 4px;
}
.legal-body p {
    margin: 0 0 10px;
    color: var(--text-main);
}
.legal-body strong {
    color: var(--text-main);
}
.legal-body ul {
    margin: 0 0 14px 18px;
    padding: 0;
}
.legal-body li {
    margin-bottom: 6px;
    color: var(--text-main);
}
.legal-body code {
    background: var(--bg-panel);
    padding: 1px 5px;
    border-radius: 3px;
    font-size: 0.85em;
    font-family: ui-monospace, Consolas, monospace;
    border: 1px solid var(--border);
}
.legal-body a {
    color: var(--gold-text);
    text-decoration: underline;
}
.legal-footer {
    display: flex;
    justify-content: flex-end;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
    flex-shrink: 0;
}
@media (max-width: 600px) {
    .legal-dialog {
        padding: 16px 14px;
        max-height: calc(100vh - 30px);
    }
    .legal-tabs {
        width: 100%;
        display: flex;
    }
    .legal-tab {
        flex: 1;
        padding: 6px 6px;
        font-size: 0.76rem;
        text-align: center;
    }
}

/* --- The guided tour (2026-09-22, js/tour.js) ---------------------------------------------------
   A spotlight over the real control and a card beside it. The spot's enormous shadow IS the dim
   layer — one element, so the lit hole always matches the target exactly. The shade under it is
   transparent and there only to take clicks: the page behind is not usable mid-tour. Above every
   other overlay (the highest, the outfit zoom, is 2000). */
.tour-root { position: fixed; inset: 0; z-index: 2500; }
.tour-shade { position: fixed; inset: 0; }
.tour-center .tour-shade { background: rgba(0, 0, 0, 0.55); }
.tour-spot {
    position: fixed; border-radius: 10px; pointer-events: none;
    box-shadow: 0 0 0 3px var(--gold), 0 0 0 200vmax rgba(0, 0, 0, 0.55);
    transition: left .22s ease, top .22s ease, width .22s ease, height .22s ease;
}
.tour-card {
    position: fixed; box-sizing: border-box; width: min(360px, calc(100vw - 24px));
    padding: 16px 18px 14px; border-radius: 10px;
    background: var(--bg-panel); color: var(--text-main);
    border: 1px solid var(--gold);
    box-shadow: var(--shadow-hover, 0 10px 30px rgba(0, 0, 0, 0.35));
    transition: left .22s ease, top .22s ease;
}
.tour-card:focus { outline: none; }
.tour-card:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.tour-center .tour-card { left: 50%; top: 50%; transform: translate(-50%, -50%); transition: none; }
.tour-sheet .tour-card { left: 12px; right: 12px; width: auto; transition: none; }
.tour-count { margin: 0 0 4px; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--gold-text); }
.tour-card h3 { margin: 0 0 6px; font-size: 1.1rem; }
/* The steps are plain text (textContent); their line breaks are real ones. */
.tour-body { margin: 0; font-size: 0.92rem; line-height: 1.55; white-space: pre-line; color: var(--text-main); }
.tour-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
.tour-gap { flex: 1; }
.tour-actions .action-btn { padding: 6px 14px; }
.tour-actions .action-btn:disabled { opacity: 0.45; cursor: default; }
@media (prefers-reduced-motion: reduce) { .tour-spot, .tour-card { transition: none; } }

/* The once-only offer on a first visit to Home. BELOW every dialog (2026-10-07): it was 1150,
   above the sign-in box (.auth-overlay, 1000), so a first-time visitor on a phone who opened
   "Create an account" had "New here? Register" and "Forgot credentials?" covered by it. Under 1000
   the sign-in box, and every other overlay, dims it like the rest of the page. */
.tour-offer {
    position: fixed; left: 20px; bottom: 20px; z-index: 990; box-sizing: border-box;
    width: min(340px, calc(100vw - 32px)); padding: 14px 16px; border-radius: 10px;
    background: var(--bg-panel); color: var(--text-main); border: 1px solid var(--gold);
    box-shadow: var(--shadow-hover, 0 10px 30px rgba(0, 0, 0, 0.35));
}
.tour-offer p { margin: 0 0 10px; font-size: 0.92rem; line-height: 1.5; }
.tour-offer-actions { display: flex; align-items: center; gap: 14px; }
/* On a phone the control rail sits along the foot of the screen — stay above it. */
@media (max-width: 700px) { .tour-offer { left: 16px; right: 16px; width: auto; bottom: 72px; } }


/* --- Home: the cards, and last week's winning outfit beside them (2026-09-22) -------------------
   Two bodies side by side, as the owner laid it out: .home-cards keeps exactly what its container's
   inline style used to say, and the winner's panel is a fixed column at its right — below it once
   the row is too narrow to keep two cards beside the panel. The picture sits in the gallery's own
   square frame (dark ground, object-fit: contain), so a tall or wide outfit shot fits uncropped. */
.home-row { display: flex; justify-content: center; align-items: flex-start; gap: 30px; margin-top: 40px; }
.home-cards { display: flex; justify-content: center; gap: 30px; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
/* The title reads like the cards' own titles — same size, top left (a card's h3 is a flex row, so
   text-align would not move it anyway). Tighter padding than a card's 30px, for the picture's sake. */
.home-winner { flex: 0 0 240px; box-sizing: border-box; padding: 16px; text-align: left; }
.home-winner[hidden] { display: none; }
.home-winner-title { margin: 5px 0 12px; }
.home-winner-fig {
    display: block; width: 100%; aspect-ratio: 1 / 1; padding: 0; overflow: hidden; cursor: pointer;
    background: #0b0b0d; border: 1px solid var(--border); border-radius: 8px;
}
.home-winner-fig img { display: block; width: 100%; height: 100%; object-fit: contain; }
.home-winner-fig:hover, .home-winner-fig:focus-visible { border-color: var(--gold); outline: none; }
@media (max-width: 960px) {
    .home-row { flex-direction: column; align-items: center; }
    .home-cards { width: 100%; }
    .home-winner { flex: none; width: min(260px, 100%); }
}

/* --- Home: the tagline, the visitor's way in, and each card's live line (2026-09-24) ---------
   The tagline's values moved here from an inline style, plus side padding: on a phone it ran to
   both edges of the screen. */
.home-tagline { font-size: 1.1rem; color: var(--text-muted); font-weight: 300; margin: 0; padding: 0 16px; }
.home-guest { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 10px 22px; margin-top: 26px; }
.home-guest[hidden] { display: none; }
.home-guest .submit-btn { width: auto; margin: 0; padding: 11px 30px; }
.home-guest-join {
    background: none; border: none; padding: 4px 0; font: inherit; cursor: pointer;
    color: var(--text-main); border-bottom: 1px solid var(--gold);
}
.home-guest-join:hover, .home-guest-join:focus-visible { color: var(--gold-text); outline: none; }
/* The Download button under them (js/download.js), the Log in button's twin. An anchor, so it
   loses the underline and gets the button's box. */
.home-download { flex-basis: 100%; display: flex; justify-content: center; margin-top: 4px; }   /* its own row in .home-guest */
.home-download-btn { display: inline-flex; align-items: center; gap: 8px; width: auto; margin: 0; padding: 11px 30px; text-decoration: none; }
.home-download-btn:hover { color: var(--on-gold); }
/* What is happening on that page right now (home.js). Accent ink, so it reads as news rather
   than as more of the card's description. */
.home-live { margin: 10px 0 0; font-size: 0.82rem; color: var(--gold-text); }
.home-live[hidden] { display: none; }
.home-live-link { color: inherit; cursor: pointer; border-bottom: 1px solid currentColor; text-decoration: none; }
.home-live-link:hover, .home-live-link:focus-visible { color: var(--text-main); }

/* A Database entry's "Guides" block (database.js fillDetailGuides, 2026-09-24): the guides that
   link to it, above the correction button. */
.detail-guides { margin-top: 30px; }
.detail-guides h4 { margin: 0 0 8px; }
.detail-guides-list { margin: 0; padding-left: 20px; }

/* ON A PHONE THE CONTROL RAIL LIES ALONG THE BOTTOM EDGE (max-width 700px, above), and at the end
   of the page it sat over the footer's legal notice. The footer keeps its own padding plus the
   rail's height (36px controls + 20px padding + 12px gap), so the last line scrolls clear of it. */
@media (max-width: 700px) {
    .site-footer { padding-bottom: calc(24px + 68px + 12px); }
}

/* ---- One search for everything (js/search.js) ---- */
.gs-overlay { position: fixed; inset: 0; z-index: 1300; display: flex; justify-content: center; align-items: flex-start;
    padding: 12vh 16px 16px; background: rgba(0, 0, 0, .45); animation: ui-fade-in .12s ease both; }
.gs-panel { width: 560px; max-width: 100%; max-height: 70vh; display: flex; flex-direction: column; overflow: hidden;
    background: var(--bg-float); border: 1px solid var(--border); border-radius: var(--r-lg); box-shadow: var(--shadow-card); }
.gs-field { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border); color: var(--text-muted); }
.gs-field input { flex: 1; min-width: 0; border: none; outline: none; background: none; color: var(--text); font: inherit; font-size: 1rem; padding: 2px 0; }
.gs-field input::-webkit-search-cancel-button { display: none; }
.gs-key { font: inherit; font-size: .68rem; padding: 2px 6px; border: 1px solid var(--border); border-radius: 4px; color: var(--text-muted); }
.gs-results { overflow-y: auto; padding: 6px; }
.gs-group { margin: 8px 8px 4px; font-size: .7rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.gs-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; width: 100%; text-align: left; cursor: pointer;
    padding: 8px 10px; background: none; border: 1px solid transparent; border-radius: var(--r-sm); color: inherit; font: inherit; }
.gs-row.on, .gs-row:hover { background: var(--gold-light); border-color: var(--border-gold); }
.gs-label { font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gs-sub { flex-shrink: 0; font-size: .74rem; color: var(--text-muted); }
.gs-hint { padding: 16px; margin: 0; text-align: center; color: var(--text-muted); font-size: .84rem; }
@media (max-width: 700px) { .gs-overlay { padding-top: 10px; } .gs-key { display: none; } }

/* ---- Download page (js/download.js) ---- */
.dl-page { max-width: 560px; margin: 24px auto; padding: 32px 30px; text-align: center; }
.dl-icon { display: block; margin: 0 auto; width: 96px; height: 96px; border-radius: 22px; box-shadow: var(--shadow-card); }
.dl-title { margin: 14px 0 4px; }
.dl-lead { margin: 0 0 22px; color: var(--text-muted); }
.dl-button { display: inline-flex; align-items: center; gap: 8px; padding: 12px 26px; font-weight: 700; text-decoration: none;
    color: var(--gold-text); border: 1px solid var(--gold); border-radius: var(--r-lg); background: var(--gold-light); }
.dl-button:hover { background: var(--gold); color: var(--bg-float); }
.dl-meta { margin: 10px 0 0; font-size: .84rem; color: var(--text-muted); }
.dl-note { margin: 12px 0 0; font-size: .86rem; color: var(--text-muted); }
.dl-h { margin: 26px 0 6px; font-size: .78rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); text-align: left; }
.dl-text { margin: 0 0 8px; text-align: left; line-height: 1.5; }
.dl-sha { display: flex; align-items: center; gap: 8px; margin: 0; text-align: left; }
.dl-sha code { flex: 1; min-width: 0; overflow-wrap: anywhere; font-size: .76rem; color: var(--text-muted); }
.dl-copy { flex-shrink: 0; background: none; border: 1px solid var(--border); border-radius: var(--r-sm); padding: 4px 6px; cursor: pointer; color: var(--text-muted); }
.dl-copy:hover { border-color: var(--gold); color: var(--gold-text); }

/* Step-by-step forms (stepform.js, 2026-10-08): one field at a time; each new one glides in
   below the last. Tab on an empty optional field skips it; on a required one it shakes red. */
.sf-hidden { display: none !important; }
.sf-step.sf-enter { animation: sf-glide .34s cubic-bezier(.2, .8, .3, 1) both; }
@keyframes sf-glide { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: none; } }
.sf-step.sf-shake { animation: sf-shake .42s ease both; }
@keyframes sf-shake {
    15%, 55% { transform: translateX(-7px); }
    35%, 75% { transform: translateX(7px); }
    90% { transform: translateX(-2px); }
}
.sf-step.sf-invalid > label { color: var(--danger-text); }
.sf-step.sf-invalid :is(input, select, textarea) { border-color: var(--danger-text) !important; box-shadow: 0 0 0 1px var(--danger-text); }
.sf-next {
    display: inline-flex; align-items: center; gap: 8px; margin-top: 10px;
    background: none; border: 1px solid var(--border); border-radius: var(--r-sm);
    padding: 5px 10px; font-size: .8rem; color: var(--text-muted); cursor: pointer;
}
.sf-next[hidden] { display: none; }
.sf-next:hover, .sf-next:focus-visible { border-color: var(--gold); color: var(--gold-text); }
.sf-next kbd {
    font: inherit; font-size: .7rem; padding: 1px 5px; border: 1px solid currentColor;
    border-radius: 3px; opacity: .7;
}
@media (prefers-reduced-motion: reduce) { .sf-step.sf-enter, .sf-step.sf-shake { animation: none; } }