/* Self-hosted Inter (variable, weights 100-900 in one file per style).
   Same niche as Discord's gg sans; the old stack led with Whitney, a commercial
   font we never shipped, so users actually saw Arial/Helvetica/Roboto. */
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url("/fonts/InterVariable.woff2") format("woff2");
}

@font-face {
    font-family: "Inter";
    font-style: italic;
    font-weight: 100 900;
    font-display: swap;
    src: url("/fonts/InterVariable-Italic.woff2") format("woff2");
}

/* Base styles for Discord-style dark theme */
:root {
    /* Backgrounds (darkest to lightest) */
    --bg-darkest: #18191c;
    --bg-header: #202225;
    --bg-sidebar: #2f3136;
    --bg-hover: #32353b;
    --bg-primary: #36393f;
    --bg-secondary: #393c43;
    --bg-input: #40444b;
    --bg-muted: #4f545c;
    --bg-tertiary: #2a2c31;

    /* Text colors (brightest to dimmest) */
    --text-white: #fff;
    --text-primary: #dcddde;
    --text-secondary: #b9bbbe;
    --text-muted: #a3a6aa;
    --text-tertiary: #96989d;
    --text-placeholder: #72767d;

    /* Accent colors (Discord blurple) */
    --accent-primary: #5865f2;
    --accent-hover: #4752c4;
    --accent-light: #dee0fc;
    --accent-focus: #00aff4;
    --accent-tint: hsl(235 85.6% 64.7% / 0.16);

    /* Status colors */
    --status-online: #3ba55c;
    --status-online-hover: #2d7d46;
    --status-away: #faa61a;
    --status-away-hover: #fcc24f;
    --status-invisible: #747f8d;

    /* Text/icon colour on the header status chip, whose BACKGROUND is the status
       colour itself. Themes with bright status colours (phosphor/neon greens) flip
       this dark - white on bright green is unreadable. */
    --status-chip-text: var(--text-white);

    /* Semantic colors */
    --color-success: #3ba55c;
    --color-warning: #faa61a;
    --color-danger: #ed4245;

    /* Borders */
    --border-dark: #202225;
    --border-subtle: #040405;

    /* Scene layers (Themes 2.0 canvas layer, see docs/themes-2.0.md).
       A theme that sets these paints a tiled scene behind the whole chat shell;
       every theme that leaves them `none` renders exactly as it did before, which
       is the acceptance test for the layer. Names and stacking order mirror the
       source theme's own five layers - do not reorder without reading Finding 3. */
    --scene-canvastile: none;
    --scene-headertile: none;
    --scene-header: none;
    --scene-footertile: none;
    --scene-footer: none;

    /* Per-layer background-size list, positionally matched to the images above.
       A single `auto` applies to every slot, which is what non-scene themes want.
       Scene themes emit a real list so the strips scale to fit the viewport. */
    --scene-sizes: auto;

    /* Surface behind the composer band (input row + typing indicator). Transparent:
       the composer sits inside .messages-container, so that panel's wash continues
       behind it uninterrupted, and .chat-container already paints --bg-primary
       beneath everything. An OPAQUE value here is the hazard that named this
       variable - it buries the scene/pattern layers and cuts a hard line across
       the wash. No shipped theme sets it (Tea House briefly did and was reverted:
       the user wanted only the input pill itself coloured); a theme that wants a
       distinct band must match its panel's terminal colour/alpha exactly. */
    --bg-input-band: transparent;

    /* Surface behind the message list. Transparent for the same reason as
       --bg-input-band above: --bg-primary is already painted by .chat-container, so
       this looks identical, but leaving it opaque would bury the scene and pattern
       layers. A scene theme overrides it with a translucent wash for legibility. */
    --bg-canvas-panel: transparent;

    /* --- Pattern layer -------------------------------------------------------
       A tint shown through a mask. The mask carries the SHAPE, the element's
       background-color carries the COLOUR - which is the whole point: an SVG or
       image tile bakes its colour in, so it would need one copy per theme, while a
       mask lets every theme paint the same shape in its own colour.

       Off by default twice over: no mask AND a transparent colour, because
       `mask-image: none` means "no mask", i.e. fully opaque, not "invisible". */
    --canvas-pattern: none;
    --pattern-color: transparent;
    /* Colour a theme wants its DM pattern drawn in. White works on every dark
       theme; light themes override it below.

       HALVED 2026-08-27, and so was every other pattern alpha in the app
       (themes.css and the Tea House generator): the motifs read as decoration
       competing with the text rather than as a whisper that the room is
       different. The ratios between them were right, only the overall level was
       hot - so the whole set was scaled by 0.5 rather than retuned one by one.
       Keep that discipline: change this default and the signature patterns move
       with it, or the DM marker starts out-shouting a theme's own motif. */
    --pattern-tint: hsl(0 0% 100% / 0.0225);

    /* Reusable pattern shapes: discrete organic motifs (waves, raindrops, specks,
       wave-crest arcs), scattered so nothing touches. They are SVG data-URI MASKS,
       not images: only the alpha channel matters, the colour still comes from the
       element's background-color, so one shape serves every theme. Gradients could
       not draw these - repeating-linear-gradient only makes lines and grids, which
       is the rigid look this set replaced. Each tile is seamless simply because no
       motif crosses its edge; mask-size auto uses the SVG's intrinsic width/height,
       so density is tuned by the tile dimensions inside each URI.
       Authoring tool (edit, preview, regenerate): themes/patterns/build.py. */
    --pattern-waves: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='104' height='104'%3E%3Cdefs%3E%3ClinearGradient id='f'%3E%3Cstop offset='0' stop-color='black' stop-opacity='0.25'/%3E%3Cstop offset='0.35' stop-color='black'/%3E%3Cstop offset='0.65' stop-color='black'/%3E%3Cstop offset='1' stop-color='black' stop-opacity='0.25'/%3E%3C/linearGradient%3E%3Cpath id='w' d='M0.0 0.0 L0.9 -0.5 L1.7 -1.0 L2.5 -1.4 L3.3 -1.7 L4.0 -1.9 L4.7 -2.0 L5.4 -2.0 L6.0 -1.9 L6.6 -1.8 L7.2 -1.6 L7.8 -1.4 L8.4 -1.1 L9.0 -0.7 L9.7 -0.2 L10.3 0.3 L11.0 1.0 L11.8 1.8 L12.7 2.4 L13.5 3.0 L14.4 3.4 L15.3 3.8 L16.2 4.0 L17.1 4.1 L18.0 4.1 L18.9 3.9 L19.7 3.7 L20.5 3.3 L21.3 2.8 L22.0 2.3 L22.7 1.6 L23.4 0.9 L24.0 0.0 L24.0 -0.0 L23.1 0.5 L22.3 1.0 L21.5 1.4 L20.7 1.7 L20.0 1.9 L19.3 2.0 L18.6 2.0 L18.0 1.9 L17.4 1.8 L16.8 1.6 L16.2 1.4 L15.6 1.1 L15.0 0.7 L14.3 0.2 L13.7 -0.3 L13.0 -1.0 L12.2 -1.8 L11.3 -2.4 L10.5 -3.0 L9.6 -3.4 L8.7 -3.8 L7.8 -4.0 L6.9 -4.1 L6.0 -4.1 L5.1 -3.9 L4.3 -3.7 L3.5 -3.3 L2.7 -2.8 L2.0 -2.3 L1.3 -1.6 L0.6 -0.9 L0.0 0.0 Z' fill='url(%23f)'/%3E%3C/defs%3E%3Cuse href='%23w' transform='translate(10 34) rotate(-45) scale(1.25)'/%3E%3Cuse href='%23w' transform='translate(48 26) rotate(-41) scale(0.8)'/%3E%3Cuse href='%23w' transform='translate(76 58) rotate(-48) scale(1.0)'/%3E%3Cuse href='%23w' transform='translate(24 82) rotate(-44) scale(0.6)'/%3E%3Cuse href='%23w' transform='translate(62 96) rotate(-46) scale(0.9)'/%3E%3C/svg%3E");
    --pattern-drops: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cg transform='translate(24 30) rotate(15) scale(1.0)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(80 19) rotate(-20) scale(0.7)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(121 49) rotate(30) scale(1.2)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(48 76) rotate(-10) scale(0.85)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(102 89) rotate(45) scale(0.6)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(22 116) rotate(-35) scale(1.1)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(75 127) rotate(10) scale(0.75)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3Cg transform='translate(126 116) rotate(-15) scale(0.9)'%3E%3Cpath d='M0 -6 C3 -2 5 1 5 3.5 A5 5 0 1 1 -5 3.5 C-5 1 -3 -2 0 -6 Z' fill='black'/%3E%3C/g%3E%3C/svg%3E");
    --pattern-pebbles: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cdefs%3E%3Cpath id='b' d='M0,-18 C12,-18 22,-8 20,4 C18,16 6,22 -6,19 C-18,16 -24,4 -20,-8 C-16,-18 -10,-18 0,-18 Z'/%3E%3C/defs%3E%3Cuse href='%23b' fill='black' transform='translate(55 60) rotate(10) scale(1.0)'/%3E%3Cuse href='%23b' fill='black' transform='translate(160 40) rotate(-25) scale(0.7)'/%3E%3Cuse href='%23b' fill='black' transform='translate(225 110) rotate(40) scale(1.15)'/%3E%3Cuse href='%23b' fill='black' transform='translate(100 150) rotate(-10) scale(0.85)'/%3E%3Cuse href='%23b' fill='black' transform='translate(195 185) rotate(15) scale(0.6)'/%3E%3Cuse href='%23b' fill='black' transform='translate(50 220) rotate(-35) scale(1.1)'/%3E%3Cuse href='%23b' fill='black' transform='translate(145 235) rotate(5) scale(0.8)'/%3E%3C/svg%3E");
    --pattern-ripples: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cdefs%3E%3Cpath id='r' d='M0,-15 C9,-16 16,-8 15,1 C14,10 6,16 -3,15 C-12,14 -17,6 -16,-3 C-15,-11 -8,-14 0,-15 Z'/%3E%3C/defs%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(72 82) scale(1.0)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(72 82) scale(1.75)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(72 82) scale(2.5)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(185 185) rotate(25) scale(0.9)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(185 185) rotate(25) scale(1.5)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(185 185) rotate(25) scale(2.1)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(185 185) rotate(25) scale(2.7)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(228 55) rotate(-15) scale(0.6)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(228 55) rotate(-15) scale(1.05)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(45 215) rotate(10) scale(0.55)'/%3E%3Cuse href='%23r' fill='none' stroke='black' stroke-width='1.5' transform='translate(45 215) rotate(10) scale(0.95)'/%3E%3Ccircle cx='150' cy='60' r='1.5' fill='black'/%3E%3Ccircle cx='120' cy='200' r='1.5' fill='black'/%3E%3Ccircle cx='240' cy='140' r='1.5' fill='black'/%3E%3C/svg%3E");
    --pattern-specks: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Ccircle cx='8' cy='10' r='1.4' fill='black'/%3E%3Ccircle cx='28' cy='6' r='1.0' fill='black'/%3E%3Ccircle cx='50' cy='14' r='1.8' fill='black'/%3E%3Ccircle cx='14' cy='34' r='1.0' fill='black'/%3E%3Ccircle cx='40' cy='38' r='1.5' fill='black'/%3E%3Ccircle cx='60' cy='52' r='1.1' fill='black'/%3E%3Ccircle cx='22' cy='56' r='1.6' fill='black'/%3E%3C/svg%3E");
    --pattern-specks-dense: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Ccircle cx='8' cy='10' r='1.4' fill='black'/%3E%3Ccircle cx='28' cy='6' r='1.0' fill='black'/%3E%3Ccircle cx='50' cy='14' r='1.8' fill='black'/%3E%3Ccircle cx='14' cy='34' r='1.0' fill='black'/%3E%3Ccircle cx='40' cy='38' r='1.5' fill='black'/%3E%3Ccircle cx='60' cy='52' r='1.1' fill='black'/%3E%3Ccircle cx='22' cy='56' r='1.6' fill='black'/%3E%3Ccircle cx='46' cy='60' r='1.2' fill='black'/%3E%3Ccircle cx='58' cy='28' r='1.0' fill='black'/%3E%3Ccircle cx='6' cy='48' r='1.2' fill='black'/%3E%3Ccircle cx='34' cy='22' r='1.1' fill='black'/%3E%3Ccircle cx='52' cy='44' r='1.4' fill='black'/%3E%3C/svg%3E");

    /* --- Glow ----------------------------------------------------------------
       Theme opt-in, `none` by default. Applied to a SMALL fixed set of elements
       (status dots, the send button). Never per-message: a box-shadow on every row
       of a scrolling list repaints every frame for no information gain. */
    --glow-accent: none;
    --glow-status: none;

    /* Wash painted over the whole composite. Per-layer tinting is impossible
       (the layers are opaque JPEGs), so legibility tuning happens here. */
    --canvas-scrim: none;
}

* {
    box-sizing: border-box;
}

/* ==========================================================================
   THE pattern layer. It lives inside the conversation and scrolls with the
   text - chosen over a fixed backdrop after an A/B toggle (2026-08-25): the
   pattern belongs to the conversation surface, not the room behind it. A side
   effect the decision embraced: patterns show only in the message column, not
   behind the sidebar or header.

   Mechanics: masks have no attachment property, so scrolling is structural -
   the ::before spans .messages-flow (the scroll CONTENT) at full height and
   rides natively with it. background-color carries the colour, the mask
   carries the shape, both set per theme/context in themes.css.

   z-index choreography: .messages-flow gets its own stacking context so the
   ::before at -1 paints above the ancestors' backgrounds (the canvas panel
   wash) but below every message. Without the local context, -1 would sink
   beneath the wash and vanish - the buried-layer bug from the pattern layer's
   first landing.

   The context also TRAPS every position:fixed popup a message renders (the
   more-menu and its backdrop, the reaction picker, the delete confirm): their
   own z-indexes only rank inside the flow, and the flow as a whole ranks
   against its siblings. z-index 1, not 0, so the flow beats the later
   .message-input-container (z auto) - at 0 the input row painted over the
   bottom of those popups (the "Delete" item, the mobile bottom sheets). Keep
   it below 98 (mobile sidebar backdrop) so drawers, header dropdowns and the
   image lightbox still cover the list. The always-on send button (z 1000) can
   still poke through a mobile sheet while text is typed; accepted.
   ========================================================================== */
.messages-flow {
    position: relative;
    z-index: 1;
}

.messages-flow::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-color: var(--pattern-color);
    -webkit-mask-image: var(--canvas-pattern);
    mask-image: var(--canvas-pattern);
}

html, body {
    margin: 0;
    padding: 0;
    font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
    background: var(--bg-primary);
    /* Gradient themes make --bg-primary an image, and the shorthand above then
       resets background-color to transparent. iOS paints the PWA status-bar
       strip, overscroll, and Safari's chrome tint from the root COLOR, not the
       image - without a real one those surfaces fall back to white. Solid
       themes fall through to --bg-primary; gradient themes must define
       --bg-canvas (their top gradient stop) in themes.css. syncThemeColorMeta()
       in App.razor reads this computed color into <meta name="theme-color">. */
    background-color: var(--bg-canvas, var(--bg-primary));
    color: var(--text-primary);
    height: 100%;
    overflow: hidden;
}

h1:focus {
    outline: none;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

.blazor-error-boundary::after {
    content: "An error has occurred."
}

/* Global error UI - shown by Blazor when unhandled exceptions occur */
/* Blazor sets display:block when error occurs, we override to flex for centering */
#blazor-error-ui {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.85);
    backdrop-filter: blur(4px);
    z-index: 10000;
    align-items: center;
    justify-content: center;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    animation: fadeIn 0.3s ease-out;
}

#blazor-error-ui[style*="block"] {
    display: flex !important;
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes bounceIn {
    0% { transform: scale(0.8); opacity: 0; }
    50% { transform: scale(1.02); }
    100% { transform: scale(1); opacity: 1; }
}

#blazor-error-ui .error-modal-content {
    background: var(--bg-sidebar, #2f3136);
    border-radius: 12px;
    padding: 2.5rem;
    max-width: 420px;
    text-align: center;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    animation: bounceIn 0.4s ease-out;
}

#blazor-error-ui .error-illustration {
    margin-bottom: 1rem;
}

#blazor-error-ui .error-ascii {
    font-family: 'Courier New', monospace;
    font-size: 12px;
    line-height: 1.2;
    color: #faa61a;
    margin: 0;
    display: inline-block;
    text-align: left;
}

#blazor-error-ui .error-title {
    color: #ed4245;
    font-size: 1.5rem;
    font-weight: 700;
    margin: 0 0 0.75rem 0;
}

#blazor-error-ui .error-message {
    color: #dcddde;
    font-size: 0.95rem;
    line-height: 1.5;
    margin: 0 0 1.5rem 0;
}

#blazor-error-ui .error-actions {
    margin-bottom: 1rem;
}

#blazor-error-ui .reload {
    display: inline-block;
    background: #5865f2;
    color: white;
    padding: 12px 24px;
    border-radius: 8px;
    text-decoration: none;
    font-weight: 600;
    font-size: 0.95rem;
    transition: background-color 0.15s ease, transform 0.15s ease;
}

#blazor-error-ui .reload:hover {
    background: #4752c4;
    transform: translateY(-1px);
}

#blazor-error-ui .error-hint {
    color: #72767d;
    font-size: 0.8rem;
    margin: 0;
    font-style: italic;
}

.custom-emoji {
    object-fit: contain;
}

/* Optimistic send echo (chat.js send pipeline): a dimmed plain-text stand-in for the
   just-sent message, swapped for the real render when the round trip completes.
   Padding mirrors .message-group's text column so the swap doesn't shift layout.
   Lives here (not scoped css) because ghosts are JS-created without scope attributes. */
.pending-message {
    padding: 0.125rem 1rem 0.125rem calc(1rem + 40px + 1rem);
    opacity: 0.55;
    white-space: pre-wrap;
    overflow-wrap: break-word;
}

/* GIF variant of the send echo: an <img>/<video> stand-in built from the picker card's
   preview, dimmed by the .pending-message opacity. Sized like the real render (360px
   cap, 8px radius — see MessageItem's gif-message) so the swap barely moves. Fixed
   360px width (not min(360px, natural)) because the client doesn't know the full-size
   dimensions at click time — narrow GIFs shrink slightly when the real message lands.
   The inline aspect-ratio (copied from the card) reserves height before the preview paints. */
.pending-message.pending-gif img,
.pending-message.pending-gif video {
    display: block;
    width: 360px;
    max-width: 100%;
    border-radius: 8px;
}
