/*
  Copyright (C) 2026 Christian Redl
  SPDX-License-Identifier: GPL-3.0-or-later

  This file is from xresearch.net and is published for entertainment and
  educational interest. It is not intended for production or safety-critical use.

  This file is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You can find a copy of the GNU General Public License at
  <https://www.gnu.org/licenses/>.
*/

/* ==========================================================================
   Cross Research Network — dark theme
   Design tokens first, then layout. No framework, no webfonts.
   ========================================================================== */

:root {
  /* Backdrop pushed deep (near-black, not flat-black) so there is real
     luminance headroom for elevated surfaces and coloured glow to sit
     above it — a card just ~10 units lighter than the page reads flat;
     these tokens deliberately widen that gap by 3-4x. */
  --bg: #05070b;
  --bg-elevated: #171c28;
  --bg-hover: #232a3c;
  --border: #2a3142;
  --border-strong: #3d4760;
  --text: #e6e9ef;
  --text-muted: #8b94a7;
  --text-dim: #646e82;

  --accent: #4dd0e1;
  --accent-2: #a78bfa;
  --accent-3: #2dd4bf;
  --accent-grad: linear-gradient(135deg, #4dd0e1 0%, #a78bfa 100%);
  --accent-soft: rgba(77, 208, 225, 0.12);

  --ok: #4ade80;
  --danger: #f87171;

  --radius: 14px;
  --radius-sm: 8px;
  --maxw: 820px;

  /* Card surface itself: a visibly lighter slab, brighter at the top edge
     (the "light source" the rim highlight below implies) fading to a
     still-comfortably-dark base. */
  --card-top: #1c2230;
  --card-bottom: #161b26;

  /* Layered card elevation: a bright inset rim light sells the top edge
     catching light, a hairline inner edge keeps the border crisp, and
     modest dark drop-shadows carry the lift. A soft cyan halo at low
     alpha sits under all of that at rest so every card reads as lit
     from within and floating above the backdrop; a single box-shadow
     layer with this blur radius is cheap to paint even across a feed
     that can grow past 60 cards. :hover scales the halo up (larger,
     more opaque, violet) so the lift stays clearly a step above rest. */
  --shadow-card: inset 0 1px 0 rgba(255, 255, 255, 0.09),
    inset 0 0 0 1px rgba(255, 255, 255, 0.02), 0 1px 1px rgba(0, 0, 0, 0.4),
    0 8px 20px -10px rgba(0, 0, 0, 0.6), 0 16px 28px -16px rgba(0, 0, 0, 0.5),
    0 0 60px -22px rgba(77, 208, 225, 0.16);
  --shadow-card-hover: inset 0 1px 0 rgba(255, 255, 255, 0.14),
    inset 0 0 0 1px rgba(255, 255, 255, 0.04), 0 2px 5px rgba(0, 0, 0, 0.45),
    0 14px 26px -12px rgba(0, 0, 0, 0.65), 0 24px 30px -18px rgba(0, 0, 0, 0.55),
    0 0 40px -6px rgba(167, 139, 250, 0.45);

  /* Driven by app.js on scroll (rAF-throttled); falls back to 0 so the
     page still looks intentional with JS disabled or motion reduced. */
  --scroll-progress: 0;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  /* Static base: a faint upper wash plus a lower vignette, under the
     scroll-driven mesh layers below, so the page has depth even before
     any blob renders (progressive-enhancement baseline). */
  background: radial-gradient(
      ellipse 140% 70% at 50% -8%,
      rgba(28, 34, 48, 0.55) 0%,
      transparent 55%
    ),
    radial-gradient(
      ellipse 160% 90% at 50% 115%,
      rgba(0, 0, 0, 0.55) 0%,
      transparent 60%
    ),
    var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* Mesh-gradient backdrop, upper layer — cyan/teal-dominant. Both mesh
   layers are fixed + pointer-events:none, sit at z-index 0 behind the
   z-index:1 content, and use `inset:0` (never vw/vh on width) so they
   can never introduce horizontal scroll.

   Perf note: this used to animate `filter: hue-rotate()` and use
   `mix-blend-mode: screen` on these full-viewport fixed layers, driven
   every scroll frame. Both are extremely expensive — an animated filter
   forces a full-viewport repaint every frame, and a blend mode forces
   non-composited compositing of everything underneath — and together
   they starved the main thread badly enough that the rAF loop stopped
   running at all. Neither is used any more. Instead, the two layers
   keep their own fixed, pre-mixed colour palette (this one cyan/teal
   weighted, body::after below violet weighted) and --scroll-progress
   (written by app.js, 0->1->0 triangle wave) only drives `opacity`
   (crossfade) and `transform` (drift/scale) — both compositor-only
   properties that skip layout and paint entirely. Fading between two
   differently-tinted static layers reads as the palette evolving while
   scrolling, at a tiny fraction of the cost of an animated filter. It
   falls back to progress 0 with JS disabled or motion reduced, which
   still looks intentional (this layer's resting opacity is the higher
   one, so the page defaults to the cyan/teal mix). */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  background: radial-gradient(
      circle at 16% 20%,
      rgba(77, 208, 225, 0.18) 0%,
      transparent 40%
    ),
    radial-gradient(circle at 82% 14%, rgba(167, 139, 250, 0.16) 0%, transparent 44%),
    radial-gradient(circle at 50% 48%, rgba(45, 212, 191, 0.12) 0%, transparent 46%);
  transform: translate3d(0, calc(var(--scroll-progress) * 60px), 0)
    scale(calc(1 + var(--scroll-progress) * 0.06));
  opacity: calc(0.85 - var(--scroll-progress) * 0.35);
  transition: transform 0.6s ease-out, opacity 0.6s ease-out;
  pointer-events: none;
  z-index: 0;
  will-change: transform, opacity;
}

/* Mesh-gradient backdrop, lower layer — violet/cyan-dominant, weighted
   toward the bottom of the viewport, moving independently of the layer
   above so the two visibly cross-fade against each other (this layer
   fades IN with scroll while body::before fades out) rather than moving
   as one flat unit. Same rule as above: only transform + opacity are
   driven by --scroll-progress, no filter, no blend mode. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  background: radial-gradient(
      circle at 22% 82%,
      rgba(167, 139, 250, 0.2) 0%,
      transparent 42%
    ),
    radial-gradient(circle at 88% 70%, rgba(77, 208, 225, 0.16) 0%, transparent 44%),
    radial-gradient(circle at 45% 92%, rgba(45, 212, 191, 0.13) 0%, transparent 40%);
  transform: translate3d(
    calc(var(--scroll-progress) * -40px),
    calc(var(--scroll-progress) * -90px),
    0
  );
  opacity: calc(0.3 + var(--scroll-progress) * 0.6);
  transition: transform 0.6s ease-out, opacity 0.6s ease-out;
  pointer-events: none;
  z-index: 0;
  will-change: transform, opacity;
}

a {
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 20px;
  background: var(--accent);
  color: #04070a;
  font-weight: 600;
  border-radius: 0 0 var(--radius-sm) 0;
}

.skip-link:focus {
  left: 0;
}

/* ==========================================================================
   Header
   ========================================================================== */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 24px;
  background: rgba(5, 7, 11, 0.72);
  backdrop-filter: blur(14px) saturate(160%);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  border-bottom: 1px solid var(--border);
}

.wordmark {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  min-width: 0;
}

.wordmark__text {
  font-size: 15px;
  font-weight: 650;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  white-space: nowrap;
}

/* ==========================================================================
   Menu
   ========================================================================== */

.menu {
  position: relative;
  flex: none;
}

.menu__button {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 15px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  font-weight: 550;
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}

.menu__button:hover {
  background: var(--bg-hover);
  border-color: var(--accent);
}

.menu__bars {
  display: grid;
  gap: 4px;
  width: 16px;
}

.menu__bars span {
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.menu__button[aria-expanded="true"] .menu__bars span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}

.menu__button[aria-expanded="true"] .menu__bars span:nth-child(2) {
  opacity: 0;
}

.menu__button[aria-expanded="true"] .menu__bars span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

.menu__panel {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  /* Wide enough that the indented submenu entries still fit their
     one-line descriptions rather than wrapping to three. */
  min-width: 268px;
  padding: 7px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 20px 50px -12px rgba(0, 0, 0, 0.65), 0 0 60px -20px rgba(77, 208, 225, 0.12);
  opacity: 0;
  transform: translateY(-8px) scale(0.98);
  transform-origin: top right;
  visibility: hidden;
  /* Belt and braces: the active view's .menu__view__inner sets
     visibility: visible on itself, which overrides this panel's
     visibility: hidden for hit-testing. Without this, the root links
     stay clickable (just invisible) while the panel is closed. */
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
}

.menu__panel[data-open="true"] {
  opacity: 1;
  transform: translateY(0) scale(1);
  visibility: visible;
  pointer-events: auto;
}

.menu__link {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--text);
  font-size: 14.5px;
  font-weight: 500;
  transition: background 0.15s ease;
}

.menu__link:hover {
  background: var(--bg-hover);
}

.menu__link svg {
  flex: none;
  width: 17px;
  height: 17px;
  color: var(--accent);
}

.menu__link small {
  display: block;
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.3;
}

/* --- Drill-down groups ---------------------------------------------------
   The panel shows one level at a time and swaps its whole contents, rather
   than revealing a nested list in place. Expanding in place is what makes
   these menus run off the bottom of the screen; here the panel only ever
   stands as tall as the level you are looking at.

   Both levels occupy the same grid cell, and the inactive one collapses
   its row to 0fr. That means the panel's height is always the active
   level's height and animates between the two without any JS measuring —
   and, unlike absolute positioning, the collapsed level contributes
   nothing to layout. */

.menu__views {
  display: grid;
}

.menu__view {
  grid-area: 1 / 1;
  /* Without this a collapsed level still stretches to fill the shared
     grid cell — grid items default to align-self: stretch — so it sits
     invisibly on top of the visible level and swallows every click.
     Zero content height is not zero box height. */
  align-self: start;
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.24s ease;
}
.menu__view[data-active="true"] {
  grid-template-rows: 1fr;
}
/* Belt and braces: the hidden level is never a pointer target, whatever
   height it ends up with mid-transition. */
.menu__view[data-active="false"] {
  pointer-events: none;
}

/* min-height:0 lets the row actually collapse; visibility keeps the
   inactive level's links out of the tab order. */
.menu__view__inner {
  overflow: hidden;
  min-height: 0;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.16s ease, transform 0.24s ease, visibility 0.24s;
}
.menu__view[data-active="true"] .menu__view__inner {
  visibility: visible;
  opacity: 1;
}
/* Slide the levels apart along the axis you travel, so going deeper reads
   as moving right and coming back reads as moving left.

   These must be qualified with [data-active="false"]. Written as
   `.menu__view[data-view="root"] .menu__view__inner` they tie on
   specificity with the active rule above and, coming later, win it —
   which leaves every level permanently shifted by its own offset and the
   two levels 28px out of line with each other. */
.menu__view[data-view="root"][data-active="false"] .menu__view__inner {
  transform: translateX(-14px);
}
.menu__view[data-view="play"][data-active="false"] .menu__view__inner {
  transform: translateX(14px);
}

.menu__back {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin-bottom: 4px;
  padding: 9px 13px;
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.menu__back:hover {
  color: var(--text);
  background: var(--bg-hover);
}
.menu__back svg {
  flex: none;
  width: 15px;
  height: 15px;
  color: var(--accent);
}

/* Button reset for menu entries that act rather than navigate. */
.menu__link--parent,
.menu__link--toggle {
  width: 100%;
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-size: 14.5px;
  font-weight: 500;
  text-align: left;
  cursor: pointer;
}

/* The switched-on state has to survive a greyscale panel, so it gets a
   filled icon and a bold label rather than just an accent tint. */
.menu__link--toggle[aria-pressed="true"] {
  background: var(--accent-soft);
}
.menu__link--toggle[aria-pressed="true"] span {
  font-weight: 650;
}

/* Needs the extra class to outrank `.menu__link svg`, which would
   otherwise paint the chevron accent-coloured at the icon size. */
.menu__link .menu__chevron {
  margin-left: auto;
  width: 15px;
  height: 15px;
  color: var(--text-dim);
  transition: transform 0.2s ease, color 0.2s ease;
}
/* The chevron points into the group rather than rotating open, because
   the panel navigates sideways now instead of unfolding. */
.menu__link--parent:hover .menu__chevron {
  color: var(--accent);
  transform: translateX(2px);
}

@media (prefers-reduced-motion: reduce) {
  .menu__view,
  .menu__view__inner,
  .menu__link .menu__chevron {
    transition: none;
  }
  /* Matches the offset rules' specificity so it can actually override
     them; a bare `.menu__view .menu__view__inner` would not. */
  .menu__view[data-view][data-active="false"] .menu__view__inner {
    transform: none;
  }
}

/* ==========================================================================
   Page shell
   ========================================================================== */

.shell {
  position: relative;
  z-index: 1;
  max-width: var(--maxw);
  margin: 0 auto;
  /* Top padding is intentionally modest: on index.html the filter chips
     are the first thing under the sticky header, so they shouldn't float
     in a large empty gap; on about.html .page-intro supplies its own
     visual weight right below it. */
  padding: 28px 24px 80px;
}

.page-intro {
  margin-bottom: 30px;
}

.page-intro h1 {
  margin: 0 0 10px;
  font-size: clamp(26px, 5vw, 38px);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
}

.page-intro p {
  margin: 0;
  max-width: 62ch;
  color: var(--text-muted);
  font-size: 16px;
}

/* Filter chips
   ------------------------------------------------------------------------
   This bar reads as a floating "window" panel using the same design
   language as the feed .card surfaces (card-top/card-bottom gradient,
   --border, --radius, --shadow-card), sitting sticky just under the
   sticky .site-header, with feed cards scrolling underneath it.

   That underlap is exactly what made the old flat strip risky: a
   rounded, shadowed panel clips its own background at the 4 corner
   arcs and lets its shadow bleed past its edges, and simply painting
   the card gradient as `.filters`' own background would leave those
   slivers transparent — a card scrolled to that exact position would
   show (title text and all) through the gap, which is precisely the
   "cards bleeding through" bug that was fixed here before by keeping
   the strip fully opaque.

   So opacity is split across two layers instead of one:
     1. `.filters` itself keeps an opaque, blurred, header-matching
        rectangle (no border-radius) as its own background. An
        element's own background always paints at the very bottom of
        its stacking context, beneath every descendant — including
        generated content — so this backstop can never be covered by
        anything scrolling behind the sticky bar, and it has no
        rounded corners to clip a gap into.
     2. `.filters::before` is the visible rounded "window": the
        card-style gradient, border and shadow, absolutely positioned
        to exactly fill that same rectangle on top of layer 1. Its
        rounded corners clip to reveal layer 1 (still opaque) in the
        tiny corner arcs, never a card underneath.
   The chips are real content, stacked above both via z-index. */

.filters {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 10px;
  position: sticky;
  top: 61px;
  z-index: 20;
  margin-bottom: 26px;
  padding: 15px 18px;
  background: rgba(5, 7, 11, 0.92);
  backdrop-filter: blur(14px) saturate(160%);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
}

.filters::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Square corners: the panel reads as a rectangular window, not a card. */
  border-radius: 0;
  background: linear-gradient(180deg, var(--card-top) 0%, var(--card-bottom) 100%);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-card);
  pointer-events: none;
}

.chip {
  position: relative;
  z-index: 1;
  padding: 7px 15px;
  /* The panel behind these chips (--card-top/--card-bottom) is much
     lighter than the old near-black strip, so --bg-elevated (which
     sits close to the panel in luminance) would nearly disappear
     against it. A translucent dark fill reads as an inset pill on top
     of the panel instead, at any panel shade. */
  background: rgba(5, 7, 11, 0.55);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text-muted);
  font: inherit;
  font-size: 13.5px;
  font-weight: 550;
  cursor: pointer;
  transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}

.chip:hover {
  color: var(--text);
  border-color: var(--accent);
  background: rgba(5, 7, 11, 0.4);
}

.chip[aria-pressed="true"] {
  /* Stronger than --accent-soft's 0.12 alpha (used elsewhere against the
     near-black page background): against the lighter panel surface that
     alpha read as barely-there, so the active fill is boosted here only. */
  background: rgba(77, 208, 225, 0.2);
  border-color: var(--accent);
  color: var(--accent);
}

/* ==========================================================================
   Feed
   ========================================================================== */

.feed {
  display: grid;
  /* minmax(0, 1fr) rather than a bare 1fr: grid items default to
     min-width:auto, so a single long unbreakable token in third-party text
     would otherwise stretch the track past the viewport. */
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
}

.card {
  position: relative;
  min-width: 0;
  overflow-wrap: anywhere;
  padding: 22px;
  /* Card surface sits ~3-4x further above the backdrop than a flat
     elevated colour would (see --card-top/--card-bottom), brighter at
     the top edge to read as a surface catching light from above; the
     rim-light inset and coloured halo in --shadow-card carry the rest. */
  background: linear-gradient(180deg, var(--card-top) 0%, var(--card-bottom) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  transition: border-color 0.2s ease, transform 0.2s ease,
    background 0.2s ease, box-shadow 0.2s ease;
  animation: card-in 0.4s ease both;
}

.card:hover {
  background: linear-gradient(180deg, var(--bg-hover) 0%, var(--card-top) 100%);
  border-color: var(--border-strong);
  transform: translateY(-6px);
  box-shadow: var(--shadow-card-hover);
}

@keyframes card-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 11px;
  font-size: 12.5px;
  color: var(--text-dim);
}

.tag {
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--text-muted);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.tag--renewables {
  color: #4ade80;
  border-color: rgba(74, 222, 128, 0.35);
}

.tag--cs {
  color: #60a5fa;
  border-color: rgba(96, 165, 250, 0.35);
}

.tag--photonics {
  color: #f0abfc;
  border-color: rgba(240, 171, 252, 0.35);
}

.tag--semiconductors {
  color: #fbbf24;
  border-color: rgba(251, 191, 36, 0.35);
}

.tag--oa {
  color: var(--ok);
  border-color: rgba(74, 222, 128, 0.35);
}

.tag--hn {
  color: #fb923c;
  border-color: rgba(251, 146, 60, 0.35);
}

.tag--ai {
  color: #fb7185;
  border-color: rgba(251, 113, 133, 0.35);
}

.card__dot {
  color: var(--border-strong);
}

.card__title {
  margin: 0 0 9px;
  font-size: 18.5px;
  font-weight: 640;
  line-height: 1.38;
  letter-spacing: -0.01em;
}

.card__title a {
  text-decoration: none;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-size: 0% 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size 0.25s ease, color 0.2s ease;
}

.card__title a:hover {
  color: var(--accent);
  background-size: 100% 1px;
}

.card__authors {
  margin: 0 0 9px;
  color: var(--text-muted);
  font-size: 13.5px;
}

.card__abstract {
  margin: 0;
  color: var(--text-muted);
  font-size: 14.5px;
  line-height: 1.62;
}

.card__footer {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--text-dim);
}

.card__footer a {
  color: var(--text-muted);
  text-decoration: none;
}

.card__footer a:hover {
  color: var(--accent);
}

.card__source {
  color: var(--text-muted);
  font-weight: 500;
}

/* ==========================================================================
   Feed states — skeleton, error, end
   ========================================================================== */

.skeleton {
  padding: 22px;
  background: linear-gradient(180deg, var(--card-top) 0%, var(--card-bottom) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.skeleton__line {
  height: 11px;
  margin-bottom: 10px;
  border-radius: 5px;
  background: linear-gradient(
    90deg,
    #1d2330 25%,
    #262e40 50%,
    #1d2330 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite linear;
}

.skeleton__line:last-child {
  margin-bottom: 0;
}

@keyframes shimmer {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}

.notice {
  padding: 26px 22px;
  background: linear-gradient(180deg, var(--card-top) 0%, var(--card-bottom) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  text-align: center;
  color: var(--text-muted);
  font-size: 14.5px;
}

.notice--error {
  border-color: rgba(248, 113, 113, 0.35);
}

.notice strong {
  display: block;
  margin-bottom: 6px;
  color: var(--text);
  font-size: 15.5px;
}

.notice button {
  margin-top: 14px;
  padding: 9px 20px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: 10px;
  color: var(--accent);
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.16s ease;
}

.notice button:hover {
  background: rgba(77, 208, 225, 0.22);
}

.sentinel {
  height: 1px;
}

/* ==========================================================================
   About page
   ========================================================================== */

.prose h2 {
  margin: 38px 0 12px;
  font-size: 20px;
  font-weight: 640;
  letter-spacing: -0.01em;
}

.prose p {
  margin: 0 0 14px;
  max-width: 68ch;
  color: var(--text-muted);
}

.prose a {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid rgba(77, 208, 225, 0.35);
}

.prose a:hover {
  border-bottom-color: var(--accent);
}

.panel {
  padding: 22px;
  background: linear-gradient(180deg, var(--card-top) 0%, var(--card-bottom) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

/* Contact panel: label on the left, mailto action on the right, stacking on
   narrow screens so the address never has to shrink to fit. */
.contact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  margin: 20px 0 22px;
}

.contact strong {
  display: block;
  margin-bottom: 3px;
  font-size: 15.5px;
  font-weight: 600;
}

.contact span {
  color: var(--text-muted);
  font-size: 14px;
}

.button {
  display: inline-block;
  padding: 11px 20px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: 10px;
  color: var(--accent);
  font-size: 14.5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.16s ease, transform 0.16s ease;
}

.button:hover {
  background: rgba(77, 208, 225, 0.22);
  transform: translateY(-1px);
}

/* The prose link rule would otherwise draw an underline through the button. */
.prose .button {
  border-bottom: 1px solid var(--accent);
}

.topic-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 14px;
  margin: 20px 0 8px;
  padding: 0;
  list-style: none;
}

.topic-grid li {
  padding: 16px 18px;
  background: linear-gradient(180deg, var(--card-top) 0%, var(--card-bottom) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 10px 22px -10px rgba(0, 0, 0, 0.55), 0 0 40px -18px rgba(167, 139, 250, 0.1);
}

.topic-grid strong {
  display: block;
  margin-bottom: 4px;
  font-size: 14.5px;
  font-weight: 600;
}

.topic-grid span {
  color: var(--text-dim);
  font-size: 13px;
}

/* ==========================================================================
   Footer
   ========================================================================== */

.site-footer {
  position: relative;
  z-index: 1;
  padding: 30px 24px 46px;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 13px;
  text-align: center;
}

.site-footer a {
  color: var(--text-muted);
  text-decoration: none;
}

.site-footer a:hover {
  color: var(--accent);
}

/* ==========================================================================
   Responsive & motion
   ========================================================================== */

@media (max-width: 560px) {
  .site-header {
    padding: 12px 16px;
  }

  .wordmark__text {
    font-size: 11.5px;
    letter-spacing: 0.1em;
    white-space: normal;
  }

  .shell {
    padding: 20px 16px 64px;
  }

  .filters {
    top: 57px;
    gap: 7px;
    padding: 12px 14px;
    /* space-between spreads a single, oddly-sized final row (e.g. 2
       leftover chips) edge to edge, which looks ragged once the chips
       wrap here. Centring each wrapped row stays tidy at any chip count. */
    justify-content: center;
  }

  .card {
    padding: 18px;
  }

  .card__title {
    font-size: 17px;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .card:hover {
    transform: none;
  }
}

/* ==========================================================================
   Encrypt page
   ========================================================================== */

.notice-inline {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 26px;
}

.notice-inline strong {
  font-size: 15px;
  font-weight: 640;
}

.notice-inline span {
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1.6;
}

.tool {
  margin-bottom: 34px;
}

.tool h2 {
  margin: 0 0 14px;
  font-size: 17px;
  font-weight: 640;
  letter-spacing: -0.01em;
}

.tool h2:not(:first-child) {
  margin-top: 30px;
  padding-top: 26px;
  border-top: 1px solid var(--border);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
  min-width: 0;
}

.field label {
  color: var(--text-muted);
  font-size: 13.5px;
  font-weight: 600;
}

.textarea {
  width: 100%;
  min-width: 0;
  max-width: 100%;
  padding: 12px 14px;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  line-height: 1.55;
  resize: vertical;
  transition: border-color 0.16s ease;
  /* Armored keys/messages are long unbroken base64 lines in the worst case
     (e.g. a paste with stripped newlines) — break them inside the box
     rather than letting the textarea (or the page) grow wider. */
  overflow-wrap: anywhere;
  word-break: break-word;
}

.textarea:hover {
  border-color: var(--text-dim);
}

.textarea:focus {
  border-color: var(--accent);
}

.textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.textarea--key,
.textarea--output {
  font-family: var(--mono);
  font-size: 12.5px;
}

.textarea--output {
  color: var(--text-muted);
}

.output-wrap {
  min-width: 0;
  max-width: 100%;
}

.tool__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  margin: 4px 0 6px;
}

.button--primary {
  background: var(--accent-grad);
  border: 1px solid transparent;
  color: #04070a;
  font: inherit;
  font-size: 14.5px;
  font-weight: 640;
  cursor: pointer;
}

.button--primary:hover {
  transform: translateY(-1px);
}

.button--primary:disabled,
button.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

button.button {
  font: inherit;
  cursor: pointer;
}

.status-line {
  min-height: 1.4em;
  font-size: 13.5px;
  color: var(--text-muted);
}

.status-line--error {
  color: var(--danger);
}

.status-line--ok {
  color: var(--ok);
}

/* Key details panel, filled in by encrypt.js once a pasted key parses. */
.key-panel:empty {
  display: none;
}

.key-card {
  margin-bottom: 22px;
  padding: 18px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.key-card--warn {
  border-color: rgba(248, 113, 113, 0.45);
}

.key-card__banner {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin-bottom: 14px;
  padding: 10px 12px;
  background: rgba(248, 113, 113, 0.1);
  border: 1px solid rgba(248, 113, 113, 0.35);
  border-radius: var(--radius-sm);
  color: var(--danger);
  font-size: 13.5px;
  font-weight: 600;
}

.key-card__error {
  color: var(--danger);
  font-size: 14px;
}

.key-card dl {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 8px 16px;
  margin: 0;
}

.key-card dt {
  color: var(--text-dim);
  font-size: 12.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  padding-top: 2px;
}

.key-card dd {
  margin: 0;
  min-width: 0;
  color: var(--text);
  font-size: 14px;
  overflow-wrap: anywhere;
}

.key-card dd.mono {
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.02em;
}

.key-card__uids {
  margin: 0;
  padding: 0;
  list-style: none;
}

.key-card__uids li {
  overflow-wrap: anywhere;
}

/* ==========================================================================
   Documentation code blocks (encrypt page gpg cheatsheet)
   ========================================================================== */

.prose pre {
  margin: 0 0 4px;
  padding: 13px 16px;
  max-width: 100%;
  overflow-x: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  -webkit-overflow-scrolling: touch;
}

.prose pre code {
  background: none;
  border: none;
  padding: 0;
  font-size: 13px;
  white-space: pre;
}

.prose code {
  padding: 2px 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 5px;
  font-family: var(--mono);
  font-size: 0.9em;
  overflow-wrap: anywhere;
}

.prose .cheat-note {
  margin: 0 0 18px;
  color: var(--text-dim);
  font-size: 13px;
}

@media (max-width: 560px) {
  .tool {
    padding: 18px;
  }

  .key-card dl {
    grid-template-columns: minmax(0, 1fr);
    gap: 3px 0;
  }

  .key-card dt {
    margin-top: 10px;
  }

  .key-card dt:first-child {
    margin-top: 0;
  }
}

/* ==========================================================================
   Paper theme — white background, black text, no decoration
   --------------------------------------------------------------------------
   For e-ink and other simple screens. This is not a "light mode" in the
   design sense: it deliberately throws away every depth cue the dark theme
   is built from — glow, blur, elevation, translucency, colour-coded tags —
   and rebuilds structure out of plain 1px rules and weight. Those cues are
   exactly what an e-ink panel cannot render: it has no colour, dark fills
   come out muddy grey rather than black, and every gradient, shadow and
   transition costs a full-panel refresh that leaves ghosting behind.

   Selectors are written html[data-theme="paper"] rather than :root so they
   outrank the page-local <style> blocks in the tool and game pages, which
   load after this file and would otherwise win on source order.

   The attribute is set by a tiny inline script in each page's <head>, so
   it lands before first paint — a flash of the dark theme is a wasted
   full-screen refresh on e-ink, which is worse than on a backlit display.
   ========================================================================== */

html[data-theme="paper"] {
  color-scheme: light;
  scroll-behavior: auto;          /* smooth scrolling smears on e-ink */

  --bg: #ffffff;
  --bg-elevated: #ffffff;
  --bg-hover: #e6e6e6;
  --border: #6b6b6b;
  --border-strong: #000000;
  --text: #000000;
  --text-muted: #1c1c1c;
  --text-dim: #3a3a3a;

  /* One ink colour. Hue carries no information on a greyscale panel, so
     emphasis has to come from weight, rules and fills instead. */
  --accent: #000000;
  --accent-2: #000000;
  --accent-3: #000000;
  --accent-grad: none;
  --accent-soft: #dcdcdc;
  --ok: #000000;
  --danger: #000000;

  --card-top: #ffffff;
  --card-bottom: #ffffff;
  --shadow-card: none;
  --shadow-card-hover: none;
  --scroll-progress: 0;
}

/* Motion and depth off everywhere, including the page-local styles. */
html[data-theme="paper"] *,
html[data-theme="paper"] *::before,
html[data-theme="paper"] *::after {
  transition: none !important;
  animation: none !important;
  box-shadow: none !important;
  text-shadow: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  filter: none !important;
}

html[data-theme="paper"] body {
  background: #ffffff;
  color: #000000;
  /* Antialiasing thins strokes; on a low-contrast panel the extra weight
     of normal rendering is worth more than the smoothing. */
  -webkit-font-smoothing: auto;
}

/* The two scroll-driven mesh-gradient layers. */
html[data-theme="paper"] body::before,
html[data-theme="paper"] body::after {
  display: none;
}

/* The skip link paints its label in near-black on --accent, which is now
   black too — it would be invisible without this. */
html[data-theme="paper"] .skip-link {
  background: #000000;
  color: #ffffff;
}

/* --- Chrome ------------------------------------------------------------- */
html[data-theme="paper"] .site-header {
  background: #ffffff;
  border-bottom: 1px solid #000000;
}

html[data-theme="paper"] .site-footer {
  background: #ffffff;
  border-top: 1px solid #000000;
}

/* Gradient-filled text renders as nothing once the gradient is removed. */
html[data-theme="paper"] .wordmark__text,
html[data-theme="paper"] .screen__title {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: #000000;
}

html[data-theme="paper"] .menu__panel,
html[data-theme="paper"] .card,
html[data-theme="paper"] .panel,
html[data-theme="paper"] .notice,
html[data-theme="paper"] .skeleton,
html[data-theme="paper"] .key-card,
html[data-theme="paper"] .topic-grid li,
html[data-theme="paper"] .stat,
html[data-theme="paper"] .detail,
html[data-theme="paper"] .board-wrap {
  background: #ffffff;
  border: 1px solid #000000;
}

html[data-theme="paper"] .menu__button,
html[data-theme="paper"] .button,
html[data-theme="paper"] .btn,
html[data-theme="paper"] .seg,
html[data-theme="paper"] .seg__btn,
html[data-theme="paper"] .menu__link:hover,
html[data-theme="paper"] .menu__back:hover {
  background: #ffffff;
  color: #000000;
}

/* Full-screen overlays (Go's setup and result screens) hardcode a near-black
   wash, which would sit as a dark slab over an otherwise white page.

   This deliberately does not try to repaint HUD elements that float directly
   on a canvas. Melee is the case that proves the point: its banner and status
   text sit on the starfield with no panel behind them, so lightening them
   just produces white text on a dark field. That page opts out of Paper mode
   entirely instead — see the note in its <head>. */
html[data-theme="paper"] .screen {
  background: #ffffff;
  color: #000000;
}

/* Selected / primary actions invert rather than tint. */
html[data-theme="paper"] .button--primary,
html[data-theme="paper"] .btn--go,
html[data-theme="paper"] .go,
html[data-theme="paper"] .seg__btn[aria-pressed="true"],
html[data-theme="paper"] .chip[aria-pressed="true"],
html[data-theme="paper"] .think__fill {
  background: #000000;
  color: #ffffff;
  border-color: #000000;
}
html[data-theme="paper"] .go--ghost {
  background: #ffffff;
  color: #000000;
}

/* --- Feed --------------------------------------------------------------- */
html[data-theme="paper"] .filters {
  background: #ffffff;
  border-bottom: 1px solid #000000;
}
html[data-theme="paper"] .filters::before {
  display: none;
}

html[data-theme="paper"] .chip {
  background: #ffffff;
  border: 1px solid #000000;
  color: #000000;
}

/* Topic tags are colour-coded in the dark theme; here they are all the
   same outlined pill and the word does the work. */
html[data-theme="paper"] .tag,
html[data-theme="paper"] [class*="tag--"] {
  background: #ffffff;
  color: #000000;
  border-color: #000000;
}

html[data-theme="paper"] .skeleton__line {
  background: #dcdcdc;
}

html[data-theme="paper"] .card__title a,
html[data-theme="paper"] .prose a,
html[data-theme="paper"] .card__footer a,
html[data-theme="paper"] .site-footer a {
  color: #000000;
  text-decoration: underline;
}

/* --- Forms -------------------------------------------------------------- */
html[data-theme="paper"] .textarea,
html[data-theme="paper"] .prose pre,
html[data-theme="paper"] .prose code {
  background: #ffffff;
  color: #000000;
  border: 1px solid #000000;
}
