/* ─────────────────────────────────────────────────────────────────────────────
   Aquarium — a public-exhibit console.
   The tank is the hero and carries all the colour; the console around it stays
   quiet, institutional and dark so the water reads as lit.
   ───────────────────────────────────────────────────────────────────────────── */

:root {
    /* Hull: the dark room the tank is lit inside */
    --abyss: #04161f;
    --hull: #0a2a37;
    --rim: #17505f;

    /* Ink */
    --foam: #e9f6fb;
    --foam-dim: #93b6c3;
    --foam-faint: #5d8493;

    /* Species palette — shared with the fish in the tank */
    --aqua: #4fd1ff;
    --coral: #ff8a3d;
    --kelp: #2ec4a6;
    --amber: #ffd166;
    --alarm: #ff5d6c;
    --violet: #c77dff;

    --placard-brass: #d8c9a3;

    --display: "Bricolage Grotesque", "Bahnschrift", "Segoe UI", sans-serif;
    --body: "Public Sans", "Segoe UI", system-ui, sans-serif;
    --data: "DM Mono", ui-monospace, "Cascadia Mono", monospace;

    --radius: 14px;
    --gutter: clamp(0.75rem, 1.2vw, 1.25rem);
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

html,
body {
    height: 100%;
}

body {
    margin: 0;
    background: var(--abyss);
    color: var(--foam);
    font-family: var(--body);
    font-size: 15px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

button {
    font: inherit;
    color: inherit;
    cursor: pointer;
}

:focus-visible {
    outline: 2px solid var(--aqua);
    outline-offset: 2px;
    border-radius: 4px;
}

/* ── Layout ───────────────────────────────────────────────────────────────── */

.exhibit {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(340px, 1fr);
    gap: var(--gutter);
    height: 100dvh;
    padding: var(--gutter);
}

/* ── The tank ─────────────────────────────────────────────────────────────── */

.tank {
    position: relative;
    overflow: hidden;
    border-radius: var(--radius);
    border: 1px solid var(--rim);
    box-shadow:
        0 0 0 6px #071e28,
        0 24px 60px rgb(0 0 0 / 55%);
}

/* The renderer paints the real water. This gradient is the backdrop behind the canvas: what
   shows for the moment before Pixi starts, and the whole of the water if WebGL or the water
   shader is unavailable. Keep the four themes in step with THEMES in js/tank/palette.js. */
.tank__water {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, #1a86b8 0%, #0d5f89 45%, #073d5c 100%);
    transition: background 900ms ease;
}

.tank__water[data-theme="dusk"] {
    background: linear-gradient(180deg, #3c4d94 0%, #23345f 50%, #131c37 100%);
}

.tank__water[data-theme="tropical"] {
    background: linear-gradient(180deg, #26d7c0 0%, #109ec0 45%, #0a5f86 100%);
}

.tank__water[data-theme="deepsea"] {
    background: linear-gradient(180deg, #0a2e4a 0%, #061c30 55%, #03101c 100%);
}

.tank__canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
}

/* The glass itself: a hairline sheen and a vignette. */
.tank__glass {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background:
        linear-gradient(104deg, rgb(255 255 255 / 10%) 0 14%, transparent 15% 100%),
        radial-gradient(120% 90% at 50% 40%, transparent 55%, rgb(2 14 22 / 55%) 100%);
    z-index: 2;
}

/* Fixed slots, filled left to right. A grid cannot transition its own reflow, so when a
   popped bubble is removed and the rest shift up a column, activity-bubbles.js slides
   them across from where they were. */
.activity-overlay {
    position: absolute;
    inset: 4.4rem 0.7rem 3.5rem;
    z-index: 4;
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.45rem;
    align-items: center;
    pointer-events: none;
}

/* Hidden bubbles keep their slots, deliberately: `visibility`, never `display: none`.
   activity-bubbles.js measures every bubble's laid-out position through offsetLeft/offsetTop to
   run the slide, and arms each completed bubble's release timer by querying the DOM — collapsing
   the layout would zero that geometry and, worse, stop the releases, so the store would fill
   with completed events for as long as the bubbles were hidden and then dump the backlog on
   screen the moment they came back. This way a run plays out exactly as it would have; the
   keeper simply is not watching it. */
.activity-overlay--hidden .activity-bubble,
.activity-overlay--hidden .activity-overlay__queue {
    visibility: hidden;
}

/* A bubble is three stacked layers: the soap film, the burst spray it leaves
   behind, and the readable content the film carries. Keeping them apart lets
   the film stretch and tear without dragging the text along. */
.activity-bubble {
    --bubble-color: 99 213 232;

    /* Every bubble rises into its slot rather than cutting in — see bubble-form. The hold
       is raised by activity-bubbles.js when a neighbour's pop is still sliding through this
       slot; everything the bubble does later is measured from the end of the entry, so the
       four-second read is never spent on arriving. */
    --bubble-form-delay: 0ms;
    --bubble-form-ms: 460ms;
    --bubble-form-total: calc(var(--bubble-form-delay) + var(--bubble-form-ms));
    --bubble-form: bubble-form var(--bubble-form-ms) cubic-bezier(0.32, 0.06, 0.22, 1)
                   var(--bubble-form-delay) both;

    position: relative;
    justify-self: center;
    width: min(100%, 210px);
    aspect-ratio: 1;
    color: #f4fdff;
    pointer-events: auto;
    font-size: clamp(0.6rem, 0.75vw, 0.72rem);
    line-height: 1.25;
    text-align: center;
    animation: var(--bubble-form);
}

.activity-bubble__membrane {
    position: absolute;
    inset: 0;
    z-index: 0;
    border: 1px solid rgb(var(--bubble-color) / 80%);
    border-radius: 50%;
    background:
        radial-gradient(circle at 28% 19%, rgb(255 255 255 / 48%) 0 3%, transparent 4%),
        radial-gradient(circle at 32% 25%, rgb(255 255 255 / 16%) 0 14%, transparent 33%),
        radial-gradient(circle at 58% 68%, rgb(var(--bubble-color) / 20%), rgb(3 19 29 / 83%) 72%);
    box-shadow:
        inset -15px -20px 34px rgb(0 0 0 / 28%),
        inset 8px 10px 20px rgb(255 255 255 / 8%),
        0 0 25px rgb(var(--bubble-color) / 34%),
        0 16px 32px rgb(0 0 0 / 32%);
    backdrop-filter: blur(9px) saturate(1.2);
    pointer-events: none;
}

.activity-bubble__membrane::after {
    content: "";
    position: absolute;
    inset: 7%;
    border-radius: 50%;
    border-top: 1px solid rgb(255 255 255 / 25%);
}

.activity-bubble__content {
    position: absolute;
    inset: 0;
    z-index: 2;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: 0.22rem;
    padding: 1.55rem 1.45rem;
    border-radius: 50%;
}

.activity-bubble--run { --bubble-color: 56 214 184; }
.activity-bubble--reasoning { --bubble-color: 182 139 255; }
.activity-bubble--message { --bubble-color: 84 210 255; }
.activity-bubble--servertool { --bubble-color: 255 183 77; }
.activity-bubble--clienttool { --bubble-color: 73 235 187; }
.activity-bubble--middleware { --bubble-color: 247 211 91; }
.activity-bubble--approval { --bubble-color: 255 156 61; }
.activity-bubble--usage { --bubble-color: 115 188 255; }
.activity-bubble--state { --bubble-color: 103 232 163; }
.activity-bubble--error { --bubble-color: 255 92 112; }

.activity-bubble__header {
    display: flex;
    gap: 0.35rem;
    align-items: flex-start;
    justify-content: center;
}

.activity-bubble__header h3,
.activity-bubble__header p,
.activity-bubble__body,
.activity-bubble__excerpt,
.activity-bubble__timer,
.activity-bubble footer {
    margin: 0;
}

.activity-bubble__header h3 {
    font-family: var(--display);
    font-size: 0.86rem;
    line-height: 1.05;
}

.activity-bubble__header p {
    margin-top: 0.12rem;
    color: rgb(222 247 252 / 72%);
    font-family: var(--data);
    font-size: 0.54rem;
    overflow-wrap: anywhere;
}

.activity-bubble__kind {
    flex: 0 0 auto;
    width: 0.48rem;
    height: 0.48rem;
    margin-top: 0.1rem;
    border-radius: 50%;
    background: rgb(var(--bubble-color));
    box-shadow: 0 0 10px rgb(var(--bubble-color));
}

.activity-bubble__badge {
    align-self: center;
    max-width: 90%;
    padding: 0.08rem 0.38rem;
    border: 1px solid rgb(var(--bubble-color) / 45%);
    border-radius: 999px;
    color: rgb(var(--bubble-color));
    font-family: var(--data);
    font-size: 0.52rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.activity-bubble__timer {
    color: rgb(var(--bubble-color));
    font: 700 1.25rem/1 var(--data);
    font-variant-numeric: tabular-nums;
    text-shadow: 0 0 12px rgb(var(--bubble-color) / 48%);
}

.activity-bubble__excerpt {
    display: -webkit-box;
    max-height: 4.7em;
    overflow: hidden;
    color: #f7fdff;
    white-space: pre-wrap;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    line-clamp: 4;
    mask-image: linear-gradient(to bottom, transparent, #000 15%, #000 83%, transparent);
}

.activity-bubble__body {
    display: -webkit-box;
    max-height: 2.6em;
    overflow: hidden;
    color: rgb(235 250 253 / 82%);
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
}

.activity-bubble__fields {
    display: grid;
    gap: 0.09rem;
    margin: 0;
    font-family: var(--data);
    font-size: 0.54rem;
}

.activity-bubble__fields > div {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.25rem;
}

.activity-bubble__fields dt {
    color: rgb(var(--bubble-color));
    text-align: right;
}

.activity-bubble__fields dd {
    min-width: 0;
    margin: 0;
    overflow: hidden;
    color: rgb(238 251 253 / 83%);
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.activity-bubble__actions {
    display: flex;
    justify-content: center;
    gap: 0.35rem;
}

.activity-bubble__actions button,
.activity-bubble__dismiss {
    border: 1px solid rgb(var(--bubble-color) / 62%);
    border-radius: 999px;
    padding: 0.2rem 0.48rem;
    color: #fff;
    background: rgb(2 20 29 / 70%);
    font: 600 0.58rem var(--display);
}

.activity-bubble__actions button:hover:not(:disabled),
.activity-bubble__dismiss:hover {
    background: rgb(var(--bubble-color) / 30%);
}

.activity-bubble footer {
    display: flex;
    justify-content: center;
    gap: 0.35rem;
    color: rgb(218 241 247 / 62%);
    font-family: var(--data);
    font-size: 0.5rem;
}

.activity-bubble__status {
    color: rgb(var(--bubble-color));
    text-transform: uppercase;
}

.activity-overlay__queue {
    position: absolute;
    right: 0.4rem;
    bottom: -2.25rem;
    margin: 0;
    padding: 0.25rem 0.5rem;
    border: 1px solid rgb(179 227 237 / 25%);
    border-radius: 999px;
    background: rgb(2 19 28 / 72%);
    color: rgb(225 247 251 / 78%);
    font: 0.6rem var(--data);
}

/* ── Popping ──────────────────────────────────────────────────────────────── */

/* What a torn film leaves behind: the rim shredded into uneven arcs that fly
   outward, and a scatter of droplets thrown clear of it. */
.activity-bubble__burst {
    position: absolute;
    inset: 0;
    z-index: 1;
    border-radius: 50%;
    pointer-events: none;
}

.activity-bubble__burst::before,
.activity-bubble__burst::after {
    content: "";
    position: absolute;
    inset: 0;
    opacity: 0;
}

/* Arcs of rim, unevenly spaced so the ring never reads as a clean halo. */
.activity-bubble__burst::before {
    background: conic-gradient(from 8deg,
        rgb(255 255 255 / 95%) 0 15deg, transparent 15deg 33deg,
        rgb(var(--bubble-color) / 95%) 33deg 52deg, transparent 52deg 75deg,
        rgb(255 255 255 / 90%) 75deg 97deg, transparent 97deg 119deg,
        rgb(var(--bubble-color) / 90%) 119deg 144deg, transparent 144deg 167deg,
        rgb(255 255 255 / 92%) 167deg 188deg, transparent 188deg 213deg,
        rgb(var(--bubble-color) / 92%) 213deg 232deg, transparent 232deg 259deg,
        rgb(255 255 255 / 88%) 259deg 280deg, transparent 280deg 303deg,
        rgb(var(--bubble-color) / 92%) 303deg 326deg, transparent 326deg 360deg);
    -webkit-mask-image: radial-gradient(circle closest-side, transparent 0 93%, #000 96% 99.5%, transparent 100%);
    mask-image: radial-gradient(circle closest-side, transparent 0 93%, #000 96% 99.5%, transparent 100%);
}

/* Droplets: two radii of travel in one layer, sizes deliberately uneven. */
.activity-bubble__burst::after {
    background-image:
        radial-gradient(circle 3.5px at 94% 41%, rgb(255 255 255 / 95%) 0 55%, transparent 60%),
        radial-gradient(circle 2.5px at 73% 15%, rgb(var(--bubble-color) / 95%) 0 55%, transparent 60%),
        radial-gradient(circle 4px at 40% 5%, rgb(255 255 255 / 92%) 0 55%, transparent 60%),
        radial-gradient(circle 2.5px at 14% 27%, rgb(var(--bubble-color) / 92%) 0 55%, transparent 60%),
        radial-gradient(circle 3.5px at 6% 59%, rgb(255 255 255 / 92%) 0 55%, transparent 60%),
        radial-gradient(circle 3px at 27% 85%, rgb(var(--bubble-color) / 95%) 0 55%, transparent 60%),
        radial-gradient(circle 4px at 60% 95%, rgb(255 255 255 / 90%) 0 55%, transparent 60%),
        radial-gradient(circle 2.5px at 86% 73%, rgb(var(--bubble-color) / 92%) 0 55%, transparent 60%),
        radial-gradient(circle 2.5px at 79% 34%, rgb(var(--bubble-color) / 88%) 0 55%, transparent 60%),
        radial-gradient(circle 2px at 53% 14%, rgb(255 255 255 / 85%) 0 55%, transparent 60%),
        radial-gradient(circle 2.5px at 21% 33%, rgb(var(--bubble-color) / 85%) 0 55%, transparent 60%),
        radial-gradient(circle 2px at 24% 68%, rgb(255 255 255 / 85%) 0 55%, transparent 60%),
        radial-gradient(circle 2.5px at 50% 85%, rgb(var(--bubble-color) / 88%) 0 55%, transparent 60%),
        radial-gradient(circle 2px at 75% 71%, rgb(255 255 255 / 85%) 0 55%, transparent 60%);
}

/* Completed activities: hold for reading, drift up, quiver, burst.
   Timings here are mirrored by the release delays in activity-bubbles.js.

   Every delay is measured from the end of the entry rather than from the moment the element
   appeared, so a bubble promoted out of the queue — which arrives already complete — still
   gets its whole four seconds of reading after it has finished rising. The entry has to be
   re-listed here too: `animation` is a shorthand, and naming only bubble-lift would drop it.
   Keeping bubble-form first in the list means adding this class part-way through a bubble's
   life continues the entry instead of restarting it. */
.activity-bubble--complete {
    animation:
        var(--bubble-form),
        bubble-lift 4.4s ease-in var(--bubble-form-total) forwards;
}

.activity-bubble--complete .activity-bubble__membrane {
    animation:
        bubble-quiver 0.36s ease-in calc(4s + var(--bubble-form-total)) forwards,
        bubble-skin-burst 0.12s cubic-bezier(0.25, 0.8, 0.4, 1) calc(4.36s + var(--bubble-form-total)) forwards;
    will-change: transform, opacity;
}

.activity-bubble--complete .activity-bubble__content {
    animation: bubble-content-dissolve 0.2s ease-in calc(4.14s + var(--bubble-form-total)) forwards;
}

.activity-bubble--complete .activity-bubble__burst::before {
    animation: bubble-shreds 0.3s cubic-bezier(0.12, 0.82, 0.3, 1) calc(4.36s + var(--bubble-form-total)) forwards;
}

.activity-bubble--complete .activity-bubble__burst::after {
    animation: bubble-spray 0.46s cubic-bezier(0.1, 0.82, 0.3, 1) calc(4.37s + var(--bubble-form-total)) forwards;
}

/* Dismissed errors: the same burst, but it sags first instead of rising.
   These delays are deliberately *not* shifted by the entry. A dismissal is a click, which can
   only land long after the bubble finished arriving, and holding the response back by the
   length of an entry the user never sees would just read as lag. */
.activity-bubble--dismissing {
    animation:
        var(--bubble-form),
        bubble-sink 0.5s ease-in forwards;
}

.activity-bubble--dismissing .activity-bubble__membrane {
    animation:
        bubble-quiver 0.36s ease-in 0s forwards,
        bubble-skin-burst 0.12s cubic-bezier(0.25, 0.8, 0.4, 1) 0.36s forwards;
    will-change: transform, opacity;
}

.activity-bubble--dismissing .activity-bubble__content {
    animation: bubble-content-dissolve 0.2s ease-in 0.14s forwards;
}

.activity-bubble--dismissing .activity-bubble__burst::before {
    animation: bubble-shreds 0.3s cubic-bezier(0.12, 0.82, 0.3, 1) 0.36s forwards;
}

.activity-bubble--dismissing .activity-bubble__burst::after {
    animation: bubble-spray 0.46s cubic-bezier(0.1, 0.82, 0.3, 1) 0.37s forwards;
}

/* Arriving: the bubble wells up from below its slot, inflating as it rises, and settles
   without overshooting — water does not bounce. It carries the same `translate` the lift
   uses, which is why the lift waits out `--bubble-form-total` above: two animations on one
   property means the later one in the list wins outright, and the rise would be flattened.
   They meet cleanly, since the rise ends exactly where the lift begins.

   Deliberately two keyframes and the slide's own curve. Buoyancy accelerates a bubble from
   rest and drag settles it, which is the shape that curve already describes for the slide, so
   the overlay travels sideways and upward by the same rule. A third keyframe to bring the
   opacity in early is tempting and wrong: the timing function applies per keyframe interval,
   so the extra waypoint quietly reshapes the rise into two fast segments.

   Note what is *not* animated here: `filter`. A blur that sharpens as the bubble forms is
   tempting, but any filter other than `none` makes this element a backdrop root, and the
   membrane's glass is a `backdrop-filter`. With `both` fill even a final `blur(0)` would
   stay, quietly flattening every bubble's glass for the rest of its life. */
@keyframes bubble-form {
    0% { opacity: 0; scale: 0.28; translate: 0 3rem; }
    100% { opacity: 1; scale: 1; translate: 0 0; }
}

/* These two move the bubble through the `translate` property rather than `transform`,
   which is left free for the slide in activity-bubbles.js. A bubble that is drifting up
   toward its burst is often also sliding sideways into a gap a neighbour left behind, and
   the two compose only while they are on separate properties — sharing `transform` means
   whichever runs last wins outright, and the bubble drops back to its baseline. */
@keyframes bubble-lift {
    0% { translate: 0 0; }
    88% { translate: 0 -0.75rem; }
    100% { translate: 0 -1.15rem; }
}

@keyframes bubble-sink {
    0% { translate: 0 0; }
    100% { translate: 0 1.1rem; }
}

/* Surface tension losing the argument: the film swells and wobbles. */
@keyframes bubble-quiver {
    0% { transform: scale(1, 1); border-color: rgb(var(--bubble-color) / 80%); }
    25% { transform: scale(1.045, 0.962); }
    52% { transform: scale(0.972, 1.052); }
    76% { transform: scale(1.052, 0.984); }
    100% { transform: scale(1.03, 1.03); border-color: rgb(255 255 255 / 92%); }
}

/* The tear itself: the film is gone in an eighth of a second. Anything slower
   reads as a fade, and a fade is what made the old bubbles feel abrupt. */
@keyframes bubble-skin-burst {
    0% { opacity: 1; transform: scale(1.03); filter: blur(0); }
    35% { opacity: 0.22; transform: scale(1.16); filter: blur(1.5px); }
    70%, 100% { opacity: 0; transform: scale(1.3); filter: blur(3px); }
}

@keyframes bubble-content-dissolve {
    0% { opacity: 1; filter: blur(0); }
    100% { opacity: 0; filter: blur(2px); }
}

@keyframes bubble-shreds {
    0% { opacity: 1; transform: scale(1.03) rotate(0deg); }
    60% { opacity: 0.4; transform: scale(1.25) rotate(5deg); }
    100% { opacity: 0; transform: scale(1.42) rotate(8deg); }
}

/* Droplets outrun the shreds — a splash always throws past its own rim. */
@keyframes bubble-spray {
    0% { opacity: 0; transform: scale(0.9) rotate(0deg); }
    12% { opacity: 1; }
    100% { opacity: 0; transform: scale(1.9) rotate(-7deg); }
}

.tank__inventory {
    position: absolute;
    right: 1rem;
    top: 0.75rem;
    margin: 0;
    max-width: 45%;
    text-align: right;
    font-family: var(--data);
    font-size: 0.7rem;
    letter-spacing: 0.02em;
    color: rgb(233 246 251 / 72%);
    text-shadow: 0 1px 6px rgb(0 0 0 / 60%);
    z-index: 5;
}

.tank__toast {
    position: absolute;
    left: 50%;
    top: 1rem;
    transform: translateX(-50%);
    margin: 0;
    max-width: min(38rem, 80%);
    padding: 0.5rem 0.9rem;
    border-radius: 10px;
    background: rgb(43 4 10 / 92%);
    border: 1px solid rgb(255 93 108 / 60%);
    color: #ffd9dd;
    font-size: 0.85rem;
    box-shadow: 0 12px 30px rgb(0 0 0 / 45%);
    z-index: 6;
}

/* ── Run stats (the exhibit label, moved off the glass) ───────────────────── */

.stats {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 1.15rem;
    margin: 0;
    padding: 0.5rem 1.1rem 0.6rem;
    border-bottom: 1px solid rgb(23 80 95 / 60%);
    background: rgb(4 22 31 / 35%);
}

.stats > div {
    display: flex;
    align-items: baseline;
    gap: 0.35rem;
}

.stats dt {
    font-family: var(--data);
    font-size: 0.58rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--foam-faint);
}

.stats dd {
    margin: 0;
    font-family: var(--data);
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--aqua);
}

.stats__model {
    flex: 1 0 100%;
    margin-bottom: 0.2rem;
}

.stats__model dd {
    color: var(--placard-brass);
}

/* ── The log panel ────────────────────────────────────────────────────────── */

.log {
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--hull);
    border: 1px solid var(--rim);
    border-radius: var(--radius);
    overflow: hidden;
}

.log__head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1rem;
    padding: 1rem 1.1rem 0.85rem;
    border-bottom: 1px solid rgb(23 80 95 / 60%);
}

.log__controls {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-shrink: 0;
}

.log__kicker {
    display: block;
    font-family: var(--data);
    font-size: 0.6rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--foam-faint);
}

.log__title {
    margin: 0;
    font-family: var(--display);
    font-weight: 800;
    font-size: 1.35rem;
    letter-spacing: -0.02em;
}

.ghost {
    background: none;
    border: 1px solid rgb(147 182 195 / 35%);
    border-radius: 999px;
    padding: 0.3rem 0.8rem;
    font-size: 0.78rem;
    color: var(--foam-dim);
    transition: border-color 150ms ease, color 150ms ease;
}

.ghost:hover:not(:disabled) {
    border-color: var(--aqua);
    color: var(--aqua);
}

.ghost:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.log__scroll {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 1rem 1.1rem;
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
    scrollbar-width: thin;
    scrollbar-color: var(--rim) transparent;
}

/* ── Turns ────────────────────────────────────────────────────────────────── */

.turn {
    max-width: 100%;
}

.turn__text {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.turn--user {
    align-self: flex-end;
    max-width: 85%;
    background: var(--coral);
    color: #26150a;
    font-weight: 500;
    padding: 0.5rem 0.8rem;
    border-radius: 12px 12px 3px 12px;
}

.turn--keeper {
    border-left: 2px solid var(--aqua);
    padding-left: 0.75rem;
}

.turn__text--waiting {
    color: var(--foam-dim);
}

.turn__error {
    margin: 0.4rem 0 0;
    padding: 0.45rem 0.6rem;
    border-radius: 8px;
    background: rgb(255 93 108 / 12%);
    border: 1px solid rgb(255 93 108 / 40%);
    color: #ffc3c8;
    font-size: 0.85rem;
}

.caret {
    display: inline-block;
    width: 0.45rem;
    height: 1em;
    margin-left: 0.15rem;
    vertical-align: text-bottom;
    background: var(--aqua);
    animation: blink 1s steps(2) infinite;
}

@keyframes blink {
    50% {
        opacity: 0;
    }
}

/* ── Thinking section ─────────────────────────────────────────────────────── */

.thinking {
    margin-bottom: 0.45rem;
}

.thinking__toggle {
    background: none;
    border: 0;
    padding: 0;
    font-family: var(--data);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--violet);
    opacity: 0.85;
}

.thinking__caret {
    display: inline-block;
    transition: transform 150ms ease;
}

.thinking--open .thinking__caret {
    transform: rotate(90deg);
}

.thinking__text {
    margin: 0.35rem 0 0;
    padding-left: 0.9rem;
    border-left: 1px dashed rgb(199 125 255 / 45%);
    font-style: italic;
    font-size: 0.85rem;
    color: var(--foam-dim);
    white-space: pre-wrap;
}

/* ── Approval card ────────────────────────────────────────────────────────── */

.approval {
    margin-top: 0.5rem;
    padding: 0.7rem 0.8rem 0.75rem;
    border: 1px solid rgb(255 93 108 / 55%);
    border-radius: 10px;
    background:
        linear-gradient(180deg, rgb(255 93 108 / 14%), rgb(255 93 108 / 6%));
    animation: alert-pulse 2s ease-in-out infinite;
}

@keyframes alert-pulse {
    50% {
        border-color: rgb(255 93 108 / 100%);
    }
}

.approval--flash {
    animation: approval-flash 700ms ease-out 2;
}

@keyframes approval-flash {
    0%,
    100% {
        box-shadow: 0 0 0 0 rgb(255 93 108 / 0%);
    }

    35% {
        box-shadow: 0 0 0 6px rgb(255 93 108 / 45%);
    }
}

.approval__lead {
    margin: 0;
    font-weight: 600;
}

.approval__lead code {
    font-family: var(--data);
    font-size: 0.85em;
    color: #ffc3c8;
}

.approval__reason {
    margin: 0.25rem 0 0;
    font-style: italic;
    color: var(--foam-dim);
}

.approval__note {
    margin: 0.15rem 0 0.6rem;
    font-size: 0.8rem;
    color: var(--foam-faint);
}

.approval__actions {
    display: flex;
    gap: 0.5rem;
}

.approval__allow,
.approval__deny {
    flex: 1;
    padding: 0.4rem 0.6rem;
    border-radius: 8px;
    border: 1px solid transparent;
    font-weight: 600;
}

.approval__allow {
    background: var(--alarm);
    color: #2b0006;
}

.approval__deny {
    background: none;
    border-color: rgb(147 182 195 / 40%);
    color: var(--foam-dim);
}

.approval__deny:hover {
    border-color: var(--foam-dim);
    color: var(--foam);
}

.approval__outcome {
    margin: 0.35rem 0 0;
    font-family: var(--data);
    font-size: 0.72rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--foam-faint);
}

/* ── Chips ────────────────────────────────────────────────────────────────── */

.chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    list-style: none;
    margin: 0 0 0.45rem;
    padding: 0;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.18rem 0.6rem;
    border-radius: 999px;
    font-family: var(--data);
    font-size: 0.72rem;
    border: 1px solid transparent;
}

.chip--tool {
    background: rgb(46 196 166 / 12%);
    border-color: rgb(46 196 166 / 45%);
    color: #a9ecdd;
}

/* A tool the circuit runs itself, not the server (§6.6). */
.chip--client {
    background: rgb(255 183 3 / 12%);
    border-color: rgb(255 183 3 / 45%);
    color: #ffdf9e;
}

.chip--client .chip__dot {
    background: var(--amber);
}

.chip--retried {
    background: rgb(147 182 195 / 8%);
    border-color: rgb(147 182 195 / 35%);
    color: var(--foam-dim);
}

.chip__where {
    font-size: 0.62rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.7;
}

.chip__dot {
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--kelp);
}

.chip--running .chip__dot {
    animation: pulse 900ms ease-in-out infinite;
}

.chip--done .chip__dot {
    background: var(--foam-faint);
}

@keyframes pulse {
    50% {
        transform: scale(1.6);
        opacity: 0.4;
    }
}

.chip--suggestion {
    background: none;
    border-color: rgb(147 182 195 / 30%);
    color: var(--foam-dim);
    font-family: var(--body);
    font-size: 0.8rem;
    padding: 0.3rem 0.7rem;
    text-align: left;
}

.chip--suggestion:hover {
    border-color: var(--aqua);
    color: var(--aqua);
}

/* ── Empty state ──────────────────────────────────────────────────────────── */

.empty {
    margin: auto 0;
    color: var(--foam-dim);
}

.empty__lead {
    margin: 0 0 0.15rem;
    font-family: var(--display);
    font-weight: 600;
    font-size: 1.05rem;
    color: var(--foam);
}

.empty__hint {
    margin: 0 0 0.5rem;
    font-size: 0.85rem;
}

.empty__prompts {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    align-items: flex-start;
}

/* ── Follow-up suggestions (§6.9) ─────────────────────────────────────────── */

/* Indented to line up with the keeper's text but without its border: these are
   the user's next move, not the keeper's words. */
.followups {
    padding-left: 0.77rem;
    margin-top: -0.35rem;
}

.followups__hint {
    margin: 0 0 0.4rem;
    font-family: var(--data);
    font-size: 0.65rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--foam-faint);
}

.followups__hint--pending {
    animation: followup-breathe 1.6s ease-in-out infinite;
}

.followups__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.followups .chip--suggestion {
    /* A 60-character prompt has to be allowed to wrap on a narrow panel. */
    max-width: 100%;
    line-height: 1.35;
    animation: followup-in 220ms ease-out both;
}

@keyframes followup-in {
    from {
        opacity: 0;
        transform: translateY(0.25rem);
    }
}

@keyframes followup-breathe {
    50% {
        opacity: 0.45;
    }
}

/* ── Logical event timeline ───────────────────────────────────────────────── */

.timeline {
    border-top: 1px solid rgb(23 80 95 / 60%);
    font-family: var(--data);
    font-size: 0.7rem;
    color: var(--foam-faint);
}

.timeline > summary {
    padding: 0.5rem 1.1rem;
    cursor: pointer;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.timeline__count {
    color: var(--foam-dim);
}

.timeline__list {
    max-height: 15rem;
    overflow-y: auto;
    margin: 0;
    padding: 0 0.8rem 0.65rem;
    list-style: none;
    color: var(--foam-dim);
}

.timeline__entry {
    --timeline-color: 99 213 232;
    display: grid;
    grid-template-columns: 0.5rem minmax(0, 1fr);
    gap: 0.45rem;
    padding: 0.45rem 0.3rem;
    border-bottom: 1px solid rgb(29 83 96 / 40%);
}

.timeline__entry--reasoning { --timeline-color: 182 139 255; }
.timeline__entry--servertool,
.timeline__entry--approval { --timeline-color: 255 183 77; }
.timeline__entry--clienttool,
.timeline__entry--state { --timeline-color: 73 235 187; }
.timeline__entry--middleware { --timeline-color: 247 211 91; }
.timeline__entry--error { --timeline-color: 255 92 112; }

.timeline__swatch {
    width: 0.48rem;
    height: 0.48rem;
    margin-top: 0.18rem;
    border-radius: 50%;
    background: rgb(var(--timeline-color));
    box-shadow: 0 0 8px rgb(var(--timeline-color) / 45%);
}

.timeline__title,
.timeline__event,
.timeline__summary,
.timeline__meta {
    margin: 0;
}

.timeline__title {
    color: var(--foam);
    font-family: var(--display);
    font-weight: 600;
}

.timeline__status {
    float: right;
    color: rgb(var(--timeline-color));
    font: 0.58rem var(--data);
    text-transform: uppercase;
}

.timeline__event {
    margin-top: 0.1rem;
    color: rgb(var(--timeline-color));
    font-size: 0.59rem;
    overflow-wrap: anywhere;
}

.timeline__summary {
    display: -webkit-box;
    margin-top: 0.15rem;
    overflow: hidden;
    color: var(--foam-dim);
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
}

.timeline__meta {
    margin-top: 0.12rem;
    color: var(--foam-faint);
    font-size: 0.58rem;
}

/* ── Composer ─────────────────────────────────────────────────────────────── */

.composer {
    display: flex;
    gap: 0.5rem;
    padding: 0.75rem 1.1rem 0.9rem;
    border-top: 1px solid rgb(23 80 95 / 60%);
    background: rgb(4 22 31 / 45%);
}

.composer__input {
    flex: 1;
    resize: none;
    font: inherit;
    color: var(--foam);
    background: var(--abyss);
    border: 1px solid var(--rim);
    border-radius: 10px;
    padding: 0.5rem 0.65rem;
}

.composer__input::placeholder {
    color: var(--foam-faint);
}

.composer__input:disabled {
    opacity: 0.6;
}

.composer__send {
    align-self: stretch;
    padding: 0 1.1rem;
    border: 0;
    border-radius: 10px;
    background: var(--aqua);
    color: #04222e;
    font-family: var(--display);
    font-weight: 600;
    letter-spacing: 0.01em;
    transition: filter 150ms ease;
}

.composer__send:hover:not(:disabled) {
    filter: brightness(1.08);
}

.composer__send:disabled {
    background: var(--rim);
    color: var(--foam-faint);
    cursor: not-allowed;
}

/* ── Blazor's error strip ─────────────────────────────────────────────────── */

#blazor-error-ui {
    display: none;
    position: fixed;
    inset: auto 0 0 0;
    z-index: 1000;
    padding: 0.7rem 1.2rem;
    background: var(--amber);
    color: #2b1d00;
    font-weight: 500;
    box-shadow: 0 -6px 24px rgb(0 0 0 / 40%);
}

#blazor-error-ui .dismiss {
    position: absolute;
    right: 0.9rem;
    top: 0.6rem;
    cursor: pointer;
}

/* ── Responsive & motion ──────────────────────────────────────────────────── */

@media (max-width: 900px) {
    .exhibit {
        grid-template-columns: 1fr;
        grid-template-rows: minmax(260px, 40dvh) minmax(0, 1fr);
        height: auto;
        min-height: 100dvh;
    }

    .placard__facts {
        grid-template-columns: repeat(2, auto);
    }

    .activity-overlay {
        inset: 2.4rem 0.45rem 2.2rem;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 0.3rem;
    }

    .activity-bubble {
        width: min(100%, 166px);
        font-size: 0.58rem;
    }

    .activity-bubble__content {
        padding: 1.25rem 1.1rem;
    }

    .activity-bubble__header h3 {
        font-size: 0.72rem;
    }

    .activity-bubble__timer {
        font-size: 1rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }

    /* Motion off still deserves a beat of feedback. The blanket rule above
       would make bubbles blink out of existence, which reads as a glitch, so
       they get a short fade instead: no travel, no wobble, no spray. */
    .activity-bubble--complete,
    .activity-bubble--dismissing,
    .activity-bubble--complete .activity-bubble__burst::before,
    .activity-bubble--complete .activity-bubble__burst::after,
    .activity-bubble--dismissing .activity-bubble__burst::before,
    .activity-bubble--dismissing .activity-bubble__burst::after {
        animation: none !important;
    }

    .activity-bubble--complete .activity-bubble__membrane,
    .activity-bubble--complete .activity-bubble__content {
        animation: bubble-fade-out 0.26s ease-out calc(4s + var(--bubble-form-total)) forwards !important;
    }

    .activity-bubble--dismissing .activity-bubble__membrane,
    .activity-bubble--dismissing .activity-bubble__content {
        animation: bubble-fade-out 0.26s ease-out 0s forwards !important;
    }

    /* Arriving deserves the same beat as leaving, and for the same reason — a bubble that
       simply exists between two frames reads as a glitch. So the rise becomes a plain fade:
       no travel, no inflate. This has to come after the `animation: none` rule above to
       reach a bubble that arrives already complete, and shortening the duration variable
       keeps every delay derived from it honest. The fade runs the existing keyframe
       backwards rather than adding a second one that says the same thing. */
    .activity-bubble {
        --bubble-form-ms: 160ms;
        animation: bubble-fade-out 160ms ease-out reverse both !important;
    }
}

@keyframes bubble-fade-out {
    to { opacity: 0; }
}
