/* =============================================================
   CYBERPUNK / FUTURISTIC THEME — Our Dark Home
   ============================================================= */

/* ---- CSS Variables ---- */
:root {
    --cy-bg:          #05050f;
    --cy-surface-1:   #09091e;
    --cy-surface-2:   #0d0d2b;
    --cy-surface-3:   #111135;
    --cy-border:      #1c1c50;
    --cy-border-glow: rgba(0, 245, 255, 0.25);
    --cy-cyan:        #00f5ff;
    --cy-cyan-dim:    rgba(0, 245, 255, 0.15);
    --cy-cyan-glow:   rgba(0, 245, 255, 0.6);
    --cy-magenta:     #ff2d78;
    --cy-magenta-dim: rgba(255, 45, 120, 0.15);
    --cy-green:       #39ff14;
    --cy-green-dim:   rgba(57, 255, 20, 0.15);
    /* Companion tone for green TEXT, same idea as --cy-yellow-soft below. Still
       stepped back from #39ff14 — the acid green is for dots, borders and icons,
       where its job is to be spotted — but only just: a green cannot get much
       brighter than this without going pastel, because luminance in this hue
       peaks around pure green and everything above it is really "more white".
       Perceived brightness past this point comes from the glow, not the fill. */
    --cy-green-soft:  #b6ffab;
    --cy-yellow:      #ffd700;
    /* Softer companions to the gold above, matching the -dim pattern used by
       cyan, magenta and green. The soft tone is for text and icons: #ffd700 at
       small sizes on a near-black background reads as an alert, which is wrong
       for a state that just means "waiting". */
    --cy-yellow-soft: #ffe98a;
    --cy-yellow-dim:  rgba(255, 215, 0, 0.12);

    /* Restricted or closed, as distinct from both gold and red. Gold means
       elevated (pinned); red means destructive (delete). A lock is neither —
       it is a door that is shut, and it needed its own tone rather than
       borrowing one that already means something else. */
    --cy-amber:       #ffa64d;
    --cy-amber-dim:   rgba(255, 166, 77, 0.14);

    /* The one hue on this site that does NOT already encode a state. Cyan is
       default, gold is pinned, amber is locked, magenta is staff, green is
       presence, red is destructive — so anything genuinely decorative had no
       colour left to use without borrowing a meaning it does not have.
       Was already here as a literal on the music activity icon; promoted to a
       token when the tab separator needed exactly this: visible, distinct, and
       saying nothing. */
    --cy-violet:      #a78bfa;
    --cy-text:        #c8d8f8;
    /* Secondary text. Was #7080b0, which cleared AA on paper (5.05:1) and still
       read as washed-out grey-blue at the small sizes it is mostly used at —
       timestamps, meta lines, hints, half the chrome on the site. Lifted to
       8.17:1, which is the single change that does most for legibility here,
       because this is the colour nearly all the hard-to-read text was in. */
    --cy-text-dim:    #a6b2da;

    /* Secondary CONTENT, as opposed to secondary chrome.
       A forum description, a "last posted by" line, a relative time: things a
       reader actually reads, that simply are not the headline. --cy-text-dim
       is for chrome that should recede — hints, placeholders, separators. The
       forum and PM modules had no middle tier available and reached for -dim
       for both, which is how ordinary content ended up painted as furniture. */
    --cy-text-soft:   #b9c4e6;
    --cy-error:       #ff3a3a;
    --cy-font-hud:    'Orbitron', 'Share Tech Mono', monospace;
    --cy-font-body:   'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}

/* ====================== BASE ====================== */
body {
    background-color: var(--cy-bg);
    background-image:
        repeating-linear-gradient(
            0deg, transparent, transparent 2px,
            rgba(0, 245, 255, 0.013) 2px,
            rgba(0, 245, 255, 0.013) 4px
        ),
        repeating-linear-gradient(
            90deg, transparent, transparent 40px,
            rgba(28, 28, 80, 0.18) 40px,
            rgba(28, 28, 80, 0.18) 41px
        ),
        repeating-linear-gradient(
            180deg, transparent, transparent 40px,
            rgba(28, 28, 80, 0.18) 40px,
            rgba(28, 28, 80, 0.18) 41px
        );
    color: var(--cy-text);
    font-family: var(--cy-font-body);
    font-size: 1rem;
    /* There was no line-height at all, so everything inherited the browser
       default of roughly 1.2 — tight enough that paragraphs of any length read
       as a block. 1.6 is the usual comfortable value for body copy and costs
       only vertical space. */
    line-height: 1.6;
    padding-top: 64px;
}

/* Ambient glow overlay */
body::before {
    content: "";
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    z-index: -2;
    background:
        radial-gradient(ellipse at 20% 20%, rgba(0, 245, 255, 0.05) 0%, transparent 55%),
        radial-gradient(ellipse at 80% 80%, rgba(255, 45, 120, 0.05) 0%, transparent 55%),
        radial-gradient(ellipse at 50% 0%,  rgba(0, 245, 255, 0.06) 0%, transparent 50%);
    pointer-events: none;
    will-change: transform;
    transform: translateZ(0);
}

/* Scanline overlay — static, no animation */
body::after {
    content: "";
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    z-index: -1;
    background: repeating-linear-gradient(
        0deg,
        rgba(0,0,0,0.06) 0px,
        rgba(0,0,0,0.06) 1px,
        transparent 1px,
        transparent 3px
    );
    will-change: transform;
    transform: translateZ(0);
    pointer-events: none;
    opacity: 0.45;
}

/* ====================== TYPOGRAPHY ====================== */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--cy-font-hud);
    letter-spacing: 0.04em;
}

.gothic-title {
    font-family: var(--cy-font-hud);
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--cy-cyan);
}

.gothic-title-enhanced {
    font-family: var(--cy-font-hud);
    letter-spacing: 0.1em;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--cy-cyan);
    text-shadow:
        0 0 8px  var(--cy-cyan),
        0 0 18px rgba(0, 245, 255, 0.5),
        0 0 35px rgba(0, 245, 255, 0.25);
    position: relative;
}

/* Glitch animation for title hover */
.gothic-title:hover {
    animation: glitch 0.4s ease-in-out;
}

@keyframes glitch {
    0%, 100% { transform: none; }
    20%  { transform: translateX(-2px) skewX(-2deg); clip-path: polygon(0 20%, 100% 20%, 100% 45%, 0 45%); }
    40%  { transform: translateX(2px)  skewX(2deg);  clip-path: polygon(0 55%, 100% 55%, 100% 80%, 0 80%); }
    60%  { transform: translateX(-1px); }
    80%  { transform: translateX(1px); }
}

/* ====================== LAYOUT ====================== */
.gothic-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 1rem;
}

.gothic-navbar {
    border-bottom: 1px solid var(--cy-border);
    background-color: rgba(5, 5, 15, 0.95);
}

/* ====================== CARDS ====================== */
.gothic-card {
    background-color: var(--cy-surface-1);
    border: 1px solid var(--cy-border);
    border-radius: 0;
    clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
    box-shadow: 0 4px 20px rgba(0,0,0,0.6);
    transition: transform 0.3s ease, box-shadow 0.3s ease;
    position: relative;
}

.gothic-card::before {
    content: '';
    position: absolute;
    inset: 0;
    border: 1px solid rgba(0, 245, 255, 0.08);
    pointer-events: none;
    transition: border-color 0.3s ease;
}

.gothic-card-hover:hover,
.gothic-card:hover {
    box-shadow:
        0 8px 30px rgba(0,0,0,0.7),
        0 0 20px rgba(0, 245, 255, 0.12);
    transform: translateY(-3px);
}

.gothic-card:hover::before {
    border-color: rgba(0, 245, 255, 0.3);
}

/* ====================== ENHANCED BORDER ====================== */
.gothic-enhanced-border {
    position: relative;
    border: 1px solid var(--cy-border);
    box-shadow:
        0 0 15px rgba(0, 245, 255, 0.08),
        inset 0 0 15px rgba(0, 245, 255, 0.03);
}

.gothic-enhanced-border::before {
    content: '';
    position: absolute;
    top: 4px; right: 4px; bottom: 4px; left: 4px;
    border: 1px solid rgba(0, 245, 255, 0.12);
    pointer-events: none;
}

/* Animated corner brackets */
.gothic-enhanced-border::after {
    content: '';
    position: absolute;
    top: -1px; left: -1px;
    width: 16px; height: 16px;
    border-top: 2px solid var(--cy-cyan);
    border-left: 2px solid var(--cy-cyan);
    box-shadow: -2px -2px 8px rgba(0, 245, 255, 0.4);
    pointer-events: none;
}

/* Bottom-right corner accent via wrapper trick */
.gothic-enhanced-border-wrap {
    position: relative;
}
.gothic-enhanced-border-wrap::after {
    content: '';
    position: absolute;
    bottom: -1px; right: -1px;
    width: 16px; height: 16px;
    border-bottom: 2px solid var(--cy-magenta);
    border-right: 2px solid var(--cy-magenta);
    box-shadow: 2px 2px 8px rgba(255, 45, 120, 0.4);
    pointer-events: none;
}

/* ====================== SURFACE COLORS ====================== */
.bg-gothic-charcoal { background-color: var(--cy-surface-2); }
.bg-gothic-black    { background-color: var(--cy-bg); }
.bg-gothic-darkgray { background-color: var(--cy-surface-1); }
.bg-gothic-accent   { background-color: var(--cy-cyan); }

/* ====================== TEXT COLORS ======================
   Prefixed with `body` deliberately. The compiled Tailwind bundle is loaded
   AFTER this file and defines .text-gothic-muted itself, at the same
   specificity — so an unprefixed rule here loses, and the lifted --cy-text-dim
   above would never reach the hundreds of views that use the utility class.
   `body .x` is (0,1,1) against Tailwind's (0,1,0) and wins without !important. */
body .text-gothic-text      { color: var(--cy-text); }
body .text-gothic-muted     { color: var(--cy-text-dim); }
body .text-gothic-highlight { color: var(--cy-cyan); }

/* Error states */
.text-error,
.text-red-300,
.text-red-400,
.text-red-500,
.text-red-600 {
    color: var(--cy-error) !important;
    text-shadow: 0 0 8px rgba(255, 58, 58, 0.5);
}

form p.text-error,
form p[class*="text-red"] {
    color: var(--cy-error) !important;
}

/* ====================== BORDERS ====================== */
.border-gothic-border    { border-color: var(--cy-border); }
.border-gothic-highlight { border-color: var(--cy-cyan); }

/* ====================== FORMS ====================== */
.gothic-input {
    color: var(--cy-text) !important;
    background-color: var(--cy-surface-2) !important;
    border-color: var(--cy-border) !important;
    transition: border-color 0.3s ease, box-shadow 0.3s ease;
    padding: 0.5rem 0.75rem;
    box-shadow: inset 0 2px 8px rgba(0,0,0,0.4);
    font-family: var(--cy-font-body);
    letter-spacing: 0.03em;
}

.gothic-input:focus {
    border-color: var(--cy-cyan) !important;
    box-shadow:
        0 0 0 2px rgba(0, 245, 255, 0.2),
        0 0 12px rgba(0, 245, 255, 0.15),
        inset 0 2px 8px rgba(0,0,0,0.4) !important;
    outline: none;
}

.gothic-input::placeholder { color: var(--cy-text-dim) !important; }

/* Global form overrides */
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="reset"]),
textarea,
select {
    color: var(--cy-text) !important;
    background-color: var(--cy-surface-2) !important;
    border-color: var(--cy-border) !important;
    font-family: var(--cy-font-body);
}

input:not([type="checkbox"]):not([type="radio"]):focus,
textarea:focus,
select:focus {
    border-color: var(--cy-cyan) !important;
    box-shadow: 0 0 0 2px rgba(0, 245, 255, 0.2), 0 0 10px rgba(0, 245, 255, 0.12) !important;
    outline: none !important;
}

/*
   [!] THE BORDER HERE IS THE WHOLE CONTROL. Do not dim it.

   Tailwind's forms plugin sets appearance:none on every checkbox and radio, so
   the browser draws NOTHING — no box, no tick. What you see unchecked is
   entirely this border and this background.

   border-color used to be --cy-border (#1c1c50). Against the near-black panels
   these sit on — the guidelines modal is a #050512 to #0a0a22 gradient — that
   is a near-black outline on a near-black background, and the control was
   effectively invisible until ticked. It still worked; there was just nothing
   to aim at. A prospective member reported the guidelines box as "not
   tickable", which is exactly what an invisible checkbox looks like.

   Cyan at 55% reads clearly on every dark surface on the site without shouting
   as loudly as a solid --cy-cyan outline would on a page full of them.
*/
input[type="checkbox"],
input[type="radio"] {
    background-color: var(--cy-surface-2) !important;
    border-color: rgba(0, 245, 255, 0.55) !important;
    accent-color: var(--cy-cyan);
    cursor: pointer;
}

input[type="checkbox"]:hover,
input[type="radio"]:hover {
    border-color: var(--cy-cyan) !important;
}

/* Tailwind's focus ring is its default blue (#2563eb), which is off-brand and
   easy to miss on these surfaces. focus-visible only, so a mouse click does not
   leave a ring behind. */
input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
    outline: 2px solid var(--cy-cyan);
    outline-offset: 2px;
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
    background-color: var(--cy-cyan) !important;
    border-color: var(--cy-cyan) !important;
}

/* Select arrow */
select {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2300f5ff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e") !important;
    background-position: right 0.5rem center !important;
}

/* Autofill override */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--cy-text);
    -webkit-box-shadow: 0 0 0px 1000px #0d0d2b inset;
    transition: background-color 5000s ease-in-out 0s;
}

/* ====================== BUTTONS ====================== */
.gothic-btn {
    background-color: transparent;
    color: var(--cy-cyan);
    border: 1px solid var(--cy-cyan);
    padding: 0.4rem 0.9rem;
    transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
    font-family: var(--cy-font-hud);
    letter-spacing: 0.06em;
    font-size: 0.8rem;
    text-transform: uppercase;
    clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
    box-shadow: 0 0 8px rgba(0, 245, 255, 0.2), inset 0 0 8px rgba(0, 245, 255, 0.04);
    cursor: pointer;
}

.gothic-btn:hover {
    background-color: var(--cy-cyan-dim);
    color: #ffffff;
    box-shadow: 0 0 16px rgba(0, 245, 255, 0.5), inset 0 0 12px rgba(0, 245, 255, 0.1);
    transform: translateY(-1px);
    text-shadow: 0 0 6px var(--cy-cyan);
}

.gothic-btn:active { transform: translateY(1px); }

/* Destructive variant — same silhouette, red instead of cyan, so Clear All
   never reads as just another action next to Mark All as Read. */
.gothic-btn-danger {
    color: var(--cy-error);
    border-color: var(--cy-error);
    box-shadow: 0 0 8px rgba(255, 58, 58, 0.2), inset 0 0 8px rgba(255, 58, 58, 0.04);
}

.gothic-btn-danger:hover {
    background-color: rgba(255, 58, 58, 0.15);
    color: #ffffff;
    box-shadow: 0 0 16px rgba(255, 58, 58, 0.5), inset 0 0 12px rgba(255, 58, 58, 0.1);
    text-shadow: 0 0 6px rgba(255, 58, 58, 0.8);
}

/* Cancel / secondary variant — same silhouette, MAGENTA instead of cyan.

   Originally this was grey at rest and only found its colour on hover, on the
   reasoning that a cancel button glowing as brightly as the action it backs out
   of leaves two equally lit buttons with nothing to say which one posts.

   The hierarchy argument was right; the conclusion was not. The site's OTHER
   button family, .gothic-button-secondary, has always been magenta at rest, so
   the two disagreed with each other — a Cancel next to a Post looked like a
   different kind of control depending on which family the screen happened to
   use. They match now.

   The subordination comes from a softer border and a dimmer glow rather than
   from having no colour at all. Still NOT red: backing out is not destructive,
   and .gothic-btn-danger is spoken for by things that actually delete. */
.gothic-btn-cancel {
    color: var(--cy-magenta);
    /* Border and glow a shade softer than the primary's solid cyan, which is
       what keeps the hierarchy the note above cares about. The COLOUR is what
       makes it a secondary button; being colourless was never the point. */
    border-color: rgba(255, 45, 120, 0.55);
    box-shadow: 0 0 8px rgba(255, 45, 120, 0.15), inset 0 0 8px rgba(255, 45, 120, 0.03);
}

.gothic-btn-cancel:hover {
    background-color: rgba(255, 45, 120, 0.15);
    border-color: var(--cy-magenta);
    color: #ffffff;
    box-shadow: 0 0 16px rgba(255, 45, 120, 0.45), inset 0 0 12px rgba(255, 45, 120, 0.08);
    text-shadow: 0 0 6px var(--cy-magenta);
}

/* "New Posts" — FILLED cyan, the only solid button on the site.

   It needed to be unmistakable next to New Thread and Mark All Read without
   inventing a meaning. Every other hue here is spoken for: gold is pinned,
   amber locked, magenta staff and secondary actions, green presence, red
   destructive, violet decorative. Cyan is the site's "there is something here"
   tone and is already what every unread signal uses — the pills, the dots, the
   left rule on an unread row — so this button is the same idea in the same
   colour.

   Filling it rather than tinting it is what makes it distinct: nothing else on
   the board is a solid block of cyan, so it reads as its own thing at a glance
   while still belonging to the unread family.

   Dark text on the fill, not white: #05050f on #00f5ff is about 14:1, where
   white on cyan is barely 1.3:1 and would be close to unreadable. */
.gothic-btn-new {
    background-color: var(--cy-cyan);
    color: var(--cy-bg);
    border-color: var(--cy-cyan);
    font-weight: 700;
    box-shadow: 0 0 12px rgba(0, 245, 255, 0.35), inset 0 0 8px rgba(255, 255, 255, 0.15);
}

.gothic-btn-new:hover {
    background-color: #6bfbff;
    color: var(--cy-bg);
    box-shadow: 0 0 20px rgba(0, 245, 255, 0.6), inset 0 0 10px rgba(255, 255, 255, 0.25);
    /* The base .gothic-btn:hover adds a cyan text-shadow, which on dark text
       over a cyan fill is a glow the same colour as the background — it reads
       as the letters smudging. */
    text-shadow: none;
}

.gothic-button {
    background-color: transparent;
    color: var(--cy-cyan);
    padding: 0.55rem 1.15rem;
    border: 1px solid var(--cy-cyan);
    font-family: var(--cy-font-hud);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease;
    position: relative;
    overflow: hidden;
    clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px));
    box-shadow: 0 0 10px rgba(0, 245, 255, 0.2), inset 0 0 10px rgba(0, 245, 255, 0.04);
}

/* Sweep animation on hover */
.gothic-button::before {
    content: '';
    position: absolute;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: linear-gradient(90deg, transparent 0%, rgba(0,245,255,0.12) 50%, transparent 100%);
    transform: translateX(-200%);
    transition: transform 0.4s ease;
}

.gothic-button:hover::before { transform: translateX(200%); }

.gothic-button:hover {
    background-color: var(--cy-cyan-dim);
    color: #ffffff;
    box-shadow: 0 0 18px rgba(0, 245, 255, 0.5), inset 0 0 14px rgba(0, 245, 255, 0.08);
    transform: translateY(-2px);
    text-shadow: 0 0 8px var(--cy-cyan);
}

.gothic-button:active {
    transform: translateY(1px);
    box-shadow: 0 0 8px rgba(0, 245, 255, 0.3);
}

/* Small variant */
.gothic-button-sm {
    padding: 0.3rem 0.75rem;
    font-size: 0.75rem;
    clip-path: polygon(0 0, calc(100% - 7px) 0, 100% 7px, 100% 100%, 7px 100%, 0 calc(100% - 7px));
}

/* Secondary button (magenta) */
.gothic-button-secondary {
    border-color: var(--cy-magenta);
    color: var(--cy-magenta);
    box-shadow: 0 0 10px rgba(255, 45, 120, 0.2), inset 0 0 10px rgba(255, 45, 120, 0.03);
}

.gothic-button-secondary::before {
    background: linear-gradient(90deg, transparent 0%, rgba(255,45,120,0.12) 50%, transparent 100%);
}

.gothic-button-secondary:hover {
    background-color: var(--cy-magenta-dim);
    color: #ffffff;
    box-shadow: 0 0 18px rgba(255, 45, 120, 0.5), inset 0 0 14px rgba(255, 45, 120, 0.08);
    text-shadow: 0 0 8px var(--cy-magenta);
    border-color: var(--cy-magenta);
    transform: translateY(-2px);
}

/* Danger button */
.gothic-button-danger {
    border-color: var(--cy-error);
    color: var(--cy-error);
    box-shadow: 0 0 10px rgba(255, 58, 58, 0.2);
}

.gothic-button-danger:hover {
    background-color: rgba(255, 58, 58, 0.15);
    color: #ff8888;
    box-shadow: 0 0 18px rgba(255, 58, 58, 0.5);
    text-shadow: 0 0 8px var(--cy-error);
    border-color: var(--cy-error);
}

/* Ornate corners via pseudo-elements */
.gothic-button-ornate {
    position: relative;
}
.gothic-button-ornate::after {
    content: '';
    position: absolute;
    top: -2px; left: -2px;
    width: 8px; height: 8px;
    border-top: 1px solid rgba(0,245,255,0.6);
    border-left: 1px solid rgba(0,245,255,0.6);
    pointer-events: none;
}

/* Pulse for primary CTA */
.gothic-button-pulse {
    /* box-shadow pulse removed for scroll performance */
}

/* ====================== LINKS ====================== */
.gothic-link {
    position: relative;
    color: var(--cy-cyan);
    transition: color 0.3s ease;
    text-decoration: none;
}

.gothic-link::after {
    content: '';
    position: absolute;
    bottom: -2px; left: 50%;
    width: 0; height: 1px;
    background: var(--cy-cyan);
    transition: width 0.3s ease;
    transform: translateX(-50%);
    box-shadow: 0 0 4px var(--cy-cyan);
}

.gothic-link:hover {
    color: #ffffff;
    text-shadow: 0 0 8px var(--cy-cyan);
}

.gothic-link:hover::after { width: 100%; }

/* ====================== DIVIDERS ====================== */
.gothic-divider {
    position: relative;
    height: 30px;
    margin: 2rem 0;
}

.gothic-divider::before {
    content: '';
    position: absolute;
    top: 50%; left: 0; right: 0;
    height: 1px;
    background: linear-gradient(to right, transparent, var(--cy-cyan), transparent);
    box-shadow: 0 0 6px var(--cy-cyan);
}

.gothic-divider::after {
    content: '◈';
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    background-color: var(--cy-surface-1);
    color: var(--cy-cyan);
    padding: 0 1rem;
    font-size: 0.9rem;
    text-shadow: 0 0 8px var(--cy-cyan);
}

/* ====================== NAVIGATION ====================== */
.sticky-nav {
    position: fixed;
    top: 0; left: 0; right: 0;
    z-index: 1000;
    background: linear-gradient(to bottom, rgba(5,5,18,0.98), rgba(9,9,28,0.97));
    box-shadow: 0 4px 20px rgba(0,0,0,0.6), 0 1px 0 rgba(0,245,255,0.15);
    border-bottom: 1px solid rgba(0, 245, 255, 0.25);
    transition: background 0.3s ease;
    will-change: transform;
    transform: translateZ(0);
}

/* Static bottom glow line */
.sticky-nav::after {
    content: '';
    position: absolute;
    bottom: -1px; left: 0; right: 0;
    height: 1px;
    background: linear-gradient(to right, transparent, var(--cy-cyan) 30%, var(--cy-magenta) 60%, var(--cy-cyan) 80%, transparent);
}

/* Logo */
.nav-logo {
    position: relative;
    display: inline-block;
    transition: transform 0.3s ease;
    text-decoration: none;
    letter-spacing: 0.08em;
    pointer-events: auto;
}

.nav-logo:hover {
    text-shadow: 0 0 12px var(--cy-cyan), 0 0 24px rgba(0,245,255,0.5);
    transform: scale(1.04);
}

/* Override glitch animation for the nav logo link:
   the standard glitch uses clip-path which trims the pointer-event hit area,
   making the link unclickable mid-animation. This version keeps the visual
   effect without clip-path so the link is always clickable. */
.nav-logo.gothic-title:hover {
    animation: nav-logo-glitch 0.4s ease-in-out;
}

@keyframes nav-logo-glitch {
    0%, 100% { transform: scale(1.04); }
    20%  { transform: scale(1.04) translateX(-2px) skewX(-2deg); }
    40%  { transform: scale(1.04) translateX(2px)  skewX(2deg); }
    60%  { transform: scale(1.04) translateX(-1px); }
    80%  { transform: scale(1.04) translateX(1px); }
}

/* Nav links */
.nav-link {
    position: relative;
    transition: color 0.3s ease;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.78rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
}

.nav-link::after {
    content: '';
    position: absolute;
    bottom: -2px; left: 50%;
    width: 0; height: 1px;
    background: var(--cy-cyan);
    box-shadow: 0 0 6px var(--cy-cyan);
    transition: width 0.3s ease;
    transform: translateX(-50%);
}

.nav-link:hover {
    color: var(--cy-cyan);
    text-shadow: 0 0 8px rgba(0, 245, 255, 0.5);
}

.nav-link:hover::after { width: 100%; }

/* Mobile menu button */
.mobile-menu-button {
    transition: background-color 0.3s ease, transform 0.3s ease;
}

/* Home icon in nav bar */
.nav-home-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 2rem; height: 2rem;
    color: var(--cy-text-dim);
    font-size: 1rem;
    border: 1px solid rgba(0, 245, 255, 0.15);
    border-radius: 6px;
    text-decoration: none;
    transition: color 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease, background-color 0.25s ease;
}

.nav-home-icon:hover {
    color: var(--cy-cyan);
    border-color: rgba(0, 245, 255, 0.45);
    background-color: rgba(0, 245, 255, 0.06);
    box-shadow: 0 0 10px rgba(0, 245, 255, 0.18);
}

/* Generic tooltip */
.gothic-tooltip-trigger {
    position: relative;
}

.gothic-tooltip {
    pointer-events: none;
    position: absolute;
    top: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    white-space: nowrap;
    background: rgba(5, 5, 18, 0.92);
    color: var(--cy-cyan);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.3rem 0.6rem;
    border: 1px solid rgba(0, 245, 255, 0.3);
    border-radius: 4px;
    box-shadow: 0 0 8px rgba(0, 245, 255, 0.15);
    opacity: 0;
    transition: opacity 0.2s ease;
    z-index: 9999;
}

/* Small arrow */
.gothic-tooltip::after {
    content: '';
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 4px solid transparent;
    border-bottom-color: rgba(0, 245, 255, 0.3);
}

.gothic-tooltip-trigger:hover .gothic-tooltip,
.gothic-tooltip-trigger:focus-within .gothic-tooltip {
    opacity: 1;
}

/* Wrapper form of the trigger: used when the tooltip cannot be a CHILD of the
   control it describes. .gothic-button has overflow:hidden and a clip-path for
   its shine sweep, which would clip an absolutely-positioned tooltip away to
   nothing — so the button and the tooltip become siblings inside this, and the
   wrapper owns the positioning context. inline-flex so it still behaves as one
   item inside the flex rows these buttons sit in. */
.gothic-tooltip-wrap { display: inline-flex; }


.mobile-menu-button:hover {
    background-color: var(--cy-cyan-dim);
    transform: scale(1.05);
}

/* Dropdown menu */
.dropdown-menu {
    background: rgb(9, 9, 28);
    border: 1px solid rgba(0, 245, 255, 0.2);
    box-shadow: 0 12px 30px rgba(0,0,0,0.7), 0 0 20px rgba(0,245,255,0.08);
}

.dropdown-item {
    transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.78rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    text-decoration: none;
    white-space: nowrap;
}

.dropdown-item:hover {
    background-color: var(--cy-cyan-dim);
    color: var(--cy-cyan);
    transform: translateX(4px);
    text-shadow: 0 0 6px rgba(0,245,255,0.4);
}

/* ====================== ACCOUNT MENU GROUPS ======================
   The account dropdown had grown to fourteen flat entries. It is now an
   accordion: four collapsible groups plus a few top-level links.

   Written by hand rather than with Tailwind utilities because the prebuilt
   bundle is from 2026-06-17 and cannot be rebuilt without npm — a utility that
   is not already compiled into it renders as nothing at all, silently. Semantic
   classes here always apply. */

.nav-dropdown-body {
    /* Fixed floor so the menu does not resize as groups open and close —
       a panel that changes width under the cursor is hard to aim at. */
    min-width: 15rem;
}

/* Shared row geometry for every entry, link or button. .dropdown-item supplies
   the colour, font and hover; this supplies the layout. */
.nav-group-item,
.nav-group-toggle {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.5rem 1rem;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
}

.nav-group-toggle {
    justify-content: space-between;
    color: var(--cy-text);
}

.nav-group-toggle > span {
    display: flex;
    align-items: center;
}

/* Fixed width so labels line up in a column regardless of glyph width. */
.nav-group-icon {
    width: 1.1rem;
    margin-right: 0.55rem;
    text-align: center;
    flex-shrink: 0;
}

.nav-group-caret {
    font-size: 0.75rem;
    margin-left: 0.75rem;
    opacity: 0.6;
    transition: transform 0.2s ease, opacity 0.2s ease;
}

.nav-group-toggle.is-open .nav-group-caret {
    transform: rotate(180deg);
    opacity: 1;
}

.nav-group-toggle.is-open {
    color: var(--cy-cyan);
}

/* The rail is what makes an expanded group read as belonging to its header
   rather than as more top-level entries. */
.nav-group-items {
    border-left: 1px solid rgba(0, 245, 255, 0.18);
    margin-left: 1.15rem;
}

.nav-group-items .nav-group-item {
    padding-left: 0.9rem;
    font-size: 0.75rem;
}

.nav-group-logout {
    color: var(--cy-text-dim);
}

/* Replaces `border-t border-gothic-border my-1`. The utilities were doing half
   a job: my-1 is not in the prebuilt bundle, so the old divider sat flush
   against the rows above and below it with no separating space at all. */
.nav-divider {
    height: 1px;
    margin: 0.35rem 0;
    background: rgba(0, 245, 255, 0.18);
}

/* ====================== FOLDED FORUM FEED ENTRIES ======================
   A burst of forum posts collapses into one feed entry listing up to four of
   them. The list is indented under the phrase with a rail, so it reads as
   "these are the posts" rather than as four unrelated feed rows. */

.gothic-forum-refs {
    list-style: none;
    margin: 0.45rem 0 0;
    padding: 0 0 0 0.85rem;
    border-left: 1px solid rgba(0, 245, 255, 0.18);
}

.gothic-forum-refs li {
    margin: 0.2rem 0;
    font-size: 0.85rem;
    line-height: 1.4;
}

/* The forum name is context, not the link — keeping it out of the anchor stops
   it competing with the thread title for the click. */
.gothic-forum-ref-forum {
    color: var(--cy-text-dim);
    font-size: 0.75rem;
    opacity: 0.75;
}

.gothic-forum-ref-forum::before {
    content: "· ";
}

/* ====================== PROFILE WALL ======================
   Posts left on a member's profile, and replies to them. Hand-written rather
   than Tailwind utilities: the prebuilt bundle is stale and cannot be rebuilt,
   so anything not already compiled into it renders as nothing at all. */

.profile-wall-form {
    margin-bottom: 1.1rem;
}

.profile-wall-input {
    width: 100%;
    background: rgba(0, 0, 0, 0.45);
    border: 1px solid var(--cy-border, rgba(0, 245, 255, 0.2));
    border-radius: 5px;
    padding: 0.6rem 0.75rem;
    color: var(--cy-text);
    font-family: inherit;
    font-size: 0.9rem;
    resize: vertical;
}

.profile-wall-input:focus {
    outline: none;
    border-color: var(--cy-cyan);
    box-shadow: 0 0 8px rgba(0, 245, 255, 0.25);
}

.profile-wall-form-actions {
    display: flex;
    /* Was flex-end, with only the submit button in here. The emoji picker now
       sits at the left of the same row and the button stays at the right — the
       arrangement every other composer on the site uses. */
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.5rem;
}
/* Groups emoji + GIF pickers as one unit at the left of .profile-wall-form-
   actions, so justify-content: space-between on the row above still only
   sees two things to spread apart (this group, and the submit button) —
   without it, a 3rd top-level flex child would land the GIF button
   somewhere in the middle of the row instead of snug next to the emoji one. */
.profile-wall-form-pickers { display: flex; align-items: center; gap: 0.4rem; }

.profile-wall-flash {
    margin-bottom: 0.85rem;
    padding: 0.5rem 0.75rem;
    border-radius: 4px;
    font-size: 0.82rem;
    border: 1px solid rgba(0, 245, 255, 0.3);
    background: rgba(0, 245, 255, 0.08);
    color: var(--cy-cyan);
}

.profile-wall-flash.is-error {
    border-color: rgba(255, 70, 90, 0.4);
    background: rgba(255, 70, 90, 0.1);
    color: #ff8d9c;
}

.profile-wall-post {
    padding: 0.75rem 0;
    border-top: 1px solid rgba(255, 255, 255, 0.07);
}

.profile-wall-post:first-child {
    border-top: 0;
}

.profile-wall-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-bottom: 0.3rem;
}

.profile-wall-author {
    font-weight: 600;
    font-size: 0.88rem;
}

.profile-wall-time {
    color: var(--cy-text-dim);
    font-size: 0.75rem;
}

/* Pushed to the far end so it never sits next to the name, where it would be
   easy to hit while aiming for the profile link. */
.profile-wall-delete {
    margin-left: auto;
}

.profile-wall-delete button {
    background: none;
    border: 0;
    padding: 0.15rem 0.3rem;
    cursor: pointer;
    color: var(--cy-text-dim);
    font-size: 0.75rem;
    transition: color 0.2s ease;
}

.profile-wall-delete button:hover {
    color: #ff6b7d;
}

.profile-wall-body {
    font-size: 0.9rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
}

/* The rail is what makes replies read as belonging to the post above rather
   than as more wall posts. */
.profile-wall-replies {
    margin: 0.6rem 0 0 0.6rem;
    padding-left: 0.85rem;
    border-left: 1px solid rgba(0, 245, 255, 0.18);
}

.profile-wall-reply {
    margin-bottom: 0.55rem;
}

.profile-wall-reply:last-child {
    margin-bottom: 0;
}

.profile-wall-reply-toggle {
    margin-top: 0.5rem;
}

.profile-wall-reply-toggle > summary {
    cursor: pointer;
    display: inline-block;
    color: var(--cy-text-dim);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    transition: color 0.2s ease;
}

.profile-wall-reply-toggle > summary:hover {
    color: var(--cy-cyan);
}

.profile-wall-reply-toggle[open] > summary {
    margin-bottom: 0.45rem;
}

/* Decorative nav dots */
.nav-decoration {
    position: absolute;
    width: 6px; height: 6px;
    border-radius: 50%;
    background-color: var(--cy-cyan);
    box-shadow: 0 0 8px var(--cy-cyan), 0 0 16px rgba(0,245,255,0.4);
    opacity: 0;
    transition: opacity 0.5s ease;
}

.sticky-nav:hover .nav-decoration { opacity: 1; }

.nav-decoration-1 { top: 8px; left: 12px; }
.nav-decoration-2 { top: 8px; right: 12px; }

/* Content container */
.content-container {
    padding-top: 4rem;
    width: 100%;
}

/* ====================== UTILITY ====================== */
.rounded-md { border-radius: 0.375rem; }

.transition-colors {
    transition-property: color, background-color, border-color;
}
.duration-300 { transition-duration: 300ms; }

/* Focus states */
.focus\:border-gothic-highlight:focus  { border-color: var(--cy-cyan); }
.focus\:ring:focus {
    --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
    --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
    box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow,0 0 #0000);
}
.focus\:ring-gothic-highlight:focus { --tw-ring-color: rgba(0,245,255,0.4); }
.focus\:ring-opacity-25:focus { --tw-ring-opacity: 0.25; }

/* Hover helpers */
.hover\:bg-gothic-highlight:hover { background-color: var(--cy-cyan-dim); }
.hover\:text-gothic-text:hover    { color: var(--cy-text); }

/* ====================== PROSE ====================== */
.prose { color: var(--cy-text); font-family: var(--cy-font-body); }

.prose h1, .prose h2, .prose h3, .prose h4 {
    font-family: var(--cy-font-hud);
    color: var(--cy-cyan);
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.prose p { margin-bottom: 1.25em; line-height: 1.75; }

.prose a {
    color: var(--cy-cyan);
    text-decoration: none;
    border-bottom: 1px solid rgba(0,245,255,0.3);
    transition: color 0.3s ease, border-bottom-color 0.3s ease;
}
.prose a:hover {
    color: #ffffff;
    border-bottom-color: var(--cy-cyan);
    text-shadow: 0 0 6px var(--cy-cyan);
}

.prose ul { list-style-type: none; padding-left: 0; }
.prose ul li::before { content: '▸ '; color: var(--cy-cyan); }

/* Big first letter */
.prose p:first-of-type::first-letter {
    font-size: 2em;
    font-weight: 800;
    color: var(--cy-cyan);
    float: left;
    line-height: 0.85;
    margin-right: 0.1em;
    font-family: var(--cy-font-hud);
    text-shadow: 0 0 12px var(--cy-cyan);
}

/* ====================== ACTIVITY FEED ====================== */
.gothic-activity-item {
    position: relative;
    margin-bottom: 1.5rem;
    padding: 1.25rem 1.25rem 1.25rem 1.5rem;
    background-color: var(--cy-surface-1);
    border: 1px solid var(--cy-border);
    border-left: 2px solid rgba(0, 245, 255, 0.15);
    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
    transition: box-shadow 0.3s ease, border-color 0.3s ease;
    overflow: hidden;
}


.gothic-activity-item:hover { 
    border-color: rgba(0, 245, 255, 0.35);
    box-shadow: 0 6px 20px rgba(0,0,0,0.5), 0 0 15px rgba(0,245,255,0.06);
}


.gothic-activity-time {
    color: var(--cy-text-dim);
    font-size: 0.78rem;
    margin-bottom: 0.5rem;
    font-family: var(--cy-font-hud);
    letter-spacing: 0.04em;
}

.gothic-activity-content { position: relative; }

.gothic-activity-user {
    font-weight: 700;
    color: var(--cy-cyan);
    font-family: var(--cy-font-hud);
    font-size: 0.85rem;
    letter-spacing: 0.05em;
    text-shadow: 0 0 6px rgba(0,245,255,0.4);
    text-decoration: none;
}

.gothic-activity-action { color: var(--cy-magenta); }

.gothic-activity-detail {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px solid rgba(0, 245, 255, 0.1);
    font-size: 0.95rem;
}

/* Inline label directly before the username, not a corner watermark.
   Fixed width so the usernames stay vertically aligned down the feed
   regardless of how wide each type's glyph is. */
.gothic-activity-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.15rem;
    margin-right: 0.4rem;
    opacity: 0.85;
    font-size: 0.9rem;
    line-height: 1;
    transition: opacity 0.3s ease, text-shadow 0.3s ease;
    color: var(--cy-cyan);
}

/* No scale() here — this sits in the text flow now, and scaling would
   nudge the username sideways on hover. */
.gothic-activity-item:hover .gothic-activity-icon {
    opacity: 1;
    text-shadow: 0 0 8px currentColor;
}

/* Per-type icon colors */
.gothic-activity-registration .gothic-activity-icon { color: var(--cy-green); }
.gothic-activity-post         .gothic-activity-icon { color: var(--cy-cyan); }
.gothic-activity-comment      .gothic-activity-icon { color: var(--cy-yellow); }
.gothic-activity-image        .gothic-activity-icon { color: var(--cy-magenta); }
.gothic-activity-music        .gothic-activity-icon { color: #a78bfa; }

/* ====================== FEED HEADER ====================== */
.gothic-feed-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 2rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--cy-border);
}

.gothic-feed-title {
    font-family: var(--cy-font-hud);
    font-size: 1.15rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--cy-cyan);
    text-shadow: 0 0 10px rgba(0,245,255,0.4);
    position: relative;
}

.gothic-feed-title::after {
    content: '';
    position: absolute;
    bottom: -0.6rem; left: 0;
    width: 60%;
    height: 1px;
    background: linear-gradient(to right, var(--cy-cyan), transparent);
    box-shadow: 0 0 6px var(--cy-cyan);
}

.gothic-feed-filter { display: flex; gap: 0.4rem; flex-wrap: wrap; }

.gothic-feed-filter-btn {
    background-color: transparent;
    color: var(--cy-text-dim);
    border: 1px solid var(--cy-border);
    padding: 0.2rem 0.65rem;
    font-size: 0.75rem;
    font-family: var(--cy-font-hud);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
    cursor: pointer;
    clip-path: polygon(0 0, calc(100% - 5px) 0, 100% 5px, 100% 100%, 5px 100%, 0 calc(100% - 5px));
}

.gothic-feed-filter-btn:hover,
.gothic-feed-filter-btn.active {
    background-color: var(--cy-cyan-dim);
    color: var(--cy-cyan);
    border-color: var(--cy-cyan);
    text-shadow: 0 0 6px rgba(0,245,255,0.4);
    box-shadow: 0 0 8px rgba(0,245,255,0.15);
}

/* Empty feed */
.gothic-feed-empty {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    border: 1px dashed var(--cy-border);
    margin: 2rem 0;
}

.gothic-feed-empty-icon {
    font-size: 2rem;
    margin-bottom: 1rem;
    opacity: 0.3;
    color: var(--cy-cyan);
    display: block;
}


/* ====================== NOTIFICATIONS ====================== */
.gothic-notifications-dropdown { position: relative; }

/* cursor: pointer because two of these are <button> elements (the bell, logout)
   and browsers default buttons to the arrow cursor — without this they feel
   dead next to the anchors beside them. */
.gothic-notifications-btn { transition: transform 0.3s ease, color 0.3s ease; cursor: pointer; }

.gothic-notifications-btn:hover {
    transform: scale(1.12);
    color: var(--cy-cyan);
    text-shadow: 0 0 8px rgba(0,245,255,0.5);
}

.gothic-notification-badge {
    color: #ffffff !important;
}

.gothic-notifications-menu {
    background-color: var(--cy-surface-1);
    border: 1px solid rgba(0, 245, 255, 0.25);
    box-shadow: 0 12px 30px rgba(0,0,0,0.7), 0 0 20px rgba(0,245,255,0.06);
    transform-origin: top right;
    transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    width: 350px !important;
}

.gothic-notifications-menu:not(.hidden) {
    animation: menu-appear 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes menu-appear {
    0%   { opacity: 0; transform: scale(0.95) translateY(-6px); }
    100% { opacity: 1; transform: scale(1)    translateY(0); }
}

.gothic-notifications-menu .p-3.border-b.border-gothic-dark {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.gothic-notifications-menu h3 {
    font-family: var(--cy-font-hud);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    font-size: 0.85rem;
    color: var(--cy-cyan);
    margin: 0;
    text-shadow: 0 0 6px rgba(0,245,255,0.4);
}

.gothic-notifications-menu a.gothic-link {
    font-family: var(--cy-font-hud);
    letter-spacing: 0.06em;
    white-space: nowrap;
    margin-left: 1rem;
    font-size: 0.75rem;
    text-transform: uppercase;
}

/* Duplicate-empty fixes */
#notifications-empty           { display: none; }
#notifications-empty.show      { display: block; }
#notifications-content .gothic-notification-empty { display: none !important; }
#notifications-content:empty + #notifications-empty { display: block !important; }

/* Scrollbar */
.gothic-notifications-list {
    scrollbar-width: thin;
    scrollbar-color: var(--cy-cyan) var(--cy-surface-1);
}
.gothic-notifications-list::-webkit-scrollbar       { width: 4px; }
.gothic-notifications-list::-webkit-scrollbar-track  { background: var(--cy-surface-1); }
.gothic-notifications-list::-webkit-scrollbar-thumb  {
    background-color: var(--cy-cyan);
    border-radius: 4px;
    box-shadow: 0 0 4px var(--cy-cyan);
}

/* Individual notification */
.gothic-notification-item {
    position: relative;
    transition: background-color 0.3s ease;
    overflow: hidden;
    border-bottom: 1px solid var(--cy-border);
}

.gothic-notification-item:hover { background-color: var(--cy-cyan-dim); }

.gothic-notification-item::after {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 2px;
    background-color: transparent;
    transition: background-color 0.3s ease, box-shadow 0.3s ease;
}

.gothic-notification-item:hover::after {
    background-color: var(--cy-cyan);
    box-shadow: 0 0 8px var(--cy-cyan);
}

.gothic-notification-unread { background-color: rgba(0, 245, 255, 0.05); }

/* The cyan unread dot that used to sit here (.gothic-notification-unread::before)
   is gone — replaced by the "UNVIEWED" / "VIEWED" status pill next to each
   timestamp. The subtle background tint above still marks an unread row. */

/* Avatar */
.gothic-avatar { position: relative; }
.gothic-avatar::after {
    content: '';
    position: absolute;
    top: -2px; left: -2px; right: -2px; bottom: -2px;
    border: 1px solid rgba(0,245,255,0.3);
    border-radius: 50%;
    opacity: 0;
    transition: opacity 0.3s ease, transform 0.3s ease;
}
.gothic-notification-item:hover .gothic-avatar::after {
    opacity: 1;
    transform: scale(1.1);
    box-shadow: 0 0 8px rgba(0,245,255,0.4);
}

/* Empty / loading states */
.gothic-notification-empty,
.gothic-notification-loading {
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.8rem;
    letter-spacing: 0.06em;
    text-align: center;
    padding: 1.5rem 1rem;
}

.gothic-notification-loading i {
    color: var(--cy-cyan);
    text-shadow: 0 0 6px var(--cy-cyan);
    animation: spin 1.5s linear infinite;
}

/* Mark-all button */
.gothic-mark-all-btn {
    width: 100%;
    background-color: transparent;
    color: var(--cy-text-dim);
    border: 1px solid var(--cy-border);
    padding: 0.35rem 0.75rem;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
    cursor: pointer;
}
.gothic-mark-all-btn:hover {
    color: var(--cy-cyan);
    border-color: var(--cy-cyan);
    background-color: var(--cy-cyan-dim);
    text-shadow: 0 0 6px rgba(0,245,255,0.4);
}

/* Clear-all button — matches the mark-all button's footprint so the two sit
   level, but red on hover to signal that it destroys rather than dismisses. */
.gothic-clear-all-btn {
    width: 100%;
    background-color: transparent;
    color: var(--cy-text-dim);
    border: 1px solid var(--cy-border);
    padding: 0.35rem 0.75rem;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
    cursor: pointer;
}
.gothic-clear-all-btn:hover {
    color: var(--cy-error);
    border-color: var(--cy-error);
    background-color: rgba(255, 58, 58, 0.15);
    text-shadow: 0 0 6px rgba(255, 58, 58, 0.4);
}

/* ====================== PAGINATION ====================== */
nav[aria-label="pagination"] span,
nav[aria-label="pagination"] a {
    background-color: var(--cy-surface-2) !important;
    border-color: var(--cy-border) !important;
    color: var(--cy-text-dim) !important;
    font-family: var(--cy-font-hud);
    font-size: 0.78rem;
    letter-spacing: 0.05em;
}

nav[aria-label="pagination"] a:hover {
    background-color: var(--cy-cyan-dim) !important;
    border-color: var(--cy-cyan) !important;
    color: var(--cy-cyan) !important;
}

nav[aria-label="pagination"] span[aria-current="page"] span {
    background-color: var(--cy-cyan-dim) !important;
    border-color: var(--cy-cyan) !important;
    color: var(--cy-cyan) !important;
    box-shadow: 0 0 8px rgba(0,245,255,0.2);
}

/* ====================== ALERTS ====================== */
.gothic-alert {
    padding: 0.75rem 1rem;
    border: 1px solid var(--cy-border);
    font-family: var(--cy-font-hud);
    font-size: 0.82rem;
    letter-spacing: 0.04em;
}

.gothic-alert-success {
    border-color: var(--cy-green);
    background-color: var(--cy-green-dim);
    color: var(--cy-green);
    text-shadow: 0 0 6px rgba(57,255,20,0.4);
}

.gothic-alert-error {
    border-color: var(--cy-error);
    background-color: rgba(255,58,58,0.1);
    color: var(--cy-error);
}

/* ====================== KEYFRAMES ====================== */
@keyframes pulse {
    0%   { transform: scale(1);   opacity: 0.4; }
    100% { transform: scale(1.6); opacity: 0.9; }
}

@keyframes spin {
    0%   { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

@keyframes data-stream {
    0%   { transform: translateY(-100%); opacity: 0; }
    10%  { opacity: 0.6; }
    90%  { opacity: 0.6; }
    100% { transform: translateY(100vh); opacity: 0; }
}

/* ====================== RESPONSIVE ====================== */
@media (max-width: 640px) {
    .prose h1 { font-size: 1.4rem; }
    .prose h2 { font-size: 1.2rem; }
    .gothic-title { font-size: 0.9em; }
}

/* ====================== MISC HELPERS ====================== */
/* Scrollbar global */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--cy-border) var(--cy-surface-1);
}
*::-webkit-scrollbar       { width: 6px; height: 6px; }
*::-webkit-scrollbar-track  { background: var(--cy-surface-1); }
*::-webkit-scrollbar-thumb  { background-color: var(--cy-border); border-radius: 4px; }
*::-webkit-scrollbar-thumb:hover { background-color: var(--cy-cyan); }

/* Selection */
::selection {
    background-color: rgba(0, 245, 255, 0.25);
    color: #ffffff;
}

/* HUD corners for sections */
.cy-hud-box {
    position: relative;
    border: 1px solid var(--cy-border);
    padding: 1.5rem;
}
.cy-hud-box::before {
    content: '';
    position: absolute;
    top: -1px; left: -1px;
    width: 14px; height: 14px;
    border-top: 2px solid var(--cy-cyan);
    border-left: 2px solid var(--cy-cyan);
    box-shadow: -2px -2px 6px rgba(0,245,255,0.3);
}
.cy-hud-box::after {
    content: '';
    position: absolute;
    bottom: -1px; right: -1px;
    width: 14px; height: 14px;
    border-bottom: 2px solid var(--cy-magenta);
    border-right: 2px solid var(--cy-magenta);
    box-shadow: 2px 2px 6px rgba(255,45,120,0.3);
}

/* Feed post textarea override */
#feed-content {
    background-color: var(--cy-surface-2) !important;
    border: 1px solid var(--cy-border) !important;
    color: var(--cy-text) !important;
    font-family: var(--cy-font-body) !important;
    resize: vertical;
    transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
#feed-content:focus {
    border-color: var(--cy-cyan) !important;
    box-shadow: 0 0 12px rgba(0,245,255,0.12) !important;
}

/* Image gallery grid hover */
.gallery-item:hover img {
    filter: brightness(1.1) saturate(1.3);
    box-shadow: 0 0 20px rgba(0,245,255,0.25);
}

/* Profile avatar border */
.gothic-avatar-ring {
    border: 2px solid var(--cy-cyan);
    box-shadow: 0 0 12px rgba(0,245,255,0.4);
}

/* Status indicators */
.text-green-500 { color: var(--cy-green) !important; text-shadow: 0 0 6px rgba(57,255,20,0.4); }
.text-blue-500  { color: #60a5fa !important; }


/* ====================== ANIMATED EMOJI ======================
   Unicode glyphs animated with CSS — see App\Support\Emoji. Messages store the
   :shortcode:, so restyling here restyles every message ever sent, and a glyph
   that never animates is still perfectly readable. */

.ae {
    display: inline-block;
    font-style: normal;
    line-height: 1;
    /* Keeps a glyph from clipping mid-transform inside a tight line box. */
    will-change: transform, opacity, filter;
}

.ae-pulse   { animation: ae-pulse   1.8s ease-in-out infinite; }
.ae-float   { animation: ae-float   3s   ease-in-out infinite; }
.ae-flit    { animation: ae-flit    2.4s ease-in-out infinite; }
.ae-crawl   { animation: ae-crawl   3.5s linear         infinite; }
.ae-sway    { animation: ae-sway    3s   ease-in-out infinite; }
.ae-flicker { animation: ae-flicker 2.2s steps(2, end) infinite; }
.ae-glow    { animation: ae-glow    2.6s ease-in-out infinite; }
.ae-beat    { animation: ae-beat    1.2s ease-in-out infinite; }
.ae-twinkle { animation: ae-twinkle 1.6s ease-in-out infinite; }
.ae-spin    { animation: ae-spin    3s   linear      infinite; }
.ae-glitch  { animation: ae-glitch  2s   steps(3, end) infinite; }
.ae-shake   { animation: ae-shake   1.6s ease-in-out infinite; }
.ae-bounce  { animation: ae-bounce  1.6s ease-in-out infinite; }
.ae-tilt    { animation: ae-tilt    2.4s ease-in-out infinite; }
.ae-nod     { animation: ae-nod     2.8s ease-in-out infinite; }
.ae-pop     { animation: ae-pop     1.4s ease-in-out infinite; }
.ae-drift   { animation: ae-drift   4s   ease-in-out infinite; }
.ae-drip    { animation: ae-drip    3.2s ease-in     infinite; }

/*
   THE GLYPH ON A PICKER'S OWN BUTTON, SIZED HERE RATHER THAN INHERITED.

   The three composers that open a picker carry three different text sizes —
   the forum toolbar's 0.8rem is set for lettered tool labels like B and I, the
   comment toggle is 0.95rem, the PM button 1rem — so the same emoji came out a
   different size in each one, and smallest exactly where the toolbar is
   densest. This decouples it from whatever the button's label size happens to
   be, which is the only reason the sizes ever diverged.

   The nudge down the line box is because an emoji sits high in its em square
   next to Latin text.
*/
.ae-toggle {
    font-size: 1.05rem;
    vertical-align: -0.1em;
}

@keyframes ae-pulse   { 0%,100% { transform: scale(1);      } 50% { transform: scale(1.22); } }
@keyframes ae-float   { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@keyframes ae-flit    { 0%,100% { transform: translate(0,0) rotate(0); }
                        25%     { transform: translate(3px,-3px) rotate(8deg); }
                        75%     { transform: translate(-3px,-2px) rotate(-8deg); } }
@keyframes ae-crawl   { 0%,100% { transform: translateX(0)    rotate(0); }
                        25%     { transform: translateX(3px)  rotate(6deg); }
                        50%     { transform: translateX(0)    rotate(0); }
                        75%     { transform: translateX(-3px) rotate(-6deg); } }
@keyframes ae-sway    { 0%,100% { transform: rotate(-7deg); } 50% { transform: rotate(7deg); } }
@keyframes ae-flicker { 0%,100% { opacity: 1; filter: brightness(1.15); }
                        45%     { opacity: .55; filter: brightness(.8); }
                        60%     { opacity: 1; filter: brightness(1.3); } }
@keyframes ae-glow    { 0%,100% { filter: drop-shadow(0 0 1px rgba(0,245,255,.5)); }
                        50%     { filter: drop-shadow(0 0 7px rgba(0,245,255,.95)); } }
@keyframes ae-beat    { 0%,100% { transform: scale(1); } 15% { transform: scale(1.3); } 30% { transform: scale(1); } }
@keyframes ae-twinkle { 0%,100% { transform: scale(1)   rotate(0);     opacity: 1; }
                        50%     { transform: scale(.72) rotate(22deg); opacity: .6; } }
@keyframes ae-spin    { from    { transform: rotate(0); } to { transform: rotate(360deg); } }
@keyframes ae-glitch  { 0%,100% { transform: translate(0,0);   filter: hue-rotate(0); }
                        33%     { transform: translate(-2px,1px); filter: hue-rotate(70deg); }
                        66%     { transform: translate(2px,-1px); filter: hue-rotate(-70deg); } }
@keyframes ae-shake   { 0%,100% { transform: translateX(0); }
                        20%     { transform: translateX(-3px); }
                        40%     { transform: translateX(3px); }
                        60%     { transform: translateX(-2px); }
                        80%     { transform: translateX(2px); } }
@keyframes ae-bounce  { 0%,100% { transform: translateY(0)    scale(1); }
                        40%     { transform: translateY(-5px) scale(1.06); } }
@keyframes ae-tilt    { 0%,100% { transform: rotate(0); } 30% { transform: rotate(-16deg); } 60% { transform: rotate(10deg); } }

/* A friendly nod rather than a tilt — smaller angle, slower, and it returns
   through centre instead of swinging past it. Next to :smirk: and :wink:, which
   both use tilt, this has to read as warm rather than sly. */
@keyframes ae-nod     { 0%,100% { transform: rotate(0) translateY(0); }
                        30%     { transform: rotate(-7deg) translateY(1px); }
                        65%     { transform: rotate(6deg)  translateY(0); } }

/* The big smile gets bigger. A scale-only pulse, deeper than ae-pulse and
   quicker, so it reads as a grin breaking out rather than a slow throb. */
@keyframes ae-pop     { 0%,100% { transform: scale(1); }
                        35%     { transform: scale(1.28); }
                        60%     { transform: scale(0.98); } }

/* A drop gathering, falling, and gone. ease-in rather than ease-in-out so it
   accelerates on the way down instead of easing to a polite stop — gravity is
   the whole gag. Holds still for the first third so it reads as gathering. */
@keyframes ae-drip    { 0%,30%  { transform: translateY(-1px) scaleY(1);    opacity: 1; }
                        70%     { transform: translateY(4px)  scaleY(1.15); opacity: 0.9; }
                        100%    { transform: translateY(7px)  scaleY(0.9);  opacity: 0; } }

/* Smoke: a slow rise that fades as it goes and resets from the bottom. Longest
   cycle in the set on purpose — anything brisk looks like panic, not a joint. */
@keyframes ae-drift   { 0%      { transform: translate(0, 2px)     rotate(0);      opacity: 0.75; }
                        45%     { transform: translate(2px, -3px)  rotate(6deg);   opacity: 1; }
                        100%    { transform: translate(-1px, -6px) rotate(-4deg);  opacity: 0.55; } }

/* Anyone who has asked the OS for less motion gets the glyph, standing still. */
@media (prefers-reduced-motion: reduce) {
    .ae, .ae[class*="ae-"] { animation: none !important; }
}

/* ====================== PRIVATE MESSAGES ======================
   Built as named classes rather than Tailwind utilities: the compiled Tailwind
   bundle is prebuilt and cannot be rebuilt on this server, so any utility not
   already in it renders as nothing. These are safe to add and to change. */

/* ---- Inbox list ---- */
.pm-thread {
    display: flex;
    align-items: flex-start;
    gap: 0.9rem;
    padding: 0.9rem 1rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    text-decoration: none;
    transition: border-color 0.25s ease, background-color 0.25s ease, transform 0.25s ease;
}
.pm-thread:hover {
    border-color: var(--cy-cyan);
    background-color: var(--cy-surface-2);
    transform: translateX(2px);
}
.pm-thread + .pm-thread { margin-top: 0.5rem; }

/* An unread thread gets the cyan edge, so the inbox scans at a glance. */
.pm-thread-unread {
    border-left: 3px solid var(--cy-cyan);
    background-color: var(--cy-surface-2);
}

.pm-thread-avatar {
    width: 44px; height: 44px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--cy-border);
    flex-shrink: 0;
}
.pm-thread-avatar-fallback {
    width: 44px; height: 44px;
    border-radius: 50%;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-bg);
    display: flex; align-items: center; justify-content: center;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    flex-shrink: 0;
}
.pm-thread-body { flex: 1; min-width: 0; }
.pm-thread-name {
    font-family: var(--cy-font-hud);
    letter-spacing: 0.05em;
    color: var(--cy-text);
    font-size: 0.9rem;
}
.pm-thread-preview {
    color: var(--cy-text-dim);
    font-size: 0.82rem;
    margin-top: 0.15rem;
    /* One line only — a long message must not stretch the row. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.pm-thread-time {
    color: var(--cy-text-dim);
    font-size: 0.75rem;
    font-family: var(--cy-font-hud);
    flex-shrink: 0;
    white-space: nowrap;
}
.pm-thread-count {
    display: inline-block;
    min-width: 1.25rem;
    padding: 0.05rem 0.35rem;
    border-radius: 999px;
    background: linear-gradient(135deg, #003344 0%, #006688 100%);
    color: #ffffff;
    font-size: 0.75rem;
    text-align: center;
    font-family: var(--cy-font-hud);
}

/* ---- Thread view ---- */
.pm-log {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
    padding: 1rem;
    max-height: 60vh;
    overflow-y: auto;
    background-color: var(--cy-bg);
    border: 1px solid var(--cy-border);
}

.pm-msg { display: flex; flex-direction: column; max-width: 78%; }
/* Sent messages sit right, received left — the only cue needed for who spoke. */
.pm-msg-out { align-self: flex-end;   align-items: flex-end; }
.pm-msg-in  { align-self: flex-start; align-items: flex-start; }

.pm-bubble {
    padding: 0.55rem 0.85rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-2);
    color: var(--cy-text);
    font-size: 0.9rem;
    line-height: 1.5;
    /* Long unbroken strings (URLs) must wrap, not widen the column. */
    overflow-wrap: anywhere;
    word-break: break-word;
}
.pm-msg-out .pm-bubble {
    border-color: rgba(0, 245, 255, 0.4);
    background-color: rgba(0, 245, 255, 0.07);
}
.pm-meta {
    margin-top: 0.2rem;
    font-size: 0.75rem;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    letter-spacing: 0.04em;
}

/* ---- Composer ---- */
.pm-composer {
    margin-top: 0.75rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    padding: 0.75rem;
}
.pm-composer textarea {
    width: 100%;
    min-height: 84px;
    resize: vertical;
    background-color: var(--cy-surface-2);
    border: 1px solid var(--cy-border);
    color: var(--cy-text);
    padding: 0.6rem 0.75rem;
    font-family: var(--cy-font-body);
    font-size: 0.9rem;
}
.pm-composer textarea:focus {
    outline: none;
    border-color: var(--cy-cyan);
    box-shadow: 0 0 10px rgba(0, 245, 255, 0.25);
}
.pm-composer-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: 0.6rem;
}
.pm-composer-left { display: flex; align-items: center; gap: 0.5rem; position: relative; }
.pm-charcount { font-size: 0.75rem; color: var(--cy-text-dim); font-family: var(--cy-font-hud); }
.pm-charcount-over { color: var(--cy-error); }

/*
   "Enter sends" hint. Hidden by default and revealed only when messages.js has
   added .pm-enter-sends to the form — that class IS the feature's on-switch, so
   the hint cannot end up promising a shortcut that is not active. On a phone,
   and with JavaScript off, neither the behaviour nor the hint exists.

   Drops out below 640px as well: the composer bar is tight on a narrow screen,
   and the hint is the least important thing in it.
*/
/*
   Sized and coloured to the readability baseline, not below it. The first cut
   was 0.7rem (11.2px) with opacity 0.75 layered over --cy-text-dim — under the
   12px floor AND dimmed twice, which is the exact combination that pass was
   written to stamp out. No opacity here: if something should be quieter, say so
   with a colour that was chosen, not by fading one that was not.
*/
.pm-enter-hint {
    display: none;
    font-size: 0.8rem;
    color: var(--cy-text-soft);
    white-space: nowrap;
}
/* The key names carry the emphasis so the sentence around them can stay calm.
   <kbd> because that is what they are; the font is inherited so it reads as
   prose rather than dropping into monospace mid-line. */
.pm-enter-hint kbd {
    font-family: inherit;
    font-size: inherit;
    font-weight: 600;
    color: var(--cy-cyan);
}
.pm-composer.pm-enter-sends .pm-enter-hint { display: inline; }

@media (max-width: 640px) {
    .pm-composer.pm-enter-sends .pm-enter-hint { display: none; }
}

/* ---- Emoji picker ---- */
.pm-emoji-btn {
    background: transparent;
    border: 1px solid var(--cy-border);
    color: var(--cy-text-dim);
    padding: 0.3rem 0.6rem;
    cursor: pointer;
    font-size: 1rem;
    line-height: 1.2;
    transition: border-color 0.25s ease, color 0.25s ease;
}
.pm-emoji-btn:hover { border-color: var(--cy-cyan); color: var(--cy-cyan); }

/* Overrides the plain .pm-emoji-btn it also carries — same violet accent as
   .cm-gif-toggle and .fm-gif-toggle, and the same reasoning: see the comment
   on .cm-gif-toggle for why violet specifically. */
.pm-gif-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--cy-violet) !important;
    border-color: rgba(167, 139, 250, 0.4) !important;
    background-color: rgba(167, 139, 250, 0.08);
}
.pm-gif-btn:hover {
    border-color: var(--cy-violet) !important;
    background-color: rgba(167, 139, 250, 0.16) !important;
}
.pm-gif-label { font-weight: 700; font-size: 0.68rem; letter-spacing: 0.03em; }

/*
   HEIGHT: the catalogue must FIT, with the scroll left as a safety valve.

   max-height was 280px, and the catalogue's natural height is — to the pixel —
   280px: three groups of twelve, six emoji to a row, so six grid rows plus
   three labels, two group gaps, the padding and the border. Landing exactly on
   the cap meant the box was permanently a rounding error away from scrolling,
   and showed a sliver of scrollbar for the last pixel or two of nothing.

   Raised to 24rem (384px), which the current catalogue clears with about three
   rows to spare, so the panel now sizes to its contents and stops. overflow-y
   stays: a catalogue big enough to pass 384px should scroll rather than run off
   the screen, and 70vh caps it on a short window.

   [!] ALL THREE PICKERS SHARE THESE NUMBERS — .pm-emoji-menu (private
       messages), .cm-emoji-menu (comments) and .fm-emoji-menu (forum). They
       render the same catalogue through the same .pm-emoji-grid, so a height
       that fits one fits all three. Change them together.
*/
.pm-emoji-menu {
    position: absolute;
    bottom: calc(100% + 0.5rem);
    left: 0;
    z-index: 60;
    width: 296px;
    max-height: min(24rem, 70vh);
    overflow-y: auto;
    background-color: var(--cy-surface-2);
    border: 1px solid var(--cy-border);
    box-shadow: 0 10px 30px rgba(0,0,0,0.75), 0 0 18px rgba(0,245,255,0.12);
    padding: 0.6rem;
}
.pm-emoji-menu[hidden] { display: none; }

/* Same shell as .pm-emoji-menu above (absolute, opens upward — the PM
   composer sits pinned to the bottom of the viewport with nothing under it,
   so unlike the comment/forum pickers this one needs no reparent-to-<body>
   trick). Wider, same reason .cm-gif-menu is wider than .cm-emoji-menu. */
.pm-gif-menu {
    position: absolute;
    bottom: calc(100% + 0.5rem);
    left: 0;
    z-index: 60;
    width: 320px;
    max-height: min(24rem, 70vh);
    overflow-y: auto;
    background-color: var(--cy-surface-2);
    border: 1px solid var(--cy-border);
    box-shadow: 0 10px 30px rgba(0,0,0,0.75), 0 0 18px rgba(0,245,255,0.12);
    padding: 0.6rem;
}
.pm-gif-menu[hidden] { display: none; }

/* ---- Comment composer emoji picker ---- */
/*
   Reuses .pm-emoji-group / -grid / -pick for the contents, so all three pickers
   on the site look identical and there is one place to restyle a swatch. Only
   the shell differs, because a comment form is not a chat composer: the menu
   opens DOWNWARD here. Comment boxes usually sit near the top of a long thread
   with the page below them, whereas the PM composer is pinned to the bottom of
   the viewport with nothing underneath.
*/
.cm-emoji { position: relative; display: inline-block; }
.cm-emoji-toggle {
    background: transparent;
    border: 1px solid var(--cy-border);
    color: var(--cy-text-dim);
    cursor: pointer;
    padding: 0.25rem 0.5rem;
    line-height: 1;
    font-size: 0.95rem;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}
.cm-emoji-toggle:hover { border-color: var(--cy-cyan); background-color: rgba(0,245,255,0.07); }
.cm-emoji-toggle:focus-visible { outline: 1px solid var(--cy-cyan); outline-offset: 2px; }

/*
   POSITION: FIXED, AND THE MENU IS MOVED TO <body> WHEN IT OPENS.

   The first cut used position:absolute inside the form and was clipped to
   ribbons. Comment composers sit inside .gothic-card, whose cut-corner clip-path
   clips EVERY descendant no matter what z-index they carry, and several of these
   panels also scroll. No stacking value can escape a clip-path; the only way out
   is to not be a descendant, so emoji-picker.js reparents the menu to <body> on
   open and puts it back on close.

   Fixed rather than absolute because the coordinates then come straight from
   getBoundingClientRect(), which is already viewport-relative — the menu stays
   correct at any scroll position without arithmetic. The JS also flips it above
   the button when there is no room below, and clamps it inside the viewport.

   top/left are set by the JS. The values here only matter for the instant
   before it is positioned.
*/
.cm-emoji-menu {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 190;
    width: 296px;
    max-width: calc(100vw - 1.5rem);
    /* Same height reasoning as .pm-emoji-menu — see the note there. */
    max-height: min(24rem, 70vh);
    overflow-y: auto;
    background-color: var(--cy-surface-2);
    border: 1px solid var(--cy-border);
    box-shadow: 0 10px 30px rgba(0,0,0,0.75), 0 0 18px rgba(0,245,255,0.12);
    padding: 0.6rem;
}
.cm-emoji-menu[hidden] { display: none; }
.pm-emoji-group + .pm-emoji-group { margin-top: 0.6rem; }
.pm-emoji-group-label {
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
    margin-bottom: 0.35rem;
}
.pm-emoji-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 0.2rem;
}
.pm-emoji-pick {
    background: transparent;
    border: 1px solid transparent;
    cursor: pointer;
    font-size: 1.25rem;
    line-height: 1;
    padding: 0.3rem 0;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}
.pm-emoji-pick:hover {
    border-color: var(--cy-cyan);
    background-color: rgba(0, 245, 255, 0.1);
}

/* ---- GIF picker (comments/wall) ----
   Structurally identical to .cm-emoji above — same relative/inline-block
   wrapper, same fixed+reparented-to-<body> menu for the same clip-path
   reason (see the long comment on .cm-emoji-menu). The forum (.fm-gif-*)
   and private messages (.pm-gif-*) have their own outer shells further
   down/in this file for the same reason their emoji pickers do — each
   composer's toolbar has different chrome around it — but all three share
   the .gif-picker-search / -grid / -item / -status classes below for the
   actual search box and results grid, driven by GifPickerCore in
   gif-picker.js so that part of the styling and behaviour is written once. */
.cm-gif { position: relative; display: inline-block; }
/* Violet rather than the cyan every other toolbar control uses — deliberately.
   --cy-violet is documented elsewhere (see .acct-tab-sep) as the one hue in
   this palette that carries no state meaning, which is exactly what a GIF
   button needs: enough colour to read as its own distinct control next to the
   plain emoji/attach buttons, without borrowing a colour someone has already
   learned means pinned/locked/staff/error somewhere else on the site. */
.cm-gif-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    background: rgba(167, 139, 250, 0.08);
    border: 1px solid rgba(167, 139, 250, 0.4);
    color: var(--cy-violet);
    cursor: pointer;
    padding: 0.25rem 0.6rem;
    line-height: 1;
    font-size: 0.8rem;
    font-family: var(--cy-font-hud);
    letter-spacing: 0.05em;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}
.cm-gif-label { font-weight: 700; font-size: 0.72rem; }
.cm-gif-toggle:hover { border-color: var(--cy-violet); background-color: rgba(167,139,250,0.16); }
.cm-gif-toggle:focus-visible { outline: 1px solid var(--cy-violet); outline-offset: 2px; }

/* Same fixed/reparented positioning as .cm-emoji-menu — see the note there
   for why. Wider, because a grid of GIF thumbnails needs more room than a
   grid of emoji glyphs. */
.cm-gif-menu {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 190;
    width: 320px;
    max-width: calc(100vw - 1.5rem);
    max-height: min(24rem, 70vh);
    overflow-y: auto;
    background-color: var(--cy-surface-2);
    border: 1px solid var(--cy-border);
    box-shadow: 0 10px 30px rgba(0,0,0,0.75), 0 0 18px rgba(0,245,255,0.12);
    padding: 0.6rem;
}
.cm-gif-menu[hidden] { display: none; }

/* ---- GIF picker: shared search box / results grid ----
   Used inside .cm-gif-menu, .fm-gif-menu and .pm-gif-menu alike. */
.gif-picker-search {
    width: 100%;
    background-color: var(--cy-surface-3);
    border: 1px solid var(--cy-border);
    color: var(--cy-text);
    padding: 0.4rem 0.6rem;
    font-size: 0.85rem;
    margin-bottom: 0.5rem;
}
.gif-picker-search:focus { outline: none; border-color: var(--cy-cyan); }
.gif-picker-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.35rem;
    max-height: 16rem;
    overflow-y: auto;
}
.gif-picker-item {
    position: relative;
    padding: 0;
    border: 1px solid transparent;
    background-color: var(--cy-surface-3);
    cursor: pointer;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    transition: border-color 0.2s ease;
}
.gif-picker-item:hover { border-color: var(--cy-cyan); }
.gif-picker-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gif-picker-item.is-loading { opacity: 0.4; cursor: default; pointer-events: none; }
.gif-picker-item.is-loading::after {
    content: '';
    position: absolute;
    inset: 0;
    border: 2px solid rgba(0,245,255,0.25);
    border-top-color: var(--cy-cyan);
    border-radius: 50%;
    width: 1.1rem;
    height: 1.1rem;
    margin: auto;
    animation: gif-picker-spin 0.7s linear infinite;
}
@keyframes gif-picker-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .gif-picker-item.is-loading::after { animation: none; }
}
.gif-picker-status {
    margin: 0.4rem 0 0;
    font-size: 0.75rem;
    color: var(--cy-text-dim);
    min-height: 1em;
}

/* The rendered result: an inline GIF from a [gif=N] marker, in a comment,
   PM or wall post. Capped rather than shown at native size — Giphy's
   "fixed_width" rendition can still be a few hundred pixels across, wider
   than a chat-style aside should run. */
.gif-embed {
    display: block;
    max-width: 240px;
    max-height: 240px;
    width: auto;
    height: auto;
    margin-top: 0.4rem;
    border: 1px solid var(--cy-border);
}

/* ---- Notices ---- */
.pm-notice {
    padding: 0.7rem 0.9rem;
    border: 1px solid var(--cy-border);
    border-left: 3px solid var(--cy-error);
    background-color: rgba(255, 58, 58, 0.07);
    color: var(--cy-text-dim);
    font-size: 0.84rem;
}

/* ---- Live thread: new-message pill ----
   Anchors to the transcript, so it can sit over the bottom of the log without
   the log itself needing to change size when it appears. */
.pm-log-wrap { position: relative; }

.pm-new-pill {
    position: absolute;
    left: 50%;
    bottom: 0.75rem;
    transform: translateX(-50%);
    z-index: 5;
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--cy-cyan);
    background-color: rgba(9, 9, 30, 0.95);
    color: var(--cy-cyan);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 0 14px rgba(0, 245, 255, 0.35);
    transition: background-color 0.25s ease, box-shadow 0.25s ease;
}
.pm-new-pill:hover {
    background-color: rgba(0, 245, 255, 0.15);
    box-shadow: 0 0 20px rgba(0, 245, 255, 0.55);
}
.pm-new-pill[hidden] { display: none; }

/* Newly-arrived messages fade in, so a live append reads as an arrival rather
   than as content that was always there. */
.pm-log .pm-msg { animation: pm-msg-in 0.28s ease-out; }

@keyframes pm-msg-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .pm-log .pm-msg { animation: none; }
}

/* The composer's inline error, when a send is refused mid-conversation. */
#pm-composer-error { margin-bottom: 0.6rem; }

/* A form wrapping a nav icon (logout) must not introduce a block-level box, or
   it breaks the row's alignment and the gap spacing around it. */
.nav-icon-form {
    display: inline-flex;
    align-items: center;
    margin: 0;
}

/* ---- Top-right nav cluster ----
   The row is now twelve icons — library, dashboard, profile, feed, forums,
   galleries, music, members, friends, messages, notifications, log out — plus
   the account menu. Every one added pushes the width at which the bar starts to
   overrun, so the ceiling here rose from 820px to 1100px when galleries and
   music went in, to 1200px when friends did, and to 1300px when The Library
   did.

   IF YOU ADD ANOTHER ICON, RAISE THIS AGAIN. Roughly 100px per icon has held
   each time. The symptom of forgetting is not an error — the bar simply starts
   wrapping or colliding with the wordmark somewhere in the low 1200s, on
   exactly the laptop widths most people use.

   Handled here rather than with a `md:` Tailwind variant because the prebuilt
   bundle ships only six md: utilities and none of the layout ones — and there
   is no local npm to rebuild it. Above 1300px nothing below applies. */
@media (min-width: 640px) and (max-width: 1300px) {
    .nav-icon-cluster { gap: 0.2rem; }

    .nav-icon-cluster .gothic-notifications-btn { padding: 0.28rem; }

    .nav-icon-cluster .nav-link {
        padding-left: 0.45rem;
        padding-right: 0.45rem;
    }

    /* A long display name is the single biggest consumer of what is left. */
    .nav-icon-cluster .nav-link > div:first-child {
        max-width: 8rem;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

/* ====================== PRESENCE ======================
   Online / offline indicator. Green reads as "here" without needing a legend;
   offline is a hollow, muted dot rather than red, because being away is not an
   error state and a wall of red dots on the members list would read as alarm. */

.presence-dot {
    display: inline-block;
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 50%;
    flex-shrink: 0;
}

.presence-online {
    background-color: var(--cy-green);
    box-shadow: 0 0 6px rgba(57, 255, 20, 0.85);
    animation: presence-pulse 2.4s ease-in-out infinite;
}

.presence-offline {
    background-color: transparent;
    border: 1px solid var(--cy-text-dim);
    box-shadow: none;
}

/* Sits over the corner of an avatar. The ring matches the page background so
   the dot reads as floating above the image rather than painted onto it. */
.presence-dot-overlay {
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 0.7rem;
    height: 0.7rem;
    border: 2px solid var(--cy-surface-1);
    box-sizing: content-box;
}
.presence-dot-overlay.presence-offline {
    background-color: var(--cy-surface-3);
    border-color: var(--cy-surface-1);
}

/* Any avatar that hosts an overlay dot needs to be a positioning context.
   inline-flex rather than inline-block: it collapses the descender gap under
   the image without setting line-height: 0, which would inherit into initial
   letters in the no-avatar fallback and squash them. flex-shrink guards the
   cases where this wrapper is itself a flex child (the PM rows). */
.avatar-with-presence {
    position: relative;
    display: inline-flex;
    flex-shrink: 0;
}

.presence-inline {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.presence-label {
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
}
.presence-inline .presence-online + .presence-label { color: var(--cy-green); }

@keyframes presence-pulse {
    0%, 100% { opacity: 1;   box-shadow: 0 0 6px  rgba(57, 255, 20, 0.85); }
    50%      { opacity: .72; box-shadow: 0 0 11px rgba(57, 255, 20, 1); }
}

@media (prefers-reduced-motion: reduce) {
    .presence-online { animation: none; }
}

/* ====================== FORUM ======================
   Named classes throughout rather than Tailwind utilities: the compiled bundle
   is prebuilt and cannot be regenerated on this server, so anything not already
   in it renders as nothing. These are safe to add and to change. */

.fm-crumbs {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--cy-text-dim);
    margin-bottom: 1.25rem;
}
.fm-crumb-sep { color: var(--cy-border-bright); }
/* The last crumb is where you already are, so it is not a link and must not
   look like one. */
.fm-crumb-here { color: var(--cy-text); }

/* Thread page only: pins the trail under the fixed nav so it stays reachable
   while scrolling a long thread, instead of the old (now-removed) repeat of
   it at the foot of the page.

   `top` matches the fixed nav's height, same reasoning as .gallery-filter-bar
   above — .sticky-nav is the h-16 (64px) bar. z-index 20 clears ordinary page
   content but stays well under .sticky-nav's 1000, so the two never fight
   when both are pinned at once. */
.fm-crumbs-sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 64px;
    z-index: 20;
    background: rgba(5, 5, 18, 0.97);
    padding: 0.6rem 0;
    margin-bottom: 1.25rem;
    border-bottom: 1px solid rgba(0, 245, 255, 0.18);
}
.fm-crumbs-sticky .fm-crumbs { margin-bottom: 0; }

.fm-tag {
    display: inline-block;
    margin-left: 0.4rem;
    padding: 0.05rem 0.4rem;
    border: 1px solid var(--cy-border);
    border-radius: 3px;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--cy-text-dim);
    vertical-align: middle;
}
.fm-tag-staff { color: var(--cy-magenta); border-color: rgba(255,45,120,0.45); }

/* ---- Index: categories and forums ---- */
.fm-category { margin-bottom: 1.75rem; }

/* Plain block again. This briefly became a flex row to hold per-category New
   Posts / New Thread buttons; both are now single controls in the toolbar above
   the board, so the heading has nothing to lay out beside the title. */
.fm-category-head {
    padding: 0.65rem 1rem;
    background: linear-gradient(90deg, rgba(0,245,255,0.09), transparent);
    border-left: 3px solid var(--cy-cyan);
}

/* ---- "New Thread" where the category has more than one forum to post in ----
   A <details> menu rather than a scripted dropdown: it opens with no
   JavaScript, closes on its own summary, and needs no focus management. */
.fm-newmenu { position: relative; }
.fm-newmenu > summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex; align-items: center;
}
.fm-newmenu > summary::-webkit-details-marker { display: none; }
.fm-newmenu-list {
    position: absolute; top: calc(100% + 0.4rem); right: 0; z-index: 60;
    /* Bounded and scrollable rather than however tall the category happens to
       make it: a section with a dozen forums in it would otherwise be taller
       than the window in either direction, and flipping it would not help. */
    max-height: min(20rem, 60vh);
    overflow-y: auto;
    min-width: 13rem;
    padding: 0.4rem;
    background: var(--cy-surface-2);
    border: 1px solid rgba(0, 245, 255, 0.35);
    border-radius: 6px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.75);
}
.fm-newmenu-label {
    margin: 0.1rem 0.4rem 0.35rem;
    font-family: var(--cy-font-hud);
    font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--cy-text-dim);
}
/* Category headings inside the menu. Now that one control covers the whole
   board, the list has to carry the structure that used to come from having a
   button per section — cyan, matching the category headings on the page. */
.fm-newmenu-group {
    margin: 0.55rem 0.4rem 0.25rem;
    padding-top: 0.45rem;
    border-top: 1px solid var(--cy-border);
    font-family: var(--cy-font-hud);
    font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase;
    color: var(--cy-cyan);
}
/* The first group sits directly under "Post in…", which is already a divider.

   Matched by ADJACENCY, not :first-of-type — the label is a <p> too, so it owns
   that slot and .fm-newmenu-group:first-of-type would never match anything. */
.fm-newmenu-label + .fm-newmenu-group {
    border-top: none;
    padding-top: 0;
    margin-top: 0.1rem;
}
.fm-newmenu-item {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.4rem 0.5rem;
    border-radius: 4px;
    font-size: 0.82rem;
    color: var(--cy-text);
    text-decoration: none;
}
.fm-newmenu-item:hover { background: rgba(0, 245, 255, 0.1); color: var(--cy-cyan); }
.fm-newmenu-item i { color: var(--cy-cyan); opacity: 0.7; width: 1rem; text-align: center; }

/* Opened UPWARD, when there is no room below.

   The class is set by the small script in forum/index.blade.php — CSS cannot
   ask where in the viewport an element is, so the decision has to be measured.
   Without JavaScript the menu still opens, downward, which is what it did
   before this existed. */
.fm-newmenu.is-above .fm-newmenu-list {
    top: auto;
    bottom: calc(100% + 0.4rem);
}
.fm-category-title {
    font-family: var(--cy-font-hud);
    font-size: 0.85rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--cy-cyan);
    margin: 0;
}
.fm-category-desc { font-size: 0.75rem; color: var(--cy-text-dim); margin: 0.2rem 0 0; }

.fm-forum {
    display: grid;
    grid-template-columns: 2.5rem 1fr auto auto;
    gap: 1rem;
    align-items: center;
    padding: 0.9rem 1rem;
    border: 1px solid var(--cy-border);
    border-top: none;
    background-color: var(--cy-surface-1);
    text-decoration: none;
    transition: background-color 0.25s ease, border-color 0.25s ease;
}
.fm-forum:hover { background-color: var(--cy-surface-2); border-color: rgba(0,245,255,0.35); }

/*
   ---- The row is no longer one big link ----

   .fm-forum used to be an <a> wrapping everything in it, which made the forum
   name, the counts, the last thread's title and the last poster a single
   destination. It is a <div> now holding three real targets: the forum name
   (its thread listing), the last thread's title (the thread, or its newest post
   when you are watching it) and the last poster (their profile, with a hover
   card).

   The row still lights up on hover, because it is still the unit being scanned —
   it simply is not the thing you click. Same arrangement as .fm-thread.
*/
.fm-forum-link {
    color: inherit;
    text-decoration: none;
}
.fm-forum-link:hover { color: var(--cy-cyan); text-decoration: underline; }

/* The last thread's title is a link now, so it needs the block behaviour the
   span it replaced had — otherwise it sits inline and the meta line rides up
   beside it. */
.fm-forum-last-title { display: block; }
.fm-forum-icon { color: var(--cy-cyan); font-size: 1.2rem; text-align: center; opacity: 0.75; }
.fm-forum-body { min-width: 0; display: flex; flex-direction: column; }
.fm-forum-title {
    font-family: var(--cy-font-hud);
    font-size: 0.9rem; letter-spacing: 0.04em;
    color: var(--cy-text);
}
.fm-forum-desc { font-size: 0.78rem; color: var(--cy-text-dim); margin-top: 0.2rem; }
.fm-forum-stats, .fm-thread-stats {
    display: flex; flex-direction: column; gap: 0.1rem;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem; letter-spacing: 0.05em;
    color: var(--cy-text-dim);
    text-align: right;
    white-space: nowrap;
}
.fm-forum-last, .fm-thread-last {
    display: flex; flex-direction: column; gap: 0.1rem;
    min-width: 11rem; text-align: right;
}
.fm-forum-last-title { font-size: 0.76rem; color: var(--cy-text); }
.fm-forum-last-meta, .fm-thread-last-meta {
    font-size: 0.75rem; color: var(--cy-text-dim); font-family: var(--cy-font-hud);
}

/* ---- Thread listing ---- */
.fm-thread {
    display: grid;
    grid-template-columns: 2rem 1fr auto auto;
    gap: 1rem;
    align-items: center;
    padding: 0.8rem 1rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    text-decoration: none;
    transition: background-color 0.25s ease, border-color 0.25s ease;
}
.fm-thread + .fm-thread { border-top: none; }
.fm-thread:hover { background-color: var(--cy-surface-2); border-color: rgba(0,245,255,0.35); }

/*
   ---- The row is no longer one big link ----

   .fm-thread used to be an <a>. That made the title, the author, the counts and
   all the space between them a single destination, so there was no way to reach
   a member's profile from a listing and no way to select a title's text. It is
   a <div> now, holding several real targets.

   The row still highlights on hover, because the row is still the unit you are
   scanning — it simply is not the thing you click.
*/
.fm-thread-link {
    color: inherit;
    text-decoration: none;
}
.fm-thread-link:hover { color: var(--cy-cyan); text-decoration: underline; }

/* Member names inside a listing. Deliberately quieter than a title link — they
   are secondary information in a meta line, not the thing the row is about. */
.fm-user-link {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px dotted rgba(0, 245, 255, 0.4);
}
.fm-user-link:hover { color: var(--cy-cyan); border-bottom-color: var(--cy-cyan); }

/* "Go to new" — the marker that says the title will jump you to where you left
   off rather than to the top. Cyan, matching every other unread signal. */
.fm-tag-new {
    border-color: rgba(0, 245, 255, 0.45);
    background-color: var(--cy-cyan-dim);
    color: var(--cy-cyan);
    text-decoration: none;
}
.fm-tag-new:hover { background-color: rgba(0, 245, 255, 0.25); }

/* Unread threads carry the cyan edge and a brighter title — the listing has to
   be scannable at a glance, which a subtle weight change alone is not. */
.fm-thread-unread { border-left: 3px solid var(--cy-cyan); background-color: var(--cy-surface-2); }
.fm-thread-unread .fm-thread-title { color: var(--cy-cyan); font-weight: 600; }

/*
   Unread markers on the forum INDEX.

   Same treatment a thread already gets inside a forum — cyan left rule, cyan
   title, filled dot — so the trail from the nav badge to the actual post reads
   as one continuous signal rather than three unrelated ones. The board badge
   said "1" and then pointed at a page that looked identical whether or not
   anything was new; this is what closes that gap.

   Cyan, not gold or magenta: this is the site's default "there is something
   here" tone, and unread is a state rather than a warning. See the forum colour
   semantics — gold is pinned, amber locked, magenta staff.
*/
.fm-forum-unread { border-left: 3px solid var(--cy-cyan); background-color: var(--cy-surface-2); }
.fm-forum-unread .fm-forum-title { color: var(--cy-cyan); font-weight: 600; }
.fm-forum-unread .fm-forum-icon { color: var(--cy-cyan); }

/* The count itself. Small, and it never renders at zero — the template only
   emits it when there is something to say. */
.fm-unread-pill {
    display: inline-block;
    margin-left: 0.4rem;
    padding: 0.05rem 0.4rem;
    border-radius: 999px;
    background-color: var(--cy-cyan-dim);
    border: 1px solid rgba(0, 245, 255, 0.45);
    color: var(--cy-cyan);
    font-family: var(--cy-font-hud);
    font-size: 0.7rem;
    line-height: 1.5;
    vertical-align: middle;
}

.fm-thread-icon { color: var(--cy-text-dim); text-align: center; }
.fm-thread-unread .fm-thread-icon { color: var(--cy-cyan); }
.fm-thread-body { min-width: 0; display: flex; flex-direction: column; }
.fm-thread-title { font-size: 0.92rem; color: var(--cy-text); }
.fm-thread-meta { font-size: 0.75rem; color: var(--cy-text-dim); font-family: var(--cy-font-hud); margin-top: 0.15rem; }
.fm-thread-heading {
    font-family: var(--cy-font-hud);
    font-size: 1.05rem; letter-spacing: 0.04em;
    color: var(--cy-cyan); margin: 0;
}

/* ---- Posts ---- */
.fm-post {
    display: grid;
    grid-template-columns: 10rem 1fr;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    margin-bottom: 0.75rem;
}
.fm-post-highlight {
    border-color: var(--cy-cyan);
    box-shadow: 0 0 18px rgba(0,245,255,0.35);
    transition: box-shadow 0.4s ease, border-color 0.4s ease;
}

.fm-post-author {
    display: flex; flex-direction: column; align-items: center; gap: 0.4rem;
    padding: 1rem 0.75rem;
    border-right: 1px solid var(--cy-border);
    background-color: rgba(0,0,0,0.18);
    text-align: center;
}
.fm-post-avatar { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; border: 1px solid var(--cy-border); }
.fm-post-avatar-fallback {
    width: 56px; height: 56px; border-radius: 50%;
    border: 1px solid var(--cy-border); background-color: var(--cy-bg);
    display: flex; align-items: center; justify-content: center;
    font-family: var(--cy-font-hud); font-size: 1.3rem; color: var(--cy-text-dim);
}
.fm-post-name { font-family: var(--cy-font-hud); font-size: 0.8rem; letter-spacing: 0.04em; }
.fm-post-joined { font-size: 0.75rem; color: var(--cy-text-dim); font-family: var(--cy-font-hud); }

.fm-post-main { padding: 0.9rem 1.1rem; min-width: 0; }
.fm-post-head {
    display: flex; justify-content: space-between; align-items: center;
    border-bottom: 1px solid var(--cy-border);
    padding-bottom: 0.5rem; margin-bottom: 0.75rem;
}
.fm-post-permalink { font-family: var(--cy-font-hud); font-size: 0.75rem; color: var(--cy-text-dim); text-decoration: none; }
.fm-post-permalink:hover { color: var(--cy-cyan); }
.fm-post-date { font-family: var(--cy-font-hud); font-size: 0.75rem; color: var(--cy-text-dim); }

.fm-post-body {
    color: var(--cy-text); font-size: 0.94rem; line-height: 1.7;
    /* Long unbroken strings must wrap, not widen the column. */
    overflow-wrap: anywhere; word-break: break-word;
}
.fm-post-edited { font-size: 0.75rem; color: var(--cy-text-dim); font-style: italic; margin-top: 0.6rem; }

.fm-post-actions {
    display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
    margin-top: 0.85rem; padding-top: 0.6rem;
    border-top: 1px solid var(--cy-border);
}
.fm-action {
    display: inline-flex; align-items: center; gap: 0.35rem;
    background: transparent; border: 1px solid transparent;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud); font-size: 0.75rem; letter-spacing: 0.05em;
    padding: 0.25rem 0.5rem; cursor: pointer;
    transition: color 0.2s ease, border-color 0.2s ease;
}
.fm-action:hover { color: var(--cy-cyan); border-color: rgba(0,245,255,0.35); }
.fm-action-danger:hover { color: var(--cy-error); border-color: rgba(255,58,58,0.4); }

/* ---- BBCode output ---- */
.bb-quote {
    margin: 0.7rem 0; padding: 0.6rem 0.9rem;
    border-left: 3px solid var(--cy-cyan);
    background-color: rgba(0,245,255,0.05);
}
.bb-quote-who {
    display: block; font-family: var(--cy-font-hud); font-size: 0.75rem;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--cy-cyan);
    margin-bottom: 0.3rem; font-style: normal;
}
.bb-quote-body { color: var(--cy-text-dim); font-size: 0.9rem; }
.bb-code {
    margin: 0.7rem 0; padding: 0.7rem 0.9rem;
    background-color: var(--cy-bg); border: 1px solid var(--cy-border);
    font-family: 'Share Tech Mono', monospace; font-size: 0.84rem;
    color: var(--cy-green);
    overflow-x: auto; white-space: pre;
}
/*
   Links in a post.

   These NEED a class selector, not a bare `a`. Tailwind's preflight ships
   `a { color: inherit; text-decoration: inherit }` and the built bundle loads
   AFTER this file, so an element-level rule here loses and every link in a post
   renders as plain body text — same colour, no underline, indistinguishable
   from the words around it. .bb-link outranks it comfortably.

   Cyan because that is this board's default interactive tone; the off-site
   marker below spends an arrow rather than a second colour, so colour keeps
   meaning what it already means elsewhere in the forum.
*/
.bb-link {
    color: var(--cy-cyan);
    text-decoration: underline;
    /* Dimmer than the text it sits under, so the line reads as a hint rather
       than competing with the words. */
    text-decoration-color: rgba(0, 245, 255, 0.4);
    text-decoration-thickness: 1px;
    text-underline-offset: 0.18em;
    transition: color 0.2s ease, text-shadow 0.2s ease, text-decoration-color 0.2s ease;
}
.bb-link:hover {
    color: #ffffff;
    text-decoration-color: var(--cy-cyan);
    text-shadow: 0 0 8px var(--cy-cyan-glow);
}
.bb-link:focus-visible {
    outline: 1px solid var(--cy-cyan);
    outline-offset: 2px;
}
/* A quiet arrow so off-site links are identifiable before they are clicked,
   not only once the confirmation appears. Scoped to .bb-link, which keeps it
   off attachment thumbnails and off the dialog's own Continue button. */
.bb-link[data-external]::after {
    content: "\2197";
    font-size: 0.78em;
    margin-left: 0.15em;
    opacity: 0.75;
}

.bb-u { text-decoration: underline; }
.bb-list { margin: 0.6rem 0 0.6rem 1.3rem; list-style: square; }
.bb-list li { margin: 0.2rem 0; }
.bb-img { max-width: 100%; height: auto; border: 1px solid var(--cy-border); margin: 0.5rem 0; }

/* Spoiler: blacked out until clicked. The JS-free fallback is :focus, so a
   keyboard user can still reveal it. */
.bb-spoiler {
    background-color: var(--cy-surface-3);
    color: transparent;
    border-radius: 3px; padding: 0 0.25rem; cursor: pointer;
    transition: color 0.2s ease, background-color 0.2s ease;
}
.bb-spoiler:hover, .bb-spoiler:focus, .bb-spoiler.revealed {
    background-color: rgba(0,245,255,0.08);
    color: var(--cy-text);
}
/*
   A link inside a spoiler must black out with everything else. .bb-link sets
   its own colour, so it would otherwise sit there in cyan, underlined, with an
   arrow on it — announcing exactly what the spoiler was hiding. The underline
   needs silencing too: text-decoration-color does not follow `color: inherit`
   once it has been set explicitly.
*/
.bb-spoiler .bb-link {
    color: inherit;
    text-decoration-color: transparent;
    text-shadow: none;
}
.bb-spoiler:hover .bb-link,
.bb-spoiler:focus .bb-link,
.bb-spoiler.revealed .bb-link {
    color: var(--cy-cyan);
    text-decoration-color: rgba(0, 245, 255, 0.4);
}

/* ---- Composer ---- */
.fm-composer { border: 1px solid var(--cy-border); background-color: var(--cy-surface-2); }
.fm-toolbar {
    display: flex; align-items: center; gap: 0.15rem; flex-wrap: wrap;
    padding: 0.4rem 0.5rem;
    border-bottom: 1px solid var(--cy-border);
    background-color: var(--cy-surface-3);
    position: relative;
}
.fm-toolbar-sep { width: 1px; height: 1.1rem; background-color: var(--cy-border); margin: 0 0.35rem; }
.fm-tool {
    background: transparent; border: 1px solid transparent;
    color: var(--cy-text-dim); cursor: pointer;
    padding: 0.3rem 0.5rem; font-size: 0.8rem; line-height: 1;
    transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}
.fm-tool:hover { color: var(--cy-cyan); border-color: rgba(0,245,255,0.35); background-color: rgba(0,245,255,0.07); }

/*
   POSITION: FIXED, AND MOVED TO <body> WHEN IT OPENS — the same arrangement as
   .cm-emoji-menu above, for the same two reasons.

   It was absolute, pinned at top:calc(100% + 0.35rem), which meant it ALWAYS
   opened downward and went off the bottom of the window from a reply box near
   the foot of a long thread. And the Quick Reply form is a .gothic-card, whose
   cut-corner clip-path clips every descendant no matter what z-index it carries,
   so the menu was being sliced off at the bevel as well.

   top/left are set by initComposer() in forum.js, which flips it above the
   button when there is no room below and clamps it inside the viewport. The
   values here only matter for the instant before it is placed.

   z-index 190 rather than 60: on <body> it is no longer inside the composer's
   stacking context and has to clear the page on its own. Matches .cm-emoji-menu.
*/
.fm-emoji-menu {
    position: fixed; top: 0; left: 0; z-index: 190;
    width: 296px;
    max-width: calc(100vw - 1.5rem);
    /* Same height reasoning as .pm-emoji-menu — see the note there. */
    max-height: min(24rem, 70vh);
    overflow-y: auto;
    background-color: var(--cy-surface-2); border: 1px solid var(--cy-border);
    box-shadow: 0 10px 30px rgba(0,0,0,0.75), 0 0 18px rgba(0,245,255,0.12);
    padding: 0.6rem;
}
.fm-emoji-menu[hidden] { display: none; }

/* Overrides the plain grey .fm-tool it also carries — same violet accent as
   .cm-gif-toggle and .pm-gif-btn, and the same reasoning: see the comment on
   .cm-gif-toggle for why violet specifically. */
.fm-gif-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--cy-violet) !important;
    border-color: rgba(167, 139, 250, 0.4) !important;
    background-color: rgba(167, 139, 250, 0.08);
}
.fm-gif-toggle:hover {
    border-color: var(--cy-violet) !important;
    background-color: rgba(167, 139, 250, 0.16) !important;
}
.fm-gif-label { font-weight: 700; font-size: 0.7rem; letter-spacing: 0.04em; }

/* Same fixed/reparented positioning as .fm-emoji-menu above, same reasons —
   see the long comment there. Wider, for the same reason .cm-gif-menu is
   wider than .cm-emoji-menu: a grid of GIF thumbnails needs more room than a
   grid of emoji glyphs. */
.fm-gif-menu {
    position: fixed; top: 0; left: 0; z-index: 190;
    width: 320px;
    max-width: calc(100vw - 1.5rem);
    max-height: min(24rem, 70vh);
    overflow-y: auto;
    background-color: var(--cy-surface-2); border: 1px solid var(--cy-border);
    box-shadow: 0 10px 30px rgba(0,0,0,0.75), 0 0 18px rgba(0,245,255,0.12);
    padding: 0.6rem;
}
.fm-gif-menu[hidden] { display: none; }

.fm-textarea {
    width: 100%; display: block; resize: vertical;
    background-color: var(--cy-surface-2); border: none;
    color: var(--cy-text); padding: 0.75rem 0.9rem;
    font-family: var(--cy-font-body); font-size: 0.94rem; line-height: 1.6;
}
.fm-textarea:focus { outline: none; box-shadow: inset 0 0 0 1px var(--cy-cyan); }

.fm-composer-foot {
    display: flex; align-items: center; justify-content: space-between; gap: 1rem;
    flex-wrap: wrap;
    padding: 0.4rem 0.7rem;
    border-top: 1px solid var(--cy-border);
    background-color: var(--cy-surface-3);
}
.fm-hint { font-size: 0.75rem; color: var(--cy-text-dim); }
.fm-hint code { color: var(--cy-cyan); font-family: 'Share Tech Mono', monospace; }
.fm-charcount { font-family: var(--cy-font-hud); font-size: 0.75rem; color: var(--cy-text-dim); white-space: nowrap; }
.fm-charcount-over { color: var(--cy-error); }

/* ---- Forms ---- */
.fm-label {
    display: block;
    font-family: var(--cy-font-hud); font-size: 0.75rem;
    letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--cy-text-dim); margin-bottom: 0.4rem;
}
.fm-input {
    width: 100%;
    background-color: var(--cy-surface-2);
    border: 1px solid var(--cy-border);
    color: var(--cy-text);
    padding: 0.55rem 0.75rem;
    font-family: var(--cy-font-body); font-size: 0.92rem;
}
.fm-input:focus { outline: none; border-color: var(--cy-cyan); box-shadow: 0 0 10px rgba(0,245,255,0.25); }
.fm-check { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.8rem; color: var(--cy-text-dim); cursor: pointer; }

/* ---- Narrow screens ---- */
@media (max-width: 800px) {
    /* The stats and last-post columns are the first things to go: the title and
       who posted last are what people actually scan for. */
    .fm-forum, .fm-thread { grid-template-columns: 2rem 1fr; }
    .fm-forum-stats, .fm-thread-stats, .fm-forum-last, .fm-thread-last { display: none; }

    /* Author details move above the post rather than squeezing a 10rem rail. */
    .fm-post { grid-template-columns: 1fr; }
    .fm-post-author {
        flex-direction: row; justify-content: flex-start;
        border-right: none; border-bottom: 1px solid var(--cy-border);
        padding: 0.6rem 0.9rem;
    }
    .fm-post-avatar, .fm-post-avatar-fallback { width: 34px; height: 34px; font-size: 0.9rem; }
    .fm-post-joined { display: none; }
}

/* ---- Forum: sidebar layout, prefixes, attachments, search ---- */

.fm-layout { display: grid; grid-template-columns: 1fr 17rem; gap: 1.5rem; align-items: start; }
.fm-layout-main { min-width: 0; }

.fm-sidebar { display: flex; flex-direction: column; gap: 1rem; }
.fm-widget { border: 1px solid var(--cy-border); background-color: var(--cy-surface-1); padding: 0.9rem 1rem; }
.fm-widget-title {
    display: flex; align-items: center; gap: 0.45rem;
    font-family: var(--cy-font-hud); font-size: 0.75rem;
    letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--cy-cyan); margin: 0 0 0.7rem;
}
.fm-widget-empty, .fm-widget-foot {
    font-size: 0.75rem; color: var(--cy-text-dim); margin: 0.6rem 0 0;
}

.fm-stat-list { margin: 0; display: flex; flex-direction: column; gap: 0.35rem; }
.fm-stat-list > div { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; }
.fm-stat-list dt { font-size: 0.75rem; color: var(--cy-text-dim); }
.fm-stat-list dd {
    margin: 0; font-family: var(--cy-font-hud); font-size: 0.82rem; color: var(--cy-text);
}
.fm-stat-online { color: var(--cy-green) !important; }

.fm-online-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.fm-online-member {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.2rem 0.45rem 0.2rem 0.2rem;
    border: 1px solid var(--cy-border); border-radius: 999px;
    text-decoration: none; transition: border-color 0.2s ease;
}
.fm-online-member:hover { border-color: var(--cy-cyan); }
.fm-online-member img, .fm-online-fallback {
    width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex-shrink: 0;
}
.fm-online-fallback {
    background-color: var(--cy-bg); color: var(--cy-text-dim);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 0.75rem; font-family: var(--cy-font-hud);
}
.fm-online-name { font-size: 0.75rem; color: var(--cy-text-dim); }

.fm-widget-search { display: flex; gap: 0.35rem; }
.fm-widget-search .fm-input { font-size: 0.78rem; padding: 0.4rem 0.55rem; }

/* ---- Prefixes ---- */
.fm-prefix {
    display: inline-block; margin-right: 0.4rem;
    padding: 0.05rem 0.4rem;
    border: 1px solid currentColor; border-radius: 3px;
    font-family: var(--cy-font-hud); font-size: 0.75rem;
    letter-spacing: 0.08em; text-transform: uppercase;
    vertical-align: middle;
}

.fm-title-row { display: grid; grid-template-columns: 12rem 1fr; gap: 0.75rem; }
.fm-prefix-field, .fm-title-field { min-width: 0; }

/* ---- Postbit stats ---- */
.fm-post-stats {
    margin: 0.35rem 0 0; width: 100%;
    display: flex; flex-direction: column; gap: 0.15rem;
}
.fm-post-stats > div { display: flex; justify-content: space-between; gap: 0.5rem; }
.fm-post-stats dt { font-size: 0.75rem; color: var(--cy-text-dim); text-transform: uppercase; letter-spacing: 0.06em; }
.fm-post-stats dd { margin: 0; font-family: var(--cy-font-hud); font-size: 0.75rem; color: var(--cy-text); }

/* ---- Attachments ----
   Rendered at the same size as its card in the composer's attach tray
   (.fm-attach-item img below) — a small preview inline in the post, not a
   large image; click still opens the full-size lightbox (data-full). */
.bb-attach { display: inline-block; margin: 0.4rem 0.4rem 0.4rem 0; line-height: 0; }
.bb-attach img {
    width: 108px; height: 64px; object-fit: cover;
    border: 1px solid var(--cy-border);
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.bb-attach:hover img { border-color: var(--cy-cyan); box-shadow: 0 0 14px rgba(0,245,255,0.3); }

.fm-attachments { margin-top: 0.9rem; padding-top: 0.7rem; border-top: 1px dashed var(--cy-border); }
.fm-attachments-label {
    font-family: var(--cy-font-hud); font-size: 0.75rem;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--cy-text-dim); margin: 0 0 0.4rem;
}
.fm-attachments-grid { display: flex; flex-wrap: wrap; gap: 0.4rem; }

/* Composer tray */
.fm-attach-tray { padding: 0.55rem 0.7rem; border-top: 1px solid var(--cy-border); background-color: var(--cy-surface-3); }
.fm-attach-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.fm-attach-item {
    position: relative; width: 108px;
    border: 1px solid var(--cy-border); background-color: var(--cy-surface-2);
    padding: 0.3rem; display: flex; flex-direction: column; gap: 0.2rem;
}
.fm-attach-item.is-uploading { align-items: center; justify-content: center; min-height: 96px; }
.fm-attach-spinner { color: var(--cy-cyan); }
.fm-attach-item img { width: 100%; height: 64px; object-fit: cover; }
.fm-attach-name {
    font-size: 0.75rem; color: var(--cy-text-dim);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fm-attach-size { font-size: 0.75rem; color: var(--cy-text-dim); font-family: var(--cy-font-hud); }
.fm-attach-buttons { display: flex; gap: 0.2rem; align-items: center; }
.fm-attach-insert, .fm-attach-remove {
    background: transparent; border: 1px solid var(--cy-border);
    color: var(--cy-text-dim); cursor: pointer;
    font-size: 0.75rem; font-family: var(--cy-font-hud);
    padding: 0.1rem 0.3rem; text-transform: uppercase; letter-spacing: 0.05em;
}
.fm-attach-insert { flex: 1; }
.fm-attach-insert:hover { color: var(--cy-cyan); border-color: var(--cy-cyan); }
.fm-attach-remove:hover { color: var(--cy-error); border-color: var(--cy-error); }
.fm-attach-hint { font-size: 0.75rem; color: var(--cy-text-dim); margin: 0.45rem 0 0; }
.fm-attach-error { font-size: 0.75rem; color: var(--cy-error); margin: 0.45rem 0 0; }

/* The upload trigger, moved below the tray/hint text and given its own
   presence rather than sitting quietly in the toolbar as a bare paperclip
   icon (still is one, just no longer THE only cue). Cyan, not violet — this
   is the site's default/primary accent (links, primary buttons), the same
   colour "attach" already implies everywhere else on the site, whereas
   violet on the GIF button next to it was chosen specifically to avoid that
   association. Block-level and left-aligned by default — no flex/centering
   here, same as .fm-attach-hint right above it. */
.fm-attach-cta {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-top: 0.65rem;
    background-color: rgba(0, 245, 255, 0.08);
    border: 1px solid rgba(0, 245, 255, 0.45);
    color: var(--cy-cyan);
    cursor: pointer;
    padding: 0.4rem 0.9rem;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    animation: fm-attach-pulse 2.4s ease-in-out infinite;
    transition: background-color 0.2s ease, border-color 0.2s ease;
}
.fm-attach-cta i { font-size: 0.85rem; }
.fm-attach-cta:hover { background-color: rgba(0, 245, 255, 0.18); border-color: var(--cy-cyan); }
.fm-attach-cta:focus-visible { outline: 1px solid var(--cy-cyan); outline-offset: 2px; }

/* Breathing glow rather than a size/position change — the button's layout
   never shifts, so it can't nudge the tray around while it pulses. */
@keyframes fm-attach-pulse {
    0%, 100% { box-shadow: 0 0 3px rgba(0, 245, 255, 0.2); border-color: rgba(0, 245, 255, 0.45); }
    50%      { box-shadow: 0 0 12px rgba(0, 245, 255, 0.6); border-color: rgba(0, 245, 255, 0.85); }
}
@media (prefers-reduced-motion: reduce) {
    .fm-attach-cta { animation: none; }
}

/* ---- Lightbox ---- */
/*
   SHARED CHROME. Everything prefixed .lb- below is used by BOTH viewers — the
   forum attachment lightbox built in public/js/forum.js, and the fullscreen
   image viewer in content/images/show.blade.php. It lives here rather than in
   either of them so the two cannot drift into looking like different products.
   .fm-lightbox is the forum overlay specifically.
*/
/*
   z-index HAS TO BEAT .sticky-nav (1000). It used to be 200, which put the
   whole overlay under the navigation bar — tolerable while the only close
   control was a bare × tucked below the nav, fatal the moment Close moved to
   the top-right corner, because a z-index inside this element is capped by this
   element's own layer no matter how large it is. The button was being painted
   behind the nav.

   Anything that must appear OVER an open lightbox has to clear this number too;
   .fm-leaving is the one that does, and it was moved with it.
*/
.fm-lightbox {
    position: fixed; inset: 0; z-index: 1100;
    background-color: rgba(2, 2, 8, 0.93);
    display: flex; flex-direction: column;
}

/*
   The picture area. flex:1 with min-height:0 — the second half matters: a flex
   child defaults to min-height:auto, which refuses to shrink below its content,
   so a tall image would push the filmstrip off the bottom of the screen.

   This replaces a hand-computed max-height budget (100vh minus 9rem, then 12rem
   once a counter row appeared). Every time a row was added to the column that
   number had to be re-derived by hand; letting flex do the arithmetic means it
   cannot fall out of step again.
*/
.lb-shell { width: 100%; height: 100%; display: flex; flex-direction: column; }
.lb-stage {
    flex: 1 1 auto; min-height: 0;
    display: flex; align-items: center; justify-content: center;
    padding: 1rem;
    /* A click here advances to the next picture, so it reads as a control. */
    cursor: pointer;
}
.lb-stage img {
    max-width: 100%; max-height: 100%;
    width: auto; height: auto;
    /* The picture is a click target, not something to drag out of the page. */
    -webkit-user-drag: none;
    user-select: none;
}
.fm-lightbox .lb-stage img {
    border: 1px solid var(--cy-border);
    box-shadow: 0 0 40px rgba(0, 245, 255, 0.2);
}

/* ---- Close ----
   A labelled pill in the corner, not a bare glyph.

   It has to be findable: clicking the backdrop used to dismiss these, and now
   that a click anywhere on the stage advances the picture instead, this button
   and Escape are the only ways out. A × that people missed was survivable while
   there was a second way; it is not any more.
*/
.lb-close {
    position: fixed; top: 1.25rem; right: 1.25rem; z-index: 10002;
    display: inline-flex; align-items: center; gap: 0.55rem;
    padding: 0.55rem 1.15rem;
    font-family: var(--cy-font-hud);
    font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--cy-cyan);
    background: rgba(2, 2, 8, 0.72);
    border: 1px solid rgba(0, 245, 255, 0.45);
    border-radius: 999px;
    cursor: pointer;
    backdrop-filter: blur(4px);
    box-shadow: 0 0 14px rgba(0, 245, 255, 0.18);
    transition: transform 0.18s ease, background-color 0.18s ease,
                border-color 0.18s ease, box-shadow 0.18s ease, color 0.18s ease;
}
.lb-close:hover {
    transform: translateY(-1px) scale(1.06);
    background: rgba(0, 245, 255, 0.16);
    border-color: var(--cy-cyan);
    color: #ffffff;
    box-shadow: 0 0 22px rgba(0, 245, 255, 0.45);
}
/* The × turns as the button lights up. Two things moving together read as one
   control responding; the scale on its own reads as a wobble. */
.lb-close .lb-close-x {
    display: inline-block;
    font-size: 0.95rem; line-height: 1;
    transition: transform 0.25s ease;
}
.lb-close:hover .lb-close-x { transform: rotate(90deg); }
.lb-close:focus-visible { outline: 2px solid var(--cy-cyan); outline-offset: 3px; }

/* ---- The row of controls in the top-right corner ----
   Close, and whatever sits beside it.

   A FLEX ROW rather than each pill positioned by hand. The gallery viewer's
   Details button was pinned at right:9.5rem, which is a number that has to be
   re-derived by eye every time a control is added or its label changes. This
   lays them out instead. */
.lb-controls {
    position: fixed; top: 1.25rem; right: 1.25rem; z-index: 10002;
    display: flex; align-items: center; gap: 0.6rem;
    flex-wrap: wrap; justify-content: flex-end;
    max-width: calc(100vw - 2.5rem);
}
/* .lb-close is fixed on its own — the forum viewer still uses it that way.
   Inside the row it defers to the row's positioning. */
.lb-controls .lb-close { position: static; top: auto; right: auto; z-index: auto; }

/* A statement, not a control: says what a click on the picture will do.
   Quieter than the buttons beside it, and it must never look pressable. */
.lb-hint {
    display: inline-flex; align-items: center; gap: 0.45rem;
    padding: 0.55rem 1rem;
    font-family: var(--cy-font-hud);
    font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--cy-text-dim);
    background: rgba(2, 2, 8, 0.55);
    border: 1px solid var(--cy-border);
    border-radius: 999px;
    backdrop-filter: blur(4px);
    cursor: default;
    user-select: none;
}
.lb-hint i { color: var(--cy-cyan); opacity: 0.7; }

/* Hidden on narrow screens: there is no cursor to click with on a phone, the
   gesture there is a tap or a swipe, and three pills across a small screen
   pushes Close off the edge. */
@media (max-width: 640px) {
    .lb-hint { display: none; }
}

/* ---- Edge arrows ----
   Fixed to the left and right edges, vertically centred — matching the pair the
   image viewer has carried since before the rework, so a picture opened from a
   gallery is navigated the same way as one opened from its own page. */
.lb-edge-arrow {
    position: fixed; top: 50%; transform: translateY(-50%);
    z-index: 10001;
    display: flex; align-items: center; justify-content: center;
    width: 3.5rem; height: 3.5rem;
    padding: 0;
    background: rgba(0, 0, 0, 0.9);
    border: 2px solid #ffffff;
    border-radius: 50%;
    color: #ffffff;
    cursor: pointer;
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6);
    transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.lb-edge-arrow:hover {
    background: rgba(0, 0, 0, 1);
    border-color: var(--cy-cyan);
    color: var(--cy-cyan);
}
.lb-edge-prev { left: 20px; }
.lb-edge-next { right: 20px; }
.lb-edge-arrow svg { width: 2rem; height: 2rem; }

/* Tucked in and shrunk rather than hidden on a phone: they are the only way
   BACK when the tap gesture only goes forward. */
@media (max-width: 640px) {
    .lb-edge-arrow { width: 2.6rem; height: 2.6rem; }
    .lb-edge-arrow svg { width: 1.4rem; height: 1.4rem; }
    .lb-edge-prev { left: 8px; }
    .lb-edge-next { right: 8px; }
}

@media (prefers-reduced-motion: reduce) {
    .lb-edge-arrow { transition: none; }
}

/* ---- Filmstrip ----
   Centred as a row. The track only begins to scroll once the thumbs outgrow it,
   so a three-image album sits centred rather than pinned left with dead space
   beside it.
*/
.lb-strip {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem 1.1rem;
}
.lb-strip.hidden { display: none; }
.lb-strip-track {
    display: flex; gap: 0.5rem;
    overflow-x: auto; scroll-behavior: smooth;
    max-width: min(1100px, 84vw);
    /* No visible scrollbar — the arrows are the affordance, and a bar under the
       thumbs reads as a second, broken border. */
    scrollbar-width: none;
}
.lb-strip-track::-webkit-scrollbar { display: none; }
.lb-thumb {
    flex: 0 0 auto;
    width: 74px; height: 74px;
    padding: 0; background: none;
    border: 1px solid var(--cy-border); border-radius: 4px;
    overflow: hidden; cursor: pointer; opacity: 0.5;
    transition: opacity 0.18s ease, border-color 0.18s ease, transform 0.18s ease;
}
.lb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lb-thumb:hover { opacity: 1; transform: translateY(-2px); border-color: rgba(0, 245, 255, 0.55); }
.lb-thumb.is-current {
    opacity: 1;
    border-color: var(--cy-cyan);
    box-shadow: 0 0 12px rgba(0, 245, 255, 0.45);
}
.lb-strip-arrow {
    flex: 0 0 auto;
    width: 34px; height: 74px;
    display: flex; align-items: center; justify-content: center;
    background: rgba(2, 2, 8, 0.6);
    border: 1px solid var(--cy-border); border-radius: 4px;
    color: var(--cy-text-dim); font-size: 1.5rem; line-height: 1; cursor: pointer;
    transition: color 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
}
.lb-strip-arrow:hover {
    color: var(--cy-cyan);
    border-color: rgba(0, 245, 255, 0.5);
    background: rgba(0, 245, 255, 0.08);
}
/* Hidden rather than disabled when the thumbs already fit: a dead arrow invites
   clicking, and there would be nothing to scroll to. */
.lb-strip-arrow.hidden { display: none; }

@media (prefers-reduced-motion: reduce) {
    .lb-close, .lb-close .lb-close-x, .lb-thumb { transition: none; }
    .lb-close:hover, .lb-close:hover .lb-close-x, .lb-thumb:hover { transform: none; }
    .lb-strip-track { scroll-behavior: auto; }
}

/* ---- Username hover card ----
   Built and positioned by public/js/user-card.js.

   [!] IT LIVES ON <body>, not beside the name it describes. Posts and listings
       sit inside .gothic-card, whose cut-corner clip-path clips every descendant
       regardless of z-index — a card rendered in place would be sliced off at
       the card edge. Same reason the reaction picker and the presence tooltips
       reparent themselves. position:fixed follows from that: it is positioned
       against the viewport, from the link's bounding rect.

   z-index only has to beat the page, not a lightbox: the two are never open at
   once, and a lightbox covers the names that would open this.
*/
.fm-ucard {
    position: fixed;
    z-index: 900;
    width: 19rem; max-width: calc(100vw - 1rem);
    padding: 0.9rem;
    background: rgba(9, 9, 30, 0.98);
    border: 1px solid rgba(0, 245, 255, 0.35);
    border-radius: 8px;
    box-shadow: 0 12px 34px rgba(0, 0, 0, 0.8), 0 0 18px rgba(0, 245, 255, 0.1);
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 0.14s ease, transform 0.14s ease;
    pointer-events: auto;
}
.fm-ucard.is-open { opacity: 1; transform: translateY(0); }
.fm-ucard[hidden] { display: none; }

.fm-ucard-head { display: flex; align-items: center; gap: 0.7rem; }
.fm-ucard-avatar-wrap { flex: 0 0 auto; }
.fm-ucard-avatar {
    display: block;
    width: 3rem; height: 3rem;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--cy-border);
}
.fm-ucard-avatar-fallback {
    display: flex; align-items: center; justify-content: center;
    background: var(--cy-surface-3);
    font-family: var(--cy-font-hud);
    font-size: 1.2rem;
    color: var(--cy-cyan);
    text-transform: uppercase;
}
.fm-ucard-ident { min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
.fm-ucard-name {
    font-family: var(--cy-font-hud);
    font-size: 0.95rem; letter-spacing: 0.03em;
    color: var(--cy-cyan);
    text-decoration: none;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fm-ucard-role {
    font-family: var(--cy-font-hud);
    font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--cy-text-dim);
}
.fm-ucard-bio {
    margin: 0.7rem 0 0;
    font-size: 0.8rem; line-height: 1.5;
    color: var(--cy-text-soft);
}

/* Four small figures on one row. auto-fit rather than a fixed four columns so
   a narrow phone wraps them instead of squeezing them into slivers. */
.fm-ucard-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(4.2rem, 1fr));
    gap: 0.5rem 0.4rem;
    margin: 0.8rem 0 0;
    padding-top: 0.7rem;
    border-top: 1px solid var(--cy-border);
}
.fm-ucard-stats dt {
    font-family: var(--cy-font-hud);
    font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--cy-text-dim);
}
.fm-ucard-stats dd {
    margin: 0.1rem 0 0;
    font-family: var(--cy-font-hud);
    font-size: 0.85rem;
    color: var(--cy-text);
}
.fm-ucard-actions {
    display: flex; gap: 0.4rem; flex-wrap: wrap;
    margin-top: 0.85rem;
}

@media (prefers-reduced-motion: reduce) {
    .fm-ucard { transition: none; transform: none; }
    .fm-ucard.is-open { transform: none; }
}

/* ---- Gallery thumbnail chooser + viewer ----
   Built by public/js/gallery-viewer.js.

   The viewer reuses the shared .lb-* chrome above — stage, filmstrip, Close
   pill — so a picture opened from a gallery, from a forum attachment or from
   the image page all look like one component. Only the shell differs, which is
   what .gv-lightbox is.
*/
.gv-lightbox {
    position: fixed; inset: 0; z-index: 1100;   /* clears .sticky-nav, as the others do */
    background-color: rgba(0, 0, 0, 0.95);
    display: none;
}
.gv-lightbox.is-open { display: block; }
.gv-lightbox .lb-shell { width: 100%; height: 100%; }

/* Details sits beside Close in the .lb-controls row — no hand-set offset, the
   row lays it out. Quieter than Close, because leaving is the commoner action,
   but it is the whole reason "full size" is not a dead end, so it is a real
   control rather than a hint. */
.lb-details {
    color: var(--cy-text-dim);
    border-color: rgba(0, 245, 255, 0.25);
    text-decoration: none;
}
.lb-details:hover {
    color: #ffffff;
    border-color: var(--cy-cyan);
    background: rgba(0, 245, 255, 0.16);
}

/* ---- The chooser ---- */
.gv-chooser {
    position: fixed; inset: 0; z-index: 1120;   /* above the viewer it can open */
    background-color: rgba(2, 2, 8, 0.86);
    display: flex; align-items: center; justify-content: center;
    padding: 1.25rem;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
}
.gv-chooser.is-open { opacity: 1; pointer-events: auto; }

/*
   Small and quick. This appears on an ordinary thumbnail click, so it has to
   feel like part of the click rather than an interruption — 0.22s, a short
   rise, and no bounce. Anything slower reads as a page load.
*/
.gv-chooser-box {
    width: 100%; max-width: 30rem;
    padding: 1.5rem;
    background: linear-gradient(135deg, #050512 0%, #0a0a22 100%);
    border: 1px solid rgba(0, 245, 255, 0.30);
    border-radius: 0.5rem;
    box-shadow: 0 0 40px rgba(0, 245, 255, 0.10), 0 20px 50px rgba(0, 0, 0, 0.7);
    transform: translateY(10px) scale(0.97);
    transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.3, 1);
}
.gv-chooser.is-open .gv-chooser-box { transform: translateY(0) scale(1); }

.gv-chooser-title {
    margin: 0 0 1.1rem;
    font-family: var(--cy-font-hud);
    font-size: 0.8rem; letter-spacing: 0.12em; text-transform: uppercase;
    color: var(--cy-cyan);
    text-align: center;
}

.gv-chooser-options { display: flex; gap: 0.75rem; flex-wrap: wrap; }

.gv-chooser-option {
    flex: 1 1 11rem;
    display: block; text-align: left; cursor: pointer;
    padding: 1rem;
    background: rgba(0, 245, 255, 0.04);
    border: 1px solid rgba(0, 245, 255, 0.25);
    border-radius: 0.5rem;
    transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
}
.gv-chooser-option:hover {
    border-color: rgba(0, 245, 255, 0.65);
    background: rgba(0, 245, 255, 0.09);
    transform: translateY(-2px);
}
.gv-chooser-icon  { display: block; color: var(--cy-cyan); font-size: 1.25rem; margin-bottom: 0.5rem; }
.gv-chooser-label { display: block; color: var(--cy-text); font-size: 0.92rem; font-weight: 700; margin-bottom: 0.25rem; }
.gv-chooser-note  { display: block; color: var(--cy-text-dim); font-size: 0.78rem; line-height: 1.45; }

.gv-chooser-remember {
    display: flex; align-items: center; gap: 0.6rem;
    margin-top: 1.1rem;
    color: var(--cy-text-soft); font-size: 0.82rem;
    cursor: pointer; user-select: none;
}
/* Bigger than the 16px Tailwind gives it — this decides a persistent setting,
   and it sits next to two large buttons that are easy to hit by comparison. */
.gv-chooser-remember input { width: 1.2rem; height: 1.2rem; flex-shrink: 0; cursor: pointer; }

.gv-chooser-cancel {
    display: block; width: 100%;
    margin-top: 0.9rem; padding: 0.5rem;
    background: transparent; border: none;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
    cursor: pointer;
}
.gv-chooser-cancel:hover { color: var(--cy-magenta); }

@media (prefers-reduced-motion: reduce) {
    .gv-chooser, .gv-chooser-box, .gv-chooser-option { transition: none; }
    .gv-chooser-box { transform: none; }
    .gv-chooser-option:hover { transform: none; }
}

/* ---- Alpine ---- */
/*
   The main app layout never defined this, so anything marked x-cloak stayed
   visible until Alpine got to it. The admin and Vexcor layouts each declare
   their own copy in an inline <style>; this is the one for everywhere else.
*/
[x-cloak] { display: none !important; }

/* ---- Profile statistics strip ---- */
/*
   Was a vertical list in a narrow sidebar. The sidebar went when profiles were
   tabbed, and five stacked rows across the full width would be mostly empty
   space, so the same numbers now read as one horizontal strip. Wraps rather
   than scrolls: unlike the tab row these are not navigation, and a member
   should never have to swipe to find out how many galleries somebody has.
*/
.profile-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem 2rem;
    padding: 0.9rem 1.1rem;
    margin-bottom: 1.25rem;
    background-color: var(--cy-bg);
    border: 1px solid var(--cy-border);
    border-radius: 0.375rem;
}
.profile-stat { display: flex; align-items: baseline; gap: 0.45rem; }
.profile-stat-value {
    font-family: var(--cy-font-hud);
    font-size: 1.05rem;
    color: var(--cy-cyan);
}
.profile-stat-label {
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
}

/* ---- Tabs (Edit Account, member profiles) ---- */
/*
   Shared by the Edit Account page and member profiles. The .acct- prefix is
   historical — Edit Account was the first to use it — and was left alone
   rather than renaming classes on a page already in production.

   Horizontal, scrollable rather than wrapping: five tabs fit on a desktop and
   scroll sideways on a phone, which keeps them on one line and keeps the row
   height predictable. Wrapping would push the panel down a line on narrow
   screens and make the page jump as the active tab changes.
*/
.acct-tabs {
    display: flex;
    gap: 0.15rem;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border-bottom: 1px solid var(--cy-border);
    margin-bottom: 1.5rem;
    /* No scrollbar under the tabs on desktop; the row is short enough that one
       would read as a second, broken border. */
    scrollbar-width: none;
}
.acct-tabs::-webkit-scrollbar { display: none; }

.acct-tab {
    flex: 0 0 auto;
    background: transparent;
    border: 0;
    border-bottom: 2px solid transparent;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.78rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.7rem 1.1rem;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}
.acct-tab i { margin-right: 0.45rem; }
.acct-tab:hover {
    color: var(--cy-cyan);
    background-color: rgba(0, 245, 255, 0.06);
}
.acct-tab.is-active {
    color: var(--cy-cyan);
    border-bottom-color: var(--cy-cyan);
    text-shadow: 0 0 8px var(--cy-cyan-glow);
}
.acct-tab:focus-visible {
    outline: 1px solid var(--cy-cyan);
    outline-offset: -2px;
}

/* Deleting your account is not the same kind of act as changing your bio, and
   the tab that leads there should not look like the others. Red only once it
   is the tab you are on, so the row does not sit there shouting. */
.acct-tab-danger:hover {
    color: var(--cy-error);
    background-color: rgba(255, 58, 58, 0.07);
}
.acct-tab-danger.is-active {
    color: var(--cy-error);
    border-bottom-color: var(--cy-error);
    text-shadow: 0 0 8px rgba(255, 58, 58, 0.6);
}
.acct-tab-danger:focus-visible { outline-color: var(--cy-error); }

/* The " - " between a tab's name and its count.

   Violet because it is the only hue here that carries no state meaning — see
   --cy-violet. A separator that borrowed gold or amber would be reading as
   "pinned" or "locked" to anyone who has learnt the rest of the palette, which
   is worse than no colour at all.

   text-shadow: none matters. The active tab sets a cyan glow, and text-shadow
   INHERITS — without this the violet dash would be lit cyan on whichever tab you
   are standing on, and only that one, which looks like a rendering fault rather
   than a choice. */
.acct-tab-sep {
    color: var(--cy-violet);
    margin: 0 0.15rem;
    text-shadow: none;
}

/* ---- Leaving-the-site confirmation ---- */
/*
   Amber, the same tone as a locked thread. Both are "stop and read this before
   you carry on" states, and they never share a surface — one is a badge on a
   thread row, this is a full dialog with its own words. Red would overstate it
   (nothing is being destroyed) and cyan would not register as a warning at all.

   z-index sits above the lightbox: an attachment viewer can be open when a
   member clicks a link in the same post. It moved from 210 to 1110 when
   .fm-lightbox moved from 200 to 1100 to clear the navigation bar — this pair
   only ever has to stay in the same ORDER, but they have to move together.
*/
.fm-leaving {
    position: fixed; inset: 0; z-index: 1110;
    background-color: rgba(2, 2, 8, 0.93);
    display: flex; align-items: center; justify-content: center;
    padding: 1.5rem;
}
.fm-leaving-box {
    width: 100%; max-width: 32rem;
    background-color: var(--cy-surface-1);
    border: 1px solid var(--cy-amber);
    box-shadow: 0 0 30px rgba(255, 166, 77, 0.25);
    padding: 1.5rem;
}
.fm-leaving-title {
    font-family: var(--cy-font-hud);
    font-size: 0.95rem; letter-spacing: 0.05em;
    color: var(--cy-amber);
    margin: 0 0 0.75rem;
}
.fm-leaving-title i { margin-right: 0.4rem; }
.fm-leaving-lead {
    font-size: 0.875rem; line-height: 1.6;
    color: var(--cy-text-soft);
    margin: 0 0 1rem;
}
/* The host is the part worth actually reading, so it gets the size and the
   contrast; the full URL below it is for verifying, not for scanning. */
.fm-leaving-host {
    font-family: var(--cy-font-hud);
    font-size: 1rem; color: var(--cy-cyan);
    word-break: break-all;
    margin: 0 0 0.35rem;
}
.fm-leaving-url {
    font-family: 'Share Tech Mono', monospace;
    font-size: 0.75rem; line-height: 1.5;
    color: var(--cy-text-dim);
    word-break: break-all;
    /* A pathological URL must not push the buttons off the screen. */
    max-height: 6rem; overflow-y: auto;
    margin: 0 0 1.25rem;
}
.fm-leaving-actions {
    display: flex; flex-wrap: wrap; gap: 0.6rem; justify-content: flex-end;
}
/* .gothic-btn sets no display, which leaves an <a> inline and its padding
   sitting crookedly against the button beside it. */
.fm-leaving-go {
    display: inline-block;
    text-decoration: none;
    border-color: var(--cy-amber);
    color: var(--cy-amber);
    box-shadow: 0 0 8px rgba(255, 166, 77, 0.2), inset 0 0 8px rgba(255, 166, 77, 0.04);
}
.fm-leaving-go:hover {
    background-color: var(--cy-amber-dim);
    color: #ffffff;
    box-shadow: 0 0 16px rgba(255, 166, 77, 0.5), inset 0 0 12px rgba(255, 166, 77, 0.1);
    text-shadow: 0 0 6px var(--cy-amber);
}

/* ---- Internal-link chooser ---- */
/*
   The same dialog shell as the leaving-the-site warning above, but this link
   stays on the site: nothing to warn about, so it swaps the amber accent for
   cyan — the palette's plain "there is something here" tone — and offers three
   actions (open here / new window / cancel) instead of two. Cyan also keeps it
   from being mistaken for the off-site warning at a glance.
*/
.fm-leaving--internal .fm-leaving-box {
    border-color: var(--cy-cyan);
    box-shadow: 0 0 30px var(--cy-border-glow);
}
.fm-leaving--internal .fm-leaving-title {
    color: var(--cy-cyan);
}
/* Both are <a>, so they need the same inline-block fix .fm-leaving-go documents;
   the cyan comes from .gothic-btn already, no colour override needed. */
.fm-leaving-open,
.fm-leaving-newwin {
    display: inline-block;
    text-decoration: none;
}

/* ---- Search ---- */
.fm-search-row { display: grid; grid-template-columns: 1fr 14rem auto; gap: 0.5rem; }
.fm-search-heading {
    font-family: var(--cy-font-hud); font-size: 0.75rem;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--cy-cyan); margin: 0 0 0.7rem;
}
.fm-search-hit {
    display: flex; flex-direction: column; gap: 0.3rem;
    padding: 0.85rem 1rem; margin-bottom: 0.5rem;
    border: 1px solid var(--cy-border); background-color: var(--cy-surface-1);
    text-decoration: none; transition: border-color 0.2s ease, background-color 0.2s ease;
}
.fm-search-hit:hover { border-color: var(--cy-cyan); background-color: var(--cy-surface-2); }
.fm-search-hit-head { display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; }
.fm-search-hit-title { font-size: 0.9rem; color: var(--cy-text); }
.fm-search-hit-body { font-size: 0.82rem; color: var(--cy-text-dim); line-height: 1.55; }
.fm-search-hit-meta { font-family: var(--cy-font-hud); font-size: 0.75rem; color: var(--cy-text-dim); white-space: nowrap; }

@media (max-width: 900px) {
    /* Sidebar drops below the board rather than squeezing both columns. */
    .fm-layout { grid-template-columns: 1fr; }
    .fm-title-row, .fm-search-row { grid-template-columns: 1fr; }
}

/* ====================== FRIENDS ======================
   Named classes throughout, not Tailwind utilities: the compiled bundle in
   public/build is prebuilt and cannot be regenerated on this server, so any
   utility not already in it renders as nothing. These are safe to add and to
   change. Same rule as the FORUM and MESSAGES blocks above. */

/* ---- Tabs (friends screens, and the members grid's Everyone/Friends toggle) ---- */
.friend-tabs {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

.friend-tab {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.45rem 0.9rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-decoration: none;
    transition: border-color 0.25s ease, color 0.25s ease, background-color 0.25s ease;
}
.friend-tab:hover {
    border-color: var(--cy-cyan);
    color: var(--cy-text);
    background-color: var(--cy-surface-2);
}
.friend-tab-active {
    border-color: var(--cy-cyan);
    color: var(--cy-cyan);
    background-color: var(--cy-surface-2);
    box-shadow: inset 0 -2px 0 var(--cy-cyan);
}
/* Pushed to the far end of the row: it leaves this screen rather than
   switching between views of it, and should not read as a fourth tab. */
.friend-tab-trailing { margin-left: auto; }

.friend-tab-count {
    display: inline-block;
    min-width: 1.2rem;
    padding: 0.05rem 0.35rem;
    border-radius: 999px;
    background: linear-gradient(135deg, #003344 0%, #006688 100%);
    color: #ffffff;
    font-size: 0.75rem;
    text-align: center;
}

.friend-search {
    display: flex;
    gap: 0.75rem;
    margin-bottom: 1.5rem;
    flex-wrap: wrap;
}
.friend-search-input {
    flex: 1;
    min-width: 12rem;
    padding: 0.5rem 1rem;
    border: 1px solid var(--cy-border);
    border-radius: 0.375rem;
    background-color: var(--cy-bg);
    color: var(--cy-text);
    font-size: 0.875rem;
}
.friend-search-input:focus {
    outline: none;
    border-color: var(--cy-cyan);
    box-shadow: 0 0 0 1px var(--cy-cyan-dim);
}

.friend-hint {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1rem;
    color: var(--cy-text-dim);
    font-size: 0.78rem;
}
.friend-hint i { color: var(--cy-cyan); opacity: 0.7; }

/* ---- Flash ---- */
.friend-flash {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.7rem 1rem;
    margin-bottom: 1.25rem;
    border-left: 3px solid var(--cy-cyan);
    background-color: var(--cy-surface-2);
    color: var(--cy-text);
    font-size: 0.85rem;
}
.friend-flash-ok    { border-left-color: var(--cy-green);   }
.friend-flash-ok i  { color: var(--cy-green);   }
.friend-flash-error { border-left-color: var(--cy-magenta); }
.friend-flash-error i { color: var(--cy-magenta); }

/* ---- Member grid ---- */
.friend-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 1rem;
}

.friend-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 1rem 0.75rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    text-align: center;
    transition: border-color 0.25s ease, transform 0.25s ease;
}
.friend-card:hover { border-color: var(--cy-cyan); transform: translateY(-2px); }

.friend-card-identity {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    width: 100%;
    text-decoration: none;
    min-width: 0;
}

.friend-card-avatar,
.friend-card-avatar-fallback {
    width: 64px; height: 64px;
    border-radius: 50%;
    border: 1px solid var(--cy-border);
    object-fit: cover;
}
.friend-card-avatar-fallback {
    display: flex; align-items: center; justify-content: center;
    background-color: var(--cy-bg);
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 1.5rem;
}
.friend-card:hover .friend-card-avatar,
.friend-card:hover .friend-card-avatar-fallback { border-color: var(--cy-cyan); }

.friend-card-name {
    font-family: var(--cy-font-hud);
    font-size: 0.82rem;
    letter-spacing: 0.04em;
    color: var(--cy-text);
    /* One line: a long display name must not stretch the card out of the grid. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.friend-card:hover .friend-card-name { color: var(--cy-cyan); }

.friend-card-meta {
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
}

.friend-card-actions,
.member-card-actions {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    width: 100%;
    margin-top: 0.5rem;
}

/* ---- The friend button, in all five of its states ---- */
.friend-action { display: flex; gap: 0.35rem; flex-wrap: wrap; }
/* On a card the buttons stack and fill the width; in a profile header they sit
   side by side at their natural size. */
.friend-action-compact { flex-direction: column; width: 100%; }

.friend-form { display: contents; }

.friend-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.45rem 0.9rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-2);
    color: var(--cy-text);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-decoration: none;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
}
.friend-btn:active { transform: translateY(1px); }

.friend-btn-compact {
    width: 100%;
    padding: 0.35rem 0.5rem;
    font-size: 0.75rem;
    gap: 0.3rem;
}

/* Card layout is driven by the CONTAINER as well as by the compact flag.
   The flag travels in the markup, and an AJAX re-render that dropped it once
   left a button sitting half-width and left-aligned until the next reload.
   Anchoring the same rules to the card containers means the layout cannot come
   apart that way again, whatever the swapped-in classes happen to say. */
.member-card-actions .friend-action,
.friend-card-actions .friend-action {
    flex-direction: column;
    width: 100%;
}
.member-card-actions .friend-btn,
.friend-card-actions .friend-btn {
    width: 100%;
    padding: 0.35rem 0.5rem;
    font-size: 0.75rem;
    gap: 0.3rem;
}

.friend-btn-add       { border-color: rgba(0, 245, 255, 0.4); color: var(--cy-cyan); }
.friend-btn-add:hover { background-color: var(--cy-cyan-dim); border-color: var(--cy-cyan); }

.friend-btn-accept       { border-color: rgba(57, 255, 20, 0.45); color: var(--cy-green); }
.friend-btn-accept:hover { background-color: var(--cy-green-dim); border-color: var(--cy-green); }

.friend-btn-decline       { color: var(--cy-text-dim); }
.friend-btn-decline:hover { border-color: var(--cy-magenta); color: var(--cy-magenta); }

/* Requested — a sent request still waiting on an answer. Structurally identical
   to Add Friend above; only the hue differs, so the three friend states read as
   one control in three colours rather than three different controls. */
.friend-btn-pending       { border-color: rgba(255, 215, 0, 0.45); color: var(--cy-yellow-soft); }
.friend-btn-pending:hover { background-color: var(--cy-yellow-dim); border-color: var(--cy-yellow); }

.friend-btn-message       { color: var(--cy-text-dim); }
.friend-btn-message:hover { border-color: var(--cy-cyan); color: var(--cy-cyan); }

/* Friends: reads as state at rest, as an action on hover. The two labels are
   both in the DOM and swapped by visibility rather than by JS, so the button
   works — and reads correctly — with scripting off. */
.friend-btn-friends       { border-color: rgba(57, 255, 20, 0.35); color: var(--cy-green); }
.friend-btn-friends:hover,
.friend-btn-friends:focus-visible {
    border-color: var(--cy-magenta);
    color: var(--cy-magenta);
    background-color: var(--cy-magenta-dim);
}
.friend-icon-hover,
.friend-label-hover   { display: none; }
.friend-btn-friends:hover .friend-icon-resting,
.friend-btn-friends:focus-visible .friend-icon-resting,
.friend-btn-friends:hover .friend-label-resting,
.friend-btn-friends:focus-visible .friend-label-resting { display: none; }
.friend-btn-friends:hover .friend-icon-hover,
.friend-btn-friends:focus-visible .friend-icon-hover,
.friend-btn-friends:hover .friend-label-hover,
.friend-btn-friends:focus-visible .friend-label-hover { display: inline; }

/* Mid-request. Pointer events off so a double click cannot fire the action
   twice while the first is still in flight. */
.friend-action-busy .friend-btn {
    opacity: 0.55;
    pointer-events: none;
}

/* ---- Faces: the profile box and mutual-friends line ---- */
.friend-face-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}
.friend-face-link { display: inline-flex; text-decoration: none; }

.friend-face {
    width: 32px; height: 32px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--cy-border);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: var(--cy-bg);
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.8rem;
}
.friend-face-lg { width: 44px; height: 44px; font-size: 1rem; }
.friend-face-fallback { text-transform: uppercase; }
.friend-face-link:hover .friend-face { border-color: var(--cy-cyan); }

.friend-mutual-line {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    padding-bottom: 0.75rem;
    margin-bottom: 0.75rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
/* Overlapped faces, so a row of six reads as "a group you both know" rather
   than as six separate links competing with the list below it. */
.friend-mutual-faces { display: inline-flex; }
.friend-mutual-faces a + a { margin-left: -0.5rem; }
.friend-mutual-faces .friend-face { border-color: var(--cy-surface-1); border-width: 2px; }

.friend-mutual-label {
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
}

/* ---- Dashboard panel ---- */
.friend-panel-empty {
    color: var(--cy-text-dim);
    font-size: 0.82rem;
}
.friend-panel-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0;
    text-decoration: none;
}
.friend-panel-row + .friend-panel-row { border-top: 1px solid rgba(255, 255, 255, 0.05); }
.friend-panel-row:hover .friend-panel-name { color: var(--cy-cyan); }
.friend-panel-name {
    flex: 1;
    min-width: 0;
    color: var(--cy-text);
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 640px) {
    /* Two columns rather than one: the cards are small, and a single column
       turns a short friends list into a long scroll. */
    .friend-grid { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); }
    .friend-tab-trailing { margin-left: 0; }
}

/* ---- Friend action toasts ----
   Fixed to the corner so an action taken halfway down the members grid is
   confirmed where the eye already is, without moving anything on the page. */
.friend-toasts {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    max-width: min(22rem, calc(100vw - 2rem));
    /* The container spans a corner of the viewport; only the toasts themselves
       should intercept the pointer. */
    pointer-events: none;
}
.friend-toasts .friend-flash {
    margin-bottom: 0;
    pointer-events: auto;
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.6);
    animation: friend-toast-in 0.22s ease-out;
}
.friend-flash-leaving { opacity: 0; transition: opacity 0.3s ease; }

@keyframes friend-toast-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .friend-toasts .friend-flash { animation: none; }
}

/* ====================== FEED SCOPE ======================
   Everyone / Friends switch above the community feed, and the friends
   empty state. Hand-written like everything else here: the compiled
   Tailwind bundle is stale and cannot be rebuilt on this server. */

.feed-scope {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
    padding-bottom: 0.85rem;
    border-bottom: 1px solid var(--cy-border);
}

/* Segmented control. One bordered group rather than two loose buttons, so it
   reads as "pick one of these" instead of two unrelated actions. */
.feed-scope-switch {
    display: inline-flex;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
    overflow: hidden;
}

.feed-scope-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.4rem 0.95rem;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-decoration: none;
    transition: color 0.2s ease, background-color 0.2s ease;
}
.feed-scope-btn + .feed-scope-btn { border-left: 1px solid var(--cy-border); }
.feed-scope-btn:hover { color: var(--cy-text); background-color: var(--cy-surface-2); }

.feed-scope-btn.is-active {
    color: var(--cy-cyan);
    background-color: var(--cy-surface-2);
    box-shadow: inset 0 -2px 0 var(--cy-cyan);
}

/* Tells the reader what a refresh will do. The toggle is deliberately
   transient, and without this that reads as the setting failing to save. */
.feed-scope-note {
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
}

.feed-scope-default-form { margin: 0; margin-left: auto; }

.feed-scope-default-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.8rem;
    border: 1px dashed var(--cy-border);
    background: transparent;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    transition: border-color 0.2s ease, color 0.2s ease;
}
.feed-scope-default-btn:hover {
    border-color: var(--cy-cyan);
    border-style: solid;
    color: var(--cy-cyan);
}
/* [hidden] needs help against the display above. */
.feed-scope-default-btn[hidden] { display: none; }


.gothic-feed-empty-hint {
    margin-top: 0.5rem;
    font-size: 0.82rem;
    color: var(--cy-text-dim);
}

@media (max-width: 640px) {
    /* The button drops onto its own line rather than squeezing the switch. */
    .feed-scope-default-form { margin-left: 0; }
    .feed-scope { gap: 0.5rem; }
}

/* Form action rows: primary action first, Cancel to its right, both pushed to
   the right edge. Declared once so every form that grows a Cancel lays it out
   the same way — the reply box, the new-thread form and the inline editors were
   each doing their own thing with text-right and ad-hoc gaps. */
.fm-form-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
    flex-wrap: wrap;
}

/* ====================== READABILITY FLOOR ======================
   A last, deliberately small set of overrides for text that was still hard to
   read after the palette lift. Everything here is a colour or spacing change —
   nothing moves, nothing resizes — so it is safe to adjust or drop wholesale.

   Prefixed with `body` throughout: the compiled Tailwind bundle loads after
   this file and would otherwise win at equal specificity. */

/* Tailwind greys used in a few older views. gray-600 lands at 2.6:1 on a card
   and gray-500 at 4.06:1 — both below AA for body text, and both used for
   things people actually have to read (timestamps, helper lines). Mapped onto
   the theme's own secondary colour rather than picked afresh, so they track any
   future change to --cy-text-dim instead of drifting away from it. */
body .text-gray-600,
body .text-gray-500 { color: var(--cy-text-dim); }

/* Uppercase Orbitron is the house label style, but at label sizes the tracking
   that makes a heading look deliberate makes a sentence hard to scan. Capped
   only where the text is small; headings keep their spacing. */
body .gothic-title { letter-spacing: 0.08em; }

/* Body copy should not glow. A text-shadow on a heading reads as neon; the same
   shadow under a paragraph is a blur that costs legibility for nothing. */
body p,
body li,
body td,
body dd,
body .gothic-activity-action,
body .gothic-activity-post-content,
body .fm-post-body,
body .pm-bubble { text-shadow: none; }

/* Paragraph rhythm. `line-height` on body is inherited, but several components
   set their own tighter value for single-line labels and then get reused for
   sentences. */
body p                        { line-height: 1.6; }
body .fm-post-body            { line-height: 1.7; }
body .gothic-activity-content { line-height: 1.6; }

/* Placeholders sat at browser-default grey on a near-black field, which is the
   one piece of text in a form nobody can read. */
body ::placeholder { color: var(--cy-text-dim); opacity: 1; }

/* Disabled controls dim to near-invisible by UA default on dark backgrounds. */
body :disabled,
body [disabled] { opacity: 0.65; }

/* ====================== FORUM SUB-THREADS ======================
   A reply can hang off a specific post, up to ForumPost::MAX_DEPTH levels.
   Indent communicates the nesting; a left rail carries it when the indent
   itself has to give way on a narrow screen. */

.fm-post-group { margin-bottom: 0.75rem; }

.fm-subthread {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    margin: 0.4rem 0 0 1.5rem;
    /* The rail is what actually says "these belong to the post above" once the
       indent is reduced on small screens. */
    border-left: 2px solid var(--cy-border);
    padding-left: 0.75rem;
}
.fm-subthread[hidden] { display: none; }

.fm-subpost {
    /* Overrides the two-column grid .fm-post uses — a reply has no author
       sidebar, so it stacks. */
    display: block;
    padding: 0.7rem 0.9rem;
    border: 1px solid var(--cy-border);
    background-color: var(--cy-surface-1);
}
.fm-subpost[hidden] { display: none; }

/* Depth is expressed as margin rather than nesting, because the replies are
   flat siblings in the DOM — see forum/partials/sub-post.blade.php. */
.fm-subpost[data-depth="2"] { margin-left: 1.25rem; }
.fm-subpost[data-depth="3"] { margin-left: 2.5rem; }

.fm-subpost-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.4rem;
}

.fm-subpost-avatar {
    width: 24px; height: 24px;
    border-radius: 50%;
    object-fit: cover;
    border: 1px solid var(--cy-border);
    flex-shrink: 0;
}
.fm-subpost-avatar-fallback {
    display: inline-flex; align-items: center; justify-content: center;
    background-color: var(--cy-bg);
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    text-transform: uppercase;
}

.fm-subpost-name {
    font-family: var(--cy-font-hud);
    font-size: 0.78rem;
    letter-spacing: 0.04em;
    color: var(--cy-text);
}

.fm-subpost-date {
    margin-left: auto;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    color: var(--cy-text-dim);
    text-decoration: none;
    white-space: nowrap;
}
.fm-subpost-date:hover { color: var(--cy-cyan); }

.fm-subpost-body { font-size: 0.9rem; }

.fm-subpost-actions {
    margin-top: 0.4rem;
    padding-top: 0.4rem;
    border-top: 1px solid rgba(255, 255, 255, 0.05);
}

/* ---- "Show all N replies" ---- */
.fm-subthread-more {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.7rem;
    border: 1px dashed var(--cy-border);
    background: transparent;
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    cursor: pointer;
    transition: border-color 0.2s ease, color 0.2s ease;
}
.fm-subthread-more:hover { border-color: var(--cy-cyan); color: var(--cy-cyan); border-style: solid; }
.fm-subthread-more[hidden] { display: none; }

/* ---- The shared inline composer, wherever it currently lives ---- */
.fm-inline-reply {
    padding: 0.85rem;
    border: 1px solid var(--cy-cyan);
    background-color: var(--cy-surface-2);
    box-shadow: 0 0 14px rgba(0, 245, 255, 0.12);
}
.fm-inline-reply[hidden] { display: none; }

.fm-inline-reply-target {
    margin: 0 0 0.6rem;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
}
.fm-inline-reply-target i { color: var(--cy-cyan); margin-right: 0.35rem; }
.fm-inline-reply-target strong { color: var(--cy-cyan); }

@media (max-width: 640px) {
    /* Indent gives way before legibility does. The rail keeps saying what the
       margin no longer can — three levels of indent on a phone leaves a column
       of text a few words wide. */
    .fm-subthread { margin-left: 0.5rem; padding-left: 0.5rem; }
    .fm-subpost[data-depth="2"] { margin-left: 0.5rem; }
    .fm-subpost[data-depth="3"] { margin-left: 1rem; }
    .fm-subpost-date { margin-left: 0; width: 100%; }
}

/* ====================== COMMENT REPLY CONTROL ======================
   One look for the Reply control on every comment surface — image galleries,
   music, the story reader. It sits in the header row immediately after the
   author and timestamp, NOT floated to the right-hand edge of the card.

   Right-aligning it put the control as far as physically possible from the
   thing it acts on: on a wide comment you were pairing a name on the left with
   a button on the right and hoping they belonged together. Next to the name it
   needs no hoping. Destructive actions (Delete) still sit apart on the right,
   which is the separation actually worth having. */

.comment-reply-link {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0;
    background: none;
    border: none;
    cursor: pointer;
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    letter-spacing: 0.03em;
    color: var(--cy-cyan);
    opacity: 0.85;
    transition: opacity 0.2s ease, color 0.2s ease;
    /* Baseline-aligned rows are the norm in these headers; without this the
       icon drags the button off the author's baseline. */
    align-self: baseline;
}
.comment-reply-link:hover,
.comment-reply-link:focus-visible { opacity: 1; text-decoration: underline; }

/* ====================== DASHBOARD SECTIONS ======================
   The dashboard is a stack of panels that all looked identical: same border,
   same background, and a header with no separation from the content under it.
   Scanning for "where does Recent Tracks end and Quick Actions begin" meant
   reading, not glancing.

   Nothing here changes a font or a size — the ask was prominence, not scale.
   Three cues do the work: a brighter edge, a rule under the header, and a
   coloured spine down the left that gives each panel an identity. */

.dash-section {
    /* Brighter than --cy-border so a panel edge is a deliberate line rather
       than a suggestion. */
    border-color: rgba(0, 245, 255, 0.32);
}

/* The header row is always the first child — the icon-plus-title bar, or on
   the alerts panel the partial whose root IS that bar. */
.dash-section > div:first-child {
    border-bottom: 1px solid rgba(0, 245, 255, 0.18);
    padding-bottom: 0.7rem;
}

/* A spine down the left edge. Starts below the clipped corner so it does not
   poke out of the card's cut, and fades out so it reads as an accent rather
   than a second border. */
.dash-section::after {
    content: '';
    position: absolute;
    left: 0;
    top: 14px;
    bottom: 0;
    width: 3px;
    background: linear-gradient(180deg, var(--cy-cyan) 0%, rgba(0, 245, 255, 0.12) 65%, transparent 100%);
    pointer-events: none;
}

/* Magenta spine for the panels whose headers are already magenta — the feed,
   tracks and alerts — so the stack is not a wall of identical cyan.
   An explicit class rather than :has() on the header icon: that would have
   tied the panel's colour to whichever icon someone picked next, and changed
   it silently the first time one was swapped. */
.dash-section-alt::after {
    background: linear-gradient(180deg, var(--cy-magenta) 0%, rgba(255, 45, 120, 0.12) 65%, transparent 100%);
}
.dash-section-alt { border-color: rgba(255, 45, 120, 0.3); }
.dash-section-alt > div:first-child { border-bottom-color: rgba(255, 45, 120, 0.18); }
.dash-section-alt:hover { border-color: rgba(255, 45, 120, 0.5); }

.dash-section:hover { border-color: rgba(0, 245, 255, 0.5); }

@media (max-width: 640px) {
    /* Panels stack on a phone, so the boundary between them is doing more
       work — keep the spine but tighten the header rule's breathing room. */
    .dash-section > div:first-child { padding-bottom: 0.55rem; }
}

/* ============== ACCOUNT MENU GROUP TONES (hover + colour) ==============
   Layered ON TOP of the ACCOUNT MENU GROUPS block earlier in this file, which
   still owns the geometry. Kept separate and later so these per-group colours
   win over the generic cyan open-state there without needing !important.

   The four accordion groups in the header's account menu. They open on hover
   (see partials/nav-dropdown-group.blade.php) and each carries its own colour,
   so the menu is scanned by hue rather than read top to bottom.

   Tones follow what the rest of the site already means by them: cyan for
   galleries, magenta for music — as everywhere music appears — green for
   community, yellow for account. */

.nav-group { position: relative; }

/* The group's icon is coloured whether it is open or shut. Colouring only the
   open state would mean the menu looks identical until you are already inside
   the group you were trying to find. */
.nav-group-cyan    > .nav-group-toggle .nav-group-icon { color: var(--cy-cyan); }
.nav-group-magenta > .nav-group-toggle .nav-group-icon { color: var(--cy-magenta); }
.nav-group-green   > .nav-group-toggle .nav-group-icon { color: var(--cy-green); }
.nav-group-yellow  > .nav-group-toggle .nav-group-icon { color: var(--cy-yellow); }

/* Open header, caret and the rail down the expanded items, all in the group's
   own colour. .nav-group-toggle.is-open sets cyan generically; these override
   it per group, and sit later in the file so they win without !important. */
.nav-group-magenta > .nav-group-toggle.is-open { color: var(--cy-magenta); }
.nav-group-green   > .nav-group-toggle.is-open { color: var(--cy-green); }
.nav-group-yellow  > .nav-group-toggle.is-open { color: var(--cy-yellow); }

.nav-group-magenta .nav-group-items { border-left-color: rgba(255, 45, 120, 0.4); }
.nav-group-green   .nav-group-items { border-left-color: rgba(57, 255, 20, 0.4); }
.nav-group-yellow  .nav-group-items { border-left-color: rgba(255, 215, 0, 0.4); }
.nav-group-cyan    .nav-group-items { border-left-color: rgba(0, 245, 255, 0.4); }

/* A tinted wash behind the whole group while it is open, which is what makes
   the boundary between one group and the next obvious at a glance. */
.nav-group-cyan:hover,
.nav-group-cyan:focus-within,
.nav-group-cyan.is-open          { background-color: rgba(0, 245, 255, 0.06); }
.nav-group-magenta:hover,
.nav-group-magenta:focus-within,
.nav-group-magenta.is-open       { background-color: rgba(255, 45, 120, 0.06); }
.nav-group-green:hover,
.nav-group-green:focus-within,
.nav-group-green.is-open         { background-color: rgba(57, 255, 20, 0.06); }
.nav-group-yellow:hover,
.nav-group-yellow:focus-within,
.nav-group-yellow.is-open        { background-color: rgba(255, 215, 0, 0.06); }

/* A spine on the left of the hovered group, matching the dashboard panels. */
.nav-group::before {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 2px;
    background: transparent;
    transition: background-color 0.18s ease;
}
.nav-group-cyan:hover::before,
.nav-group-cyan.is-open::before    { background: var(--cy-cyan); }
.nav-group-magenta:hover::before,
.nav-group-magenta.is-open::before { background: var(--cy-magenta); }
.nav-group-green:hover::before,
.nav-group-green.is-open::before   { background: var(--cy-green); }
.nav-group-yellow:hover::before,
.nav-group-yellow.is-open::before  { background: var(--cy-yellow); }

/* Items inside a group pick up the group's colour on hover rather than the
   global cyan, so a row always tells you which group you are inside. */
.nav-group-magenta .nav-group-item:hover { color: var(--cy-magenta); }
.nav-group-green   .nav-group-item:hover { color: var(--cy-green); }
.nav-group-yellow  .nav-group-item:hover { color: var(--cy-yellow); }

@media (hover: none) {
    /* No hover on a touchscreen — the wash and spine would either never appear
       or stick after a tap. The click handler is what opens groups there. */
    .nav-group:hover:not(.is-open) { background-color: transparent; }
    .nav-group:hover:not(.is-open)::before { background: transparent; }
}

/* ============== ACCOUNT MENU GROUP MOTION ==============
   The slide, the stagger and the hover polish for the account menu accordion.
   Timing lives here; the DELAYS before a group opens or closes are in the
   Alpine scope in layouts/navigation.blade.php — those are behaviour, these
   are presentation. */

.nav-group-items {
    /* max-height rather than height: the natural height is unknown to CSS, and
       a group only ever holds three or four rows so a generous ceiling costs
       nothing. The easing is the standard decelerate curve — it lands softly
       instead of stopping dead. */
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    /* Keeps collapsed links out of the tab order. Delayed on the way out so it
       does not blank the panel mid-collapse. */
    visibility: hidden;
    transform: translateY(-4px);
    transition:
        max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        opacity    0.2s ease,
        transform  0.25s cubic-bezier(0.4, 0, 0.2, 1),
        visibility 0s linear 0.3s;
}

.nav-group-items.is-open {
    max-height: 22rem;
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition:
        max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        opacity    0.22s ease 0.04s,
        transform  0.25s cubic-bezier(0.4, 0, 0.2, 1),
        visibility 0s;
}

/* Rows arrive one after another rather than as a block. Four steps only — the
   groups hold three or four items, and a longer ladder would just feel slow. */
.nav-group-items.is-open .nav-group-item {
    animation: nav-item-in 0.26s cubic-bezier(0.4, 0, 0.2, 1) both;
}
.nav-group-items.is-open .nav-group-item:nth-child(1) { animation-delay: 0.04s; }
.nav-group-items.is-open .nav-group-item:nth-child(2) { animation-delay: 0.09s; }
.nav-group-items.is-open .nav-group-item:nth-child(3) { animation-delay: 0.14s; }
.nav-group-items.is-open .nav-group-item:nth-child(n+4) { animation-delay: 0.19s; }

@keyframes nav-item-in {
    from { opacity: 0; transform: translateX(-6px); }
    to   { opacity: 1; transform: translateX(0); }
}

/* The header nudges right as its group opens, so the group reads as one moving
   piece rather than a static label with a panel appearing under it. */
.nav-group-toggle {
    transition: color 0.2s ease, background-color 0.2s ease, padding-left 0.2s ease;
}
.nav-group.is-open > .nav-group-toggle { padding-left: 1.2rem; }

/* The spine grows from the top rather than snapping in. */
.nav-group::before {
    transform: scaleY(0);
    transform-origin: top;
    transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.18s ease;
}
.nav-group:hover::before,
.nav-group.is-open::before { transform: scaleY(1); }

.nav-group { transition: background-color 0.22s ease; }

@media (prefers-reduced-motion: reduce) {
    /* Keep the state changes, drop the movement. The panel still opens and
       closes — it just does it instantly. */
    .nav-group-items,
    .nav-group-items.is-open,
    .nav-group-toggle,
    .nav-group,
    .nav-group::before { transition: none; }
    .nav-group-items.is-open .nav-group-item { animation: none; }
    .nav-group.is-open > .nav-group-toggle { padding-left: 1rem; }
}

/* Hover bridge for the account menu.
   The panel sits 8px below its button (mt-2). That gap belongs to neither, so
   travelling from one to the other fires mouseleave on the wrapper and starts
   the close timer — the delay covers it, but the state still flickers. This
   invisible strip spans the gap so the pointer never actually leaves.
   Only while open: an always-on strip would sit over whatever is underneath. */
.dropdown-menu::before {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: -0.55rem;
    height: 0.55rem;
}

/* Story panels on the dashboard take the Library's yellow spine, the same way
   .dash-section-alt takes magenta. Declared here beside that rule rather than
   in the page's local <style>, so all three panel tones live together. */
.dash-section-story::after {
    background: linear-gradient(180deg, var(--cy-yellow) 0%, rgba(255, 215, 0, 0.12) 65%, transparent 100%);
}
.dash-section-story { border-color: rgba(255, 215, 0, 0.3); }
.dash-section-story > div:first-child { border-bottom-color: rgba(255, 215, 0, 0.18); }
.dash-section-story:hover { border-color: rgba(255, 215, 0, 0.5); }

/* ============== PROSE THAT WAS WEARING A LABEL'S CLOTHES ==============
   Orbitron, uppercased, letter-spaced is the house style for LABELS — one or
   two words, or a button. It is the wrong dress for a sentence: all-caps
   removes the word-shape cues people actually read by, and tracking pushes the
   letters far enough apart that a phrase stops being one object.

   The earlier readability pass fixed size and contrast and left this alone,
   which is why these still read badly at a perfectly legible 12px.

   THE RULE, for anything added later:
     - one or two words, or a button        -> display font, uppercase, tracked
     - a sentence, a relative time, or ANY
       text containing a member's own words
       (a display name, a title)            -> body font, sentence case

   That last case matters most. A display name forced to uppercase is not the
   name that member chose. */

.feed-scope-note,
.presence-label,
.friend-mutual-label,
.friend-card-meta,
.fm-inline-reply-target,
.bb-quote-who {
    font-family: var(--cy-font-body);
    text-transform: none;
    letter-spacing: normal;
}

/* "Just for now — a refresh returns to your default." A whole sentence, and
   the one that prompted this. */
.feed-scope-note { font-size: 0.8rem; line-height: 1.5; }

/* "Last seen 3 hours ago" / "Online now" — a relative time reads as a phrase,
   not a tag. The green-when-online rule earlier still applies. */
.presence-label { font-size: 0.8rem; }

/* "3 friends in common". */
.friend-mutual-label { font-size: 0.8rem; }

/* Presence line under a member card. */
.friend-card-meta { font-size: 0.78rem; }

/* "Replying to <name>" — contains a display name, so it must not be uppercased.
   The name itself keeps the accent colour it already had. */
.fm-inline-reply-target { font-size: 0.82rem; }

/* The "X said:" citation on a quote. Same reason: it is somebody's name. */
.bb-quote-who { font-size: 0.82rem; font-weight: 600; }

/* The counter / meta line under a page heading — "28 members found, 3 online
   now", "12 conversations", "Blocked members cannot message you…".

   These were text-xs + tracking-widest + font-mono: 12px wide-tracked
   monospace, which is a fine costume for a two-word status tag and a poor one
   for a sentence. Body font at a readable size, normal tracking, and the muted
   colour that everything secondary already uses.

   Deliberately NOT applied to the auth and guest screens, which use the same
   utilities on purpose for their HUD-style status banners and were tuned
   separately. */
.meta-line {
    font-family: var(--cy-font-body);
    font-size: 0.85rem;
    letter-spacing: normal;
    line-height: 1.55;
    color: var(--cy-text-dim);
}

/* ============================================================
   SECONDARY CONTENT: OFF THE FLOOR
   ============================================================

   The readability pass set 0.75rem / 12px as the FLOOR — the smallest text
   allowed anywhere. The forum and PM modules then adopted it as their
   DEFAULT, so 57 rules landed on the smallest permitted size and the dimmest
   permitted colour at the same time, several of them also in Orbitron.

   Contrast alone was not the fault: --cy-text-dim measured 8.17:1, which
   passes AA comfortably. Three things were stacking:

     size    12px is a floor for a tag, not a target for a sentence
     weight  light-on-dark blooms — thin strokes erode in a way the same
             pairing inverted does not, so dark themes need more size or
             weight for equal legibility, and this had neither
     face    Orbitron is a geometric display face with a low x-height and
             uniform strokes; it is built for six characters at 20px, and
             at 12px it stops resolving

   So this is mostly a SIZE and FACE fix with a modest contrast lift, not a
   contrast fix. Appended last, so single-class selectors here beat their
   originals on source order without !important.

   Left deliberately alone: tags, form labels, tab strips, filter and
   mark-all buttons, character counters, avatar-fallback initials, the emoji
   picker's group headings. Those are real labels — small, dim and
   letter-spaced is correct for them, and lifting them would flatten the
   distinction that makes the content read as content. */

/* ---- Carries a name, a relative time, or a sentence: body face ---- */
.fm-forum-last-meta,
.fm-thread-last-meta,
.fm-thread-meta,
.fm-post-date,
.fm-subpost-date,
.fm-post-joined,
.fm-search-hit-meta,
.fm-online-name,
.pm-meta,
.pm-thread-time,
.gothic-activity-time {
    font-family: var(--cy-font-body);
    letter-spacing: normal;
    text-transform: none;
    font-size: 0.82rem;
    color: var(--cy-text-soft);
}

/* ---- Already the body face, just too small and too dim ---- */
.fm-forum-desc,
.fm-category-desc {
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--cy-text-soft);
}

.fm-hint,
.fm-attach-hint,
.friend-hint,
.fm-widget-empty,
.fm-widget-foot,
.fm-post-edited,
.profile-wall-time,
.fm-stat-list dt,
.gothic-notification-empty,
.gothic-notification-loading {
    font-size: 0.82rem;
    color: var(--cy-text-soft);
}

/* Dim AND translucent — the two compounded to roughly 5.5:1, the lowest
   contrast on the site. The forum reference is a navigational cue; it can be
   quiet without being faint. */
.gothic-forum-ref-forum {
    opacity: 1;
    font-size: 0.8rem;
    color: var(--cy-text-dim);
}

/* ---- Keeps the HUD face (stat readouts, controls) — size and tone only ---- */
.fm-forum-stats,
.fm-thread-stats,
.fm-crumbs,
.fm-post-permalink {
    font-size: 0.8rem;
    color: var(--cy-text-soft);
}

/* Post controls. These are the primary verbs on a post — Reply especially —
   and they were rendering fainter than the timestamp beside them. */
.fm-action {
    font-size: 0.8rem;
    color: var(--cy-text-soft);
}

/* Page numbers are targets as much as text. */
nav[aria-label="pagination"] span,
nav[aria-label="pagination"] a {
    font-size: 0.84rem;
}

/* Fixed in the prose pass for face; these bring size and tone into line.
   The green "online" state is set by a three-class selector and still wins. */
.presence-label,
.friend-card-meta,
.friend-mutual-label,
.fm-inline-reply-target {
    color: var(--cy-text-soft);
}

/* The value beside a lifted label. Left on the HUD face and the bright tier —
   "2", "Mar 2025" are readouts and that is what the face is for — but matched
   in size to the dt above, which would otherwise sit larger than its own
   value. */
.fm-post-stats dd { font-size: 0.82rem; }

/* ============================================================
   FORUM: COLOUR THAT MEANS SOMETHING
   ============================================================

   The forum was doing 26 of its 33 accent jobs in cyan — category heads,
   forum icons, unread rows, headings, focus rings, every hover. When one
   colour marks everything, it marks nothing, and the board reads as a single
   undifferentiated wall.

   Colour is assigned to STATE and to GROUPING here, never as decoration:

     cyan     the default / unread / interactive
     gold     pinned — elevated, deliberately placed above the rest
     amber    locked or read-only — a door that is shut
     magenta  staff, and the like heart
     green    presence

   The one rule: if two things look different they must BE different. Nothing
   below tints something merely to break up the grey. */

/* ---- Category zones ----
   Each category gets an accent, applied to its head bar, its title, and the
   icons of the forums inside it — so a category reads as a zone rather than
   as another run of rows.

   USED TO be a four-step rotation keyed by loop position (fm-cat-1..4).
   That broke once a member's own district could be sorted to the front of
   the forum index (see App\Support\Districts / ForumController::index()):
   the same category would then render a DIFFERENT colour depending who was
   looking, since "position 1" stopped meaning the same category for every
   viewer. The accent is now a fixed property of the category itself — set
   inline per category in forum/index.blade.php from
   Districts::findByCategoryId(), reading that district's own colour/wash —
   so it matches the /districts page and never depends on viewer or sort
   order. --fm-accent and --fm-accent-wash below are only the FALLBACK for
   a category with no matching district (none exist today; only relevant if
   a 5th category is ever added without one). */
.fm-category      { --fm-accent: var(--cy-cyan);    --fm-accent-wash: rgba(0,245,255,0.09); }

.fm-category .fm-category-head {
    background: linear-gradient(90deg, var(--fm-accent-wash), transparent);
    border-left-color: var(--fm-accent);
}
.fm-category .fm-category-title { color: var(--fm-accent); }
.fm-category .fm-forum-icon     { color: var(--fm-accent); }

/* The icon carries the zone colour, so it no longer needs to be held back to
   0.75 opacity to avoid competing with a title in the identical colour. */
.fm-category .fm-forum:hover .fm-forum-icon { opacity: 1; }

/* ---- Thread state ----
   Pinned and locked were both rendering in the same neutral as an ordinary
   thread, which meant the icon was the only signal and the tag beside it was
   decoration. */
.fm-thread-pinned .fm-thread-icon { color: var(--cy-yellow-soft); }
.fm-thread-pinned { border-left: 3px solid rgba(255,215,0,0.55); }

.fm-thread-locked .fm-thread-icon { color: var(--cy-amber); }

/* Unread outranks pinned: what you have not read is the more useful thing to
   spot, and a pinned thread is pinned every time you look at it. Declared
   after so it wins on source order at equal specificity. */
.fm-thread-unread { border-left-color: var(--cy-cyan); }

/* ---- Tags ---- */
.fm-tag-pinned {
    color: var(--cy-yellow-soft);
    border-color: rgba(255,215,0,0.45);
    background-color: var(--cy-yellow-dim);
}
.fm-tag-locked {
    color: var(--cy-amber);
    border-color: rgba(255,166,77,0.45);
    background-color: var(--cy-amber-dim);
}
/* Read-only is a restriction, but a mild one — you can still read everything.
   Outlined rather than filled, so it sits below Locked in loudness. */
.fm-tag-readonly {
    color: var(--cy-text-soft);
    border-color: var(--cy-border);
}
.fm-tag-staff { background-color: var(--cy-magenta-dim); }

/* ---- Post controls ----
   Reply / Edit / Delete now separate on hover instead of all going cyan:
   the safe one, the one that changes your own words, and the one that
   destroys. Colour arrives on hover only — a resting toolbar of three
   different colours would shout. */
.fm-edit-toggle:hover { color: var(--cy-yellow-soft); }

/* A like is affection, not a fault. It was painted in the error red used by
   Delete and the attachment failures, which put "somebody liked this" in the
   same visual language as "this went wrong". */

/* ---- Sidebar widgets ----
   Three stacked panels in one colour read as one panel. A tinted title marks
   where each begins; the bodies stay neutral. */
.fm-widget-title-stats  { color: var(--cy-yellow-soft); }
.fm-widget-title-search { color: var(--cy-magenta); }

/* ============================================================
   PRESENCE: THREE STATES
   ============================================================

   Offline was a transparent circle with a thin dim ring — which reads as an
   empty slot, or as a dot that failed to load, rather than as information.
   Red says it.

     online   green   solid, pulsing
     away     yellow  solid, still — no pulse, because the pulse is what
                      signals "here right now" and away is precisely not that
     offline  red     solid, no pulse

   Declared after the originals so these win on source order.

   Colour is not the only channel: every dot carries the state as text in its
   title and aria-label ("Online now" / "Away" / "Last seen 2 hours ago"), and
   the inline variant prints it beside the dot. That matters for the red/green
   pairing in particular, which is the one a colour-blind reader is most
   likely to lose. */

.presence-away {
    background-color: var(--cy-yellow);
    border: none;
    box-shadow: 0 0 6px rgba(255, 215, 0, 0.7);
    animation: none;
}

.presence-offline {
    background-color: var(--cy-error);
    border: none;
    /* Dimmer halo than the other two. Offline is the resting state for most
       of the member list most of the time, and a page of glowing red dots
       would read as a page of alarms. */
    box-shadow: 0 0 5px rgba(255, 58, 58, 0.45);
    animation: none;
}

/* The overlay variant previously repainted offline to a flat surface colour,
   which is what produced the empty circle over the avatar. */
.presence-dot-overlay.presence-offline {
    background-color: var(--cy-error);
    border-color: var(--cy-surface-1);
}
.presence-dot-overlay.presence-away {
    background-color: var(--cy-yellow);
    border-color: var(--cy-surface-1);
}

/* Label tone follows the dot for the two live states. Offline is left in the
   ordinary secondary colour on purpose — the text there is "Last seen 3 hours
   ago", and colouring an ordinary fact red would read as a warning about the
   member rather than a statement about their presence. */
.presence-inline .presence-away + .presence-label {
    color: var(--cy-yellow-soft);
}

/* ---- Floating tooltips ----
   Same .gothic-tooltip component the header icon buttons use, in the form that
   is positioned from <body> by script rather than nested inside its trigger.

   TWO SCRIPTS USE THIS: presence.js, for the avatar dots (.presence-floating-tip),
   and tooltips.js, which replaces every native title="" tooltip on the site
   (.gothic-floating-tip). Both share the rules below, so the header buttons, a
   presence dot and an ordinary title all look like one component.

   IT CANNOT LIVE INSIDE THE CARD. .gothic-card carries the cut-corner
   clip-path that gives the site its shape, and a clip-path clips every
   descendant — unconditionally, with no z-index able to escape it. A tooltip
   anchored to an avatar near a card's edge was therefore being sliced off by
   the card's own bevel. (.gothic-card:hover also sets a transform, which
   creates a stacking context and would have trapped it a second way even
   without the clip.)

   So presence.js keeps ONE tooltip element as a direct child of <body>,
   positioned to the hovered avatar. Outside every card, so outside every
   clip-path, overflow and stacking context on the site.

   It is revealed by hovering the AVATAR, not the dot: the dot is about 13px
   across, which is a fiddly target, and enlarging its hit area would mean it
   swallowed clicks meant for the profile link underneath. */
.presence-floating-tip,
.gothic-floating-tip {
    position: fixed;
    top: 0;
    left: 0;
    /* .gothic-tooltip supplies transform: translateX(-50%), so the `left` the
       script sets is read as the trigger's CENTRE. */
}

.presence-floating-tip.is-visible,
.gothic-floating-tip.is-visible {
    opacity: 1;
}

/* Flipped above the trigger when there is no room below — the arrow has to
   turn over with it. */
.presence-floating-tip.is-above::after,
.gothic-floating-tip.is-above::after {
    bottom: auto;
    top: 100%;
    border-bottom-color: transparent;
    border-top-color: rgba(0, 245, 255, 0.3);
}

/*
   The generic one takes any text a title attribute happened to hold, which is
   not always the two or three words a header button carries. .gothic-tooltip is
   nowrap — right for a label, wrong for a sentence, which would run off the
   screen with only the horizontal clamp between it and the edge. So this one
   wraps, inside a sensible measure.

   Everything else about it — font, colour, border, glow, uppercase — is
   inherited from .gothic-tooltip unchanged, because looking identical to the
   header buttons is the entire point.
*/
.gothic-floating-tip {
    white-space: normal;
    max-width: min(22rem, calc(100vw - 2rem));
    text-align: center;
    line-height: 1.45;
}

/* ---- Wordmark ----
   The logo is a 430x115 RGBA PNG carried over from the old site, and it was
   drawn for a WHITE background. Measured against the navbar's #05050f, every
   part of it failed the 3:1 floor for a meaningful graphic:

       OUR   #9c1318   2.43:1
       DARK  #ba1b1e   3.15:1
       HOME  #634044   2.26:1

   brightness(1.8) is the sweet spot — it lifts all three to 5.3-5.5:1, evenly,
   without the colour shift that a stronger push causes (at 2.2 the "HOME"
   greys turn pink, which changes the artwork rather than just revealing it).

   Done in CSS rather than by re-exporting because this is the largest copy of
   the file that survives; if a higher-resolution or light-background original
   ever turns up, drop the filter and use it directly.

   width/height are on the element so the row does not reflow while the image
   loads. */
.nav-logo-img {
    height: 40px;
    width: auto;
    display: block;
    filter: brightness(1.8) drop-shadow(0 0 6px rgba(255, 45, 60, 0.35));
    transition: filter 0.3s ease;
}

.nav-logo-img-guest { height: 34px; }

/* The text logo used a text-shadow on hover, which does nothing to an image.
   A slightly hotter glow reads as the same gesture. */
.nav-logo:hover .nav-logo-img {
    filter: brightness(2) drop-shadow(0 0 10px rgba(255, 45, 60, 0.6));
}

@media (max-width: 480px) {
    .nav-logo-img { height: 32px; }
}

/* ============================================================
   WORDMARK — set in type, not an image
   ============================================================

   Replaces the PNG. The image was 430x115 and could not be enlarged past its
   native size without going soft, which is the whole reason for this.

   Nosifer carries the drips in the glyph outlines, so the blood shapes come
   from the font rather than from a stack of shadows. It is the closest thing on
   Google Fonts to the original artwork. Added to the EXISTING font URL, so
   there is no extra connection — same request, one more family.

   COLOURS ARE MEASURED, not sampled from the artwork. The original's own values
   fail badly on this background:

       original red   #ba1b1e   3.15:1
       original grey  #634044   2.26:1   <- effectively invisible

   These clear 4.5:1 while keeping the red/neutral split the artwork had:

       --brand-red    #ef2b31   4.88:1
       --brand-grey   #c9c9d2  12.30:1

   ONE KNOB: --brand-size. Everything scales from it. Change that and the whole
   mark grows or shrinks — there is no second place to edit. */
.brand-mark {
    --brand-size: 2.1rem;
    --brand-red:  #ef2b31;
    --brand-grey: #c9c9d2;

    font-family: 'Nosifer', Impact, 'Arial Black', sans-serif;
    font-size: var(--brand-size);
    /* Nosifer's drips hang below the baseline; line-height 1 would clip them. */
    line-height: 1.35;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    white-space: nowrap;
    display: inline-block;
    user-select: none;
}

.brand-mark-a { color: var(--brand-red); }
.brand-mark-b { color: var(--brand-grey); }

/* A wet, lit look — the artwork's gloss, without a second image. Kept subtle;
   a heavy glow on a face this busy turns to mush at small sizes.

   THIS IS ALSO THE FALLBACK. Everything below in the @supports block replaces
   these rules on browsers that can clip a gradient to text. Anything that
   cannot keeps exactly this, which is the version that shipped at launch — it
   must stay a complete, working wordmark on its own. */
.brand-mark-a { text-shadow: 0 0 10px rgba(239, 43, 49, 0.45), 0 1px 2px rgba(0,0,0,0.8); }
.brand-mark-b { text-shadow: 0 0 8px rgba(201, 201, 210, 0.22), 0 1px 2px rgba(0,0,0,0.8); }

.brand-mark-guest { --brand-size: 1.8rem; }

/*
   EDGE EROSION — the bit CSS cannot do at all.

   Shadows, gradients and masks all work on a CLEAN vector outline. Nosifer's
   edges stay razor sharp underneath every one of them, which is why the mark
   kept reading as new no matter how much grime went on top. feDisplacementMap
   pushes that outline around with noise, so the letters come out pitted and
   chewed — print that has been through something.

   Applied to the PARENT so the children's drop-shadows get roughened with the
   glyphs; filtering them separately would leave crisp shadows under ragged
   letters, which reads as a rendering fault rather than as wear.

   Guarded on the partial actually being on the page. If partials/brand-filters
   is ever dropped from a layout, filter: url() on a missing id makes the element
   INVISIBLE in some browsers rather than un-filtered — so this only applies
   where the markup is known to be present, and both layouts that show the
   wordmark include it.

   No @supports for SVG filters exists, so the mitigation is the scale value:
   at 2.2 an unexpected rendering is still a readable wordmark.
*/
.nav-logo .brand-mark,
.brand-mark-guest {
    filter: url(#brand-rough);
}

/* Matches the gesture the old text logo had on hover. */
.nav-logo:hover .brand-mark-a { text-shadow: 0 0 16px rgba(239, 43, 49, 0.8), 0 1px 2px rgba(0,0,0,0.8); }
.nav-logo:hover .brand-mark-b { text-shadow: 0 0 14px rgba(201, 201, 210, 0.45), 0 1px 2px rgba(0,0,0,0.8); }

/* ------------------------------------------------------------------
   DEPTH PASS — lit crest, heavy shadow, congealed foot
   ------------------------------------------------------------------

   Same face, same two colours, more dimension and considerably more weight.
   Three things are happening:

   1. A GRADIENT down each word, lit at the crest and settling to the measured
      colour by the baseline. This is what gives Nosifer's drips somewhere to
      go — a flat fill makes them read as noise, a lit top makes them read as
      running off the letter.

      *** THE GRADIENT ONLY EVER GOES LIGHTER. *** Measured on #05050f:

          red   #ff5257  6.37:1  (crest)  ->  #ef2b31  4.89:1  (baseline, the floor)
          grey  #e8e8f0 16.65:1  (crest)  ->  #c9c9d2 12.34:1  (baseline, the floor)

      Every darker red was tested and every one fails AA on this background —
      #e0242a is 4.30:1, #cf1f26 is 3.75:1. So the documented floor stays the
      floor and the depth is bought entirely above it. Do not be tempted to
      deepen the foot; that is the one direction this cannot go.

   2. HEAVY BLACK SHADOW under both words. This is where "darker" actually
      comes from: a hard 1px seat plus a soft spread, so the mark sits IN the
      bar rather than floating on it. Weight, not dimness.

   3. A TIGHTER, DEEPER HALO. The old glow was wide and bright and read as neon.
      This one is closer in and drawn in a deeper red, so it reads as light
      coming off something wet.

   filter: drop-shadow(), NOT text-shadow — text-shadow paints BEHIND the glyph,
   and the glyph here is transparent with a gradient clipped into it, so the
   shadows would show straight through the letters. drop-shadow follows the
   alpha silhouette instead, drips included. text-shadow is explicitly cleared
   for the same reason. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
    .brand-mark-a,
    .brand-mark-b {
        -webkit-background-clip: text;
        background-clip: text;
        -webkit-text-fill-color: transparent;
        color: transparent;
        text-shadow: none;
        transition: filter 0.3s ease;
    }

    /*
       AGED, not polished.

       The colours shift from fresh red / cool grey to RUST and BONE. That is
       the single biggest part of the effect: #ef2b31 is the red of new paint,
       and nothing done to its edges will stop it looking new.

       *** THE RED IS RUNNING CLOSE TO THE CONTRAST LIMIT. *** Measured on
       #05050f, AA needs 4.50:1, and the foot now sits at 4.64:1 — there is
       almost nothing left to give. The next steps down were tested and fail:

           #ce4634  4.40:1  FAILS      #cc4433  4.29:1  FAILS

       Current stops:

           rust crest  #e85c4a  5.86:1     bone crest  #d6cfc2  13.10:1
           rust mid    #e05340  5.30:1     bone mid    #cdc6ba  11.96:1
           rust foot   #d34a38  4.64:1     bone foot   #c4bdb2  10.89:1

       If a darker red is ever wanted again, the letterform cannot supply it —
       the nav background would have to lighten, or the mark needs a backing
       plate to sit on. Do not simply lower these values.

       The gradient runs light at the TOP and dusty at the FOOT, which is how
       pigment actually fades: the crest catches light, the drips sit in shadow
       and dry darker.
    */
    .brand-mark-a {
        background-image: linear-gradient(180deg,
            #e85c4a 0%,
            #e05340 45%,
            #d34a38 100%);
        filter:
            drop-shadow(0 1px 0 rgba(0,0,0,0.95))
            drop-shadow(0 3px 7px rgba(0,0,0,0.9))
            drop-shadow(0 0 7px rgba(150, 38, 22, 0.5));
    }

    .brand-mark-b {
        background-image: linear-gradient(180deg,
            #d6cfc2 0%,
            #cdc6ba 45%,
            #c4bdb2 100%);
        filter:
            drop-shadow(0 1px 0 rgba(0,0,0,0.95))
            drop-shadow(0 3px 7px rgba(0,0,0,0.9))
            drop-shadow(0 0 6px rgba(120, 112, 96, 0.35));
    }

    /*
       WEAR — scratches taken out of the ink.

       A mask, because this has to REMOVE pigment. Everything CSS offers besides
       masking only adds to the outside of the glyph, which is exactly why the
       previous pass looked polished however heavy the shadows got.

       Partial alphas rather than full transparency: these thin the ink rather
       than punching holes through it, so the letters stay readable at 1.25rem
       on a phone while still reading as scuffed.

       ONE mask layer only, deliberately. Multiple mask-image layers composite
       with `add` by default, which unions the visible areas and fills back in
       every gap the other layer made — two layers here would cancel each other
       out and look untouched. The irregularity comes from uneven stops inside a
       single repeat instead.

       DENSITY: six scratches per 38px, up from three per 44px — roughly
       two and a half times as many. The alphas stay in the 0.15–0.55 range on
       purpose. Denser scratches are fine because each one only thins the ink;
       push these toward 0 and the extra count starts perforating the thin
       strokes of Nosifer instead, and HOME is the first thing to fall apart at
       1.25rem on a phone. If more wear is wanted, add BANDS before deepening
       the ones that are there.
    */
    .brand-mark-a,
    .brand-mark-b {
        --brand-wear: repeating-linear-gradient(101deg,
            #000             0px,  #000             4px,
            rgba(0,0,0,0.25) 4px,  rgba(0,0,0,0.25) 5px,
            #000             5px,  #000            10px,
            rgba(0,0,0,0.55) 10px, rgba(0,0,0,0.55) 11px,
            #000            11px,  #000            15px,
            rgba(0,0,0,0.15) 15px, rgba(0,0,0,0.15) 16px,
            #000            16px,  #000            21px,
            rgba(0,0,0,0.45) 21px, rgba(0,0,0,0.45) 23px,
            #000            23px,  #000            27px,
            rgba(0,0,0,0.3)  27px, rgba(0,0,0,0.3)  28px,
            #000            28px,  #000            33px,
            rgba(0,0,0,0.5)  33px, rgba(0,0,0,0.5)  34px,
            #000            34px,  #000            38px);
        -webkit-mask-image: var(--brand-wear);
        mask-image: var(--brand-wear);
    }

    /* Hover lifts the halo only. The black stays put — the mark should look lit,
       not levitating. */
    .nav-logo:hover .brand-mark-a {
        text-shadow: none;
        filter:
            drop-shadow(0 1px 0 rgba(0,0,0,0.95))
            drop-shadow(0 3px 7px rgba(0,0,0,0.9))
            drop-shadow(0 0 13px rgba(239, 43, 49, 0.85));
    }
    .nav-logo:hover .brand-mark-b {
        text-shadow: none;
        filter:
            drop-shadow(0 1px 0 rgba(0,0,0,0.95))
            drop-shadow(0 3px 7px rgba(0,0,0,0.9))
            drop-shadow(0 0 11px rgba(201, 201, 210, 0.6));
    }

    /* Stacked drop-shadows are the expensive part of this, and the hover
       transition is the only thing that animates them. Anyone who asked for
       less motion gets the mark exactly as it sits. */
    @media (prefers-reduced-motion: reduce) {
        .brand-mark-a, .brand-mark-b { transition: none; }
    }
}

@media (max-width: 900px) { .brand-mark { --brand-size: 1.6rem; } }
@media (max-width: 480px) { .brand-mark { --brand-size: 1.25rem; } }

/* ================== GALLERY CATEGORY FILTER BAR ==================
   The category tabs on the community gallery, stuck under the fixed nav so
   they stay reachable while scrolling a long list of galleries.

   THE ONE THING THAT WILL BREAK THIS: position:sticky is inert inside any
   ancestor with overflow:hidden — the element dutifully sticks to a scrollport
   that never scrolls. The card wrapping this bar was overflow-hidden, so the
   Blade switches that one wrapper to overflow:clip, which clips identically
   but does NOT create a scroll container. If you ever put the bar inside
   another hidden-overflow box, it will silently stop sticking with no error.

   `top` matches the fixed nav's height: .sticky-nav is the h-16 (64px) bar,
   the same 64px that body's padding-top compensates for. If the nav height
   changes, this changes with it.

   Styled here rather than with Tailwind utilities because the compiled bundle
   in public/build is stale and there is no npm on this box to rebuild it — a
   utility that isn't already in it is silently inert. This file is loaded with
   a ?v=time() cache-buster, so edits show up immediately. */
.gallery-filter-bar {
    position: -webkit-sticky;
    position: sticky;
    top: 64px;
    z-index: 20;

    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.5rem;

    /* The card pads its contents by 1.5rem (p-6). Pulling out by exactly that
       and padding back in makes the bar's background span the full card width,
       so covers passing underneath are covered edge to edge instead of showing
       a strip down either side. */
    margin-left: -1.5rem;
    margin-right: -1.5rem;
    padding: 0.75rem 1.5rem;

    /* Fully opaque, not a backdrop blur: the covers sliding underneath are
       busy, and blurring a full-width bar over a long image grid is exactly
       the kind of thing that costs frames while scrolling. */
    background: rgba(5, 5, 18, 0.97);
    border-bottom: 1px solid rgba(0, 245, 255, 0.18);
}

/* On a phone a wrapping bar of a dozen categories is four or five rows deep —
   stuck to the top, that eats most of the viewport it exists to help you
   scroll. One row that scrolls sideways instead. */
@media (max-width: 640px) {
    .gallery-filter-bar {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
    }
    .gallery-filter-bar::-webkit-scrollbar { display: none; }
    .gallery-filter-bar > * { flex: 0 0 auto; }
}

/* ================== FORUM POST REACTIONS ==================
   Tally chips plus a shared picker, on ROOT posts only — sub-thread replies
   keep the single heart.

   Colour follows the board's existing semantics: cyan is the default "there is
   something here" tone, and magenta is reserved for staff and for a like. A
   reaction the reader has pressed themselves is the one that goes magenta, so
   "mine" reads the same way a liked post always did.

   The glyphs animate via the .ae-* classes already defined for private-message
   emoji, so nothing new animates and reduced-motion is already handled where
   those keyframes are declared. */
.fm-reactions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.75rem;
}

.fm-react-chip,
.fm-react-add {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.5rem;
    border-radius: 999px;
    border: 1px solid var(--cy-border);
    background: rgba(0, 245, 255, 0.05);
    color: var(--cy-text-dim);
    font-family: var(--cy-font-hud);
    font-size: 0.75rem;
    line-height: 1.6;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.fm-react-chip:hover,
.fm-react-add:hover {
    border-color: rgba(0, 245, 255, 0.55);
    background: rgba(0, 245, 255, 0.12);
    color: var(--cy-cyan);
}

/* The reader's own reaction. Magenta, matching what a liked post has always
   looked like on this board. */
.fm-react-chip.is-mine,
.fm-react-add.is-mine {
    border-color: rgba(255, 45, 120, 0.6);
    background: rgba(255, 45, 120, 0.14);
    color: var(--cy-magenta);
}

/* Guests see the tallies but cannot press them, so drop the affordances rather
   than dimming the text — the counts still have to be readable. */
.fm-react-chip[disabled] {
    cursor: default;
    opacity: 1;
}
.fm-react-chip[disabled]:hover {
    border-color: var(--cy-border);
    background: rgba(0, 245, 255, 0.05);
    color: var(--cy-text-dim);
}

.fm-react-n { font-variant-numeric: tabular-nums; }

.fm-react-add { padding: 0.15rem 0.45rem; }

/* ---- the picker ----
   Reparented to <body> on open (see initReactions in forum.js), so it is
   positioned against the viewport and z-index only has to beat the page, not
   the clipped card it came from. Below the nav's 1000 on purpose. */
.fm-react-menu {
    position: fixed;
    z-index: 900;
    display: grid;
    /* Same 6-column glyph grid as .pm-emoji-grid, so this picker reads as the
       same control as the emoji one instead of a bespoke list. */
    grid-template-columns: repeat(6, 1fr);
    gap: 0.25rem;
    padding: 0.45rem;
    width: 296px;
    border: 1px solid rgba(0, 245, 255, 0.35);
    border-radius: 8px;
    background: rgba(5, 5, 18, 0.98);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.75);
    /* Same cap as .pm-emoji-menu/.cm-emoji-menu — thirteen entries fit in
       three rows at six columns, but a short viewport still needs the guard. */
    max-height: min(24rem, 70vh);
    overflow-y: auto;
}

.fm-react-menu[hidden] { display: none; }

/* Icon-only grid cell — the label lives in title/aria-label now rather than
   as visible text, same tradeoff .pm-emoji-pick already makes. */
.fm-react-pick {
    border: 1px solid transparent;
    border-radius: 5px;
    background: transparent;
    padding: 0.4rem 0;
    line-height: 1;
    cursor: pointer;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.fm-react-pick:hover {
    border-color: var(--cy-cyan);
    background-color: rgba(0, 245, 255, 0.1);
}

/* Already pressed — the picker doubles as a display of what you chose. */
.fm-react-pick.is-mine {
    border-color: rgba(255, 45, 120, 0.6);
    background-color: rgba(255, 45, 120, 0.14);
}

.fm-react-pick .ae { font-size: 1.25rem; line-height: 1; }
.fm-react-chip .ae { font-size: 1rem; line-height: 1; }

/* Reactions on a sub-thread reply. Same control, less of it: a reply is a
   sentence or two, and the chips should not outweigh what they are attached
   to. The picker itself is shared and unchanged. */
.fm-subpost .fm-reactions {
    margin-top: 0.5rem;
    gap: 0.3rem;
}

.fm-subpost .fm-react-chip,
.fm-subpost .fm-react-add {
    padding: 0.1rem 0.4rem;
    font-size: 0.7rem;
}

.fm-subpost .fm-react-chip .ae { font-size: 0.9rem; }

/* ================== PROFILE PROMPT ANSWERS ==================
   The "About you" replies on a profile, in place of the old social icon row.

   A definition list because that is what this is: a prompt and its answer. It
   also means a screen reader announces the two as a pair rather than as two
   unrelated runs of text.

   Two columns on a wide screen, one on a narrow one — several long answers in
   a single column push the profile tabs a long way down the page. */
.profile-answers {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 0.9rem 1.5rem;
    margin-top: 1.25rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--cy-border);
}

.profile-answer dt {
    font-family: var(--cy-font-hud);
    font-size: 0.7rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--cy-cyan);
    margin-bottom: 0.25rem;
}

.profile-answer dd {
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--cy-text-soft);
    /* A pasted URL or a very long single word must not widen the grid column
       and push the whole profile sideways. */
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* ================== WALL THREADING ==================
   "Reply" beside every name, and one reply box per thread that retargets. */

.profile-wall-reply-btn {
    background: none;
    border: 0;
    padding: 0;
    font-family: var(--cy-font-hud);
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
    cursor: pointer;
    transition: color 0.15s ease;
}

.profile-wall-reply-btn:hover,
.profile-wall-reply-btn:focus-visible { color: var(--cy-cyan); }

/* "↰ Name" — shown only when a reply answers another REPLY rather than the
   post it already sits under, where it would state the obvious. */
.profile-wall-replyto {
    font-size: 0.7rem;
    color: var(--cy-text-dim);
    white-space: nowrap;
}

/*
   The name is NOT a link and must not look like one. It carried a dotted
   underline and a hover colour, both of which promise a destination — and the
   post it pointed at was usually a few lines up, so the jump was invisible and
   clicking read as broken.

   Magenta because every real link on this page is cyan. Borrowing the link
   colour for a non-link is what caused the confusion; the other accent cannot
   be mistaken for one. No underline, no hover, default cursor.
*/
.profile-wall-replyto-name {
    color: var(--cy-magenta);
    font-weight: 600;
    cursor: default;
}

.profile-wall-reply-form {
    margin-top: 0.75rem;
    padding-top: 0.75rem;
    border-top: 1px dashed var(--cy-border);
}

.profile-wall-replying {
    font-size: 0.72rem;
    color: var(--cy-text-dim);
    margin-bottom: 0.4rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.profile-wall-replying strong { color: var(--cy-cyan); font-weight: 600; }

.profile-wall-reply-cancel {
    background: none;
    border: 0;
    padding: 0;
    margin-left: auto;
    font-size: 0.7rem;
    color: var(--cy-text-dim);
    cursor: pointer;
    text-decoration: underline;
}

.profile-wall-reply-cancel:hover { color: var(--cy-magenta); }

/* The brief flash on a post arrived at from a notification. Scrolling to a
   small block of text on a long wall is not the same as finding it. */
.profile-wall-flash-target {
    animation: profile-wall-flash 2.6s ease-out 1;
}

@keyframes profile-wall-flash {
    0%, 55% { background: rgba(0, 245, 255, 0.14); box-shadow: 0 0 0 1px rgba(0, 245, 255, 0.35); }
    100%    { background: transparent;             box-shadow: 0 0 0 1px transparent; }
}

@media (prefers-reduced-motion: reduce) {
    /* Still marked, just not animated — the point is finding it, not the fade. */
    .profile-wall-flash-target { animation: none; background: rgba(0, 245, 255, 0.12); }
}


/* ============ PROFILE: RECENT FORUM POSTS ============
   Rows in the Forum tab. Deliberately quieter than the gallery rows next to
   them — a post is a pointer to a conversation, not a piece of work. */
.profile-forum-post {
    padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--cy-border);
}

.profile-forum-post:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.profile-forum-title {
    display: block;
    font-size: 0.95rem;
    line-height: 1.4;
    /* A thread title can be long and unbroken; it must not widen the panel. */
    overflow-wrap: anywhere;
}

.profile-forum-meta {
    margin-top: 0.15rem;
    font-family: var(--cy-font-hud);
    font-size: 0.7rem;
    letter-spacing: 0.04em;
    color: var(--cy-text-dim);
}

/* ============ ANNOUNCEMENT ATTRIBUTION ============
   The "— Drac, Site Admin and Creator" line under an announcement. Says who an
   official notice came from, so it is distinguishable at a glance from the
   ordinary traffic around it.

   Bright green with a glow, by request. Note this is the one place green is used
   for something other than presence on this site — online dots, "online" labels
   and the friend-accept button are all green.

   BRIGHTNESS COMES FROM THREE PLACES, having been asked for three times:

     1. The fill. #b6ffab, up from #7dff70 and #a8f5a0 before it. This one did
        finally have to go pale. Luminance in this hue peaks around pure green,
        so past that point a brighter green IS a whiter one — there is no more
        saturated green available that is also more luminous. It is still
        plainly green, just no longer neon.
     2. Weight. 700, up from normal. More lit pixels per glyph is the cheapest
        real brightness there is at 12px, and it costs no colour.
     3. A three-layer glow: tight bright core, mid halo, wide faint wash. One
        big blur would smear HUD text instead of lighting it.

   NO opacity here, deliberately. This used to carry opacity: 0.85 to sit quieter
   than the message it follows — but the notification ITEM used to dim itself
   once read, and opacity multiplies down the tree, so the byline landed at
   0.6 x 0.85 = 0.51 effective and blended halfway into the background.

   [!] THAT PARENT DIMMING IS NOW SUPPRESSED FOR ANNOUNCEMENTS, in both
       notification templates — see the note beside the read/unread classes
       there. It was the single biggest thing holding this line down, and no
       amount of fill or glow here could undo it, because a child cannot
       cancel an ancestor's opacity.

   Hierarchy against the message above is carried by SIZE — the message is
   larger — rather than by dimming, because size does not compound with a
   parent's state styling the way opacity does.

   [!] The admin compose screen draws its own copy of this line, to preview it.
       The panel does NOT load this stylesheet, so that copy lives in a
       @push('head') block in admin/announcements/index.blade.php and has to be
       changed alongside this rule or the preview starts lying. */
.notification-byline {
    margin-top: 0.3rem;
    font-family: var(--cy-font-hud);
    /* 12px, the site-wide floor for readable text. 0.7rem was 11.2px. */
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--cy-green-soft);
    text-shadow:
        0 0 6px  rgba(182, 255, 171, 0.75),
        0 0 14px rgba(57, 255, 20, 0.45),
        0 0 28px rgba(57, 255, 20, 0.20);
}

/* ------------------------------------------------------------------------
   Notification preference toggles (Account -> Preferences).

   Drawn entirely by hand rather than relying on native checkbox rendering:
   see [[odh-invisible-checkbox]] in memory — Tailwind's forms plugin sets
   appearance:none on [type=checkbox] and the theme's own border tokens made
   the resulting bare border nearly invisible against dark panels. This
   control never shows the native box at all, so that whole failure mode
   cannot recur here. The real <input> stays in the DOM, full size and
   opacity:0, stacked over the drawn track — it is what makes the control
   keyboard-operable and what :checked/:disabled/:focus-visible below key off.
   ------------------------------------------------------------------------ */
.gothic-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
    width: 2.75rem;
    height: 1.5rem;
    flex-shrink: 0;
}

.gothic-toggle input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.gothic-toggle-track {
    position: absolute;
    inset: 0;
    border-radius: 999px;
    background-color: var(--cy-surface-3);
    border: 1px solid var(--cy-border);
    transition: background-color 0.15s ease, border-color 0.15s ease;
    pointer-events: none;
}

.gothic-toggle-thumb {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    background-color: var(--cy-text-dim);
    transition: transform 0.15s ease, background-color 0.15s ease;
    pointer-events: none;
}

/* Green reads as "on" site-wide — see [[odh-presence-three-state]] and
   [[odh-forum-colour-semantics]] — rather than inventing a new meaning for
   cyan or magenta here. */
.gothic-toggle input:checked ~ .gothic-toggle-track {
    background-color: var(--cy-green-dim);
    border-color: var(--cy-green);
}

.gothic-toggle input:checked ~ .gothic-toggle-thumb {
    transform: translateX(1.25rem);
    background-color: var(--cy-green);
    box-shadow: 0 0 6px var(--cy-green);
}

.gothic-toggle input:focus-visible ~ .gothic-toggle-track {
    outline: 2px solid var(--cy-cyan);
    outline-offset: 2px;
}

.gothic-toggle input:disabled {
    cursor: not-allowed;
}

.gothic-toggle input:disabled ~ .gothic-toggle-track {
    opacity: 0.45;
}

/* The Site/E-mail table itself: a CSS grid so the label column can be wide
   and the two switch columns stay narrow and aligned, on one row per content
   type. Wraps to stacked rows below 480px rather than scrolling sideways. */
.gothic-notif-table {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--cy-border);
    border-radius: 0.5rem;
    overflow: hidden;
}

.gothic-notif-row {
    display: grid;
    grid-template-columns: 1fr 5rem 5rem;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.9rem;
    border-top: 1px solid var(--cy-border);
}

.gothic-notif-row:first-child {
    border-top: none;
}

.gothic-notif-row-head {
    background-color: var(--cy-surface-2);
    padding-top: 0.45rem;
    padding-bottom: 0.45rem;
}

.gothic-notif-row-head .gothic-notif-row-col {
    font-size: 0.7rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
    justify-content: center;
}

.gothic-notif-row-label {
    font-size: 0.85rem;
    color: var(--cy-text);
}

.gothic-notif-row-col {
    display: flex;
    justify-content: center;
}

@media (max-width: 480px) {
    .gothic-notif-row {
        grid-template-columns: 1fr;
        row-gap: 0.5rem;
        text-align: left;
    }

    .gothic-notif-row-head {
        display: none;
    }

    .gothic-notif-row-col {
        justify-content: flex-start;
    }

    .gothic-notif-row-col::before {
        content: attr(data-label);
        font-size: 0.7rem;
        color: var(--cy-text-dim);
        margin-right: 0.5rem;
        width: 3.5rem;
    }
}

/* ====================== DISTRICTS ====================== */

/* ---- Badge ----
   A member's chosen city district: icon + name, from partials/district-
   badge.blade.php, used on the profile header and the username hover card.
   --district-color is set inline per-district (one of the --cy-* tokens
   from App\Support\Districts), so this one rule covers all 4 without a
   modifier class per district — same trick .fm-tag-* uses per state, just
   driven by a custom property instead, since the set of colours lives in
   PHP, not here.

   Normal case, not the uppercase HUD styling .fm-tag carries — a district
   name is an identity to read, not a status/permission marker. */
.district-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.15rem 0.65rem;
    border: 1px solid var(--district-color, var(--cy-border));
    border-radius: 999px;
    font-size: 0.75rem;
    color: var(--district-color, var(--cy-text-dim));
    white-space: nowrap;
}
.district-badge i { font-size: 0.7rem; }

/* ---- Dedicated page (resources/views/districts/index.blade.php) ----
   Same flex-wrap-cards technique the onboarding modals use for laying out
   several same-shaped options, just wider and richer since this is a full
   page rather than a modal. */
.district-page-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    margin-top: 0.5rem;
}

.district-card {
    flex: 1 1 320px;
    display: flex;
    flex-direction: column;
    padding: 1.5rem;
    border-radius: 0.5rem;
    background: var(--cy-surface-1);
    border: 1px solid var(--district-color, var(--cy-border));
}

/* The member's current pick gets the same glow-ring treatment .lb-thumb
   uses for "this is the one you're on" — a filled ring rather than a
   second badge, so there's exactly one way to tell at a glance. Solid
   colour twice (a sharp ring, then a blurred one) rather than color-mix()
   for the fade: a blurred box-shadow already fades to transparent on its
   own, so there's no need for an rgba/dim companion per district colour. */
.district-card-current {
    box-shadow: 0 0 0 2px var(--district-color), 0 0 18px var(--district-color);
}

.district-card-icon {
    font-size: 1.75rem;
    color: var(--district-color, var(--cy-cyan));
    margin-bottom: 0.5rem;
}

.district-card-title {
    font-family: var(--cy-font-hud);
    font-size: 1rem;
    font-weight: 700;
    color: var(--cy-text);
    margin-bottom: 0.6rem;
}

.district-card-blurb {
    font-size: 0.9rem;
    line-height: 1.6;
    color: var(--cy-text-soft);
    flex: 1 1 auto;
}

.district-card-forums {
    margin-top: 1rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--cy-border);
}

.district-card-forums-label {
    display: block;
    font-family: var(--cy-font-hud);
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--cy-text-dim);
    margin-bottom: 0.4rem;
}

.district-card-forums ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.9rem;
    font-size: 0.82rem;
}

.district-card-form {
    margin-top: 1.25rem;
}

/* =========================================================================
   POP-OUT MUSIC PLAYER  (public/js/player.js + partials/music-player-bar)
   -------------------------------------------------------------------------
   A slim HUD bar docked to the bottom of every page, and the same markup
   re-used full-window inside the detached pop-out (.odh-player-bar--popout).
   Cyan-on-smoked-glass to match the header tooltips and the lightbox chrome.
   ===================================================================== */

body.odh-has-player { padding-bottom: 4.75rem; }

.odh-player-bar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 1090;                 /* under modals (1100+), over page chrome */
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding: 0.5rem 0.9rem;
    background: rgba(7, 8, 20, 0.92);
    border-top: 1px solid var(--cy-border-glow);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    box-shadow: 0 -6px 22px rgba(0, 0, 0, 0.45);
    font-family: var(--cy-font-body);
    color: var(--cy-text);
}
.odh-player-bar[hidden] { display: none; }

.odh-player-bar #odh-audio { display: none; }

/* cover thumb */
.odh-pl-cover {
    flex: 0 0 auto;
    width: 42px; height: 42px;
    border-radius: 6px;
    overflow: hidden;
    display: flex; align-items: center; justify-content: center;
    background: var(--cy-cyan-dim);
    border: 1px solid var(--cy-border-glow);
    color: var(--cy-cyan-glow);
}
.odh-pl-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* title / artist + seek */
.odh-pl-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.odh-pl-meta { display: flex; align-items: baseline; gap: 0.55rem; min-width: 0; }
.odh-pl-title {
    font-family: var(--cy-font-hud);
    font-size: 0.82rem;
    letter-spacing: 0.03em;
    color: #e8f0ff;
    text-decoration: none;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    max-width: 46%;
}
.odh-pl-title:hover { color: var(--cy-cyan); }
.odh-pl-artist {
    font-size: 0.72rem;
    font-family: var(--cy-font-hud);
    color: var(--cy-text-dim);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    flex: 1 1 auto; min-width: 0;
}

.odh-pl-seek { display: flex; align-items: center; gap: 0.5rem; }
.odh-pl-t {
    font-family: var(--cy-font-hud);
    font-size: 0.66rem;
    color: var(--cy-text-dim);
    flex: 0 0 auto;
    min-width: 2.4ch;
    text-align: center;
}
.odh-pl-track {
    position: relative;
    flex: 1 1 auto;
    height: 4px;
    border-radius: 999px;
    background: rgba(0, 245, 255, 0.14);
    cursor: pointer;
}
.odh-pl-fill {
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 0%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--cy-cyan), var(--cy-magenta));
}
.odh-pl-track::after {
    content: "";
    position: absolute; top: 50%; left: var(--odh-knob, 0%);
    width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
    border-radius: 50%;
    background: #fff;
    opacity: 0;
    transition: opacity 0.12s;
}
.odh-pl-track:hover::after { opacity: 0; }   /* knob reserved; fill is enough */

/* controls */
.odh-pl-controls { flex: 0 0 auto; display: flex; align-items: center; gap: 0.3rem; }
.odh-pl-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 34px; height: 34px;
    border: 1px solid transparent;
    border-radius: 8px;
    background: transparent;
    color: var(--cy-text);
    cursor: pointer;
    transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.odh-pl-btn:hover { background: var(--cy-cyan-dim); color: var(--cy-cyan); }
.odh-pl-btn.odh-pl-sm { width: 30px; height: 30px; font-size: 0.82rem; }
.odh-pl-btn.is-on { color: var(--cy-cyan); border-color: var(--cy-border-glow); background: var(--cy-cyan-dim); }
.odh-pl-btn .odh-rep1 { position: relative; }
.odh-pl-btn .odh-rep1::after {
    content: "1"; position: absolute; top: -3px; right: -5px;
    font-size: 0.5rem; font-family: var(--cy-font-hud); color: var(--cy-cyan);
}
.odh-pl-play {
    width: 40px; height: 40px;
    border-radius: 50%;
    border-color: var(--cy-cyan-glow);
    color: var(--cy-cyan);
    background: rgba(0, 245, 255, 0.08);
}
.odh-pl-play:hover { background: rgba(0, 245, 255, 0.18); }

.odh-pl-vol { display: flex; align-items: center; gap: 0.25rem; }
.odh-pl-volrange {
    width: 84px;
    accent-color: var(--cy-cyan);
    cursor: pointer;
    vertical-align: middle;
}

/* "playing in the detached window" mode. The seek bar is inert (the window
   owns the timeline); the transport + volume still work as a remote, and the
   detach button turns into "bring it back here" (magenta), so it stays live. */
.odh-player-bar.is-remote .odh-pl-seek { opacity: 0.4; pointer-events: none; }
.odh-player-bar.is-remote [data-odh="detach"] { color: var(--cy-magenta); }
.odh-player-bar.is-remote::before {
    content: "\f2d2";                 /* fa clone — "in the pop-out" hint */
    font-family: "Font Awesome 6 Free"; font-weight: 900;
    color: var(--cy-cyan); font-size: 0.7rem;
    align-self: center;
}

/* queue panel */
.odh-pl-queue {
    position: absolute;
    right: 0.9rem; bottom: calc(100% + 0.5rem);
    width: min(360px, calc(100vw - 1.8rem));
    max-height: 52vh;
    overflow-y: auto;
    background: rgba(9, 10, 26, 0.98);
    border: 1px solid var(--cy-border-glow);
    border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
    padding: 0.5rem;
}
.odh-pl-queue[hidden] { display: none; }
.odh-pl-qhead {
    display: flex; align-items: center; justify-content: space-between;
    font-family: var(--cy-font-hud); font-size: 0.68rem;
    text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--cy-text-dim);
    padding: 0.25rem 0.35rem 0.5rem;
}
.odh-pl-qactions { display: flex; gap: 0.4rem; }
.odh-pl-qactions button {
    font-family: var(--cy-font-hud); font-size: 0.6rem;
    text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--cy-cyan);
    background: var(--cy-cyan-dim);
    border: 1px solid var(--cy-border-glow);
    border-radius: 6px;
    padding: 0.2rem 0.5rem;
    cursor: pointer;
}
.odh-pl-qlist { list-style: none; margin: 0; padding: 0; }
.odh-pl-qrow {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.35rem 0.35rem;
    border-radius: 6px;
}
.odh-pl-qrow.is-current { background: var(--cy-cyan-dim); }
.odh-pl-qrow:hover { background: rgba(255, 255, 255, 0.04); }
.odh-pl-qplay, .odh-pl-qx {
    flex: 0 0 auto;
    width: 26px; height: 26px;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; background: transparent;
    color: var(--cy-text-dim); cursor: pointer; border-radius: 5px;
}
.odh-pl-qplay:hover { color: var(--cy-cyan); }
.odh-pl-qx:hover { color: var(--cy-magenta); }
.odh-pl-qmeta { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.odh-pl-qtitle { font-size: 0.78rem; color: #e8f0ff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.odh-pl-qartist { font-size: 0.68rem; color: var(--cy-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.odh-pl-qempty, .odh-pl-menu-e { padding: 0.9rem; text-align: center; color: var(--cy-text-dim); font-size: 0.8rem; }

/* add-to-playlist popover */
.odh-pl-menu {
    position: absolute;
    z-index: 1095;
    min-width: 210px; max-width: 240px;
    background: rgba(9, 10, 26, 0.98);
    border: 1px solid var(--cy-border-glow);
    border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
    padding: 0.35rem;
}
.odh-pl-menu-h {
    font-family: var(--cy-font-hud); font-size: 0.62rem;
    text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--cy-text-dim);
    padding: 0.4rem 0.5rem 0.3rem;
}
.odh-pl-menu button {
    display: block; width: 100%; text-align: left;
    padding: 0.45rem 0.55rem;
    background: transparent; border: none;
    color: var(--cy-text); font-size: 0.82rem;
    border-radius: 6px; cursor: pointer;
}
.odh-pl-menu button:hover { background: var(--cy-cyan-dim); color: var(--cy-cyan); }
.odh-pl-menu-new { border-top: 1px solid var(--cy-border) !important; margin-top: 0.25rem; color: var(--cy-cyan) !important; }

/* toast */
.odh-pl-toast {
    position: fixed;
    left: 50%; bottom: 5.5rem;
    transform: translate(-50%, 1rem);
    z-index: 1096;
    background: rgba(9, 10, 26, 0.97);
    border: 1px solid var(--cy-border-glow);
    color: var(--cy-cyan);
    font-family: var(--cy-font-hud); font-size: 0.72rem;
    letter-spacing: 0.04em;
    padding: 0.5rem 0.95rem;
    border-radius: 999px;
    opacity: 0; pointer-events: none;
    transition: opacity 0.18s, transform 0.18s;
}
.odh-pl-toast.is-on { opacity: 1; transform: translate(-50%, 0); }

/* the detached pop-out window: same bar, laid out to fill the little window */
.odh-player-bar--popout {
    position: static;
    flex-direction: column;
    align-items: stretch;
    gap: 1rem;
    min-height: 100vh;
    overflow-y: auto;
    border-top: none;
    padding: 1.4rem 1.2rem;
    box-shadow: none;
    text-align: center;
}
.odh-player-bar--popout .odh-pl-cover {
    width: 180px; height: 180px;
    align-self: center;
    border-radius: 12px;
}
.odh-player-bar--popout .odh-pl-cover i { font-size: 2.4rem; }
.odh-player-bar--popout .odh-pl-meta { flex-direction: column; align-items: center; gap: 0.3rem; }
.odh-player-bar--popout .odh-pl-title { max-width: 100%; font-size: 1rem; white-space: normal; }
.odh-player-bar--popout .odh-pl-artist { text-align: center; }
.odh-player-bar--popout .odh-pl-controls { justify-content: center; flex-wrap: wrap; gap: 0.4rem; }
.odh-player-bar--popout .odh-pl-queue {
    position: static; width: 100%; bottom: auto; right: auto;
    max-height: none; box-shadow: none;
    margin-top: 0.4rem;
}
.odh-player-bar--popout .odh-pl-volrange { width: 120px; }

/* Narrow screens: slim the DOCKED bar down to the essentials. Scoped away from
   .odh-player-bar--popout on purpose — the detach window is always <640px wide
   but its column layout has room for every control, so it must keep them. */
@media (max-width: 640px) {
    body.odh-has-player { padding-bottom: 4rem; }
    .odh-player-bar:not(.odh-player-bar--popout) { gap: 0.5rem; padding: 0.4rem 0.55rem; }
    .odh-player-bar:not(.odh-player-bar--popout) .odh-pl-seek .odh-pl-t { display: none; }
    .odh-player-bar:not(.odh-player-bar--popout) .odh-pl-title { max-width: 60%; }
    .odh-player-bar:not(.odh-player-bar--popout) .odh-pl-controls [data-odh="shuffle"],
    .odh-player-bar:not(.odh-player-bar--popout) .odh-pl-controls [data-odh="repeat"],
    .odh-player-bar:not(.odh-player-bar--popout) .odh-pl-vol { display: none; }
}
