/* Fonts are loaded from index.html (<link rel=preconnect> + <link rel=stylesheet>) so they
   are not stuck behind a render-blocking @import chain. 'Apple Color Emoji' below is the
   system font — no download needed. */

:root {
  --bg-primary: #090909;
  --bg-secondary: #0f0f0f;
  --bg-card: #111113;
  --bg-card-inner: #151517;
  --bg-card-hover: #1a1a1d;
  --bg-elevated: #1e1e21;
  --border-subtle: rgba(255, 255, 255, 0.05);
  --border-medium: rgba(255, 255, 255, 0.08);
  --border-accent: rgba(255, 255, 255, 0.13);
  --text-primary: #ffffff;
  --text-secondary: #b0b0b8;
  --text-muted: #818189;
  --text-accent: #e4e4e7;
  --status-online: #22c55e;
  --status-idle: #eab308;
  --status-dnd: #ef4444;
  --status-offline: #52525b;
  --spotify-green: #1DB954;
  --radius-sm: 10px;
  --radius-md: 16px;
  --radius-lg: 20px;
  --radius-xl: 24px;
  --radius-full: 9999px;
  --transition-smooth: cubic-bezier(0.25, 1, 0.5, 1);
  --transition-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --transition-ease: cubic-bezier(0.4, 0, 0.2, 1);
  --transition-fluid: cubic-bezier(0.22, 1, 0.36, 1);
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Apple Color Emoji', sans-serif;
  --font-display: 'Space Grotesk', 'Inter', sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
}


*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  font-size: 17.5px;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scrollbar-color: var(--bg-elevated) transparent;
  scrollbar-width: thin;
}

/* The default blue selection is the one bit of stock browser chrome that shows through
   on an otherwise monochrome page. */
::selection {
  background: rgba(255, 255, 255, 0.16);
  color: #fff;
}

/* WebKit still ignores scrollbar-color, so it gets the long-hand version. */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--bg-elevated);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: var(--radius-full);
}

::-webkit-scrollbar-thumb:hover {
  background: #2c2c31;
  background-clip: content-box;
}

body {
  font-family: var(--font-sans);
  background: var(--bg-primary);
  color: var(--text-primary);
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow-x: hidden;
  overflow-y: auto;
  position: relative;
}


body::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
  pointer-events: none;
  z-index: 0;
}

body::after {
  content: '';
  position: fixed;
  inset: 0;
  background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.5) 100%);
  pointer-events: none;
  z-index: 0;
}

#bg-video {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  object-fit: cover;
  z-index: 0;
  pointer-events: none;
  opacity: 0.35;
  filter: brightness(0.6);
}


.main-wrapper {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 640px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* script.js writes --tilt-x/--tilt-y on pointer move. Kept on the wrapper so the nav
     and the card tilt as one object, and so it never collides with the reveal transform
     on .panel. The transition doubles as the damping — the values are set per frame. */
  transform: perspective(1400px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg));
  transition: transform 0.25s var(--transition-fluid);
}


.nav-bar {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-bottom: 14px;
  padding: 4px;
  background: var(--bg-card);
  border-radius: var(--radius-full);
  border: 1px solid var(--border-subtle);
  width: auto;
  align-self: flex-start;
  position: relative;
}

.nav-indicator {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  border-radius: var(--radius-full);
  background: var(--bg-elevated);
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.3),
    inset 0 1px 0 rgba(255, 255, 255, 0.04);
  transition: transform 0.4s var(--transition-fluid), width 0.4s var(--transition-fluid);
  z-index: 1;
}

.nav-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 18px;
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  background: none;
  border: none;
  border-radius: var(--radius-full);
  cursor: pointer;
  transition:
    color 0.35s var(--transition-fluid),
    transform 0.25s var(--transition-spring);
  font-family: var(--font-sans);
  user-select: none;
  white-space: nowrap;
  position: relative;
  z-index: 2;
}

.nav-tab svg {
  width: 14px;
  height: 14px;
  stroke-width: 2;
  flex-shrink: 0;
  opacity: 0.45;
  transition: opacity 0.3s var(--transition-fluid);
}

.nav-tab:active {
  transform: scale(0.94);
}

.nav-tab.active {
  color: var(--text-primary);
}

.nav-tab.active svg {
  opacity: 1;
}

/* kills the grey/blue flash box on mobile taps */
.nav-tab,
.mute-button,
.social-card {
  -webkit-tap-highlight-color: transparent;
}


.panel {
  width: 100%;
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-xl);
  overflow: hidden;
  position: relative;
  box-shadow:
    0 0 0 1px var(--border-subtle),
    0 24px 80px -16px rgba(0, 0, 0, 0.55);
}

.panel::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
  z-index: 5;
}

/* Reveal, played once when the entrance overlay is dismissed.

   Transition-driven rather than keyframed, and gated on .js-ready, so the hidden state
   can only ever exist while the script is alive to undo it. With JS off, broken, or slow
   the card simply sits there fully visible — no fill-mode trap that can leave it stuck
   at opacity 0. */
.nav-bar,
.panel {
  transition:
    opacity 0.65s var(--transition-fluid),
    transform 0.65s var(--transition-fluid);
}

body.js-ready:not(.revealed) .nav-bar,
body.js-ready:not(.revealed) .panel {
  /* not 0 — the card staying faintly visible behind the blur is part of the entrance's
     character, so it only dims and drops, then resolves as the overlay lifts */
  opacity: 0.45;
  transform: translateY(14px) scale(0.985);
}

.panel {
  transition-delay: 0.08s;
}

/* Header image behind the top of the panel, faded into the card by the ::after scrim so
   the name and status stay readable over it. */
.panel-bg-image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 380px;
  background: url('../assets/header.jpg') center top / cover no-repeat;
  z-index: 0;
  pointer-events: none;
}

.panel-bg-image::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(17, 17, 19, 0.15) 0%, rgba(17, 17, 19, 0.85) 65%, var(--bg-card) 100%);
}

.banner {
  position: relative;
  width: 100%;
  height: 150px;
  background: transparent;
  overflow: hidden;
  z-index: 1;
}




.panel-body {
  padding: 0 32px 28px;
  position: relative;
}


.profile-header {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  margin-top: -44px;
  position: relative;
  z-index: 10;
  margin-bottom: 14px;
}

.avatar-container {
  position: relative;
  flex-shrink: 0;
  width: 84px;
  height: 84px;
}

.avatar-ring {
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  --ring: var(--status-offline);
  border: 3px solid var(--ring);
  box-shadow: 0 0 12px -2px var(--ring);
  transition:
    border-color 0.6s var(--transition-fluid),
    box-shadow 0.6s var(--transition-fluid);
  z-index: 2;
  background: var(--bg-card);
  padding: 3px;
  box-sizing: content-box;
}

.avatar-ring.online {
  --ring: var(--status-online);
}

.avatar-ring.idle {
  --ring: var(--status-idle);
}

.avatar-ring.dnd {
  --ring: var(--status-dnd);
}

.avatar-ring.offline {
  --ring: var(--status-offline);
  box-shadow: none;
}

.avatar-img {
  width: 84px;
  height: 84px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
  background: var(--bg-secondary);
  position: relative;
  z-index: 3;
}

/* The picture itself holds still — only the ring answers. Pointing at the avatar reads as the
   frame lighting up, rather than as a photo shifting around inside it. */
.avatar-container:hover .avatar-ring:not(.offline) {
  box-shadow: 0 0 20px -2px var(--ring);
}


.avatar-decoration {
  position: absolute;
  inset: -16px;
  width: calc(100% + 32px);
  height: calc(100% + 32px);
  z-index: 4;
  pointer-events: none;
  object-fit: contain;
}

.profile-identity {
  padding-bottom: 6px;
  min-width: 0;
  flex: 1;
}

.name-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.display-name {
  font-family: var(--font-display);
  font-size: 1.6rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  line-height: 1.2;
}

.pronouns {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  background: linear-gradient(180deg, #55CDFC 15%, #F7A8B8 35%, #FFFFFF 50%, #F7A8B8 65%, #55CDFC 85%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  color: transparent;
  align-self: center;
  margin-top: 4px;
  user-select: none;
}


.guild-tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border-radius: var(--radius-full);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border-subtle);
  font-size: 0.65rem;
  font-weight: 500;
  color: var(--text-secondary);
  letter-spacing: 0.03em;
  text-transform: uppercase;
}


.status-line {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 2px;
  flex-wrap: wrap;
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--status-offline);
  flex-shrink: 0;
  transition: background 0.5s var(--transition-fluid);
  position: relative;
}

/* Only the online dot pulses — if idle/dnd/offline pulsed too the motion would stop
   carrying any information. */
@keyframes status-ping {
  0% {
    transform: scale(1);
    opacity: 0.5;
  }

  70%,
  100% {
    transform: scale(2.6);
    opacity: 0;
  }
}

.status-dot.online {
  background: var(--status-online);
}

.status-dot.online::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--status-online);
  animation: status-ping 2.4s var(--transition-ease) infinite;
  pointer-events: none;
}

.status-dot.idle {
  background: var(--status-idle);
}

.status-dot.dnd {
  background: var(--status-dnd);
}

/* couldn't reach Lanyard — hollow dot, distinct from a confirmed offline */
.status-dot.unknown {
  background: transparent;
  border: 1.5px solid var(--status-offline);
}

.status-label {
  font-size: 0.76rem;
  color: var(--text-muted);
  font-weight: 500;
}

/* ═══════════════════════════════════════════════════
   LOCAL CLOCK
   Page furniture, not a profile field: pinned to the corner opposite the mute button so the
   two frame the card. Fixed rather than in-flow because the panel changes height when tabs
   switch and posts expand, and a clock that moves with that is distracting.
   ═══════════════════════════════════════════════════ */
.local-clock {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 10001;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  /* opaque on purpose — a backdrop-filter here would keep a compositing layer alive while the
     chip is still faded out behind the entrance overlay, and wash out the blur underneath */
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  cursor: default;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition:
    opacity 0.6s var(--transition-fluid),
    transform 0.6s var(--transition-fluid),
    border-color 0.3s var(--transition-fluid),
    color 0.3s var(--transition-fluid);
}

.local-clock:hover {
  color: var(--text-secondary);
  border-color: var(--border-medium);
}

/* Same gate as the card, so it rises in with everything else instead of sitting on top of
   the entrance blur. Gated on .js-ready for the same reason the panel is: with the script
   dead there's nothing to add .revealed, and the clock has to stay visible anyway. */
body.js-ready:not(.revealed) .local-clock {
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
}

.local-clock svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  opacity: 0.55;
}

.local-clock-zone {
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  opacity: 0.7;
}


.status-bubble {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  border-radius: var(--radius-md);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  font-size: 0.76rem;
  color: var(--text-secondary);
  margin-bottom: 18px;
  max-width: 100%;
  overflow: visible;
  transition: all 0.4s var(--transition-spring);
  position: relative;
}

.status-bubble::before {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 32px;
  border-width: 0 6px 7px 6px;
  border-style: solid;
  border-color: transparent transparent var(--border-subtle) transparent;
  transition: border-color 0.4s var(--transition-fluid);
}

.status-bubble::after {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 32px;
  border-width: 0 6px 7px 6px;
  border-style: solid;
  border-color: transparent transparent var(--bg-card-inner) transparent;
  transform: translateY(1.5px);
  transition: border-color 0.4s var(--transition-fluid);
}

.status-bubble:hover::before {
  border-color: transparent transparent var(--border-medium) transparent;
}

.status-bubble:hover::after {
  border-color: transparent transparent var(--bg-elevated) transparent;
}

.status-bubble:hover {
  border-color: var(--border-medium);
  background: var(--bg-elevated);
}

.status-emoji {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  object-fit: contain;
}

.status-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


.section-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
  margin-top: 4px;
}

.section-title {
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--text-muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* Fades out toward the panel edge instead of stopping dead against it — the flat
   rule read as a table border and drew more attention than the label it follows. */
.section-line {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--border-medium), transparent);
}


/* These two sections get flipped on by script.js when a presence comes in, which
   otherwise makes the panel jump. Fading them in from a few pixels down turns that
   jump into a reveal. display:none -> block restarts the animation for free. */
@keyframes section-reveal {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.activity-section {
  margin-bottom: 18px;
  animation: section-reveal 0.45s var(--transition-fluid) both;
}

.activity-card {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 14px;
  border-radius: var(--radius-md);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  transition: all 0.35s var(--transition-fluid);
}

.activity-card:hover {
  border-color: var(--border-medium);
  background: var(--bg-card-hover);
}

.activity-images {
  position: relative;
  flex-shrink: 0;
  width: 60px;
  height: 60px;
}

.activity-large-img {
  width: 60px;
  height: 60px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--bg-secondary);
}

.activity-small-img {
  position: absolute;
  bottom: -4px;
  right: -4px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--bg-card-inner);
  border: 2.5px solid var(--bg-card-inner);
}

.activity-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.activity-name {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.activity-details,
.activity-state {
  font-size: 0.72rem;
  color: var(--text-secondary);
  line-height: 1.4;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.activity-elapsed {
  font-size: 0.67rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  margin-top: 2px;
}




.spotify-section {
  margin-bottom: 18px;
  animation: section-reveal 0.45s var(--transition-fluid) both;
}

.spotify-icon-header {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

.spotify-card {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 14px;
  border-radius: var(--radius-md);
  background: var(--bg-card-inner);
  border: 1px solid rgba(29, 185, 84, 0.1);
  transition: all 0.35s var(--transition-fluid);
  /* it's an <a> now (click through to the track) — keep it looking like the card it was */
  text-decoration: none;
  color: inherit;
  cursor: default;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* Album art bled across the card, heavily blurred and mostly transparent, so the card
   picks up the colour of whatever is playing without turning into a light show.
   --art is set by script.js; with no art the gradient never paints. */
.spotify-card::before {
  content: '';
  position: absolute;
  inset: -40%;
  z-index: -1;
  background-image: var(--art, none);
  background-size: cover;
  background-position: center;
  filter: blur(34px) saturate(1.25);
  opacity: 0;
  transition: opacity 0.6s var(--transition-fluid);
  pointer-events: none;
}

.spotify-card[style*='--art']::before {
  opacity: 0.2;
}

/* Fade the tint out toward the text so nothing loses contrast. */
.spotify-card::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(100deg, transparent 0%, var(--bg-card-inner) 62%);
  pointer-events: none;
}

.spotify-card.linked {
  cursor: pointer;
}

.spotify-card:hover {
  border-color: rgba(29, 185, 84, 0.18);
  background: var(--bg-card-hover);
}

.spotify-card:hover::before {
  opacity: 0.28;
}

/* keep the fade matched to the card's own hover background */
.spotify-card:hover::after {
  background: linear-gradient(100deg, transparent 0%, var(--bg-card-hover) 62%);
}


.spotify-album-art {
  width: 60px;
  height: 60px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--bg-secondary);
  flex-shrink: 0;
}

.spotify-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.spotify-song {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.spotify-artist {
  font-size: 0.72rem;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.spotify-album {
  font-size: 0.68rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.spotify-progress-wrap {
  margin-top: 6px;
}

.spotify-progress-bar {
  width: 100%;
  height: 3px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.06);
  overflow: hidden;
}

.spotify-progress-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--spotify-green);
  transition: width 1s linear;
  width: 0%;
}

.spotify-timestamps {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-size: 0.6rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}


/* tab wrapper + tab-content rules live in one place further down */


.social-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
}

/* With an odd number of socials the last one would sit alone in a half-width slot,
   so let it stretch across instead. Self-correcting if a fourth one gets added. */
.social-grid > .social-card:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

.social-card {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 16px;
  border-radius: var(--radius-md);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  cursor: pointer;
  text-decoration: none;
  color: var(--text-secondary);
  transition:
    background 0.2s ease,
    border-color 0.2s ease,
    color 0.2s ease,
    transform 0.25s var(--transition-spring);
  overflow: visible;
}

/* Each card carries its own accent, spent only on hover/focus. The resting grid
   stays monochrome on purpose — three saturated logos sitting there permanently
   would pull focus off the avatar and the status line, which are the point of the
   card. This way the colour is a reward for pointing at something. */
#social-telegram {
  --brand: #2aabee;
  --brand-wash: rgba(42, 171, 238, 0.13);
}

#social-discord {
  --brand: #5865f2;
  --brand-wash: rgba(88, 101, 242, 0.14);
}

#social-twitter {
  --brand: #e7e9ea;
  --brand-wash: rgba(231, 233, 234, 0.1);
}

.social-card:hover,
.social-card:focus-visible {
  background: var(--bg-card-hover);
  border-color: var(--border-medium);
  color: var(--text-primary);
  transform: translateY(-1px);
}

.social-card:active {
  background: var(--bg-elevated);
  transform: translateY(0);
}

.social-icon-wrap {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.04);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background 0.3s var(--transition-fluid);
}

.social-card:hover .social-icon-wrap,
.social-card:focus-visible .social-icon-wrap {
  background: var(--brand-wash, rgba(255, 255, 255, 0.07));
}

.social-icon {
  width: 18px;
  height: 18px;
  opacity: 0.55;
  transition:
    opacity 0.3s var(--transition-fluid),
    color 0.3s var(--transition-fluid);
  flex-shrink: 0;
}

.social-card:hover .social-icon,
.social-card:focus-visible .social-icon {
  opacity: 1;
  color: var(--brand, currentColor);
}

.social-info {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.social-label {
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  line-height: 1.3;
}

.social-handle {
  font-size: 0.7rem;
  color: var(--text-muted);
  font-weight: 400;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 1px;
}


/* Trailing ↗ hint. Hidden until hover/focus so the resting state stays as clean
   as it was, and it travels along the diagonal it points down rather than sliding
   in sideways against its own direction. */
.social-action {
  width: 14px;
  height: 14px;
  margin-left: auto;
  flex-shrink: 0;
  color: var(--text-muted);
  opacity: 0;
  transform: translate(-3px, 3px);
  transition:
    opacity 0.25s var(--transition-fluid),
    transform 0.25s var(--transition-fluid),
    color 0.2s ease;
}

.social-card:hover .social-action,
.social-card:focus-visible .social-action {
  opacity: 0.75;
  transform: translate(0, 0);
}





.blog-status {
  font-size: 0.8rem;
  color: var(--text-muted);
  font-family: var(--font-mono);
  padding: 6px 0 2px;
}

.blog-status.is-error {
  color: #e0a0a0;
}

.blog-retry {
  margin-left: 8px;
  font-family: var(--font-sans);
  font-size: 0.72rem;
  color: var(--text-secondary);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-sm);
  padding: 3px 9px;
  cursor: pointer;
  transition: background 0.24s var(--transition-fluid), color 0.24s var(--transition-fluid);
}

.blog-retry:hover {
  background: var(--bg-card-hover);
  color: var(--text-primary);
}

/* Sits between .section-line and the repo link, so the header reads title — rule — reload —
   repo, with both controls grouped at the right edge. */
.blog-reload {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: color 0.24s var(--transition-fluid), background 0.24s var(--transition-fluid),
    border-color 0.24s var(--transition-fluid), transform 0.24s var(--transition-fluid);
}

.blog-reload svg {
  width: 13px;
  height: 13px;
}

.blog-reload:hover:not(:disabled) {
  color: var(--text-primary);
  background: var(--bg-card-inner);
  border-color: var(--border-subtle);
  transform: translateY(-1px);
}

/* Dimmed as well as spun, so there's still a "working" cue with reduced motion requested —
   the global rule up top stops every animation on the page dead. */
.blog-reload:disabled {
  cursor: default;
  color: var(--text-muted);
  opacity: 0.5;
}

/* Spins for as long as the request is out. Two-arrow icon, so rotation is the one animation
   that reads as "working" rather than as decoration. */
.blog-reload.is-spinning svg {
  animation: blog-reload-spin 0.9s linear infinite;
}

@keyframes blog-reload-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Sits after .section-line, so it lands hard against the right edge of the header row. */
.blog-repo-link {
  flex-shrink: 0;
  font-size: 0.68rem;
  font-family: var(--font-mono);
  color: var(--text-muted);
  text-decoration: none;
  letter-spacing: 0.02em;
  transition: color 0.24s var(--transition-fluid);
}

.blog-repo-link:hover {
  color: var(--text-secondary);
}

.blog-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.blog-post {
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: border-color 0.28s var(--transition-fluid), background 0.28s var(--transition-fluid);
}

.blog-post:hover,
.blog-post.is-open {
  border-color: var(--border-medium);
}

.blog-post.is-open {
  background: var(--bg-card-hover);
}

.blog-post-head {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  color: var(--text-secondary);
  font-family: var(--font-sans);
  transition: color 0.24s var(--transition-fluid);
  -webkit-tap-highlight-color: transparent;
}

/* The chevron leans in with the row rather than waiting for the click. */
.blog-post-head:hover .blog-post-chevron {
  color: var(--text-secondary);
  transform: translateX(1px);
}

.blog-post.is-open .blog-post-head:hover .blog-post-chevron {
  transform: rotate(90deg) translateX(1px);
}

.blog-post-head:hover,
.blog-post.is-open .blog-post-head {
  color: var(--text-primary);
}

/* Mono, because the title *is* the filename — extension included. Anything else would
   be pretending it's a headline. */
.blog-post-title {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.blog-post-date {
  margin-left: auto;
  flex-shrink: 0;
  font-size: 0.68rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.blog-post-chevron {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform 0.35s var(--transition-fluid), color 0.2s ease;
}

.blog-post.is-open .blog-post-chevron {
  transform: rotate(90deg);
  color: var(--text-secondary);
}

/* Height is animated by script.js — it measures the content, then hands the element a
   fixed cap and its own scrollbar once the expand has finished. */
.blog-post-body {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.4s var(--transition-fluid);
}

.blog-post-body.is-scrollable {
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Reserves the gutter so the text doesn't shift sideways the moment the scrollbar appears —
     which also stops the height watcher in script.js from chasing its own scrollbar. */
  scrollbar-gutter: stable;
  scrollbar-width: thin;
  scrollbar-color: var(--bg-elevated) transparent;
}

.blog-md {
  padding: 2px 14px 14px;
  font-size: 0.84rem;
  line-height: 1.7;
  color: var(--text-secondary);
  overflow-wrap: break-word;
}

/* One hairline separating the head from the body, drawn on the content so it only
   exists while the post is open. */
.blog-md::before {
  content: '';
  display: block;
  height: 1px;
  margin: 0 0 12px;
  background: linear-gradient(90deg, var(--border-medium), transparent);
}

/* The body fades up as the box grows, so an expand reads as one movement rather than a slab of
   text being shoved into view. Opacity only — nothing here can shift the height the script is
   busy measuring. */
.blog-md {
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity 0.34s var(--transition-fluid) 0.05s,
    transform 0.34s var(--transition-fluid) 0.05s;
}

.blog-post.is-open .blog-md {
  opacity: 1;
  transform: none;
}

.blog-md> :last-child {
  margin-bottom: 0;
}

.blog-md h1,
.blog-md h2,
.blog-md h3 {
  font-family: var(--font-display);
  color: var(--text-primary);
  line-height: 1.3;
  margin: 16px 0 8px;
}

.blog-md> :first-child {
  margin-top: 0;
}

.blog-md h1 {
  font-size: 1.05rem;
}

.blog-md h2 {
  font-size: 0.95rem;
}

.blog-md h3 {
  font-size: 0.87rem;
}

.blog-md p,
.blog-md ul,
.blog-md ol,
.blog-md pre,
.blog-md blockquote {
  margin-bottom: 10px;
  text-wrap: pretty;
}

.blog-md ul,
.blog-md ol {
  padding-left: 20px;
}

.blog-md li {
  margin-bottom: 3px;
}

.blog-md a {
  color: var(--text-accent);
  text-decoration: underline;
  text-decoration-color: var(--border-accent);
  text-underline-offset: 2px;
  transition: text-decoration-color 0.24s var(--transition-fluid), color 0.24s var(--transition-fluid);
}

.blog-md a:hover {
  text-decoration-color: var(--text-accent);
}

.blog-md strong {
  color: var(--text-primary);
  font-weight: 600;
}

.blog-md code {
  font-family: var(--font-mono);
  font-size: 0.78em;
  background: rgba(255, 255, 255, 0.06);
  border-radius: 5px;
  padding: 1.5px 5px;
}

.blog-md pre {
  background: var(--bg-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  overflow-x: auto;
}

.blog-md pre code {
  background: none;
  padding: 0;
  font-size: 0.76rem;
  line-height: 1.6;
}

/* script.js wraps every <pre> in one of these and drops a copy button in the corner. The
   wrapper exists because <pre> scrolls horizontally, and a button positioned inside a
   scrolling box slides away the moment a long line is scrolled. */
.code-block {
  position: relative;
  margin: 10px 0;
}

.code-block pre {
  margin: 0;
}

.code-copy {
  position: absolute;
  top: 7px;
  right: 7px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.02em;
  color: var(--text-muted);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  padding: 2px 7px;
  cursor: pointer;
  /* Out of the way until the block is hovered, but always reachable by keyboard. */
  opacity: 0;
  transition: opacity 0.24s var(--transition-fluid), color 0.24s var(--transition-fluid),
    background 0.24s var(--transition-fluid);
}

.code-block:hover .code-copy,
.code-copy:focus-visible {
  opacity: 1;
}

.code-copy:hover {
  color: var(--text-primary);
  background: var(--bg-card-hover);
}

/* Touch has no hover, so there'd be nothing to reveal the button. */
@media (hover: none) {
  .code-copy {
    opacity: 1;
  }

  /* Safari zooms the entire page in when you focus a field smaller than 16px and never quite
     zooms back out. 16px exactly is the price of not having that happen. */
  .contact-input {
    font-size: 16px;
  }
}

.blog-md blockquote {
  border-left: 2px solid var(--border-accent);
  padding-left: 12px;
  color: var(--text-muted);
}

.blog-md hr {
  border: none;
  height: 1px;
  background: var(--border-subtle);
  margin: 14px 0;
}

.blog-md img {
  max-width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  display: block;
}

/* Embeds. A link alone on its own line comes back from the renderer as one of these, so they
   get the same bordered, rounded treatment as everything else on the page instead of sitting
   in the post as a bare rectangle.

   Each variant carries its box up front — an aspect ratio where the platform has one, a fixed
   height where it doesn't. That matters here specifically: the post measures its own content
   to animate open, and an iframe that hasn't loaded yet measures as nothing. */
.blog-embed {
  position: relative;
  margin: 12px 0;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-card-inner);
}

.blog-embed iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.blog-embed-video {
  aspect-ratio: 16 / 9;
}

.blog-embed-gif {
  aspect-ratio: 4 / 3;
}

/* Vertical video reads as a mistake at full column width, so it's capped and centred. */
.blog-embed-portrait {
  aspect-ratio: 9 / 14;
  max-width: 260px;
  margin-inline: auto;
}

.blog-embed-spotify {
  height: 152px;
}

.blog-embed-spotify-tall {
  height: 352px;
}

.blog-embed-audio-frame {
  height: 120px;
}

/* Starting height only — the frame measures the post and sends back its real one. */
.blog-embed-tweet {
  height: 400px;
  transition: height 0.25s var(--transition-ease);
}

/* Files served straight from wherever they live — no frame in the way, so these are real
   media elements and size themselves off the file. */
.blog-media-img,
.blog-media-video {
  display: block;
  max-width: 100%;
  height: auto;
  margin: 12px 0;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  background: var(--bg-card-inner);
}

.blog-media-video {
  width: 100%;
}

.blog-media-audio {
  display: block;
  width: 100%;
  margin: 12px 0;
}

/* A silent looping video standing in for a gif. Nothing about it should invite a click. */
.blog-media-video.is-loop {
  cursor: default;
}

/* Media arrives whenever the network says so, which is always after the post has opened. Fading
   each piece in on its own load event is the difference between a post that settles and a post
   that snaps. script.js adds is-fading when it wires the element and is-loaded when it has pixels,
   so anything it never got to stays plainly visible. */
.blog-media-img.is-fading,
.blog-media-video.is-fading,
.blog-embed iframe.is-fading {
  transition: opacity 0.45s var(--transition-fluid), transform 0.45s var(--transition-fluid);
}

.blog-media-img.is-fading:not(.is-loaded),
.blog-media-video.is-fading:not(.is-loaded),
.blog-embed iframe.is-fading:not(.is-loaded) {
  opacity: 0;
  transform: translateY(5px);
}

/* The placeholder a resolved embed grows out of. It already holds whatever stands in if the lookup
   never lands, so the only job of the pending state is to say a request is out. */
.blog-resolve.is-pending .blog-link-card-kind {
  animation: resolve-pulse 1.5s var(--transition-ease) infinite;
}

@keyframes resolve-pulse {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.4;
  }
}

/* The fallback for links that can't become media: a Tenor share code this page has no way to
   resolve, or an image the host refuses to serve to us. Sized and bordered like the embeds it
   stands in for, so a post with one doesn't look like a post with a hole in it. */
.blog-link-card {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0;
  padding: 11px 13px;
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background 0.28s var(--transition-fluid), border-color 0.28s var(--transition-fluid),
    transform 0.28s var(--transition-fluid);
}

.blog-link-card:hover {
  background: var(--bg-card-hover);
  border-color: var(--border-medium);
  transform: translateY(-1px);
}

.blog-link-card-kind {
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 3px 7px;
  background: var(--bg-elevated);
  border-radius: 6px;
}

/* The path is the only variable-length part, so it's the part that gets to shrink. */
.blog-link-card-host {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.blog-link-card-go {
  flex-shrink: 0;
  font-size: 0.8rem;
  color: var(--text-muted);
  transition: color 0.28s var(--transition-fluid), transform 0.28s var(--transition-fluid);
}

.blog-link-card:hover .blog-link-card-go {
  color: var(--text-primary);
  transform: translate(1px, -1px);
}

/* An author display: flex outranks the UA stylesheet's [hidden] { display: none }, so the
   pager needs to say so itself — otherwise it sits there on a single-page list. */
.blog-pager[hidden] {
  display: none;
}

.blog-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-top: 14px;
}

.blog-pager-btn {
  width: 30px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  color: var(--text-muted);
  cursor: pointer;
  transition: background 0.24s var(--transition-fluid), color 0.24s var(--transition-fluid),
    border-color 0.24s var(--transition-fluid), transform 0.24s var(--transition-fluid);
  -webkit-tap-highlight-color: transparent;
}

.blog-pager-btn svg {
  width: 15px;
  height: 15px;
}

.blog-pager-btn:hover:not(:disabled) {
  background: var(--bg-card-hover);
  border-color: var(--border-medium);
  color: var(--text-primary);
  transform: translateY(-1px);
}

.blog-pager-btn:disabled {
  opacity: 0.3;
  cursor: default;
}

.blog-pager-count {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}





.tab-content-wrapper {
  position: relative;
  overflow: hidden;
  transition: height 0.45s var(--transition-fluid);
  will-change: height;
}

.tab-content {
  display: none;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.3s var(--transition-fluid), transform 0.3s var(--transition-fluid);
}

.tab-content.active {
  display: block;
  opacity: 1;
  transform: translateY(0);
}

.tab-content.exit {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-8px);
}


.about-body {
  font-size: 0.88rem;
  line-height: 1.75;
  color: var(--text-secondary);
  /* cap the measure — over-long lines are the one thing that reads badly in this panel */
  max-width: 58ch;
}

.about-body p {
  margin-bottom: 10px;
  /* avoids a one-word last line; ignored by browsers that don't support it */
  text-wrap: pretty;
}

.about-body p:last-child {
  margin-bottom: 0;
}


.skeleton {
  background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-elevated) 50%, var(--bg-secondary) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.8s ease-in-out infinite;
  border-radius: var(--radius-sm);
}

@keyframes shimmer {
  0% {
    background-position: 200% 0;
  }

  100% {
    background-position: -200% 0;
  }
}

.skeleton-avatar {
  width: 84px;
  height: 84px;
  border-radius: 50%;
}

.skeleton-text {
  height: 18px;
  width: 130px;
  display: inline-block;
}


::-webkit-scrollbar {
  width: 5px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--border-medium);
  border-radius: 5px;
}

::selection {
  background: rgba(255, 255, 255, 0.1);
  color: var(--text-primary);
}


@media (max-width: 680px) {
  .main-wrapper {
    max-width: 100%;
    padding: 16px;
  }

  .panel-body {
    padding: 0 24px 28px;
  }
}

@media (max-width: 520px) {
  body {
    align-items: flex-start;
    padding-top: 12px;
  }

  .main-wrapper {
    padding: 10px;
  }

  .panel-body {
    padding: 0 18px 24px;
  }

  .panel {
    border-radius: var(--radius-lg);
  }

  .banner {
    height: 110px;
  }

  .profile-header {
    margin-top: -34px;
    gap: 14px;
  }

  .avatar-img {
    width: 68px;
    height: 68px;
  }

  .avatar-container {
    width: 68px;
    height: 68px;
  }

  .avatar-ring {
    inset: -4px;
    border-width: 2.5px;
  }

  .display-name {
    font-size: 1.15rem;
  }

  .social-grid {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .nav-tab {
    padding: 7px 13px;
    font-size: 0.73rem;
  }

  .contact-intro {
    font-size: 0.82rem;
  }

  .activity-card,
  .spotify-card {
    padding: 12px;
    gap: 12px;
  }

  .activity-large-img,
  .spotify-album-art {
    width: 50px;
    height: 50px;
  }

  .activity-small-img {
    width: 20px;
    height: 20px;
  }

  /* The panel runs nearly edge to edge down here, so the chip tucks into the corner tighter
     rather than floating a thumb's width off it. */
  .local-clock {
    bottom: 14px;
    right: 14px;
    padding: 6px 11px;
    font-size: 0.66rem;
  }
}

@media (max-width: 360px) {

  /* Four tabs and a 340px column. The labels give up their side padding before the row gives
     up a tab — wrapping isn't an option, the indicator slides along a single line. */
  .nav-bar {
    padding: 3px;
  }

  .nav-tab {
    padding: 7px 9px;
    gap: 5px;
  }

  .name-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
  }

  /* the album art + three lines of metadata stop fitting side by side down here */
  .spotify-timestamps {
    font-size: 0.62rem;
  }
}


/* ═══════════════════════════════════════════════════
   ENTRANCE OVERLAY
   ═══════════════════════════════════════════════════ */
.entrance-overlay {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  background: rgba(9, 9, 9, 0.7);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  opacity: 1;
  visibility: visible;
  transition:
    opacity 0.8s var(--transition-fluid),
    backdrop-filter 0.8s var(--transition-fluid),
    -webkit-backdrop-filter 0.8s var(--transition-fluid),
    /* flipped only once the fade has finished, never on the way in */
    visibility 0s linear 0.8s;
  cursor: pointer;
}

.entrance-overlay.active {
  opacity: 1;
  pointer-events: auto;
}

.entrance-overlay.hidden {
  opacity: 0;
  pointer-events: none;
  /* Has to leave the paint tree, not just go transparent. A viewport-sized
     backdrop-filter left alive at opacity 0 keeps its compositing layer, and
     everything underneath gets pulled into it — the card rendered
     half-transparent with the video showing straight through. The blur fades
     out with the scrim; visibility lands at the end of that same 0.8s. */
  visibility: hidden;
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
}

.entrance-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  pointer-events: auto;
}

/* Breathing, so the prompt reads as something you're meant to click rather than a
   label that happens to be there. Stops the moment the overlay starts leaving. */
@keyframes entrance-breathe {
  0%,
  100% {
    opacity: 0.55;
  }

  50% {
    opacity: 1;
  }
}

.entrance-text {
  font-size: clamp(0.9rem, 3vw, 1.4rem);
  font-weight: 400;
  letter-spacing: 0.15em;
  color: rgba(255, 255, 255, 0.6);
  user-select: none;
  pointer-events: none;
  text-transform: lowercase;
  font-family: var(--font-sans);
  transition: color 0.4s var(--transition-fluid), letter-spacing 0.5s var(--transition-fluid);
}

.entrance-overlay.active .entrance-text {
  animation: entrance-breathe 2.8s var(--transition-ease) infinite;
}

.entrance-overlay.hidden .entrance-text {
  animation: none;
}

/* Hover tightens the pulse toward the bright end rather than stopping it — killing the
   animation outright snaps the opacity, since an animated value can't be transitioned. */
@keyframes entrance-breathe-hover {
  0%,
  100% {
    opacity: 0.85;
  }

  50% {
    opacity: 1;
  }
}

.entrance-overlay:hover .entrance-text {
  color: rgba(255, 255, 255, 0.92);
  letter-spacing: 0.19em;
}

.entrance-overlay.active:hover .entrance-text {
  animation: entrance-breathe-hover 2.8s var(--transition-ease) infinite;
}



/* ═══════════════════════════════════════════════════
   MUTE BUTTON
   ═══════════════════════════════════════════════════ */
.mute-button {
  position: fixed;
  top: 24px;
  right: 24px;
  width: 48px;
  height: 48px;
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10001;
  transition:
    background 0.3s var(--transition-fluid),
    border-color 0.3s var(--transition-fluid),
    color 0.3s var(--transition-fluid),
    opacity 0.3s var(--transition-fluid),
    transform 0.25s var(--transition-spring);
  color: var(--text-secondary);
  font-family: var(--font-sans);
  font-size: 0.9rem;
  font-weight: 500;
  opacity: 1;
  pointer-events: auto;
  transform: scale(1);
}

.mute-button.visible {
  opacity: 1;
  pointer-events: auto;
  transform: scale(1);
}

.mute-button:active {
  transform: scale(0.92);
}

/* Two inline SVGs live in the button; aria-pressed decides which one shows.
   This replaced Font Awesome — it was a 103 KB stylesheet plus a webfont for two icons. */
.mute-icon {
  width: 20px;
  height: 20px;
  display: none;
}

.mute-button[aria-pressed="false"] .mute-icon[data-state="unmuted"],
.mute-button[aria-pressed="true"] .mute-icon[data-state="muted"] {
  display: block;
}

.mute-button[aria-pressed="true"] {
  opacity: 0.5;
  color: var(--text-muted);
}

.mute-button:hover {
  border-color: var(--border-medium);
  background: var(--bg-elevated);
  color: var(--text-primary);
}


/* ═══════════════════════════════════════════════════
   CONTACT
   ═══════════════════════════════════════════════════ */
.contact-intro {
  font-size: 0.85rem;
  line-height: 1.7;
  color: var(--text-secondary);
  /* same measure as the about copy — this panel is read, not scanned */
  max-width: 54ch;
  margin-bottom: 14px;
  text-wrap: pretty;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.contact-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* The placeholder says what the box is for, but it leaves the moment you type in it. The label
   is what's left after that, and it's what a screen reader reads out either way. */
.contact-label {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.contact-input {
  width: 100%;
  font-family: var(--font-sans);
  font-size: 0.85rem;
  line-height: 1.6;
  color: var(--text-primary);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  /* drops the inner shadow iOS otherwise draws into every field */
  -webkit-appearance: none;
  appearance: none;
  transition:
    background 0.26s var(--transition-fluid),
    border-color 0.26s var(--transition-fluid),
    box-shadow 0.26s var(--transition-fluid);
}

.contact-input::placeholder {
  color: var(--text-muted);
  opacity: 0.75;
}

.contact-input:hover:not(:focus) {
  border-color: var(--border-medium);
}

/* Its own focus treatment rather than the page's ring: a 2px outline sitting 3px off a field
   this size swamps it, and the field can afford to say "active" in its own vocabulary. */
.contact-input:focus {
  outline: none;
  background: var(--bg-card-hover);
  border-color: var(--border-accent);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.045);
}

.contact-textarea {
  min-height: 94px;
  max-height: 280px;
  /* horizontal resizing would break out of the card; vertical is genuinely useful */
  resize: vertical;
}

/* Not a real field. It stays in the document because form-filling scripts read the document
   rather than the screen — anything that types in here has told on itself. */
.contact-hp {
  display: none;
}

.contact-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 1px;
}

.contact-count {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--text-muted);
  /* so the row doesn't twitch sideways as the digits change under the cursor */
  font-variant-numeric: tabular-nums;
  transition: color 0.26s var(--transition-fluid);
}

.contact-count.is-near {
  color: var(--text-secondary);
}

.contact-count.is-full {
  color: #e0a0a0;
}

.contact-send {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-primary);
  background: var(--bg-elevated);
  border: 1px solid var(--border-medium);
  border-radius: var(--radius-full);
  padding: 9px 18px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    background 0.26s var(--transition-fluid),
    border-color 0.26s var(--transition-fluid),
    color 0.26s var(--transition-fluid),
    opacity 0.26s var(--transition-fluid),
    transform 0.25s var(--transition-spring);
}

.contact-send:hover:not(:disabled) {
  background: var(--bg-card-hover);
  border-color: var(--border-accent);
  transform: translateY(-1px);
}

.contact-send:active:not(:disabled) {
  transform: scale(0.96);
}

.contact-send:disabled {
  cursor: default;
  opacity: 0.6;
}

.contact-send svg {
  width: 13px;
  height: 13px;
  stroke-width: 2;
  flex-shrink: 0;
  transition: transform 0.3s var(--transition-fluid);
}

/* The plane leans the way it points before you've even clicked. */
.contact-send:hover:not(:disabled) svg {
  transform: translate(2px, -1px);
}

/* Mid-send it actually leaves and comes back, which is a much clearer "working on it" than a
   spinner glued to a button this small. */
.contact-form.is-sending .contact-send svg {
  animation: contact-send-away 0.85s var(--transition-fluid) infinite;
}

@keyframes contact-send-away {

  0% {
    transform: translate(0, 0);
    opacity: 1;
  }

  55% {
    transform: translate(14px, -6px);
    opacity: 0;
  }

  56% {
    transform: translate(-10px, 5px);
    opacity: 0;
  }

  100% {
    transform: translate(0, 0);
    opacity: 1;
  }
}

/* Landed. The button keeps the colour just long enough to be believed, then script.js hands it
   back — see markContactSent. */
.contact-form.is-sent .contact-send {
  border-color: rgba(150, 205, 165, 0.4);
  color: #cfe8d5;
}

.contact-form.is-sent .contact-send svg {
  animation: contact-send-land 0.5s var(--transition-spring);
}

@keyframes contact-send-land {

  0% {
    transform: translate(-9px, 5px);
    opacity: 0;
  }

  100% {
    transform: translate(0, 0);
    opacity: 1;
  }
}

.contact-status {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--text-muted);
  /* holds the row open, so a line arriving can't shove the form up under the pointer */
  min-height: 1.1em;
  opacity: 0;
  transform: translateY(-3px);
  transition:
    opacity 0.3s var(--transition-fluid),
    transform 0.3s var(--transition-fluid),
    color 0.26s var(--transition-fluid);
}

.contact-status.is-shown {
  opacity: 1;
  transform: none;
}

.contact-status.is-ok {
  color: #a8d8b4;
}

.contact-status.is-warn {
  color: #e0c9a0;
}

.contact-status.is-error {
  color: #e0a0a0;
}


/* ═══════════════════════════════════════════════════
   FOCUS RINGS
   Keyboard users get a visible ring; mouse users never see it.
   ═══════════════════════════════════════════════════ */
:focus-visible {
  outline: 2px solid rgba(255, 255, 255, 0.55);
  outline-offset: 3px;
}

.nav-tab:focus-visible,
.mute-button:focus-visible {
  outline-offset: 2px;
}

/* The head fills its card, so an offset ring would be clipped by the card's own
   overflow: hidden — inset it instead. */
.blog-post-head:focus-visible {
  outline-offset: -3px;
}

.blog-pager-btn:focus-visible {
  outline-offset: 2px;
}

/* The fields already light up on focus, and they do it whether you arrived by tab or by click.
   Stacking the page ring on top of that reads as two different kinds of highlight at once. */
.contact-input:focus-visible {
  outline: none;
}

.contact-send:focus-visible {
  outline-offset: 2px;
}

/* The inner post scroller gets a narrower thumb than the page's — a 10px bar inside a
   320px box looks like a mistake. */
.blog-post-body.is-scrollable::-webkit-scrollbar {
  width: 6px;
}

.blog-post-body.is-scrollable::-webkit-scrollbar-thumb {
  border-width: 1px;
}

.social-card:focus-visible {
  background: var(--bg-card-hover);
  border-color: var(--border-medium);
  color: var(--text-primary);
}

/* a full-screen outline looks broken; brighten the prompt instead */
.entrance-overlay:focus-visible {
  outline: none;
}

.entrance-overlay:focus-visible .entrance-text {
  color: rgba(255, 255, 255, 0.95);
  text-shadow: 0 0 22px rgba(255, 255, 255, 0.22);
}


/* ═══════════════════════════════════════════════════
   NOSCRIPT
   ═══════════════════════════════════════════════════ */
.noscript-note {
  position: relative;
  z-index: 2;
  max-width: 640px;
  margin: 16px auto 0;
  padding: 12px 16px;
  border-radius: var(--radius-md);
  background: var(--bg-card-inner);
  border: 1px solid var(--border-subtle);
  color: var(--text-muted);
  font-size: 0.76rem;
  text-align: center;
}


/* ═══════════════════════════════════════════════════
   REDUCED MOTION
   The page leans hard on transitions and a looping video; honour the OS setting.
   ═══════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  #bg-video {
    display: none;
  }

  /* belt and braces — initTilt() already bails out, but this kills the perspective too */
  .main-wrapper {
    transform: none;
  }

  .skeleton {
    animation: none;
    background: var(--bg-secondary);
  }
}