/* uwu.kim — powoershell.exe window chrome + the chat room inside it.
 *
 * Loaded on the landing page, /home and the admin pages — anywhere term.js
 * runs.
 *
 * EVERY WINDOW RULE IS SCOPED TO .term--app, and that is load-bearing. /home
 * has a SECOND, older console — the upload one, built by uploads.js and driven
 * by home.js — which uses the same .term__* class names and is styled by
 * home.css. Both stylesheets load on that page. Without the scope they would
 * fight, and whichever lost would break: the chat window would lose its
 * max-height and grow past the viewport, or the upload console would be
 * restyled underneath a feature that never asked to touch it.
 *
 * So: rules that style the window go inside .term--app. The only exception is
 * .term__task, the floating taskbar button, which lives outside the window.
 *
 * The two consoles remain a duplication worth merging one day. Doing it means
 * moving the upload flow onto this shared window, which is not something to
 * attempt in the same change as anything else.
 */

:root {
  --term-bg:   #07070c;
  --term-line: #2b2b45;
}

/* -------------------------------- WINDOW -------------------------------- */
/* Positioned with left/top so dragging can move it with transform without the
   two fighting each other. */
.term--app {
  position: fixed;
  top: 84px;
  left: 50%;
  /* A window should never be buried by page furniture. */
  z-index: 2000;
  width: min(620px, calc(100vw - 28px));
  margin-left: min(-310px, calc(-50vw + 14px));
  max-height: min(70vh, 620px);
  display: flex;
  flex-direction: column;
  font-family: var(--mono, ui-monospace, Consolas, monospace);
  background: var(--term-bg);
  border: 1px solid var(--term-line);
  border-radius: 7px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.7);
  overflow: hidden;
}
.term--app[hidden] { display: none; }

.term--app .term__bar {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  background: linear-gradient(#1d1d2e, #161624);
  border-bottom: 1px solid var(--term-line);
  cursor: grab;
  user-select: none;
  touch-action: none;            /* let the drag handler own the gesture */
  flex: 0 0 auto;
}
.term--app .term__bar.is-dragging { cursor: grabbing; }

.term--app .term__dot {
  width: 9px; height: 9px; flex: 0 0 auto;
  border-radius: 50%;
  background: var(--cyan, #5ef1e8);
  box-shadow: 0 0 8px var(--cyan, #5ef1e8);
}
.term--app .term__title {
  flex: 1;
  font-size: 12px;
  color: #b9b9d4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.term--app .term__min,
.term--app .term__max,
.term--app .term__x {
  flex: 0 0 auto;
  width: 22px; height: 20px;
  padding: 0;
  font: inherit;
  font-size: 12px;
  line-height: 1;
  color: #8a8aa8;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 3px;
  cursor: pointer;
}
.term--app .term__min:hover,
.term--app .term__max:hover { color: #fff; background: #34344f; }
.term--app .term__x:hover   { color: #fff; background: var(--red, #ff5c7a); }

/* Baseline for every control in the window, so it renders identically on any
   page that hosts it.
 *
 * admin.css styles the BARE `button` element — margin-top:14px, min-height:44px,
 * bold, cyan fill — which is right for its forms and very wrong for a tab strip.
 * The class rules below out-specify it, but only for properties they actually
 * set; anything they leave alone leaks through. That is why the tabs came out
 * thick on /admin and correct everywhere else: nothing here was setting
 * min-height or margin, so admin's 44px and 14px applied.
 *
 * Fixed here rather than by narrowing admin.css, because the window is dropped
 * into pages with their own global styles and shouldn't depend on what those
 * happen to be. Specificity (0,1,1) — beats a bare element selector, loses to
 * the two-class rules below, so it only ever fills gaps. */
.term--app button,
.term--app input {
  margin: 0;
  min-height: 0;
  font-weight: 400;
  font-family: var(--mono, ui-monospace, Consolas, monospace);
  /* Pinned so a control's height comes from its own font-size and padding and
     nothing else. Left to inherit, the line box is computed from whatever
     --mono resolves to on the host page, and the two stacks differ enough to
     shift a tab by a pixel between the landing page and /admin. */
  line-height: 1.5;
}

/* --------------------------------- TABS --------------------------------- */
.term--app .term__tabs {
  display: flex;
  gap: 2px;
  padding: 6px 8px 0;
  background: #101019;
  border-bottom: 1px solid var(--term-line);
  overflow-x: auto;
  flex: 0 0 auto;
}
.term--app .term__tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Explicit, not derived from padding + line box + border. admin.css resets
     `button { border: 0 }`, which changes how the tab's own border resolves at
     sub-pixel widths and left it ~1px shorter there than on the landing page.
     box-sizing is border-box in every stylesheet on this site, so a fixed
     height is the whole box and lands identically wherever it renders. */
  height: 28px;
  padding: 5px 10px;
  font: inherit;
  font-size: 11.5px;
  color: #7c7c9c;
  background: #16162a;
  border: 1px solid var(--term-line);
  border-bottom: none;
  border-radius: 4px 4px 0 0;
  cursor: pointer;
  white-space: nowrap;
}
.term--app .term__tab:hover { color: #c9c9e4; }
.term--app .term__tab.is-on {
  color: var(--cyan, #5ef1e8);
  background: var(--term-bg);
}
.term--app .term__tabx {
  padding: 0 2px;
  font: inherit;
  font-size: 10px;
  color: inherit;
  background: transparent;
  border: 0;
  cursor: pointer;
  opacity: 0.55;
}
.term--app .term__tabx:hover { opacity: 1; color: var(--red, #ff5c7a); }

.term--app .term__body { display: flex; flex: 1 1 auto; min-height: 0; }
.term--app .term__pane { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.term--app .term__pane[hidden] { display: none; }

/* -------------------------------- CONSOLE ------------------------------- */
.term--app .term__log {
  flex: 1 1 auto;
  min-height: 140px;
  margin: 0;
  padding: 10px 12px;
  font-size: 12px;
  line-height: 1.55;
  color: #9a9ab8;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
}
.term--app .term__log span { display: block; }
.term--app .term__log .t-ok   { color: var(--green, #6ee787); }
.term--app .term__log .t-warn { color: var(--amber, #ffcc66); }
.term--app .term__log .t-hit  { color: var(--pink, #ff6ac1); }
.term--app .term__log .t-ps   { color: var(--cyan, #5ef1e8); }
.term--app .term__log .t-dim  { color: #55556e; }

.term--app .term__cmd {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-top: 1px solid var(--term-line);
  background: #0b0b13;
  flex: 0 0 auto;
}
.term--app .term__ps { flex: 0 0 auto; font-size: 12px; color: var(--cyan, #5ef1e8); }
.term--app .term__in {
  flex: 1 1 auto;
  min-width: 0;
  padding: 4px 0;
  font: inherit;
  font-size: 12px;
  color: var(--fg, #e8e0f0);
  background: transparent;
  border: 0;
  outline: none;
}

/* ------------------------------ TASKBAR TAB ----------------------------- */
/* The landing page has no header to put a taskbar entry in, so term.js makes a
   floating one. Pages with a header supply their own #consoleTab and this never
   gets created. (Not #termTab — that id belongs to /home's upload console.) */
.term__task {
  position: fixed;
  left: 14px;
  bottom: 14px;
  z-index: 2000;
  padding: 7px 12px;
  font-family: var(--mono, ui-monospace, Consolas, monospace);
  font-size: 11.5px;
  color: var(--cyan, #5ef1e8);
  background: #16162a;
  border: 1px solid var(--term-line);
  border-radius: 5px;
  cursor: pointer;
}
.term__task[hidden] { display: none; }
.term__task:hover { background: #1e1e38; }

/* ================================= CHAT ================================= */
.chat { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }

.chat__list {
  flex: 1 1 auto;
  min-height: 160px;
  margin: 0;
  padding: 10px 12px;
  list-style: none;
  overflow-y: auto;
}

.msg {
  padding: 6px 8px;
  margin-bottom: 6px;
  font-size: 12.5px;
  line-height: 1.5;
  border: 1px solid transparent;
  border-radius: 5px;
  word-break: break-word;
}
.msg--empty { color: #55556e; font-style: italic; }
.msg--mine { background: #101019; }

/* The blue outline is rendered ONLY for the person who was @'d — chat.js adds
   this class based on the viewer, so everyone else sees a plain message. */
.msg--tome {
  border-color: #4d8dff;
  background: rgba(77, 141, 255, 0.09);
  box-shadow: 0 0 0 1px rgba(77, 141, 255, 0.25);
}

.msg__quote {
  display: flex;
  gap: 6px;
  margin-bottom: 4px;
  padding: 4px 7px;
  font-size: 11px;
  color: #7c7c9c;
  background: #0e0e17;
  border-left: 2px solid #3a3a5c;
  border-radius: 3px;
}
.msg__quotewho { flex: 0 0 auto; color: var(--pink, #ff6ac1); }
.msg__quotebody { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg__quotegone { font-style: italic; color: #55556e; }

.msg__head { display: flex; align-items: center; gap: 7px; margin-bottom: 2px; }
/* The @ and the handle are separate buttons — @ replies, handle opens info.
   They sit flush so they still read as one name. */
.msg__who { display: inline-flex; align-items: baseline; }
.msg__atbtn,
.msg__id {
  padding: 0;
  font: inherit;
  font-size: 11.5px;
  color: var(--cyan, #5ef1e8);
  background: transparent;
  border: 0;
  cursor: pointer;
}
.msg__atbtn:hover,
.msg__id:hover { text-decoration: underline; }
.msg__atbtn:hover { color: var(--pink, #ff6ac1); }
.msg--admin .msg__atbtn,
.msg--admin .msg__id { color: var(--amber, #ffcc66); }

.msg__badge {
  padding: 1px 5px;
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #0a0a0f;
  background: var(--amber, #ffcc66);
  border-radius: 3px;
}
.msg__badge.is-user { background: var(--pink, #ff6ac1); }

.msg__ts { margin-left: auto; font-size: 10.5px; color: #4d4d66; }

.msg__mod {
  padding: 1px 5px;
  font: inherit;
  font-size: 10.5px;
  color: #8a8aa8;
  background: transparent;
  border: 1px solid var(--term-line);
  border-radius: 3px;
  cursor: pointer;
}
.msg__mod:hover { color: #fff; background: var(--red, #ff5c7a); border-color: var(--red, #ff5c7a); }
.msg__mod--ban { font-size: 10px; }

.msg__body { color: #cfcfe6; white-space: pre-wrap; }
.msg__at { color: #4d8dff; }
.msg__at.is-me { font-weight: 700; }
.msg__link { color: var(--pink, #ff6ac1); text-decoration: underline; }

.chat__replying {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  font-size: 11px;
  color: #7c7c9c;
  background: #0e0e17;
  border-top: 1px solid var(--term-line);
}
.chat__replying[hidden] { display: none; }
.chat__replywho { color: var(--cyan, #5ef1e8); }
.chat__replyx {
  margin-left: auto;
  padding: 0 4px;
  font: inherit;
  font-size: 10px;
  color: #8a8aa8;
  background: transparent;
  border: 0;
  cursor: pointer;
}
.chat__replyx:hover { color: var(--red, #ff5c7a); }

.chat__form {
  display: flex;
  gap: 7px;
  padding: 8px 12px;
  border-top: 1px solid var(--term-line);
  background: #0b0b13;
  flex: 0 0 auto;
}
.chat__in {
  flex: 1 1 auto;
  min-width: 0;
  padding: 6px 8px;
  font: inherit;
  font-size: 12px;
  color: var(--fg, #e8e0f0);
  background: #12121c;
  border: 1px solid var(--term-line);
  border-radius: 4px;
  outline: none;
}
.chat__in:focus { border-color: var(--cyan, #5ef1e8); }
.chat__send {
  flex: 0 0 auto;
  padding: 6px 12px;
  font: inherit;
  font-size: 11.5px;
  color: #0a0a0f;
  background: var(--cyan, #5ef1e8);
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}
.chat__send:hover { background: #8ff7f0; }

.chat__status {
  padding: 5px 12px 8px;
  font-size: 10.5px;
  color: #55556e;
  background: #0b0b13;
}
.chat__status.is-bad { color: var(--red, #ff5c7a); }
.chat__status.is-ok  { color: var(--green, #6ee787); }

/* ------------------------------- USER INFO ------------------------------ */
.uinfo {
  padding: 12px 14px;
  font-size: 12px;
  color: #cfcfe6;
  overflow-y: auto;
}
.uinfo__title {
  margin-bottom: 10px;
  font-size: 13px;
  color: var(--cyan, #5ef1e8);
}
.uinfo__load,
.uinfo__none { color: #55556e; font-style: italic; }

.uinfo__row {
  display: flex;
  gap: 10px;
  padding: 3px 0;
  border-bottom: 1px solid #16162a;
}
.uinfo__k { flex: 0 0 84px; color: #7c7c9c; }
.uinfo__v { flex: 1 1 auto; word-break: break-all; }
.uinfo__v.is-admin  { color: var(--amber, #ffcc66); }
.uinfo__v.is-banned { color: var(--red, #ff5c7a); font-weight: 700; }

.uinfo__sep {
  margin: 12px 0 6px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--amber, #ffcc66);
}
.uinfo__ban {
  margin-top: 12px;
  padding: 6px 12px;
  font: inherit;
  font-size: 11.5px;
  color: var(--red, #ff5c7a);
  background: transparent;
  border: 1px solid var(--red, #ff5c7a);
  border-radius: 4px;
  cursor: pointer;
}
.uinfo__ban:hover:not(:disabled) { color: #fff; background: var(--red, #ff5c7a); }
.uinfo__ban:disabled { opacity: 0.5; cursor: default; }

/* Admin-only kill switch, appended to the chat pane by adminchat.js. */
.chat__kill {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px 9px;
  font-size: 10.5px;
  background: #0b0b13;
  border-top: 1px solid var(--term-line);
}
.chat__killlabel { color: #55556e; }
.chat__killbtn {
  padding: 3px 9px;
  font: inherit;
  font-size: 10.5px;
  color: var(--green, #6ee787);
  background: transparent;
  border: 1px solid var(--term-line);
  border-radius: 3px;
  cursor: pointer;
}
.chat__killbtn.is-off { color: var(--red, #ff5c7a); border-color: var(--red, #ff5c7a); }
.chat__killbtn:hover { background: #16162a; }

/* Deliberately styled as dangerous, and pushed to the far end of the row so it
   isn't adjacent to anything you might click routinely. */
.chat__purge {
  margin-left: auto;
  padding: 3px 9px;
  font: inherit;
  font-size: 10.5px;
  color: var(--red, #ff5c7a);
  background: transparent;
  border: 1px solid var(--red, #ff5c7a);
  border-radius: 3px;
  cursor: pointer;
}
.chat__purge:hover:not(:disabled) { color: #fff; background: var(--red, #ff5c7a); }
.chat__purge:disabled { opacity: 0.5; cursor: default; }

/* Taskbar entry in the admin header, matching the one on /home. */
.bar__tab {
  padding: 4px 9px;
  font: inherit;
  font-size: 11px;
  color: var(--cyan, #5ef1e8);
  background: #16162a;
  border: 1px solid var(--term-line);
  border-radius: 4px;
  cursor: pointer;
}
.bar__tab[hidden] { display: none; }

@media (max-width: 520px) {
  .term--app { top: 60px; max-height: 78vh; }
  .msg__badge { display: none; }
}

/* Moderator badge — distinct from admin (amber) and user (pink). */
.msg__badge.is-mod { background: #7c5cff; color: #fff; }


/* ------------------------------ EASTER EGG ------------------------------ */
/* Shown by term.js when o, r, e, o are entered one at a time. Sits above the
   console window itself (z-index 2000) so nothing can cover it. */
.egg {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 5vmin;
  background: rgba(5, 5, 10, 0);
  opacity: 0;
  /* Only clickable once it is actually up. Removing .is-on to fade out also
     hands clicks straight back to the page, so the dying second of the fade
     can't swallow one. */
  pointer-events: none;
  transition: opacity 900ms ease, background-color 900ms ease;
}
.egg.is-on {
  opacity: 1;
  background: rgba(5, 5, 10, 0.82);
  pointer-events: auto;
  cursor: pointer;                   /* click anywhere to dismiss early */
}
.egg__img {
  max-width: min(80vw, 900px);
  max-height: 80vh;
  border-radius: 8px;
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.75);
  transform: scale(0.94);
  transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1);
}
.egg.is-on .egg__img { transform: scale(1); }

/* The fade survives reduced-motion, the drift does not.
 *
 * style.css collapses EVERY transition on the page to 0.001ms with !important,
 * so without the !important here the egg would snap in rather than fade — a
 * pop being precisely the thing a smooth transition was asked for to avoid.
 * This only wins because term.css is linked after style.css; if that order ever
 * changes, this silently stops working.
 *
 * Keeping opacity while dropping the scale is the same split used for the
 * comets and stars in sky.css: a cross-fade is not what reduced-motion is
 * about, movement through space is. */
@media (prefers-reduced-motion: reduce) {
  .egg {
    transition: opacity 900ms ease, background-color 900ms ease !important;
  }
  .egg__img,
  .egg.is-on .egg__img { transform: none !important; transition: none !important; }
}

/* ----------------------------- RESIZING -------------------------------- */
/* Desktop only, and gated on a fine pointer rather than width alone: a resize
   grip is a ~14px target that a finger cannot hit, so a large tablet would get
   a control it can't use. Both conditions have to hold.
 *
 * Native CSS resize rather than a scripted handle. The browser gives correct
 * pointer capture, edge detection and RTL behaviour for free, and it writes to
 * the element's own style — which the CSP allows, since it isn't an author
 * style attribute in markup.
 *
 * max-height has to be raised here. The base rule caps it at min(70vh, 620px),
 * and a max-height that low silently makes the grip refuse to grow the window
 * vertically — it looks broken rather than constrained. */
@media (min-width: 768px) and (pointer: fine) {
  .term--app {
    min-width: 380px;
    min-height: 260px;
    max-width: 96vw;
    max-height: 92vh;
  }
}

/* Resize corners. Deliberately invisible: no handle graphic, just a grab zone
   with the right cursor, which is how an actual window manager signals this.
 *
   14px is a compromise. Bigger is easier to hit, but the top-right corner sits
   under the close button — and since these are appended after the title bar
   they win the hit test. At this size the overlap is a few pixels and the X
   stays comfortably clickable. Do not enlarge these without checking that. */
.term--app .term__grip {
  position: absolute;
  width: 14px;
  height: 14px;
  z-index: 3;
}
.term--app .term__grip--nw { top: 0;    left: 0;  cursor: nwse-resize; }
.term--app .term__grip--ne { top: 0;    right: 0; cursor: nesw-resize; }
.term--app .term__grip--sw { bottom: 0; left: 0;  cursor: nesw-resize; }
.term--app .term__grip--se { bottom: 0; right: 0; cursor: nwse-resize; }

/* Full-height side strips, inset past the corners so those still win.
 *
   Only 5px wide, and that number is load-bearing rather than timid: the chat
   list scrolls, so its scrollbar sits flush against the window's right edge.
   A wider strip would sit on top of it and make scrolling hard to grab. 5px
   takes the outermost sliver and leaves the scrollbar usable. */
.term--app .term__grip--w,
.term--app .term__grip--e {
  top: 14px;
  bottom: 14px;
  width: 5px;
  height: auto;
  cursor: ew-resize;
}
.term--app .term__grip--w { left: 0; }
.term--app .term__grip--e { right: 0; }

/* Top and bottom, same idea rotated. The top strip sits over the title bar, so
   it is inset past the buttons on the right and kept thin — the bar still has
   to be draggable along its whole length. */
.term--app .term__grip--n,
.term--app .term__grip--s {
  left: 14px;
  right: 14px;
  width: auto;
  height: 5px;
  cursor: ns-resize;
}
.term--app .term__grip--n { top: 0; }
.term--app .term__grip--s { bottom: 0; }

/* Maximised. The caps below exist to keep a dragged window on screen; while
   maximised they would stop it reaching the edges, so they are lifted. */
.term--app.is-max {
  max-width: none;
  max-height: none;
  border-radius: 0;
}

/* Stops the log or a message being selected while a corner is dragged. */
.term--app.is-resizing { user-select: none; }

/* Room settings wheel. Sits at the far right of the owner-only control row,
   with a gap before it so a mis-click lands on nothing rather than on purge. */
.chat__cog {
  margin-left: 10px;
  padding: 3px 8px;
  font: inherit;
  font-size: 13px;
  line-height: 1;
  color: #8a8aa8;
  background: transparent;
  border: 1px solid var(--term-line);
  border-radius: 3px;
  cursor: pointer;
}
.chat__cog:hover { color: var(--cyan, #5ef1e8); border-color: var(--cyan, #5ef1e8); }

/* ---------------------------- ROOM SETTINGS ----------------------------- */
/* The settings tab. Same visual language as the console: labels dim, values
   cyan, everything monospaced. */
.cfg {
  padding: 14px 16px;
  font-size: 12.5px;
  color: #cfcfe6;
  overflow-y: auto;
}
.cfg__title {
  margin-bottom: 12px;
  font-size: 13px;
  color: var(--cyan, #5ef1e8);
}
.cfg__row {
  display: flex;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid #16162a;
}
.cfg__k { flex: 0 0 130px; color: #7c7c9c; }
.cfg__v { flex: 1 1 auto; color: var(--cyan, #5ef1e8); word-break: break-all; }

.cfg__note {
  margin: 12px 0 16px;
  font-size: 11.5px;
  line-height: 1.6;
  color: #7c7c9c;
}

.cfg__form { display: flex; flex-direction: column; gap: 5px; }
.cfg__label { font-size: 11px; color: #7c7c9c; }
.cfg__label + .cfg__in { margin-bottom: 8px; }
.cfg__in {
  padding: 7px 9px;
  font: inherit;
  font-size: 12.5px;
  color: var(--fg, #e8e0f0);
  background: #12121c;
  border: 1px solid var(--term-line);
  border-radius: 4px;
  outline: none;
}
.cfg__in:focus { border-color: var(--cyan, #5ef1e8); }
.cfg__in::placeholder { color: #4d4d66; }

.cfg__save {
  align-self: flex-start;
  margin-top: 4px;
  padding: 7px 14px;
  font: inherit;
  font-size: 12px;
  color: #0a0a0f;
  background: var(--cyan, #5ef1e8);
  border: 0;
  border-radius: 4px;
  cursor: pointer;
}
.cfg__save:hover:not(:disabled) { background: #8ff7f0; }
.cfg__save:disabled { opacity: 0.5; cursor: default; }

.cfg__msg { margin-top: 12px; font-size: 11.5px; line-height: 1.5; color: #7c7c9c; }
.cfg__msg.is-bad { color: var(--red, #ff5c7a); }
.cfg__msg.is-ok  { color: var(--green, #6ee787); }

/* ------------------------------ THE SHOWER ------------------------------ */
/* Triggered by the phrase command in term.js. Decorative, and deliberately
   click-through: it is up for 15 seconds, and an overlay that swallowed clicks
   would make the whole site unusable for that long. Dismissal is handled by a
   document listener in term.js instead. */
.burst {
  position: fixed;
  inset: 0;
  z-index: 3000;
  pointer-events: none;
  overflow: hidden;
  opacity: 1;
  /* The fade-out lives here rather than on each piece, so dismissing lets them
     carry on drifting as they go instead of freezing mid-air. */
  transition: opacity 650ms ease;
}
.burst.is-out { opacity: 0; }

.burst__bit {
  position: absolute;
  opacity: 0;
  will-change: transform, opacity;
  animation: burstFloat 15000ms cubic-bezier(0.33, 0, 0.25, 1) forwards;
}

/* The 5-stripe lesbian pride flag, drawn rather than approximated — there is
   no emoji for it, and the rainbow flag is a different flag. */
.burst__flag {
  width: 38px;
  height: 24px;
  border-radius: 3px;
  box-shadow: 0 3px 14px rgba(0, 0, 0, 0.5);
  background: linear-gradient(
    #D52D00 0 20%,
    #FF9A56 20% 40%,
    #FFFFFF 40% 60%,
    #D362A4 60% 80%,
    #A30262 80% 100%);
}
.burst__foot {
  font-size: 26px;
  line-height: 1;
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, 0.5));
}

/* Drifts to --dx/--dy over its life, wobbling either side of --rot on the way.
   Both offsets are randomised per piece and can be negative, so they wander in
   all directions rather than rising together. The middle keyframes are not on a
   straight line between start and end, which is what makes it read as floating
   rather than sliding. */
@keyframes burstFloat {
  0% {
    opacity: 0;
    transform: translate(0, 26px) scale(0.5) rotate(var(--rot, 0deg));
  }
  6% {
    opacity: 1;
    transform: translate(0, 0) scale(1) rotate(var(--rot, 0deg));
  }
  /* The middle stops swing OFF the direct line, using a per-piece --sway. A
     straight interpolation from start to end reads as a slide; the detour is
     what makes it look like drifting. */
  27% {
    transform: translate(calc(var(--dx, 0px) * 0.3 + var(--sway, 0px)),
                         calc(var(--dy, 0px) * 0.28))
               rotate(calc(var(--rot, 0deg) + var(--spin, 8deg)));
  }
  48% {
    transform: translate(calc(var(--dx, 0px) * 0.55 - var(--sway, 0px) * 0.7),
                         calc(var(--dy, 0px) * 0.5))
               rotate(calc(var(--rot, 0deg) - var(--spin, 8deg)));
  }
  70% {
    transform: translate(calc(var(--dx, 0px) * 0.78 + var(--sway, 0px) * 0.5),
                         calc(var(--dy, 0px) * 0.76))
               rotate(calc(var(--rot, 0deg) + var(--spin, 8deg) * 0.6));
  }
  88% {
    opacity: 1;
    transform: translate(calc(var(--dx, 0px) * 0.94 - var(--sway, 0px) * 0.3),
                         calc(var(--dy, 0px) * 0.92))
               rotate(calc(var(--rot, 0deg) - var(--spin, 8deg) * 0.4));
  }
  100% {
    opacity: 0;
    transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(0.9)
               rotate(var(--rot, 0deg));
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The float SURVIVES reduced motion. Same call Kim made for the comets and
     stars in sky.css: this is opt-in flair, triggered by deliberately typing a
     phrase, not motion imposed on someone trying to read a page.
     style.css collapses every animation to 0.001ms with !important, so the
     duration has to be re-asserted here or the whole thing finishes in a single
     frame. LONGHAND, not the `animation:` shorthand - that would also reset
     animation-delay and wipe the per-piece stagger term.js sets inline,
     landing all 34 at the same instant. */
  .burst__bit {
    animation-name: burstFloat !important;
    animation-duration: 15000ms !important;
    animation-timing-function: cubic-bezier(0.33, 0, 0.25, 1) !important;
    animation-fill-mode: forwards !important;
    animation-iteration-count: 1 !important;
  }
}

/* =============================== EVIL MODE =============================== */
/* Toggled by the `evil` command. A MODE rather than a moment: it stays until
   you type it again, which is why it is a class on <html> and not an overlay.
 *
 * THE CONSOLE IS EXEMPT, by request and because it reads better — the terminal
 * is the thing doing this, so it being the one surface still intact is the
 * right way round. The reset at the bottom is what enforces that, and it has to
 * come last.
 *
 * Nothing here strobes. The shifts are continuous rather than on/off, and no
 * rule flips luminance at speed — a hard flicker is the one effect that can
 * actually hurt someone, and it is not needed to read as broken. */

/* The face, and its tears, go red. It is an SVG driven by `fill`, so this is a
   clean swap rather than a filter over the top. */
.is-evil .ttg {
  fill: #ff2020;
  filter: drop-shadow(0 0 22px rgba(255, 20, 20, 0.75));
  animation: evilFacePulse 2.4s ease-in-out infinite;
}
.is-evil .ttg__tear { fill: #ff2020; }

@keyframes evilFacePulse {
  0%, 100% { filter: drop-shadow(0 0 18px rgba(255, 20, 20, 0.55)); }
  50%      { filter: drop-shadow(0 0 34px rgba(255, 40, 40, 0.95)); }
}

/* Text corruption: the red/cyan channels pull apart and drift. Applied to text
   elements by name rather than with a universal selector, so images, the sky
   and the window chrome are left alone. */
.is-evil h1,
.is-evil h2,
.is-evil h3,
.is-evil p,
.is-evil li,
.is-evil dt,
.is-evil dd,
.is-evil label,
.is-evil .replay,
.is-evil .foot__login,
.is-evil .foot__console,
.is-evil .foot__c,
.is-evil .brand,
.is-evil .tagline {
  animation: evilSplit 1.9s steps(12, end) infinite;
}

@keyframes evilSplit {
  0%   { text-shadow:  1px 0 rgba(255,0,40,.85),  -1px 0 rgba(0,230,255,.85); }
  20%  { text-shadow:  3px 0 rgba(255,0,40,.85),  -2px 0 rgba(0,230,255,.85); transform: translateX(-1px); }
  40%  { text-shadow: -2px 0 rgba(255,0,40,.85),   3px 0 rgba(0,230,255,.85); }
  60%  { text-shadow:  2px 1px rgba(255,0,40,.85), -3px -1px rgba(0,230,255,.85); transform: translateX(1px); }
  80%  { text-shadow: -3px 0 rgba(255,0,40,.85),   2px 0 rgba(0,230,255,.85); }
  100% { text-shadow:  1px 0 rgba(255,0,40,.85),  -1px 0 rgba(0,230,255,.85); }
}

/* A faint red wash over the whole page, under everything. Sells "the site
   turned" rather than "some text is broken". */
.is-evil body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 40%,
              rgba(255, 0, 20, 0.05) 0%, rgba(120, 0, 10, 0.20) 100%);
}

/* THE EXEMPTION. Must stay last so it beats everything above. The console keeps
   its own colours, no split, no wash, no pulse — it is the one thing in the
   room that still works. */
.is-evil .term--app,
.is-evil .term--app * {
  animation-name: none !important;
  text-shadow: none !important;
  filter: none !important;
}
/* transform is reset on the window's CONTENTS but never on the window itself.
   Its drag position lives in an inline transform, and `transform: none` here
   snapped a dragged window back to centre the instant evil mode came on. An
   earlier attempt used `transform: revert`, which is just as wrong — revert
   discards author styles, and the inline transform is an author style. */
.is-evil .term--app * { transform: none !important; }

@media (prefers-reduced-motion: reduce) {
  /* The corruption stays visible but stops moving: the colour split holds at a
     fixed offset instead of jittering. Unlike the drifting flags, a rapid
     positional jitter on TEXT is exactly what reduced motion is asking to be
     spared, and the effect still reads as broken when it is static. */
  .is-evil h1, .is-evil h2, .is-evil h3, .is-evil p, .is-evil li,
  .is-evil dt, .is-evil dd, .is-evil label, .is-evil .replay,
  .is-evil .foot__login, .is-evil .foot__console, .is-evil .foot__c,
  .is-evil .brand, .is-evil .tagline {
    animation: none !important;
    text-shadow: 2px 0 rgba(255,0,40,.85), -2px 0 rgba(0,230,255,.85) !important;
  }
  .is-evil .ttg { animation: none !important; }
}

/* --------------------------- EVIL: STATIC LAYER -------------------------- */
/* Built by term.js when evil mode turns on, and removed when it turns off, so
   there is never a hidden overlay sitting over the page the rest of the time.
   Click-through throughout — this must not make the site unusable. */
.evilfx {
  position: fixed;
  inset: 0;
  z-index: 1500;          /* over the page, under the console at 2000 */
  pointer-events: none;
  overflow: hidden;
}

/* Film-grain noise. SVG feTurbulence as a data URI: CSP allows data: under
   img-src, and a background is cheaper than an <img> or a canvas loop. */
.evilfx__static {
  position: absolute;
  inset: -50%;
  opacity: 0.16;
  mix-blend-mode: screen;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: evilGrain 0.6s steps(4, end) infinite;
}

/* Shifting the whole layer by a few pixels re-seeds where the grain lands,
   which is what makes it crawl. Cheaper than regenerating noise per frame. */
@keyframes evilGrain {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-8px, 6px); }
  50%  { transform: translate(5px, -9px); }
  75%  { transform: translate(-6px, -4px); }
  100% { transform: translate(0, 0); }
}

/* Scanlines, plus a brighter band that sweeps down the page like a bad signal.
   Slow on purpose: a fast sweep starts to read as a flash. */
.evilfx__scan {
  position: absolute;
  inset: 0;
  opacity: 0.5;
  background:
    linear-gradient(rgba(255, 40, 40, 0) 0%, rgba(255, 60, 60, 0.10) 48%,
                    rgba(255, 60, 60, 0.16) 50%, rgba(255, 40, 40, 0) 52%,
                    rgba(255, 40, 40, 0) 100%),
    repeating-linear-gradient(rgba(0, 0, 0, 0) 0 2px, rgba(0, 0, 0, 0.22) 2px 4px);
  background-size: 100% 300%, 100% 4px;
  background-repeat: no-repeat, repeat;
  animation: evilSweep 7s linear infinite;
}
@keyframes evilSweep {
  0%   { background-position: 0 -100%, 0 0; }
  100% { background-position: 0 200%, 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* Grain and sweep hold still. The corruption is still plainly visible as a
     static overlay, and a crawling full-screen texture is exactly the sort of
     thing this setting is asking to be spared. */
  .evilfx__static, .evilfx__scan { animation: none !important; }
}

/* ============================== VIKING MODE ============================== */
/* Toggled by the `viking` command. Text is transliterated to Elder Futhark in
   JS; this only makes sure a font that COVERS the Runic block gets picked, and
   gives the page a colder, carved feel.

   No @font-face and no download. Segoe UI Historic ships with Windows, Noto
   Sans Runic with most Linux distros, and Apple Symbols covers it on macOS. A
   webfont would mean an asset, a font-src change, and a flash of missing
   glyphs — for something already installed. */
.is-viking body {
  font-family: 'Segoe UI Historic', 'Noto Sans Runic', 'Junicode',
               'Apple Symbols', var(--mono, monospace);
  letter-spacing: 0.06em;
}

/* Runes are carved, not printed — a cold stone tint and a faint chisel shadow
   read better than the site's usual neon. */
.is-viking h1,
.is-viking h2,
.is-viking h3,
.is-viking .brand,
.is-viking .tagline {
  color: #cfd8dc;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.9), 0 0 18px rgba(150, 190, 210, 0.25);
}

/* Same exemption as evil mode, and for the same reason: the console is the
   thing you type the command into, so it has to stay readable. Must come after
   the rules above. */
.is-viking .term--app,
.is-viking .term--app * {
  font-family: var(--mono, ui-monospace, Consolas, monospace) !important;
  letter-spacing: normal !important;
  text-shadow: none !important;
}
